Skip to content

[Fix] compressed-tensors: Serve split-expert MXFP4 MoE via FP4 Marlin - #41289

Merged
BBuf merged 10 commits into
sgl-project:mainfrom
jlqibm:fix/compressed-tensors-mxfp4-moe
Oct 28, 2026
Merged

BBuf merged 10 commits into
sgl-project:mainfrom
jlqibm:fix/compressed-tensors-mxfp4-moe

Conversation

@jlqibm

@jlqibm jlqibm commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Stacks on #41288 (fix/compressed-tensors-mxfp4-marlin), which fixed MXFP4 dense
linear
. This fixes MXFP4 MoE.

A compressed-tensors mxfp4-pack-quantized MoE checkpoint whose experts are
stored split per expert (experts.{e}.{gate,up,down}_proj.*, as emitted by
llmcompressor) cannot load today. The A/B control, same node and same venv:

The cause is a layout mismatch, not a missing kernel. Mxfp4MoEMethod and
Mxfp4MarlinMoEMethod handle the fused GPT-OSS layout and register
w13/w2_weight; a split-expert checkpoint's per-expert tensors have no
destination there.

Modifications

Add CompressedTensorsW4A16Mxfp4MoE, a weight-only MXFP4 MoE scheme registering
the _packed parameter names and routing to the FP4 Marlin MoE runner. It serves
both mxfp4a16 and mxfp4 w4a4; the latter's activation quantization is
dropped, since the Marlin MoE kernel is weight-only. A w4a4 checkpoint still
ships input global scales and the split-expert loader raises KeyError on any
tensor with no registered destination, so they are registered and only then
dropped in process_weights_after_loading.

Unlike NVFP4 there is no weight_global_scale, so the compressed-tensors
reciprocal convention (CT stores 1/scale where Marlin wants scale) does not
arise here.

check_moe_marlin_supports_layer is deliberately not called: it tests
group_size against the GPTQ/AWQ set and reports on int4 zero-point parameters
this layout does not have. The shape constraints it would have covered
(hidden_size % 128 == 0, intermediate_size_per_partition % 64 == 0) are
validated explicitly instead.

How the two layouts are told apart

They cannot be told apart from the config. GraniteMoe and Kimi-K3 ship
indistinguishable mxfp4-pack-quantized configs — same quant_method, same
format, config_groups targeting ["Linear"] in both, both using
make_expert_params_mapping. The real difference lives in the model loader:
Kimi-K3 renames weight_packed→weight and substitutes Mxfp4Config before
building its experts.

So the selection is an explicit opt-out rather than config sniffing: FusedMoE
gains a serves_fused_mxfp4: bool = True constructor parameter, and
granitemoe.py passes False. It must be a constructor parameter, not a
post-construction attribute, because get_quant_method runs inside
FusedMoE.__init__.

Every existing fused MXFP4 MoE keeps the default and is unaffected. That is
asserted in both directions by a new test, since a silent kernel change on the
GPT-OSS path would be the worst failure mode here.

Accuracy Tests

Granite MoE (GraniteMoeForCausalLM), H100 80GB, all coherent text:

model TP mxfp4 mxfp4a16 nvfp4 nvfp4a16
20b 1 OK OK OK OK
20b 2 OK OK OK —
20b 4 OK OK OK —
120b 4 OK OK — —

13 [OK], rc=0, 0 tensors dropped. 120b is independently valuable: 128 experts
vs 56, hidden 3072 vs 2048.

18 CPU scheme-selection tests + 8 subtests, covering the MXFP4/NVFP4 predicate
boundary (both are FP4 group quant; only group_size and strategy separate
them, so a widened predicate would silently steal the other's checkpoints), the
registered-parameter contract against the checkpoint's names/shapes/dtypes, and
the fused-bypass gate in both directions.

Dense MXFP4 output is byte-identical to PR 2's, confirming the MoE scheme does
not perturb the dense path.

The fused GPT-OSS MXFP4 MoE path is covered by the CPU gate test rather than
end-to-end: GPT-OSS uses native Mxfp4Config, so it never enters
CompressedTensorsConfig.get_quant_method, and no compressed-tensors fused MXFP4
MoE checkpoint was available to test against. The gate test asserts the dispatch
branch directly, which is a stronger check than inferring it from generated text.

Speed Tests and Profiling

No benchmark run. Weight-only via the existing FP4 Marlin MoE runner — no new
kernel, and activations stay bf16. get_min_capability() is 80 and the routing
is capability-independent, so Blackwell takes the same MoE path as Hopper. The one
asymmetry: an NVFP4 w4a4 MoE checkpoint on SM100 still keeps the native
CompressedTensorsW4A4Nvfp4MoE scheme (capability 100); the weight-only fallback
must not lower that gate, which is why they are separate classes and is asserted
by test_nvfp4_w4a4_keeps_native_scheme_on_blackwell.

Checklist

🤖 Generated with Claude Code

Review and Merge Process

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
    • Common commands include /tag-and-rerun-ci, /tag-run-ci-label, /rerun-failed-ci
  4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.

CI States

Latest PR Test (Base): ✅ Run #40493897581
Latest PR Test (Extra): ❌ Run #40493897438
Latest PR Test (AMD ROCm 10): ⏳ Run #40493897389

jlqibm and others added 3 commits October 13, 2026 22:41
The dense FP4 Marlin kernel only emitted group_blocks == 1, so a group_size 32
call computed group_blocks == 2, matched no branch, and fell through to the no-op
MarlinDefault. It also could not decode E8M0 scales: it called the
unparameterized dequant_fp8_scales overload, which is the E4M3 decoder, and it
dereferenced the global scale unconditionally although MXFP4 carries none.

Thread s_type_id through the dense template, deriving S_TYPE inside _GET_IF from
W_TYPE and GROUP_BLOCKS so no call site changes, and add an MXFP4 kernel family
guarded on bfloat16. That guard is load-bearing rather than stylistic:
dequant_fp8_scales<half2, kFE8M0fnu> is declared but never defined, so an fp16
instantiation fails to compile.

Gate the global-scale requirement on group_size == 16 and reject fp16 MXFP4 on
the host, which would otherwise reach MarlinDefault and return silent garbage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MXFP4 dense linears had no path at all: they fell through to the NVFP4 branches
and were rejected for an unsupported group_size. Add a weight-only scheme over
the FP4 Marlin kernel, which needs no FP4 tensor cores and so runs from SM80.

Unlike NVFP4 this is not a pre-Blackwell fallback. No native MXFP4 dense-linear
kernel exists on any architecture, so both mxfp4a16 and the w4a4 variant are
served weight-only everywhere, and w4a4 warns that its activation quantization is
ignored. The two variants' weight blocks are byte-identical, so one scheme covers
both and the predicate keys on the weights alone.

Scales are raw E8M0 exponent bytes, so they are reinterpreted rather than
converted -- a conversion would read exponents as small integers -- and BF16 is
the only supported activation dtype, matching the kernel's instantiation.

Also add mxfp4_pack_quantized to the activation-quantization formats. The
input_activations parse is gated on that list, so a w4a4 MXFP4 checkpoint had its
input_activations silently discarded at parse time.

Verified on H100 (SM90) against granite-4.2-30b mxfp4a16 and mxfp4, both
generating coherent text; nvfp4, nvfp4a16, int4 pack-quantized and fp8 all
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow the base PR's consolidation: MXFP4 linear selection becomes rows in the
shared selection table, its weight-creation checks join the per-format
subTests, and its numeric cases join the shared linear dequant-reference test,
replacing test_compressed_tensors_mxfp4_linear_numeric.py. The shared file
already runs on SM100 and is registered on 4-gpu-b200, which is what the
dropped standalone SM100 commit added for the MXFP4 file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jlqibm and others added 3 commits October 13, 2026 22:41
A compressed-tensors mxfp4 MoE checkpoint whose experts are stored one
tensor per expert per projection could not load. `_is_mxfp4_moe` diverted
every MXFP4 MoE to `Mxfp4MoEMethod`, which registers the fused
`w13_weight`/`w2_weight` that the GPT-OSS layout uses, so a split-expert
checkpoint died with
`KeyError: model.layers.0.block_sparse_moe.experts.w2_weight_packed`.

Add `CompressedTensorsW4A16Mxfp4MoE`, mirroring the NVFP4 weight-only MoE
scheme with group_size 32, uint8 E8M0 scales, and no weight global scale.
The Marlin prep math already exists, so the scheme only registers the
parameters and bridges the `_packed` names.

Gate the `Mxfp4MoEMethod` bypass on the new `FusedMoE.serves_fused_mxfp4`
flag rather than on the quant config: Kimi-K3 and GraniteMoe ship
indistinguishable `mxfp4-pack-quantized` configs and differ only in whether
the model's own loader renames `weight_packed` to `weight`. GraniteMoe,
which does not, opts out.

MXFP4 w4a4 is served weight-only on every capability since there is no
FP4-activation MoE kernel in tree; the input global scales are registered so
the split-expert loader finds a destination, then dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Carries the sgl-project#40022 review fix to the MXFP4 MoE path. w13 was always
allocated with two shards, so a non-gated checkpoint (up projection only)
failed to load on a shape mismatch. Size w13 and its input scales from
moe_runner_config.is_gated, and teach prepare_moe_mxfp4_layer_for_marlin the
shard count. Layers without a runner config -- the fused GPT-OSS callers --
keep the gated default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow the base PR's consolidation: MXFP4 MoE selection becomes rows in the
shared selection table (which also covers the MXFP4/NVFP4 predicates not
overlapping), its weight-creation checks -- uint8 E8M0 scales, no weight
global scale, input-scale destination, non-gated shapes -- join the
per-format subTests, and the fused-layout bypass test moves over unchanged.

The MoE numeric test runs MXFP4 alongside NVFP4: checkpoint-format experts
with raw E8M0 scale bytes, process_weights_after_loading, then the Marlin MoE
forward against experts dequantized in PyTorch, for gated silu and non-gated
relu2 and silu. Without the is_gated fix the two non-gated MXFP4 cases fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BBuf

BBuf commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

This branch includes the dense MXFP4 bias issue described in #41288 (comment). I reproduced the same Python-side mismatch at this PR's head, 8ba1730d08dda0bea18a29477db5f243d0482826: preparation permutes bias, but the dense apply path adds it to normal-order GEMM output.

Please carry the fix and bias regression coverage from #41288 into this PR as well. The detailed reproduction and its CPU-only validation scope are in the linked comment.

@BBuf BBuf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving the MoE changes. Please also bring in the dense MXFP4 bias fix and regression coverage from #41288, as noted in my earlier comment.

I'll trigger run-ci after the fix is pushed. Holding off on CI for now.

BBuf and others added 2 commits October 19, 2026 12:43
apply_fp4_marlin_linear adds the bias after the GEMM, since sglang's dense
gptq_marlin_gemm takes no bias, but prepare_{nv,mx}fp4_layer_for_marlin
padded the bias to the Marlin N and permuted it into Marlin's scale order
(a pattern from vLLM, whose kernel applies bias in-kernel). Each output
channel got another channel's bias, and with skip_bias_add the linear
returned the padded, permuted bias to its caller.

Leave layer.bias untouched at load and add it after cropping the output to
the logical N. The MoE Marlin path is unchanged: its kernel takes the bias
in-kernel, so permuting it there is correct.

test_marlin_linear_bias covers NVFP4 and MXFP4 with aligned (192) and padded
(160 -> 192) N, both bias added by the scheme and skip_bias_add, plus the
stored bias. Without the fix all four cases fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jlqibm jlqibm changed the title Fix/compressed tensors mxfp4 moe [Fix] compressed-tensors: Serve split-expert MXFP4 MoE via FP4 Marlin Oct 20, 2026
@jlqibm

jlqibm commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@BBuf the bias fix for #41288 is merged here as well. Can you please issue run-ci?

@jlqibm

jlqibm commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

@BBuf can you please trigger run-ci? Fixes are merged.

@BBuf BBuf added the run-ci CI: run the baseline test suite on this PR label Oct 27, 2026
@BBuf
BBuf merged commit bb4bb01 into sgl-project:main Oct 28, 2026
182 of 191 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blackwell SM100/SM120 documentation Improvements or additions to documentation jit-kernel run-ci CI: run the baseline test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants