Prhub

#32370 Optimize FP32 LM head for bf16/fp16

原始 PR 作者 ilyasher-harmonic 合并时间 2026-08-12 06:14 文件变更 2 提交数 5 评论 7 代码增减 +84 / -7

执行摘要

FP32 LM head 跳过 FP32 cast,H200 上约 10x 加速

PR body 明确指出旧实现的问题:"The previous behavior was to cast the hidden states and LM weights into new fp32 tensors and then perform a fp32xfp32 -> fp32 matmul",这不仅产生昂贵的 FP32 显存拷贝,也拖慢了 LM head 计算。作者希望通过 "skipping the fp32 cast and using a bf16xbf16 -> fp32 matmul with an fp32 accumulator" 在保留 use_fp32_lm_head 所需精度的前提下,获得 H200 上 5.7ms -> 0.56ms(约 10x)的加速。

值得精读,是一个小而精准的性能优化示范:利用 torch.mm(out_dtype=...) 避免显式 dtype cast,同时用条件分支 + 测试确保非目标平台安全回退。关注点在于:

1) 测试对平台差异(CUDA vs 非 CUDA)的处理方式;
2) 数值精度是否满足 use_fp32_lm_head 的语义承诺;
3) 后续若要在其他后端启用同类优化,可复用该条件分支模式。

讨论亮点

该 PR 没有实质性的设计争辩(review_comments 为 0),讨论主要集中在 CI 流程管理:

  • b8zhong 两次发起 /rerun-test test/registered/rl/test_fp32_lm_head.py,第一次因分支相对 main 分叉未触发,第二次在 rebase 后成功跑通。
  • ispobock 发起 /rerun-failed-ci
  • b8zhong 最后表示 "@ispobock Can we merge this? I think the remaining failures are unrelated.",即剩余 CI 失败与本次改动无关,随后两位 reviewer(b8zhong、Fridge003)均批准合并。

实现拆解

  1. 变更入口:修改 python/sglang/srt/layers/logits_processor.pyLogitsProcessor._compute_lm_head 方法的 use_fp32_lm_head 分支,这是 LM head 计算的核心路径。
  2. 核心逻辑:新增 use_mm_out_dtype 条件,要求 hidden_states.is_cuda 为真、hidden_stateslm_head.weight dtype 相同且属于 (torch.float16, torch.bfloat16)。满足条件时调用 torch.mm(hidden_states, lm_head.weight.T, out_dtype=torch.float32),由底层在 FP32 累加器中完成矩阵乘,避免物化两份 FP32 张量;不满足时回退到原有 torch.matmul 显式 cast 路径,保证非 CUDA 平台、fp32 输入、dtype 不一致等场景行为不变。
  3. 测试配套:重写 test/registered/rl/test_fp32_lm_head.py,新增 probe_mm 拦截器捕获 torch.mm 调用,并扩展 _run_case 断言 operation、输入 dtype 与 out_dtype;新增 test_flag_true_fp32_falls_back_to_explicit_fp32_matmul 验证 fp32 输入走回退路径。CUDA 可用时 fp16/bf16 用例期望走 mm,非 CUDA(如 XPU)则期望走 matmul
  4. 配套范围:GGUF、量化、LoRA、Intel AMX、RL on-policy target 等既有分支均未改动,风险集中在新增条件分支本身。
文件 模块 状态 重要度
python/sglang/srt/layers/logits_processor.py logits 层 modified 6.14
test/registered/rl/test_fp32_lm_head.py RL 测试 modified 5.95

关键符号

_compute_lm_head probe_mm test_flag_true_fp32_falls_back_to_explicit_fp32_matmul

关键源码片段

python/sglang/srt/layers/logits_processor.py core-logic

性能优化的核心逻辑所在,通过新增条件分支在 CUDA 同 dtype fp16/bf16 场景下用 torch.mm(out_dtype=fp32) 跳过显式 FP32 cast,是本次 10x 加速的来源。

def _compute_lm_head(
    self,
    hidden_states: torch.Tensor,
    lm_head: VocabParallelEmbedding,
    embedding_bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
    quant_method = getattr(lm_head, "quant_method", None)
    if hasattr(lm_head, "set_lora") and hasattr(lm_head, "apply_lora"):
        # LoRA 包装的模块直接走 forward 方法
        logits = lm_head(hidden_states)
    elif should_apply_lm_head_quant_method(lm_head, quant_method):
        logits = quant_method.apply(lm_head, hidden_states, embedding_bias)
    elif hasattr(lm_head, "weight"):
        # 普通 Linear 层:优先走快速路径,不物化 FP32 副本
        if self.use_fp32_lm_head:
            # CUDA 上且 hidden_states 与权重同为 fp16/bf16 时,
            # 直接用 torch.mm 的 out_dtype 参数做 bf16xbf16 -> fp32
            # 的矩阵乘,省掉两次显式 FP32 cast(H200 上约 10x 加速)。
            use_mm_out_dtype = (
                hidden_states.is_cuda
                and hidden_states.dtype == lm_head.weight.dtype
                and hidden_states.dtype in (torch.float16, torch.bfloat16)
            )
            if use_mm_out_dtype:
                logits = torch.mm(
                    hidden_states,
                    lm_head.weight.T,
                    out_dtype=torch.float32,
                )
            else:
                # 非 CUDA 或 dtype 组合不受支持时回退到显式 FP32 cast
                logits = torch.matmul(
                    hidden_states.to(torch.float32),
                    lm_head.weight.to(torch.float32).T,
                )
        elif use_intel_amx_backend(lm_head):
            # Intel AMX 后端继续走 weight_packed_linear
            logits = torch.ops.sgl_kernel.weight_packed_linear(
                hidden_states.to(lm_head.weight.dtype),
                lm_head.weight,
                None, # bias
                True, # is_vnni
            )
        elif self.rl_on_policy_target is not None:
            # tie-weight 场景下可能无法改权重 dtype,统一转 bf16 计算
            logits = torch.matmul(
                hidden_states.bfloat16(), lm_head.weight.T.bfloat16()
            )
        else:
            logits = torch.matmul(
                hidden_states.to(lm_head.weight.dtype), lm_head.weight.T
            )
    else:
        # GGUF 模型:无 weight 属性,走 quant_method
        if self.use_fp32_lm_head:
            with torch.cuda.amp.autocast(enabled=False):
                logits = lm_head.quant_method.apply(
                    lm_head, hidden_states.to(torch.float32), embedding_bias
                )
        else:
            logits = lm_head.quant_method.apply(
                lm_head, hidden_states, embedding_bias
            )
    return logits
test/registered/rl/test_fp32_lm_head.py test-coverage

测试配套验证新路径与 fallback 路径:新增 probe_mm 拦截 torch.mm 并断言 out_dtype,新增 fp32 输入 fallback 用例,同时按 CUDA 可用性区分平台预期。

def probe_mm(a, b, *args, **kw):
    # 拦截 torch.mm,记录首次调用的 operation、输入 dtype 与 out_dtype
    if not state["called"]:
        state.update(
            called=True,
            operation="mm",
            a=a.dtype,
            b=b.dtype,
            out_dtype=kw.get("out_dtype"),
        )
    return original_mm(a, b, *args, **kw)
​
​
def test_flag_true_fp32_falls_back_to_explicit_fp32_matmul(self):
    # 输入与权重都是 fp32 时,新路径不适用,必须回退到
    # 显式 FP32 matmul,并保持原有 dtype 断言。
    self._run_case(
        torch.float32,
        True,
        torch.float32,
        torch.float32,
        torch.float32,
        "matmul",
    )

评论区精华

CI 重跑与 rebase 要求 other

b8zhong 两次发起 /rerun-test test/registered/rl/test_fp32_lm_head.py,第一次因分支相对 main 分叉未触发,第二次在 rebase 后成功跑通;ispobock 随后发起 /rerun-failed-ci。

结论:测试重跑通过,确认 test_fp32_lm_head.py 在 1-gpu-5090 上成功。 · 已解决

剩余 CI 失败与合并决策 question

b8zhong 询问 "@ispobock Can we merge this? I think the remaining failures are unrelated.",即认为剩余失败与本次改动无关。

结论:两位 reviewer(b8zhong、Fridge003)均 APPROVED,PR 最终合并。 · 已解决

风险与影响

  1. API 兼容性风险torch.mm(..., out_dtype=...) 依赖较新版本 PyTorch,若运行环境版本过旧可能报 TypeError。当前条件限制在 CUDA 且有测试覆盖,但 sglang 支持的多后端(XPU、NPU、CPU)中非 CUDA 路径会自动 fallback,降低暴露面。
  2. 数值精度风险:虽然 out_dtype=torch.float32 保证累加精度,但乘法本身从 FP32 降为 bf16/fp16,与显式 FP32 cast 的结果存在潜在微小差异;use_fp32_lm_head 的语义是防溢出/保精度,对精度敏感的 RL 场景需关注。测试仅断言 dtype,未做数值等价性校验。
  3. 平台分支风险hidden_states.is_cuda 硬编码判断,未来若引入其他支持 torch.mm(out_dtype) 的加速设备(如部分 XPU)需扩展条件。
  4. 回归范围:仅改动 _compute_lm_headuse_fp32_lm_head 分支,LoRA、量化、GGUF、AMX 各分支不受影响。

影响所有开启 use_fp32_lm_head 且隐藏状态与 LM head 权重为 bf16/fp16 的模型(常见于 RL 训练和需要高精度 logits 的推理场景),在 H200 上 LM head 单次计算约 10x 加速,可显著降低 decode 阶段尾部延迟。非 CUDA 后端与 fp32 输入场景完全回退到旧路径,无行为变化。对团队而言,改动小、测试完备,合并成本低,但需留意 PyTorch 版本下限与数值一致性。

核心路径变更 依赖 PyTorch 新 API out_dtype 数值精度潜在差异 平台差异 fallback

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论