Gemma 4 NVFP4 MoE 空输出
Issue:
vLLM#51525:Gemma 4 NVFP4 MoE在CUTLASS路径返回PAD token或空内容ROOT CAUSE —
VLLM_CUTLASSW4A4消费未完成完整性校验的per-expert activation scale
ModelOpt calibration按模型的真实MoE routing收集activation statistics。低频expert若在校准语料中没有接收到token,对应的gate/up或down projection就不会形成有效统计量;exporter随后不会为该expert写出w13_input_scale或w2_input_scale。Packed FP4 weight及其weight scale仍然存在,因此checkpoint可以通过只关注权重数量和shape的检查
vLLM的ModelOptNvFp4FusedMoE使用torch.empty分配完整的per-expert scale tensor,loader只覆盖checkpoint中实际存在的条目。加载完成后,代码没有在backend format conversion前验证所有expert slot都已写入。选择VLLM_CUTLASS时,format conversion继续对w13_input_scale和w2_input_scale取倒数,生成a1_gscale和a2_gscale;缺失slot中的未初始化bit pattern因而被转换为inf、NaN或任意有限垃圾值,并被当作合法量化参数传给kernel
Router命中缺项expert后,CUTLASS路径首先将a1_gscale[expert_idx]传给scaled_fp4_experts_quant。CUDA kernel用该值计算每 16 个activation的E4M3 block scale,并把归一化结果编码为E2M1;错误scale会在进入第一个w13expert GEMM前直接破坏activation。SiLU/mul之后,第二次量化再由a2_gscale[expert_idx]驱动,并进入w2expert GEMM。污染随后返回transformer residual path,继续影响后续layer、final norm、LM head logits和sampling。GPU kernel、worker和HTTP server均可正常结束,因此请求仍返回HTTP 200,但token可能全部为PAD或被解码为空内容
| 项目 | 已确认结论 |
|---|---|
| 复现模型 | bg-digitalservices/Gemma-4-26B-A4B-it-NVFP4 |
| 已确认环境 | RTX 5090 / SM120,vLLM 0.24.0 |
| 失败路径 | 显式选择 VLLM_CUTLASS NVFP4 MoE,执行 W4A4 expert computation |
checkpoint 缺陷 |
12 个 layer 中共有 25 个 expert activation scale 条目缺失 |
loader 缺陷 |
per-expert scale 由 torch.empty 分配,加载结束后没有验证 checkpoint coverage |
| 外部症状 | 请求耗尽 max_tokens,返回 PAD token、content: null 或空字符串 |
NVFP4
ModelOpt 是 NVIDIA 的量化工具及量化 checkpoint 格式,不是一种数据类型。bg-digitalservices/Gemma-4-26B-A4B-it-NVFP4 的量化配置包含 producer=modelopt、quant_algo=NVFP4 和 group_size=16:原始 BF16 模型先由 NVIDIA ModelOpt 执行 PTQ,再将量化权重及其 scale 按 ModelOpt 的字段约定保存到 Hugging Face checkpoint。vLLM 启动参数 --quantization modelopt 不会在服务启动时重新量化模型;该参数用于声明 checkpoint 的序列化格式,使 loader 按 ModelOpt 的字段名称、shape 和 layout 加载已有量化数据。ModelOpt、NVFP4 和执行 backend 分属不同层级
| 名称 | 所属层级 | 职责 |
|---|---|---|
Gemma 4 MoE | 模型结构 | 决定 expert、router、gate/up/down projection 和每个 token 的执行路径 |
ModelOpt | 量化工具与 schema | 执行 PTQ、calibration 和 export,同时规定量化 checkpoint 的序列化字段 |
NVFP4 | 数值格式 | 规定 E2M1 数据以及两级 scaling factor |
CUTLASS / Marlin | 运行时 backend | vLLM 加载 checkpoint 后实际执行 GEMM |
google/gemma-4-26B-A4B-it
│ BF16 权重
▼
NVIDIA ModelOpt 执行 PTQ
│ 导出 NVFP4 W4A4 checkpoint
▼
bg-digitalservices/Gemma-4-26B-A4B-it-NVFP4
│ vLLM 按 ModelOpt schema 加载
▼
ModelOptNvFp4FusedMoE
│
├── VLLM_CUTLASS → W4A4 执行 → 需要 activation input_scale
│
└── MARLIN → W4A16 执行 → 不消费 activation input_scale
Gemma 4 的 expert weight 原本是 fused 3D tensor,而不是 128 组普通 nn.Linear。该模型的量化脚本先通过 ModelOpt plugin 将 expert 展开为可量化的 Linear,完成量化后再把导出键名转换为 vLLM FusedMoE 所需的形式。模型卡记录的 calibration 配置是 ModelOpt 0.43、CNN/DailyMail 4096 个样本、seq_len=1024,并保留由真实 router 决定的 natural expert routing;vision encoder 不参与量化
ModelOpt 负责生成 checkpoint,vLLM 不会在启动时重新 calibration。对 MoE expert,checkpoint 中与 NVFP4 计算直接相关的数据可以归纳为
| 数据 | checkpoint 内容 |
粒度 | 产生时间 |
|---|---|---|---|
Weight FP4 | w13_weight、w2_weight | 两个 E2M1 值打包进一个 byte | ModelOpt export |
Weight block scale | w13_weight_scale、w2_weight_scale | K 维每 16 个 weight 一个 E4M3 scale | ModelOpt export |
Weight global scale | w13_weight_scale_2、w2_weight_scale_2 | 每个 expert/projection 一个 FP32 scale | ModelOpt calibration/export |
Activation global scale | w13_input_scale、w2_input_scale | 每个 expert/projection 一个 FP32 scale | ModelOpt calibration/export |
Activation FP4 与 block scale | 不在 checkpoint 中 | 当前请求路由到 expert 后动态生成 | vLLM runtime |
NVFP4 是面向 NVIDIA Blackwell Tensor Core 的 4-bit 浮点量化格式。其数值主体采用 FP4 E2M1:最高 1 bit 为符号位,中间 2 bit 为指数位,最低 1 bit 为尾数位。6 是 E2M1 的最大有限幅值,全部编码如下
0000 -> 0.0 0001 -> 0.5 0010 -> 1.0 0011 -> 1.5
0100 -> 2.0 0101 -> 3.0 0110 -> 4.0 0111 -> 6.0
1000 -> -0.0 1001 -> -0.5 1010 -> -1.0 1011 -> -1.5
1100 -> -2.0 1101 -> -3.0 1110 -> -4.0 1111 -> -6.0
vLLM 的 NVFP4 activation kernel 不使用手写分段公式选择 E2M1 编码,而是在 CUDA C++ 中调用 Blackwell PTX 转换指令
// Convert 4 float2 values into 8 e2m1 values (represented as one uint32_t).
__device__ __forceinline__ uint32_t fp32_vec8_to_e2m1(
float2 (&array)[4]) {
uint32_t val;
asm volatile(
"{\n"
".reg .b8 byte0;\n"
".reg .b8 byte1;\n"
".reg .b8 byte2;\n"
".reg .b8 byte3;\n"
"cvt.rn.satfinite.e2m1x2.f32 byte0, %2, %1;\n"
"cvt.rn.satfinite.e2m1x2.f32 byte1, %4, %3;\n"
"cvt.rn.satfinite.e2m1x2.f32 byte2, %6, %5;\n"
"cvt.rn.satfinite.e2m1x2.f32 byte3, %8, %7;\n"
"mov.b32 %0, {byte0, byte1, byte2, byte3};\n"
"}\n"
: "=r"(val)
: "f"(array[0].x), "f"(array[0].y),
"f"(array[1].x), "f"(array[1].y),
"f"(array[2].x), "f"(array[2].y),
"f"(array[3].x), "f"(array[3].y));
return val;
}
__device__ __forceinline__ uint32_t pack_fp4(float2 (&v)[4]) {
return fp32_vec8_to_e2m1(v);
}
cvt.rn.satfinite.e2m1x2.f32 是真正完成数值选择的指令:rn 表示 round-to-nearest、ties-to-even,satfinite 将超出 E2M1 范围的输入饱和到最大有限值,e2m1x2.f32 表示从两个 FP32 输入生成两个 E2M1 4-bit 值。四条转换指令共生成 8 个 E2M1 值,每两个值装入一个 byte;mov.b32 再把 4 个 byte 合并为一个 uint32_t。后面的 cvt_warp_fp16_to_fp4 在完成 block scaling 后调用 pack_fp4,因此上面的编码表就是由硬件转换指令实施的,而不是由软件逐项查表。NVFP4 不直接用这组离散值拟合整个 tensor,而是采用两级 scale
| 项目 | 第一层缩放 | 第二层缩放 |
|---|---|---|
| 名称 | Micro-block scale |
Tensor-level global scale |
| 格式 | FP8 E4M3 |
FP32 |
| 范围 | 每连续 16 个元素共享一个 scale |
整个 tensor;在 MoE 中细化到 expert/projection |
| 权重 | weight_scale |
weight_scale_2 |
| 激活 | 运行时动态生成 | input_scale |
第一层 block scale 描述 16 元素 micro-block 的局部动态范围。FP4 E2M1 主要保留 block 内部的相对数值,E4M3 block scale 将该 block 恢复到相应量级。第二层 global scale 描述 tensor 级量化范围,用于把各 block scale 映射到 E4M3 可表示的范围,并在 GEMM 输出缩放时恢复整体数值尺度。E2M1 的最大有限值为 6,E4M3 的最大有限值为 448,二者对应的组合范围为 2688;ModelOpt 保存的 global scale 以这一范围为基础,并可结合 calibration headroom 调整
FP4 数据不能脱离 scale 独立解释。Weight 是静态参数,ModelOpt 在 PTQ/export 阶段把 packed FP4 weight、E4M3 block scale 和 FP32 global scale 一并写入 checkpoint。Activation 随请求内容和 MoE routing 改变,checkpoint 只保存 calibration 得到的 FP32 global scale;FP4 activation 和 E4M3 block scale 由 vLLM 在每次 GEMM 前动态生成
input_scale对应第二层FP32 activation global scale它不是 16 元素
micro-block使用的E4M3 block scale,也不是某次请求即时统计得到的scale;它由calibration确定、写入checkpoint,并在MoE中按expert/projection使用。动态block quantization不能替代input_scale
从原始 activation 到 Tensor Core operand 的运行时处理顺序如下
BF16/FP16 hidden state
│ router 已确定 expert_idx
▼
读取该 expert/projection 的 FP32 input_scale
│ vLLM 取倒数,得到传给量化 kernel 的 SFScaleVal
▼
每 16 个元素计算绝对值最大值
│ 结合 SFScaleVal 和 E2M1 最大值 6
▼
生成并舍入为 FP8 E4M3 block scale
│ 用 block scale 归一化当前 16 个值
▼
舍入到 {-6, -4, -3, -2, -1.5, -1, -0.5, 0, ..., 6}
│ 两个 E2M1 值打包为一个 byte
▼
FP4 activation + FP8 block scale 进入 W4A4 GEMM
│ GEMM scaling 同时纳入 FP32 activation/weight global scale
▼
恢复到正确的输出数值尺度
E4M3 舍入是两级缩放不可省略的原因。global scale 先把不同 block 的局部范围映射到 E4M3 的有效表示区间;block scale 完成舍入后,再与 FP32 global scale 一起恢复整体量级。错误的 input_scale 会改变 E4M3 block scale 的舍入或裁剪结果,并进一步改变 E2M1 编码。Weight 侧执行同样的数值重建,只是其 E2M1 数据、E4M3 block scale 和 FP32 global scale 已由 ModelOpt 写入 checkpoint。实际 CUTLASS kernel 会融合 block scaling、Tensor Core GEMM 与 global scaling,但上述数据依赖保持不变
MoE 版本不会对所有 token 使用同一个 global scale,而是按照 router 已选中的 expert_idx 读取对应条目。关键代码位于 csrc/libtorch_stable/quantization/fp4/nvfp4_experts_quant.cu
float const SFScaleVal =
SFScale == nullptr ? 1.0f : SFScale[expert_idx];
out_pos = cvt_warp_fp16_to_fp4<
Type, CVT_FP4_NUM_THREADS_PER_SF, UE8M0_SF>(
quant_input, SFScaleVal, sf_out);
vLLM 的 MoE format conversion 在进入该内核前分别对 w13_input_scale 和 w2_input_scale 取倒数,生成 a1_gscale 和 a2_gscale。第一次 expert GEMM 前使用 a1_gscale 量化输入;SiLU/mul 后再使用 a2_gscale 量化中间 activation,然后执行第二次 expert GEMM
-
PTQ(Post-Training Quantization): 不重新训练基础模型,通过calibration statistics和量化规则生成低精度checkpoint。本案例由ModelOpt对原始BF16 Gemma 4执行PTQ -
Calibration: 使用代表性样本收集activation range/statistics,以确定运行时不能仅由weight推导的量化参数。本案例中的自然MoE routing决定哪些expert能获得input_scale统计 -
Static weight quantization:Weight在export阶段完成量化,packed data与scale固定写入checkpoint。本案例对应w13/w2 weight、weight_scale和weight_scale_2 -
Dynamic activation quantization: 运行时依据当前activation生成低精度数据及局部scale。本案例在每次expert GEMM前生成FP4 activation和E4M3 block scale -
Granularity: 一个scale覆盖的数据范围,例如per-tensor、per-channel、per-group、per-block或per-expert。NVFP4 micro-block覆盖 16 个元素,global activation scale则细化到expert/projection -
Packing: 将多个低位宽元素编码进byte或machine word;它是存储布局,不是另一种量化算法。本案例中两个E2M1值占一个byte,8 个值由pack_fp4组成一个uint32_t -
Backend / kernel: 消费量化权重与scale并执行GEMM的运行时实现。本案例中CUTLASS执行W4A4,Marlin执行不消费activationinput_scale的路径
矩阵融合
共享输入的多个 projection 可以沿权重的输出维拼接,一次 GEMM 后再切分结果。以 PyTorch 的 [out_features, in_features] 布局为例,融合增加输出宽度,输入维度保持不变。下面用 H 表示 hidden size,I 表示 FFN intermediate size,E 表示 expert 数量;shape 均指量化打包前的逻辑布局
| 模块 | 融合方式 | 融合后的权重或输出 |
|---|---|---|
Attention |
q_proj + k_proj + v_proj → qkv_proj |
权重 [Q_dim + K_dim + V_dim, H] |
Dense FFN |
gate_proj + up_proj → w13 |
w13 [2I,H];down 仍为 w2 [H,I] |
MoE FFN |
每个 expert 分别融合 gate/up |
w13 [E,2I,H]、w2 [E,H,I] |
Gated DeltaNet |
Q/K/V/Z 与 B/A 分成两组投影 |
输出分别切为 [Q,K,V,Z] 与 [B,A] |
QKV projection: Q、K、V 共享 hidden state,因此三个 GEMM 可以合并为一个。GQA 的 K/V head 数少于 Q,输出必须按实际宽度切分
qkv, _ = self.qkv_proj(hidden_states)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
融合只合并输入投影;后续 normalization、RoPE、attention 和 o_proj 仍有各自的计算语义
Gated FFN: Gate 与 up 共享输入,融合为 gate_up_proj / w13。Down projection 依赖 activation/mul 的结果,仍需第二次 GEMM。Gemma4MLP 的主干只需三步
gate_up, _ = self.gate_up_proj(x) # GEMM 1:同时生成 gate 和 up
x = self.act_fn(gate_up) # activation(gate) × up
x, _ = self.down_proj(x) # GEMM 2:down projection
MoE FFN: 每个 expert 使用同样的 w13/w2 结构,但接收的 token 行数 M_e 不同。Backend 根据 routing 结果重排输入,把不同 expert 的矩阵乘组成 Grouped GEMM,减少逐 expert launch 的开销
CUTLASS NVFP4 在两次 GEMM 前分别量化 activation:w13_input_scale 对应 gate/up 的输入,w2_input_scale 对应 SiLU/mul 后的中间结果;取倒数后分别得到 a1_gscale、a2_gscale
token 重排 → a1_gscale 量化 → Grouped w13 GEMM
→ SiLU/mul + a2_gscale 量化 → Grouped w2 GEMM
→ 恢复 token 顺序并按 routing weight 汇总
这里的几种融合发生在不同层次
| 层次 | 本案例中的变化 |
|---|---|
Projection fusion |
Gate/up 权重沿输出维拼成 w13 |
Layout conversion |
Packed weight 与 scale 转成 CUTLASS/Marlin 要求的排列 |
Grouped scheduling |
把不同 expert 的变长 GEMM 一起提交 |
Operator fusion |
把 SiLU、multiply、第二次量化合进同一个 kernel |
Gated DeltaNet 的投影融合: Qwen3.5 MoE 中,GDN 负责序列混合,MoE 负责 FFN。vLLM 将 checkpoint 的 in_proj_qkv 与 in_proj_z 合为 in_proj_qkvz,将 in_proj_b 与 in_proj_a 合为 in_proj_ba,运行时执行两个输入投影 GEMM
mixed_qkvz, _ = self.in_proj_qkvz(hidden_states)
ba, _ = self.in_proj_ba(hidden_states)
QKVZ 的四段输出宽度为 key_dim、key_dim、value_dim、value_dim;B/A 的宽度均为 num_v_heads。Q/K/V 随后进入 convolution 和 recurrent update,Z 用于输出门控,B/A 控制状态更新。这些后续算子与投影 GEMM 分开,使用低精度 projection 不会自动改变它们的数值格式
Kernel Backend
vLLM 按算子分别选择后端。本案例的故障发生在 MoE expert,直接相关的配置是 --moe-backend
| 配置 | 执行范围 |
|---|---|
--linear-backend |
普通量化 Linear,如 qkv_proj、o_proj、Dense FFN |
--moe-backend |
MoE 的 token 重排、w13/w2 expert GEMM、activation 与加权汇总 |
后端选择原则
- 量化格式确定候选集。
NVFP4 Linear与MoE使用独立的selector,各自维护候选实现 auto按优先级选择。 依次检查GPU架构、dtype、shape和并行配置,使用首个满足条件的实现- 显式配置限制候选范围。
MoE校验指定后端,不兼容时通常报错。Linear若在指定后端中找不到该层类型的实现,会告警并对该层恢复自动选择
选定后端后,vLLM 将 packed weight 与 scale 转成该实现要求的 layout,再交给具体 kernel
量化格式 → Linear / MoE 候选集 → 配置与支持条件检查 → layout conversion → kernel
本案例:CUTLASS 与 Marlin
MoE 配置 |
Activation 路径 |
缺失 input_scale 的影响 |
|---|---|---|
--moe-backend cutlass |
W4A4:两次 expert GEMM 前均量化为 FP4 |
读取缺项 expert 的 scale 时,错误数值进入量化与 GEMM |
--moe-backend marlin |
W4A16:保持 BF16/FP16 |
不消费 activation input_scale,避开本次缺项 |
cutlass 对应内部名称 VLLM_CUTLASS,实际 expert class 是 CutlassExpertsFp4。Format conversion 对 w13_input_scale、w2_input_scale 取倒数,生成两次 activation quantization 使用的 a1_gscale、a2_gscale。Marlin 则在转换时将 activation scale 置为 None,使用 weight-only 路径