Skip to content

feat(moe_wna16): 2-bit experts, expert streaming, N-contiguous GEMV - #37796

Draft
HaberstrohSystems wants to merge 2 commits into
sgl-project:qwen4-main-squashedfrom
HaberstrohSystems:q4-pr2
Draft

HaberstrohSystems wants to merge 2 commits into
sgl-project:qwen4-main-squashedfrom
HaberstrohSystems:q4-pr2

Conversation

@HaberstrohSystems

@HaberstrohSystems HaberstrohSystems commented Sep 3, 2026

Copy link
Copy Markdown

Draft: stacked on #37793 (part 1); parts precede it in the series.

Part 2/5 of the Qwen3.8-Flash-Next 24 GB serving series (RFC issue: #37792). Patch:
upstream/series-q4head/0002-feat-moe_wna16-2-bit-experts-expert-streaming-N-cont.patch
in the companion repository (9 files, +1,583 / -33). Nothing in this part is Qwen4-specific.

Motivation

moe_wna16 accepts 4 and 8 bits only, so a symmetric 2-bit AutoRound / GPTQ MoE checkpoint
cannot be loaded at all. For Qwen3.8-Flash-Next (512 experts, top-10, moe_intermediate_size
640) the 2-bit experts are what makes the model fit next to a 24 GB card: the served checkpoint
is 2.572 bpw (model card, "Quantization"). Two more things are needed once the experts are
offloaded: --cpu-offload-gb copies a whole module's state_dict to the device on every forward
(measured ~26 GB per token at 2.3 tok/s, while 10 x 48 x 1.31 MB = 0.63 GB of expert rows are
actually needed; docs/WRITEUP.md section 5), and the byte-row [E, N, K/4] layout reads
128-byte chunks at 640-byte stride, which limits an in-place decode GEMV to 73 GB/s from device
memory and 5 GB/s from pinned host memory (CAMPAIGN 2026-09-02 08:29-08:31).

Sources: dated entries of docs/CAMPAIGN.md and docs/WRITEUP.md in
https://github.com/HaberstrohSystems/qwen3.8-flash-next-24gb-sglang and the model card
https://huggingface.co/HaberstrohSystems/Qwen3.8-Flash-Next-int2-mixed-AutoRound-24GB-SGLang.
Reference machine: one RTX PRO 4000 Blackwell (24 GB, sm_120) with 32 GB host RAM.

Modifications

  • kernels/ops/moe/fused_moe_triton_kernels.py: 2-bit unpack in fused_moe_kernel_gptq_awq as
    the generalization of the 4-bit branch (offs_k // 4, shift (offs_k % 4) * 2, mask 0x3,
    zero point 2). New fused_moe_kernel_gptq_awq_word: the same kernel reading the N-contiguous
    int32-word layout (one coalesced 128-byte line per warp load instead of 32 distinct lines). The
    layout is derived from the tensor dtype (int2 + int32 = word layout, int2 + uint8 = byte
    layout), so nothing crosses the custom-op schema. Reviewer's choice: keep the second Triton
    function or fold it into a constexpr branch of the existing kernel.
  • moe_runner/triton.py, triton_utils/fused_moe.py, triton_utils/fused_moe_triton_config.py:
    use_int2_w2a16 plumbing, shape handling for the word layout, config dtype name int2_w2a16
    so a tuned int4 config is never picked up by accident. No int2 config files ship; the tuned
    configs used below (for E={10,512}, N=160 on the RTX PRO 4000 Blackwell) live in the
    companion repository under assets/moe_configs and are selected through SGLANG_MOE_CONFIG_DIR.
  • layers/quantization/moe_wna16.py: 2-bit loader; asymmetric qzeros raise
    NotImplementedError by design. process_weights_after_loading re-lays every 2-bit expert
    tensor once from [E, N, K/4] uint8 to [E, K/16, N] int32 and scales to [E, K/128, N]
    (same bytes; SGLANG_MOE_NCONTIG=0 keeps the byte layout); pinned host tensors are re-pinned
    after the copy, which transiently doubles one layer's host footprint. Deferred placement pass
    at the last layer (SGLANG_MOE_PLACEMENT=<expert_freq.pt> with {"mass": [L, E]},
    SGLANG_MOE_PLACEMENT_S, default 184): the hottest S experts of every layer are resident on
    the GPU, cold rows of GPU layers take the pinned slots that host layers' hot rows vacate,
    memory-neutral on both sides. With SGLANG_MOE_ELASTIC=1 the pass hands the layers to
    ExpertElastic (part 3).
  • layers/moe/expert_gemv.py (new): batch-1 int2 GEMV (moe_gemv_int2_tab, used for M <= 16
    in apply(), SGLANG_MOE_GEMV=0 disables) that reads experts in place through int64 address
    tables; an entry may point into device or pinned host memory, the kernel indexes with the
    original expert ids. to_word_ncontig, make_tables.
  • layers/moe/expert_stream.py (new, SGLANG_MOE_EXPERT_STREAM=1): gathers exactly the routed
    experts of a forward into one shared, shape-keyed staging buffer and renumbers the top-k ids
    onto it; fully GPU-resident layers skip the gather; arange/cast tensors are memoized.
  • test/registered/unit/layers/quantization/test_moe_wna16_int2.py (new, register_cuda_ci,
    est_time=60, stage base-b, runner 1-gpu-small; synthetic data, no checkpoint): the exact
    2-bit unpack expressions against a torch reference (bit-exact, including the real expert dims
    2560 x 640 g128); to_word_ncontig byte -> word round trip (bit-exact and invertible, CPU);
    fused_moe(use_int2_w2a16=True) on the byte layout (symmetric and with per-group qzeros)
    and on the word layout against a torch MoE reference on dequantized weights;
    moe_gemv_int2_tab through make_tables addresses against an fp32 reference (w13 and w2
    forms, fp16 and bf16 scales, N not a multiple of the block; a bad block is rejected).
  • test/manual/test_triton_moe_wna16.py: w2a16 / w2a16b2 cases and the 2-bit packing. This
    stays a manual test: its 8-bit path fails the tolerance check with the unmodified upstream
    kernel on the reference machine (129 of 144 cases in the 8-bit / group-128 subset, identical on
    the unpatched tree), so it is not a usable gate.

Accuracy Tests

  • test_moe_wna16_int2.py: 7 tests, 34 subtests, pass (packing order, unpack expressions and
    the word-layout round trip bit-exact; fused_moe on both layouts and the GEMV within
    atol 1e-3 / rtol 2e-2 of the dequantized references, measured ~4e-3 relative). Also passes on
    the finished 5-commit branch.
  • Original A/B of the same kernel path on real shapes (dequantized bf16 vs packed 2-bit through
    the same MoE machinery): bit-identical on small shapes, 1e-5 relative on large ones including
    e=64, n=640, k=2560, g128 (docs/WRITEUP.md section 8).
  • Word layout and GEMV: A/B through the patched invoke_fused_moe_kernel at 64/64/32,
    16/16/128, 16/32/128 identical (2e-5) (CAMPAIGN 2026-09-02 11:04); per-layer dump A/B with
    identical inputs 5.1e-5 (M=6) / 1.7e-4 (M=1) relative, i.e. bf16 output rounding from a
    different fp32 accumulation order (CAMPAIGN 2026-09-02 11:25). End-to-end oracle on the 10k
    prompt max 0.275 / mean 0.0125, inside the kernel-class band (CAMPAIGN 2026-09-02 11:30).
  • Placement is exact by construction (rows move, values do not): oracle max 0.057 / mean 0.0013
    (CAMPAIGN 2026-09-02 12:10). Tuned int2 configs: max 0.086 / mean 0.0018 (2026-09-02 09:13).

Reproduction (needs ninja on PATH (otherwise FileNotFoundError: ninja) and an nvcc that
supports the GPU architecture at CUDA_HOME (compute_120a on the reference machine; a system
nvcc 12.0 does not, the virtualenv's 13.3 does)):

git clone https://github.com/sgl-project/sglang.git && cd sglang
git fetch origin qwen4-main-squashed && git checkout 78c5024e9d9f589dcb4deb7f4ba4fb23f7e85385
git am /path/to/upstream/series-q4head/0001-*.patch /path/to/upstream/series-q4head/0002-*.patch
pip install -e python
export CUDA_HOME=<toolkit whose nvcc supports the GPU architecture>; export PATH="$CUDA_HOME/bin:$PATH"
python -m pytest -v test/registered/unit/layers/quantization/test_moe_wna16_int2.py

Speed Tests and Profiling

Streaming bench, single request, ~10k context (tools/bench_speed.py; card, "Performance"),
measured on 73a255206f with the flat patch:

  • PCIe traffic per decoded token 26 GB -> 0.31 GB with expert-only offload and the streamer
    (docs/WRITEUP.md section 5; first working path 2.24 -> 15.5 tok/s, sections 4 and 5).
  • Tuned int2 configs: decode 13.4 -> 15.2 tok/s (CAMPAIGN 2026-09-02 09:13).
  • N-contiguous GEMV micro-benchmark on real layer-5 tensors: 320 GB/s from device memory,
    51 GB/s (PCIe line rate) from pinned host memory, 2.1 ms/token all-device; the byte-shuffled
    table variant reached 157 / 14 GB/s and was rejected (CAMPAIGN 2026-09-02 08:29-08:31, 09:45).
  • N-contiguous layout + GEMV end to end: decode 19.4 -> 21.8 tok/s, prefill 1,443 -> 2,249 tok/s
    (CAMPAIGN 2026-09-02 11:30).
  • Routing-mass placement: the top 171 of 512 experts per layer cover 82 % of routing mass on a
    2,496-token, 3-domain probe (CAMPAIGN 2026-09-02 09:48-09:53); decode 40.0 -> 48.4 tok/s,
    prefill 2,249 -> 2,334 tok/s (CAMPAIGN 2026-09-02 12:10).

Not re-measured on 78c5024e9d (see the RFC, open question 3). The placement profile
(assets/expert_freq.pt, built by tools/expert_freq.py from a routing dump) and the tuned
configs are per model and per GPU.

Checklist

  • Format your code according to the Format code with pre-commit. (black 26.1.0, isort 7.0.0, ruff 0.15.1 F401,F821,UP037, codespell and py_compile clean on the commit.)
  • Add unit tests according to the Run and add unit tests. (test/registered/unit/layers/quantization/test_moe_wna16_int2.py, 7 tests / 34 subtests, register_cuda_ci.)
  • Update documentation according to Write documentations. (Missing: the layout-by-dtype convention on fused_experts, the placement-profile format, the environment variables.)
  • Provide accuracy and speed benchmark results according to Test the accuracy and Benchmark the speed. (Kernel A/Bs, the logprob oracle, GEMV micro-benchmark and the streaming bench above; no standard benchmark score.)
  • Follow the SGLang code style guidance. (Known deviation: switches are environment variables, not server arguments.)

Suggested reviewers

  • python/sglang/kernels (Triton MoE kernels): @BBuf (Kernel).
  • python/sglang/srt/layers/moe, layers/quantization/moe_wna16.py: no Merge Oncall area is
    listed for these paths; CODEOWNERS will be requested automatically. @JustinTong0323 as author
    of Introduce Qwen 3.8 Flash Next #36497.

CI States

Latest PR Test (Base): ❌ Run #33750768338
Latest PR Test (Extra): ❌ Run #33750768055
Latest PR Test (AMD ROCm 10): ❌ Run #33750768353

## Motivation

Qwen3.8-Flash-Next (Qwen4-Exp: 176B total / 6B active, 48 layers =
36 GatedDeltaNet + 12 Qwen sparse attention, 512 experts top-10, a 51 GB
PLE n-gram embedding table) only fits a 24 GB card with `--cpu-offload-gb`.
On the base commit that path does not work: weight loading fails on a
device mismatch in `GemmaRMSNorm`, `functional_call` rejects the tied
`A_log` tensors, the GDN convolution reads a stale `.view()` of a
parameter whose `.data` the offloader has replaced (GDN output exactly
zero, NaN logits), checkpoints saved through transformers carry the layer
type `qwen_sparse_attention`, the fused `in_proj_ba` lands at the wrong bit
width because `packed_modules_mapping` never reaches the quant config, and
the PLE table is allocated in full (51.2 GB as fp8, 51,200,245,760 B;
102.4 GB if materialised as bf16, derived as 2 x 51,200,245,760 B) before
any offload can act. These are the nine findings of `WRITEUP.md` section 3
("Nine findings in SGLang"), with finding 8 (the silent NaN) analysed in
section 4 and finding 9 (26 GB of PCIe traffic per token) in section 5.

The reference machine is one RTX PRO 4000 Blackwell (24 GB, sm_120) with
32 GB host RAM, serving a 2.572 bpw AutoRound checkpoint (model card,
"Quantization").

Measurements and logs: https://github.com/HaberstrohSystems/qwen3.8-flash-next-24gb-sglang
(`docs/CAMPAIGN.md`, an append-only log cited below by its dated entries;
`docs/ELASTIC_MEMORY.md`; `docs/WRITEUP.md`; `docs/DECODE_PERF_PLAN.md`;
`tools/bench_speed.py`) and the model card
https://huggingface.co/HaberstrohSystems/Qwen3.8-Flash-Next-int2-mixed-AutoRound-24GB-SGLang
(cited by section name).

## Modifications

Correctness under `--cpu-offload-gb` (general, not Qwen4-specific):

* `layers/layernorm.py`: `GemmaRMSNorm` re-derives `gemma_weight` on the
  device of the parameter (`_weight_loader`) and of the input
  (`_gemma_weight_for`); the non-persistent buffer was left on the GPU
  while the parameter went to the CPU.
* `utils/offloader.py`: `functional_call(..., tie_weights=False)` in both
  hook variants; `_CpuParamOffloader` keeps a hard reference to the pinned
  host storage (`cpu_data`). With `SGLANG_MOE_EXPERT_STREAM=1` only expert
  tensors (`w13_qweight`, `w2_qweight`, `w13_scales`, `w2_scales`,
  `w13_qzeros`, `w2_qzeros`) are offloaded, and the forward hook is not
  installed when every offloaded parameter of a module is a streamed expert
  parameter (the hook would reparametrize the module with tensors that are
  already on the device). The streamer itself is part 2 of this series.
* `layers/radix_linear_attention.py`, `models/qwen3_5.py`: the GDN module
  passes its `nn.Conv1d` instead of a `.view()` of its weight;
  `RadixLinearAttention.conv_weights` is a property that builds the 2D view
  from the current parameter on every access. Tensors and tuples pass
  through unchanged, so KDA / ShortConv / Lightning are not affected.

Qwen4-Exp specific:

* `models/qwen4_exp.py`: accept the layer type `qwen_sparse_attention`
  (transformers renames `full_attention` on load; loading otherwise fails
  with `KeyError: 'qwen_sparse_attention'`). The matching
  `layers_block_type` alias in `configs/qwen4_exp.py` is already on
  `qwen4-main-squashed` since sgl-project#36772 and is not repeated here;
  `Qwen4ExpForConditionalGeneration.__init__` calls
  `quant_config.update_packed_modules_mapping(...)` like `deepseek_v2.py`
  does for Quark.
* `server_args.py`: `Qwen4ExpForConditionalGeneration` in
  `LANGUAGE_MODEL_ONLY_ARCHITECTURES` (the class inherits the
  `language_model_only` implementation from `Qwen3VLForConditionalGeneration`;
  only the entry was missing). Saves the vision tower (0.84 GiB) and the
  multimodal reservation in the KV budget for text-only serving.
* `models/qwen4_exp.py`: `Qwen4ExpMmapEmbedding` serves the PLE table from
  a memory-mapped file (`SGLANG_QWEN4_PLE_MMAP=<dir>` with `ple.f8_e4m3.bin`
  and `ple.json`; `ple.json` carries `file`, `rows`, `dim`, `dtype` and
  `weight_scale`, the file is `rows x dim x itemsize` bytes and is checked).
  The `VocabParallelEmbedding` is created on the `meta` device in that mode
  so nothing is allocated first. Decode fetches its rows with parallel
  `os.pread` (16 threads, GIL released; the numpy fancy-index into the
  memmap held the GIL for the whole cold row fetch, measured at 2.56-3.09
  ms per token; `DECODE_PERF_PLAN.md`, "2. Parallelise the PLE row
  fetch"), bulk gathers go through the memmap. `MADV_RANDOM` on the
  mapping and `POSIX_FADV_RANDOM` on the fd stop the page-cache
  read-around per 160-byte row (`CAMPAIGN.md` 2026-09-02 14:12);
  `POSIX_FADV_DONTNEED` after bulk gathers of >= 512 ids drops the pages
  again (random text otherwise churns ~1M page-cache pages per long
  prompt; `ELASTIC_MEMORY.md`, "What the reference host taught").
* `models/qwen4_exp.py`: `Qwen4ExpMmapEmbedding.forward` is wrapped with
  `eager_on_graph(True)`, so decode runs under
  `--cuda-graph-backend-decode breakable` with the PLE lookup as the one
  eager break.
* `model_executor/runner_backend/breakable_cuda_graph_backend.py`: the four
  output-structure helpers understand the `LogitsProcessorOutput`
  dataclass; capture died with "Unsupported BCG output type" for any graph
  body that returns it.
* `layers/attention/qsa/qsa_indexer.py`: `_qsa_ensure_rope` hoists the
  `positions.max().item()` device sync (once per QSA layer, 12 per token;
  `DECODE_PERF_PLAN.md`, "5. Two micro-fixes") and pre-sizes the rotary
  cos/sin cache from `context_length`.
* `mem_cache/memory_pool.py`: fp8 write path in both `set_kv_buffer`
  variants skips the no-op `div_` by a unit scale and saturates to +-448
  before the e4m3 cast (the fp32/bf16 -> e4m3fn cast returns NaN beyond
  the range). Consumed by the fp8 read path of part 4.

No new server flags; the two switches are `SGLANG_MOE_EXPERT_STREAM` and
`SGLANG_QWEN4_PLE_MMAP`. No tests are added in this part.

## Accuracy Tests

Exactness oracle: teacher-forced logprobs on a fixed 10k-token prompt,
mean and max |dlogprob| per token against the previous configuration;
noise floor mean 0.002, threshold for host-side changes mean <= 0.01,
max <= 0.5 (model card, "Serving fidelity"; `CAMPAIGN.md` 2026-09-02
11:25).

* Host fixes (offloader hook skip, indexer hoist, PLE pread; together with
  the streamer items of part 2): oracle max 0.078 / mean 0.0019, equivalent
  (`CAMPAIGN.md` 2026-09-02 09:30).
* Breakable decode graphs + `LogitsProcessorOutput` support: oracle max
  0.079 / mean 0.0022, equivalent (`CAMPAIGN.md` 2026-09-02 11:48).
* PLE page-cache fixes involve no numerics; the 248k-token needle test on
  the same server retrieved 5/5 codes after the page-drop fix
  (`CAMPAIGN.md` 2026-09-02 20:00).
* fp8 KV write path: a 1-token prompt still crashes in the QSA indexer
  prefill in `fp8_e4m3` mode; >= 101-token prompts work (`CAMPAIGN.md`
  2026-09-02 15:26). Open issue, documented here.

## Speed Tests and Profiling

Streaming bench (`tools/bench_speed.py`, 200 streamed tokens, single
request, ~10k context; model card, "Performance"):

* host fixes: decode 15.4 -> 19.4 tok/s (`CAMPAIGN.md` 2026-09-02 09:30;
  includes the streamer items of part 2);
* breakable decode graphs: decode 21.8 -> 40.0 tok/s at prefill 2249
  tok/s, graph capture 3.97 s / 0.12 GB (`CAMPAIGN.md` 2026-09-02 11:33
  and 11:48);
* fp8 write path: `fp8_e4m3` mode at parity with bf16, decode 55.9-57.2 /
  prefill 2303-2339 tok/s (`CAMPAIGN.md` 2026-09-02 15:12);
* PLE read-around: before `MADV_RANDOM` / `POSIX_FADV_RANDOM` long
  prompts (~55k tokens) drove host memory pressure past systemd-oomd
  (`CAMPAIGN.md` 2026-09-02 14:12); the 248k random-text needle run
  survived only after `POSIX_FADV_DONTNEED` (2026-09-02 19:37 and 20:00).

Part 1/5 of the Qwen3.8-Flash-Next 24 GB serving series. The series is
based on 73a2552, the first commit of the branch qwen4-main-squashed
(PR sgl-project#36497); there is no part 0.
## Motivation

`moe_wna16` accepts 4 and 8 bits only, so a symmetric 2-bit AutoRound /
GPTQ MoE checkpoint cannot be loaded at all. For Qwen3.8-Flash-Next
(512 experts, top-10, `moe_intermediate_size` 640) the 2-bit experts are
what makes the model fit next to a 24 GB card: the served checkpoint is
2.572 bpw (model card, "Quantization"). Two more things are needed once
the experts are offloaded: `--cpu-offload-gb` copies a whole module's
`state_dict` to the device on every forward (measured ~26 GB per token at
2.3 tok/s, while 10 x 48 x 1.31 MB = 0.63 GB of expert rows are actually
needed; `WRITEUP.md` section 5), and the byte-row `[E, N, K/4]` layout
reads 128-byte chunks at 640-byte stride, which limits an in-place decode
GEMV to 73 GB/s from device memory and 5 GB/s from pinned host memory
(`CAMPAIGN.md` 2026-09-02 08:29-08:31).

Measurements and logs: https://github.com/HaberstrohSystems/qwen3.8-flash-next-24gb-sglang
(`docs/CAMPAIGN.md`, an append-only log cited below by its dated entries;
`docs/WRITEUP.md`; `tools/bench_speed.py`) and the model card
https://huggingface.co/HaberstrohSystems/Qwen3.8-Flash-Next-int2-mixed-AutoRound-24GB-SGLang
(cited by section name). The reference machine is one RTX PRO 4000
Blackwell (24 GB, sm_120) with 32 GB host RAM.

## Modifications

2-bit path (general for symmetric 2-bit `moe_wna16` checkpoints):

* `kernels/ops/moe/fused_moe_triton_kernels.py`: 2-bit unpack in
  `fused_moe_kernel_gptq_awq` as the generalization of the 4-bit branch
  (`offs_k // 4`, shift `(offs_k % 4) * 2`, mask `0x3`, zero point 2).
  New `fused_moe_kernel_gptq_awq_word`: the same kernel reading the
  N-contiguous int32-word layout (one coalesced 128-byte line per warp
  load instead of 32 distinct lines). The layout is derived from the
  tensor dtype (int2 + `int32` = word layout, int2 + `uint8` = byte
  layout), so nothing crosses the custom-op schema.
* `moe_runner/triton.py`, `triton_utils/fused_moe.py`,
  `triton_utils/fused_moe_triton_config.py`: `use_int2_w2a16` plumbing,
  shape handling for the word layout, config dtype name `int2_w2a16` so a
  tuned int4 config is never picked up by accident (no int2 config files
  ship; the tuned configs used for the measurements below live outside
  the tree, `SGLANG_MOE_CONFIG_DIR`).
* `layers/quantization/moe_wna16.py`: 2-bit loader; asymmetric `qzeros`
  raise `NotImplementedError` by design. `process_weights_after_loading`
  re-lays every 2-bit expert tensor once from `[E, N, K/4]` uint8 to
  `[E, K/16, N]` int32 and scales to `[E, K/128, N]` (same bytes;
  `SGLANG_MOE_NCONTIG=0` keeps the byte layout); pinned host tensors are
  re-pinned after the copy, which transiently doubles one layer's host
  footprint.
* `layers/moe/expert_gemv.py` (new): batch-1 int2 GEMV
  (`moe_gemv_int2_tab`, used for M <= 16 in `apply()`, `SGLANG_MOE_GEMV=0`
  disables) that reads experts in place through int64 address tables; an
  entry may point into device or pinned host memory, the kernel indexes
  with the original expert ids. `to_word_ncontig`, `make_tables`.
* `layers/moe/expert_stream.py` (new, `SGLANG_MOE_EXPERT_STREAM=1`):
  gathers exactly the routed experts of a forward into one shared,
  shape-keyed staging buffer and renumbers the top-k ids onto it; fully
  GPU-resident layers skip the gather and run on the original ids;
  `arange`/cast tensors are memoized (no per-token allocations).
* `layers/quantization/moe_wna16.py`: deferred placement pass at the last
  layer (`SGLANG_MOE_PLACEMENT=<expert_freq.pt>` with `{"mass": [L, E]}`,
  `SGLANG_MOE_PLACEMENT_S`, default 184): the hottest S experts of every
  layer are resident on the GPU, cold rows of GPU layers take the pinned
  slots that host layers' hot rows vacate, memory-neutral on both sides.
  With `SGLANG_MOE_ELASTIC=1` the pass hands the layers to `ExpertElastic`
  (part 3) instead.
* `test/registered/unit/layers/quantization/test_moe_wna16_int2.py` (new,
  `register_cuda_ci`, synthetic data, no checkpoint): the exact 2-bit
  unpack expressions of `fused_moe_kernel_gptq_awq` against a torch
  reference (bit-exact, including the real expert dims 2560 x 640 g128);
  `to_word_ncontig` byte -> word layout round trip (bit-exact and
  invertible, CPU); `fused_moe(use_int2_w2a16=True)` on the byte layout
  (symmetric and with per-group `qzeros`) and on the word layout against
  a torch MoE reference on the dequantized weights; `moe_gemv_int2_tab`
  through `make_tables` addresses against an fp32 reference (w13 and w2
  forms, fp16 and bf16 scales, N not a multiple of the block; a bad block
  is rejected).
* `test/manual/test_triton_moe_wna16.py`: `w2a16` / `w2a16b2` cases and
  the 2-bit packing. This stays a manual test: its 8-bit path fails the
  tolerance check with the unmodified upstream kernel on the reference
  machine (129 of 144 cases in the 8-bit / group-128 subset, identical on
  the unpatched tree), so it is not a usable gate; the 2-bit path is
  covered by `test_moe_wna16_int2.py` instead.

## Accuracy Tests

* `test_moe_wna16_int2.py` (7 tests, 34 subtests, pass; run as
  `PYTHONPATH=python python test/registered/unit/layers/quantization/test_moe_wna16_int2.py`
  with ninja and an nvcc that supports the GPU architecture on PATH):
  packing order, the 2-bit unpack expressions and the word-layout round
  trip are bit-exact; `fused_moe` on both layouts and the GEMV agree with
  the dequantized references within atol 1e-3 / rtol 2e-2. The original
  A/B of the same kernel path on real shapes (dequantized bf16 vs packed
  2-bit through the same MoE machinery) was bit-identical on small shapes
  and 1e-5 relative on large ones including e=64, n=640, k=2560, g128
  (`WRITEUP.md` section 8).
* Word layout and GEMV: A/B through the patched `invoke_fused_moe_kernel`
  at 64/64/32, 16/16/128, 16/32/128 identical (2e-5) (`CAMPAIGN.md`
  2026-09-02 11:04); per-layer dump A/B with identical inputs 5.1e-5
  (M=6) / 1.7e-4 (M=1) relative, i.e. bf16 output rounding from a
  different fp32 accumulation order, verdict numerically equivalent
  (2026-09-02 11:25). End-to-end oracle on the 10k prompt (teacher-forced
  logprobs, model card, "Serving fidelity") max 0.275 / mean 0.0125,
  inside the kernel-class band (2026-09-02 11:30).
* Placement is exact by construction (rows move, values do not): oracle
  max 0.057 / mean 0.0013 (`CAMPAIGN.md` 2026-09-02 12:10).
* Tuned int2 configs: oracle max 0.086 / mean 0.0018 (2026-09-02 09:13).

## Speed Tests and Profiling

Streaming bench, single request, ~10k context (`tools/bench_speed.py`;
model card, "Performance"; `CAMPAIGN.md`):

* PCIe traffic per decoded token 26 GB -> 0.31 GB with expert-only
  offload and the streamer (`WRITEUP.md` section 5; first working path
  2.24 tok/s -> 15.5 tok/s, sections 4 and 5).
* Tuned int2 configs: decode 13.4 -> 15.2 tok/s (2026-09-02 09:13).
* N-contiguous GEMV micro-benchmark on real layer-5 tensors: 320 GB/s
  from device memory, 51 GB/s (PCIe line rate) from pinned host memory,
  2.1 ms/token all-device; the byte-shuffled table variant reached
  157 / 14 GB/s and was rejected (2026-09-02 08:29-08:31 and 09:45).
* N-contiguous layout + GEMV end to end: decode 19.4 -> 21.8 tok/s,
  prefill 1443 -> 2249 tok/s (2026-09-02 11:30).
* Routing-mass placement: the top 171 of 512 experts per layer cover
  82 % of routing mass on a 2496-token, 3-domain probe (2026-09-02
  09:48-09:53); decode 40.0 -> 48.4 tok/s, prefill 2249 -> 2334 tok/s
  (2026-09-02 12:10).

Part 2/5 of the Qwen3.8-Flash-Next 24 GB serving series; stacked on part 1.
@Jiminator
Jiminator deleted the branch sgl-project:qwen4-main-squashed September 14, 2026 04:45
@Jiminator Jiminator closed this Sep 14, 2026
@hnyls2002 hnyls2002 reopened this Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants