Prhub

#28354 [FlashInfer v0.6.16] Support FlashInfer CuTe DSL NVFP4 MoE quantization

原始 PR 作者 zianglih 合并时间 2026-08-14 08:33 文件变更 14 提交数 34 评论 70 代码增减 +403 / -131

执行摘要

CuTe DSL 后端接入 per-token NVFP4 在线量化与 reload

PR body 明确提出目标:"Add FlashInfer CuTe DSL v2 MoE support to --quantization nvfp4_online",核心诉求是让 online NVFP4 权重转换 + 逐 token FP32 激活 scale 的计算路径在 CuTe DSL MoE 后端可用。作者同时强调要 "Keep the quantization contract established by merged upstream work":此前 PR#31382 把 fixed/per-tensor activation-scale 路径错误标为 nvfp4_online,本 PR 在 #33115 的基础上把 per-tensor 行为归入 modelopt_fp4,使契约语义恢复清晰。Nemotron 的 MTP/EAGLE 投机路径要求 target 与 embedded draft 共用同一 per-token 契约,这也是放行 embedded MTP experts 继承 nvfp4_online 的直接动机。

值得精读。该 PR 的价值不仅在于功能实现,更在于三处设计决策:一是量化契约的语义边界划分(nvfp4_online 严格对应 per-token,per-tensor 归 modelopt_fp4),为后续其他后端接入提供了可参照的契约模板;二是 reload 场景下保持 CUDA graph 捕获 tensor 身份不变的 in-place 刷新机制,这是在线服务场景的隐性约束;三是对上游依赖缺口的处理策略——用窄化 workaround 覆盖启动阶段、及时回退与注释 TODO,避免将上游 bug 固化到生产路径。建议关注 FlashInfer 0.6.17 发布后的 _synchronize_cutedsl_autotune_replay 清理,以及 #4486 修复后测试 skip 的解除。

讨论亮点

Review 核心交锋集中在四个方向:一是命名与风格,b8zhong 建议将 refresh_cutedsl_standard_scales 改名为 refresh_cutedsl_standard_scales_for_weight_update 并尽量避免 getattr,作者均采纳并显式初始化 _cutedsl_wrapper;二是复用边界,b8zhong 问 "why not modifying fp4_quantize wrapper",作者先改为复用共享 wrapper,但在全面审计后回退为直接调用 FlashInfer nvfp4_quantize,理由是扩展共享 wrapper 需要新增 per-token-only overload、fake 实现和 custom-op 注册,反而扩大共享 surface;三是数值常量,b8zhong 询问 e4m3_max 是否有共享常量,作者改用 FlashInfer 的 make_nvfp4_global_scale helper 并撤销自建 NVFP4_SF_VEC_SIZE 导出;四是测试规模,b8zhong 要求用更小的模型并分离在线 NVFP4 测试文件,作者新建 test_flashinfer_nvfp4_online_moe_backend.py 并以 Qwen3-30B 覆盖双后端。b8zhong 还关注 _synchronize_cutedsl_autotune_replay 对正常 serving 的影响,作者确认该同步仅限启动 autotune 阶段,b8zhong 回复 "Ok" 认可。未解决项是 b8zhong 提出的 CuteDSL/TRTLLM/CUTLASS weight update wrapper 未来统一("in the future, not now")。

实现拆解

  1. 量化契约与后端准入调整:在 nvfp4_online.py 中,NvFp4OnlineConfig.__init__fp4_ignored_layers 在 per-token 模式(_use_per_token_activation=True)下不再继承 source_ignored_layers,保证 embedded MTP draft experts 也能被在线量化;ModelOptNvFp4OnlineFusedMoEMethod.__init__ 的后端校验扩展为接受 flashinfer_cutedsl(仅限 no A2A 或 FlashInfer A2A)。在 server_args.py_handle_moe_kernel_config 中新增 nvfp4_onlineflashinfer_cutedsl 的组合校验,并拒绝 deepep + per-token NVFP4 激活的组合;_handle_environment_variables 在 deterministic 推理下强制 SGLANG_FLASHINFER_MOE_FUSED_FINALIZE=0
  2. CuTe DSL runner 的 per-token 量化路径flashinfer_cutedsl.py 新增 _make_per_token_global_scale(复用 FlashInfer 的 make_nvfp4_global_scale + current_nvfp4_4over6_config);fused_experts_none_to_flashinfer_cutedsl_fp4fused_experts_flashinfer_to_flashinfer_cutedsl_fp4 中通过 CuteDslFp4MoeQuantInfo.use_per_token_activation 分支调用 FlashInfer nvfp4_quantize(..., per_token_activation=True, backend="cute-dsl"),对 x_fp4/x_sf 做 reshape/view 适配 wrapper 输入布局,并在 wrapper.run 中转发 per_token_scale;FlashInfer A2A 分支要求 BF16 dispatch(x_sf is None),否则抛 ValueErrorflashinfer_cutlass.py 同步接入 SGLANG_FLASHINFER_MOE_FUSED_FINALIZEenviron.py 注册该环境变量。
  3. 权重 reload 与 CUDA graph 兼容modelopt_quant.py 中 CuTe DSL 相关 Parameter 赋值从 Parameter(...) 改为 copy_or_rebind_param,其中 w13_blockscale_mma/w2_blockscale_mma 的绑定方式保留参数身份;create_moe_runner 显式初始化 layer._cutedsl_wrapper = None 以替代 getattr 兼容。flashinfer_cutedsl.py 新增 refresh_cutedsl_standard_scales_for_weight_update,在 reload 后以 copy_ 原地刷新 scale tensor,避免破坏 decode CUDA graph 捕获的地址。
  4. 测试配套与文档:新增 test_flashinfer_nvfp4_online_moe_backend.py,用 Qwen3-30B-A3B 小模型注册 nightly(4-gpu-b200),覆盖 TRTLLM 与 CuTe DSL 双后端 GSM8K 精度 + 投机接受长度;重构 test_flashinfer_trtllm_gen_moe_backend.py 移除旧 Base 并落 skip;test_update_weights_from_disk_blackwell.py 增加 memory-saver 生命周期(release_memory_occupation/resume_memory_occupation)并新增 TestServerUpdateWeightsFromDiskNVFP4CuteDSLdocs/docs/references/environment_variables.mdx 同步新环境变量说明。
文件 模块 状态 重要度
python/sglang/srt/layers/moe/moe_runner/flashinfer_cutedsl.py MoE 执行 modified 8.07
python/sglang/srt/layers/quantization/nvfp4_online.py 量化配置 modified 6.37
python/sglang/srt/layers/quantization/modelopt_quant.py 量化处理 modified 6.82
python/sglang/srt/server_args.py 服务参数 modified 6.27
python/sglang/srt/environ.py 环境变量 modified 5.21
test/registered/backends/test_flashinfer_nvfp4_online_moe_backend.py 后端测试 added 7.86
test/registered/backends/test_flashinfer_trtllm_gen_moe_backend.py 后端测试 modified 7.24
test/registered/rl/test_update_weights_from_disk_blackwell.py 权重热更新 modified 6.87
python/sglang/srt/configs/model_config.py 模型配置 modified 5.18

关键符号

_make_per_token_global_scale refresh_cutedsl_standard_scales_for_weight_update ensure_cutedsl_wrapper fused_experts_none_to_flashinfer_cutedsl_fp4 fused_experts_flashinfer_to_flashinfer_cutedsl_fp4 NvFp4OnlineConfig.get_quant_method ModelOptNvFp4OnlineFusedMoEMethod.__init__ server_args._handle_moe_kernel_config server_args._handle_environment_variables

关键源码片段

python/sglang/srt/layers/moe/moe_runner/flashinfer_cutedsl.py dependency-wiring

核心源码:承接 per-token NVFP4 量化的实际执行路径。新增 `_make_per_token_global_scale`、`refresh_cutedsl_standard_scales_for_weight_update`、`CuteDslFp4MoeQuantInfo.use_per_token_activation` 字段,并在两个 fused func 中接入 `nvfp4_quantize(..., per_token_activation=True, backend="cute-dsl")` 与 `per_token_scale` 转发;同时通过 `use_fused_finalize` 贯通 deterministic 控制。

def _make_per_token_global_scale(input_tensor: torch.Tensor) -> torch.Tensor:
    # 复用 FlashInfer 共享 helper,与 TRTLLM 路径保持完全相同的数值契约;
    # per_token_activation=True 意味着全局 scale 是逐 token 的动态 FP32 值
    from flashinfer.quantization.nvfp4_quantization_utils import (
        current_nvfp4_4over6_config,
        make_nvfp4_global_scale,
    )
​
    return make_nvfp4_global_scale(
        input_tensor,
        per_token_activation=True,
        nvfp4_4over6_config=current_nvfp4_4over6_config(),
    )
​
​
def refresh_cutedsl_standard_scales_for_weight_update(layer: torch.nn.Module) -> None:
    # 权重 reload 后重新解析 alpha 与 input scale;per-token 模式额外构造
    # 4over6 全局 scale。新值必须 in-place 写入既有 tensor:decode CUDA graph
    # 捕获了这些地址,直接替换 tensor 会让已捕获的图失效
    w1_alpha, fc2_input_scale, w2_alpha, used_input_scale = (
        resolve_cutedsl_standard_scales(layer)
    )
    if layer.quant_config.use_per_token_activation:
        used_input_scale = _make_per_token_global_scale(used_input_scale)
​
    new_scales = (w1_alpha, fc2_input_scale, w2_alpha)
    current_scales = layer._cutedsl_scales
    current_input_scale = layer._cutedsl_input_scale
​
    # 元数据(shape/dtype/device)一旦变化就必须重新捕获 CUDA graph,
    # 这里直接报错而不是静默降级,避免产生隐性错误输出
    if (
        not isinstance(current_scales, tuple)
        or len(current_scales) != len(new_scales)
        or not isinstance(current_input_scale, torch.Tensor)
    ):
        raise RuntimeError(
            "CuTe DSL scale metadata changed during weight reload; "
            "CUDA graph recapture is required."
        )
    scale_pairs = (
        *zip(current_scales, new_scales),
        (current_input_scale, used_input_scale),
    )
    for current, new in scale_pairs:
        if (
            not isinstance(current, torch.Tensor)
            or current.shape != new.shape
            or current.dtype != new.dtype
            or current.device != new.device
        ):
            raise RuntimeError(
                "CuTe DSL scale metadata changed during weight reload; "
                "CUDA graph recapture is required."
            )
​
    with torch.no_grad():
        for current, new in scale_pairs:
            current.copy_(new)
python/sglang/srt/layers/quantization/nvfp4_online.py core-logic

量化契约入口:调整 `fp4_ignored_layers` 的继承规则使 embedded MTP draft 可继承 per-token 量化,放宽 `flashinfer_cutedsl` 后端准入并保留对 DeepEP 的禁止。

def get_quant_method(self, layer: torch.nn.Module, prefix: str):
    # FusedMoE 分支:per-token 模式下必须放行所有 MoE 层(含 embedded MTP draft),
    # 否则 draft experts 会掉回 FP8/ 未量化路径,破坏 target/draft 契约一致性
    if isinstance(layer, FusedMoE):
        source_layer_ignored = is_layer_skipped(
            prefix, self.exclude_modules, self.packed_modules_mapping
        ) or self.is_layer_excluded(prefix)
        # per-token 模式即使 source 层被排除也继续走在线量化
        if source_layer_ignored and not self.use_per_token_activation:
            return None
        if is_layer_skipped(
            prefix, self.fp4_ignored_layers, self.packed_modules_mapping
        ):
            # 只有非 per-token 模式下才允许落回 Fp8MoEMethod
            if self.is_checkpoint_fp8_serialized and not source_layer_ignored:
                return Fp8MoEMethod(self)
            return None
        return ModelOptNvFp4OnlineFusedMoEMethod(self, prefix)
    return None

评论区精华

refresh 函数命名需体现 weight update 语义 style

b8zhong 建议将 `refresh_cutedsl_standard_scales` 更名为 `refresh_cutedsl_standard_scales_for_weight_update` 或类似名称,使其职责更明确。

结论:作者采纳并命名为 `refresh_cutedsl_standard_scales_for_weight_update`,限定为权重 reload 后的 in-place scale 刷新。 · 已解决

避免 getattr 并显式初始化 CuTe DSL 私有状态 style

b8zhong 提出 "Avoid getattr if possible",针对 `_cutedsl_scales` 与 `_cutedsl_wrapper` 的惰性初始化方式。

结论:作者在 `create_moe_runner` 中显式初始化 `layer._cutedsl_wrapper = None`,改直接属性访问,消除兼容性查找。 · 已解决

per-token 量化是否复用共享 fp4_quantize wrapper 设计

b8zhong 质疑为何不直接扩展共享 `fp4_quantize` wrapper("why not modifying fp4_quantize wrapper")。作者先改为复用,后经全量审计回退为直接调用 FlashInfer `nvfp4_quantize`,理由是扩展共享 wrapper 需新增 per-token-only overload、fake 实现与 custom-op 注册,扩大共享 surface。

结论:保留直接调用 FlashInfer `nvfp4_quantize(..., per_token_activation=True, backend="cute-dsl")`,与 TRTLLM runner 模式对齐。 · 已解决

e4m3_max 数值常量应复用 FlashInfer 共享 helper 设计

b8zhong 询问 `e4m3_max` 数值常量是否已有共享定义;作者先集中为 `NVFP4_SF_VEC_SIZE`,后改为复用 FlashInfer 的 `make_nvfp4_global_scale` 与 `current_nvfp4_4over6_config`,并撤销 SGLang 侧新导出。

结论:数值计算落在 FlashInfer 共享 helper 上,SGLang 侧仅保留 runner 局部 `_FP4_SF_VEC_SIZE = 16`。 · 已解决

online NVFP4 测试应使用小模型并独立成文件 测试

b8zhong 认为 DeepSeek-V3 测试太长,建议用 Qwen3-30B 小模型,并咨询是否可迁移到 manual;进一步建议将 online 量化测试独立成文件并同时注册 TRTLLM-gen 与 CuteDSL 双后端。

结论:新建 `test_flashinfer_nvfp4_online_moe_backend.py`,注册 nightly 4-gpu-b200,CuTe DSL 用例使用 Nemotron-3-Super-120B(含 EAGLE 投机),TRTLLM 用例使用 Qwen3-30B。 · 已解决

autotune replay 同步是否会拖慢正常 serving 性能

b8zhong 询问 `_synchronize_cutedsl_autotune_replay` 是否在正常场景就有问题;作者说明该同步仅发生在启动 autotune 的 selected-tactic replay 阶段,由 `wrapper.use_fused_finalize` 与 `AutoTuner.is_tuning_mode` 双重门控,不进入 steady-state 路径。

结论:b8zhong 回复 "Ok" 认可;该 workaround 在 FlashInfer #4192 进入 0.6.17 后移除。 · 已解决

CuteDSL/TRTLLM/CUTLASS weight update wrapper 未来统一 设计

b8zhong 指出三个 MoE runner 各自维护 weight update wrapper,建议未来做统一封装("in the future, not now")。

结论:作为遗留设计债记录,不在本 PR 处理。 · 待处理

新环境变量需同步更新 env 文档 documentation

b8zhong 在 environ.py 上直接要求 "Can you update the env docs"。

结论:`docs/docs/references/environment_variables.mdx` 同步更新 `SGLANG_FLASHINFER_MOE_FUSED_FINALIZE` 与 per-token 激活变量说明。 · 已解决

风险与影响

  1. FlashInfer 上游依赖缺口_synchronize_cutedsl_autotune_replay 是为 FlashInfer #4192(selected-tactic replay 顺序修复)未进入 0.6.16.post1 而保留的窄化 workaround,只同步启动 autotune 阶段;0.6.17 发布后需及时清理,否则长期保留会与上游行为产生分歧。
  2. 上游 bug 导致的测试空洞:FlashInfer #4486 在 SM100/SM103 上 TRTLLM_GEN tile-192 BMM 路径返回非有限输出(详见 test_flashinfer_trtllm_gen_moe_backend.py 的 skip 注释),两个 NVFP4 online 测试被 @unittest.skip,后续 FlashInfer 修复前无 CI 守护。
  3. CUDA graph 地址稳定性约束modelopt_quant.pycopy_or_rebind_paramrefresh_cutedsl_standard_scales_for_weight_updatecopy_ 机制依赖 tensor 身份不变;任何未来逻辑若替换 tensor 而非 in-place 更新,都会使 decode graph 捕获的地址失效,属于隐式契约,需在注释和 code review 中持续强调。
  4. 量化配置语义变化nvfp4_online.py 中 per-token 模式清空 fp4_ignored_layers 的 source 继承,意味着原本被 exclude_modules 排除的层也可能被在线量化,虽然这是为了让 embedded MTP draft 继承契约,但对该行为变化的回归测试依赖 test_flashinfer_nvfp4_online_moe_backend.py 的覆盖。
  5. deterministic 行为差异SGLANG_FLASHINFER_MOE_FUSED_FINALIZE 默认开启 fused atomic finalize(非确定性),仅在 --enable-deterministic-inference 下关闭;对要求严格可复现输出的用户存在行为差异。

用户侧:nvfp4_online 的量化合集从 TRTLLM 后端扩展到 flashinfer_cutedsl(仅限 no A2A / FlashInfer A2A),Nemotron 等含 MTP/EAGLE 投机解码的模型可在 NVFP4 下以统一 per-token 契约运行,且 /update_weights_from_disk 后保持精度一致。系统侧:MoE 量化后端矩阵新增 CuTe DSL 维度,新增 SGLANG_FLASHINFER_MOE_FUSED_FINALIZE 环境变量并联动 deterministic 推理;modelopt_fp4 的 per-token 选项(SGLANG_FLASHINFER_NVFP4_PER_TOKEN_ACTIVATION=1)也覆盖 flashinfer_cutedsl。团队侧:本 PR 与 flashinfer-ai/flashinfer#3645、#3976、#4192 深度耦合,后续 FlashInfer 版本升级时需同步移除 workaround 与测试 skip;测试文件组织的变化(在线 NVFP4 测试从 test_flashinfer_trtllm_gen_moe_backend.py 迁出)会影响后续新增后端测试的落位习惯。

依赖未发布上游修复 NVFP4 在线量化核心路径 CUDA graph 地址稳定性约束 上游 bug 导致测试 skip deterministic 行为差异

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论