Found while auditing the GDN dtype polarity on the MoE arms at dd8a3b0e1. Every
anchor below was read at that SHA.
The defect
src/vllm/model_executor/models/qwen3_5.cpp:3190:
DType GdnOutDType(bool dense_model) {
static const int override = [] {
const char* e = std::getenv("VT_GDN_OUT_BF16");
if (e == nullptr) return -1;
return e[0] == '0' ? 0 : 1;
}();
const bool bf16 = override >= 0 ? override != 0 : dense_model;
return bf16 ? DType::kBF16 : DType::kF32;
}
All three call sites pass cfg.num_experts == 0 — :3749 (dense GDN block),
:4155 (paged GDN block) and :4386 (paged GDN block with the packed-decode
decision). The resolved default is therefore bf16 on a dense checkpoint and f32
on every MoE checkpoint.
outdt is the dtype of exactly two tensors, and they are the two largest
per-layer GDN activations:
| tensor |
shape |
allocated at |
dcore, the GDN recurrence output |
[T, Hv, Dv] |
:3807, :4253, :4265, :4285, :4587 |
z, the output gate |
[T, value_dim] |
ProjectGdnQkvz :3584, :3688-3690 |
Each is written once by the recurrence or the input projection and read once by
the gated RMSNorm (vt::RmsNormGated :4778, or the fp8-fused
vt::RmsNormGatedQuantFp8 :4768 on the 35B's W8A8 out_proj). The gated norm's
weight follows them: dnw is ResidentWeightF32 at f32 and ResidentWeight
(native bf16) at bf16 (:3817-3818, :4301-4302, :4733-4734). So an f32
outdt doubles the bytes on the store and again on the load, on both tensors.
vLLM keeps both at the bf16 model dtype
Primary oracle, pin 5559679229bc961848b121ccdeaa8fa5d79bec98
(.agents/upstream-sync.md), vllm/model_executor/layers/mamba/gdn/qwen_gdn_linear_attn.py:
:870-873 — core_attn_out = torch.zeros((num_tokens, num_v_heads // tp, head_v_dim), dtype=hidden_states.dtype, ...). That is the model dtype, bf16. The
ROCm arm does the same with torch.empty at :805-808.
:843, :859-860 — mixed_qkvz, _ = self.in_proj_qkvz(hidden_states) then
mixed_qkv, z = mixed_qkvz.split(...). z is a view of the bf16 linear output.
:459-465 — self.norm = RMSNormGated(head_v_dim, ...) is constructed with no
dtype override, so its weight is the default (bf16) parameter dtype.
None of these branches on the model being dense or MoE. vLLM resolves one
model dtype and every layer inherits it. Our f32 is the deviation.
SGLang keeps both at bf16 as well
Secondary oracle sglang @ f63458b5be:
python/sglang/srt/models/qwen3_5.py:522-536 — RMSNormGated(..., dtype=config.torch_dtype), i.e. the model dtype, explicitly.
python/sglang/srt/models/qwen3_5.py:281 and :515-527 — z is a slice of the
bf16 projected_states_qkvz.
python/sglang/srt/layers/attention/linear/kernels/gdn_triton.py:79-83 —
out = mixed_qkv.new_empty(B, 1, num_v_heads, head_v_dim), i.e. the packed
decode output inherits mixed_qkv's bf16 dtype.
So this is not an "SGLang does it differently" case. Both references agree, and
the primary one is the binding reference.
Why nothing detected it
CLAUDE.md, "Inherit vLLM defaults": "A token gate cannot detect a dtype that is
too wide. The tokens still match, and the goldens still pass, although the path
moves twice the bytes." test_qwen36_paged_engine passes 315/315 with dcore
and z at f32 precisely because f32 is the more precise deviation.
The code already documented this and deferred it
qwen3_5.cpp:3172-3189 (the comment above GdnOutDType) ends:
This is correctness-significant for the 27B: with the repaired full NVFP4
tactic stack, f32 core/z takes the alternate whitespace near-tie branch while
bf16 reproduces native vLLM 16/16. Keep every unmeasured 35B arm, including
GGUF, on its prior f32 default; the explicit env override remains available
for its later independently gated campaign.
That campaign was never opened. This issue opens it. The record of the 27B flip
is .agents/specs/nvfp4-small-m-dispatch.md:706-711 (KERNEL-GDN-AOT-BF16).
Scope, stated honestly
The affected arms are the MoE checkpoints:
| checkpoint |
linear V-heads |
gateable on this hardware |
nvidia/Qwen3.6-35B-A3B-NVFP4 |
32 |
yes |
Qwen/Qwen3.8-2.4T-A95B |
128 |
no — ~4.8 TB bf16 against 128 GB unified memory |
The dense Qwen3.8-27B is already bf16 here and gains nothing. Any claim that
this issue moves the 27B is wrong.
A coupled term, and what it does NOT unlock on its own
detail::ShouldUsePackedGdnDecode (:76-84) carries an e.dense_model term,
populated as cfg.num_experts == 0 at :4406. It entered at f344decf4
("feat(gdn): dispatch exact packed decode"), whose body describes it as one of the
"real-model safety gates" — a day-one staging gate, not a measured decision.
Neither reference gates packed decode on model shape: SGLang keys only on the
platform (gdn_triton.py:43, supports_packed_decode = not is_cpu() and not is_npu()) and vLLM's VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE defaults True
model-agnostically (vllm/envs.py:124).
Removing dense_model alone is inert: GdnPackedDecodeDTypesCompatible
(:115-122) pins mixed_qkv, ba_out and core_out to bf16, and core_out is
outdt, so a MoE checkpoint already fails that term. The dense_model term is
redundant behind the dtype and should be removed with the dtype change, not
before it.
Removing it is also not sufficient, and this correction matters: see the
companion issue on the merged in_proj_ba owner. has_packed_ba is false on every
MoE checkpoint too.
VT_GDN_OUT_BF16 already exists
The override at :3185-3189 makes this a same-binary A/B, so the measurement is
cheap. VT_GDN_OUT_BF16=1 on a 35B run is the experiment.
Owned by row GDN-MOE-BF16-OUT, spec .agents/specs/gdn-moe-bf16-out.md.
Found while auditing the GDN dtype polarity on the MoE arms at
dd8a3b0e1. Everyanchor below was read at that SHA.
The defect
src/vllm/model_executor/models/qwen3_5.cpp:3190:All three call sites pass
cfg.num_experts == 0—:3749(dense GDN block),:4155(paged GDN block) and:4386(paged GDN block with the packed-decodedecision). The resolved default is therefore bf16 on a dense checkpoint and f32
on every MoE checkpoint.
outdtis the dtype of exactly two tensors, and they are the two largestper-layer GDN activations:
dcore, the GDN recurrence output[T, Hv, Dv]:3807,:4253,:4265,:4285,:4587z, the output gate[T, value_dim]ProjectGdnQkvz:3584,:3688-3690Each is written once by the recurrence or the input projection and read once by
the gated RMSNorm (
vt::RmsNormGated:4778, or the fp8-fusedvt::RmsNormGatedQuantFp8:4768on the 35B's W8A8out_proj). The gated norm'sweight follows them:
dnwisResidentWeightF32at f32 andResidentWeight(native bf16) at bf16 (
:3817-3818,:4301-4302,:4733-4734). So an f32outdtdoubles the bytes on the store and again on the load, on both tensors.vLLM keeps both at the bf16 model dtype
Primary oracle, pin
5559679229bc961848b121ccdeaa8fa5d79bec98(
.agents/upstream-sync.md),vllm/model_executor/layers/mamba/gdn/qwen_gdn_linear_attn.py::870-873—core_attn_out = torch.zeros((num_tokens, num_v_heads // tp, head_v_dim), dtype=hidden_states.dtype, ...). That is the model dtype, bf16. TheROCm arm does the same with
torch.emptyat:805-808.:843,:859-860—mixed_qkvz, _ = self.in_proj_qkvz(hidden_states)thenmixed_qkv, z = mixed_qkvz.split(...).zis a view of the bf16 linear output.:459-465—self.norm = RMSNormGated(head_v_dim, ...)is constructed with nodtype override, so its weight is the default (bf16) parameter dtype.
None of these branches on the model being dense or MoE. vLLM resolves one
model dtype and every layer inherits it. Our f32 is the deviation.
SGLang keeps both at bf16 as well
Secondary oracle
sglang@f63458b5be:python/sglang/srt/models/qwen3_5.py:522-536—RMSNormGated(..., dtype=config.torch_dtype), i.e. the model dtype, explicitly.python/sglang/srt/models/qwen3_5.py:281and:515-527—zis a slice of thebf16
projected_states_qkvz.python/sglang/srt/layers/attention/linear/kernels/gdn_triton.py:79-83—out = mixed_qkv.new_empty(B, 1, num_v_heads, head_v_dim), i.e. the packeddecode output inherits
mixed_qkv's bf16 dtype.So this is not an "SGLang does it differently" case. Both references agree, and
the primary one is the binding reference.
Why nothing detected it
CLAUDE.md, "Inherit vLLM defaults": "A token gate cannot detect a dtype that is
too wide. The tokens still match, and the goldens still pass, although the path
moves twice the bytes."
test_qwen36_paged_enginepasses 315/315 withdcoreand
zat f32 precisely because f32 is the more precise deviation.The code already documented this and deferred it
qwen3_5.cpp:3172-3189(the comment aboveGdnOutDType) ends:That campaign was never opened. This issue opens it. The record of the 27B flip
is
.agents/specs/nvfp4-small-m-dispatch.md:706-711(KERNEL-GDN-AOT-BF16).Scope, stated honestly
The affected arms are the MoE checkpoints:
nvidia/Qwen3.6-35B-A3B-NVFP4Qwen/Qwen3.8-2.4T-A95BThe dense
Qwen3.8-27Bis already bf16 here and gains nothing. Any claim thatthis issue moves the 27B is wrong.
A coupled term, and what it does NOT unlock on its own
detail::ShouldUsePackedGdnDecode(:76-84) carries ane.dense_modelterm,populated as
cfg.num_experts == 0at:4406. It entered atf344decf4("feat(gdn): dispatch exact packed decode"), whose body describes it as one of the
"real-model safety gates" — a day-one staging gate, not a measured decision.
Neither reference gates packed decode on model shape: SGLang keys only on the
platform (
gdn_triton.py:43,supports_packed_decode = not is_cpu() and not is_npu()) and vLLM'sVLLM_ENABLE_FLA_PACKED_RECURRENT_DECODEdefaultsTruemodel-agnostically (
vllm/envs.py:124).Removing
dense_modelalone is inert:GdnPackedDecodeDTypesCompatible(
:115-122) pinsmixed_qkv,ba_outandcore_outto bf16, andcore_outisoutdt, so a MoE checkpoint already fails that term. Thedense_modelterm isredundant behind the dtype and should be removed with the dtype change, not
before it.
Removing it is also not sufficient, and this correction matters: see the
companion issue on the merged
in_proj_baowner.has_packed_bais false on everyMoE checkpoint too.
VT_GDN_OUT_BF16already existsThe override at
:3185-3189makes this a same-binary A/B, so the measurement ischeap.
VT_GDN_OUT_BF16=1on a 35B run is the experiment.Owned by row
GDN-MOE-BF16-OUT, spec.agents/specs/gdn-moe-bf16-out.md.