4
DCU技术达人
新人贴
为什么大模型 Decode 会让 DCU“算力闲置”:从 Prefill、GEMV 到 HBM 带宽的实测解释
2026-10-19 21:34:50
43
> **适用环境**:海光 DCU gfx936、DTK/ROCm/HIP、BF16 大模型推理。 > **本文回答**:为什么监控中 DCU 一直有 kernel 在运行,生成 token 仍然不够快;为什么 Prefill 和 Decode 的优化方向不同;以及怎样用一组简单实测判断双缓冲、调高并行度是否值得做。 > **结论先行**:在单请求、单 token Decode 中,Qwen3.5-27B 的大量线性层会退化为 GEMV,时间主要消耗在从 HBM 流式读取权重,而不是 BF16 矩阵单元的乘加。若实测带宽已接近峰值,继续增加双缓冲或并行 wave 通常无效;应转而减少额外访存、优化实际 shape 的 kernel,或承认已经接近硬件边界。 --- ## 1. 一个常见误判:DCU 忙,不代表算力用满 在海光 DCU 上部署大模型时,常能看到 `hy-smi` 显示设备并不空闲,profiler 时间线也几乎没有空洞。直觉上这很容易被理解为“计算资源已经吃满,没什么可优化的了”。 这两个观察只能说明 kernel 在持续执行,不能说明矩阵计算单元在高效做乘加。GPU/DCU 执行一个访存受限 kernel 时,大量 wave 仍处于活跃状态:它们发起 HBM load、等待数据返回、再被调度器切换出去。时间线上 kernel 是连续的,但计算单元可能绝大部分时间都在等数据。 判断是否还有优化空间,关键不是“设备忙不忙”,而是下面两个问题: 1. 这个 kernel 每搬运 1 byte 数据做了多少浮点运算? 2. 实测带宽或算力距离当前测试环境的硬件上限还有多远? 前者是**算术强度**,后者决定应该优化计算、访存,还是停止在错误方向上继续投入。 --- ## 2. Decode 和 Prefill 不是同一种负载 大模型的自回归推理可粗略分为两个阶段。 | 阶段 | 一次处理多少 token | 典型矩阵形状 | 主要影响的用户指标 | 常见优化方向 | | --- | ---: | --- | --- | --- | | Prefill | 一段 prompt 中的多个 token | 较大的 GEMM | TTFT(首 token 时延) | Matrix Core、GEMM tile、FlashAttention、批处理 | | Decode | 每步 1 个新 token | `N=1` 的 GEMV/skinny GEMM | TPOT(每输出 token 时延) | HBM 有效带宽、细长 GEMV、减少中间张量访问、kernel launch | 以一层线性投影为例,Prefill 计算多个 token 时,一份权重可被多个输入向量复用;矩阵足够大,运算量相对权重读取量更高。Decode 则不同:每一步只来了一个新 token,但仍要读取这一层的大部分权重。矩阵乘法在形状上退化为矩阵向量乘法: ```text Prefill: [S, K] x [K, M] -> [S, M] S 通常较大 Decode: [1, K] x [K, M] -> [1, M] S = 1 ``` 这不是“算得少所以一定快”。对 Decode 而言,乘加减少了,但权重读取量并没有按同样比例减少。于是 HBM 中的权重流成为主要成本。 --- ## 3. 用单张海光 DCU `gfx936`的 Roofline 给直觉一个量纲 性能上界可以用 Roofline 的简化形式表达: $$ P_{\text{achievable}} = \min(P_{\text{peak}},\ B_{\text{HBM}} \times I) $$ 其中: - $P_{\text{peak}}$ 是设备可达到的计算峰值; - $B_{\text{HBM}}$ 是显存带宽; - $I$ 是算术强度,即 FLOPs/Byte。 在单张海光 DCU `gfx936`的环境中,通过单卡微基准得到的参考值如下: | 指标 | 微基准结果 | 用途 | | --- | ---: | --- | | HBM 可达带宽 | `1206 GB/s` | 判断流式读权重的带宽利用率 | | BF16 可达算力 | 约 `395 TFLOP/s` | 判断大 GEMM 是否接近算力上限 | | Roofline 拐点 | 约 `327 FLOP/byte` | 低于该值时更可能受 HBM 限制 | 这些数字的作用是建立这套环境的参照系:一个 kernel 如果已经接近 `1206 GB/s`,就不应再把“提高计算吞吐”作为首要假设。 用字符图表示 Roofline: ```text 性能 ^ 计算上限 P_peak | ---------------------- | / | / 带宽上限: B_HBM x I | / +----------------+----------------------------> 算术强度 I 约 327 FLOP/byte ``` 可以直接理解为“**最终速度取两道上限中较低的那一道**”:一条上限由 HBM 每秒能搬多少字节决定,另一条由计算单元每秒能做多少 FLOPs 决定。 例如,假设某个 kernel 每读取 1 byte 只完成 10 FLOP。即使 HBM 已满速,按 `1206 GB/s × 10 FLOP/byte` 也只能提供约 `12.06 TFLOP/s` 的计算数据,远低于 `395 TFLOP/s` 的 BF16 计算能力;此时加大计算 tile 或提高 Matrix Core 利用率并不能让数据更快到达。反过来,若每读取 1 byte 可完成 500 FLOP,则带宽侧允许的计算速率约为 `603 TFLOP/s`,高于 `395 TFLOP/s`,这时才是计算能力先成为上限。 因此,这类 kernel 的速度首先由搬数据决定,应优先减少无效读写或提高有效带宽;高于这个值时,才更值得检查 Matrix Core 利用率、GEMM tile 和计算指令效率。 --- ## 4. 把这个判断代入 Qwen3.5-27B 的 Decode 下面用 Qwen3.5-27B 做一个数量级估算。BF16 的 27B 参数约为 54 GB。单 token Decode 会依次经过大量线性层,因此可以把“读取主要权重”近似看成一次约 54 GB 的 HBM 数据流。它不是某一个 kernel 的耗时,也不是完整服务的 TPOT,只是为了和纯乘加成本作对照。 先问第一个问题:**如果 HBM 持续以 1206 GB/s 读取这 54 GB,光搬完这些字节至少需要多久?** $$ T_{\text{weight IO}} \approx \frac{54\ \text{GB}}{1.206\ \text{TB/s}} \approx 45\ \text{ms/token} $$ 然后问第二个问题:**假设所有数据都已经在计算单元旁边,只计算这一个 token 的乘加,理论上要多久?** 单 token 前向约有 $2 \times 27\text{B}=54\text{ GFLOP}$ 的乘加量;若按 395 TFLOP/s 计算,纯计算下界为: $$ T_{\text{compute}} \approx \frac{54\ \text{GFLOP}}{395\ \text{TFLOP/s}} \approx 0.14\ \text{ms/token} $$ 45 ms 与 0.14 ms 的巨大差距说明:对单 token Decode,**把权重从 HBM 搬到计算单元**比完成乘加更可能是主成本。 真实 TPOT 还包括 KV-cache attention、norm、采样、kernel launch、同步和框架开销,且权重访问不一定始终达到峰值带宽。因此这两个数只能看作理论下界,不能直接作为服务性能指标;它们的作用是避免一开始就把问题误判成“BF16 算力不够”。 长上下文还会引入另一条 HBM 数据流:KV-cache。权重读取量在每个 Decode step 中基本固定,而 attention 读取的 K/V 长度随上下文增长。因此不能只把 Decode 简化成“读权重”,而应分开观察: ```text 单 token Decode ├── 线性层: 读取固定规模的模型权重,常见为 skinny GEMV ├── Attention: 读取随上下文增长的 KV-cache └── 其他: norm、rope、采样、调度和 kernel launch ``` 这也解释了为什么 4K、16K、32K 上下文不应盲目使用同一个优化参数。 --- ## 5. DCU 上的一个反例:双缓冲并不自动带来提速 双缓冲、软件流水和更高 occupancy 常被当作优化访存 kernel 的通用答案,但它们解决的是**访存延迟没有被隐藏**的问题,而不是 HBM 总带宽已经饱和的问题。 在 gfx936 上,对 Qwen3.5 类 GEMV 形状的对照微测中,无双缓冲版本已经达到 HBM 峰值的 `92%--101%`;加入双缓冲后,多个形状的差异在 `±1.5%` 内。这个结果不是说双缓冲永远无用,而是说明在该 kernel、该 shape 和该硬件状态下,瓶颈已经是带宽管道本身。 为什么 wave64 的 DCU 在这里能自然隐藏大部分 load latency?原因在于资源和并发度: - gfx936 的 wavefront 为 64 线程; - 该 GEMV 示例中,VGPR 使用量约为 58、LDS 使用量约为 32,资源占用不高; - 一个 `34816` 输出通道的 gate/up 投影可以产生大量独立 wave。 当一个 wave 等待 HBM 数据时,硬件有足够多的其他常驻 wave 可以切换执行,load latency 已被 wave 调度隐藏。此时手写双缓冲做的是同一件事的重复劳动:它不能把 HBM 管道变宽。 可以用下面的判据快速筛选方向: | 观察到的现象 | 更可能的瓶颈 | 优先尝试 | | --- | --- | --- | | `MemUnitBusy` 高、带宽接近峰值、计算吞吐低 | 带宽受限 | 减少字节、减少额外读写、改善访问连续性 | | 带宽未满、memory stall 高、可驻留 wave 少 | 延迟/occupancy 受限 | 增加并行度、调整寄存器/LDS、预取或双缓冲 | | 计算吞吐高且接近 BF16 上限 | 算力受限 | GEMM tile、Matrix Core、算法或融合 | 在 ROCm/HIP 环境中,可以用 `rocprof` 或 `omniperf` 抓取 kernel 指标。重点不是记住某一个百分比阈值,而是用实测回答“HBM 是否已经被喂满”和“是否还有足够 wave 隐藏延迟”。 --- ## 6. 同样是“分块”,GEMV 和 attention 切的却不是同一个维度 前面提到的权重流和 KV-cache 流,最终都会落到“如何把工作分给 DCU 上的 block 和 wave”这个问题。这里容易产生一个误解:既然 GEMV 有 tile,attention 是否也应复用同一套 tile?答案是否定的,因为两者的张量形状、数据生命周期和跨块依赖不同。 ### 6.1 先定义 shape:它是一次计算中张量各维度的大小 以线性层 `Y = XW^T` 为例,常用 `m`、`k`、`n` 描述计算: ```text X: [n, k] 输入 token 数 x 输入特征维度 W: [m, k] 输出特征维度 x 输入特征维度 Y: [n, m] 输出 token 数 x 输出特征维度 ``` 因此,Qwen3.5 gate/up 的 Decode `shape` 为 `m=34816,k=5120,n=1`:一个长度为 5120 的输入向量,得到 34816 个输出值。严格说,shape 只指维度大小;在 kernel dispatch 中,BF16 dtype、是否有 bias、内存布局等也必须一并检查,才能保证走到正确路径。 attention 的逻辑 shape 则不同。对一个 Q head,当前 query 是 `Q:[1,D]`,历史 key/value 是 `K:[L,D]` 与 `V:[L,D]`,其中 `L` 是上下文长度: ```text scores: [1, L] = Q @ K^T prob: [1, L] = softmax(scores) out: [1, D] = prob @ V ``` 这里的 K/V 逻辑上同样是矩阵,并不是普通“一维数组”。为了让多请求、不同长度的序列能增长、回收和复用,vLLM 会将它们物理保存为分页的 KV-cache block,例如 `[block, token_in_block, kv_head, head_dim]`。权重则是模型加载后不再变化的稠密二维张量。两者存储组织不同,根因是一个是静态参数,一个是按请求逐 token 增长的运行时状态。 ### 6.2 GEMV 的 tile:切权重矩阵的输出维 对 `Y:[1,M] = X:[1,K] @ W^T:[K,M]`,一个输出通道 `Y[j]` 都是对权重 `W[j,:]` 和输入 `X` 的独立点积。GEMV 的 tile/行块通常按 `M` 维切分: ```text W 的输出行: [ row 0 ... row M-1 ] |--- block 0 ---|--- block 1 ---| ... 每个 block: 负责若干个独立的 Y[j],每个 Y[j] 都要遍历完整 K 维 ``` 这类切分的目的不是增加总 FLOP。`M x K` 的点积数量没有改变;它是在静态权重矩阵上安排 wave 的工作和读权重方式,争取让 HBM 流式读取、寄存器归约和 block 数量更平衡,从而以更短时间完成相同的 GEMV。 Qwen3.5 的 `m=34816,k=5120,n=1` 是一个真实例子。LLMM1 扫描 `rows_per_block=2/4/8/16` 时,耗时为 `0.402/0.363/0.350/0.827 ms`:8 行最优,16 行反而变慢。它说明 tile 不是越大越好,必须对真实 shape 测量。实际服务只让这一精确 shape 走 8 行路径,qkv、o 等其他 shape 回退原始 `F.linear`。 ### 6.3 attention 的 segment:切 KV-cache 的序列维 attention 不能按 GEMV 的方式只切输出通道,因为它最重的工作沿着历史序列长度 `L` 展开。16 段或 32 段指的是把 `K:[L,D]` 和 `V:[L,D]` 按 token 位置切成 16 或 32 个连续区间: ```text 16 段: KV token [0 ................................ L) |---- segment 0 ----| ... |---- segment 15 ----| 32 段: KV token [0 ................................ L) |- seg 0 -| ... |---------- seg 31 ----------| ``` 每一段都可以独立完成本段的 `Q @ K^T`、局部最大值、局部 softmax 分母和局部 `prob @ V`。但各段不能直接把输出相加:softmax 的分母必须覆盖全部 `L` 个位置。因此 kernel 还需要根据各段的局部最大值和局部和,做一次数值稳定的全局合并。这就是这里的 **softmax reduction**。 同样,分段不改变总 FLOP:QK 点积和加权 V 的运算量没有少。它的目的是在长序列中暴露更多独立的 KV 读取和局部 reduction 工作,让 DCU 有足够并行任务;代价是需要更多中间缓冲和一次更大的最终合并。 | 对比项 | skinny GEMV tile | attention segment | | --- | --- | --- | | 主要数据 | 静态权重 `W:[M,K]` | 动态 KV-cache `K/V:[L,D]` | | 切分维度 | 输出通道 `M` | 历史 token 序列长度 `L` | | 子任务关系 | 各输出 `Y[j]` 基本独立 | 各段需通过全局 softmax 统计量合并 | | 优化目标 | 更高效地完成权重流式读取和点积归约 | 给长 KV 序列提供足够的并行读取与 reduction | | 不变的量 | GEMV 总 FLOP 不变 | QK 和 PV 总 FLOP 不变 | 这也把两类优化连接起来:它们不是“一个提高 FLOP、一个提高 QK 匹配速度”,而是面对两条不同的数据流,把**相同计算**分解成更适合 DCU 并行与访存特性的工作单元。 --- ## 7. 将分段数量与上下文长度匹配:attention 的实测例子 第 6 节说明了为什么 attention 应沿序列维分段。本节继续回答“应该分多少段”。分段太少时,16K/32K 的单段工作过长,并行度不足;分段太多时,局部统计量和最终合并的开销反而成为负担。 在 Qwen3.5 的真实 attention 头形状(16 个 Q heads、4 个 KV heads、head dim 256)上,BF16、page size 784 的微测结果如下: | 上下文长度 | 16 段 parallel softmax | 32 段 parallel softmax | 结论 | | ---: | ---: | ---: | --- | | 8K | `0.397 ms` | `0.404 ms` | 序列较短,32 段的合并成本超过并行收益 | | 16K | `0.480 ms` | `0.432 ms` | 32 段子核约快 `10%` | | 32K | `0.730 ms` | `0.573 ms` | 32 段子核约快 `21%` | 因此,合理实现不是“永远用 32 段”,而是只在单 token Decode、上述 Qwen3.5 头形状、且最大序列长度不低于 12,288 时选择 32 段;Prefill、短上下文与其他模型保留原有 16 段路径。与 16 段对照时,32 段输出最大绝对误差不超过 `2.44e-4`、平均绝对误差不超过 `1.58e-5`,均为有限值;差异来自浮点归并顺序变化。 这部分数据是 attention **子核**测量,不能直接相加到端到端吞吐。真实服务还要经过数值回归、health check、短请求 smoke,以及统一 benchmark 的 TPOT/TTFT 验证。 --- ## 8. 一套适合 DCU Decode 的最小诊断流程 面对一个“生成慢”的服务,可以按以下顺序排查,避免一开始就大改框架: ```text 1. 固定真实模型、上下文长度、batch 和输出长度,抓取服务级 TPOT/TTFT。 2. 用 profiler 按线性层、attention、其他 kernel 聚合耗时。 3. 对热点 kernel 测字节量/耗时,计算有效带宽;同时查看 MemUnitBusy、计算吞吐、VGPR/LDS。 4. 若带宽接近当前测试环境的上限: - 不再优先投入双缓冲或单纯增大并行度; - 检查是否有多余中间张量读写、是否存在可验证的真实 shape 特化。 5. 若带宽未满且 wave 不足: - 再评估 tile、LDS、VGPR、预取或双缓冲。 6. 每个改动分别完成:真实 shape 微测 -> 数值对照 -> 服务 smoke -> 同口径端到端测试。 ``` 这里最重要的是第 6 步。微基准帮助解释硬件机制,但只有服务级验证才能确认改动没有被框架开销抵消,也没有破坏请求完成率或时延尾部。 --- ## 9. 总结 在单请求大模型 Decode 中,DCU 的挑战往往不是“如何让更多计算单元同时做乘加”,而是“如何让 HBM 中的权重流和 KV-cache 流少走冤枉路”。 对 gfx936 而言,wave64、VGPR/LDS、可达 HBM 带宽和真实模型 shape 共同决定一个 kernel 是否值得改。实测已经接近 HBM 上限时,双缓冲和更高并行度未必有效;针对 Qwen3.5 的精确 skinny GEMV shape 扫描行块,或根据上下文长度调整 attention reduction,才是更可证伪、也更稳健的优化路径。 一句话概括本文的方法:**先用 DCU 指标确认自己撞的是哪堵墙,再为真实 shape 写受保护的优化路径,最后用数值与服务级测试决定是否保留。** --- ## 参考与说明 - [vLLM](https://github.com/vllm-project/vllm):本文服务框架及部分执行路径参考。 - [ROCm Documentation](https://rocm.docs.amd.com/):HIP、rocprof 与性能分析工具资料。 - 本文实验使用前文列出的单张 gfx936 环境;型号、驱动、DTK/ROCm 版本、时钟状态和测试 shape 均会影响结果,请以自己的设备实测为准。
全部评论(0)
只看楼主
暂无数据
--
楼主
Level.4
0 帖子
楼主热帖
暂无数据