Skip to content

GDN core/z stay f32 on every MoE checkpoint while vLLM keeps both at the bf16 model dtype: 2x traffic on the two largest per-layer GDN activations, on the store and again on the load #1168

Description

@localai-bot

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-873core_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-860mixed_qkvz, _ = self.in_proj_qkvz(hidden_states) then
    mixed_qkv, z = mixed_qkvz.split(...). z is a view of the bf16 linear output.
  • :459-465self.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-536RMSNormGated(..., dtype=config.torch_dtype), i.e. the model dtype, explicitly.
  • python/sglang/srt/models/qwen3_5.py:281 and :515-527z 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions