[ROCm][Perf] W4A16: packed fp16 prefill dequant - #55711
Draft
mgehre-amd wants to merge 1 commit into
Draft
mgehre-amd wants to merge 1 commit into
mgehre-amd wants to merge 1 commit into
Conversation
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ction The Triton prefill path of RDNAHybridW4A16LinearKernel is VALU-issue-bound on RDNA3, and its tile ladder was tuned before the dequant changed shape. Packed fp16 dequant. OR-ing a 4-bit code n into the low mantissa of fp16 1024.0 (0x6400) bitcasts to exactly 1024+n. Applying that to a full 32-bit lane with one v_and_or_b32 dequants a nibble PAIR per instruction, against the scalar v_and_b16 + v_or_b16 pair Triton emits from the elementwise form. The ExLlama shuffle already stores val[2p] at bits [4p:4p+4] and val[2p+1] at [16+4p:20+4p], so a single pre-shift by 4p yields both halves of a half2 in K order and the downstream affine packs into v_pk_fma_f16. Dtype-aware gfx1151 tile selection. The packed fp16 path and the scalar bf16 path want different tiles -- most visibly BLOCK_N at deep M, 256 against 64 -- so a single M-ladder cannot serve both. Measured on gfx1151, 3 interleaved reps per arm, 2048-token prefill: Qwen3-8B-quantized.w4a16 fp16 asymmetric +7.8% prefill throughput / -7.3% TTFT; Qwen3-4B-quantized.w4a16 fp16 symmetric +13.0% / -11.5%. Both fp16 rep ranges are disjoint. The bf16 control lands at +0.02% with overlapping rep ranges, i.e. the null it should be. Changes: - The kernel resolves the unpack itself, from its own compile target and the activation dtype -- there is no flag to pass and no way for the host's view of the GPU to disagree with what is being compiled. That needs a @triton.constexpr_function helper, which in turn needs constexpr_function added to vLLM's Triton placeholder shim: this module is imported on every platform, and the shim exposed jit/autotune/heuristics/Config but not constexpr_function, so a module-scope use raised AttributeError on builds without Triton. A dummy decorator is sufficient there, since such bodies only run while a kernel is being compiled. - The packed dequant is enabled for the whole gfx11 family on fp16: fp16 is a hard requirement of the magic constant, and given that, the packed form is a pure instruction-count reduction producing bit-identical values, so there is nothing to tune per part. Only the tile table, which IS tuned, stays gfx1151-gated. Verified in the generated ISA: the fp16 kernel contains 65 v_and_or_b32, the bf16 one none from the dequant. - num_stages=1 is applied to the fp16 arm ONLY. Applying it to bf16 as well measured -15.8% end-to-end prefill on an asymmetric bf16 model: the packed fp16 path issues one per-group load and does not miss the pipelining, while the scalar asymmetric path issues two (scale and zero point) and needs the software pipeline to hide the second gather. - The bf16 arm is therefore byte-for-byte the pre-existing scalar-tuned ladder, its per-shape override table, and its pipeline depth. Verified identical to the previous selection across all 576 shape x group-size combinations swept. - Scope is gfx1151 only. Widening the tuned table to the whole gfx11 family was considered and rejected: it was swept on gfx1151, and gfx1100 is a 96-CU discrete part that cannot be measured here. - #923's K>=4096 and N>=4096 bf16 tile branch was evaluated and deliberately NOT carried over. At group_size 128 it is shadowed by the later, more specific per-shape override table for every shape that table covers; at group_size 32/64 its distinguishing BLOCK_K=128 collapses to the group size anyway. It would only ever fire on shapes nobody measured, in a region where the later sweep disagreed with it. - The kernel benchmark gains --dtype: it was fp16-hardcoded and so could not reach the scalar bf16 path at all. Numerics. The dequant arithmetic is unchanged: 1024+n is integer-exact in fp16, whose 11-bit mantissa covers every integer below 2048, so folding the 1024 into the subtrahend -- (b_raw - (1024 + zp)) == (nibble - zp) -- is exact and the multiply that follows rounds once, as before. End-to-end output is nonetheless not GUARANTEED identical on fp16, because the new tiles move BLOCK_K (64 -> 32 on most shapes) and so change the fp32 accumulation order. Measured: greedy decode over 6 fixed prompts is byte-identical on all three configurations, and GSM8K 5-shot over 500 questions is unchanged on the asymmetric model (0.884 -> 0.884) and moves by one question on the symmetric one (0.848 -> 0.850), against a ~1.5pp single-arm sampling error. Testing: pytest tests/kernels/quantization/test_rdna_hybrid_w4a16.py tests/kernels/quantization/test_w4a16_kernel_selection.py -- 103 passed on gfx1151 (Radeon 8060S, torch 2.11.0+rocm7.15, Triton 3.8.0).
mgehre-amd
force-pushed
the
matthias.w4a16-triton-packed-dequant
branch
from
September 7, 2026 11:31
cf0c3ca to
0a42169
Compare
Contributor
|
This pull request has merge conflicts that must be resolved before it can be |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[ROCm][Perf] W4A16: packed fp16 prefill dequant and gfx1151 tile selection
Purpose
RDNAHybridW4A16LinearKernelroutes prefill (M > 5) through a Triton fused-dequant GEMM.On RDNA3 that inner loop is VALU-issue-bound: the dequant instructions, not the WMMAs
and not memory, set the rate. This PR attacks the instruction count and retunes the tiles
that the new dequant wants.
Performance
Same build, only
rdna_hybrid_w4a16.pyswapped between arms; arms interleaved(before, after, before, ...) so thermal and clock drift hit both. 3 reps per arm,
--input-len 2048 --output-len 1 --num-prompts 10(prefill-weighted, so M is well past theskinny-decode threshold). Medians.
RedHatAI/Qwen3-8B-quantized.w4a16RedHatAI/Qwen3-4B-quantized.w4a16RedHatAI/Qwen3-8B-quantized.w4a16The bf16 row is the control, it is not affected by this PR.
1. Packed fp16 dequant
For a 4-bit code
n,0x6400 | nreinterpreted as float16 is exactly1024 + n(fp16's 11-bit mantissa represents every integer below 2048, so nothing rounds). Applying
that to a whole 32-bit lane with a single
v_and_or_b32—(x & 0x000F000F) | 0x64006400— dequantizes a nibble pair per instruction, against the scalar
v_and_b16+v_or_b16pair Triton emits from the elementwise form.
The ExLlama shuffle layout this kernel already uses makes the pairing free: an int32 holds
val[2p]at bits[4p:4p+4]andval[2p+1]at bits[16+4p:20+4p], so one pre-shift by4p(p = 0..3) puts both halves of ahalf2in K order with no cross-lane shuffling. Thedownstream affine then also packs, into
v_pk_fma_f16.The dequant arithmetic is unchanged. The nibble arrives as
1024 + n, so the subtrahendabsorbs the 1024:
(b_raw - (1024 + zp)) == (nibble - zp), exactly, and the multiply thatfollows rounds once as before.
2. dtype-aware gfx1151 tile selection
The packed fp16 path and the scalar bf16 path want materially different tiles — most visibly
BLOCK_Nat deep M, 256 against 64 — so one M-ladder cannot serve both.BLOCK_Kstayscapped to
group_sizeso a K-block never straddles a quant group.The packed dequant is enabled for the whole gfx11 family; only the tile table, which is
what actually gets tuned, stays gfx1151-gated.
num_stages=1is applied to the fp16 armonly. Applying it to bf16 as well measured
−15.8% end-to-end prefill on an asymmetric bf16 model: the packed fp16 path issues one
per-group load and does not miss the pipelining, while the scalar asymmetric path issues two
(scale and zero point) and needs the software pipeline to hide the second gather.
The bf16 arm is consequently byte-for-byte the pre-existing scalar-tuned ladder, its
per-shape override table, and its pipeline depth — verified identical to the previous
selection across all 576 shape × group-size combinations swept. The measured bf16
control below is the null this predicts.
Duplicate-work check
Two open PRs touch
rdna_hybrid_w4a16.py; neither overlaps this change:the weight-processing path; it does not touch the Triton prefill dequant or the tile table.
Whichever of the two lands second rebases — the conflict is mechanical.
process_weights_after_loading.Orthogonal to the prefill kernel body.
No open PR changes the prefill dequant or the gfx1151 tile selection.
Scope
Deliberately gfx1151 only.
Testing
New tests:
symmetric and asymmetric dequants. A wrong magic constant or wrong ExLlama bit position
moves the dequantized weight by whole integers, so this catches it comfortably.
BLOCK_K > group_size, swept in Python so the invariant holdsfor shapes there is no hardware test for.
Hardware: gfx1151 (Radeon 8060S, 40 CU), torch 2.11.0+rocm7.15, Triton 3.8.0.
Model evaluation
GSM8K, 5-shot, 500 questions, via the in-tree
tests/evals/gsm8kharness, same before/afterfile swap as above:
RedHatAI/Qwen3-8B-quantized.w4a16RedHatAI/Qwen3-4B-quantized.w4a16Greedy decode (temperature 0, 6 fixed prompts, 64 tokens each), before vs after:
RedHatAI/Qwen3-8B-quantized.w4a16RedHatAI/Qwen3-4B-quantized.w4a16RedHatAI/Qwen3-8B-quantized.w4a16Byte-identical output is the observed result here, not a guarantee: the fp16 tiles move
BLOCK_Kfrom 64 to 32 on most shapes, so the fp32 accumulation order does change and adifferent prompt set could land differently. The probe was confirmed deterministic run to run
(a same-arm repeat is 6/6), so these are real comparisons rather than sampling noise.
AI assistance
AI assistance (Claude) was used for this change: to derive the split from a larger internal
branch, write the code and tests, and run the measurements. I have reviewed every changed
line, and ran the tests and benchmarks reported above myself.