cuda: read the WHT signs as packed bits in a warp-local kernel - #351
Conversation
Reading SIGNS1[t] makes a warp touch 32 constant addresses, which the constant cache serializes. Unused until the next commit. Assisted-by: Claude Opus 5
One group per warp, lane t holding elements 4t..4t+3 as a float4. The two sign multiplies and the four-times-larger instruction count were the cost, not the traffic: 1.560 -> 0.203 ms on Orin at 612 MHz, against a 0.202 ms float4-copy floor. Other group sizes keep the original kernel. Output is bit-identical, verified over 1,572,864 elements in both directions and with and without InnerQ scaling. Assisted-by: Claude Opus 5
…unches MSVC warns on the implicit int64_t narrowing in a kernel launch.
|
Thanks, this is a clean piece of work. Reviewed and tested; merging once CI re-runs on the one-line push below. What I checked:
I pushed e5d6c09: |
|
Ran this on AMD, since the wave size is the one thing a Jetson cannot tell you. Three cards: MI210 (gfx90a, CDNA2, wave64), Radeon RX 7900 XTX (gfx1100, RDNA3, wave32) and Radeon PRO V620 (gfx1030, RDNA2, wave32). Short version: correct everywhere, bit-identical on wave64, and the kernel is roughly twice as fast on all three. Correctness.
Bit-identity on wave64. Perplexity over 8 chunks at n_ctx 2048, turbo3 K and V, Qwen3.8-27B TQ3_1S, both sides built fresh from the same commit: Kernel throughput.
Across the four sizes I measured the speedup runs 1.8x to 2.1x on the MI210, 1.4x to 2.2x on the XTX and 1.6x to 2.7x on the V620, smallest at the smallest size in each case. Note the working set is resident in Infinity Cache on the two RDNA cards, so their absolute figures sit above DRAM bandwidth; the ratio is the meaningful part. End to end it is much smaller, at least here. Prefill on the MI210 with the same model and cache type, native TQ kernels and MMQ prefill enabled:
About 0.15%, consistently positive but nowhere near your 6%. I read that as the rotation being a small share of prefill on a card with this much bandwidth and cache, rather than as a contradiction: your Jetson at 15 W is exactly where constant-cache replays and barriers cost the most. Worth knowing the kernel win is real and large regardless. One review note on the wave64 path. This comment is not accurate on CDNA: A wavefront there is 64 lanes, so one wavefront carries two independent groups and the early return does split it. The code is still correct, but for a different reason than the one given: every shuffle mask you use is at most 16, so Happy to re-run any of the above if you change the shuffle structure. |
80007e7
into
TheTom:feature/turboquant-kv-cache
Overview
Packing WHT rotation sign tables into bit masks & giving each 128-element group a single warp.
These changes remove the constant-cache serialization on the per-thread sign reads and output is bit-identical and prefill throughput rises about 6% on a Jetson Orin Nano.
Additional information
Problem
The WHT rotation kernel assigns one element per thread, so a 128-element group is handled by 128 threads that stage the data through shared memory for the butterfly. In
__constant__ float SIGNS1[128], each thread reads a different address asSIGNS1[t]. The constant cache can broadcast only one address per warp, so a request for 32 distinct addresses is serialized into 32 replays. This read happens twice per kernel invocation. The last two butterfly stages (h=32, 64) also still use shared memory and barriers.My idea to solve the problem
I implemented three changes.
First, I used float4 instead of float, so one thread reads 4 consecutive values instead of one. A group is now owned by a single warp (32 lanes) rather than by 128 threads (4 warps). With the group inside one warp, the first two butterfly stages complete in registers and the rest are handled by __shfl_xor_sync, so shared memory and the barriers disappear entirely.
Second, I store each sign as 1 bit instead of a float. The storage for all signs drops from 512 B (4 B * 128) to 16 B (4 B * 4). The signs for the 4 elements owned by lane t (4t..4t+3) land in exactly one contiguous nibble of word t>>3, so the addresses touched by the whole warp drop from 32 to 4, and 8 lanes share each word, which turns the access into a broadcast.
Third, I apply the sign with XOR instead of a floating-point multiply. In IEEE 754 the top bit is the sign, so x ^ (bit << 31) flips it without a -1.0f multiply.
Correctness
Stage order, pairing, and operand order are unchanged from the original kernel, and flipping the sign bit is the same operation as multiplying by -1.0f. The output is therefore expected to be bit-identical, and I confirmed this by measurement:
Result
Environment
Benchmark settings
1.0fmultiply, so the output is bit-identical.Requirements
I did the problem diagnosis and the design of the optimization (warp-level reorganization, packing the signs into bits, applying the sign with XOR). I used AI for: writing the kernel implementation, writing and running the benchmark harness, aggregating the measurement results, and drafting/translating this pull request description.