Prhub

#37075 [Diffusion][Kernel] Fuse Wan2.2 NVFP4 bias + GELU on Blackwell

原始 PR 作者 BBuf 合并时间 2026-08-31 01:37 文件变更 10 提交数 1 评论 2 代码增减 +411 / -5

执行摘要

融合 Wan2.2 NVFP4 bias+GELU,GB300 去噪提速约 3%

Wan2.2 ModelOpt NVFP4 MLP 的 fc_in 在 GB300 上以「NVFP4 GEMM -> BF16 bias add -> tanh GELU」三步执行,后两步是 memory-bound 操作,占去噪时间相当比例。PR body 明确目标是融合后两步,同时保持 eager 半精度舍入边界(bias 加法后先舍入回 BF16/FP16 再做 GELU),使默认 quality=lossless 路径完全不变,只在 quality=high 下为合格 NVFP4 站点启用加速。

值得精读。重点关注三点:一是 bias_gelu_tanh_kernel 如何通过 PDL 与 occupancy 限制在 memory-bound 场景下取得近 3 倍微基准加速;二是 nvfp4_bias_gelu_site.py 如何复用 QualityGatedFusion 实现请求级门控,避免影响 lossless 路径;三是测试设计,用 torch.equal 做 bit-exact 断言并覆盖非法 width/dtype 的拒绝行为,这种精度验证方式可用于其他融合 kernel。

讨论亮点

该 PR 没有实质 review 讨论线程,只有两条作者触发的 CI 命令评论(/tag-and-rerun-ci/tag-run-ci-label extra)。技术上的关键决策都体现在 PR body 中:作者用 MP4 SHA256 完全一致、81 帧 SSIM=1.0、PSNR=inf、LPIPS=0 证明了融合路径与 baseline 视觉无损;同时给出 quality=high 与 lossless 的固有差异(SSIM 0.9866)作为已知边界。

实现拆解

该 PR 的实现按以下步骤展开:

  1. 新增 JIT CUDA 融合内核:在 python/sglang/kernels/jit/csrc/elementwise/bias_gelu.cuh 中实现 bias_gelu_tanh_kernel,按向量化宽度(Blackwell 上 32 字节,其余 16 字节)逐元素执行「FP32 相加 -> 舍入回原 dtype -> tanh GELU」,保证与 eager F.gelu(input + bias, approximate="tanh") 的舍入边界一致;并支持 occupancy 上限的 persistent launch 几何与 PDL wait/trigger。

  2. JIT 封装与参数校验:新增 python/sglang/kernels/ops/elementwise/bias_gelu.py,用 cache_once 缓存按 dtype 编译的 TVM 模块,仅接受 FP16/BF16;对外暴露 bias_gelu_tanh(input, bias),内部用注册的 custom op _bias_gelu_tanh 写入预先分配的 output

  3. 请求级融合站点:新增 python/sglang/kernels/ops/diffusion/sites/nvfp4_bias_gelu_site.py,复用现有 QualityGatedFusion 基础设施,提供 mark_nvfp4_bias_gelu_sitemount_nvfp4_bias_geluunmount_nvfp4_bias_gelunvfp4_bias_gelu_activemount 时校验站点满足 fuse_bias_gelu_tanhfc_inModelOptFp4LinearMethod 且带 bias,然后把 fc_in.skip_bias_add 置为 True,使 GEMM 返回延迟 bias。

  4. MLP 激活分发改造:在 python/sglang/multimodal_gen/runtime/layers/mlp.py 中新增 fuse_bias_gelu_tanh 构造参数和 _apply_activation(x, bias, use_fused_bias_gelu) 方法。forward 改为接收 fc_in 返回的延迟 bias,在满足 CUDA、FP16/BF16 且门控激活时调用 bias_gelu_tanh,否则回退到 self.act(x + bias)

  5. Wan 模型接入与门控注册:在 python/sglang/multimodal_gen/runtime/models/dits/wanvideo.py_WanGELUMLP 中根据 fc_in.quant_method 是否为 ModelOptFp4LinearMethod 决定是否标记 NVFP4 融合站点;在 python/sglang/multimodal_gen/runtime/pipelines_core/stages/denoising.py_QUALITY_FUSION_HANDLERS 中注册 mount/unmount 回调,使 quality=high 生命周期自动启用与恢复。

  6. 测试与 Benchmark 配套:新增 test/registered/kernels/ops/elementwise/test_bias_gelu.py(bit-exact、非法宽度/类型拒绝)、test/registered/kernels/benchmark/elementwise/bench_bias_gelu.py(JIT vs PyTorch 对比),扩展 python/sglang/multimodal_gen/test/unit/test_wan_gelu_mlp.py 覆盖 NVFP4 站点的 mount/unmount 与 excluded linear 回归。

文件 模块 状态 重要度
python/sglang/kernels/jit/csrc/elementwise/bias_gelu.cuh 融合核函数 added 5.84
python/sglang/kernels/ops/diffusion/sites/nvfp4_bias_gelu_site.py 融合门控 added 6.87
python/sglang/multimodal_gen/runtime/layers/mlp.py 激活层 modified 6.62
python/sglang/multimodal_gen/runtime/models/dits/wanvideo.py Wan 模型 modified 6.33
python/sglang/kernels/ops/elementwise/bias_gelu.py JIT 封装 added 6.17
python/sglang/multimodal_gen/runtime/pipelines_core/stages/denoising.py 去噪编排 modified 5.08
test/registered/kernels/ops/elementwise/test_bias_gelu.py 核函数测试 added 6.28
python/sglang/multimodal_gen/test/unit/test_wan_gelu_mlp.py 集成测试 modified 5.63
test/registered/kernels/benchmark/elementwise/bench_bias_gelu.py 性能基准 added 5.92
python/sglang/kernels/ops/diffusion/__init__.py 导出配置 modified 3.13

关键符号

bias_gelu_tanh_kernel gelu_tanh bias_gelu_tanh _jit_bias_gelu_tanh_module _bias_gelu_tanh _apply_activation mark_nvfp4_bias_gelu_site nvfp4_bias_gelu_active mount_nvfp4_bias_gelu unmount_nvfp4_bias_gelu _WanGELUMLP.forward

关键源码片段

python/sglang/kernels/jit/csrc/elementwise/bias_gelu.cuh dependency-wiring

融合 kernel 的核心实现,包含向量化、PDL 支持与半精度舍入边界控制,是整个 PR 的性能来源。

// 融合 bias 加法与 tanh GELU 的 CUDA kernel 核心片段
// 关键设计:bias 加法后立即舍回原 dtype,再转 FP32 计算 GELU,
// 以复刻 eager F.gelu(input + bias, approximate="tanh") 的舍入边界。template <typename T, int kVecN, bool kUsePDL>
__global__ void bias_gelu_tanh_kernel(
    const T* __restrict__ input,
    const T* __restrict__ bias,
    T* __restrict__ output,
    int64_t num_vecs,
    int64_t row_vecs) {
  using vec_t = device::AlignedVector<T, kVecN>;  device::PDLWaitPrimary<kUsePDL>();
  const int64_t stride = static_cast<int64_t>(blockDim.x) * gridDim.x;  // 每个线程负责多个向量,stride 循环覆盖整个张量
  for (int64_t vec_id = static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
       vec_id < num_vecs; vec_id += stride) {
    vec_t x;
    vec_t b;
    x.load(input, vec_id);
    b.load(bias, vec_id % row_vecs);    vec_t result;
#pragma unroll
    for (int i = 0; i < kVecN; ++i) {
      const float x_f32 = device::cast<fp32_t>(x[i]);
      const float bias_f32 = device::cast<fp32_t>(b[i]);
      const T biased = device::cast<T>(x_f32 + bias_f32); // 半精度舍入边界
      result[i] = device::cast<T>(gelu_tanh(device::cast<fp32_t>(biased)));
    }
    result.store(output, vec_id);
  }
  device::PDLTriggerSecondary<kUsePDL>();
}// 宿主编排:校验输入并计算 occupancy 上限的 grid 尺寸
// grid = min(num_sms * occupancy, ceil(num_vecs / block_size))
// 避免过度占用,同时保证小块也能被填满。
template <typename T, bool kUsePDL>
void bias_gelu_tanh(tvm::ffi::TensorView input, tvm::ffi::TensorView bias,
                    tvm::ffi::TensorView output) {
  using namespace host;  auto num_rows = SymbolicSize{"num_rows"};
  auto hidden_dim = SymbolicSize{"hidden_dim"};
  auto device_ = SymbolicDevice{};
  device_.set_options<kDLCUDA>();  TensorMatcher({num_rows, hidden_dim}).with_dtype<T>().with_device(device_).verify(input);
  TensorMatcher({hidden_dim}).with_dtype<T>().with_device(device_).verify(bias);
  TensorMatcher({num_rows, hidden_dim}).with_dtype<T>().with_device(device_).verify(output);  constexpr int kVecN = device::kMaxVecBytes / sizeof(T);
  const int64_t rows = num_rows.unwrap();
  const int64_t width = hidden_dim.unwrap();
  CHECK_HOST(rows > 0) << "bias_gelu_tanh: num_rows must be positive";
  CHECK_HOST(width > 0 && width % kVecN == 0)
      << "bias_gelu_tanh: hidden_dim must be positive and divisible by " << kVecN;  const int64_t row_vecs = width / kVecN;
  const int64_t num_vecs = rows * row_vecs;
  constexpr int64_t kBlockSize = 256;
  const auto kernel = bias_gelu_tanh_kernel<T, kVecN, kUsePDL>;
  const int64_t occupancy = runtime::get_blocks_per_sm(kernel, kBlockSize);
  const int64_t num_sms = runtime::get_sm_count(device_.unwrap().device_id);
  const int64_t grid = std::min(num_sms * occupancy, div_ceil(num_vecs, kBlockSize));  LaunchKernel(grid, kBlockSize, device_.unwrap())
      .enable_pdl(kUsePDL)(kernel,
                           static_cast<const T*>(input.data_ptr()),
                           static_cast<const T*>(bias.data_ptr()),
                           static_cast<T*>(output.data_ptr()),
                           num_vecs, row_vecs);
}
python/sglang/kernels/ops/diffusion/sites/nvfp4_bias_gelu_site.py infrastructure

请求级融合门控的实现,决定哪些站点、何时启用融合,并负责 skip_bias_add 的挂载与恢复。

"""请求级 Wan NVFP4 bias+GELU 融合站点。"""
from __future__ import annotationsimport loggingimport torch.nn as nnfrom sglang.kernels.ops.diffusion.sites.quality_gate import QualityGatedFusionlogger = logging.getLogger(__name__)# 复用通用 QualityGatedFusion,以 marker/enabled 属性区分不同融合站点
_FUSION = QualityGatedFusion(
    name="Wan NVFP4 bias+GELU",
    marker_attr="_sgl_nvfp4_bias_gelu_site",
    enabled_attr="_sgl_nvfp4_bias_gelu_enabled",
)
​
​
def mark_nvfp4_bias_gelu_site(module: nn.Module) -> None:
    """将 module 标记为可延迟 fc_in bias 的融合站点。"""
    _FUSION.mark(module)
​
​
def nvfp4_bias_gelu_active(module: nn.Module) -> bool:
    """查询该站点当前是否处于挂载(激活)状态。"""
    return _FUSION.is_enabled(module)
​
​
def _site_reject_reason(site: nn.Module) -> str | None:
    """返回拒绝挂载的原因;返回 None 表示可挂载。"""
    if not getattr(site, "fuse_bias_gelu_tanh", False):
        return "site is not an NVFP4 fused-GELU target"
    linear = getattr(site, "fc_in", None)
    if linear is None:
        return "missing fc_in"
​
    from sglang.multimodal_gen.runtime.layers.quantization.modelopt_quant import (
        ModelOptFp4LinearMethod,
    )
​
    if not isinstance(getattr(linear, "quant_method", None), ModelOptFp4LinearMethod):
        return "fc_in is not ModelOpt NVFP4"
    if getattr(linear, "bias", None) is None:
        return "fc_in has no bias"
    return None
​
​
def mount_nvfp4_bias_gelu(root: nn.Module) -> bool:
    """挂载所有合格站点:把 fc_in 的 bias 延迟到激活阶段。"""
    sites = list(_FUSION.iter_sites(root))
    mounted = _FUSION.mount(root, reject_reason=_site_reject_reason, logger=logger)
    for site in sites:
        site.fc_in.skip_bias_add = mounted # 让 GEMM 返回 (output, bias)
    return mounted
​
​
def unmount_nvfp4_bias_gelu(root: nn.Module) -> None:
    """卸载并恢复所有标记站点的原始 eager 路径。"""
    _FUSION.unmount(root)
    for site in _FUSION.iter_sites(root):
        site.fc_in.skip_bias_add = False
python/sglang/multimodal_gen/runtime/layers/mlp.py core-logic

MLP 前向逻辑的改动点,新增 _apply_activation 统一分发 eager 与融合路径,是模型侧唯一的行为入口。

class MLP(nn.Module):
    """DiT 块的 MLP,无门控线性单元。"""
​
    def __init__(self, input_dim, mlp_hidden_dim, output_dim=None, bias=True,
                 act_type="gelu_pytorch_tanh", dtype=None, prefix="",
                 quant_config=None, fuse_bias_gelu_tanh=False):
        super().__init__()
        self.fuse_bias_gelu_tanh = fuse_bias_gelu_tanh
        self.fc_in = ColumnParallelLinear(
            input_dim, mlp_hidden_dim, bias=True,
            skip_bias_add=False, # 默认 eager 路径;挂载融合门控时置 True
            gather_output=False, quant_config=quant_config,
            prefix=add_prefix("fc_in", prefix),
        )
        self.act = get_act_fn(act_type)
        if output_dim is None:
            output_dim = input_dim
        self.fc_out = RowParallelLinear(
            mlp_hidden_dim, output_dim, bias=True,
            input_is_parallel=True, quant_config=quant_config,
            prefix=add_prefix("fc_out", prefix),
        )
​
    def _apply_activation(self, x, bias, *, use_fused_bias_gelu=False):
        """统一激活入口:优先走 JIT 融合 kernel,否则回退 eager。"""
        if self.fuse_bias_gelu_tanh and bias is not None:
            if (use_fused_bias_gelu and x.is_cuda
                    and x.dtype in (torch.float16, torch.bfloat16)):
                from sglang.kernels.ops.elementwise.bias_gelu import bias_gelu_tanh
                return bias_gelu_tanh(x, bias)
            return self.act(x + bias)
        return self.act(x)
​
    def forward(self, x):
        x, bias = self.fc_in(x) # 挂载融合门控时 bias 非 None
        x = self._apply_activation(x, bias)
        x, _ = self.fc_out(x)
        return x

评论区精华

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

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

风险与影响

主要风险集中在以下几点:

  1. 内核适用性限制bias_gelu_tanh 要求 hidden_dim 能被向量宽度整除(FP16 下 16 或 32 字节对应 8/16 个元素),且仅支持 FP16/BF16。不满足时 bias_gelu_tanh 直接抛错,但调用方 _apply_activationuse_fused_bias_gelu=False 或 dtype 不符合时会优雅回退 eager,因此只有 Blackwell 上 NVFP4 的合格站点才会走融合路径。

  2. 与既有 cublasLt GELU epilogue 的交互_WanGELUMLP.forwardfused_gelu_active 优先于 NVFP4 融合。当 fused_linear_gelu 挂载时走 fused_linear_gelu_tanh,此时 fc_in.skip_bias_add 也会被 NVFP4 门控设为 True,但由于 epilogue 直接消费 weight+bias,不受 skip_bias_add 影响。两个门控同时挂载时的行为需要后续留意,现有单元测试覆盖了常见顺序。

  3. AMD ROCm CI 失败:PR 的 ROCm 7.2 测试结果为失败,PR body 未说明原因。该 kernel 是 CUDA JIT,可能只注册了 CUDA CI;需确认是否会影响 AMD 平台回归。

  4. bit-exact 依赖 FP32 中间计算:内核先将 x 和 bias 各自转 FP32 相加,再舍入回原 dtype,最后转 FP32 算 GELU。该顺序必须与 eager PyTorch 行为严格一致,否则会引入精度漂移;测试已覆盖主要 shape,但极端数值(如 NaN/Inf)未覆盖。

影响范围集中在 diffusion 推理路径:Wan2.2 NVFP4 模型在 Blackwell(GB300/B200)上、quality=high 模式下,fc_in 的 bias+GELU 被融合,端到端延迟降低约 2.95%(去噪约 2.98%),峰值显存不变。默认 quality=lossless 路径与未量化模型完全不受影响。对团队而言,该 PR 展示并复用了 quality-gated fusion 的模式,为后续更多融合内核(如 Qwen-Image 的 bias 吸收)提供了可参考的 mount/unmount 与 bit-exact 验证范式。

AMD ROCm CI 失败 kernel 仅支持 16/32 字节对齐维度 与 cublasLt GELU epilogue 门控交互需关注 bit-exact 依赖 FP32 中间计算顺序

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论