Conversation
HIP implements __byte_perm as a software routine, so the existing Q1_0 unpack issues 20 software calls per 32-element chunk (2 for the nibble indices, 4 for the byte values, 4 to unshuffle, twice per block). That makes Q1_0 decode VALU-bound rather than bandwidth-bound on AMD. Spread the sign bits arithmetically instead and select the byte constants with one hardware v_perm_b32 per nibble: n * 0x00204081 lands bit i at position 8i, the mask leaves one sign bit per byte, and 0x0D - spread picks 0xFF or 0x00 per lane. Same shape as the Q2_0 HIP path from ggml-org#26753. The non-HIP branch is unchanged. Measured on 1x Radeon AI PRO R9700 (gfx1201), Bonsai-27B Q1_0, tg64 r=5, interleaved: 29.402 -> 61.657 t/s (+109.7%). Correctness: exhaustive equivalence over all 65536 possible q values is bit-identical to the __byte_perm path, and test-backend-ops -o MUL_MAT passes all 11 q1_0 cases.
|
Hi @The-Monk, thanks for your contribution! Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:
Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below. |
There was a problem hiding this comment.
Please test against master with #25628 reverted, for nvidia the previous seemingly naive unpacking resulted in extremely efficient code. You might also want to test applying the same changes to mmq_load_tiles.cuh and see if it improves prefill as well.
| const uint32_t n = (q >> (4 * j)) & 0x0Fu; | ||
| const uint32_t spread = (n * 0x00204081u) & 0x01010101u; | ||
| const uint32_t sel = 0x0D0D0D0Du - spread; | ||
| return (int) (__builtin_amdgcn_perm(0u, 0u, sel) | spread); |
There was a problem hiding this comment.
Can these two lines be a single __builtin_amdgcn_perm? I think it should be possible to directly select from 0x00/0x01 to 0xFF/0x01 via __builtin_amdgcn_perm(0x01FF, 0x01FF, spread).
Overview
Follows the same approach as #26753, which did this for
Q2_0.HIP has no hardware
__byte_perm; it lowers to a software routine. The currentQ1_0unpack calls it 20 times per 32-element chunk -- 2 for the nibble indices, 4 for the byte values, 4 to unshuffle, twice per block. That makesQ1_0decode VALU-bound on AMD rather than bandwidth-bound.This spreads the sign bits arithmetically and selects the byte constants with one hardware
v_perm_b32per nibble:n * 0x00204081lands bitiat position8i, so& 0x01010101leaves one sign bit per byte0x0D - spreadselects thev_perm_b32byte constant0xFF(sel0x0D) or0x00(sel0x0C)-1/+1as int8One helper plus an
#if defined(GGML_USE_HIP)/#else. The non-HIP branch is unchanged, so CUDA behaviour is identical.Measured
1x Radeon AI PRO R9700 (gfx1201), ROCm/TheRock 7.13, Bonsai-27B
Q1_0(3.53 GiB),-p 0 -n 64 -r 5, interleaved against a clean build of the same base commit:mean 29.402 -> 61.657 t/s (+109.7%)
Correctness
qvalues, both paths compiled for gfx1201, all 4 nibbles each -- bit-identical to the__byte_permreference. The change cannot alter numerics.test-backend-ops -o MUL_MAT: all 11q1_0cases pass (n=1..9, k=256 and 4096).Caveats
#else, but it has not been run.Q1_0still will not beatQ2_0on this hardware even with this fix -- that appears to be a property of sub-2-bpw formats rather than of any implementation (cf. ik_llama.cpp#45 on RTX-4080, and ggml-vulkan: Add support for TQ1_0 and TQ2_0 types for MUL_MAT #19743's RTX 4090 Vulkan numbers). This removes an AMD-specific software-lowering penalty; it does not change the format's ceiling.Additional information
Context and further measurements in #27127, including a note on why the figure there was originally reported as +16% (that was measured against a fork where other changes already masked most of the loss, not against master).
Requirements
test-backend-ops; and draft this description. I have reviewed the diff and am responsible for it.