Skip to content

perf(gguf): share Q4_K/Q6_K vector loads across lanes - #134

Merged
iamwavecut merged 4 commits into
iamwavecut:masterfrom
liqlos:perf/cooperative-q4-q6
Oct 25, 2026
Merged

iamwavecut merged 4 commits into
iamwavecut:masterfrom
liqlos:perf/cooperative-q4-q6

Conversation

@liqlos

@liqlos liqlos commented Oct 24, 2026 •

Copy link
Copy Markdown

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.

Phase Speedup 95% CI
Prefill (primary) +0.14% [−0.15%, +0.43%]
Decode (secondary) +8.12% [7.78%, 8.45%]
Whole generation (secondary) +3.60% [3.30%, 3.91%]

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_tests executable was not run. The cooperative test selects both routes internally. The standalone GGUF oracle below uses scalar defaults.

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_CUDA_ARCHITECTURES=86 -DBUILD_TESTING=ON -DNINFER_BUILD_BENCHMARKS=ON
cmake --build build -j --target ninfer_tests ninfer-calibrate ninfer_bench
build/tests/ninfer_tests ninfer_device_profile_test
build/tests/ninfer_tests ninfer_linear_gguf_cooperative_test
build/tests/ninfer_tests ninfer_linear_gguf_test
build/tests/ninfer_tests ninfer_gguf_expert_cpu_test
build/apps/ninfer-calibrate --only gguf_ --out calibrated.json
NINFER_DEVICE_PROFILES="$PWD/ARM-profile.json" build/bench/ninfer_bench \
  --weights "$ARTIFACT" --corpus bench/fixtures/bench_corpus.ids \
  --prompt-gen 4096,256 --repetitions 3 --warmup 1 --max-ctx 65536 \
  --prefill-chunk 1024 --kv-dtype int8 --spec mtp --draft-tokens 3 \
  --ngram-draft-tokens 0 --output json --output-file RUN.json

Set ARTIFACT to the compatible Jonathan artifact. Replace ARM with scalar or selected and RUN.json with a unique report filename. Make scalar/selected copies of calibrated.json, setting all six GGUF routes to scalar through width 8 (width 4 for gguf_moe_up/*). In selected, override gguf_vector/q4_k with [[1,"cooperative"],[8,"scalar"]] and gguf_vector/q6_k with [[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.

@iamwavecut iamwavecut left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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__ == 860 check. We'd like this on every card, picked per (type, T) through device-profile routes, like q6_head or q4_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 Decoder loop. 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.
@liqlos
liqlos force-pushed the perf/cooperative-q4-q6 branch from b42619a to b4081ed Compare October 24, 2026 22:18
@liqlos

liqlos commented Oct 24, 2026

Copy link
Copy Markdown
Author

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 iamwavecut left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@iamwavecut
iamwavecut merged commit 9026853 into iamwavecut:master Oct 25, 2026
15 of 17 checks passed
13thblade added a commit to 13thblade/ninfer-all-nvfp4 that referenced this pull request Oct 26, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants