Support AProjQ4 GGUFs: Q4_K dense attention projections - #621
Support AProjQ4 GGUFs: Q4_K dense attention projections#621GiorgioOppo wants to merge 9 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/18 You should be able to download the template from https://huggingface.co/antirez/deepseek-v4-gguf/resolve/5ca1abfe6d0c518ffcd731eae01bc0c88a2e59db/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ4-SExpQ8-OutQ8-chat-v2-imatrix.gguf?download=true |
|
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.
|
Can you share th GGUF? |
|
Final data point on the requantized Full-window load test, one session, prompt sized to fill the window, sampling
So it is 1.5x more usable context with ~5.8x more headroom at the worst moment. The memory profile over the run: start 21.9 GB → min 14.6 GB → median 19.0 GB → end 17.4 GB. The minimum is a brief spike, not a plateau — we were within 1 GiB of it for only about 6 seconds. That matters here: on the Q8_0 model, The model also answered correctly from the full 293k-token context (a sensible summary of the input), so this is not just an allocation test. One caveat on the prefill number, since it looks lower than my earlier sweep: 293,390 tokens in 449 s is ~653 t/s effective, versus 840 t/s on the short bench frontiers. That is the attention cost growing with window length, not a regression — the Q8_0 baseline shows the same pattern (735 t/s when filling 200k, versus 878 t/s on the bench). Don't compare those two full-window numbers across the models, they cover windows of different sizes; the like-for-like prefill comparison is still the bench table (−4%). Summary of where this lands for a 128 GB DGX Spark, all on the same
For long-context serving on this hardware that is a clear win, and none of it would have been reachable without Still happy to test the newer-checkpoint GGUF when you publish it, and to re-run the grouped-tok8 / QKV-pair comparison if you change those paths. |
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.