Kestrel_VLLM_CUDA - 04 · 分组量化 GEMM 内核

简介: 本文介绍一种高效分组量化GEMM内核,统一支持decode(M=1)与prefill(M=32)场景:采用“每线程固定4列、沿K维扫描”映射,实现权重warp级满事务读取、激活广播;通过group轴z-split补足小M并行度,单次launch完成;结合pinned内存与单同步传输优化,显著降低小投影开销。

04 · 分组量化 GEMM 内核

一句话:一个内核同时服务 decode(M=1)与 prefill(M=32),用「每线程固定几列、沿 K 扫」的映射让权重按 warp 满事务读、激活广播;用 z-split 把小 M 的欠填充网格补满,且只发一次 launch。
前置:建议先读 第 03 篇 · 权重条带缓存与融合权重。
环境:x86-64 + NVIDIA RTX 5090(sm_120)· 阶段一 2B 稠密(Qwen3-VL-2B q8)。

一、问题与结论

问题 做法 判定
一个内核怎么同时吃 decode 与 prefill? 同一映射,M 走行、n 走列;decode 时靠 z-split 补并行度 实测可行
权重与激活各怎么访存? 权重按 warp 满事务读、激活一次读全 warp 广播 实测(权重密集、激活广播)
小 M 网格欠填充怎么办? 沿 group 轴切 z,一次 launch 完成所有 slice 实测(避免 launch 串行)
小投影该不该 offload? FLOP 门槛 1e7 自动把太小的留在 CPU 实测(见下节反直觉表)

二、背景

要算的东西是逐块点积再乘 scale:

out[m][n] = Σ_g  a_scale[m][g] · b_scale[n][g] · ( Σ_{k ∈ block g} Aq[m][k] · Wq[n][k] )
  • G:分组宽度(Q8_0/Q4_0 是 32)。
  • prec:0 = int8 权重码 [-127,127](Q8_0),1 = int4 [-8,7](Q4_0 nibble 已解包)。
  • 整数点积必须整组完成再乘 scale——这是精确量的划分,不能被拆开。

头文件的 contract 与此一致(include/npu/cuda/vllm_cuda.h):布局与 RK3588 NPU 完全相同,所以同一份 strip-cache artifact 喂两个后端;而内核是「对 K 的普通归约」,天然支持 M==1——这正是它相对 NPU cube(要求 M==0 mod 4、无法服务 decode)的结构性收益。

三、核心机制

3.1 两代内核:int8 激活 → float 激活

内核 激活输入 用在哪
k_gw_gemm int8 Aq + float asc 早期 prefill offload
k_gw_gemm_f32act float Ax full-decode + 现行 prefill

两者的分块与「沿 K 扫」的结构一致,只有两点不同:一是激活的处理(int8 码 + scale vs float),二是 f32 版本在 M == 1(decode)时把整块线程全映射到 n 方向,避免 m 方向线程空转。第 11 篇会讲为什么最终选了 float 激活(PPL Δ3.0% → 0.19%)。

3.2 线程映射

/* 文件:src/npu/cuda/vllm_cuda_kernels.cu(节选) */
#define VC_NT   64   /* threads along n per block */
#define VC_MT   4    /* threads along m per block */
#define VC_VEC  4    /* output columns per thread */
/* 文件:src/npu/cuda/vllm_cuda_kernels.cu(k_gw_gemm / k_gw_gemm_f32act,节选) */
/* 通用形状(prefill / M>1):*/
const int n0 = (blockIdx.x * VC_NT + threadIdx.x) * VC_VEC;  /* 输出列 */
const int m  =  blockIdx.y * VC_MT + threadIdx.y;            /* 输出行 */

/* f32 版本在 M==1(decode)时把线程全压到 n:*/
if (M == 1) {
   
    n0 = (blockIdx.x * (VC_NT * VC_MT) + threadIdx.y * VC_NT + threadIdx.x) * VC_VEC;
    m  = 0;
}

每个线程负责一行 m 上连续的 4 个输出列 [n0, n0+4)。内层沿 K 扫:

/* 文件:src/npu/cuda/vllm_cuda_kernels.cu(k_gw_gemm_f32act 的 float 激活路径,节选) */
for (int g = g_begin; g < g_end; ++g) {
   
    const int8_t *wt = WT  + (size_t)g * G * Npad + n0;
    const float  *xb = arow + (size_t)g * G;
    float p0 = 0.f, p1 = 0.f, p2 = 0.f, p3 = 0.f;
    for (int kk = 0; kk < G; ++kk) {
   
        const float xv = xb[kk];              /* 整 warp 广播同一地址 */
        const int8_t *w = wt + (size_t)kk * Npad;
        p0 += (float)w[0] * xv;
        p1 += (float)w[1] * xv;
        p2 += (float)w[2] * xv;
        p3 += (float)w[3] * xv;
    }
    const float4 bs = *reinterpret_cast<const float4 *>(bsT + (size_t)g * Npad + n0);
    acc0 += p0 * bs.x;  acc1 += p1 * bs.y;
    acc2 += p2 * bs.z;  acc3 += p3 * bs.w;
}

(int8 激活版本 k_gw_gemm 的内层取 uchar4 权重码、以 int 累加,最后再乘 a_scale 与 b_scale。)

两个访存特征值得点出:

  • 权重:VC_NT=64 线程 × VC_VEC=4 字节 = warp 一次读 128 字节连续数据——满事务。
  • 激活:xb[kk] 对同一行 m 的所有线程是同一个地址——一次读、全 warp 广播。

这是「权重密集、激活广播」的取舍,因为 decode 时瓶颈在权重带宽。

3.3 z-split:小 M 时怎么喂饱设备

decode 时 M=1,nblk_y = 1,nblk_x 也不大——(x,y) 网格严重欠填充。解法是沿 group 轴再切一刀,用 blockIdx.z:

/* 文件:src/npu/cuda/vllm_cuda_kernels.cu(k_gw_gemm,节选) */
const int z = blockIdx.z;
const int base = ngroups / nsplit;
const int rem  = ngroups % nsplit;
const int g_begin = z * base + (z < rem ? z : rem);
const int g_end   = g_begin + base + (z < rem ? 1 : 0);

要点:

  1. 每个 slice 拥有整数个 scale block,所以整数部分和仍是精确的——这是纯划分。
  2. 一次 launch 完成所有 slice,不是循环 launch。decode 形状下 nsplit 可达 64,逐个 launch 的开销会盖过整个 GEMM。
  3. 结果落在 dPart[z],最后用 k_gw_reduce 按固定顺序求和——固定顺序是确定性的前提。
/* 文件:src/npu/cuda/vllm_cuda_kernels.cu(k_gw_reduce,节选) */
__global__ void k_gw_reduce(const float *__restrict part,
                            float *__restrict dst,
                            size_t total, int nsplit) {
   
    const size_t i = (size_t)blockIdx.x * blockDim.x + threadIdx.x;
    if (i >= total) return;
    float s = 0.f;
    for (int z = 0; z < nsplit; ++z) s += part[(size_t)z * total + i];
    dst[i] = s;
}

nsplit 的选择:

/* 文件:src/npu/cuda/vllm_cuda_kernels.cu(vcuda_dev_gemm_gw,节选) */
if (nblk_xy > 0 && nblk_xy < VC_SPLIT_TARGET && ngroups > 1) {
   
    nsplit = (VC_SPLIT_TARGET + nblk_xy - 1) / nblk_xy;   /* 目标 512 个 block */
    if (nsplit > ngroups) nsplit = ngroups;
}
while (nsplit > 1 && (size_t)nsplit * ou_sz > VC_PART_CAP_BYTES) nsplit--;   /* part 缓冲上限 64 MiB */

mermaid diagram

图 1:解码/预填充共用映射下的数据流。blockIdx.x/y 摊出「列 × 行」网格(VC_NT=64 × VC_MT=4),每个线程盯住 [n0, n0+4) 那 4 个输出列、沿 K 扫过本 slice 的 group 区间;两个访存特征正来自这套映射——权重每 warp 满事务读 128 B,激活 xb[kk] 对同列线程是同一地址、一次读全 warp 广播。decode M=1 时 (x,y) 网格欠填充,改由 blockIdx.z 沿 group 轴切成 nsplit 片(目标 VC_SPLIT_TARGET=512 个 block,nsplit 受 64 MiB 的 VC_PART_CAP_BYTES 上限约束),各片写 dPart[z],最后由 k_gw_reduce 按固定顺序求和保确定性。

3.4 调用路径上的传输优化

设备层 vcuda_dev_gemm_gw() 的传输形态是实测调出来的:

  • H2D 走 pinned 暂存(pageable 源会串行化);
  • 全部 cudaMemcpyAsync 发在默认流,只在 D2H 后做一次 cudaDeviceSynchronize。
/* 文件:src/npu/cuda/vllm_cuda_kernels.cu(vcuda_dev_gemm_gw,节选) */
memcpy(s->hAq, Aq, aq_sz);
memcpy(s->hAsc, asc, as_sz);
if (cudaMemcpyAsync(s->dAq, s->hAq, aq_sz, cudaMemcpyHostToDevice) != cudaSuccess ||
    cudaMemcpyAsync(s->dAsc, s->hAsc, as_sz, cudaMemcpyHostToDevice) != cudaSuccess) {
    ... }
...
/* One async D2H into pinned staging + THE single synchronize of the call. */
cudaMemcpyAsync(s->hOut, s->dOut, ou_sz, cudaMemcpyDeviceToHost);
cudaDeviceSynchronize();

旧形态(阻塞 D2H + 额外一次 cudaDeviceSynchronize)多付了一整个往返。profile 显示:对小投影,传输/同步这对开销,而不是内核本身,才是主导。

3.5 小投影为什么单独 offload 是亏的

2B q8 的 decode 桶实测(ms/token):

投影 单投影 offload 前 单投影 offload 后 结论
QKV / O / K / V 6.39 / 3.70 / … 7.24 / 3.73 / … 变慢
gate/up 16.96 7.56 大幅变快
down 10.79 3.98 大幅变快
lm_head 6.87 — 变快(最大矩阵)

原因是每次调用的固定往返 ~100 µs 超过小投影的计算量。所以:

  • QKV/K/V 单独 offload 是亏的 → 必须融合(QKV 三合一、gate+up 二合一),把三次往返压成一次;
  • 用 FLOP 门槛(1e7)把「太小不值得」的投影自动留在 CPU:M=1 时 Q/O 是 8.4 MFLOP、K/V 4.2 MFLOP(排除),gate/up/down 25 MFLOP 与 lm_head 622 MFLOP(通过)。

这条「小算子别 offload、融合才划算」的结论,直接决定了 第 05 篇 里卸载集合的结构。

四、实测数据

口径 数值 判定 标注
QKV 单投影 offload(2B q8 decode) 6.39 → 7.24 ms/tok 变慢 实测
gate/up 16.96 → 7.56 ms/tok 大幅变快 实测
down 10.79 → 3.98 ms/tok 大幅变快 实测
lm_head 6.87 → … 变快 实测
每次调用固定往返 ~100 µs 小投影的主导开销 实测
M=1 各投影 FLOPs Q/O 8.4 M / K/V 4.2 M(排除);gate/up/down 25 M、lm_head 622 M(通过) 门槛 1e7 干净切开 实测

五、边界与已知限制

  • 这里的实测是 2B q8 的口径;小投影「负收益」的临界由固定往返(~100 µs)决定,换设备/换后端不能照搬这条门槛,需要按自己的往返成本重测。
  • prec 只支持 0/1(int8/int4 码);K % G != 0 一律退回 CPU。
  • z-split 的 part 缓冲有 64 MiB 上限,超限时自动减少 nsplit(并行度换显存)。
  • 内核的数值确定性依赖 k_gw_reduce 的固定求和顺序,以及块内整数点积整组完成;改变划分方式会改变位级结果。

CPU 对照(迁移前基线)

  • CPU 参考:kestrel-llm/src/model/vllm_safetensors.c(函数 st_q4_row_dot / q4_dense4_x86;dyn_matvec_q8)—— CPU 侧按行(q4 走 4 行组)逐块反量化点积。
  • 迁移要点:CPU 侧逐行/逐 4 行组算 → CUDA 侧用一个映射同时吃 decode(M=1) 与 prefill(M=32):每线程固定 4 列沿 K 扫,权重满事务读、激活广播;小 M 用 blockIdx.z 切 group(z-split 一次 launch);固定顺序 reduce 保确定性。
  • 真机验证:命中 E2(selftest:int8/int4 × decode/prefill/非整除 N 全过)。

六、小结(可复用结论)

  1. 一个映射吃两种负载:每线程固定几列、沿 K 扫,让权重满事务读、激活广播——这是 decode 权重带宽受限下的正确取舍。
  2. 小 M 靠 z-split 补并行度,且必须一次 launch:decode 形状下 nsplit 可达 64,逐个 launch 会把 GEMM 本身盖过去。
  3. 精确量要整组完成:整数点积在块内做完再乘 scale,才能做纯划分、用固定顺序 reduce 保确定性。
  4. 传输/同步才是小投影的主导开销:pinned 暂存 + 单次同步是实测调出来的,不是默认写法。
  5. 小算子不该单独 offload:FLOP 门槛加融合,才是把「快」变成「更快」的正确方向。

相关篇目:第 03 篇 · 权重条带缓存与融合权重、第 05 篇 · 卸载集合
源码与配套资源:本仓库 https://gitee.com/pei-xiaoguang/kestrel-llm-cuda.git;
CPU 推理源码 https://gitee.com/pei-xiaoguang/kestrel-llm

相关文章
|
22天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
9215 29
|
21天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
4527 16
|
8天前
|
存储 人工智能 并行计算
大模型本地部署终端选型方法论:以 Qwen3.8-27B 为例的四档分层完整流程
本文提出一套大模型本地部署终端选型方法论:定约束、定档位、定框架、定参数四步决策法,配合入门、主力、质量、无损四档分层模型。以 Qwen3.8-27B 实测数据为例,逐环节解读显存、带宽、存储、散热、系统、预算等要素,给出面向不同预算的优选方案、决策自查清单与市场观察框架。文末前瞻 AI 笔记本的 CPU+GPU 与统一内存两条路线,论证四步决策法在新品类上的延续性。
|
20天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
2401 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
7天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型深度拆解:原理解读、实战测评与保姆级落地教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为**System One Model(系统一模型)**,对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
567 1
|
2天前
|
存储 缓存 测试技术
QWT 3D 模块重构记:旧渲染管线改进介绍及实时瀑布图示例
QWT 3D模块完成重大重构:解耦窗口与图形(Plot+Item),摒弃OpenGL 1.x固定管线,迁至Core Profile着色器渲染;支持多图元同场景叠加,并以实时3D瀑布图验证——滑动窗口、固定色域、64帧×256点达61fps。兼容Qt 5.12+/6,LGPL协议。
151 120
|
9天前
|
人工智能 Linux Windows
千问办公(QwenWork)官网入口:其实有2个,一个是网页端千问办公,一个是介绍指南页面
千问办公(QwenWork)是阿里云推出的AI智能办公平台,支持网页端直接使用及Windows/Mac/Linux客户端下载。提供PPT生成、财报分析、网页搭建等AI功能,个人版免费,企业版198元/席/月。详情见官网qwenwork.cn或阿里云产品页。
1126 0
千问办公(QwenWork)官网入口:其实有2个,一个是网页端千问办公,一个是介绍指南页面
|
14天前
|
Windows
【保姆级】.NET Framework 3.5离线安装教程(2026最新实测)
.NET Framework 3.5是微软经典运行库,专为兼容老软件、老游戏设计。Win7自带,Win10/11需手动启用(在线或离线安装)。它与4.x系列并存不冲突,缺它会导致“找不到组件”报错。本文详解两种安装方式及常见问题解决,小白也能轻松上手。(239字)
1667 0
|
3天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型全解析:核心原理拆解,性能实战测评与落地部署教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为System One Model(系统一模型),对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
341 0

热门文章

最新文章