Prhub

#42982 [ROCm][Perf] DSv3.2 MI355X TP4 decode-step orchestration cleanup (3 micro-opts)

原始 PR 作者 frida-andersson 合并时间 2026-05-29 19:26 文件变更 2 提交数 8 评论 14 代码增减 +59 / -29

执行摘要

ROCm DSv3.2 解码三步 CPU 微优化,减延迟 ~3%

在 DeepSeek-V3.2 TP4 解码热路径中发现不必要的 CPU 和 GPU 开销:路由器日志类型转换内核(每步 58 次)、重复的元数据计算(每步约 1 次)和全缓冲区零填充(每步 3 次)。这些对最终结果无贡献但占用调度时间和 GPU 资源。

值得精读,尤其是元数据缓存和 shrink-tail 设计思路。注意事项:缓存键缺少对 seq_lens_cpu 不可用的保护,建议团队在合并时确认该场景;后续应增加单元测试覆盖缓存逻辑。

讨论亮点
  1. shrink-tail 安全性问题(gemini-code-assist)指出 req_id_per_token_buffertorch.empty 初始化可能残留脏数据,被 CUDA graph 读取后导致越界。作者将初始化改为 torch.zeros 解决。
  2. 元数据缓存键鲁棒性(gemini-code-assist)质疑当 seq_lens_cpu 可能为 None 时缓存键不完整导致错误命中。该风险在代码中未被修复,但假设 seq_lens_cpu 始终存在(来自公共注意力元数据),若实际缺失则问题仍存。
  3. 注释过多(AndreasKaratzas)要求减少 AI 生成的不必要注释。作者通过两次提交大幅删减注释,最终获得批准。
  4. 精度验证要求(tjtanaa)要求在准确率修复 PR (#43781) 合并后使用 20-shot GSM8K 重新测试。作者提交了 0.9522 ± 0.0059 (flexible-extract) 的结果,确认精度无损。

实现拆解

该 PR 包含三个独立优化,均位于 CPU 调度侧,不修改 GPU 内核:

  1. 路由器 bf16 调度(deepseek_v2.py)
    - 将 set_out_dtype(torch.float32) 改为 set_out_dtype(self.gate.weight.dtype)。AITER 的 biased_grouped_topk 内核内部累加为 fp32,但直接接受 bf16 输入。旧代码强制 fp32 输出导致每个 MoE 层插入一次 bf16->fp32 拷贝内核。新代码直接输出 bf16,消除该拷贝,且后处理中的 e_score_correction_bias 转换已存在,无需额外处理。

  2. 稀疏 MLA 元数据缓存(rocm_aiter_mla_sparse.py)
    - 在 __init__ 中新增 _prev_metadata_key 等字段缓存指纹。build 方法中将 (num_tokens, max_query_len, num_heads, min(seq_lens, topk_tokens)) 作为键,仅当变化时才调用 get_mla_metadata_v1。稳态解码时键不变,避免重复启动元数据内核。

  3. 缩小尾部零填充(rocm_aiter_mla_sparse.py)
    - 将 req_id_per_token_buffer.fill_(0), paged_kv_indices.fill_(0), paged_kv_indptr.fill_(0) 替换为有条件的切片填充:只清除比新扩展更大的旧扩展部分。paged_kv_indptr 完全由后续 cumsum 重写,直接移除。同时将 req_id_per_token_buffer 初始化由 empty 改为 zeros,确保未写入区域干净。

该 PR 不包含单独测试,但通过 Perfetto trace 验证了内核启动次数减少,并通过 20-shot GSM8K 验证了精度稳定。

文件 模块 状态 重要度
vllm/v1/attention/backends/mla/rocm_aiter_mla_sparse.py 稀疏注意 modified 7.07
vllm/model_executor/models/deepseek_v2.py MoE 路由 modified 5.23

关键符号

ROCMAiterMLASparseMetadataBuilder.__init__ ROCMAiterMLASparseMetadataBuilder.build DeepseekV2MoE.__init__

关键源码片段

vllm/v1/attention/backends/mla/rocm_aiter_mla_sparse.py core-logic

核心优化文件,实现元数据缓存和缩小尾部零填充,直接减少 GPU 内核启动和冗余填充操作。

# __init__ 中新增缓存字段(partial)
self._prev_req_extent: int = 0
self._prev_indices_extent: int = 0
self._prev_metadata_key: tuple | None = None# build() 中的 shrink-tail 逻辑
new_req_extent = int(req_id_per_token.shape[0])
new_indices_extent = num_tokens * self.topk_tokens
# 只在旧范围大于新范围时零填充尾部,避免全缓冲区 fill_
if self._prev_req_extent > new_req_extent:
    self.req_id_per_token_buffer[new_req_extent : self._prev_req_extent].fill_(0)
if self._prev_indices_extent > new_indices_extent:
    self.paged_kv_indices[new_indices_extent : self._prev_indices_extent].fill_(0)
self._prev_req_extent = new_req_extent
self._prev_indices_extent = new_indices_extent# 元数据缓存逻辑
clamped_seq_lens = np.minimum(
    common_attn_metadata.seq_lens_cpu[:num_reqs].numpy(),
    self.topk_tokens,
)
metadata_key = (
    num_tokens,
    int(common_attn_metadata.max_query_len),
    self._num_attention_heads,
    clamped_seq_lens.tobytes(),
)
if metadata_key != self._prev_metadata_key:
    from aiter import get_mla_metadata_v1
    get_mla_metadata_v1(..., self._mla_work_meta_data, ...)
    self._prev_metadata_key = metadata_key
vllm/model_executor/models/deepseek_v2.py data-contract

修改 MoE 路由器输出类型,消除 bf16->fp32 数据类型转换内核,每步减少 58 次启动。

# 变更前
if (
    self.is_rocm_aiter_moe_enabled
    and self.gate.e_score_correction_bias is not None
):
    # 显式要求 fp32 输出,导致后续 cast
    self.gate.set_out_dtype(torch.float32)# 变更后
if (
    self.is_rocm_aiter_moe_enabled
    and self.gate.e_score_correction_bias is not None
):
    # 直接使用 bf16,AITER 内核内部累加为 fp32
    self.gate.set_out_dtype(self.gate.weight.dtype)

评论区精华

shrink-tail 零填充安全性 正确性

gemini-code-assist 指出 req_id_per_token_buffer 用 torch.empty 初始化可能导致 CUDA graph 读取脏数据。

结论:作者将初始化改为 torch.zeros 并调整注释,风险消除。 · 已解决

元数据缓存键鲁棒性 正确性

gemini-code-assist 指出若 seq_lens_cpu 为 None 则缓存键不完整,可能导致错误缓存命中。

结论:作者未明确修复;代码假定 seq_lens_cpu 始终存在,但若 future 改动引入 None 则存在风险。 · unresolved

减少 AI 生成的注释 style

AndreasKaratzas 要求删除过多 AI 生成的注释,保持简洁。

结论:作者通过 fadadfc 和 3e5fe489 两次提交大幅删减注释,最终获得认可。 · 已解决

req_id_per_token_buffer 清空注释简化 style

AndreasKaratzas 指出 ROCm 默认 zero 初始化是已知习惯,无需注释说明。

结论:作者删除相关注释。 · 已解决

精度验证要求 测试

tjtanaa 要求在 #43781 修复后使用 20-shot GSM8K 重新验证精度,以确保稀疏索引器不引入退化。

结论:作者提供结果:flexible-extract 0.9522 ± 0.0059,strict-match 0.9530 ± 0.0058,精度无损。 · 已解决

风险与影响

  • 缓存键正确性风险:元数据缓存依赖 seq_lens_cpu 存在且正确。若在其他场景(如 prefix caching)中该字段被跳过或为 None,可能错误复用旧元数据,导致注意力计算错误。当前代码未做防御性校验。(文件 rocm_aiter_mla_sparse.pybuild 函数)
  • 精度风险:路由器输出类型从 fp32 改为 bf16,虽然 AITER 内部累加是 fp32,但外部路由逻辑精度可能下降。GSM8K 20-shot 测试未发现退化,但其他任务或长文本场景需验证。
  • 仅 ROCm 路径有效:优化条件为 rocm_aiter_ops.is_fused_moe_enabled() 等,非 ROCm 平台无影响,不会引入退化。
  • 无测试配套:缺乏针对缓存逻辑和 shrink-tail 的单元测试,回归依赖集成测试和手动 trace 验证。

用户影响:仅影响使用 ROCm AITER 加速的 DeepSeek-V3.2 模型用户,解码延迟降低约 3.1%,无精度退化。不影响其他模型或 GPU 平台。
系统影响:减少每次解码步骤的 GPU 内核启动次数(54 次 fill_ 拷贝、约 1 次元数据内核),降低 CPU 调度开销。
团队影响:展示了 CPU 端优化如何通过减少内核启动和内存操作带来可量化提升,为后续类似优化提供参考。

缓存键在 seq_lens_cpu 缺失时可能错误命中 需要 20-shot 长上下文验证精度 仅对 ROCm AITER 路径生效

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论