Repository navigation
perf(gguf): share Q4_K/Q6_K vector loads across lanes - #134
Conversation
iamwavecut
left a comment
There was a problem hiding this comment.
Thanks, great numbers! The shuffles look right, and the explicit masks are exactly what we asked for.
A few changes before we merge:
- Please drop the CMake option and the
__CUDA_ARCH__ == 860check. We'd like this on every card, picked per (type, T) through device-profile routes, likeq6_headorq4_linear_swiglu. Your 3090 results can go into its profile (Q6_K T1–8, Q4_K T1), and the calibration tool can measure the route on other cards. - The MoE decode kernel runs the same
Decoderloop. Could you enable it there too? MoE models spend most of their decode time in experts, and the CPU expert parity still holds. - Prefill is −0.11% with a CI that excludes zero. Any idea where that comes from?
Select scalar or cooperative loads through device profiles and calibration for dense, MoE-vector and decode-up kernels. Seed the measured RTX 3090 dense routes and preserve decoded values and dot-product rounding. Cover host dispatch, prepared MoE inputs, tails and accumulation with exact comparisons. Qualify the public GGUF oracle and paired Engine runs.
b42619a to
b4081ed
Compare
|
Thanks! I’ve switched this to device profiles and added calibration for dense and MoE kernels. The CMake option and sm86 guard are gone. I also reran both routes in the same binary. There’s no statistically significant prefill difference in that run, but I haven’t pinned down what caused the earlier slowdown. I’ve kept the earlier measurements in the description too. |
Q4_K and Q6_K run the GGUF Linear oracle cases once with scalar and once with cooperative loads forced, so the cooperative route is qualified directly. Drop the device-profile checks that restated the band lookup and the RTX 3090 seed.
… calibrates Name the GGUF vector and MoE schedules beside the GDN switch in the Volta comment and guide, without the superseded V100 timing, and record the GGUF seed in the RTX 3090 profile's origin.
…X 3090 ninfer-calibrate --only gguf_ on an RTX 3090 at 350 W picked cooperative Q6_K MoE products at chunks 1 to 4 (9-16% faster) and Q6_K decode-up at chunk 4, and kept every Q4_K MoE route scalar; the six keys took 2.1 s. The dense seed stays: calibration also takes Q6_K at widths 1 to 8, and Q4_K at width 1 loses only on the fused form while whole-model decode of Qwen3.8-27B GSQ-RCO IQ3_S (Q4_K is 14.7% of its weights, the head among them) gains 0.7% plain and 1.2-1.3% with MTP3, two ABBA blocks each, prefill within noise and the greedy text unchanged.
iamwavecut
left a comment
There was a problem hiding this comment.
Thanks, this looks good! We added three small commits on top: the GGUF FP64 oracle test now runs Q4_K/Q6_K with both load schedules, a docs/comment cleanup, and RTX 3090 MoE Q6_K seeds from calibration. On a 3090, Qwen3.8-27B GSQ-RCO IQ3_S decode went +0.7% plain and +1.2% with MTP3, with identical greedy output. Merging once CI is green.
Twenty-seven commits upstream since a53a4f3, 86 files and 5049/941 lines. The substantive ones: 8c2e864 bounds the fast INT8 prompt kernel's V rescale, b4081ed shares Q4_K/Q6_K vector loads across lanes with cc24f1d taking the cooperative schedule on the RTX 3090 (PR iamwavecut#134, liqlos), 4285bcc links the device route profiles into the Windows kernel dll, 35c749c reduces every logprob row over one vocabulary split, 4d40f1b separates the CPU pool counters and issues copies first, and the DeepSeek line gains DSpark drafting, a CPU share of host-expert misses and four decode/perf kernels. The rest are docs and CI cache pruning. Eight of those files are ones this fork also changes; seven merged on their own (README.md, docs/device-profiles.md, apps/cli/options.cpp, src/serve/serve_options.cpp, src/models/load_options.h, src/calibration/route_catalog.cpp, tests/ops/linear/tests.cmake). One conflicted, and it is on the critical path: the calibrate_* sequence in src/calibration/device_calibration.cu, which is where the nvfp4 keys enter the device route profile. Upstream wrapped the groupwise family into a block that also runs the new gguf vector, moe-vector and moe-up calibrations, and gated linear_attention on the same gguf_only filter. The merge keeps upstream's block whole, re-adds the fork's nvfp4 line after it, and now gates that line with gguf_only like every sibling: a --only gguf_* run no longer also calibrates the nvfp4 family. The resolved file is byte-identical to the one the rebased pr/nvfp4-w4a16 carries (blob 404c913). Both review branches were rebased in the same pass: pr/nvfp4-w4a16 = 18491bed, whose file set and line counts are unchanged (73 files, 1356/257) and whose only content difference from b76d912 is that gguf_only gate; pr/nvfp4-w4a8-int = 220dbfb1, rebased clean with the same patch-id dcbdbfc7 and the same 96 files. Unlike the last sync, this one touches numerics and route installation, so the build and the nvfp4 gates follow.
Problem and scope
Related Issue: #107. Cooperative-load follow-up to #127.
Q4_K/Q6_K vector decoders reload packed words needed by neighboring lanes.
Implementation
Share those words through explicit eight-lane Q4 and four-lane Q6 shuffles, preserving decoded values, dot-product arithmetic and rounding. Replace the CMake option and sm86 guard with device-profile selection by type and width.
The RTX 3090 built-in dense profile selects Q4_K T1 and Q6_K T1–8. Q4_K T2–8 stay scalar in that profile: the original T4 screen was neutral and T8 regressed. MoE-vector and decode-up have separate calibration. Unconfigured routes stay scalar. A shared plain/fused schedule requires both forms to win.
Verification
RTX 3090 at 350 W, Ubuntu 24.04/WSL2, CUDA 13.1.115, GCC 13.3, Release/sm86, based on
e1f2d961. Jonathan Q4_K_M, 4096 prompt + 256 decode tokens, INT8 KV, MTP3, n-gram drafting off, context capacity 65536, prefill chunk 1024, graphs on, prefix reuse off. Both arms used the same lab artifact with legacy tokenizer metadata updated. Tensor bytes were unchanged.Same executable, scalar versus selected profiles: eight balanced ABBA/BAAB blocks, 32 fresh processes, one warmup and three measured repetitions per process. Speedup = scalar/selected − 1. The 95% CIs use eight block log ratios after averaging repetitions within each process (t, df=7). Load/warmup and profiling are excluded.
All eight decode and whole-generation blocks were positive. None were discarded. The original prefill result was −0.106% [−0.199%, −0.012%]. A separate two-block screen was −0.192%. The confirmation CI crosses zero. The earlier slowdown remains unexplained. Zero effect is not established.
Exact dense/MoE scalar-versus-cooperative checks, CPU expert tests, the public GGUF FP64 oracle and a 128-token CLI comparison passed. The direct CPU/GPU expert parity suite was not rerun. Affected CUDA units compile for sm70/75/80/86/89/120a. GPU execution was tested only on RTX 3090. Other contexts and full MoE-model performance remain unmeasured.
Reproduction commands
Recorded test runs used a focused runner linking the public test OBJECT targets, with scalar/cooperative profiles installed in-process. These are the equivalent public entrypoints. The full
ninfer_testsexecutable was not run. The cooperative test selects both routes internally. The standalone GGUF oracle below uses scalar defaults.Set
ARTIFACTto the compatible Jonathan artifact. ReplaceARMwithscalarorselectedandRUN.jsonwith a unique report filename. Make scalar/selected copies ofcalibrated.json, setting all six GGUF routes toscalarthrough width 8 (width 4 forgguf_moe_up/*). In selected, overridegguf_vector/q4_kwith[[1,"cooperative"],[8,"scalar"]]andgguf_vector/q6_kwith[[8,"cooperative"]]. Other routes must match.The benchmark command runs one fresh process. For the reported CI, use scalar/selected as A/B, alternate ABBA/BAAB for eight blocks, leave four seconds between processes and save each report separately. Average repetitions per process, form each block’s log ratio from its two scalar and two selected process means, then use a t interval over the eight ratios.