Pull requests / #3084
#3084 qwen35: FP16 K/V cache by default, int8 with --kv-q8 (262144 fits a 16 GiB card)
open · @benthic-io · 0 commentaires · Sur GitHub
BenchmarksAMD / HIPModels & quantsDocumentation
Description
## Title qwen35: FP16 K/V cache by default, int8 with `--kv-q8` (262144 fits a 16 GiB card) Issue: Resolves # ## Summary The Qwen3.6 / `qwen35moe` engine stored its K/V cache in FP32 with no option to change it. At the model's native 262144 cells that is **12.0 GiB** - the whole of a 16 GiB card before a single expert - so the context had to stop at 192K. FP32 is also the wrong precision for this path, for a reason the code already made obvious: **the prompt has always attended over FP16** while the decode of the next token read FP32. FP16 becomes the default (6.0 GiB at 262144), matching what the main engine's `kKvF16` already does, and `--kv-q8` stores int8 codes with one FP16 scale per 64 dims - the main engine's own `kv_q8.hpp` scheme (`KV_Q8_GROUP`) - at 3.1 GiB. This could not be reached with a configuration flag: **this engine path has no KV or precision option at all.** Its parser has none of `--kv`, `--kv-resident` or anything else, and the allocation was a hardcoded `dalloc(...)` (float) with the kernels reading `const float*` directly. There was nothing to set; the flag and the storage had to be added together. ## What changed `src/qwen35/q36_engine.cu` - `k_attn_prep`, `k_attn_part` and `k_kv_f16` are templated on the cache format, so the FP16 path carries no branch and the prompt path's existing half-staging (`k_kv_f16`) becomes a pure compaction instead of an FP32→FP16 conversion. - `--kv-q8` quantizes on write with `scale = fp16(max|x| / 127)`, `code = clamp(rint(x / scale), -127, 127)` - the main engine's scheme, so a cell is stored the same way in both engines. - `k_attn_prep` loses its `if (i >= HD) return;` early return. The q8 group max needs a `__syncthreads()` that every thread has to reach, so the stores are guarded with `i < HD` instead. Behaviour on the FP16 path is unchanged. - `RowSel`, `Sess` and the batch rows' per-session pointer tables carry the scale arrays alongside the code arrays. The session-to-slot copy was sized in FP32 elements (`* 4`) and is now sized in the format's, with a second copy for the scales under q8. `docs/QWEN36.md` - a section on the two formats with the numbers below. ## Extra Notes **Why FP32 was never the right choice here.** `k_kv_f16` has always compacted `[kvh][ctx][HD]` into the `[kvh][P][HD]` halves that the prompt chunk's Q K^T and P V GEMMs read. So a prompt attended over FP16 values while the split-K decode of the very next token read FP32. The cache format only ever gave the decode a higher precision than the prefill sharing its sequence; FP16 makes the two agree. **Measured**, Radeon VII (gfx906, 16 GiB), ROCm 7.1, `--max-context 262144 --chunk 4096 --expert-vram 8`, two runs, back to back: | cache | VRAM free | experts in VRAM | tg32 | | --- | ---: | ---: | ---: | | FP32 (before) | does not allocate | - | - | | FP16 | 4.46 GiB | 49 / 256 | 41.4 / 45.0 tok/s | | `--kv-q8` | 7.12 GiB | 91 / 256 | 49.1 / 52.6 tok/s | q8 is both the smaller cache and the faster one, which is not the obvious outcome: the VRAM it returns goes to the expert cache, and the q8 read also halves the bytes the split-K decode pulls across PCIe. The gap is about +18%, with a few points of run-to-run spread. **Parity** against an FP32 build of the same tree: 4 prompts of 4,000 tokens, greedy 96, `--mtp 0`, with both binaries checked to place the same 81/256 experts so the comparison is the cache format and nothing else. FP16 **383 / 384** generated tokens identical, `--kv-q8` **384 / 384**. The one FP16 difference is an argmax near-tie at token 43 of the first prompt; no earlier token moves. **A characteristic `--bench-pp` hides**, recorded in docs/QWEN36.md: it measures a 2,048-token prompt, nowhere near the context's end, and throughput falls as a prompt fills the context. One 198,603-token prompt through the server read 4,096 tokens at 1365 tok/s, 106,496 at 195, 172,032 at 72 - a steady decline that finishes, about 17 minutes end to end. Worth knowing before filling a 262144 context in one request; irrelevant to a conversation built up over turns. **Not run here**: `tools/qwen35/q36_solo_gate.py` over the 10 parity prompts, `q36_batch_gate.py`, and the vision gates. Those are the obvious next step on the project's own hardware.
Sur le site
Liens install, modèles, releases.