海光 DCU 上 Qwen3.5 推理性能工程:从 Prefill/Decode 热点到端到端收益
海光 DCU 上 Qwen3.5 推理性能工程从 Prefill/Decode 热点到端到端收益摘要在大模型推理优化中最容易被误读的一件事是把某个 kernel 的局部加速比直接当成服务吞吐的提升。一个 kernel 即使在独立测试中快了 1.08 倍完整服务也可能只快几个百分点甚至几乎没有变化。本文以海光 DCU gfx936 路径上的 Qwen3.5-27B BF16 推理为例使用本地已经完成的 profile、kernel microbench 和同容器端点 A/B 数据回答三个问题Prefill 和 Decode 分别被什么算子限制为什么 BF16 的 M1 GEMM/GEMV 会成为 Decode 的主要性能墙一个局部优化要经过哪些验证才能判断它确实改善了完整推理服务。本文只讨论 DCU 推理性能工程。文中所有性能数字均来自本地固定环境的已有测量记录独立 kernel 数据、profile 数据和端到端服务数据分别标注不把它们混成一个结论。1. 先把推理拆成 Prefill 和 Decode一次生成请求通常可以分成两个阶段Prefill一次处理较长的输入把提示词转换成初始状态Decode之后逐 token 生成每一步通常只处理一个新 token同时读取此前保存的状态。这两个阶段看起来属于同一个模型硬件行为却很不一样。Prefill 的 query 维度较大矩阵乘法更接近大 GEMMAttention 也能够在较大的 token 块上并行。Decode 的 query 维度通常是 1很多矩阵运算退化成 M1 的 GEMM 或 GEMV计算量不大但每一步都要读取很大的权重并且要承担大量 kernel launch 和运行时调度开销。可以用一个简化的时间模型表示完整请求request time prefill GEMM prefill attention other prefill linear layers decode linear layers decode state access sampling and runtime overhead因此“Attention 很快”或者“某个 GEMM 很快”都只说明了总时间中的一个部分。真正值得关注的是这个部分在目标 workload 中占多大比例以及它是否会在每个生成 token 中反复出现。2. 实验环境与测量协议本文引用的本地测量使用以下环境和口径项目配置加速卡海光 DCUgfx936-class 路径模型Qwen3.5-27B权重计算类型BF16 原始权重PyTorch2.10.0推理框架vLLM 本地运行环境Standalone warmup10 次Standalone measured iterations40 次端点测试固定请求集、并发 1、同一容器 A/B主要指标output throughput、mean/p99 TPOT、TTFT、输出文本、失败数测量分成三层Profile回答时间消耗在哪里Standalone/CUDAGraph microbench回答目标 kernel 的局部上限端点 A/B回答完整服务实际得到多少收益。这三层不能互相替代。Profile 适合定位问题不适合直接当吞吐成绩microbench 适合筛选实现不适合直接代表服务端点 A/B 才能验证全链路效果。本文中的 profile、RPB2 端点和 Tail-M 反例来自不同的历史实验对象它们用于解释不同层次的性能现象不是同一个源码组合的叠加结果文中的提升数字不能相加。3. 一次长上下文 profile 看到了什么本地 profile 记录了一个 page832、prefix caching 关闭、all-Tail-Q vendor attention 的官方 8K prompt 路径请求的阶段耗时q4096 prefill total: 2720.145 ms q1685 prefill total: 464.801 ms decode total: 2586.460 ms decode average: 43.108 ms/token3.1 PrefillAttention 之后大 GEMM 成为主要时间桶在 q4096 的 Prefill 中主要时间项如下时间项耗时Attention238.718 msaten::mm1840.596 msaten::mm 占 q4096 Prefill67.67%这组数据给出了一个很直接的判断当 Attention 已经被压低之后继续在 Attention 的边界参数上做小范围调整收益会受到总时间占比限制。此时更值得研究的是大 GEMM 的数据访问、tile 组织和实际调用形状。这里的 aten::mm 不是一个抽象的“模型耗时”而是一批真实矩阵乘法调用的汇总。要继续优化必须把它拆成具体的 M、N、K 形状、dtype、调用频率和是否被其他算子覆盖而不是只看总桶名称。3.2 DecodeLinear/GEMV 占据绝大多数时间同一份 profile 的 Decode 统计如下时间项每 token 耗时Linear 总计38.078 msLLMM123.470 msrocBLAS GEMV14.607 msLinear 占 Decode88.33%每个 Decode token 中观察到LLMM1: 176 launches/token rocBLAS GEMV: 129 launches/token129 次 GEMV 的来源可以按模块归纳为64 MLP down projection 48 GDN output projection 16 attention output projection 1 lm_head这说明 Decode 的优化重点和 Prefill 不同。Decode 不是单纯寻找一个“更快的 Attention”而是要处理大量小矩阵运算每一步都重复执行单次节省很小但乘上层数和生成 token 数后可能形成可观的端到端差异。4. 为什么 DCU 上的 BF16 M1 运算特别难当矩阵乘法的 M 维度为 1 时计算并行度相对有限而权重矩阵仍然很大。此时 kernel 往往更接近受权重读取和访存效率限制而不是受理论 FLOPS 限制。一个简化的 M1 计算可以写成y[j] sum_k x[k] * W[j, k]对每个输出行都要遍历 K 维权重。性能会受到以下因素共同影响权重加载是否连续、是否能够合并访问一个 block 处理多少输出行K 维归约时 wave 内线程如何协作BF16 读取和 FP32 累加之间的转换开销输出写回是否造成额外同步kernel launch 和图 replay 是否已经成为主要成本。因此M1 kernel 的优化通常不是简单地把线程数调大。线程过多会增加归约和同步成本线程过少又无法充分隐藏权重读取延迟。真正有效的配置往往只适合一组固定的模型形状。本文中的 RPB2 实验保持 BF16 原始权重并保留已验证的 FP32 pair accumulation只调整同一 LLMM1 实现的输出行分块方式。这里的 RPB 可以理解为 rows per block一个 block 负责的输出行数量不同权重读取和归约的组织也会随之变化。5. BF16 M1 GEMM 的本地 standalone 结果在相同 DCU、相同 BF16 输入和相同编译扩展下对比 RPB4 与 RPB2权重形状RPB4RPB2加速比最大差异14336 x 51200.114160 ms0.106480 ms1.0721x016384 x 51200.131040 ms0.120960 ms1.0833x034816 x 51200.272480 ms0.250400 ms1.0882x0测试条件为 warmup 10 次、正式测量 40 次。三种形状的输出都与参考路径逐位一致重复运行结果稳定。根据权重矩阵字节数除以实测 kernel 时间估算有效权重带宽约为 1.38-1.42 TB/s这是该 kernel 的有效带宽估算不是 DCU 峰值 HBM 带宽的直接测量。这组数据说明 RPB2 的收益是真实的局部执行收益但它还没有回答服务层问题。Standalone 只测了目标矩阵乘法完整 Decode 还包括其他 Linear、状态读取、采样和运行时开销。6. 从 kernel 到服务同容器 A/B 才是关键一关为了判断局部收益能否传递到完整推理本地在同一容器、同一模型、同一请求集和相同运行条件下进行了 A/B。基线配置和 RPB2 都使用 BF16 原始权重端点指标如下档位Baseline output tok/sRPB2 output tok/s吞吐变化Baseline p99 TPOTRPB2 p99 TPOT4K-8K18.836919.59614.03%46.9627 ms44.9923 ms8K-16K14.583715.03063.06%47.5920 ms45.5662 ms16K-32K11.749512.02862.38%49.8507 ms47.8545 ms端点结果与 standalone 的关系很典型局部 kernel 是 1.07x-1.09x服务层不是同样的 7%-9% 变化而是 2.38%-4.03%。原因是 RPB2 只影响一部分重复执行的 Linear 工作其余时间仍由其他算子和运行时组成。正确性和稳定性检查如下4K-8K: 2644 vs 2644 output tokens10/10 文本 byte-identical 8K-16K: 1768 vs 1768 output tokens10/10 文本 byte-identical 16K-32K: 1877 vs 1877 output tokens10/10 文本 byte-identical failed: 0TTFT 基本不变档位Baseline p99 TTFTRPB2 p99 TTFT4K-8K2049.317 ms2047.251 ms8K-16K4620.050 ms4614.045 ms16K-32K7044.241 ms7037.622 ms这符合优化位置的预期RPB2 主要作用在 Decode 的 M1 Linear首 token 的 Prefill 路径没有发生明显变化因此 TTFT 基本保持不变而 TPOT 得到改善。7. 为什么 1.08x kernel 不会自动变成 1.08x 服务可以用一个粗略的 Amdahl 模型理解这个现象。假设目标 kernel 占总时间的比例为 f局部加速为 s理想情况下整体加速上限近似为service speedup ≈ 1 / ((1 - f) f / s)当 s 1.08 时如果目标 kernel 只覆盖总时间的一部分整体收益必然小于 8%。实际服务还会受到更多因素影响不同层的矩阵形状不完全相同某些调用仍然走原有实现kernel launch、graph replay 和状态访问无法被目标 kernel 消除Prefill 的大 GEMM 与 Decode 的小 GEMM 位于不同时间阶段测量中还存在请求长度、缓存和系统调度带来的波动。所以端到端 A/B 的正确问题不是“这个 kernel 快了几倍”而是它覆盖了多少真实调用 这些调用占总时间多少 它是否改变了其他路径的行为RPB2 的本地结果之所以有意义正是因为它同时满足了三点目标调用在 Decode 中出现很多次、局部收益在多个真实形状上重复出现、输出和请求行为保持一致。8. 一个反例Standalone 有收益端点几乎不变同一批本地实验还提供了一个很有代表性的反例。对五类大 GEMM 的 145 个 shape、M 组合进行筛选后有 106 个形状同时满足BF16 输出 hash 与默认路径一致 Standalone CUDAGraph speedup 1.01x单独看这些形状它们都像是值得接入的优化。接入完整服务后固定中档请求集的结果却只有output throughput: 16.583027 - 16.586315 tok/s increment: 0.0198% texts: 10/10 exact tokens: 1809 - 1809 failed: 0 - 0另一个保持精确输出的 gate/up GEMM 方案在稳态同容器 A/B 中的平均收益也只有 0.3250%。这并不说明 standalone 测量错误而是说明这些调用在完整请求中的时间占比、调用覆盖率或可重叠程度不足以形成明显的服务收益。这个反例非常重要Standalone speedup 是局部上限端点 A/B 才是产品层收益。如果只根据 microbench 选择方案很容易把几十个小的正收益叠加成一个看起来很大的预测但实际运行时它们可能互相覆盖、被其他时间项淹没或者只命中很少的真实请求。9. 正确性和稳定性应该怎样验性能优化不能只记录一个耗时数字。对 DCU 上的 BF16 kernel至少要同时检查以下内容。9.1 Kernel 层输出是否为 finite输出 shape 和 stride 是否正确与参考路径的最大绝对差异或 hash重复运行是否稳定不同 batch、不同 M 和边界形状是否仍然正确。9.2 服务层每个请求是否完成输出 token 数是否一致生成文本是否逐字节一致或是否符合预设精度标准mean/p99 TPOT 是否改善TTFT 是否出现回归是否有失败请求、超时或异常重试。9.3 测量层两种配置是否使用同一模型与同一扩展是否在同一容器、同一请求集下比较是否把编译、首次加载和缓存建立时间单独处理是否采用足够的 warmup是否重复运行确认方向而不是只相信一次结果。尤其要避免把“代码路径存在”当成“kernel 已命中”。最可靠的做法是在运行时记录真实 dispatch、输入形状和选中的实现再将 marker 与性能结果一一对应。10. 一套可复用的 DCU 推理优化流程第一步先区分 Prefill 和 Decode分别测 TTFT、TPOT 和阶段耗时。不要用 Decode 的优化方法解释 Prefill也不要用 Prefill 的大 GEMM 结论代替 Decode 的 M1 分析。第二步用 profile 找时间墙先确认是大 GEMM、GEMV、Attention、状态读写还是运行时开销。只有知道时间占比才知道局部加速是否有足够的物理预算。第三步还原真实调用形状记录 M、N、K、dtype、batch、调用次数和输出写回方式。泛化矩阵的 microbench 只能作为参考不能替代真实形状。第四步在 standalone 中筛选先用固定 warmup 和多次迭代测局部耗时同时检查正确性和确定性。对明显低于门槛的实现不要急着接入服务。第五步做同容器端点 A/B保持模型、数据集、并发、采样设置和运行环境一致分别记录吞吐、TPOT、TTFT、输出长度和失败数。候选必须在多个档位或多个真实形状上重复出现正向信号。第六步保留负结果一个 standalone 正向但端点无收益的方案同样是有价值的工程结论。它可以帮助后续工作避免重复测试并让性能预算更接近真实服务而不是停留在 kernel 表格上。11. 结论本地实测数据呈现出一条比较清晰的 DCU 推理性能链路Prefill 的主要矛盾可以落在大 GEMMq4096 profile 中 aten::mm 占到 67.67%Decode 的主要矛盾是大量 Linear/GEMVLinear 在该 profile 中占 Decode 的 88.33%BF16 M1 kernel 的小幅局部改善经过重复调用后可以传递到端点RPB2 在三档同容器 A/B 中带来 2.38%-4.03% 的吞吐提升局部加速并不保证端到端收益106 个 standalone 正向且 exact 的尾部大 GEMM 形状接入后只有 0.0198%正确性、dispatch 命中、重复性和测量协议和 kernel 本身的耗时一样重要。对海光 DCU 做推理优化最值得复用的不是某一个固定的 block 参数而是一套验证顺序先用 profile 找到真实时间墙再用真实形状做 kernel microbench最后用同容器端点 A/B 验证它是否改善了完整服务。只有三层证据方向一致局部的“快”才有资格被称为服务层的“快”。本文中的性能数字来自本地固定环境的已有测量记录仅用于说明该模型、软件栈和 DCU 路径下的工程观察。不同 DCU 型号、驱动、DTK、编译缓存、请求分布和系统负载可能得到不同结果独立 kernel 加速比不应直接外推为通用服务吞吐。