Skip to content

Support AProjQ4 GGUFs: Q4_K dense attention projections; Metal, ROCm and CUDA performance improvements; SSD streaming support for speed-bench; quality-of-life improvements; bugfixes - #621

Closed
GiorgioOppo wants to merge 189 commits into
antirez:mainfrom
GiorgioOppo:aprojq4-dense-attention

Conversation

@GiorgioOppo

@GiorgioOppo GiorgioOppo commented Jul 28, 2026

Copy link
Copy Markdown

PR #621 — consolidated AProjQ4 results

This comment supersedes the exploratory and intermediate benchmark reports in the thread. The tables below retain only the latest meaningful matched AProjQ4/AProjQ8 comparisons and exclude known first-load artefacts from steady-state conclusions.

Note

Current PR head: 6a20b131e18f6a1650706d8f76e15f60ac2bc91f.

Some of the latest complete backend A/B runs predate this exact head. Before merge, the compact validation matrix should therefore be rerun once on the final SHA.


What this PR adds

This PR implements explicit support for the DeepSeek V4 Flash AProjQ4 layout.

The following five dense attention-projection tensor families are converted from Q8_0 to Q4_K in each of the 43 layers:

  • attn_q_a
  • attn_q_b
  • attn_kv
  • attn_output_a
  • attn_output_b

This represents 215 converted tensors in total. Routed experts, shared experts, output quantization and checkpoint identity remain unchanged.

The implementation includes:

  • CPU reference execution;
  • Metal resident and SSD-streaming paths;
  • CUDA decode, paired projection, MMQ and grouped prefill paths;
  • ROCm decode, TILE8 and direct-WMMA prefill paths;
  • direct GGUF-to-GGUF requantization with dense-imatrix support;
  • backend-specific exactness, parity, canary and fail-closed tests;
  • benchmark harnesses and expanded QA/environment-variable documentation.

This is support for one explicit DS4 model layout, not generic support for arbitrary Q4 GGUFs.


Executive summary

Backend Prefill, Q4 vs Q8 Decode, Q4 vs Q8 Current conclusion
Metal Effectively tied in the strongest published resident A/B Approximately +13% to +19%, with a +15.5% median over the full M5 Max sweep Clear decode win; published prefill is parity
CUDA / GB10 +1.98% to +2.39% at 4K–8K +13.9% to +18.0% Q4 wins both prefill and decode
ROCm / gfx1151 Approximately 2–3% behind at the longer measured frontiers Approximately +11.5% to +12.6% Decode win; small prefill gap remains
CPU No performance claim No performance claim Portability and correctness reference

Across the three GPU backends, AProjQ4 now has a consistent and substantial decode advantage. CUDA has also crossed Q8 in steady-state prefill. ROCm has reduced its original large prefill deficit to a small residual gap.


Model footprint

Layout File/model footprint
AProjQ8 86,720,111,488 bytes — 80.76 GiB
AProjQ4 84,420,584,288 bytes — 78.62 GiB
Saving 2,299,527,200 bytes — 2.14 GiB / 2.65%

The saved memory can be used as:

  • additional context/KV headroom on resident systems;
  • a larger routed-expert cache on SSD-streaming systems;
  • reduced memory pressure during startup and inference.

Metal results

M5 Max, resident execution

Three interleaved repetitions per model at the 2K frontier:

Metric AProjQ8 AProjQ4 Q4 change
Prefill 825 tok/s 824 tok/s −0.1% — parity
Steady decode 45.69 tok/s 53.35 tok/s +16.8%
First generated token 26.86 ms 23.32 ms −13.2% latency

AProjQ4 exceeds 50 generated tokens/s and is faster than AProjQ8 at every measured decode frontier.

Full Metal decode sweep

Context range: 2,048 to 65,536 tokens, 32 frontiers, three interleaved repetitions.

Context Q8 decode Q4 decode Q4/Q8
2,048 40.6 48.4 1.191 / +19.1%
14,336 1.171 / +17.1%
26,624 1.155 / +15.5%
38,912 1.140 / +14.0%
51,200 1.130 / +13.0%
65,536 30.4 34.3 1.129 / +12.9%

Summary:

  • paired median Q4/Q8 ratio: 1.155, or +15.5%;
  • Q4 wins at 32 of 32 tested frontiers;
  • the advantage narrows with context length but never inverts;
  • published Metal prefill is currently statistically tied, so a positive prefill claim should wait for the final-head matched rerun.

CUDA / GB10 results

The default grouped attention-A prefill path introduced at 35b87dda is the first CUDA configuration in this PR where Q4 clearly exceeds Q8 in steady-state prefill.

The 2K row is omitted here because Q8 showed a first-load artefact. The 4K–8K rows are the reliable steady-state result.

Context Q8 prefill Q4 prefill Q4 change
4,096 956.45 tok/s 975.43 tok/s +1.98%
6,144 941.55 tok/s 963.70 tok/s +2.35%
8,192 937.46 tok/s 959.91 tok/s +2.39%
Mean, 4K–8K +2.24%

Decode remains substantially faster with Q4:

Metric Result
Q4 decode advantage across the tested frontiers +13.9% to +18.0%
Decode change caused by the grouped-prefill optimization itself Within −0.7% to +0.2%, as expected for a prefill-only change
Numerical result of grouped prefill vs rollback path Byte-identical

The default path is the correct one for GB10:

CUDA path Default Prefill effect on GB10 Quality
Grouped attention-A prefill Enabled approximately +4.5% over the previous path Byte-identical
Experimental Q4 16-warp MMQ Disabled / opt-in −6.3% when enabled on GB10 Byte-identical

The 16-warp path should therefore remain experimental or architecture-gated; the grouped path is the production win on GB10.


ROCm / gfx1151 results

The original ROCm implementation had a large Q4 prefill penalty. After TILE8, direct-WMMA, staging and scheduling work, the residual deficit has been reduced to approximately 2–3%, while decode remains consistently faster.

Latest complete published steady-state A/B table:

Context Q8 prefill Q4 prefill Q4 change Q8 decode Q4 decode Q4 change
4,096 228.42 221.54 −3.0% 14.53 16.22 +11.6%
6,144 224.81 217.99 −3.0% 14.47 16.15 +11.6%
8,192 221.61 214.81 −3.1% 14.32 15.97 +11.5%

The later K128 staging work is intended to close the remaining gap further; the current reported state is approximately 2% behind Q8 in prefill. The exact final-head table should replace the table above once that rerun is published.

ROCm conclusions:

  • decode is a clear +11.5% to +12.6% Q4 win;
  • the remaining prefill difference is now small compared with the original implementation;
  • the previously identified deterministic k0 GPU-hang bug is fixed;
  • the unsafe compounded-WMMA attention-output-B path is no longer selected by the normal runtime control;
  • normal real-model decode-consistency runs are coherent and complete without GPU hangs.

Startup, cold-start and time-to-stable-throughput

Q4 reaches usable and stable performance faster than Q8. These measurements should be kept separate from steady-state prefill throughput.

Platform / measurement AProjQ8 AProjQ4 Q4 effect
ROCm model preparation 19.700 s 17.583 s 2.117 s faster / −10.7%
Metal first generated token 26.86 ms 23.32 ms −13.2% latency
Metal residency request, mean 497.42 ms 480.77 ms −3.3%
CUDA first 2K frontier 830.54 tok/s 906.02 tok/s +9.1%*
ROCm first 2K frontier 174.30 tok/s 231.81 tok/s +33.0%*

* The CUDA and ROCm 2K rows document first-load behaviour, not steady-state prefill. Q8 was still in a cold-start condition, while Q4 had already reached its normal throughput range. These rows are deliberately excluded from the main prefill comparison.

The practical conclusion is that Q4 not only uses less memory, but also tends to reach stable execution sooner and reduces time-to-first-token.


Quantization quality

Same-checkpoint CUDA comparison on the tracked official-continuation fixture:

Model avg_nll First-token match Average greedy LCP API top-1
AProjQ8 0.404811251 55/100 5.150 0.858625
AProjQ4 without imatrix 0.407383304 56/100 5.540 0.853437
AProjQ4 with imatrix 0.396432403 60/100 6.090 0.860355

On this fixture, the imatrix-built AProjQ4 model shows no measured quality regression and obtains the best measured NLL, first-token match, LCP and API top-1 values of the three tested models.

This is a limited fixture of 100 cases and 2,313 target tokens. It demonstrates preservation on the measured workload; it is not proof that Q4 is universally better than Q8.


Advantages

  1. Lower memory footprint: 2.14 GiB saved without changing the routed experts, shared experts, output quantization or checkpoint identity.

  2. Faster decode on every tested GPU backend:

    • Metal: approximately +13% to +19%;
    • CUDA: approximately +14% to +18%;
    • ROCm: approximately +11.5% to +12.6%.
  3. CUDA prefill is now faster than Q8: approximately +2.0% to +2.4% at the reliable 4K–8K frontiers.

  4. Faster startup and first response: lower model-preparation time, lower residency-request time and lower first-token latency.

  5. Quality is preserved on the tracked fixture: the imatrix Q4 artifact produces the best measured values in the matched comparison.

  6. Correctness gaps are fixed: Q4 tensors no longer enter Q8-only execution paths that could produce BOS loops, corrupted output, non-finite logits or failed prefills.

  7. Broader validation: CPU oracles, backend parity tests, canaries, fail-closed REQUIRE controls and production-shape benchmark harnesses have been added.

  8. Useful tooling: direct same-checkpoint GGUF-to-GGUF requantization avoids rebuilding from hundreds of gigabytes of original safetensors.


Disadvantages and remaining work

  1. ROCm prefill has not completely reached Q8. The remaining difference is approximately 2–3% at the longer reliable frontiers.

  2. Published Metal data do not yet prove a prefill win. They show statistical parity on M5 Max and a clear decode win. A current-head A/B is needed before saying that Metal wins both phases.

  3. The exact final SHA still needs one compact cross-backend rerun. The branch moved after some of the latest complete Metal, CUDA and ROCm comparisons.

  4. The PR is very large: 189 commits, 117 changed files and roughly 78k additions. This raises review, maintenance and regression risk beyond the core AProjQ4 feature.

  5. There is a large runtime-tuning surface. Many optimized paths have default, rollback, opt-in and strict diagnostic controls. The default/experimental distinction must remain clearly documented.

  6. The experimental CUDA 16-warp path is slower on GB10. It is numerically correct but costs approximately 6.3% prefill there and should not become the GB10 default.

  7. Mixed ROCm Q4-A/Q8-B parity cases still need an explicit final status. They do not represent the published all-Q4 AProjQ4 GGUF, but the remaining test result should not be hidden.

  8. Coverage is not universal. CUDA SSD streaming, non-GB10 CUDA, multi-GPU/tensor-parallel execution and a full CPU end-to-end performance run have not all received equivalent model-backed validation.

  9. A compiled benchmark binary is currently tracked in the PR. speed-bench/metal_q4_attn_out_a_direct_bench should be removed from the commit and generated locally from its .m source.

  10. This is not generic Q4 support. It intentionally recognizes and executes the specific AProjQ4 layout.


Benchmark provenance

Backend Main published test system Benchmark snapshot
Metal Apple M5 Max, 128 GB, resident Metal 2669a8e9…
CUDA NVIDIA DGX Spark, GB10 / sm_121, 128 GB, resident CUDA 35b87dda…
ROCm AMD Strix Halo / gfx1151, 128 GB, resident ROCm latest complete public A/B around 5c5b1d5…; final K128/current-head rerun still to be added
Current PR head 6a20b131e18f6a1650706d8f76e15f60ac2bc91f

Conclusion

The central objective of this PR has been achieved:

  • the AProjQ4 GGUF is smaller;
  • it executes correctly on CPU, Metal, CUDA and ROCm;
  • it avoids the former Q8-only dispatch failures;
  • it preserves measured quality;
  • it starts faster;
  • and it provides a clear decode advantage on all tested GPU backends.

CUDA now wins both prefill and decode. Metal has a strong and broad decode win, with published prefill currently at parity. ROCm also wins decode and has reduced its prefill deficit to a small remaining 2–3%.

The remaining work before merge is validation and cleanup rather than a redesign:

  1. rerun the compact benchmark and correctness matrix on the exact final SHA;
  2. publish the final Metal and ROCm A/B tables;
  3. record the remaining mixed-parity status;
  4. remove the committed benchmark executable;
  5. keep experimental paths clearly separated from production defaults.

La parte relativa a layout, dimensioni, qualità e stato corrente della PR deriva dal riepilogo e dai metadati della PR.  I dati Metal provengono dai test resident M5 Max e dal relativo sweep completo.  I risultati CUDA sono quelli del percorso grouped-prefill verificato come realmente attivo.  La tabella ROCm pubblica più recente e completa è quella a circa `−3%` nel prefill e `+11.5…+12.6%` nel decode.  I tempi di preparazione ROCm e le misure di memoria provengono dal confronto Q8/Q4 su Strix Halo.  Il benchmark Metal compilato risulta effettivamente presente tra i file aggiunti e ha già generato una richiesta di modifica in review.

Non ho pubblicato nulla.

Questa è la versione che metterei nella PR. Ho corretto due affermazioni per non sovrastimare i risultati:

- **Metal:** i benchmark end-to-end pubblicati dimostrano una chiara vittoria nel decode e nel time-to-first-token, ma il prefill risulta in sostanziale parità: `824` contro `825 tok/s` su M5 Max; sul test interleaved M3 Ultra Q8 era avanti dello `0,72–0,93%`. Non scriverei quindi che Metal vince già nel prefill.  
- **ROCm:** l’ultima tabella pubblica completa mostra circa `−3%` nel prefill a 4–8K. Il `−2%` può essere presentato come stato approssimativo successivo, ma non come tabella definitiva finché non vengono pubblicati i numeri dell’ultimo commit. 
- La HEAD corrente è `6a20b131`, mentre l’ultimo confronto CUDA pubblicato è stato eseguito sul parent `35b87dda`; questo va dichiarato chiaramente.  

---

# PR #621 — consolidated AProjQ4 results and merge assessment

This comment consolidates the latest comparable AProjQ4-versus-AProjQ8 results and intentionally excludes superseded intermediate experiments.

The comparison uses the matched DeepSeek V4 Flash `-0731` models, where the only model-layout difference is that the 215 dense attention-projection tensors are stored as `Q4_K` instead of `Q8_0`.

> [!IMPORTANT]
> **Snapshot limitation:** the current PR head is `6a20b131e18f6a1650706d8f76e15f60ac2bc91f`.  
> The latest published CUDA A/B below was measured on its parent, `35b87dda22421dbf20271444ece635412117a09c`. Metal and ROCm results are also tied to explicitly identified tested snapshots. A final exact-head rerun is therefore still required before merge.

## Result in one table

| Backend | Prefill, Q4 versus Q8 | Decode, Q4 versus Q8 | Startup / first-token behavior | Conclusion |
|---|---:|---:|---:|---|
| **Metal** | Approximately equal on M5 Max: `824` vs `825 tok/s`; Q8 ahead by `0.72–0.93%` in the M3 Ultra interleaved test | `+15.5%` paired median over 32 frontiers on M5 Max; `+16.8%` at the isolated 2K test | First token `13.2%` faster; residency request about `3.3%` faster | Clear decode and latency win; prefill is currently neutral, not a demonstrated win |
| **CUDA / GB10** | **`+1.98%` to `+2.39%` at 4–8K** | **`+13.9%` to `+18.0%`** | Q4 avoids the recurring Q8 first-frontier cold-start dip | Q4 wins both prefill and decode |
| **ROCm / gfx1151** | Approximately **`2–3%` behind** at 4–8K; last complete published table is about `−3%` | **`+11.5%` to `+12.6%`** | Model preparation `10.7%` faster | Decode and startup win; small prefill gap remains |

## Model footprint

| Layout | File size | Resident-model size | Difference |
|---|---:|---:|---:|
| AProjQ8 | `86,720,111,488` bytes | `80.76 GiB` | Baseline |
| AProjQ4 | `84,420,584,288` bytes | `78.62 GiB` | **−2.14 GiB / −2.65%** |

The conversion covers five tensor families in each of the 43 layers:

- `attn_q_a`
- `attn_q_b`
- `attn_kv`
- `attn_output_a`
- `attn_output_b`

On resident systems, the saved `2.14 GiB` becomes additional KV/context headroom. Under SSD streaming, it can instead be used for a larger expert cache and fewer repeated expert reads.

## Quantization quality

Same checkpoint, same CUDA backend, same official-continuation fixture: 100 cases and 2,313 target tokens.

| Model | `avg_nll` ↓ | First-token match | Average greedy LCP | API top-1 |
|---|---:|---:|---:|---:|
| AProjQ8 | `0.404811251` | 55/100 | 5.150 | `0.858625` |
| AProjQ4 without imatrix | `0.407383304` | 56/100 | 5.540 | `0.853437` |
| **AProjQ4 with imatrix** | **`0.396432403`** | **60/100** | **6.090** | **`0.860355`** |

The imatrix AProjQ4 model produced the best measured NLL in this fixture, approximately `2.07%` lower than Q8. This establishes **no measured quality regression on the tracked sample**; it is not evidence that Q4 is universally better than Q8.

## Startup and warm-up behavior

Q4 reaches useful throughput sooner than Q8. The strongest directly comparable startup measurements are:

| Measurement | AProjQ8 | AProjQ4 | Q4 change |
|---|---:|---:|---:|
| ROCm model preparation | `19.700 s` | `17.583 s` | **−10.7%** |
| Metal first-token latency, M5 Max | `26.86 ms` | `23.32 ms` | **−13.2%** |
| Metal residency-request mean, M3 Ultra | `497.42 ms` | `480.77 ms` | **−3.3%** |

The first measured frontier also repeatedly exposed a Q8 cold-start penalty that did not appear on Q4:

| Backend | Frontier | Q8 prefill | Q4 prefill | Apparent Q4 advantage |
|---|---:|---:|---:|---:|
| CUDA / GB10 | 2K | `830.54 tok/s` | `906.02 tok/s` | `+9.09%` |
| ROCm / gfx1151 | 2K | `174.30 tok/s` | `231.81 tok/s` | `+33.0%` |

These first-frontier values are **not used as steady-state performance claims**, because the Q8 measurements are warm-up contaminated. They are nevertheless operationally relevant: Q4 ramps to normal throughput more quickly and does not show the same initial dip.

Strictly speaking, the Metal line reported as internal `warmup` time was similar between Q4 and Q8. The measurable Q4 advantage is in model preparation, residency, ramp-up behavior and time to first generated token.

## Metal

### M5 Max, resident, isolated 2K frontier

Three interleaved repetitions per model:

| Metric | AProjQ8 | AProjQ4 | Q4 versus Q8 |
|---|---:|---:|---:|
| Median steady decode | `45.69 tok/s` | **`53.35 tok/s`** | **`+16.8%`** |
| Median prefill | `825 tok/s` | `824 tok/s` | `−0.1%`, indistinguishable |
| First-token latency | `26.86 ms` | **`23.32 ms`** | **`−13.2%`** |

AProjQ4 exceeded 50 generated tokens/s and remained ahead at every tested decode frontier.

### M5 Max decode sweep, 2K–64K

| Context | Q8 decode | Q4 decode | Q4/Q8 |
|---:|---:|---:|---:|
| 2,048 | `40.6` | `48.4` | **`1.191`** |
| 14,336 | — | — | **`1.171`** |
| 26,624 | — | — | **`1.155`** |
| 38,912 | — | — | **`1.140`** |
| 51,200 | — | — | **`1.130`** |
| 65,536 | `30.4` | `34.3` | **`1.129`** |

The paired median was **`+15.5%`**, with Q4 ahead at **32 of 32** frontiers. The advantage narrows with context length but never reverses.

A separate M3 Ultra interleaved run reproduced the same direction:

- Q4 decode advantage: approximately `8.3–9.2%`.
- Q8 prefill advantage: approximately `0.72–0.93%`.
- Q4 had lower time to first token at every tested context.

Therefore the defensible Metal conclusion is: **decode and first-token latency clearly improve; end-to-end prefill is currently at parity, not yet a Q4 win.**

## CUDA / DGX Spark GB10

The default grouped Q4 attention-A prefill path introduced at `35b87dda` closed and crossed the previous Q4/Q8 prefill gap.

Same binary, interleaved arms, context 2K–8K:

| Context | Q8 prefill | Q4 grouped prefill | Q4 versus Q8 |
|---:|---:|---:|---:|
| 2,048 | `830.54` | `906.02` | `+9.09%`* |
| 4,096 | `956.45` | **`975.43`** | **`+1.98%`** |
| 6,144 | `941.55` | **`963.70`** | **`+2.35%`** |
| 8,192 | `937.46` | **`959.91`** | **`+2.39%`** |

\* The 2K row is affected by Q8 cold-start behavior and is not used for the steady-state conclusion.

The same report found:

- Q4 decode ahead of Q8 by **`13.9–18.0%`**.
- The grouped prefill path itself worth **`+4.4–4.6%`** over the previous Q4 default.
- No quality movement: output was byte-identical with the grouped path enabled or disabled.
- The path was verified as active through fail-closed `REQUIRE` testing.

Two CUDA scheduling options point in opposite directions:

| CUDA path | Default | GB10 prefill effect | Numerical result |
|---|---|---:|---|
| `DS4_CUDA_ENABLE_Q4_GROUPED_ATTN_A_PREFILL` | **On** | **approximately `+4.5%`** | Byte-identical |
| `DS4_CUDA_Q4_MMQ_16WARP` | Off / opt-in | **approximately `−6.3%`** | Byte-identical |

The shipping default is therefore the correct one for GB10. The 16-warp path should remain experimental or disabled on this architecture unless another shape envelope demonstrates a benefit.

## ROCm / Strix Halo gfx1151

The last complete, clean and fully tabulated public A/B shows:

| Context | Q8 prefill | Q4 prefill | Prefill delta | Q8 decode | Q4 decode | Decode delta |
|---:|---:|---:|---:|---:|---:|---:|
| 4,096 | `228.42` | `221.54` | **`−3.0%`** | `14.53` | **`16.22`** | **`+11.6%`** |
| 6,144 | `224.81` | `217.99` | **`−3.0%`** | `14.47` | **`16.15`** | **`+11.6%`** |
| 8,192 | `221.61` | `214.81` | **`−3.1%`** | `14.32` | **`15.97`** | **`+11.5%`** |

Subsequent K128 staging work was intended to reduce the remaining gap further, and the current branch status is approximately `−2%`. A new exact-head table is still needed before replacing the fully published `−3%` figures above.

ROCm nevertheless already provides:

- A stable `11.5–12.6%` decode advantage.
- `2.14 GiB` lower resident-model usage.
- `10.7%` shorter model-preparation time.
- Successful real-model deterministic generation.
- No BOS loops, corrupted output, NaNs or GPU hangs in the final clean sweeps.
- Correct fail-closed behavior for unsupported diagnostic paths.

## Advantages

| Advantage | Evidence |
|---|---|
| Lower memory use | `2.14 GiB`, or approximately `2.65%`, removed from the model footprint |
| Faster decode everywhere | Metal `+8–17%`, CUDA `+14–18%`, ROCm approximately `+12%` |
| CUDA prefill now wins | `+2.0–2.4%` at the reliable 4–8K frontiers |
| Faster startup and response | Model preparation `−10.7%`; Metal first token `−13.2%` |
| Quality preserved | No measured regression on the tracked 100-case fixture; imatrix Q4 has the best measured NLL |
| Correct backend dispatch | Fixes the previous situation where Q4 tensors could enter Q8-only execution paths |
| Better failure behavior | Unsupported optimized paths have rollback and fail-closed `REQUIRE` controls |
| Broader backend support | CPU reference, Metal, CUDA and ROCm implementations |
| Better testability | CPU oracles, parity tests, canaries, production-shape tests and benchmark harnesses |
| SSD-streaming support | Metal and ROCm paths plus benchmark support for low-memory systems |
| Reproducible model creation | Direct same-checkpoint GGUF-to-GGUF requantization with dense imatrix support |

## Disadvantages and remaining risks

| Issue | Impact |
|---|---|
| Metal prefill is not yet a demonstrated win | Current end-to-end A/B data show parity or a sub-1% Q8 advantage |
| ROCm prefill remains behind | Approximately `2–3%` at 4–8K, despite the clear decode win |
| No exact-current-head certification | Current head `6a20b131` is newer than the latest published CUDA A/B |
| Very large review surface | At this snapshot: 189 commits, 117 files, `+78,663 / −8,167` lines |
| Mixed ROCm Q4-A/Q8-B parity remains unresolved | It does not affect the shipped all-Q4 AProjQ4 layout, but the test status should still be recorded explicitly |
| At least one compiled binary is tracked | `speed-bench/metal_q4_attn_out_a_direct_bench` should be removed from the PR |
| Architecture-specific optimizations | GB10, gfx1151 and Apple-generation-specific defaults increase maintenance and regression risk |
| Some paths remain experimental | In particular, the CUDA 16-warp path is slower on GB10 and should not become the default |
| Incomplete coverage | CUDA SSD streaming, non-GB10 CUDA, multi-GPU/TP and a full CPU end-to-end run still lack final coverage |
| Limited quality sample | 100 cases and 2,313 target tokens establish no measured regression, not universal superiority |
| Explicitly narrow model support | This implements the AProjQ4 DeepSeek V4 Flash layout, not arbitrary Q4 GGUF compatibility |

## Conclusion

The central objective of this PR has been achieved:

- AProjQ4 models no longer enter incompatible Q8-only paths.
- The model footprint is reduced by `2.14 GiB`.
- Decode is faster on every tested GPU backend.
- CUDA now wins in both prefill and decode.
- Metal has a large decode and first-token advantage, with prefill currently at parity.
- ROCm has a large decode and startup advantage, with only a small prefill gap remaining.
- No quality regression was measured on the tracked fixture.
- Several real correctness and stability bugs were found, fixed and converted into regression tests.

The merge case is strong, but the following should be completed first:

1. Run the compact validation matrix on the exact final SHA.
2. Remove committed build artifacts.
3. Publish the final ROCm table supporting the approximately `−2%` prefill figure.
4. Record the final mixed Q4-A/Q8-B parity status.
5. Confirm Q8-on-PR versus Q8-on-main on the final SHA.
6. Clearly identify default, opt-in and diagnostic-only optimized paths.

### Source reports

- [Metal M5 Max resident A/B and 32-frontier sweep](https://github.com/antirez/ds4/pull/621#issuecomment-5470605362)
- [Metal M3 Ultra interleaved Q4/Q8 test](https://github.com/antirez/ds4/pull/621#issuecomment-5384560407)
- [CUDA GB10 grouped-prefill result](https://github.com/antirez/ds4/pull/621#issuecomment-5510480480)
- [ROCm gfx1151 clean A/B result](https://github.com/antirez/ds4/pull/621#issuecomment-5499014462)
- [ROCm model-size, startup and functional validation](https://github.com/antirez/ds4/pull/621#issuecomment-5354801376)
- [ROCm correctness follow-up](https://github.com/antirez/ds4/pull/621#issuecomment-5505885849)

---

# PR #621 — final AProjQ4 status, benchmark summary and trade-offs

This comment consolidates the final useful results from the PR discussion and intentionally omits superseded intermediate experiments.

The PR adds explicit support for the DeepSeek V4 Flash **AProjQ4** layout. The 215 dense attention-projection tensors—five tensor families across 43 layers—are stored as `Q4_K` instead of `Q8_0`, while the remaining quantization mix and checkpoint identity stay unchanged.

The implementation covers CPU reference execution, Metal, CUDA, ROCm, SSD streaming, direct GGUF-to-GGUF requantization with an importance matrix, backend-specific tests, CPU oracles, canaries and fail-closed diagnostic controls.

> **Benchmark-snapshot caveat**
>
> The PR currently points to head `6a20b131`. The published benchmark results below were produced on explicitly named earlier snapshots. They represent the latest matched Q4/Q8 measurements currently available in the discussion, but an exact-final-SHA smoke rerun is still required before merge.

## Executive summary

| Backend | Prefill: AProjQ4 vs AProjQ8 | Decode: AProjQ4 vs AProjQ8 | Result |
|---|---:|---:|---|
| **Metal / M5 Max** | Approximately equal in the published end-to-end run | Median **+15.5%** over 2K–64K; **+16.8%** in the isolated 2K run | Clear decode win; prefill parity |
| **CUDA / GB10** | **+2.0% to +2.4%** at 4K–8K | **+13.9% to +18.0%** | Q4 wins both prefill and decode |
| **ROCm / gfx1151** | Approximately **−2% to −3%**; last complete public table is around −3% | Approximately **+11.5% to +12.6%** | Decode win; small prefill gap remains |
| **CPU** | Correctness/reference backend | Not a principal performance target | Supported |

The central result is therefore:

- AProjQ4 is faster in decode on every tested GPU backend.
- CUDA has crossed Q8 in both prefill and decode.
- Metal has a large and persistent decode lead, with end-to-end prefill currently at parity in the strongest published comparison.
- ROCm has nearly closed the prefill gap, but still trails Q8 slightly while retaining a clear decode advantage.

## Model size and memory

| Layout | File/model footprint | Difference |
|---|---:|---:|
| AProjQ8 | 86,720,111,488 bytes — **80.76 GiB** | Baseline |
| AProjQ4 | 84,420,584,288 bytes — **78.62 GiB** | **−2.14 GiB** |
| Saving | 2,299,527,200 bytes | Approximately **−2.65%** |

On a resident system, the 2.14 GiB saving becomes additional context/KV headroom. In SSD-streaming configurations it can instead be used for a larger routed-expert cache, reducing repeated expert reads.

The matched AProjQ4 artifact is:

```text
DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ4-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf

SHA-256:
413cf0a68ca8d084e89f3f810eef5046b5308174d441a80017a0ff388933c767

Quantization quality

Same-checkpoint CUDA comparison on the tracked official-continuation fixture:

Model avg_nll First-token match Average greedy LCP API top-1
AProjQ8 0.404811251 55/100 5.150 0.858625
AProjQ4 without imatrix 0.407383304 56/100 5.540 0.853437
AProjQ4 with imatrix 0.396432403 60/100 6.090 0.860355

On this fixture, AProjQ4 with the importance matrix has approximately 2.07% lower NLL than the matched Q8 control and does not show a measured quality regression.

This is a limited fixture of 100 cases and 2,313 target tokens. It is evidence that quality is preserved for this matched comparison, not proof that Q4 is universally better than Q8. The difference between the two Q4 rows also shows that the importance matrix is an important part of the final artifact.

Backend results

Metal — Apple M5 Max, resident

Isolated 2K result

Metric AProjQ8 AProjQ4 Q4 delta
Steady decode 45.69 tok/s 53.35 tok/s +16.8%
Prefill 825 tok/s 824 tok/s −0.1%, within noise
First generated token 26.86 ms 23.32 ms −13.2% latency

AProjQ4 exceeds 50 generated tokens/s and produces the first token approximately 3.54 ms earlier.

Decode sweep, 2K–64K

Context Q4/Q8 decode ratio Q4 advantage
2,048 1.191 +19.1%
14,336 1.171 +17.1%
26,624 1.155 +15.5%
38,912 1.140 +14.0%
51,200 1.130 +13.0%
65,536 1.129 +12.9%

Across the complete sweep:

Paired median Q4/Q8: 1.155, or +15.5%
Frontiers won by Q4: 32 of 32

The decode advantage becomes smaller as context increases, but it never reverses.

The published end-to-end Metal prefill measurement is effectively tied. Therefore the defensible Metal conclusion is decode win plus prefill parity, rather than a claimed prefill win.

Source: M5 Max Q4/Q8 benchmark

CUDA — NVIDIA GB10 / sm_121

The default grouped Q4 attention-A prefill path is the first published CUDA configuration in this PR where AProjQ4 exceeds the matched AProjQ8 model in prefill.

The 2K result is intentionally excluded from the headline comparison because the Q8 arm showed a first-load cold-start artifact.

Stable prefill frontiers

Context AProjQ8 prefill AProjQ4 prefill Q4 delta
4,096 956.45 tok/s 975.43 tok/s +1.98%
6,144 941.55 tok/s 963.70 tok/s +2.35%
8,192 937.46 tok/s 959.91 tok/s +2.39%

Average advantage across 4K–8K: approximately +2.24%.

Decode remains ahead of Q8 by approximately +13.9% to +18.0% across the measured frontiers.

The grouped prefill path itself was measured as approximately +4.4% to +4.6% over the previous default CUDA path, and the corresponding quality outputs were byte-identical.

An important configuration detail:

CUDA path Default Effect on GB10 prefill Numerical result
Grouped attention-A prefill On Approximately +4.5% Byte-identical
Experimental Q4 16-warp MMQ Off / opt-in Approximately −6.3% on GB10 Byte-identical

The default shipping configuration is therefore the faster one for GB10. The 16-warp path should remain experimental or architecture-gated rather than becoming the general default.

Source: final CUDA grouped-prefill benchmark

ROCm — AMD Strix Halo / gfx1151

The latest complete clean public short-context table shows:

Context AProjQ8 prefill AProjQ4 prefill Q4 delta AProjQ8 decode AProjQ4 decode Q4 delta
4,096 228.42 tok/s 221.54 tok/s −3.0% 14.53 tok/s 16.22 tok/s +11.6%
6,144 224.81 tok/s 217.99 tok/s −3.0% 14.47 tok/s 16.15 tok/s +11.6%
8,192 221.61 tok/s 214.81 tok/s −3.1% 14.32 tok/s 15.97 tok/s +11.5%

The 2K frontier is omitted because Q8 repeatedly exhibited a first-frontier warm-up artifact, making that row unsuitable for the steady-state comparison.

Subsequent ROCm work has targeted the remaining gap with wider K64/K128 WMMA staging. The current status can reasonably be described as approximately 2–3% behind Q8 in prefill, pending publication of a final raw table for the latest head.

The decode result is already unambiguous: AProjQ4 is roughly 11–12% faster.

The previous deterministic GPU-hang issue in the k0 loop has been fixed. The default real-model path completed cleanly without GPU hangs, non-finite output, BOS loops or corrupted text.

Source: ROCm clean Q4/Q8 rerun

Cold-start and time-to-first-token behavior

Q4 has a noticeably better startup profile than Q8. This should be described as a cold-start / model-preparation / first-token advantage, rather than claiming that every internal warm-up kernel is faster.

Platform and metric AProjQ8 AProjQ4 Q4 improvement
ROCm model preparation 19.700 s 17.583 s −10.7%
Metal residency-request mean 497.42 ms 480.77 ms −3.3%
Metal first generated token 26.86 ms 23.32 ms −13.2%

Q8 also repeatedly showed a large first-load or first-frontier prefill dip in several benchmark windows, while Q4 reached its normal throughput more directly. Those contaminated Q8 rows have been excluded from the steady-state performance conclusions, but they are relevant to interactive cold-start behavior.

In practical terms, Q4:

  • prepares the smaller model faster;
  • reaches the first generated token sooner;
  • shows less sensitivity to the first timed frontier;
  • needs to move approximately 2.14 GiB less model data.

Advantages

  1. Lower memory footprint. AProjQ4 saves approximately 2.14 GiB without changing the checkpoint or the routed/shared/output quantization mix.

  2. Faster decode on every tested GPU backend. The gain is approximately 15.5% median on the M5 Max Metal sweep, 13.9–18.0% on CUDA/GB10, and 11.5–12.6% on ROCm/gfx1151.

  3. CUDA prefill now exceeds Q8. The stable 4K–8K results show a 2.0–2.4% advantage.

  4. Better cold-start behavior. Model preparation, residency and first-token latency all benefit from the smaller dense-attention representation.

  5. No measured quality regression on the tracked fixture. The imatrix Q4 model produced the best measured NLL in the matched comparison.

  6. Fixes a real correctness gap. DS4 could previously load Q4 dense-attention tensors while dispatching Q8-only assumptions, leading to BOS loops, corrupted output, non-finite logits, incompatible kernels or prefill failures.

  7. Cross-backend implementation. CPU reference, Metal, CUDA and ROCm all understand the explicit AProjQ4 layout.

  8. Direct requantization workflow. The final model can be produced directly from the matched Q8 GGUF using the dense importance matrix, without returning to the original full-precision checkpoint.

  9. SSD-streaming support. The smaller footprint can be converted into additional expert-cache capacity on memory-constrained machines.

  10. Substantial validation infrastructure. The PR adds backend-specific oracles, parity tests, canaries, boundary tests, benchmark harnesses and fail-closed REQUIRE controls.

Disadvantages and remaining risks

  1. The PR is extremely large. At the current snapshot it contains 189 commits, touches 117 files and reports approximately 78,663 additions and 8,167 deletions. This significantly increases review and integration risk.

  2. Benchmarks are not all on the exact current head. The branch continued to change after the strongest Metal, CUDA and ROCm benchmark snapshots. A compact final rerun on 6a20b131 or the actual merge SHA remains necessary.

  3. ROCm prefill is still slightly behind Q8. The remaining difference is small compared with earlier versions, but it has not yet crossed parity in the last complete published table.

  4. Some mixed ROCm Q4-A/Q8-B parity cases remain unresolved or diagnostic. They do not affect the published all-Q4 AProjQ4 GGUF, but the final status should be explicitly documented.

  5. The best CUDA results are GB10-specific. Non-GB10 CUDA, CUDA SSD streaming and broader architecture coverage are not established by the current benchmark set.

  6. Multi-GPU and tensor-parallel coverage is incomplete. The PR touches shared engine and placement code, so this remains an integration-risk area.

  7. CPU is primarily a correctness backend. End-to-end CPU AProjQ4 performance is not a demonstrated advantage.

  8. Some optimized paths depend on architecture-specific policy and environment controls. This adds maintenance cost and makes it important to document which paths are default, opt-in, diagnostic or rollback-only.

  9. Warning-clean ROCm builds still need attention. Several published ROCm builds completed successfully but emitted warnings, while the release QA document treats new warnings as failures.

  10. Generated executables/blobs should not be committed. Tracked benchmark or test binaries should be removed from the PR and produced by the build instead.

  11. The quality fixture is limited. Its positive result should not be generalized beyond the tested 100 cases and 2,313 target tokens.

Merge assessment

The core feature is valuable and has been demonstrated to work:

  • the AProjQ4 model loads and executes correctly;
  • the model is 2.14 GiB smaller;
  • decode is faster on Metal, CUDA and ROCm;
  • CUDA now also wins in prefill;
  • ROCm is close to prefill parity;
  • quality is preserved on the tracked matched fixture;
  • the original BOS-loop, corruption, non-finite-output and backend-dispatch failures have been addressed.

Before merge, the remaining work should be limited to release validation rather than additional feature expansion:

  • remove committed generated binaries;
  • run the compact Metal, CUDA, ROCm and CPU smoke matrix on the exact final SHA;
  • publish the latest raw ROCm Q4/Q8 table;
  • confirm the final mixed ROCm parity status;
  • verify existing Q8 behavior against main;
  • state clearly which optimized paths are default, opt-in or diagnostic.

With those items recorded, the measured memory reduction, startup improvement and cross-backend decode gains provide a strong justification for the AProjQ4 layout.


Questa è la formulazione che userei adesso, senza pubblicarla. Il punto che non scriverei ancora è “Metal wins prefill”: con i numeri presenti nella PR, la frase tecnicamente sostenibile è “Metal prefill parity, strong decode win”. Analogamente, per scrivere ROCm −2% in modo netto sostituirei la tabella da −3% con il run più recente che produce quel risultato.

Non ho pubblicato nulla. La bozza sotto è quella che userei come commento principale, in inglese per coerenza con la PR.

Ho mantenuto due cautele importanti:

  • Il benchmark Metal numericamente più solido già pubblicato mostra il prefill sostanzialmente in parità: 824 contro 825 tok/s. Il vantaggio nel decode è invece netto. Per dichiarare formalmente che Metal vince anche nel prefill inserirei la nuova tabella completa.
  • Per ROCm, il −2% è lo stato più recente da te indicato; l’ultima tabella completa pubblica prima dell’ottimizzazione K128 mostrava circa −3%. Nella bozza distinguo i due valori, senza inventare misurazioni mancanti.

Consolidated AProjQ4 status — final results, benefits and trade-offs

This comment supersedes the intermediate benchmark reports scattered throughout the discussion. Exploratory configurations, temporary regressions, subsequently reverted paths, first-load outliers and memory-pressure-contaminated runs have intentionally been excluded.

The tables below use the latest clean and directly comparable AProjQ4/AProjQ8 measurements available for each backend. Cold first-frontier results identified as warm-up artefacts are not used for steady-state conclusions.

Current PR head at the time of writing: 6a20b131e18f6a1650706d8f76e15f60ac2bc91f. Since the last CUDA optimization landed after the most recent complete benchmark, one final smoke matrix should still be run on the exact merge SHA.

1. What the PR changes

This PR adds explicit support for the DeepSeek V4 Flash AProjQ4 layout.

The 215 dense attention-projection tensors are converted from Q8_0 to Q4_K:

Tensor family Layers Converted tensors
attn_q_a 43 43
attn_q_b 43 43
attn_kv 43 43
attn_output_a 43 43
attn_output_b 43 43
Total 43 215

The routed experts, shared experts, output head, checkpoint identity and remaining quantization mix are unchanged.

This is support for one explicit DS4 model layout. It is not intended to be generic support for arbitrary Q4 GGUF files.

2. Executive performance summary

Backend Prefill Decode Startup / first token Current conclusion
Metal Latest result is reported ahead; the last complete public M5 A/B was effectively tied at 824 vs 825 tok/s +15.5% paired median over 2K–64K; +16.8% in the isolated 2K run First token 13.2% faster; residency request about 3.3% faster Strong decode win; no measured prefill regression
CUDA / GB10 +2.0% to +2.4% at the reliable 4K–8K frontiers +13.9% to +18.0% Q4 reaches stable throughput sooner; cold Q8 outliers excluded Q4 wins both prefill and decode
ROCm / gfx1151 Approximately 2% behind in the latest reported state; last complete public table was about 3% behind +11.5% to +12.6% Model preparation 10.7% faster Clear decode win; small prefill gap remains

3. Model size and memory

Layout File size Approximate resident model
AProjQ8 86,720,111,488 bytes 80.76 GiB
AProjQ4 84,420,584,288 bytes 78.62 GiB
Saving 2,299,527,200 bytes 2.14 GiB / 2.65%

The 2.14 GiB reduction is available as:

  • additional KV/context headroom on resident systems;
  • a larger expert cache on constrained unified-memory machines;
  • reduced repeated expert I/O in SSD-streaming configurations;
  • lower startup residency and model-preparation cost.

A CUDA long-context capacity test also demonstrated the practical effect:

Model / configured context Filled tokens Minimum free memory
AProjQ8 at 200K 194,681 2.5 GB
AProjQ4 at 300K 293,390 14.6 GB

These rows use different context sizes and are therefore a capacity demonstration, not a like-for-like speed comparison.

4. Quantization quality

Final same-checkpoint comparison using the tracked official-continuation fixture:

Model avg_nll First-token match Average greedy LCP API top-1
AProjQ8 0.404811251 55/100 5.150 0.858625
AProjQ4 with imatrix 0.396432403 60/100 6.090 0.860355

On this fixture, AProjQ4 with the dense imatrix shows:

  • avg_nll approximately 2.07% lower;
  • five additional first-token matches;
  • average greedy LCP higher by 0.94;
  • API top-1 rate higher by 0.00173.

This demonstrates no measured quality regression on this fixture. The sample contains 100 cases and 2,313 target tokens, so it must not be interpreted as proof that Q4 is universally better than Q8.

5. Metal

M5 Max, resident, isolated 2K frontier

Three interleaved repetitions per model:

Model Prefill tok/s Steady decode tok/s First-token latency
AProjQ4 824 53.35 23.32 ms
AProjQ8 825 45.69 26.86 ms
Q4 delta −0.12%, parity +16.8% −13.2%

The published numerical evidence therefore establishes:

  • a large and repeatable decode advantage;
  • lower time to first token;
  • prefill statistically indistinguishable in this run.

Full decode sweep

The resident M5 Max sweep covered every 2K frontier from 2,048 to 65,536 tokens:

Result Value
Frontiers where Q4 beat Q8 32 / 32
Paired median Q4/Q8 ratio 1.155
Paired median decode advantage +15.5%
Q4/Q8 ratio at 2K 1.191
Q4/Q8 ratio at 64K 1.129
Q4 decode at 64K 34.3 tok/s
Q8 decode at 64K 30.4 tok/s

The decode advantage narrows as context grows, but it never inverts across the tested frontiers.

The final current-head Metal prefill table should be added here before claiming a numerical prefill win. The already-published M5 data supports parity, not a statistically established lead.

6. CUDA / DGX Spark GB10

The final default grouped attention-A prefill path is the first CUDA configuration in this PR that puts AProjQ4 ahead of the matched AProjQ8 model.

The 2K row is intentionally excluded below because the Q8 first load showed a cold-start artefact. The reliable 4K–8K rows are:

Context Q4 grouped prefill Q4 legacy prefill Grouped-path gain Q8 prefill Q4 vs Q8
4K 975.43 932.72 +4.58% 956.45 +1.98%
6K 963.70 922.45 +4.47% 941.55 +2.35%
8K 959.91 919.40 +4.41% 937.46 +2.39%

Therefore:

  • enabling the grouped default is worth approximately +4.4% to +4.6% over the previous Q4 prefill route;
  • AProjQ4 is approximately +2.0% to +2.4% faster than AProjQ8 in prefill at 4K–8K;
  • AProjQ4 remains approximately +13.9% to +18.0% faster in decode;
  • the grouped prefill candidate is byte-identical to its control in the published quality comparison.

CUDA scheduling note

Two CUDA optimization controls have opposite results on GB10:

Path Default Prefill effect Numerical result
Grouped Q4 attention-A prefill Enabled approximately +4.5% Byte-identical
Experimental Q4 16-warp MMQ Disabled / opt-in approximately −6.3% when enabled Byte-identical

The configuration that ships by default is the faster one. The 16-warp path should remain experimental or be gated by architecture-specific policy.

7. ROCm / Strix Halo gfx1151

The latest fully posted clean A/B table before the final K128 staging work was:

Context Q8 prefill Q4 prefill Prefill delta Q8 decode Q4 decode Decode delta
4K 228.42 221.54 −3.0% 14.53 16.22 +11.6%
6K 224.81 217.99 −3.0% 14.47 16.15 +11.6%
8K 221.61 214.81 −3.1% 14.32 15.97 +11.5%

The subsequent K128 staging work is reported to have narrowed the remaining prefill difference to approximately 2%. That result should be accompanied by its exact current-head table in the final comment.

The stable conclusion is:

  • ROCm decode is a clear Q4 win;
  • ROCm prefill has improved dramatically from the original implementation;
  • a small approximately 2% Q4 prefill gap remains;
  • no GPU hangs were observed in the final clean sweeps;
  • the unsafe compounded-WMMA attention-output-B path was removed from normal selection.

8. Startup, residency and warm-up behaviour

Q4 reaches useful steady-state operation sooner because less model data must be prepared and made resident.

Measurement AProjQ8 AProjQ4 Q4 improvement
ROCm model preparation 19.700 s 17.583 s 2.117 s / 10.7% faster
Metal residency-request mean 497.42 ms 480.77 ms 16.65 ms / 3.3% faster
Metal first-token latency 26.86 ms 23.32 ms 3.54 ms / 13.2% faster

Several benchmark windows also showed a substantial first-frontier depression when Q8 was loaded cold, while Q4 reached its normal throughput immediately or much sooner. Those cold Q8 points were deliberately excluded from steady-state performance comparisons rather than being counted as a Q4 throughput win.

The low-level Metal kernel warm-up itself was approximately 6–7 ms for both models. The practical Q4 advantage comes primarily from:

  • the smaller resident model;
  • faster model preparation and residency;
  • reduced first-token latency;
  • less severe cold-start behaviour.

9. Main advantages

  1. Lower memory use: 2.14 GiB less model memory, directly increasing context or cache headroom.

  2. Faster decode on every tested GPU backend: Metal, CUDA and ROCm all show a clear and repeatable Q4 decode advantage.

  3. CUDA wins both phases: the default grouped path now puts Q4 ahead in both prefill and decode on GB10.

  4. Faster startup behaviour: lower preparation, residency and first-token times, with Q4 reaching steady throughput more quickly.

  5. No measured quality regression: the imatrix-based Q4 model equals or improves the tracked Q8 metrics on the current 100-case fixture.

  6. Correctness gap fixed: Q4 models no longer load successfully only to be executed through incompatible Q8-only assumptions, avoiding the previously observed BOS loops, corrupted output, non-finite logits and prefill failures.

  7. Broader backend support: CPU reference, Metal, CUDA and ROCm implementations, including resident and SSD-streaming paths where supported.

  8. Much stronger validation surface: CPU oracles, canaries, parity tests, decode-consistency tests, fail-closed REQUIRE controls, benchmark harnesses and expanded environment-variable/release documentation.

10. Disadvantages and remaining risks

  1. ROCm prefill is not yet fully at parity: the remaining difference is approximately 2%, despite the clear decode win.

  2. The PR is very large: approximately 189 commits, 117 changed files, 78,663 additions and 8,167 deletions. This increases review, maintenance and regression risk.

  3. Some optimizations are architecture-specific: a path that wins on one GPU can regress on another. The CUDA 16-warp experiment is a concrete example, costing approximately 6.3% on GB10.

  4. The final head has moved since some backend benchmarks: the exact merge candidate SHA must receive one final compact validation run.

  5. Mixed ROCm Q4-A/Q8-B parity remains a known diagnostic issue: it does not affect the shipped all-Q4 AProjQ4 layout, but it should remain documented rather than hidden.

  6. Coverage is not universal: CUDA SSD streaming, non-GB10 CUDA hardware and complete multi-GPU/tensor-parallel execution have not received the same end-to-end coverage.

  7. Generated binaries or other build artefacts must not remain in the diff: only source, tests, reproducible data and documentation should be committed.

  8. Quality conclusions remain fixture-bound: 100 cases and 2,313 target tokens are useful evidence, but not a universal evaluation.

11. Final assessment

The core objective of this PR has been achieved:

  • AProjQ4 is correctly supported on CPU, Metal, CUDA and ROCm;
  • the model is 2.14 GiB smaller;
  • decode is faster on every tested GPU backend;
  • CUDA now wins both prefill and decode;
  • Metal has a strong decode win and no published prefill regression, with the latest run reported ahead;
  • ROCm has a strong decode win and only a small remaining prefill gap;
  • startup and first-token behaviour improve with Q4;
  • no quality regression is measured on the tracked fixture;
  • several real correctness and performance defects discovered during development were fixed and converted into permanent tests.

Before merge, the remaining work should be limited to:

  1. removing committed generated binaries/build artefacts;
  2. rerunning the compact Metal, CUDA, ROCm and CPU smoke matrix on the exact final SHA;
  3. adding the exact current-head Metal prefill table;
  4. adding the exact post-K128 ROCm table supporting the approximately 2% remaining gap;
  5. confirming that the matched AProjQ8 paths have not regressed.

Subject to those final validation and cleanup steps, the performance, memory and correctness case for AProjQ4 is positive.


Verifica dei numeri usati nella bozza

La PR è attualmente aperta, mergeable, con head 6a20b131, 189 commit, 117 file modificati, 78.663 aggiunte e 8.167 rimozioni. La descrizione documenta i 215 tensori, il risparmio di 2,14 GiB e il confronto qualitativo.

Il test Metal M5 Max riporta 53,35 contro 45,69 tok/s nel decode, 824 contro 825 nel prefill, 23,32 contro 26,86 ms sul primo token e un vantaggio decode in 32 frontiere su 32.

Il test CUDA sul percorso grouped riporta +1,98%, +2,35% e +2,39% nel prefill a 4K, 6K e 8K, con un vantaggio decode compreso tra +13,9% e +18,0%.

L’ultimo A/B ROCm completo pubblicato prima dell’ottimizzazione K128 riporta circa −3% nel prefill e +11,5–11,6% nel decode.

I dati di preparazione ROCm, 19,700 contro 17,583 secondi, e il risparmio di memoria sono riportati nella validazione Strix Halo; i tempi Metal di residency e warm-up provengono dal test interleaved M3 Ultra.

Prima di pubblicarla, completerei soltanto le due tabelle mancanti: il nuovo prefill Metal realmente vincente e il ROCm post-K128 che porta il divario dal 3% a circa il 2%.

Non è stato pubblicato nulla.

Questa è la versione che userei come commento iniziale. Farei però una correzione rispetto alla formulazione originale: i dati pubblicati non dimostrano ancora un vantaggio di Q4 nel prefill Metal. Sul test M5 Max il risultato è 824 contro 825 tok/s, quindi sostanziale parità; su CUDA, invece, il sorpasso è documentato tra +1,98% e +2,39%. Su ROCm l’ultimo sweep completamente tabellato mostra circa −3%; descriverei quindi il divario come circa 2–3%, non come un 2% esatto.


Consolidated AProjQ4 results — final measurements only

This comment consolidates the relevant final AProjQ4 results and intentionally omits intermediate experiments, superseded regressions, unsuccessful tuning attempts, and cold-start-contaminated performance claims.

Important

The current PR head is 6a20b131e18f6a1650706d8f76e15f60ac2bc91f.
The latest complete backend-specific A/B measurements were produced on different recent snapshots. A compact Metal, CUDA, ROCm and CPU validation should therefore still be repeated on the exact merge SHA.

What this PR changes

AProjQ4 keeps the same DeepSeek V4 Flash checkpoint and the same routed-expert, shared-expert and output quantization mix, but converts the five dense attention-projection families in all 43 layers from Q8_0 to Q4_K:

  • attn_q_a
  • attn_q_b
  • attn_kv
  • attn_output_a
  • attn_output_b

This corresponds to 215 converted tensors.

The PR adds tensor-type-aware execution for these projections on CPU, Metal, CUDA and ROCm, including prefill, decode and the relevant SSD-streaming paths. It also adds direct GGUF-to-GGUF requantization with dense-imatrix support, backend-specific tests, CPU oracles, canaries and fail-closed diagnostic controls.

Executive summary

Backend Prefill, AProjQ4 vs AProjQ8 Decode, AProjQ4 vs AProjQ8 Result
Metal / M5 Max Approximately equal in the published full-model A/B Median +15.5%, Q4 ahead at 32/32 tested frontiers Clear decode win
CUDA / GB10 +1.98% to +2.39% at 4K–8K +13.9% to +18.0% Q4 wins both phases
ROCm / gfx1151 Approximately −2% to −3%; last fully tabulated sweep is about −3% +11.5% to +12.6% Decode win; small prefill gap remains

The consistent result across all three GPU backends is that AProjQ4 materially improves decode. CUDA has now also crossed Q8 in prefill. ROCm is close, but still slightly behind in prefill.

Model size and memory

Layout File size Approximate resident size
AProjQ8 86,720,111,488 bytes 80.76 GiB
AProjQ4 84,420,584,288 bytes 78.62 GiB
Saving 2,299,527,200 bytes 2.14 GiB / 2.65%

On fully resident systems, this becomes additional context and KV-cache headroom. Under SSD streaming, it can instead be used for a larger routed-expert cache and fewer repeated expert reads.

Quantization quality

Same-checkpoint CUDA comparison on the tracked official-continuation fixture:

Model avg_nll First-token match Average greedy LCP API top-1
AProjQ8 0.404811251 55/100 5.150 0.858625
AProjQ4 without imatrix 0.407383304 56/100 5.540 0.853437
AProjQ4 with imatrix 0.396432403 60/100 6.090 0.860355

On this fixture, AProjQ4 with the dense imatrix shows no measured quality regression and records the best measured NLL of the three arms.

The fixture contains 100 cases and 2,313 target tokens. These results demonstrate preservation on this test set; they should not be interpreted as proof that Q4 is universally better than Q8.


Metal

M5 Max, resident, isolated 2K frontier

Metric AProjQ8 AProjQ4 Q4 delta
Prefill 825 tok/s 824 tok/s −0.1% — parity
Steady decode 45.69 tok/s 53.35 tok/s +16.8%
First generated token 26.86 ms 23.32 ms −13.2% latency

AProjQ4 exceeds 50 generated tokens/s and is faster than AProjQ8 in decode.

Decode sweep, 2K–64K context

Context Q8 decode Q4 decode Q4 advantage
2,048 40.6 tok/s 48.4 tok/s +19.1%
14,336 +17.1%
26,624 +15.5%
38,912 +14.0%
51,200 +13.0%
65,536 30.4 tok/s 34.3 tok/s +12.9%

Summary:

  • Paired median Q4/Q8 ratio: 1.155, or +15.5%.
  • AProjQ4 wins at 32 of 32 context frontiers.
  • The advantage narrows as context grows, but it never reverses.
  • Published full-model prefill is currently best described as parity, not a demonstrated Q4 win.

CUDA / DGX Spark GB10

The default grouped attention-A prefill path introduced at 35b87dda is the first published CUDA result where AProjQ4 overtakes AProjQ8 in prefill.

The 2K row is omitted from the steady-state conclusion because the Q8 arm showed a first-load cold-start artifact.

Context Q4 grouped prefill Q4 fallback prefill Grouped-path gain Q8 prefill Q4 vs Q8
4,096 975.43 932.72 +4.58% 956.45 +1.98%
6,144 963.70 922.45 +4.47% 941.55 +2.35%
8,192 959.91 919.40 +4.41% 937.46 +2.39%

Decode is unaffected by this prefill-only routing change, and AProjQ4 retains a +13.9% to +18.0% decode advantage over AProjQ8 across the measured frontiers.

Quality checks for the grouped path were byte-identical:

grouped default vs fallback  → byte-identical
grouped default vs prior SHA → byte-identical

CUDA scheduling note

Path Default GB10 prefill effect Numerics
DS4_CUDA_ENABLE_Q4_GROUPED_ATTN_A_PREFILL On Approximately +4.5% Byte-identical
DS4_CUDA_Q4_MMQ_16WARP Off, experimental Approximately −6.3% on GB10 Byte-identical

The shipping default is therefore the faster path on GB10. The experimental 16-warp implementation remains useful for investigation or other hardware, but should not be enabled by default on this device.


ROCm / Strix Halo gfx1151

ROCm now builds and runs the AProjQ4 model successfully. The current short-context picture is a clear decode win with a small remaining prefill gap.

The 2K prefill result is excluded from the steady-state conclusion because the Q8 arm shows a cold-first-frontier artifact.

Context Q8 prefill Q4 prefill Q4 delta Q8 decode Q4 decode Q4 delta
4,096 228.42 221.54 −3.0% 14.53 16.22 +11.6%
6,144 224.81 217.99 −3.0% 14.47 16.15 +11.6%
8,192 221.61 214.81 −3.1% 14.32 15.97 +11.5%

The branch has reduced a much larger earlier ROCm prefill gap to only a few percent. The remaining difference should be summarized as approximately 2–3%, pending an exact-current-head rerun.

Correctness results on the default path are clean:

  • coherent deterministic output;
  • bounded decode-consistency results;
  • no BOS loop or corrupted output;
  • no GPU hang across the clean build and benchmark sweeps;
  • the unsafe compounded attention-output-B WMMA selection was removed from the normal path;
  • strict unsupported paths now fail closed instead of silently falling back or corrupting output.

Startup and warm-up behavior

AProjQ4 also reaches useful execution more quickly than AProjQ8.

Measurement AProjQ8 AProjQ4 Q4 improvement
ROCm model preparation 19.700 s 17.583 s 2.117 s / 10.7% faster
Metal residency request, mean 497.42 ms 480.77 ms 16.65 ms / 3.3% faster
Metal M5 first generated token 26.86 ms 23.32 ms 3.54 ms / 13.2% lower latency

CUDA and ROCm measurements also repeatedly show a pronounced Q8 first-load or first-frontier throughput dip that is absent or much smaller with Q4. Those cold-start-contaminated rows are deliberately excluded from the steady-state prefill claims, but they support the operational observation that Q4 reaches stable throughput sooner.


Advantages

Advantage Practical impact
2.14 GiB smaller model More context/KV headroom or a larger SSD-streaming expert cache
Faster decode on Metal, CUDA and ROCm Higher interactive and agent-generation throughput
CUDA prefill now ahead of Q8 Q4 no longer trades short-context prompt ingestion for decode speed on GB10
Faster startup and first-token behavior Lower cold-start and interactive latency
No measured quality regression Q4 with imatrix performs at least as well as Q8 on the tracked fixture
Correct tensor-type dispatch Prevents Q4 tensors from reaching incompatible Q8-only kernels
Cross-backend implementation CPU reference, Metal, CUDA and ROCm support
Direct requantization workflow Existing same-checkpoint Q8 GGUFs can be converted without rebuilding from the full source checkpoint
Expanded tests and fail-closed controls Regressions produce explicit failures instead of silent corruption

Disadvantages and remaining risks

Issue Impact
ROCm prefill remains approximately 2–3% behind Q8 ROCm does not yet reproduce the CUDA prefill win
Metal prefill win is not yet demonstrated by the published full-model A/B Current evidence supports parity; a newer winning table is required before claiming otherwise
Large review surface Current PR contains 189 commits, 117 changed files, 78,663 additions and 8,167 deletions
Hardware- and shape-specific optimization policy More dispatch logic, environment controls and long-term maintenance burden
Not generic Q4 support The implementation targets the explicit AProjQ4 layout, not arbitrary Q4 GGUF files
Mixed ROCm Q4-A/Q8-B parity remains unresolved It does not affect the published all-Q4 AProjQ4 GGUF, but the diagnostic case should be documented or fixed
Incomplete exact-head validation The final SHA still needs a complete cross-backend build, correctness and performance pass
Coverage gaps CUDA SSD streaming, non-GB10 CUDA and end-to-end multi-GPU/TP are not fully validated
Tracked benchmark binary/blob Generated executable artifacts should be removed from the PR before merge
Warning cleanliness The final ROCm/CUDA build logs should be checked against the repository’s release-QA policy

Conclusion

The main objective of this PR has been achieved:

  • AProjQ4 models execute through the correct tensor-type-aware paths;
  • the model footprint is reduced by 2.14 GiB;
  • decode is faster on all tested GPU backends;
  • CUDA now also beats Q8 in prefill;
  • ROCm is close to parity, with only a small remaining prefill gap;
  • startup and first-token behavior are faster;
  • the tracked quality fixture shows no regression;
  • several real correctness and performance bugs discovered during development have been fixed and converted into permanent tests.

The PR provides a meaningful user-facing improvement, particularly for memory-constrained resident execution, SSD-streaming cache capacity and decode-heavy workloads.

Before merge, the remaining work should be limited to cleanup and exact-final-SHA validation rather than additional feature expansion.

@GiorgioOppo
GiorgioOppo deleted the aprojq4-dense-attention branch July 28, 2026 11:42
@GiorgioOppo
GiorgioOppo restored the aprojq4-dense-attention branch July 28, 2026 12:11
@GiorgioOppo GiorgioOppo reopened this Jul 28, 2026
@GiorgioOppo

Copy link
Copy Markdown
Author

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

@GiorgioOppo

GiorgioOppo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@antirez ,
Could you please review this post in case you accept it or close it and the related post on hf (https://huggingface.co/antirez/deepseek-v4-gguf/discussions/22)?

@adamlawi

adamlawi commented Aug 6, 2026

Copy link
Copy Markdown

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 make cuda-spark. Not a Mac and not SSD streaming — the model is fully resident, so this exercises the decode path under different pressure than the M1 Pro run.

What I can run, using a harness already in place from measuring ds4f-q2 vs ds4f-q2-q4 on this box:

  • --decode-consistency and greedy A/B against the AProjQ8 baseline (correctness first)
  • ds4-bench sweeps at 2k/4k/6k/8k, 128 generated tokens per frontier — the AProjQ8 baseline here is 872 t/s prefill / 14.69 t/s decode at 8k
  • full-window prefill (194k tokens) with free-memory sampling every 2 s, reporting the minimum at peak
  • perplexity on a fixed sample, to put a number on the quality delta on this hardware

Why the memory result is the interesting one here. Resident ds4f-q2 leaves very little headroom on a 128 GB Spark: at --ctx 200000, filling 97% of the window leaves 2.5 GB free at peak; --ctx 220000 leaves 0.94 GB; --ctx 300000 cannot be filled past ~52% (0.7 GB at 155k tokens). The dense tensors account for 8.20 GiB of the startup span phase on this build, so cutting them to Q4_K should show up directly as usable context. If it frees ~2 GiB, that plausibly moves the ceiling from 200k to 250–300k — a benefit orthogonal to the SSD-streaming speedup you measured.

What blocks me: I could not find an AProjQ4 GGUF published (not in antirez/deepseek-v4-gguf, and none under your HF account), and gguf-tools/deepseek4-quantize only builds from --hf DIR with the original safetensors — ~568 GB, which does not fit here alongside the ~78 GB output (644 GB free). Your write-up mentions wiring --requant-gguf to consume a dense imatrix, but I do not see that tool on any of the three branches of your fork.

So: could you publish DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ4-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf, or share the requant path that derives it from an existing GGUF? Either way I can turn results around quickly.

Context for the numbers above: #705.

@GiorgioOppo

GiorgioOppo commented Aug 6, 2026

Copy link
Copy Markdown
Author

@adamlawi

adamlawi commented Aug 6, 2026

Copy link
Copy Markdown

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 aprojq4-dense-attention at 41aefd2c, make cuda-spark, no local changes — builds clean in 2m52s with zero warnings. Model is the GGUF you linked (5ca1abfe, 78.62 GiB). Baseline everywhere below is IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731, run with the same binary, so the comparison isolates the quant rather than the code version.

One caveat up front: the AProjQ4 file is from an older checkpoint (no -0731) and I did not download a matching AProjQ8 baseline, so memory and correctness numbers are clean, but speed and perplexity carry a checkpoint difference that I cannot separate out.

1. Correctness: the CUDA path looks good

--decode-consistency 64:

model max_abs rms
AProjQ8 (baseline) 1.476 0.293
AProjQ4 1.118 0.191

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

--inspect shows 215 tensors moved Q8_0 → Q4_K (345 → 130 Q8_0, plus 215 Q4_K), everything else byte-identical. Startup spans and the memory plan both drop by exactly the file-size delta:

  • startup model preparation: 8.20 GiB → 6.06 GiB, and no time penalty (2.12s → 1.70s)
  • planned at --ctx 200000: 85.21 GiB → 83.07 GiB
  • aligned artifacts: 474 / 78.71 GiB → 259 / 74.16 GiB (the Q4_K tensors get no repack builder)

But free memory after startup improves by ~11 GB, well beyond the 2.14 GiB the file shrank:

ctx AProjQ8 free AProjQ4 free
200000 9.8 GB 21.3 GB
250000 8.7 GB 20.1 GB
300000 8.1 GB 19.1 GB

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: --ctx 300000 now starts with more headroom than --ctx 200000 had before.

3. Prefill: ~16x slower, and this is the blocker

ds4-bench, same prompt file, 128 generated tokens per frontier:

ctx prefill Q8 prefill Q4 decode Q8 decode Q4
2048 817.32 53.98 17.55 17.28
4096 892.89 54.20 15.08 14.86
6144 883.59 54.18 14.97 14.78
8192 877.75 54.17 14.83 14.65

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 matmul_q4_K_dense_kernel predicts it ("correct-but-unoptimized for prefill chunks"; every token re-reads the weight rows). With a 4096-token prefill chunk this dominates.

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 only

Same 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.

@GiorgioOppo

Copy link
Copy Markdown
Author

@adamlawi,
Thanks for the feedback, it's hard to test on cuda without it :) Anyway, it should be optimized now and the prefill should be faster. Let me know if the prefill is accelerated or unchanged. In the next few days I'll update gguf with the new version of deepseek that wasn't available.

@adamlawi

adamlawi commented Aug 6, 2026

Copy link
Copy Markdown

Retested 6c4d769d on the same DGX Spark. Prefill is fixed — ~15.5x faster than before, now within ~4-5% of the Q8_0 baseline. Same binary, same prompt file, same model as my previous comment.

ctx prefill 41aefd2c prefill 6c4d769d AProjQ8 baseline gap to baseline
2048 53.98 783.60 817.32 −4.1%
4096 54.20 850.37 892.89 −4.8%
6144 54.18 841.37 883.59 −4.8%
8192 54.17 837.14 877.75 −4.6%

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:

  • --decode-consistency 64: rms 0.182 / max_abs 1.088, versus 0.191 / 1.118 on 41aefd2c and 0.293 / 1.476 on the Q8_0 baseline. Still better than the baseline path.
  • perplexity on the same 1882-token sample: 12.730174, versus 12.729817 before the change — identical to four decimal places.

No ds4_mmq_q4_K_dense returned ... falling back lines appeared in any run, so MMQ accepted every shape in this model.

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 --ctx 300000 starts with more headroom than --ctx 200000 had with AProjQ8.

Nice work — that was a fast turnaround.

One question about the other half of the commit, --requant-gguf: does it work from an existing GGUF as input? Our production model is the -0731 checkpoint, and the blocker for us so far has been that deepseek4-quantize only builds from --hf DIR (~568 GB of safetensors, which does not fit next to the output on this box). If requantizing an existing AProjQ8-...-0731.gguf into AProjQ4 is supported, I'd be glad to try it and report back — that would let us run this variant on the checkpoint we actually serve, and would also give you a clean same-checkpoint quality delta, which my perplexity numbers above cannot provide.

@adamlawi

adamlawi commented Aug 6, 2026

Copy link
Copy Markdown

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--inspect, --decode-consistency, the ds4-bench prefill/decode sweep, perplexity, and the startup-span / free-memory measurements at 200k / 250k / 300k context. Same harness as the two runs above, so the results will be directly comparable.

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 --requant-gguf whenever you get a chance to answer: if it accepts an existing GGUF as input, I'll requantize our production AProjQ8-…-0731.gguf and report back on that too.

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.

@GiorgioOppo

Copy link
Copy Markdown
Author

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.

@GiorgioOppo

GiorgioOppo commented Aug 7, 2026

Copy link
Copy Markdown
Author

try use DS4_CUDA_NO_Q4_ATTN_GROUPED_TOK8=1 DS4_CUDA_NO_Q4_QKV_PAIR=1 in the next run

@adamlawi

adamlawi commented Aug 7, 2026

Copy link
Copy Markdown

Ran 8b84e878 both ways on the DGX Spark. The two new paths cost ~38% of prefill — setting both env vars restores the 6c4d769d numbers exactly. Same binary, same prompt file, same model throughout.

ctx A: default (new paths on) B: both env vars =1 6c4d769d AProjQ8 baseline
2048 495.15 797.21 783.60 817.32
4096 521.72 851.00 850.37 892.89
6144 518.51 843.21 841.37 883.59
8192 516.13 838.35 837.14 877.75

So DS4_CUDA_NO_Q4_ATTN_GROUPED_TOK8=1 DS4_CUDA_NO_Q4_QKV_PAIR=1 is a clean revert to the previous prefill performance — the regression is fully attributable to those two paths, not to anything else in the commit.

They also hurt numerics, which I did not expect:

variant --decode-consistency 64 rms max_abs
A: new paths on 0.307 1.529
B: env vars =1 0.185 0.942
6c4d769d 0.182 1.118
AProjQ8 baseline 0.293 1.476

With the new paths active the divergence is worse than the Q8_0 baseline; with them off it is back to the 6c4d769d value. That looks like a correctness signal on the grouped/paired path, not just a scheduling tradeoff.

The good news — decode improved, and it is not from these two paths:

ctx decode A decode B 6c4d769d AProjQ8 baseline
2048 18.21 18.28 17.32 17.55
4096 15.59 15.64 14.90 15.08
6144 15.50 15.52 14.82 14.97
8192 15.35 15.37 14.67 14.83

Decode is ~+5% over 6c4d769d in both variants, i.e. the gain survives disabling the two paths and comes from the rest of the commit. That is the best decode I have measured on this machine — above the Q8_0 baseline, which is a notable result on its own.

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 6c4d769d. So unlike prefill and rms, ppl does not return to the previous value when the env vars are set — a ~0.47% regression that appears to come from elsewhere in the commit. Also note A and B differ by 0.0068 here, while two runs of 6c4d769d agreed to within 0.0004, so there may be some nondeterminism in this build.

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 --requant-gguf question from my previous comment still stands whenever you have a moment — if it accepts an existing GGUF as input, I'd like to requantize our production -0731 model and give you a clean same-checkpoint comparison.)

@GiorgioOppo

Copy link
Copy Markdown
Author

@adamlawi Feel free to try the requantization test

(And the --requant-gguf question from my previous comment still stands whenever you have a moment — if it accepts an existing GGUF as input, I'd like to requantize our production -0731 model and give you a clean same-checkpoint comparison.)

@GiorgioOppo

GiorgioOppo commented Aug 7, 2026

Copy link
Copy Markdown
Author

@adamlawi could you run ./gguf-tools/quality-testing/score_official to compare the two quantizations?
these are my results on macos
q4.tsv
q8.tsv

@adamlawi

adamlawi commented Aug 7, 2026

Copy link
Copy Markdown

--source-gguf works, and the result is better than I expected. I requantized our production -0731 model directly from GGUF — no safetensors needed — and then ran score_official on both. Summary: on a same-checkpoint comparison the AProjQ4 quality cost is +0.64% avg_nll, while decode gets faster than Q8_0 and the model frees ~13 GB of memory.

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.gguf

Converted 215 tensors q8_0 -> q4_K, output 84,420,584,032 bytes — 224 bytes off your published AProjQ4 file, so the layout matches exactly. --inspect confirms it: 130 × q8_0 (1.59 GiB), 215 × q4_K (2.41 GiB), everything else byte-identical to the source.

Two notes for anyone repeating this:

  • The tool streams 16 rows at a time, so memory use is negligible — it ran fine alongside a live 80 GB ds4-server on the same box, only costing disk I/O.
  • No imatrix was used. write_imatrix_kvs only records the imatrix filename and entry count in the KV block, not the vectors, so a requantized GGUF cannot recover them from its source. This is therefore a double quantization (Q8_0 → f32 → Q4_K) with unweighted Q4_K, i.e. a worse case than your imatrix-based path — which makes the quality numbers below a pleasant surprise.

2. score_official — as requested, plus your files for comparison

data/flash/manifest.tsv, ctx 4096, both env vars set to 1 (so the grouped-tok8 / QKV-pair paths from my previous comment are out of the picture):

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.

@GiorgioOppo

Copy link
Copy Markdown
Author

Can you share th GGUF?

@adamlawi

adamlawi commented Aug 7, 2026

Copy link
Copy Markdown

Final data point on the requantized -0731 model: a full 300k-token context window now fits comfortably on a 128 GB GB10, which was not possible with the Q8_0 attention projections.

Full-window load test, one session, prompt sized to fill the window, sampling free every 2s:

Q8_0 baseline @ --ctx 200000 AProjQ4 requant @ --ctx 300000
filled 194,681 tok (97%) 293,390 tok (97.8%)
prefill time 265 s 449 s
min free memory at peak 2.5 GB 14.6 GB

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, --ctx 220000 left 0.94 GB for long enough that a cron job or a log rotation firing mid-prefill could push it over, so we never shipped it.

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 -0731 checkpoint:

  • +13.3 GB free memory at 200k, and 300k is now not just startable but fillable
  • decode ~5% faster than Q8_0 (from the rest of 8b84e878)
  • prefill −4%, quality +0.64% avg_nll / +1.87% ppl
  • requantized locally from our own production GGUF via --source-gguf, no safetensors, no imatrix

For long-context serving on this hardware that is a clear win, and none of it would have been reachable without --source-gguf and the prefill fix. Thanks for turning both around so quickly.

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.

@adamlawi

adamlawi commented Aug 7, 2026

Copy link
Copy Markdown

Sorry for the delay — I posted the load-test results before spotting your question.

I'd rather give you the recipe than the file, because the GGUF is fully reproducible from a source you already have, and it saves you an 78 GiB download. It is a deterministic transform of DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf from antirez/deepseek-v4-gguf, using your own tool at 6c4d769d or later:

cd gguf-tools && make        # the checked-in binary predates --source-gguf
SRC=DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf
./deepseek4-quantize \
  --source-gguf "$SRC" \
  --template    "$SRC" \
  --attention-proj q4_k \
  --out DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ4-SExpQ8-OutQ8-chat-v2-0731-requant.gguf

Expected result, so you can confirm you got the same thing:

  • size: 84,420,584,032 bytes
  • sha256: 8b19cc48dcfa844ee5da6462416b7bb8087cd24f968faaa738ef399dbc77dc10
  • 215 tensors converted q8_0 -> q4_K; --inspect reports 130 × q8_0 (1.59 GiB), 215 × q4_K (2.41 GiB), 86 × iq2_xxs (44.34 GiB), 43 × q2_k (28.22 GiB), 359 × f16 (2.04 GiB)
  • ~50 min on the DGX Spark, and it streams 16 rows at a time — I ran it alongside a live 80 GB ds4-server without touching it

If the sha256 matches, you have a bit-identical copy of what produced all the numbers I posted. Note there is no imatrix involved (the KV block only stores the imatrix filename, not the vectors), so this is the unweighted double-quantization path — which is what makes the +0.64% avg_nll result interesting rather than disappointing.

If you specifically want our file rather than your own rebuild — e.g. to rule out a toolchain difference on aarch64 — say the word and I'll arrange an upload; I just didn't want to push 78 GiB at you for something one command reproduces.

@GiorgioOppo

Copy link
Copy Markdown
Author

I see your point; I just wanted to make a direct comparison. Could you upload the result in TSV format? I’d like to compare it with the requant using imatrix.

@adamlawi

adamlawi commented Aug 8, 2026

Copy link
Copy Markdown

Here you go: https://gist.github.com/adamlawi/bf83e3b7efb90fd28fa9db0281c8589e

Both files are per-case score_official output from the same checkpoint (0731), same binary (b4922c96), same fixtures (data/flash/manifest.tsv, 100 cases), --ctx 4096, both new paths disabled via DS4_CUDA_NO_Q4_ATTN_GROUPED_TOK8=1 DS4_CUDA_NO_Q4_QKV_PAIR=1:

  • score-q8-0731.tsv — the published AProjQ8 imatrix build, as baseline
  • score-q4-requant.tsv — our AProjQ4 requant of that exact file (sha256 8b19cc48…dc10), no imatrix

26 columns as emitted by score_official (nll, avg_nll, first_match, greedy_lcp, plus the api_* metrics). Aggregate delta Q8 → Q4 is +0.64% avg_nll.

One caveat for the comparison: only the delta inside a pair is meaningful here. The fixtures come from the 0731 API, so they match this checkpoint — absolute values won't line up against a run on a different one.

And a question, since you mention a requant using imatrix: where do you get imatrix vectors for the dense tensors? On my side ds4 --imatrix-out only emits routed-MoE entries (ffn_gate_exps / ffn_up_exps / ffn_down_exps, three per layer), so --imatrix has nothing to match for the attention projections and imatrix_find returns NULL — which is exactly why my requant is unweighted. If there's a way to produce imatrix data covering the dense tensors, I'd redo the requant weighted and post the same TSVs for it.

@GiorgioOppo

Copy link
Copy Markdown
Author

It seems to improve even further with the imatrix; I'm still finishing up the tests, but the results look positive. Unfortunately, it takes hours on my Mac. If you want to try the imatrix I created, it's available here: https://huggingface.co/antirez/deepseek-v4-gguf/discussions/22/files

@GiorgioOppo

Copy link
Copy Markdown
Author

Pushed follow-up commit f5662897.

This commit:

  • removes the CUDA grouped-tok8 attention-output and paired-QKV prefill paths, so the proven pack -> MMQ -> unpack / separate-QKV fallbacks are now the default without DS4_CUDA_NO_Q4_ATTN_GROUPED_TOK8 or DS4_CUDA_NO_Q4_QKV_PAIR;
  • preserves the Metal Q4 paired projection path;
  • fixes and hardens direct GGUF-to-GGUF requantization (--source-gguf is now the actual source, incomplete/colliding sources are rejected, and strict imatrix validation is enforced);
  • adds the complete 0731 Q4+imatrix quality-score TSV to the branch.

The DGX A/B in the previous comment disabled both CUDA paths together, so this does not claim that either path was independently isolated. It makes the known-good fallback permanent while preserving the unrelated decode gain.

Official-continuation quality score

Lower avg_nll is better. All rows cover the same 100 official cases / 2,313 target tokens.

model backend avg_nll first-token matches avg greedy LCP API top-1 API top-N recall pair agreement
Q8 0731 DGX Spark / CUDA 0.404811251 55/100 5.150 0.858625162 0.760544424 0.985996426
Q4 requant, no imatrix DGX Spark / CUDA 0.407383304 56/100 5.540 0.853437095 0.749191395 0.986109540
Q4 requant, 220k imatrix Apple M1 Pro / Metal + SSD streaming 0.398263336 59/100 6.490 0.865974924 0.757244882 0.986742981

The clean same-backend CUDA comparison is Q8 vs Q4 without imatrix: Q4 changes avg_nll by +0.635%. The Q4+imatrix run is -2.239% vs Q4 without imatrix and -1.618% vs Q8, but that comparison is cross-backend and must not be attributed entirely to the imatrix.

Q4+imatrix TSV SHA-256: 0bc8d6d74de97a5c694b024316596f0137350b0144468095bd9d0557456ed774

Imatrix: DeepSeek-V4-Flash-chat-v2-routed-and-dense-ds4-220k.dat, SHA-256 fa84a8c2a7894e71e56f2cb6d3d2ae8d8b0ba2dc92296d12446e34d94b81ec14.

Score command:

gguf-tools/quality-testing/score_official "$Q4" \
  gguf-tools/quality-testing/data/flash/manifest.tsv \
  /tmp/score-q4-imatrix-0731.tsv 4096 \
  --ssd-streaming --ssd-streaming-cache-experts 16

Machine/backend: Apple M1 Pro, 16 GB, Metal, SSD streaming with 16 cached experts. The first scorer process was SIGKILLed after 72 cases from cumulative memory pressure; cases 72-99 were resumed in a fresh process and merged. The final TSV was validated as exactly 100 unique case IDs with no duplicates or manifest mismatches.

Local checks run on this commit:

make clean
make
make ds4_test
./ds4_test --metal-kernels
make -C gguf-tools
git diff --check

metal-kernels: OK; ds4 tests: ok. Direct requant dry-run validated 1,328 tensors, 215 Q8_0 -> Q4_K changes, and all applicable imatrix entries; the completed GGUF matched the predicted 84,420,584,288-byte size and passed --inspect. Source/output collision, strict-without-imatrix, incomplete-source, and no-op-plan checks reject as expected.

CUDA compilation/regression and the post-change DGX benchmark still need to be rerun on the DGX Spark.

@GiorgioOppo

GiorgioOppo commented Aug 8, 2026

Copy link
Copy Markdown
Author

Hi @adamlawi, would you like to run a test with dspark added on your machine?

@adamlawi

adamlawi commented Aug 8, 2026

Copy link
Copy Markdown

Ran the full set on the DGX Spark. Gist updated with the third TSV: https://gist.github.com/adamlawi/bf83e3b7efb90fd28fa9db0281c8589e

1. Q4 + imatrix, same backend, same build

Your imatrix comparison was cross-backend, so here is the single-backend version — all three on CUDA, one build (823186e2), same fixtures, --ctx 4096:

model avg_nll first-token avg greedy LCP API top-1
Q8 0731 0.404811251 55/100 5.150 0.858625
Q4 requant, no imatrix 0.407383304 56/100 5.540 0.853437
Q4 requant + imatrix 0.396432403 60/100 6.090 0.860355

Q8 → Q4 no imatrix: +0.635%. Q8 → Q4+imatrix: −2.070%. Q4 no imatrix → Q4+imatrix: −2.688%, and that number is now clean of any backend difference.

So the weighted Q4 requant is better than the Q8 it came from, on a model 2.14 GiB smaller. Worth noting the Q8 baseline is itself an imatrix build, so this isn't "weighted vs unweighted" — I don't have an explanation for why it lands below Q8, only the measurement. Sample is small (100 cases, 2,313 target tokens).

Our CUDA figure (0.396432) is also slightly better than your Metal run (0.398263), so the cross-backend comparison was understating the imatrix, not overstating it.

2. f5662897 CUDA regression — byte-for-byte identical

You mentioned the CUDA regression still needed a run. I re-measured Q8 and Q4-no-imatrix on 823186e2 and compared against my earlier runs on b4922c96 with DS4_CUDA_NO_Q4_ATTN_GROUPED_TOK8=1 DS4_CUDA_NO_Q4_QKV_PAIR=1:

The output TSVs are byte-for-byte identical. Removing the grouped-tok8 and paired-QKV paths is exactly equivalent to disabling them via the env vars, down to the last logit. Build was clean, zero warnings, make cuda-spark.

3. Speed: the imatrix costs nothing

ds4-bench, same binary, Q4 vs Q4+imatrix:

ctx prefill no-imx prefill +imx decode no-imx decode +imx
2048 835.51 840.27 18.50 18.48
4096 906.55 905.78 15.78 15.83
8192 887.83 890.24 15.54 15.56

Differences are under 0.6% and go both ways. Memory identical: 259 aligned artifacts / 74.16 GiB. We've switched our production endpoint to the imatrix model on that basis.

4. DSpark — currently slower here

823186e2, Q4+imatrix, --temp 0, 128 tokens, 1,574-token prompt:

prefill decode
without DSpark 755.63 t/s 18.71 t/s
with DSpark 851.31 t/s 14.08 t/s

The support model loads cleanly (stages=3 block=5 markov_rank=256 tensors=81 missing=0 invalid=0 metadata_errors=0, capture on layers 40,41,42), but decode drops ~25%.

Treat this as a signal rather than a verdict: it's a single run, the prompt is synthetic and highly repetitive (which should favour speculation), and I don't see accept-rate statistics in the output — so I can't tell whether drafts are being rejected or the overhead is in the module itself. The prefill difference (756 vs 851) is also unexplained, since DSpark shouldn't touch prefill; that suggests run-to-run variance of over 10%, which argues for caution on the decode figure too. Happy to rerun with a realistic agent prompt, or with accept-rate logging if you can point me at a flag.

@OPS-NeoRetro OPS-NeoRetro left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Untested...

@GiorgioOppo

Copy link
Copy Markdown
Author

@adammawi can you retake the test now?

@OPS-NeoRetro

Copy link
Copy Markdown

@adamlawi, @iammac2, can you send me the logits so I can analyze them for each tested checkpoint or commit? I'll give the full picture of problems once I have analyzed them, because sometimes, certain metrics miss certain things.

@adamlawi

adamlawi commented Sep 2, 2026

Copy link
Copy Markdown

@OPS-NeoRetro Happy to help, but let me first say exactly what the tool in this branch can and cannot give you, so you can tell me whether it is what you need.

score_official gained --dump-first-logits PATH in the main merge. What it actually writes:

if (first_logits_path && case_n == 0 && i == 0 && !dump_logits(...))
  • One vector per run — the first target position of case_000 only. Not all 100 cases, not all positions within a case.
  • Full vocabulary, one line per token: token<TAB>logit, the logit as %a (hex float), so no decimal rounding is introduced.
  • Header carries case, target, greedy and vocab, so the file is self-describing.

Two practical consequences on our side:

  1. Every checkpoint needs its own score_official run, and on this host that means taking the inference server down for a maintenance window each time. Five commits tested so far means five windows. Doable, but I would rather not spend them on a shape of data that turns out not to be what you wanted.
  2. If your analysis needs more than the first position — the whole 100-case sweep, or several positions per case — that is a code change in dump_logits/its call site, not a flag. Small change, but it has to land in the branch first.

There is also something I can give you immediately at zero cost: the full per-case TSV that score_official already writes. 101 lines × 26 columns per checkpoint, with nll, avg_nll, first_match, greedy_lcp, api_target_mae, api_top1_count/match/rate, api_topn_recall, api_pair_agree and the rest, per case. That is a lot more surface than one logit vector, and it may already show whatever the headline metrics are hiding — which is the concern you raised.

So, to avoid burning windows on the wrong artifact:

  • would the per-case TSVs be enough to start with?
  • if you do need raw logits, is one vector per checkpoint useful, or do you need broader coverage — in which case, what exactly?
  • which checkpoints matter to you? Ours are c4494aea, 3152c835, 90290779, d60b6223, 9a448bb1, and I am measuring 35b87dda right now.

Our platform, for context: GB10 / sm_121 (DGX Spark), CUDA, …AProjQ4-0731-requant-IMATRIX.gguf, ctx 4096 for scoring.

@adamlawi

adamlawi commented Sep 2, 2026

Copy link
Copy Markdown

@GiorgioOppo Retested at 35b87dda on GB10 / sm_121 (DGX Spark, CUDA). This is the first commit where Q4 beats Q8 on prefill on our CUDA box.

Prefill

Same binary throughout, interleaved DEFAULT / NO=1 / Q8, two rounds, ctx 2048–8192.

ctx grouped DEFAULT NO_..._PREFILL=1 DEFAULT vs NO Q8 DEFAULT vs Q8
2048 906.02 871.56 +3.95% 830.54 +9.09%*
4096 975.43 932.72 +4.58% 956.45 +1.98%
6144 963.70 922.45 +4.47% 941.55 +2.35%
8192 959.91 919.40 +4.41% 937.46 +2.39%

These are round-2 numbers, not the two-round mean, and I want to be explicit about why. Round 1 carries a first-load artifact in two cells: NO=1 at 4096 measured 866.60 then 932.72 t/s (7.35% spread), and Q8 at 2048 measured 862.24 then 830.54 (3.75%). Averaging both rounds would report +9.03% at 4096, which is the artifact talking, not your kernel. Round-to-round spread of the DEFAULT arm was 0.83–1.66%, so the +4.4…+4.6% is comfortably above it. The 2048 column against Q8 is inflated by the Q8 cold start — treat the 4–8k rows as the real signal.

Two results in one measurement:

  1. Flipping the default is worth +4.4…+4.6% of prefill throughput on GB10.
  2. Q4 is now ahead of Q8 by +2.0…+2.4% at 4–8k. On 9a448bb1 this morning the same benchmark had Q4 behind by −1.5…−1.9%. That closes the gap and crosses it.

Decode is unchanged (−0.7…+0.2%), as expected for a prefill-side change. Q4 still leads Q8 on decode by +13.9…+18.0%.

Quality: no movement

DEFAULT vs NO=1     → byte-identical
DEFAULT vs 9a448bb1 → byte-identical

The grouped prefill path is numerically neutral here. The small numerical shift I reported earlier today against the 11.08 baseline is still present and now confirmed by a third independent run, so it came in with the main merge and is stable, not drifting.

I verified the path is actually active

After being wrong about this once today, I am not asserting activation without proof. Two arms:

  • DS4_CUDA_REQUIRE_Q4_GROUPED_ATTN_A_PREFILL=1 alone → rc=0, no diagnostics: eligible.
  • the same plus DS4_CUDA_NO_Q4_GROUPED_ATTN_A_PREFILL=1rc=1 and ds4: required CUDA Q4 grouped attention-A prefill path is not eligible.

The control arm fails hard, which proves the code reaches that decision for our shapes, so rc=0 in the first arm means the path really ran. Variable names were also checked with strings on the built binary in both directions — the three real ones present, a deliberately bogus name absent.

For the merge decision, the two CUDA levers point opposite ways

lever default prefill effect on GB10 quality
DS4_CUDA_Q4_MMQ_16WARP off (opt-in) −6.3% byte-identical
DS4_CUDA_ENABLE_Q4_GROUPED_ATTN_A_PREFILL on since 35b87dda +4.5% byte-identical

So the configuration that ships by default is the good one, and the 16-warp kernel is a cost on this chip if anyone enables it. Both are numerically exact, so this is purely a scheduling question, not a correctness one.

Window: 12 min 19 s of downtime, production restored automatically, zero DS4_* variables in the serving process, no Xid events. Happy to extend to 16k/32k/64k if the larger contexts matter for the merge.

@GiorgioOppo

Copy link
Copy Markdown
Author

@GiorgioOppo Retested at 35b87dda on GB10 / sm_121 (DGX Spark, CUDA). This is the first commit where Q4 beats Q8 on prefill on our CUDA box.

Prefill

Same binary throughout, interleaved DEFAULT / NO=1 / Q8, two rounds, ctx 2048–8192.

ctx grouped DEFAULT NO_..._PREFILL=1 DEFAULT vs NO Q8 DEFAULT vs Q8
2048 906.02 871.56 +3.95% 830.54 +9.09%*
4096 975.43 932.72 +4.58% 956.45 +1.98%
6144 963.70 922.45 +4.47% 941.55 +2.35%
8192 959.91 919.40 +4.41% 937.46 +2.39%
These are round-2 numbers, not the two-round mean, and I want to be explicit about why. Round 1 carries a first-load artifact in two cells: NO=1 at 4096 measured 866.60 then 932.72 t/s (7.35% spread), and Q8 at 2048 measured 862.24 then 830.54 (3.75%). Averaging both rounds would report +9.03% at 4096, which is the artifact talking, not your kernel. Round-to-round spread of the DEFAULT arm was 0.83–1.66%, so the +4.4…+4.6% is comfortably above it. The 2048 column against Q8 is inflated by the Q8 cold start — treat the 4–8k rows as the real signal.

Two results in one measurement:

  1. Flipping the default is worth +4.4…+4.6% of prefill throughput on GB10.
  2. Q4 is now ahead of Q8 by +2.0…+2.4% at 4–8k. On 9a448bb1 this morning the same benchmark had Q4 behind by −1.5…−1.9%. That closes the gap and crosses it.

Decode is unchanged (−0.7…+0.2%), as expected for a prefill-side change. Q4 still leads Q8 on decode by +13.9…+18.0%.

Quality: no movement

DEFAULT vs NO=1     → byte-identical
DEFAULT vs 9a448bb1 → byte-identical

The grouped prefill path is numerically neutral here. The small numerical shift I reported earlier today against the 11.08 baseline is still present and now confirmed by a third independent run, so it came in with the main merge and is stable, not drifting.

I verified the path is actually active

After being wrong about this once today, I am not asserting activation without proof. Two arms:

  • DS4_CUDA_REQUIRE_Q4_GROUPED_ATTN_A_PREFILL=1 alone → rc=0, no diagnostics: eligible.
  • the same plus DS4_CUDA_NO_Q4_GROUPED_ATTN_A_PREFILL=1rc=1 and ds4: required CUDA Q4 grouped attention-A prefill path is not eligible.

The control arm fails hard, which proves the code reaches that decision for our shapes, so rc=0 in the first arm means the path really ran. Variable names were also checked with strings on the built binary in both directions — the three real ones present, a deliberately bogus name absent.

For the merge decision, the two CUDA levers point opposite ways

lever default prefill effect on GB10 quality
DS4_CUDA_Q4_MMQ_16WARP off (opt-in) −6.3% byte-identical
DS4_CUDA_ENABLE_Q4_GROUPED_ATTN_A_PREFILL on since 35b87dda +4.5% byte-identical
So the configuration that ships by default is the good one, and the 16-warp kernel is a cost on this chip if anyone enables it. Both are numerically exact, so this is purely a scheduling question, not a correctness one.

Window: 12 min 19 s of downtime, production restored automatically, zero DS4_* variables in the serving process, no Xid events. Happy to extend to 16k/32k/64k if the larger contexts matter for the merge.

Now that we are winning, extend the test to see if we can maintain the lead, thanks

@GiorgioOppo

Copy link
Copy Markdown
Author

So at this point q4 is winning on both prefill and decode in both metal and cuda, we only have RoCm left on which we are winning only in decode but losing 2% on prefill

@GiorgioOppo

Copy link
Copy Markdown
Author

@kyuz0 Do you have time to take a test on RoCm?

@kyuz0

kyuz0 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

@kyuz0 Do you have time to take a test on RoCm?

It'll need to be a few days sorry, I have all my devices busy on long 24 hours benchmarks.

@GiorgioOppo

Copy link
Copy Markdown
Author

@kyuz0 Do you have time to take a test on RoCm?

It'll need to be a few days sorry, I have all my devices busy on long 24 hours benchmarks.

thanks anyway, @iammac2 could you try again?

@adamlawi

adamlawi commented Sep 2, 2026

Copy link
Copy Markdown

@GiorgioOppo Extended range as you asked, at 6a20b131 on GB10 / sm_121 (DGX Spark, CUDA). Short answer: the lead does not extend past 8k — above it, Q4 and Q8 are at parity, with Q4 a few tenths of a percent behind.

Before the numbers, one methodological finding that affects everyone benchmarking this PR, not just us.

The arm that runs first wins by ~0.4–0.5 pp

My earlier extended-range runs put the arms in a fixed order and always measured Q8 last. That was wrong. Throughput declines monotonically over a measurement window on this box — in two earlier windows, all 24 arm-to-arm comparisons drifted downward, so whichever arm runs later is penalised. Measured directly, with everything else held constant:

ctx Q4 measured first Q8 measured first positional bias
8192 +0.60% +0.07% 0.53 pp
16384 −0.13% −0.54% 0.41 pp
32768 +0.00% −0.44% 0.44 pp
65536 −0.10% −0.48% 0.38 pp

The bias is larger than three of the four effects we are trying to measure. At 32768 the sign of the result depends entirely on which model you load first. This is not thermal: SM clock sat at 2405 MHz for the whole two hours with no throttling flags, temperature 31–61 °C.

So the numbers below come from a design that balances it: round 1 runs Q4→Q8, round 2 runs Q8→Q4, and the pair ratio is averaged over both orders.

Result: 10 windows over 2 hours, 20 paired samples per context

Pure prefill (--gen-tokens 0), same binary, ctx 8192→65536, arms adjacent in time, 160 runs, zero failures.

ctx mean Q4 vs Q8 sd SE 95% CI
8192 +0.33% 0.47% 0.11% [+0.13%, +0.54%]
16384 −0.34% 0.29% 0.07% [−0.47%, −0.21%]
32768 −0.22% 0.30% 0.07% [−0.35%, −0.09%]
65536 −0.29% 0.29% 0.06% [−0.41%, −0.16%]

All four intervals exclude zero, so the tilts are real, but they are fractions of a percent. Q4 is ahead at 8k and marginally behind above it. Nothing here approaches the ~2% you quote for ROCm.

The series had converged well before it ended: adding the tenth window moved every mean by ≤0.02 pp. More repetitions would not sharpen this further.

Two corrections to my own earlier posts

The −1.0% I reported at 16–64k was mostly positional bias, not a real deficit. With the order balanced it is −0.34%. I would drop that earlier figure entirely.

My +2.39% at 8192 from the 35b87dda run is not comparable to this series. That run used a different prompt file (135 kB vs 405 kB here) and --gen-tokens 128 rather than pure prefill. I isolated the --gen-tokens part — it is worth about 0.5 pp — but the prompt difference remains confounded, and it was a cross-window comparison besides. Treat this balanced series as superseding it.

What is unchanged and solid

  • Grouped prefill is worth +2.0% across 8k–64k (+1.98…+2.20%, sd 0.18–0.35%, ~10σ). This was measured with adjacent arms, so the positional bias does not touch it.
  • Quality is byte-identical across 9a448bb1, 35b87dda and 6a20b131 — all 100 cases, same avg_nll, avg_lcp, top1_rate. Your new grouped Q4 quantizer is numerically exact here, not merely close.
  • The Q81 quantizer really engages on our shapes. REQUIRE=1 alone returns rc=0 silently; REQUIRE=1 plus NO=1 returns rc=1 with required CUDA Q4 grouped attention-A K4096/G8 Q8_1 quantizer is not eligible. The control failing proves the code reaches that decision, so the silence in the first arm means the path ran.

Suggestion for the other testers

If @iammac2 or @trueimage are comparing Q4 against Q8 at differences below ~1%, the fixed-order effect will dominate their numbers too. Alternating the order between rounds costs nothing and removes it. Happy to share the harness if useful.

Raw CSVs, per-run SM clock and temperature, and the full log are kept on our side — say the word if you want any of it attached.

@GiorgioOppo

Copy link
Copy Markdown
Author

I'm planning to close this PR and open a cleaner one without the various intermediate tests. What do you think?

@GiorgioOppo

Copy link
Copy Markdown
Author

@GiorgioOppo Extended range as you asked, at 6a20b131 on GB10 / sm_121 (DGX Spark, CUDA). Short answer: the lead does not extend past 8k — above it, Q4 and Q8 are at parity, with Q4 a few tenths of a percent behind.

Before the numbers, one methodological finding that affects everyone benchmarking this PR, not just us.

The arm that runs first wins by ~0.4–0.5 pp

My earlier extended-range runs put the arms in a fixed order and always measured Q8 last. That was wrong. Throughput declines monotonically over a measurement window on this box — in two earlier windows, all 24 arm-to-arm comparisons drifted downward, so whichever arm runs later is penalised. Measured directly, with everything else held constant:

ctx Q4 measured first Q8 measured first positional bias
8192 +0.60% +0.07% 0.53 pp
16384 −0.13% −0.54% 0.41 pp
32768 +0.00% −0.44% 0.44 pp
65536 −0.10% −0.48% 0.38 pp
The bias is larger than three of the four effects we are trying to measure. At 32768 the sign of the result depends entirely on which model you load first. This is not thermal: SM clock sat at 2405 MHz for the whole two hours with no throttling flags, temperature 31–61 °C.

So the numbers below come from a design that balances it: round 1 runs Q4→Q8, round 2 runs Q8→Q4, and the pair ratio is averaged over both orders.

Result: 10 windows over 2 hours, 20 paired samples per context

Pure prefill (--gen-tokens 0), same binary, ctx 8192→65536, arms adjacent in time, 160 runs, zero failures.

ctx mean Q4 vs Q8 sd SE 95% CI
8192 +0.33% 0.47% 0.11% [+0.13%, +0.54%]
16384 −0.34% 0.29% 0.07% [−0.47%, −0.21%]
32768 −0.22% 0.30% 0.07% [−0.35%, −0.09%]
65536 −0.29% 0.29% 0.06% [−0.41%, −0.16%]
All four intervals exclude zero, so the tilts are real, but they are fractions of a percent. Q4 is ahead at 8k and marginally behind above it. Nothing here approaches the ~2% you quote for ROCm.

The series had converged well before it ended: adding the tenth window moved every mean by ≤0.02 pp. More repetitions would not sharpen this further.

Two corrections to my own earlier posts

The −1.0% I reported at 16–64k was mostly positional bias, not a real deficit. With the order balanced it is −0.34%. I would drop that earlier figure entirely.

My +2.39% at 8192 from the 35b87dda run is not comparable to this series. That run used a different prompt file (135 kB vs 405 kB here) and --gen-tokens 128 rather than pure prefill. I isolated the --gen-tokens part — it is worth about 0.5 pp — but the prompt difference remains confounded, and it was a cross-window comparison besides. Treat this balanced series as superseding it.

What is unchanged and solid

  • Grouped prefill is worth +2.0% across 8k–64k (+1.98…+2.20%, sd 0.18–0.35%, ~10σ). This was measured with adjacent arms, so the positional bias does not touch it.
  • Quality is byte-identical across 9a448bb1, 35b87dda and 6a20b131 — all 100 cases, same avg_nll, avg_lcp, top1_rate. Your new grouped Q4 quantizer is numerically exact here, not merely close.
  • The Q81 quantizer really engages on our shapes. REQUIRE=1 alone returns rc=0 silently; REQUIRE=1 plus NO=1 returns rc=1 with required CUDA Q4 grouped attention-A K4096/G8 Q8_1 quantizer is not eligible. The control failing proves the code reaches that decision, so the silence in the first arm means the path ran.

Suggestion for the other testers

If @iammac2 or @trueimage are comparing Q4 against Q8 at differences below ~1%, the fixed-order effect will dominate their numbers too. Alternating the order between rounds costs nothing and removes it. Happy to share the harness if useful.

Raw CSVs, per-run SM clock and temperature, and the full log are kept on our side — say the word if you want any of it attached.

Do you think it's noise or a solid result?

@GiorgioOppo

Copy link
Copy Markdown
Author

@evandhoffman Could you do another test on the final commit 6a20b13?

@evandhoffman

Copy link
Copy Markdown

M5 Max 128 GB re-test at 6a20b13: the AProjQ4 decode advantage holds — 1.155, 32/32 frontiers

@GiorgioOppo asked for a re-run on the final commit. Done, twice, on the same MacBook Pro Mac17,6 / Apple M5 Max / 128 GB as my 2026-08-30 report.

6a20b13 is 226 commits past the 2669a8e I measured then, so this is a fresh measurement rather than a re-quote.

Result

6a20b13 cold start 6a20b13 hot start 2669a8e (2026-08-30)
decode q4/q8, 3-rep paired median 1.155 1.147 1.155
frontiers where q4 > q8 32 / 32, every rep 32 / 32 32 / 32
prefill q4/q8 1.003 (0.980–1.027) 1.021 0.998

The ratio reproduces to three decimals across five weeks and 226 commits. Prefill is indistinguishable for the third independent time — AProjQ4's gain remains decode, and only decode.

ctx q4 gen q8 gen ratio
2,048 48.80 41.62 1.173
18,432 1.154
34,816 1.145
51,200 1.135
65,536 1.129

Same shape as before: widest at short context, narrowing with depth, never inverting.

Something worth knowing before quoting any single number

The advantage shrinks within a session, and it is not heat.

Per-rep paired median, both runs:

rep 1 rep 2 rep 3
run 1 1.187 1.147 1.139
run 2 1.194 1.156 1.132

Absolute sweep-median decode falls q4 −10.0%, q8 −4.1% across the three reps (run 1: −9.5% / −5.1%). q4 loses more, so the ratio narrows.

I assumed thermal throttling and measured it. Die temperature, sampled every 60 s, after a 300 s idle to a stable baseline:

14:21:38  34.85 C   baseline, idle
14:25:39  78.11 C   peak, 4 minutes in
14:38:40  69.55 C
14:46:41  70.43 C   still under full load, sweep 6 of 6

The machine saturates in about two minutes and then runs cooler for the rest of the session while throughput keeps falling. After minute four, temperature and decode move in opposite directions. So this is not die temperature — sustained-power limits on a longer time constant, or accumulated memory-system state across repeated 80 GiB model loads, are both still candidates and this run does not separate them.

Practical consequence: a cold single-rep benchmark of this pair reports about 1.19; three reps report 1.155. If a contributor posts a number near 1.19, that is consistent with these results and not a disagreement — it is one cold rep.

Method

  • Worktree detached at 6a20b13, make clean && make -j18, Metal.
  • Weights from refs/pr/22 of antirez/deepseek-v4-gguf, unmodified. AProjQ4 is 84,420,584,288 bytes, matching the PR. Resident 78.62 GiB.
  • ds4-bench --prompt-file speed-bench/promessi_sposi.txt --ctx-start 2048 --ctx-max 65536 --step-incr 2048 --gen-tokens 128, fully resident, no SSD streaming.
  • Arms interleaved at sweep level (q4, q8, q4, q8, q4, q8), 3 reps. Not a block design — with 10% within-session drift, blocks would confound the ratio with the drift.
  • sysctl iogpu.wired_limit_mb=114688 → 112.00 GiB Metal working set.
  • Run 2 additionally: 300 s idle to a stable baseline first, temperature logged throughout.

New in the runtime since 2669a8e, recorded because it changes what is being measured, not because anything is attributed to it:

ds4: Metal 4 tensor API enabled for Tensor kernels
ds4: drift-patch flags hc_stable=on norm_unify=on kv_raw_f32=off
     rope_exp2_log2=off math_safe=off tensor_matmul=on

What this does not cover

  • Decode only. No agent loop, no end-to-end wall time.
  • No quality scoring this pass. I did not re-run the --temp 0 coherence check; the August run's no-drift result is not re-certified here.
  • This GGUF pair onlyIQ2XXS-w2Q2K-AProj{Q4,Q8}-SExpQ8-OutQ8, OutQ8 on both arms, so nothing is said about quantizing the output head.
  • I could not answer the open checklist item. From rep 1 alone it looked like AProjQ8 had regressed against my August numbers; three reps showed both arms had simply drifted together, so this run gives no evidence either way on Q8-on-PR versus Q8-on-main. That still wants a dedicated interleaved run, which I am happy to do if it is useful — say the word.

Full write-up, per-rep tables, the complete temperature log and raw CSVs: evanwtf/local-llm#91evanwtf/local-llm#91

@OPS-NeoRetro

Copy link
Copy Markdown

@OPS-NeoRetro Happy to help, but let me first say exactly what the tool in this branch can and cannot give you, so you can tell me whether it is what you need.

score_official gained --dump-first-logits PATH in the main merge. What it actually writes:

if (first_logits_path && case_n == 0 && i == 0 && !dump_logits(...))
  • One vector per run — the first target position of case_000 only. Not all 100 cases, not all positions within a case.
  • Full vocabulary, one line per token: token<TAB>logit, the logit as %a (hex float), so no decimal rounding is introduced.
  • Header carries case, target, greedy and vocab, so the file is self-describing.

Two practical consequences on our side:

  1. Every checkpoint needs its own score_official run, and on this host that means taking the inference server down for a maintenance window each time. Five commits tested so far means five windows. Doable, but I would rather not spend them on a shape of data that turns out not to be what you wanted.
  2. If your analysis needs more than the first position — the whole 100-case sweep, or several positions per case — that is a code change in dump_logits/its call site, not a flag. Small change, but it has to land in the branch first.

There is also something I can give you immediately at zero cost: the full per-case TSV that score_official already writes. 101 lines × 26 columns per checkpoint, with nll, avg_nll, first_match, greedy_lcp, api_target_mae, api_top1_count/match/rate, api_topn_recall, api_pair_agree and the rest, per case. That is a lot more surface than one logit vector, and it may already show whatever the headline metrics are hiding — which is the concern you raised.

So, to avoid burning windows on the wrong artifact:

  • would the per-case TSVs be enough to start with?
  • if you do need raw logits, is one vector per checkpoint useful, or do you need broader coverage — in which case, what exactly?
  • which checkpoints matter to you? Ours are c4494aea, 3152c835, 90290779, d60b6223, 9a448bb1, and I am measuring 35b87dda right now.

Our platform, for context: GB10 / sm_121 (DGX Spark), CUDA, …AProjQ4-0731-requant-IMATRIX.gguf, ctx 4096 for scoring.

I'm late, but I need the full logits before the sampler. Like, every token generated. I need to find if the difference between AProjQ4 and AProjQ8 are just quantization noise and/or FP accumulation order in kernels. I need to find MSE, RMS, SAD, Hamadard SATD, square sum, sum, dot product, negatively-accumulated dot product, average, mean absolute differences, median absolute differences, mean differences, sum of differences, median differences, minimum, maximum, negative sum, exponential sum and square root sum. I'm kind of scientific here, but those are among the most useful metrics to know what a very long vector actually is.

@GiorgioOppo

GiorgioOppo commented Sep 2, 2026

Copy link
Copy Markdown
Author

@OPS-NeoRetro more than anything, perhaps now we should make a benchmark like @kyuz0's mini bench

If I'm not mistaken, @trueimage did it with only 1 incomplete pass and showed excellent results

@GiorgioOppo

Copy link
Copy Markdown
Author

@evandhoffman would you have 24 hours to let the benchmark run?

@evandhoffman

Copy link
Copy Markdown

@evandhoffman would you have 24 hours to let the benchmark run?

I probably can kick it off right now and let it go 24h. Is there an existing script for 24h benchmark run?

@GiorgioOppo

Copy link
Copy Markdown
Author

@evandhoffman would you have 24 hours to let the benchmark run?

I probably can kick it off right now and let it go 24h. Is there an existing script for 24h benchmark run?

@kyuz0 Can you help him, you who have more experience with the benchmark?

@GiorgioOppo

Copy link
Copy Markdown
Author

I would continue to use this pr for general info to keep the real pr clean

@GiorgioOppo

Copy link
Copy Markdown
Author

@evandhoffman would you have 24 hours to let the benchmark run?

I probably can kick it off right now and let it go 24h. Is there an existing script for 24h benchmark run?

try to take a look here https://www.tbench.ai/run, but not having the right hardware I've never even looked at how it works

@evandhoffman

Copy link
Copy Markdown

Cross-referencing, since this PR is now the benchmark archive: the M5 Max re-test at 6a20b13 I posted above is written up in full — per-rep tables, the 60-second die-temperature log, both runs, and the raw CSVs — at

evanwtf/local-llm#91

Note that 6a20b13 is also the head of #952, so those numbers certify the production PR as well; nothing needs re-running for it.

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.

10 participants