Skip to content

[EXL3] route K6/MCG shards away from the B12X decode kernel at prefill row counts - #318

Open
malaiwah wants to merge 5 commits into
local-inference-lab:codex/gg-exl3-r7-k345-20260810from
malaiwah:feat/exl3-b12x-prefill-routing
Open

[EXL3] route K6/MCG shards away from the B12X decode kernel at prefill row counts#318
malaiwah wants to merge 5 commits into
local-inference-lab:codex/gg-exl3-r7-k345-20260810from
malaiwah:feat/exl3-b12x-prefill-routing

Conversation

@malaiwah

@malaiwah malaiwah commented Aug 15, 2026

Copy link
Copy Markdown

Stacked on #316.

The problem

Exl3LinearMethod._apply_one sends any K6/MCG shard with 128-divisible dimensions to
_b12x_trellis_linear before the reconstruct dispatch from #316 is consulted. On a dense
Qwen3.8-27B EXL3 checkpoint with serialized attention that is 208 attention projections, 64
down_proj and the head
— the majority of the model — running a decode-shaped kernel through
prefill. Exl3OnlineLinearMethod.apply does the same unconditionally.

B12X is the right kernel at decode and the wrong one at prefill. Measured on SM120
(RTX PRO 6000, real geometries, median of 10):

shape m=1 m=8 m=128 m=512 m=2048
down_proj 17408x5120 K6 b12x 5.4x b12x 5.4x b12x 1.2x recon 1.4x recon 1.11x
lm_head 5120x248320 K6 b12x 4.1x b12x 5.8x recon 1.26x b12x 1.04x recon 1.40x
attention in_proj_qkvz 5120x16384 K6 b12x 9.4x b12x 2.6x recon 1.52x b12x 1.09x recon 1.08x

The change

Route by row count inside vllm::b12x_trellis_linear_out, which is already an opaque
custom op. A Python-level if rows >= N around the two calls is resolved once at trace time
and would bake the prefill branch into the decode graphs — the same trap documented in #316,
where it cost 56.5 -> 22.6 tok/s at C1 while prefill improved.

Measured end to end

Dense Qwen3.8-27B EXL3 K5/K6 with serialized K5 attention, --max-num-seqs 8, graphs on,
median of 3 runs:

TG C1 TG C4 TG C8 PP 2k PP 6k
B12X everywhere 56.0 197.2 397.3 5,078 5,188
this patch 56.0 197.0 398.8 5,250 5,249

+3.4 % prefill, decode unchanged. Fidelity cost measured on 136 held-out contexts with
exact full-vocabulary KL against BF16: +0.0000377, 95 % CI [−0.00001, +0.00009], 59/136
contexts — a coin flip, i.e. free.

Also in this branch: an opt-in int8 input-embedding overlay

VLLM_EXL3_EMBED_BITS=8 narrows the input embedding table to per-row symmetric int8. On this
model that is 2.543 GB -> 1.272 GB resident. It starts at native 262,144 under a 30.24 GiB
vLLM engine budget (279,007 KV tokens), for +0.000065 mean KLD and unchanged multimodal
scoring. The proof host was a capped 96 GB SM120, not a physically constrained RTX 5090; a
hard-limit 5090 run remains required. Quantization is row-chunked so a full 5.1 GB float32
temporary cannot defeat that use case. It needs the companion model-file change (separate PR)
because VocabParallelEmbedding is constructed without a quant config.

Negative result recorded so nobody repeats it: emitting the reconstructed weight as FP8 E4M3
and using torch._scaled_mm gives +31 % prefill but costs +0.0141 mean KLD — worse
than official FP8. Row-wise scaling does not help; the loss is FP8 activations. Left behind
VLLM_EXL3_PREFILL_FP8=1, off by default.

…gemm

exl3_gemm autotunes with timing launches, so _require_enforce_eager refuses
non-eager execution for every dense tensor_storage checkpoint. Decode capture
sizes are a known finite set, so prime every serialized shard over them during
process_weights_after_loading -- the same pattern _warm_decode_shapes already
uses for the online-K6 path -- and permit decode-only capture behind
VLLM_EXL3_GRAPH_DECODE.

Measured on Qwen3.8-27B EXL3 K4 (RTX PRO 6000, SM120, TP1): decode throughput
+92%/+84%/+98% at C1/C4/C8, distribution parity vs eager exactly 0.000000 KLD
over 32 contexts.
exl3_gemm decodes the trellis once per output tile, so its cost grows with M.
exllamav3's own LinearEXL3.reconstruct_hgemm switches to reconstruct+hgemm above
1024 rows for that reason. Measured crossover with the extension vLLM loads is
m=128, and the win at m=2048 is 4.1-5.2x per geometry.

Dispatch happens inside the existing custom op, at runtime: a Python-level branch
around the call is resolved at trace time, which makes the decode CUDA graphs
capture the prefill path and collapses decode throughput.

Measured on Qwen3.8-27B EXL3 (RTX PRO 6000, SM120, TP1): prefill 2,369 -> 5,050
tok/s at 2k and 2,362 -> 5,146 at 6k, decode unchanged, +0.43% divergence.
Same change as on the vllm-project#314 branch, kept in sync so the stacked prefill
dispatch PR is testable on its own.
…l row counts

Also adds an opt-in int8 input-embedding overlay (VLLM_EXL3_EMBED_BITS=8).
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • dev/*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 644ffc5a-16b0-4502-844f-ff9a547313ff

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@malaiwah

Copy link
Copy Markdown
Author

Hardware-validation request for the embedding overlay (the model-file half is #319): I have narrowed the public claim after auditing the memory receipt. The current evidence is a 96 GB SM120 with vLLM capped to 30.24 GiB; that is an engine-budget proof, not a hard physical 32 GB proof.

If someone has an RTX 5090, the closing test is:

The capped run allocated 279,007 KV tokens and passed 3/3 retrievals. The overlay now quantizes in 4,096-row chunks, avoiding the previous 5.1 GB float32 load-time temporary. A report of total VRAM, startup accounting, exact commit SHAs, and request outcome would close the remaining hardware caveat.

@malaiwah

Copy link
Copy Markdown
Author

Production data from the single-5090 Qwen3.8-27B deployment, independently converging on this PR's dispatch-by-row-count conclusion — with one asymmetry worth recording.

Decode side (confirms this PR): we shipped an env-gated M-threshold (VLLM_EXL3_B12X_MIN_M=128, b12x for m≥128, fused exl3_gemm below) on the all-trellis profile. n=3 boots: TG-essay 90.0±0.2 → 93.1±0.1 (+3.4%), MTP acceptance 0.281 → 0.304 (+8.2%), prefill unchanged (1966.8±8.3 → 1965.4±2.1). The fused kernel doesn't just decode faster — it drafts better, so the win compounds through speculative acceptance.

Prefill side (contradicts the m=2048 column, with a suspected cause): our end-to-end control measured b12x winning K6 prefill decisively — all-recon PP 1504 vs b12x-for-K6 1967 at m=3072 on RTX 5090 — where this PR's table has recon 1.08–1.4× ahead at m=2048. Difference in the recon implementations: ours allocates a fresh full-size (K,N) fp16 buffer per call and folds full-width (per-call cache disabled for KV-budget reasons); this PR's stack sits on #316's persistent per-geometry scratch. #397's shared arena is plausibly the whole gap — we've queued adopting it and re-running the A/B.

One caution for adopters: our dispatch is a Python-level branch, safe only because decode CUDA graphs are shape-specialised and compile=NONE. This PR's placement inside the custom op is the correct general solution, exactly per its own warning.

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