Keep W4A16 zero-points packed at 4 bits instead of expanding to 2 bytes - #1073
Draft
mgehre-amd wants to merge 1 commit into
Draft
Keep W4A16 zero-points packed at 4 bits instead of expanding to 2 bytes#1073mgehre-amd wants to merge 1 commit into
mgehre-amd wants to merge 1 commit into
Conversation
process_weights_after_loading unpacked the zero-points and stored them in the activation dtype, so the skinny GEMM streamed 2 bytes per group for values that only ever span 0..15. The checkpoint already ships them packed 8-per-int32 along N; expanding them cost 4x the DRAM traffic. On gemma-4-31B-it-AWQ-4bit gate_up (1x43008x5376, g=32, asymmetric) that is 14.5 MB per call instead of 3.6 MB, against 115.6 MB of weights. The kernel is memory-bound -- every wvsplitk_int4 shape sits at 187-204 GiB/s of real traffic, next to 216 GiB/s for the fp16 lm_head in the same trace -- so those bytes are time, and removing them is the only lever that moves it. Two earlier attempts that rearranged loads without changing byte counts (wider A_CHUNK, staging scale/zp in LDS) both measured within noise; this one lands. The kernel now takes the packed tensor and extracts the nibble: row n -> word[n/8], bits 4*(n%8), matching the layout unpack_quantized_values_into_int32(packed_dim=0) inverts. The unpacked values are still built at load time, but only transiently, for the Triton scale/zp carrier and the optional dense dequant copy, both of which need them expanded. Measured on gfx1151, 5 reps, median per-call kernel time: gate_up 1x43008x5376 642.3 -> 608.5 us (-5.3%) qkv 1x16384x5376 251.7 -> 238.1 us (-5.4%) down 1x5376x21504 340.0 -> 316.7 us (-6.8%) against a -7.5% byte reduction, so 75-90% of the theoretical maximum. End to end on the same model: decode 9.0 -> 9.9 tok/s (+10.0%), TTFT 2297 -> 2093 ms (-8.9%). Decode moves from 79.6% to 87.6% of the roofline for that configuration. Changes: - Kernel resource footprint is unchanged (VGPR 86, no spills, no scratch, LDS 65536): the shift+mask absorbs into existing pressure, which is free on a memory-bound kernel. - The op now rejects act-dtype zero-points instead of reinterpreting them, since a silently misread layout produces plausible garbage. - MoE reuses the same compute bodies, so it follows automatically; expert_stride_zp is already in int32 elements and needs no adjustment. In practice the compressed-tensors MoE path passes no zero-points. - Adds value-level coverage to test_hip_w4a16.py. Nothing in the suite checked what wvSplitK_int4_g actually computes, so a wrong nibble index would have been silent. Covers fp16/bf16 x g32/g128 x three shapes including N=8 (one packed word), plus the rejection path. Signed-off-by: Matthias Gehre <matthias.gehre@amd.com>
1 task
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.
Keep W4A16 zero-points packed at 4 bits instead of expanding to 2 bytes
Problem
process_weights_after_loadingunpacked the zero-points and stored them in the activationdtype, so the skinny GEMM streamed 2 bytes per group for values that only ever span 0..15.
The checkpoint already ships them packed 8-per-int32 along N; expanding them cost 4x the DRAM
traffic.
On
gemma-4-31B-it-AWQ-4bitgate_up (1x43008x5376, g=32, asymmetric) that is 14.5 MB percall instead of 3.6 MB, against 115.6 MB of weights.
This matters because the kernel is at the memory roofline. Counting all bytes it streams
(weights + scale + zero-point), every
wvsplitk_int4shape sits at 187-204 GiB/s next to216 GiB/s for the fp16
wvSplitKlm_head in the same trace. There is no scheduling headroomleft: two earlier experiments that rearranged loads without changing byte counts (a wider
A_CHUNK, staging scale/zp in LDS) both measured within noise. Removing bytes is the onlylever that moves this kernel.
Change
The kernel takes the packed tensor and extracts the nibble: row n ->
word[n/8], bits4*(n%8), matching the layoutunpack_quantized_values_into_int32(packed_dim=0)inverts.The unpacked values are still built at load time, but only transiently, for the Triton
scale/zp carrier and the optional dense dequant copy, both of which need them expanded.
Results (gfx1151)
Per-call kernel time, 5 reps, median:
1x43008x53761x16384x53761x5376x2150475-90% of the theoretical maximum realised, which is what you expect if time tracks bytes.
End to end on the same model (unprofiled, 10 prompts):
Kernel resource footprint is unchanged (VGPR 86, no spills, no scratch, LDS 65536) -- the
shift+mask absorbs into existing register pressure, which is free on a memory-bound kernel.
Known regression / why this is a draft
Re-running every golden entry that moved by more than the tolerance band, three reps each,
five are reproducible slowdowns with an exact shared signature:
-zp-zp-zp-zp-bf16-zpAll g=128, all bs=4, all asymmetric. That is a mechanism, not noise:
Testing
pytest tests/kernels/quantization/test_hip_w4a16.py-> 119 passed (106 pre-existing + 13new).
This PR adds the value-level coverage that did not exist: nothing in the suite checked what
wvSplitK_int4_gactually computes, so a wrong nibble index would have been silent. New testscompare against a reference dequant across fp16/bf16 x g32/g128 x three shapes, including
N=8(exactly one packed word), plus a test that the op rejects act-dtype zero-points ratherthan reinterpreting them.
End-to-end:
gemma-4-31B-it-AWQ-4bitMM sanity check passes (-> 'Red'), 10/10 requests,1280 output tokens.
Goldens deliberately not included
golden/hybrid_w4a16_gfx1151.jsonis not updated here. Regenerating it showed the expected+6.3% median on g=32 asymmetric entries (vs +7.5% theoretical), and symmetric entries unmoved
as a control -- but re-running the outliers found 11 newly-recorded entries that do not
reproduce: the fresh median matches the previous value instead. That is the recording-run
reproducibility issue already open on #1065, and committing those goldens would bake in
failures. They should be regenerated once that is resolved.