Skip to content

[Kernel] TD path for fused_moe_kernel_gptq_awq (int4)#6

Open
afierka-intel wants to merge 3 commits into
mainfrom
afierka/moe-gptq-awq-triton-td
Open

[Kernel] TD path for fused_moe_kernel_gptq_awq (int4)#6
afierka-intel wants to merge 3 commits into
mainfrom
afierka/moe-gptq-awq-triton-td

Conversation

@afierka-intel

@afierka-intel afierka-intel commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a tensor-descriptor (TD) A-gather + B-load path to fused_moe_kernel_gptq_awq, for int4_w4a16 only (GPTQ/AWQ, packed 2 nibbles/byte along K).
  • E2E perf is neutral-to-positive — real vllm bench sweep serve numbers across prefill/decode/mixed scenarios show TD roughly matching or beating the pointer path in most metrics (see below). An earlier kernel-level microbenchmark (isolated fused_experts() calls/s in a tight loop) suggested a 30%+ regression at larger batch — that metric was flawed (no scheduler/sampler/serving context, and calls/s isn't comparable in the way it was presented) and has been replaced with real E2E numbers. This is infrastructure-completion work for RFC #42545.
  • int8_w8a16 has no TD path at all — force-disabled at the launch site due to a Triton-XPU 3.7.1 codegen bug (descriptor-loaded uint8 B tile × dequant scale → tl.dot produces garbage at this kernel's default BLOCK_SIZE_N=64). Pinned by a dedicated regression test so a future accidental re-enable fails loudly instead of corrupting output. Filed upstream: intel/intel-xpu-backend-for-triton#7510, with a minimal standalone repro and full isolation notes.
  • TD is XPU-only, gated in the resolver. .gather() requires Blackwell (sm_100+) on CUDA and hard-fails PTX codegen on older archs — verified empirically on H200 (Hopper).

Perf — real E2E serve sweep, not a kernel microbenchmark

vllm bench sweep serve, N=3 median per scenario, real server (not isolated kernel calls), Qwen/Qwen1.5-MoE-A2.7B-Chat-GPTQ-Int4, --quantization moe_wna16 (forces this kernel's dispatch), seed=42, prefix caching off:

Scenario req/s (off→on) Δ output tok/s (off→on) Δ mean TTFT ms (off→on) Δ mean TPOT ms (off→on) Δ
Prefill-heavy (4000 in / 200 out) 0.81→0.79 -2.9% 161.82→157.10 -2.9% 12275.70→11933.12 -2.8% 103.05→111.55 +8.2%
Decode-heavy (200 in / 2000 out) 0.20→0.22 +10.7% 398.77→441.42 +10.7% 374.86→346.44 -7.6% 24.90→22.49 -9.7%
Mixed (1024 in / 1024 out) 0.46→0.51 +9.8% 475.06→521.60 +9.8% 1917.73→1765.15 -8.0% 40.15→36.53 -9.0%
  • Decode-heavy and mixed both improve with TD on (~+10% throughput, ~-8-10% latency). Prefill-heavy is roughly neutral (within a few %, TPOT slightly worse).
  • This supersedes an earlier kernel-level microbenchmark that isolated fused_experts() in a tight loop (calls/s at fixed batch sizes) and showed an apparent 30%+ regression at m≥64 — that metric didn't reflect real serving (no scheduler, no sampler, no request-level concurrency dynamics) and the cross-row comparison it invited wasn't meaningful. Retracting that claim; the numbers above are the real signal.
  • No recommendation to reconsider the auto-on-XPU default at this point — the E2E data doesn't show the regression the earlier microbenchmark suggested.

Test plan (updated)

Kernel-level (unit)

  • tests/kernels/moe/test_fused_moe_kernel_gptq_awq.py33/33 pass on B70 (XPU-portable, unlike test_moe.py::test_fused_moe_wn16 which is hardcoded to device="cuda").
    • int4 TD-vs-pointer bit-exact comparison, including K-tail (block_k_diviable=False) zero-fill case.
    • test_int8_td_forced_off_at_default_config pins the int8 guard.
  • Isolated tl.interleave + tensor-descriptor micro-test validated on B70 hardware before integration.

E2E / model-level (added — see note below on the earlier claim)

  • tests/models/quantization/test_gptq_awq_moe_td.py — new committed test: loads the real Qwen/Qwen1.5-MoE-A2.7B-Chat-GPTQ-Int4 checkpoint via vllm_runner, compares greedy-decode logprobs TD-off vs TD-on via check_logprobs_close. 1/1 pass on B70.
  • tests/evals/gsm8k/configs/Qwen1.5-MoE-W4A16-TD-XPU.yaml + configs/models-xpu-td.txt — new committed gsm8k accuracy config (TD forced on via env: block), runnable via pytest tests/evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-xpu-td.txt. accuracy_threshold is a real measured value: 0.4473 on B70 (1319 questions, 5-shot, greedy decode). A second confirmation run measured 0.4458, within the harness's default 0.08 tolerance — expected GPU float non-determinism, not a regression. 1/1 pass.
  • Correction to the previously reported "20/20 exact-match gsm8k" E2E result: that manual validation used the model's default dispatch path, which — for this model's shape (hidden=2048, intermediate=1408, both Marlin-tile-aligned) — routes to the native XPUExpertsWNA16 (cutlass) backend, not this PR's Triton kernel (fused_moe_kernel_gptq_awq). The new committed test above forces --quantization moe_wna16 to actually exercise the changed code path. The prior 20/20 result is real but doesn't demonstrate what it was reported as demonstrating; the new test does.

Upstream bugs found and fixed while building the above (all independent of this PR's kernel change)

Forcing --quantization moe_wna16 on a real GPTQ checkpoint (rather than this kernel's own logic) surfaced 4 pre-existing issues in the general GPTQ/MoE-WNA16 loading path on fresh main:

  1. _moe_C::moe_sum() XPU/CUDA arg-count mismatch (unrelated upstream commit f7aadae5e5 added args the XPU kernel wasn't updated to match) — fixed in vllm-xpu-kernels#462, released as v0.1.11.1. Verified the fix on real B70 hardware (reproduced the crash on the currently-pinned v0.1.11, confirmed the 4-arg call succeeds and is bit-exact against a torch.sum reference after overlaying v0.1.11.1). Opened the pin-bump PR upstream: vllm-project/vllm#48942 (self-reviewed first as afierka-intel/vllm#11).
  2. modules_in_block_to_quantize list-of-lists handling — already covered by open PR vllm-project/vllm#35865; not duplicated here.
  3. packed_modules_mapping not propagated to per-layer delegate quant configs in moe_wna16.pyalso covered by PR#35865; not duplicated here.
  4. Unregistered checkpoint tensors (bias/g_idx) crashing instead of being skipped in weight loading — genuinely new, fixed with regression tests: afierka-intel/vllm#9 (linear layers) and afierka-intel/vllm#10 (MoE experts).

None of these are required to review/merge this PR's actual kernel change — they're prerequisites only for reproducing the E2E test on fresh main + XPU right now, and are called out here for transparency.

  • ruff check + ruff format --check clean; local pre-commit run all green.
  • Duplicate-work check: no existing/competing PR found for this specific kernel conversion. #43389 (ROCm int4 repacking) touches the same kernel signature/K-loop with different intent — real textual rebase risk, no logical conflict, flagged for reviewers.

Gated on VLLM_TRITON_USE_TD (tri-state). #45781 merged upstream 2026-07-15 as the canonical registration for this env var; this PR's own registration was removed at rebase.

AI assistance was used (Claude Code) for implementation and testing; every changed line was reviewed and all tests/benchmarks above were run personally on B70 and H200 hardware.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Once the PR is approved and ready to go, your PR reviewer(s) can run CI to test the changes comprehensively before merging.

To run CI, PR reviewers can either: Add ready label to the PR or enable auto-merge.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@afierka-intel
afierka-intel force-pushed the afierka/moe-gptq-awq-triton-td branch from afaa866 to 6d2761a Compare July 7, 2026 10:38
Adds a tensor-descriptor (TD) A gather + B load path to
fused_moe_kernel_gptq_awq for int4_w4a16 (GPTQ/AWQ, packed 2
nibbles/byte along K), closing the last MoE Triton kernel without any
TD coverage. This is infrastructure-completion work for RFC vllm-project#42545's
adoption goal, not a proven optimization -- kernel-level benchmarks
below show a mixed perf picture, not a clear win.

B's packed layout is handled by building the descriptor at half-K-width
(byte granularity) and using tl.interleave to reconstruct the full-K
tile from the unpacked nibbles, matching the existing pointer path's
even-k/low-nibble, odd-k/high-nibble convention. Verified bit-exact
against the pointer path on B70, including the K-tail
(block_k_diviable=False) case that exercises the descriptor's
automatic zero-fill.

int8_w8a16 has no TD path in this kernel: a Triton-XPU 3.7.1 codegen
bug corrupts output whenever a descriptor-loaded uint8 B tile is
multiplied by a dequant scale and fed into tl.dot, at exactly the
BLOCK_SIZE_N=64 config this kernel's wna16 launcher uses by default for
int8. Root-caused via isolated micro-probes (tile-shape sweep: fails
whenever BLOCK_SIZE_N>=64 and BLOCK_SIZE_M!=32; ruled out the bf16-cast
workaround that fixed a similar unified-attention bug, multiply-before-
transpose, and splitting the dot width -- none fixed it) to a genuine
compiler issue, not this kernel's logic. resolve_moe_gptq_awq_use_td()
never returns True for int8 regardless of the env var, and a dedicated
regression test pins this so a future accidental re-enable is caught
loudly instead of silently corrupting output. The upstream bug report
is a follow-up, not blocking this change.

TD is XPU-only. On-device tl.make_tensor_descriptor(...).gather() (used
for A) requires Blackwell (sm_100+) on CUDA and fails PTX codegen on
older architectures -- verified empirically on H200 (Hopper, sm_90):
`.gather()` hard-fails at ptxas regardless of whether triton.set_allocator
is called first. resolve_moe_gptq_awq_use_td() now checks
current_platform.is_xpu() before consulting the env var override, so
VLLM_TRITON_USE_TD=1 can never route this kernel through
device-side TD construction on non-XPU hardware.

Gated on VLLM_TRITON_USE_TD (tri-state), independent of the shared
registration in vllm-project#42436 / #5 / vllm-project#45781 -- whichever lands
first keeps it, this one dedupes at rebase.

Perf (kernel-level, isolating fused_moe_kernel_gptq_awq via the real
fused_experts dispatch path, N=3 median per shape, Qwen1.5-MoE-A2.7B
dims: hidden=2048, intermediate=1408, experts=60, topk=4): m=1 -0.4%
(noise), m=16 +14.8%, m=64 -33.5%, m=256 -32.3%. TD trades a small
decode-batch win for a real regression at moderate/large batch on this
kernel; it is not a net perf improvement. E2E gsm8k on
Qwen1.5-MoE-A2.7B-Chat-GPTQ-Int4 (20 real prompts, greedy decode):
20/20 exact-match generations TD-off vs TD-on, +1.7% median throughput
in a hand-rolled offline-generate harness (not vllm bench) -- consistent
with the kernel-level mixed picture given the model's batch-size mix.

Correctness is unaffected either way; this is reported for reviewers to
judge the tradeoff, not as a justification to default this on.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Artur Fierka <artur.fierka@intel.com>
afierka-intel and others added 2 commits July 17, 2026 09:47
Adds the committed E2E/accuracy test artifacts referenced in the PR
description but missing from the actual diff: a model-level TD-on vs
TD-off correctness test (forcing --quantization moe_wna16 so the test
actually exercises fused_moe_kernel_gptq_awq, since this model's native
shape would otherwise dispatch to the native XPUExpertsWNA16 backend)
and a gsm8k accuracy config for the same forced-TD path.

- tests/models/quantization/test_gptq_awq_moe_td.py: check_logprobs_close
  between TD-off and TD-on greedy-decode output on
  Qwen/Qwen1.5-MoE-A2.7B-Chat-GPTQ-Int4. 1/1 pass on B70.
- tests/evals/gsm8k/configs/Qwen1.5-MoE-W4A16-TD-XPU.yaml +
  configs/models-xpu-td.txt: gsm8k accuracy harness config, TD forced on
  via env. accuracy_threshold is still a placeholder pending a real
  measured run.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Artur Fierka <artur.fierka@intel.com>
Replaces the placeholder accuracy_threshold with the actual value
measured on B70: 0.4473 (Qwen/Qwen1.5-MoE-A2.7B-Chat-GPTQ-Int4,
VLLM_TRITON_USE_TD=1, --quantization moe_wna16, 1319 questions,
5-shot, greedy decode). A second confirmation run measured 0.4458,
within the harness's default 0.08 tolerance (expected GPU float
non-determinism, not a real accuracy regression).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Artur Fierka <artur.fierka@intel.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant