Prhub

#34506 [Diffusion] Make weight-only FP8 dequant cache torch.compile-safe

原始 PR 作者 BBuf 合并时间 2026-08-12 16:26 文件变更 2 提交数 1 评论 2 代码增减 +22 / -2

执行摘要

修复 FP8 反量化缓存与 torch.compile 不兼容

PR body 说明:weight-only FP8 cache 首次使用时将每个 FP8 权重提升为持久化 compute-dtype 参数;服务器 warmup 运行在 torch.inference_mode() 下,导致缓存参数成为无版本计数器的 inference tensor,后续 torch.compile 追踪时抛出 RuntimeError: Inference tensors do not track version counter。

值得精读。改动虽小,但展示了一个典型的 PyTorch 机制坑位——inference_mode 产物会丢失版本计数器,进而影响 torch.compile 的 guard 建立;其修复方式(局部退出 inference_mode + no_grad)和回归测试写法(先 promotion 再 fullgraph 编译并对比 bit-exact)都值得在后续缓存类功能中复用。

讨论亮点

该 PR 无 reviewer 评论,issue 评论仅包含作者触发的 /tag-and-rerun-ci 指令和一次 CI 运行链接,没有出现设计或正确性层面的交锋。

实现拆解

  1. 定位根因:在 python/sglang/multimodal_gen/runtime/layers/quantization/weight_only_fp8.py_maybe_promote_fp8_weight 中,首次 forward 会调用 dequantize_rowwise_fp8_weight 生成反量化权重并替换 module.weight。由于 warmup 处于 torch.inference_mode(),新生成的 tensor 也是 inference tensor,没有版本计数器,Dynamo 无法对其建立 guard。
  2. 修改方案:将反量化操作放入 with torch.inference_mode(False), torch.no_grad(): 上下文,使生成的参数是普通可追踪 tensor 且不参与梯度图;其余逻辑(缓存开关、低内存 fallback、W8A8 直通路径)保持不变。
  3. 回归测试:在 test/unit/test_weight_only_fp8_dequant_cache.py 中新增 test_inference_mode_promotion_supports_torch_compile,先以 inference_mode 触发提升并断言 weight.is_inference() 为 False,再通过 torch.compile(fullgraph=True) 执行并验证 eager/compiled 输出与参考实现 bit-identical;同时测试基类改为 CustomTestCase 以接入仓库统一测试设施。
  4. 实机验证:B300 单元套件 6 项通过;Ideogram-4 FP8 compile preset 在 2xB300 上 warmup 完成,真实 20 步请求 denoise 2.202 s、E2E 2.314 s,且 eager 与 compiled 输出 bit-identical。
文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/layers/quantization/weight_only_fp8.py 量化层 modified 5.13
python/sglang/multimodal_gen/test/unit/test_weight_only_fp8_dequant_cache.py 测试套件 modified 5.1

关键符号

_maybe_promote_fp8_weight test_inference_mode_promotion_supports_torch_compile

关键源码片段

python/sglang/multimodal_gen/runtime/layers/quantization/weight_only_fp8.py core-logic

核心修复点:在 _maybe_promote_fp8_weight 中将反量化物化移入 torch.inference_mode(False) 与 torch.no_grad(),确保缓存参数不是 inference tensor,从而支持后续 torch.compile。

# _maybe_promote_fp8_weight 的核心决策片段:
# 决定是否将 FP8 权重一次性反量化为计算精度并缓存
weight = module.weight
if weight.dtype != FP8_WEIGHT_DTYPE:
    module._fp8_dequant_decided = True
    returnif weight.device.type != "cuda":
    returnif torch.cuda.is_current_stream_capturing():
    # 不能在 CUDA graph capture 内分配内存,留待 eager 调用时再处理
    returnmodule._fp8_dequant_decided = True
if not envs.SGLANG_DIFFUSION_FP8_WEIGHT_DEQUANT_CACHE:
    returnif module.enable_fused_w8a8:
    # W8A8 GEMM 路径直接消费 FP8 权重,无需反量化
    returndtype = module.compute_dtype or x_dtype
if dtype not in (torch.float16, torch.bfloat16, torch.float32):
    returnneeded_bytes = weight.numel() * dtype.itemsize
free_bytes, _ = torch.cuda.mem_get_info(weight.device)
if free_bytes < needed_bytes + _DEQUANT_CACHE_RESERVE_BYTES:
    # 显存不足时保持 FP8 驻留,每次前向都反量化
    if not _dequant_low_memory_logged:
        logger.warning(
            "Keeping weight-only FP8 linear weights FP8-resident (low "
            "free device memory); they dequantize on every forward."
        )
        _dequant_low_memory_logged = True
    return# 服务器 warmup 常在 inference_mode 下运行,若在此模式下物化缓存参数,
# 生成的 tensor 会丢失版本计数器,导致后续 torch.compile 无法建立 guard。
# 因此临时退出 inference_mode 并关闭梯度,得到普通可追踪的参数。
with torch.inference_mode(False), torch.no_grad():
    dequant = dequantize_rowwise_fp8_weight(weight, module.weight_scale, dtype)module.weight = nn.Parameter(dequant, requires_grad=False)
python/sglang/multimodal_gen/test/unit/test_weight_only_fp8_dequant_cache.py test-coverage

新增回归测试覆盖 inference_mode 提升后 torch.compile(fullgraph=True) 路径,并断言缓存参数不是 inference tensor;测试基类改为 CustomTestCase。

class TestWeightOnlyFP8DequantCache(CustomTestCase):
    # 回归测试:inference_mode 下的提升必须与后续 torch.compile 兼容
    @unittest.skipUnless(torch.cuda.is_available(), "requires CUDA")
    def test_inference_mode_promotion_supports_torch_compile(self):
        linear = _make_linear(torch.device("cuda"))
        x = torch.randn(8, 64, device="cuda", dtype=torch.bfloat16)
        reference = _reference(linear, x)
​
        # 模拟服务器 warmup 的 inference_mode 路径触发 FP8 提升
        with torch.inference_mode():
            eager_out = linear(x)
        # 修复后缓存参数不应是 inference tensor,否则 Dynamo 无法建立 guard
        self.assertFalse(linear.weight.is_inference())
​
        # 再走 torch.compile(fullgraph=True),确保编译追踪不抛错
        compiled = torch.compile(linear, fullgraph=True)
        with torch.inference_mode():
            compiled_out = compiled(x)
        self.assertTrue(torch.equal(reference, eager_out))
        self.assertTrue(torch.equal(reference, compiled_out))

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  • 行为变更torch.inference_mode(False) 可能让 dequant 运算在普通 eager 模式下记录置位,但外层 torch.no_grad() 保证不追踪梯度,输出仍 bit-identical;已有的 is_current_stream_capturing() 检查可避免在 CUDA graph capture 中触发。
  • 性能影响:仅改变张量创建时的模式标注,不改变反量化算法与缓存策略,低内存 fallback 和 W8A8 路径完全绕过该分支,性能风险极低。
  • 兼容性:改动集中在单个函数,未触碰 dequantize_rowwise_fp8_weightswap_linears_to_weight_only_fp8,对既有 eager 推理用户透明。
  • 测试盲区:新增测试只覆盖 CUDA 上的 bfloat16 输入,未覆盖 fp16/fp32 或 CPU compile 场景,但仓库现有 test_promotion_dtype_follows_input_when_unset 部分覆盖 dtype 分支。

对使用 diffusion 模型 + FP8 weight-only 量化 + torch.compile 的用户,此前 warmup 后编译必现的 RuntimeError 被消除;对 eager 路径无行为变化,输出保持 bit-identical。对团队而言,确立了一个值得复用的模式:持久化缓存参数必须在非 inference_mode 下物化,并配套 fullgraph 回归测试;该 PR 也验证了 Ideogram-4 FP8 compile 端到端可用。

缓存参数物化模式变更 torch.compile 兼容性回归风险

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论