Support AProjQ4 GGUFs: Q4_K dense attention projections - #621
Support AProjQ4 GGUFs: Q4_K dense attention projections#621GiorgioOppo wants to merge 24 commits into
Conversation
The AProjQ4 DeepSeek V4 Flash GGUFs quantize the five dense attention projections per layer (attn_q_a, attn_q_b, attn_kv, attn_output_a, attn_output_b) as Q4_K instead of Q8_0. Loading already accepted them (tensor_expect_dense_quant_layout), but the decode graph read the Q4_K blocks through the hardcoded Q8_0 kernels and generated garbage (BOS loops), and the CPU reference died with "expected a 2D Q8_0 tensor". - Metal decode graph: gate the fused Q8_0 q_a/kv pair kernel and the plain Q8_0 matvec fallbacks on the actual tensor type, dispatching through the existing generic dense-quant path for Q4_K. Q8_0 models keep the exact kernels they used before, so their output stays bit-identical. - CPU reference: add a dense Q4_K matvec/matmul family (activations prequantized to Q8_K, rows reduced with ds4_vec_dot_q4_K_q8_K), with grouped, decode-scratch and prefill-batch variants, and dispatch the attention projection call sites on tensor type. - CUDA decode-TP attention output split now refuses non-Q8_0 output projections with a clear error instead of computing garbage. Verified on Apple M1 Pro 16 GB with --metal --ssd-streaming on DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ4-SExpQ8-OutQ8-chat-v2-imatrix: coherent greedy output, --decode-consistency max_abs=0 rms=0, and no regression on the AProjQ8 gguf. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DS4_METAL_STREAMING_EXPERT_NOCACHE=1 serves the streaming expert preads from a second F_NOCACHE descriptor (reopened by path: a dup would share the file description with the mmap-backed fd) and turns off the F_RDADVISE readahead hints, which only warm page cache the preads no longer consume. On tight-RAM machines the ~1 GB/token of routed-expert churn was flowing through the page cache and kept evicting the ~7 GiB of mapped dense weights that decode re-reads every token; once the dense set fell out, generation collapsed to SSD-fault speed and could never recover because the next run's expert traffic flushed it again. A/B on Apple M1 Pro 16 GB, AProjQ4 gguf, greedy 32 tokens, cold page cache: baseline 0.29/0.29 tok/s (stuck across runs); with the flag the dense set survives the expert traffic and warms across runs — 0.29, 2.09, 1.93, 1.91 tok/s. Generated tokens bit-identical to baseline in all runs; --decode-consistency max_abs=0 rms=0. Opt-in because on the >=96 GB target machines everything fits in RAM and cached preads are strictly better (second touch is free). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DS4_METAL_STREAMING_EXPERT_PREAD_SPLIT=N expands every expert slab pread into up to N disjoint 16 KB-aligned ranges read concurrently by the existing pread pool. Decode misses queue only a handful of slabs per layer (~4 experts x 3 slabs) while NVMe drives reach their random-read ceiling around ~24 requests in flight: splitting deepens the queue at identical bytes. Results are folded back per original slab so callers keep per-task ok/bytes/ms. Default 1 = historical path. Interleaved A/B on Apple M1 Pro 16 GB, AProjQ4 gguf, greedy 32 tokens, warm state, with DS4_METAL_STREAMING_EXPERT_NOCACHE=1: split=1 1.94/1.92/1.93 tok/s, split=4 2.24/2.20/2.24 tok/s (+16%). Tokens bit-identical in all runs; --decode-consistency max_abs=0 rms=0. Same trick as the Swift port's DS4_PREAD_SPLIT, where 4 also measured best. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
DS4_METAL_STREAMING_EXPERT_NOCACHE=1 DS4_METAL_STREAMING_EXPERT_PREAD_SPLIT=4 ./ds4 --metal --ssd-streaming --ssd-streaming-cache-experts 448 -m gguf/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ4-SExpQ8-OutQ8-chat-v2-imatrix.gguf |
The AProjQ4 GGUFs load fine on CUDA but every dense Q4_K projection hit the "matmul_quant: unsupported type 12" error. Add the missing path: - matmul_q4_K_dense_kernel + cuda_matmul_q4_K_tensor: activations are quantized to Q8_K rows with the existing routed-MoE quantizer and each weight row is reduced with the shared dev_dot_q4_K_q8_K_block, so the numerics match the already-validated MoE Q4_K path. 8 lanes per row, same idiom as the MoE decode kernels. Wired into ds4_gpu_matmul_quant_tensor as type 12. Every token re-reads the weight rows from device memory: fine for decode, correct but unoptimized for prefill chunks (a dequant+GEMM path can follow if it shows up in profiles). ROCm compiles the same source through HIP. - ds4.c: the specialized Q4_K attention-output low projection now falls through to the generic per-group dense-quant loop when the backend returns 0 (the CUDA/ROCm stub), instead of failing the layer. Metal keeps its fast path; a quick A/B confirms bit-identical tokens. Still unsupported with Q4_K projections on CUDA: the decode-TP attention-output split and the kslice fused paths — both already refuse loudly via the type guards instead of computing garbage. NOT compile-tested on a CUDA machine (authored on a Mac): testers with NVIDIA/ROCm hardware, please build and run the AProjQ4 gguf with --temp 0 plus --decode-consistency, and confirm AProjQ8 stays bit-identical to main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@antirez , |
|
Volunteering as a tester for the CUDA path — this is the configuration your table lists as implemented but untested. Machine: stock NVIDIA DGX Spark, GB10 (sm_121), 128 GB unified memory, aarch64, Ubuntu 24.04.4, driver 580.173.02, CUDA 13.0.88. Built with What I can run, using a harness already in place from measuring
Why the memory result is the interesting one here. Resident What blocks me: I could not find an AProjQ4 GGUF published (not in So: could you publish Context for the numbers above: #705. |
|
Hi, the download is blocked by a pr on hf, https://huggingface.co/antirez/deepseek-v4-gguf/discussions/22 You should be able to download the template from https://huggingface.co/antirez/deepseek-v4-gguf/blob/refs%2Fpr%2F22/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ4-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf |
|
Tested on a stock NVIDIA DGX Spark (GB10, sm_121, 128 GB unified, CUDA 13.0.88, driver 580.173.02), as offered above. Short version: the CUDA path is correct, the memory win is bigger than expected, but prefill regresses ~16x, which is a blocker for resident use on this machine. Setup: branch One caveat up front: the AProjQ4 file is from an older checkpoint (no 1. Correctness: the CUDA path looks good
Top-1 matches, top-10 sets match. The divergence is smaller than on the existing Q8_0 path, so the new kernel is not introducing numerical error. 2. Memory: a bigger win than the file-size delta suggests
But free memory after startup improves by ~11 GB, well beyond the 2.14 GiB the file shrank:
My guess is that the Q4_K tensors no longer exist simultaneously as an aligned artifact and a mapping, which accounts for roughly 6.7 GiB of it — I have not verified the rest, so treat the exact figure with caution. On GB10 (CPU and GPU share one 128 GB pool) this is a real change: 3. Prefill: ~16x slower, and this is the blocker
Decode is essentially free (−1.2%) — impressive for halving the precision of the attention projections. All the cost is in prefill, exactly where your comment above Practical effect for a long-context server: a full 194681-token prefill went at a steady 54.6 t/s, i.e. ~59 minutes instead of ~4.5 (I aborted the run at 17%). So on this machine the patch trades a context ceiling we can now afford for one we can no longer fill — a 15k-token page would go from ~17s to ~4.5 minutes. That makes the dequant + GEMM prefill path you mention in the comment the deciding factor here, rather than a later optimization. Is that something you're planning? If you push a prefill path, I'm happy to re-run this whole sweep on the same machine — the harness is set up and a rebuild plus full sweep is under an hour. 4. Perplexity — reported for completeness onlySame 1882-token sample: AProjQ4 12.730 (avg_nll 2.5439) vs AProjQ8-0731 11.108 (avg_nll 2.4077). Please do not read this as a +14.6% quantization cost — it is dominated by the checkpoint difference, and it is inconsistent with your own +0.99% measured within one checkpoint. I mention it only so the number isn't misread if it surfaces elsewhere. If you publish an AProjQ8 GGUF from the same older checkpoint, I'll rerun and give you a clean quality delta. Also worth noting for anyone reading this from the DGX Spark side: this is a different issue from #705, which is about routed Q4_K experts having no aligned repack builder. This PR touches dense tensors only and does not OOM. |
|
@adamlawi, |
|
Retested
Decode is unchanged (17.32 / 14.90 / 14.82 / 14.67 vs 17.55 / 15.08 / 14.97 / 14.83 for Q8_0, i.e. −1.1%), so the MMQ path did not cost anything on the decode side. Two more checks, since an optimization like this could plausibly trade accuracy for speed — it did not:
No Memory is unchanged from my previous report, as expected: startup spans 6.06 GiB (vs 8.20 GiB for AProjQ8), 259 aligned artifacts / 74.16 GiB, and ~11 GB more free memory at every context size. So on a 128 GB GB10 this now looks like a straight win: ~2 GiB smaller resident model plus ~11 GB more free memory, for ~4.5% prefill and ~1% decode — and Nice work — that was a fast turnaround. One question about the other half of the commit, |
|
Standing offer, so you don't have to ask: when you publish the GGUF for the newer DeepSeek checkpoint, I'll run the full suite on the DGX Spark and post the numbers here — That run would also finally give a clean same-checkpoint quality delta, which neither of my perplexity numbers so far can provide (the AProjQ4 file I tested is from the older checkpoint, and I don't have a matching AProjQ8 baseline for it). The same applies to No rush from my side — the machine is a stock DGX Spark that stays available, and a rebuild plus the whole measurement suite takes well under an hour. Just ping me here. |
|
The new gguf is online https://huggingface.co/antirez/deepseek-v4-gguf/tree/8268ad9384d11cbb63f472d097e499a9c59709ec. It's made by requantizing the q8. I know it's not the best choice, but on my hardware it's the only thing I can do. |
|
try use DS4_CUDA_NO_Q4_ATTN_GROUPED_TOK8=1 DS4_CUDA_NO_Q4_QKV_PAIR=1 in the next run |
|
Ran
So They also hurt numerics, which I did not expect:
With the new paths active the divergence is worse than the Q8_0 baseline; with them off it is back to the The good news — decode improved, and it is not from these two paths:
Decode is ~+5% over One more thing worth a look: perplexity on the same 1882-token sample is 12.7836 (A) and 12.7904 (B), versus 12.7302 on Summary from this machine: keep whatever gave the decode win, and either fix or gate the grouped-tok8 / QKV-pair paths — on GB10 they cost 38% of prefill and worsen decode-consistency. Happy to re-run anything on request. (And the |
|
@adamlawi Feel free to try the requantization test
|
|
1. Requantization./deepseek4-quantize \
--source-gguf DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf \
--template <same file> \
--attention-proj q4_k \
--out ...-AProjQ4-SExpQ8-OutQ8-chat-v2-0731-requant.ggufConverted 215 tensors Two notes for anyone repeating this:
2.
|
| avg_nll | top1 | first_match | avg_lcp | |
|---|---|---|---|---|
| yours: Q8 (macOS, older ckpt) | 0.632957 | 82.79% | 44 | 3.830 |
| yours: Q4 (macOS, older ckpt) | 0.653162 | 82.88% | 41 | 3.420 |
mine: Q8 -0731 (CUDA) |
0.404811 | 85.86% | 55 | 5.150 |
mine: Q4 requant -0731 (CUDA) |
0.407383 | 85.34% | 56 | 5.540 |
Q8 → Q4 delta: +3.19% for your pair, +0.64% for mine.
Please don't compare the absolute values across our two pairs — the data/flash fixtures were collected from the official DeepSeek V4 Flash 0731 API, so they match my checkpoint and not the older one. That alone explains most of the level difference (0.405 vs 0.633). Only the within-pair deltas are meaningful, and even those differ in backend and checkpoint.
Worth noting: on my pair first_match and avg_lcp actually go up slightly with Q4 (56 vs 55, 5.540 vs 5.150), while top1 drops 0.5 pp — so greedy behaviour is essentially preserved.
Classic perplexity on my own 1882-token sample agrees: 11.108 → 11.316, i.e. +1.87%. That also settles the confusion from my earlier comments — the +14.6% I reported before was almost entirely the checkpoint difference, not the quant.
3. Speed and memory on the requantized -0731
| ctx | prefill Q8 | prefill Q4 | decode Q8 | decode Q4 |
|---|---|---|---|---|
| 2048 | 817.32 | 789.63 | 17.55 | 18.41 |
| 4096 | 892.89 | 850.92 | 15.08 | 15.71 |
| 6144 | 883.59 | 842.99 | 14.97 | 15.61 |
| 8192 | 877.75 | 839.80 | 14.83 | 15.44 |
Decode is ~5% faster than Q8_0 (that gain is from the rest of 8b84e878, as reported earlier), prefill is ~4% slower. Startup spans 8.20 → 6.06 GiB, aligned artifacts 474/78.71 GiB → 259/74.16 GiB, and free memory after startup:
| ctx | Q8 -0731 |
Q4 requant -0731 |
|---|---|---|
| 200000 | 9.8 GB | 23.1 GB |
| 300000 | 8.1 GB | 20.3 GB |
So on a 128 GB GB10 this variant now gives us --ctx 300000 with more headroom than --ctx 200000 had before, at +0.64% avg_nll and with faster decode. That is a straightforward win for long-context serving, and it is only possible because --source-gguf exists — thanks for adding it.
Happy to re-run any of this, or to try an imatrix-guided requantization if you think it is worth measuring the difference.
|
Thanks for the push — the variables are there now and I measured Short version:
All non-timing counters are byte-identical within each pair, so the ordering is solid. Where the time goes
In the default run That unaccounted 4230 ms is almost exactly the 4290 ms you saved on Is that path instrumented anywhere I missed? If you add a counter for it I'll re-run immediately — right now the biggest single cost in the speculative path is invisible in the stats output. Two details that may help localize it: the speculation counters are identical to the default run ( The other two flags
Best result so far, with a caveat I want to flag myself
The direction from my previous comment still holds: every configuration that speculates less does better, and even the best case is net-negative. Unrelated, for your informationBaseline decode is unchanged after the merge (19.29 vs 19.32 on Usual caveats: one synthetic prompt, one machine, |
|
Thanks for testing it, I'll try something else. After this attempt, I'm out of ideas and will close the PR. I'll reopen it with a clean version using only the Q4 implementation. |
|
@adamlawi Can I ask you for a comparison between q4 and q8 with and without dispark for troubleshooting? |
|
Here's the Q4 vs Q8 comparison you asked for, plus a correction to something I got wrong in my previous comment. Same machine and method throughout: DGX Spark (GB10), CUDA, Q4 vs Q8, with and without DSpark
The regression is essentially identical on both quantizations, so this isn't a Q4-specific problem. What makes that more interesting is that draft quality is not the same on the two models:
Q4 produces 63% better drafts than Q8 ( For reference on the non-DSpark path: Q4 is 5.1% faster than Q8 in decode (19.22 vs 18.28 avg) and 2.3% slower in prefill (759.7 vs 777.5). Your
|
conf 0.9 |
decode | verify_layer |
replay |
spec_total |
|---|---|---|---|---|
5e05a6c7 (before your optimizations) |
15.15 | — | — | — |
0c20bfb2, flags off |
14.94 / 15.01 | 752 ms | 1588 ms | 2356 ms |
0c20bfb2, flags on |
16.18 / 16.22 | 85 ms | 476 ms | 1799 ms |
The merge alone is −1.1%. Your flags are worth +8.2%. At conf 0.9 they cut verify_layer by 89% and replay by 70%, and this time the saving is not eaten by the unaccounted cost I described last time — spec_total drops by 24% and it shows up in decode.
So the picture is more specific than "the optimizations don't help":
- at
conf 0.7(heavy speculation) the added cost cancels the saving — 13.03 → 13.10, no change - at
conf 0.9(light speculation) the saving survives — 14.98 → 16.20, +8.2% - at
conf 0(always speculate) it's worst of all — 11.26
Your work measurably helps in the low-speculation regime. That's a narrower claim than you were probably hoping for, and DSpark is still net-negative overall here (best case net_saved −1137 ms). But it isn't nothing, and I wanted you to have that number before you decide what to do with the PR — it's a different result from the one I sent you this morning.
If it's useful before you close: I'm happy to run the full --dspark-confidence sweep (0.5 / 0.7 / 0.8 / 0.9 / 0.95) with the flags on, which would show exactly where the crossover sits. That's one window on my side.
Usual caveats: one synthetic prompt, one machine, --temp 0.
|
Retracting the prefill observation from my earlier comment — it was an artifact of my own method, not something your merge did. I said baseline prefill looked ~1.7% lower after the merge (758.69 / 760.34 against ~773 on
Prefill is 0.6% higher after the merge, not lower. Nothing regressed. What misled me: both binaries measured ~762–767 in that afternoon window, while the same Incidentally, and opposite to what I expected: decode on Sorry for the noise on that point. |
Port antirez#766 commit d99de5c while preserving the local Metal/CUDA DSpark exact-N, tiny-batch, and Q4 paths. Add fail-closed diagnostics, parity tests, and GB10 benchmark documentation.
Reuse canonical Q8_1 scratch for Q4 MMVQ, add grouped attention-A and the exact K1024 persistent path, and cover the new dispatches with rollback gates and parity tests.
|
I ran the full Method as before: DGX Spark (GB10), CUDA, Q4+imatrix, The curve
The curve does not converge to baseline. It flattens at −16.7%. At Where it comes from — it's the flag, not the memoryI ran three states in one window, interleaved, two runs each:
B is indistinguishable from A, to two decimal places, in both runs. Loading the 5.6 GB support model costs nothing at all (B also emits no DSpark stats, confirming the module is inactive). The entire ~16% appears the moment So: not memory, not the cost of speculating. Enabling What I think this means for the PRThis reframes the last three rounds. Both of us have been treating the cost as proportional to speculation — which is why That's not a reason to abandon the work — it's a different target. Your flags do measurably help in the low-speculation regime (+8.2% at If you want a starting point: whatever One counter question
Caveats
|
|
oh wow 850 t/s on prefill side is really impressive |
|
Heads-up: the GB10 fast path changes numerics — the code comment says it doesn't Setup: DGX Spark (GB10), DeepSeek V4 Flash 0731, own AProjQ4 requant + imatrix,
Why this is solid: This is not a quality regression — I want to be precise about that:
So the fast path looks fine quality-wise, and it's a big win: decode 18.73 → 25.11 t/s (vs 20.58 with the fast path off). The only issue is that the comment promises bit-exactness that isn't there — worth either relaxing the comment or checking whether the epilogue has an unintended precision difference (accumulation order?). |
|
Thanks for the feedback @GiorgioOppo and @adamlawi |
|
Thanks @adamlawi, I'll review the code. |
|
@adamlawi Here are a couple of optimizations, I hope. Could you rerun the q4 vs q8 vs main antirez tests? |
|
I've pushed the physical limits of my hardware by generating at 3 t/s on my 16GB MacBook Pro. I don't think I'd suggest any other optimizations. I encourage everyone to continue with the Q4 because it has good prospects. Compared to the Q8, it doesn't seem to lose quality, but rather brings benefits. SUMMARY:
|
|
Thanks — ran the q4 vs q8 vs main antirez comparison you asked for, on Good news: the numerics fix in Setup: DGX Spark (GB10), CUDA, same 1,574-token prompt,
This looks like a strict improvement over what we're running now: same speed, correct numerics. We haven't switched production to it yet — want to let it sit for a bit given how fresh it is, same caution as last time. Let us know if you want anything else measured. |
|
@adamlawi can you add also prefill speed, and the comparison with the q8 in the main branch of Antirez? |
|
Sure — prefill from the same run, plus the main-branch Q8 comparison. Prefill (t/s, avg of the two interleaved runs, same 1,574-token prompt):
Same story as decode: within noise, no regression from the fix. main branch (antirez/ds4, Q8 doesn't touch the Q4_K attention path, so it loads fine on main (unlike Q4, which needs
Bit-identical to the 9th digit — our fork's Q8 path matches main exactly, so the fork isn't introducing any drift there. The only thing that changed anything numerically was the new Q4_K attention path, and main + Q8 speed for reference (same 4 frontiers): prefill 839.25 / 924.28 / 926.63 / 928.63 t/s, decode 17.58 / 15.10 / 14.99 / 14.87 t/s — this is the Q8 baseline your Q4 work is competing against, consistent with what we measured on our own Q8 file back in August. We switched production to |
|
@DanteCpp would you like to test this implementation? |
|
Happy to. Since @adamlawi has already covered CUDA thoroughly on GB10, let me say up front what Jetson AGX Thor T5000 - Blackwell, compute capability sm_110 (not sm_121), 2560 CUDA cores / Why the arch difference is not cosmetic:
So the useful output from here is whatever separates sm_121-specific from CUDA-in-general. Two runs, because they answer different questions1. Reproduce adamlawi's runs on the published file. Same GGUF, same tests, different arch - that 2. Requantize our own Both matter: run 1 is comparable to his results, run 2 is comparable to ours. The baseline for run 2 is already published and on the same box: canonical One practical note: the AProjQ4 file is not on the main branch of Which commit should I build? The thread mentions Timing: the board is part-way through an unrelated benchmark queue, so this will not be immediate. I |
|
@marcodelpin Looking forward to the sm_110 control — if the |
The AProjQ4 DeepSeek V4 Flash GGUFs quantize the five dense attention projections per layer (attn_q_a, attn_q_b, attn_kv, attn_output_a, attn_output_b) as Q4_K instead of Q8_0. Loading already accepted them (tensor_expect_dense_quant_layout), but the decode graph read the Q4_K blocks through the hardcoded Q8_0 kernels and generated garbage (BOS loops), and the CPU reference died with "expected a 2D Q8_0 tensor".
Verified on Apple M1 Pro 16 GB with --metal --ssd-streaming on DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ4-SExpQ8-OutQ8-chat-v2-imatrix: coherent greedy output, --decode-consistency max_abs=0 rms=0, and no regression on the AProjQ8 gguf.