Skip to content

✨ [llm][npu][quant] Add W8A8 MXFP8 quantization for Qwen3 MoE on Ascend NPU - #30768

Merged
sglang-npu-bot merged 25 commits into
sgl-project:mainfrom
TallMessiWu:junlin_qwen3_moe_w8a8
Jul 29, 2026
Merged

sglang-npu-bot merged 25 commits into
sgl-project:mainfrom
TallMessiWu:junlin_qwen3_moe_w8a8

Conversation

@TallMessiWu

@TallMessiWu TallMessiWu commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds W8A8 MXFP8 quantization (8-bit weights + 8-bit activations, block_size = 32) for Qwen3 / Qwen3.5 MoE (FusedMoE) LLM models on Ascend NPU, continuing the NPU quantization work tracked in #21584. Both online (--quantization mxfp8) and offline (msmodelslim, flagless — selected by the checkpoint's quant_model_description.json) paths are supported and share the same fused-experts kernel. Requires Ascend A5 (Ascend 950) — the fused MoE MX kernels (npu_grouped_matmul_swiglu_quant_v2, npu_dynamic_mx_quant) are A5-only.

#23795 (dense W4A4) has landed and this branch is merged up to main, so the diff is now the MoE MXFP8 delta only. It reuses the merged dense MXFP8 infrastructure — NPUMXFP8LinearMethod, the Fp8Config NPU dispatch, and ModelSlimMXFP8Scheme (from #22352).

Online quantization (--quantization mxfp8)

  • On the Ascend NPU backend, Fp8Config.get_quant_method() dispatches FusedMoE layers to the new NPUMXFP8OnlineMoEMethod (the isinstance(layer, FusedMoE) branch, mirroring the existing dense NPUMXFP8LinearMethod linear branch). It subclasses UnquantizedFusedMoEMethod and overrides only create_moe_runner, to attach the MXFP8 per-gmm kernels — weight creation, weight post-processing and the forward pass are the unquantized Ascend ones. On GPU the FusedMoE branch is untouched.
  • BF16/FP16 expert weights w13/w2 are quantised once at load via npu_dynamic_mx_quant(dst_type = torch_npu.float8_e4m3fn) — a 3D [E, N, K] input is accepted directly (no per-expert loop) — into float8_e4m3fn weights + UE8M0 (float8_e8m0fnu) block scales, kept as non-contiguous transpose views.
  • Forward: on the ascend_tp dispatcher, npu_moe_init_routing_v2 with quant_mode=3 (count group_list) — the per-token MXFP8 activation quant is fused directly into routing, so the same kernel launch emits the sorted float8_e4m3fn payload and its float8_e8m0fnu block scale, saving a separate npu_dynamic_mx_quant pass — → gmm1 npu_grouped_matmul_swiglu_quant_v2 (gate_up + swiglu + requant fused, cumulative group_list) → gmm2 npu_grouped_matmul (count group_list + explicit group_list_type). The UE8M0 scale dtypes are passed explicitly; the e4m3 x/weight dtypes are left implicit. DeepEP has no MXFP8 dispatch dtype, so on that path the dispatcher keeps sending BF16 and gmm1 quantizes the hidden states itself via npu_dynamic_mx_quant before the fused kernel — both dispatchers reach gmm1 with the same input. Only the ascend_tp path is validated end-to-end on A5.
  • Fused-routing quant optimization: the fused quant_mode=3 scale comes back flat ([N, K/32]) instead of the pair-split [N, K/64, 2] layout gmm1 expects, so _normalize_mxfp_scale reshapes it before use. Verified via a standalone probe script (npu_moe_init_routing_v2(quant_mode=3) vs. the two-step quant_mode=-1 + npu_dynamic_mx_quant reference) to be byte-identical (cos = 1.0) on Ascend A5 + torch_npu 2.10.0.post2, then confirmed end-to-end on the online and offline launch scripts — so this fused path is the only routing-quant implementation kept (no runtime toggle, no two-step fallback branch).

Offline quantization (msmodelslim pre-quantized weights, flagless)

  • W8A8_MXFP8 MoE checkpoints are detected from quant_model_description.json and handled by the new ModelSlimMXFP8MoEScheme, which registers float8_e4m3fn expert weights + uint8 (E8M0, exponent + 127) block scales and delegates weight post-processing + forward to the same NPUMXFP8MoEMethod kernel (offline branch: reshape [E, N, K/32] → [E, N, K/64, 2] + transpose, no re-quant).
  • Router gate: msmodelslim also quantises mlp.gate (W8A8_MXFP8). The gate is a ReplicatedLinear, so its quantization is now description-driven — for the offline modelslim path the gate receives the quant config and is dequantised correctly via ModelSlimMXFP8Scheme; the online path keeps it in BF16 (unchanged). Previously the fp8 gate was cast to BF16 without its block scale, scrambling the router and producing garbage output; this is the fix that makes the offline path usable.

Benchmark Results

Validated end-to-end on Ascend A5 hardware (Qwen3-30B-A3B). Both W8A8 MXFP8 modes (online and offline msmodelslim) are compared against the BF16 baseline.

Performance

Both paths ship the FRACTAL_NZ expert-weight layout (review item 8). Against BF16, online and offline W8A8 MXFP8 deliver ~+44% throughput and ~-30% TPOT / E2E latency; the two modes are within noise of each other.

Metric BF16 (avg) Online W8A8 (avg) vs BF16 Offline W8A8 (avg) vs BF16
Request Throughput (req/s) 3.03 4.37 ↑ +44.2% 4.35 ↑ +43.6%
Input Token Throughput (tok/s) 1191.70 1719.21 ↑ +44.3% 1709.32 ↑ +43.4%
Output Token Throughput (tok/s) 640.99 924.73 ↑ +44.3% 919.41 ↑ +43.4%
Total Token Throughput (tok/s) 1832.69 2643.95 ↑ +44.3% 2628.72 ↑ +43.4%
Mean E2E Latency (ms) 10101.74 7004.54 ↓ -30.7% 7043.46 ↓ -30.3%
Median E2E Latency (ms) 7159.17 4950.51 ↓ -30.9% 4986.98 ↓ -30.3%
P99 E2E Latency (ms) 38648.60 26909.35 ↓ -30.4% 27005.87 ↓ -30.1%
Mean TTFT (ms) 168.29 134.96 ↓ -19.8% 136.54 ↓ -18.9%
Median TTFT (ms) 153.14 133.13 ↓ -13.1% 136.12 ↓ -11.1%
P99 TTFT (ms) 1204.71* 231.07 ↓ -80.8% 227.01 ↓ -81.2%
Mean TPOT (ms) 47.44 32.79 ↓ -30.9% 32.96 ↓ -30.5%
Median TPOT (ms) 46.91 33.20 ↓ -29.2% 33.24 ↓ -29.1%
P99 TPOT (ms) 80.22 49.60 ↓ -38.2% 49.81 ↓ -37.9%
Mean ITL (ms) 47.44 32.81 ↓ -30.8% 32.98 ↓ -30.5%

Improvement = (quantized - bf16)/bf16 for throughput and (bf16 - quantized)/bf16 for latency; positive values are a performance gain.

* The BF16 P99 TTFT is skewed by a single outlier run (3100.86 ms); the other two BF16 runs were 263.73 and 249.55 ms. Every quantized configuration is consistently low and stable here.

FRACTAL_NZ weight layout (review item 8)

ND vs NZ on the same benchmark, everything else equal:

Metric Online: ND → NZ Offline: ND → NZ
Total Token Throughput (tok/s) 2592.53 → 2643.95 (+2.0%) 2552.76 → 2628.72 (+3.0%)
Mean E2E Latency (ms) 7146.17 → 7004.54 (-2.0%) 7259.02 → 7043.46 (-3.0%)
Mean TTFT (ms) 144.32 → 134.96 (-6.5%) 145.93 → 136.54 (-6.4%)
P99 TPOT (ms) 51.58 → 49.60 (-3.8%) 52.73 → 49.81 (-5.5%)

Not the ~10% the int paths saw, but consistent across both modes, so NZ is what this PR
ships. A standalone kernel probe on the same box (gmm1 + gmm2 only, 128 experts) measured
+1.4% on decode-shaped and +3.8% on prefill-shaped inputs against a 0.2-0.3% noise floor,
matching the end-to-end delta.

Two things worth recording, since neither is obvious from the int8 precedent:

  • The cast must precede the transpose. npu_grouped_matmul_swiglu_quant_v2 asserts
    that weight and weight_scale carry the same transpose flag (CheckMXTranspose), and
    npu_format_cast returns a physically retiled, non-transposed tensor — so casting the
    already-transposed view fails outright with transpositions are true/false. The int8 MoE
    methods transpose first, but they carry no MX scale to keep in sync; the dense W4A8 path
    already uses the order adopted here.
  • Contiguous weights are not the answer. Making weight and scale both contiguous is
    numerically correct but measurably slower (-6.2% decode at 128 experts), so the
    transpose views stay. A small-expert-count microbenchmark inverts this conclusion
    entirely (E=4 rates contiguous 58% faster), which is why the numbers above come from
    the real 128-expert shape.

Accuracy (GSM8K)

Validated on Ascend A5 hardware (Qwen3-30B-A3B), three runs each. Both W8A8 MXFP8 modes stay within run-to-run noise of the BF16 baseline.

Run BF16 Online W8A8 Offline W8A8
Run 1 95.68 95.53 95.68
Run 2 95.91 95.98 95.91
Run 3 96.13 96.29 95.75
Average 95.91 95.93 95.78
vs BF16 — ↑ +0.03 ↓ -0.13

GSM8K accuracy (%); vs BF16 is the average-over-average delta in percentage points.
Both deltas are smaller than the 0.45 pp spread of the BF16 baseline's own three runs.

Style / consistency with the merged NPU quant PRs (#22352, #23650, #23795)

  • All NPU ops go through torch.ops.npu.*; no top-level import torch_npu, no current_platform guards (module stays importable on CUDA/CPU/AMD/XPU CI).
  • Online returns the method directly from Fp8Config.get_quant_method (mirrors dense NPUMXFP8LinearMethod). Offline subclasses ModelSlimMoEScheme and delegates weight post-processing / forward to a self.kernel (NPUMXFP8MoEMethod), registered in get_moe_scheme's moe_quant_schemes list — the same pattern as the int8 / W4A4 / W4A8 MoE schemes.
  • MoE weight + scale are kept as non-contiguous transpose views (.transpose(1, 2) with no .contiguous()), with the expert weight cast to FRACTAL_NZ before that transpose; gmm1 uses the fused npu_grouped_matmul_swiglu_quant_v2; the group_list count↔cumulative conversion is explicit (gmm1 cumulative, gmm2 count).

Files Changed

New

File Change
srt/layers/quantization/modelslim/schemes/modelslim_mxfp8_moe.py offline ModelSlimMXFP8MoEScheme for W8A8_MXFP8 MoE checkpoints

Modified

File Change
srt/hardware_backend/npu/quantization/moe_methods.py add the per-gmm kernel NPUMXFP8MoEMethod (online + offline process_weights_after_loading, apply_fused_gmm1_swiglu for w13, apply for w2); cast the expert weights to FRACTAL_NZ ahead of the transpose
srt/hardware_backend/npu/utils.py _is_nz_aligned: add the fp8 branch — e4m3 is single-byte, so it shares int8's column rule. It previously fell through to the catch-all True, which no caller exercised (the packed-FP4 callers return early via customize_dtype, and dense MXFP8 never format-casts)
srt/hardware_backend/npu/quantization/online_moe_methods.py (new) NPUMXFP8OnlineMoEMethod(UnquantizedFusedMoEMethod), the --quantization mxfp8 entry point
srt/hardware_backend/npu/moe/matmul.py add GroupedMatmulSwigluQuant, the reusable wrapper around npu_grouped_matmul_swiglu_quant_v2
srt/hardware_backend/npu/moe/hidden_states_quant.py → quant.py rename + teach HiddenStatesDynamicQuant the float8_e4m3fn (npu_dynamic_mx_quant) dtype
srt/layers/quantization/unquant.py drop the hardcoded dispatcher_output_dtype: "bf16" that clobbered the value the NPU kernel had just set (it duplicated NPUUnquantMoEMethod)
srt/layers/moe/moe_runner/ascend.py stop rejecting DeepEP at runner construction
srt/layers/quantization/fp8.py dispatch FusedMoE layers to NPUMXFP8OnlineMoEMethod under is_npu() and use_mxfp8
srt/arg_groups/overrides.py skip the mxfp8 → flashinfer_trtllm runner-backend override on NPU, so the backend stays auto and resolves to Ascend (it also gates FusedMoE's w1/w3 shard swap)
srt/layers/quantization/modelslim/modelslim.py add ("W8A8_MXFP8", ModelSlimMXFP8MoEScheme) to get_moe_scheme's moe_quant_schemes
srt/layers/quantization/modelslim/schemes/__init__.py export ModelSlimMXFP8MoEScheme
srt/models/qwen3_moe.py make the router gate quant description-driven (offline modelslim only) so a quantised mlp.gate loads/dequantises correctly instead of being cast to BF16 without its scale
docs_new/docs/advanced_features/quantization.mdx, docs_new/docs/hardware-platforms/ascend-npus/ascend_npu_quantization.mdx document MXFP8 MoE (Ascend support row + online/offline prose + router-gate note)

Status

WIP / Draft. Online (--quantization mxfp8) and offline (msmodelslim) MoE MXFP8 are both validated end-to-end on Ascend A5 (see Performance above). GSM8K accuracy is within run-to-run noise of the BF16 baseline (see Accuracy above).

@OrangeRedeng's review is addressed. It was first applied as a single refactor commit (6bd7056), which garbled generation on A5 in a way that could not be bisected, so that commit was dropped and the review was redone one item at a time, each verified on A5 before the next:

# Item Commit
6 offline checkpoints load flagless, drop --quantization from the docs 9efe9998d
7 rename hidden_states_quant.py → quant.py, teach it mxfp8 efe50dfa3
2, 5 GroupedMatmulSwigluQuant wrapper + pick the matmul by weight_prefix 2d06d61a5
1 stop rejecting DeepEP; gmm1 quantizes when the dispatcher sends bf16 1efd2ca35
3, 4 inherit UnquantizedFusedMoEMethod, rename to NPUMXFP8OnlineMoEMethod ea24f248d
8 npu_format_cast (NZ) d419aa41f

Re-validating the baseline first also surfaced three regressions from the preceding main merge, in which upstream restructured the NPU MoE layer (fused_moe_method_npu.py deleted, moe_methods.py / moe_runner/ascend.py / npu/moe/*.py added) and this PR's implementation was re-ported onto the new layout. None are review-related:

Commit Regression
e9b36bb5d the e8m0 block-scale dtype was resolved off torch instead of torch_npu; the grouped matmuls reject torch's dtype object, so every gmm1 died
9ac21f5ff --quantization mxfp8 forced moe_runner_backend to flashinfer_trtllm unconditionally, including on NPU. Beyond picking a runner that does not exist there, that flag also drives FusedMoE's w1/w3 shard swap and a 128-alignment round-up — and once item 3 above makes this method inherit UnquantizedFusedMoEMethod, it matches the shard-swap list, loading every expert with gate and up exchanged. Silent degenerate output; this is what garbled the big-bang refactor
969087364 ModelSlimMoEMethod.apply reads {w13,w2}_weight_offset unconditionally; MXFP8 is a pure scale format with no zero point, so the offline scheme never created one

Related Issues

Part of #21584 (MXFP8/MXFP4 support on Ascend NPU for Qwen3 LLM). Builds on merged #23795 (dense W4A4) and #22352 (dense W8A8 MXFP8 — provides the dense linear MXFP8 method + Fp8Config NPU dispatch + ModelSlimMXFP8Scheme this MoE work reuses).


CI States

Latest PR Test (Base): 🚫 Run #30416404909
Latest PR Test (Extra): ❌ Run #30416404805

Add NPUMXFP8FusedMoEMethod (online weight quant via npu_dynamic_mx_quant with 3D input) + npu_fused_experts_mxfp8 kernel (routing -> act quant -> swiglu_quant_v2 -> grouped_matmul -> finalize_routing, mirroring vllm-ascend A5 path). Wire dispatch in Fp8Config.get_quant_method for FusedMoE layers (is_npu + use_mxfp8). Weights kept as strided transpose views — kernel contract verified by A5 probe.
ModelSlimMXFP8MoEScheme for --quantization modelslim with W8A8_MXFP8 pre-quantised checkpoint. NPUMXFP8FusedMoEMethod now detects fp8 dtype and takes offline transpose-only branch. Registered via FusedMoeWeightScaleSupported.BLOCK.
Offline modelslim (W8A8_MXFP8) checkpoints quantise mlp.gate to float8_e4m3fn + weight_scale, but Qwen3MoeSparseMoeBlock hard-coded the gate as ReplicatedLinear(quant_config=None) -> bf16. The fp8 gate was then cast to bf16 WITHOUT its block scale and the scale key dropped, scrambling the router (probe: norm 70195 vs 31.4) -> wrong experts -> garbage. Online (bf16 gate) was unaffected.

Pass quant_config to the gate only when it is the offline ModelSlim path (get_name()=='modelslim'); ModelSlimConfig.get_quant_method then dequantises a quantised gate correctly or skips a FLOAT/absent one. The online Fp8/mxfp8 path keeps gate_quant_config=None -> bf16, unchanged. Mirrors vllm-ascend's description-driven dispatch.
ascend_npu_quantization.mdx: mark MXFP8 MoE supported on A5, add an MXFP8-for-LLM-MoE section (online + offline launch commands, kernel path, and the router-gate description-driven caveat). quantization.mdx: extend the mxfp8 Ascend cell to cover LLM MoE (FusedMoE/TP). Mirrors the doc pattern from PR sgl-project#23795.
@github-actions github-actions Bot added documentation Improvements or additions to documentation quant LLM Quantization npu labels Jul 10, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for MXFP8 quantization (both online and offline) for LLM MoE models (such as Qwen3-30B-A3B) on Ascend NPUs (A5 series). It implements the NPUMXFP8FusedMoEMethod and the corresponding offline ModelSlimMXFP8MoEScheme scheme, utilizing specialized NPU kernels like npu_grouped_matmul_swiglu_quant_v2 and npu_dynamic_mx_quant. Additionally, the router gate in qwen3_moe.py has been updated to support description-driven quantization for offline paths, and the documentation has been updated accordingly. No review comments were provided, so I have no further feedback.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@OrangeRedeng

OrangeRedeng commented Jul 10, 2026 •

Copy link
Copy Markdown
Collaborator

Hi! We have already 5 PR's to mxfp MoE implementation #26408, #29762, #30317, #30318, #30319 and , i think we should collaborate in some way to avoid collisions

@ping1jing2 ping1jing2 self-assigned this Jul 10, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@TallMessiWu TallMessiWu changed the title 🚧 [WIP][llm][npu][quant] Add W8A8 MXFP8 quantization for Qwen3 MoE on Ascend NPU ✨ [WIP][llm][npu][quant] Add W8A8 MXFP8 quantization for Qwen3 MoE on Ascend NPU Jul 13, 2026
Add an opt-in fused path to npu_fused_experts_mxfp8: when SGLANG_ENABLE_NPU_FUSED_MOE_ROUTING_QUANT is on and the op supports it, npu_moe_init_routing_v2(quant_mode=3) does the MXFP8 activation quant inline (e4m3 payload + e8m0 block scale as ret[3]), replacing the separate npu_dynamic_mx_quant pass and saving one kernel launch. Gated by SGLANG_ENABLE_NPU_FUSED_MOE_ROUTING_QUANT (default off) plus a cached runtime capability probe that falls back to the two-step path when quant_mode=3 is unsupported.

A5 probe (Q9, torch_npu 2.10.0.post2): quant_mode=3 accepted with x_dtype=None; ret[3] scale is 2D [N,K/32] e8m0 so _normalize_mxfp_scale reshapes it to [N,K/64,2]; fused output is byte-identical to the two-step path (cos=1.0). DeepEP path (apply_without_routing_weights) does not go through init_routing and is unchanged.
Review feedback on sgl-project#30768: the hard rejection at runner construction was wrong.
DeepEP works with mxfp MoE even without quantization inside the dispatcher --
it simply returns bf16, and the method can quantize the activations itself.

Three pieces:

- AscendRunnerCore no longer raises for DeepEP + MXFP8. activation stays None
  either way, since gmm1 fuses gate/up + swiglu + requant.
- process_weights_after_loading sets the dispatcher output dtype per backend.
  DispatcherOutputDtype has no MXFP8 entry in deepep.py's config_map (only
  BF16/FP8/INT8/NVFP4), so asking for 'mxfp8' there would KeyError -- hence an
  explicit bf16 branch rather than letting it through.
- apply_fused_gmm1_swiglu takes pertoken_scale as Optional and, when it is None,
  quantizes the hidden states through the HiddenStatesDynamicQuant wrapper from
  the previous commit. Both dispatchers therefore reach the fused kernel with
  the same e4m3 + e8m0 input; only where the activation quant happens differs.

ascend_tp is unaffected: is_deepep() is False there, so the dispatcher dtype
stays 'mxfp8' and pertoken_scale is never None.

Docs updated to match. Only the ascend_tp path is validated end-to-end on A5 --
I have no EP setup, so in particular it is unverified whether low-latency
dispatch's 3D hidden states are accepted by npu_grouped_matmul_swiglu_quant_v2.
…oint

Review feedback on sgl-project#30768: the online entry point duplicated create_weights,
process_weights_after_loading, create_moe_runner and apply from the unquantized
method, and the name did not distinguish it from the per-gmm kernel.

NPUMXFP8FusedMoEMethod becomes NPUMXFP8OnlineMoEMethod(UnquantizedFusedMoEMethod)
and overrides only create_moe_runner, to attach the MXFP8 kernels before
MoeRunner is built (AscendRunnerCore.__init__ reads layer.w2_kernel to pick its
activation). That removes ~85 lines. Verified the inherited pieces against the
current unquant.py rather than assuming: create_weights produces the same
[E, 2I, H] / [E, H, I] parameters given is_gated and no bias; the aiter /
cpu-amx / deep_gemm / flashinfer branches of process_weights_after_loading are
all False on NPU, leaving the _is_npu branch that calls the kernels; apply ->
forward_npu -> runner.run(dispatch_output, layer) works because layer carries
the same w{13,2}_weight* attribute names AscendQuantInfo did.

It has to live in a new module: unquant.py imports moe_methods.py at module
scope, so subclassing there would be a circular import. moe_methods.py does not
import unquant.py, so the new chain stays acyclic.

Two supporting changes:

- unquant.py no longer hardcodes dispatcher_output_dtype 'bf16' right after
  calling the kernels. It duplicated what NPUUnquantMoEMethod already does for
  w13, and it clobbered the value a quantized subclass kernel had just set.
  There are no other subclasses, so unquant is unaffected.
- create_moe_runner rejects a non-Ascend runner backend loudly. Inheriting
  UnquantizedFusedMoEMethod puts this method in FusedMoE's w1/w3 shard-swap list
  (layer.py:942), so an explicit --moe-runner-backend flashinfer_trtllm would
  load every expert with gate and up exchanged -- silent degenerate output. The
  automatic override was already fixed in 9ac21f5; this covers the manual one.
  The other two isinstance sites (layer.py:365 and the weight-update hook at
  :904) are both gated on is_flashinfer_trtllm_routed() and inert on NPU.
Answers the NZ question raised in review of PR sgl-project#30768. On A5 (torch_npu 2.10.0.post2, Ascend950PR, Qwen3-30B-A3B shapes with 128 experts) this gains 1.4% on decode and 3.8% on prefill against a 0.2-0.3% noise floor, with outputs identical to before. Nowhere near the ~10% the int8 path reported, but consistent across both token shapes.

The cast must precede the transpose. gmm1 asserts that weight and weight_scale share a transpose flag (CheckMXTranspose), and the cast returns a physically retiled, non-transposed tensor -- so casting the already-transposed view fails outright. That is why this cannot follow the int8 MoE methods in the same file, which transpose first but carry no MX scale to keep in sync; the dense W4A8 path in linear_method_npu.py already uses this order. Measurements also rule out .contiguous() on either side: making both contiguous costs 6.2% on decode.

_is_nz_aligned gains an fp8 branch sharing the int8 column rule, since e4m3 is likewise single-byte. It previously fell through to the catch-all True, which no caller exercised -- the packed-FP4 callers return early via customize_dtype and dense MXFP8 never format-casts -- so this only guards the weights introduced here.
# Conflicts:
#	python/sglang/srt/layers/quantization/modelslim/schemes/__init__.py

@OrangeRedeng OrangeRedeng 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.

Great changes! Thank you!

@OrangeRedeng

OrangeRedeng commented Jul 21, 2026 •

Copy link
Copy Markdown
Collaborator

We'll start CI a little later, after we fix it

@ping1jing2 ping1jing2 changed the title ✨ [WIP][llm][npu][quant] Add W8A8 MXFP8 quantization for Qwen3 MoE on Ascend NPU ✨ [llm][npu][quant] Add W8A8 MXFP8 quantization for Qwen3 MoE on Ascend NPU Jul 21, 2026
@ping1jing2

Copy link
Copy Markdown
Collaborator

/tag-and-rerun-ci

@github-actions github-actions Bot added the run-ci CI: run the baseline test suite on this PR label Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

CI investigation

The current CI failures are not caused by this PR's MXFP8 MoE changes.

1. Non-CUDA platforms fail in an upstream DWDP import path

The MLX, Xeon, and Ascend/NPU jobs all fail while importing ModelRunner:

ModelRunner -> DwdpManager -> dwdp/vmm.py:11
from cuda.bindings import driver as cuda
ModuleNotFoundError: No module named 'cuda'

This unconditional CUDA-only import was introduced by upstream DWDP PR #29778 (commit 37a830b66). It is outside this PR's diff from base c0ed009f5 to head e22b7bef8; this PR does not modify model_runner.py or any layers/moe/dwdp/ file.

The failed NPU case is Qwen2.5-7B-Instruct HiCache, so it does not exercise the Qwen3/Qwen3.5 MoE MXFP8 code added here.

2. The Base GPU root failure is runner state, then fast-fail cascades

The first Base CI root-cause job (base-c-test-8-gpu-h200 (0)) runs a MiMo test, then its following DeepSeek MTP test aborts because every GPU still holds about 99 GiB from the previous server processes:

RuntimeError: GPU(s) still not idle after waiting 30s before setUpClass

The remaining H100/B200/DeepEP Base failures are the workflow's fast-fail cascade from that root job, rather than independent test or quantization failures.

No failing log contains an MXFP8 kernel/quantization assertion or a failure in the files changed by this PR. Lint and XPU checks passed.

Conclusion: this PR is blocked by the upstream DWDP non-CUDA import regression plus an independent dirty-GPU-runner failure. The DWDP import needs an upstream platform guard/lazy import; after that, the GPU job should be rerun on a clean runner.

@OrangeRedeng

Copy link
Copy Markdown
Collaborator

CI investigation

The current CI failures are not caused by this PR's MXFP8 MoE changes.

1. Non-CUDA platforms fail in an upstream DWDP import path

The MLX, Xeon, and Ascend/NPU jobs all fail while importing ModelRunner:

ModelRunner -> DwdpManager -> dwdp/vmm.py:11
from cuda.bindings import driver as cuda
ModuleNotFoundError: No module named 'cuda'

This unconditional CUDA-only import was introduced by upstream DWDP PR #29778 (commit 37a830b66). It is outside this PR's diff from base c0ed009f5 to head e22b7bef8; this PR does not modify model_runner.py or any layers/moe/dwdp/ file.

The failed NPU case is Qwen2.5-7B-Instruct HiCache, so it does not exercise the Qwen3/Qwen3.5 MoE MXFP8 code added here.

2. The Base GPU root failure is runner state, then fast-fail cascades

The first Base CI root-cause job (base-c-test-8-gpu-h200 (0)) runs a MiMo test, then its following DeepSeek MTP test aborts because every GPU still holds about 99 GiB from the previous server processes:

RuntimeError: GPU(s) still not idle after waiting 30s before setUpClass

The remaining H100/B200/DeepEP Base failures are the workflow's fast-fail cascade from that root job, rather than independent test or quantization failures.

No failing log contains an MXFP8 kernel/quantization assertion or a failure in the files changed by this PR. Lint and XPU checks passed.

Conclusion: this PR is blocked by the upstream DWDP non-CUDA import regression plus an independent dirty-GPU-runner failure. The DWDP import needs an upstream platform guard/lazy import; after that, the GPU job should be rerun on a clean runner.

This known issue was fixed in #31919, I re-ran the CI

Copy link
Copy Markdown
Contributor Author

CI investigation — current run (b747bcc47613)

The current failures are not caused by this PR's MXFP8 MoE changes.

1. Base CI: transient crates.io/network failure before tests

The sole root-cause job is base-c-test-deepep-8-gpu-h200 (0). It failed while building the editable SGLang package, before any DeepEP or MXFP8 test ran:

error: failed to get `unindent` as a dependency of package `pyo3`
download of un/in/unindent failed
curl failed
[16] Error in the HTTP2 framing layer
cargo metadata ... failed with code 101

The remaining Base GPU failures explicitly report a fast-fail cascade from that job, rather than independent test failures.

2. AMD CI: runner queue timeout

wait-for-stage-a-amd timed out after waiting four hours while stage-a-test-1-gpu-small-amd remained queued:

stage-a-test-1-gpu-small-amd ... status=queued, conclusion=null
Timeout waiting for stage-a-amd jobs

The actual Stage A AMD test later acquired a runner and passed, so this is runner scheduling/capacity rather than a code failure.

3. Extra / AMD Extra: missing opt-in label

Both Extra workflow gates failed because the PR had run-ci but not run-ci-extra:

PR Labels: [documentation,quant,npu,run-ci]
Missing required label 'run-ci-extra'.

No Extra tests ran. Add it via /tag-and-rerun-ci extra only if the Extra suite is required.

The NPU workflow and lint both passed. This PR does not change rust/, dependency metadata, or CI workflows. The appropriate action is to rerun Base/AMD; no MXFP8 code change is indicated by these logs.

@sglang-npu-bot
sglang-npu-bot merged commit f05c92f into sgl-project:main Jul 29, 2026
125 of 155 checks passed
saturn-acc pushed a commit to saturn-acc/sglang that referenced this pull request Aug 16, 2026
…nd NPU (sgl-project#30768)

Co-authored-by: Артем Савкин <58187114+OrangeRedeng@users.noreply.github.com>
Co-authored-by: ronnie_zheng <zl19940307@163.com>
Atituiset pushed a commit to Atituiset/sglang that referenced this pull request Sep 10, 2026
…nd NPU (sgl-project#30768)

Co-authored-by: Артем Савкин <58187114+OrangeRedeng@users.noreply.github.com>
Co-authored-by: ronnie_zheng <zl19940307@163.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation npu quant LLM Quantization run-ci CI: run the baseline test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants