Repository navigation
[OMNIML-6073] [3/21] Quantize fused experts under grouped_mm, ungated and transposed layouts - #2745
shengliangxu wants to merge 17 commits into
Conversation
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
6e6ac9c to
bad2de9
Compare
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## shengliangx/hfep-7-amax-sync #2745 +/- ##
================================================================
+ Coverage 79.61% 79.63% +0.02%
================================================================
Files 660 660
Lines 74774 74906 +132
================================================================
+ Hits 59528 59653 +125
- Misses 15246 15253 +7
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
/ok to test bad2de9 |
bad2de9 to
16e5c17
Compare
|
/ok to test 16e5c17 |
5c0d5fa to
698f459
Compare
|
/ok to test 698f459 |
698f459 to
eaa41c2
Compare
|
/ok to test eaa41c2 |
eaa41c2 to
3ff1d97
Compare
|
/ok to test f2be168 |
|
/ok to test 233d659 |
|
/ok to test 2a18b13 |
|
/ok to test 6c1e2d3 |
|
/ok to test e322d20 |
|
/ok to test b452b7b |
|
/ok to test a38ba3e |
|
/ok to test 99ff96e |
|
/ok to test 220ad2d |
|
/ok to test 0124a4e |
|
/ok to test efce4ee |
|
/ok to test 4038ec7 |
The fused-experts wrapper only handled one shape of MoE: gated experts (gate_up_proj/down_proj) in the (num_experts, out, in) layout, calibrated through the eager backend where HF calls the expert matmul once per expert. Several real architectures are not that shape, and each failed differently -- usually by silently collecting no amax rather than by raising. grouped_mm. transformers' default experts implementation when none is requested is grouped_mm, not eager, and it passes one expert-sorted batch per projection instead of one call per expert. The eager interception has nothing to hook, so calibration sees no per-expert activations. The wrapper now intercepts the grouped path directly and stashes the cumulative token offsets, and the local-Hessian capture splits that batch back into per-expert chunks using them. Rows past the last offset are EP dispatch padding and belong to no expert. Backends that cannot be intercepted at all (batched_mm, the opaque sonicmoe kernel) are normalized onto grouped_mm before calibration rather than quietly producing an uncalibrated model. Ungated experts. NemotronH has a single up_proj with no gate/up split, so every reader that assumed "gate_up_proj" found nothing. The first projection's name is a class attribute the subclass overrides, the quantizer attribute names derive from it, and _is_gated = False tells the export not to split the first projection into gate|up halves. A split gated layout (separate 3-D gate_proj and up_proj) is not claimed as ungated. Transposed experts. Llama-4 stores experts as (in, out). The packers halve the LAST dim, so a transposed weight would be packed along the wrong axis. This raises rather than exporting a wrongly-packed checkpoint. Under EP each rank holds only its slice of experts, so expert weight quantizers are indexed by LOCAL position; global indices would attach the wrong quantizer to every expert but rank 0's. Finally, calibrate() fails loudly when a forward loop ran but a fused-experts module was never intercepted: every failure above presented identically -- an export that looks complete and is quietly unquantized -- and the whole class is cheap to detect at the point where it is still recoverable. The check is gated on the loop actually having run, since max_calibrate may skip it when no enabled quantizer needs activation statistics. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
normalize_experts_impl_on_the_fly replaced main's eager-forcing, and its docstring said the unset default "dispatches to the model's eager F.linear forward". transformers never leaves it unset on a built model: from_pretrained and the model constructor both resolve an unrequested backend to grouped_mm wherever that can dispatch. So a fused-experts model now calibrates on the grouped path where main calibrated on eager. The exported checkpoint shows it. A single-GPU Qwen3-30B-A3B export from this stack matches main's in every weight and weight scale, but 4647 activation scales differ, by up to 10.2% (an expert down_proj input scale), across 45 of 48 layers. A dense Qwen2.5-0.5B export is byte-identical to main's, which points at the experts' forward rather than the quantizers; an eager rerun has not isolated it yet. Correct the docstring, and record the change in the changelog along with how to calibrate on eager instead (experts_implementation="eager"). Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The previous commit left the backend attribution open. An eager rerun has since isolated it: a single-GPU Qwen3-30B-A3B export loaded with experts_implementation="eager" matches main's byte for byte -- all 74,835 tensors for NVFP4 and all 56,211 for FP8, non-tensor files included -- where grouped_mm differs in the same 4,647 scales in both recipes. Main's own single-GPU export reproduces byte for byte, so that zero is exact. So the scale changes come from the experts backend alone, and the way back the changelog offers restores main's checkpoint exactly. Say so. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Nothing tested that register_fused_experts_on_the_fly refuses a transposed experts container, or that it leaves one with its own quantized wrapper alone. The guard's comment also pointed at a _reject_transposed_fused_experts that does not exist, and _fused_experts_wrapper claimed to return None for GptOss and Llama-4, when it matches them by shape and relies on their registrations to keep them out. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… experts Three ways the fused-experts changes broke models that worked on main: - GPT-OSS: its own wrapper quantizes down_proj's input by intercepting the eager forward's bmm/matmul. Main forced every fused-experts model onto eager; the series kept grouped_mm, where that interception never fires, so down_proj's input quantizer was never calibrated and its exported scale fell back to gate_up_proj's. A model with any fused-experts class that has its own (non-_QuantFusedExperts) wrapper keeps main's forced eager again. - The interception check raised for an experts module no calibration token reached, e.g. Ernie-4.5-VL's vision experts under text-only calibration, which main exported with weight-derived scales. It now counts the module's forwards and fails only when the forward ran past both hooks. - A transposed fused-experts class without its own wrapper made quantization raise. It is now left unquantized with a warning: still never exported wrong, without aborting the rest of the model. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
transformers 5.18's grouped_mm_experts_forward reads _is_expert_parallel, which its @use_experts_implementation sets on real experts classes. The test's synthetic experts call the forward directly, so they failed with an AttributeError on CI's latest transformers -- and the failures left state behind that broke later tests in the same session. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Under expert parallelism transformers keeps fused expert weights as DTensors sharded over the experts, with num_experts set to the local count. Outside the forward, max calibration's weight pass and fold_weight sliced them by local expert index, which on a DTensor reads the global expert; with the dense part FSDP2-sharded, FSDP2's weight-access window had first gathered every expert onto every rank. Either way every rank but rank 0 calibrated each expert as the max with rank 0's expert of the same local index, and folded into the wrong experts: expert weight scales came out too large. Index the local shard of an expert-sharded DTensor, and keep DTensors that do not live on FSDP's mesh out of FSDP2's weight-access window. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With fake quantizers inserted, fused experts split over two ranks must train through the torch_a2a forward like the whole quantized module. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
main opened 0.49.0 for what merges after 0.48.0's cut, which this PR does. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Each of them spawned two ranks, about 14 s of the linux unit job apiece. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The torch_a2a expert-parallel forward runs its expert matmuls through _grouped_linear as the grouped_mm backend does, but _running_grouped_mm recognized only grouped_mm. local_hessian therefore took the eager per-call hook, which never moved past expert 0: expert 0's Hessian got every expert's rows and the others fell back to plain MSE. The grouped hook also reads each weight the way the quantizers index it, rank-local: indexing an expert-parallel DTensor names a global expert and redistributes it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…r EP Hook registration for local Hessian and imatrix handed on_weight weight[0] of each fused-experts projection. Under expert parallelism the fused weight is a DTensor sharded over the experts, so indexing it named a global expert and all-gathered every expert of the projection to report one expert's shape, then paired that global expert with this rank's first quantizer. It now reads this rank's experts, as the grouped capture hook does. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…eir imports A second copy of the Qwen3-VL section header, constants and helpers sat before the NemotronH tests, shadowed by the original block the Qwen3-VL tests use. The imports the expert-parallel and grouped tests made inside functions move to module scope: none is optional at the transformers 5.5 and torch 2.8 floors, and a missing name now fails at collection instead of inside a pooled worker. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
- The ungated synthetic classes and TestNemotronHUngatedExperts become a gated/ungated parameter of the grouped_mm passthrough and calibration tests; TestNonGatedFusedExperts keeps the detection and quantizer checks the Nemotron-H class repeated. - The three "leaves a supported value unchanged" tests become one, parametrized over eager, grouped_mm and None. - test_intercept_fires_twice_and_uses_full_3d_weight asserted on a spy's arguments; a missed or wrong interception fails the calibration and local-Hessian tests instead. - test_a_transposed_class_with_its_own_wrapper_is_left_to_it is covered by the real GPT-OSS calibration test. - The QAT-through-torch_a2a job becomes the fake-quantized case of the a2a file's whole-module backward test, and the FSDP-wrapped EP job the fsdp2-dense case of the EP-sharded calibration test. Each regression a dropped test caught fails a remaining test. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Main's native FP8 experts (#2820) route the forward's and calibration's per-expert weights through get_expert_weight, which indexed the fused weight directly. Under expert parallelism that weight is a DTensor sharded over the experts, so indexing it read the global expert: from rank 1 on, another rank's. Index this module's own experts, as the rest of the wrapper does, for the weight and for the FP8 experts' scales. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Main's GPTQ for HF fused-MoE experts feeds each expert's Hessian from the shared input quantizer, keyed by _current_expert_idx: one call per expert, as the eager loop runs. This stack calibrates fused experts on grouped_mm, where that quantizer runs once per projection on the whole expert-sorted batch, so the first expert took every token and the others none (a tiny Qwen3-MoE: expert 0 got all 64 rows, experts 1-3 none, so they fell back to the plain search). Split the batch by _grouped_offs, as the weight-input hooks already do, and note both dispatches in _fused_expert_projections' docstring. On that Qwen3-MoE every expert now gets the token counts eager calibration gives it. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Under expert parallelism a fused experts' weight is a DTensor sharded over the experts. GPTQ built each expert's helper from weight[idx], where idx is the local index its quantizer has: that names a global expert and gathers every expert's weight, once per local expert. The helper only reads its expert's shape and device, so the values were right, but the gathers cost the memory and time of the whole expert weight each time. Index this rank's experts instead (_local_experts), as the weight-input hooks beside it already do. Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
|
/ok to test aa80229 |
What does this PR do?
Type of change: New feature, with a backward-incompatible change to MoE calibration (see the changelog entry)
The fused-experts wrapper handled one shape of MoE: gated experts (
gate_up_proj/down_proj) in the(num_experts, out, in)layout, calibrated through the eager backend, where Hugging Face calls the expert matmul once per expert. Several real architectures are not that shape. Each failed differently, usually by silently collecting no amax rather than by raising.grouped_mm, not eager, and it passes one expert-sorted batch per projection instead of one call per expert, so the eager interception had nothing to hook. The wrapper now intercepts the grouped path and keeps the cumulative token offsets, and the local-Hessian capture splits the batch back into per-expert chunks with them. Backends that cannot be intercepted (batched_mm, the opaquesonicmoekernel) are moved togrouped_mmbefore calibration rather than quietly producing an uncalibrated model.up_projwith no gate/up split. The first projection's name is now a class attribute the subclass overrides, the quantizer names derive from it, and_is_gated = Falsetells the export not to split it.(num_experts, in, out)) without a wrapper of their own are left unquantized, with a warning. The fused-experts export would split gate and up along the wrong axis for them and write wrong weights; main did so silently. GPT-OSS and Llama-4 keep their own wrappers.fold_weightindexed them by local position too, which on a DTensor reads the global expert; with the dense part FSDP2-sharded, FSDP2's weight-access window had first gathered every expert onto every rank.calibrate()raises when a fused-experts module's forward ran but neither hook fired. Every failure above otherwise looks the same: an export that seems complete and is quietly unquantized. An experts module that no calibration token reached is not an error and falls back to weight-derived scales as on main: Ernie-4.5-VL's vision experts, under text-only calibration.Behavior change. main forced MoE calibration onto eager; transformers now resolves an unrequested backend to
grouped_mm, so fused-experts models calibrate there. A model whose experts class has its own quantized wrapper keeps main's forced eager. GPT-OSS's wrapper, for instance, quantizesdown_proj's input by intercepting the eager forward's matmuls. A single-GPU Qwen3-30B-A3B export matches main in every weight and weight scale, but 4,647 activation scales move, by up to 10.2%. Loading withexperts_implementation="eager"reproduces main's checkpoint byte for byte (all 74,835 tensors for NVFP4 and 56,211 for FP8). The changelog says so. On the grouped path each forward fake-quantizes a projection's experts together, so calibration holds a transient copy of that projection's weights.Usage
Testing
tests/unit/torch/quantization/plugins/test_fused_experts.py(+611 lines), against synthetic and real transformers experts modules:down_proj's input on the eager forward.The last two, and the transposed warning, fail on the code before those fixes.
Expert parallelism, on CPU under gloo:
torch_a2aforward like the whole quantized module: the fake-quantized case oftest_moe_a2a.py's whole-module test (mutation-checked twice).The GPU tests of QAT under expert parallelism, which this PR carried before the series was reordered, are in [10/21], the first PR with an FSDP2-sharded dense part they also run.
Against main, on tiny real models:
down_proj's input amax equal to main's;Local-Hessian calibration captures each expert's own rows under
grouped_mmand under thetorch_a2aexpert-parallel forward ([1/21]); both feed the quantizers one expert-sorted batch.Registering those input hooks under expert parallelism reads each rank's own experts: indexing the expert-sharded weight all-gathered every expert of the projection. A two-rank test checks that registration issues no collective.
Near-duplicate tests are merged into parametrized ones, tests a remaining test already covers are dropped, and the two expert-parallel jobs become cases of the tests they repeated. Each drop is checked by mutation.
Locally on this revision, the reordered series rebased on main 264fd26: the full unit suite on transformers 5.18, and on 5.5 apart from the tests whose ONNX, TensorRT or diffusers packages that environment lacks; CI's transformers-free unit lane; pre-commit over the whole PR against main; and the Sphinx docs build. The same at every PR of the stack.
The GPU results here were measured on earlier revisions of this branch; this revision's GPU tests run in CI only.
Byte-for-byte checkpoint comparison against main on one GPU: Qwen3-30B-A3B, eager and grouped_mm, NVFP4 and FP8. Qwen2.5-0.5B (dense) is byte-identical.
Before your PR is "Ready for review"
grouped_mm, so some activation scales differ from earlier releases;experts_implementation="eager"restores the earlier checkpoint exactly.CONTRIBUTING.md: N/AAdditional Information
Part 3 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after [2/21].
Summary by CodeRabbit
Compatibility
experts_implementation="eager"to restore the previous calibration setup.Bug Fixes