Skip to content

CUDA: use selected expert cache for IQ2 SSD prefill - #1031

Open
Matthley wants to merge 4 commits into
antirez:mainfrom
Matthley:fix/cuda-ssd-expert-cache
Open

Matthley wants to merge 4 commits into
antirez:mainfrom
Matthley:fix/cuda-ssd-expert-cache

Conversation

@Matthley

@Matthley Matthley commented Sep 12, 2026

Copy link
Copy Markdown

Fixes CUDA SSD streaming for routed MoE models.

  1. IQ2/Q2 batch prefill now uses the compact selected-expert table rather than resolving full expert tables.
  2. --ssd-streaming-cache-experts is a bounded CUDA LRU, released on model/lifecycle changes.
  3. Resident experts are stored in contiguous gate/up/down slabs. When the selected set is resident, kernels receive the slab bases plus remapped stable slot IDs, avoiding per-layer GPU-to-GPU copies into compact staging.
  4. CUDA now supports --expert-profile / DS4_EXPERT_HOTLIST to collect routed-expert locality data.

AProjQ4 matched prefill (GB10, e9528de, ctx 1024, forced batch prefill):

  • Before: 1.09 t/s; CUDA file cache 78.62 GiB.
  • Selected-expert path: 2.59 t/s; CUDA file cache 6.06 GiB.
  • Result: 2.38x prefill (+138%) and 92.3% less file cache.

AProjQ8 cache-size tuning smoke (GB10, ctx 1024, deterministic prompt):

Expert slots Expert cache Flash generation Vision generation
256 1.69 GiB 1.62 t/s 1.61 t/s
512 3.38 GiB 2.05 t/s 3.04 t/s
1024 6.76 GiB 4.02 t/s 4.17 t/s
2048 13.52 GiB 6.24 t/s 6.00 t/s

The two locally available Flash variants show the same capacity curve: 256 is below the routed working set, 1024 is the measured throughput/memory compromise, and 2048 favors generation throughput where the extra memory is available. Flash hit/miss at 256/512/1024/2048: 755/18,056; 5,027/13,784; 11,066/7,745; 13,809/5,002. Vision: 721/18,048; 8,707/10,062; 11,546/7,223; 13,922/4,847. These are single-run GB10/model-specific tuning observations, not a general performance claim.

A follow-up can expose a non-binding runtime hint based on 4 × (local routed layers × top-k) when the memory planner permits it; this PR intentionally does not alter the user-selected cache budget.

Validation:

  • make -j4 cuda-spark: PASS
  • make test-cuda-q8-scratch: PASS
  • Deterministic SSD-streaming smoke: PASS, identical output.

@Hazelhof

Copy link
Copy Markdown

Independent reproduction on a 3× RTX 5080 (16 GB) host — the target hardware class this PR matters for.

Base: 8db1d1d (2026-09-16). Artifact: the ds4-native
DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf (80.76 GiB).
Command (identical before/after):

DS4_CUDA_NO_DIRECT_IO=1 DS4_CUDA_KEEP_MODEL_PAGES=1 \
  ds4-server -m <artifact> --cuda --ctx 16384 --ssd-streaming --prefill-chunk 512

Before (stock 8db1d1d): the model loads and serves short turns, and every ≥10 k prompt dies:

ds4: CUDA SSD cache cannot stage 107 experts with system headroom
ds4: gpu layer 0 ffn batch encode failed
ds4: gpu layer-major prefill layer 0 encode failed

The boundary is tight and reproducible: 93 experts (0.61 GiB) stages; 107 (0.72 GiB) does not. Reading
ds4_cuda.cu, staging is bounded by available = cudaMemGetInfo().free − 8 GiB against
unique_experts × 6.75 MiB; on a 16 GB card that leaves ~8.0 GiB free at staging time, so available ≈ 0.
--ssd-streaming-cache-experts 8GB prints a plan that would fit
(8.00 GiB = 3.38 GiB prefill headroom + 4.62 GiB dynamic cache (701 experts, 6.75 MiB each)) and still
fails
— the cache hint is not the binding term. --prefill-chunk 512 alone gets 93 experts staged.

After (this PR, built for sm_120): the same 12,422-token prompt completes at 113.45 t/s, and a
66-token chat turn goes from 1.34 → 3.15 t/s. G1/G2/G3 all pass. Nothing else about the run changed except
the binary.

Two things that may be worth a line in the PR text rather than the code:

  1. --ssd-streaming + --mtp-model/--dspark is refused upstream (issue Support --ssd-streaming alongside --mtp / --dspark #809), and on small-VRAM cards
    streaming is the only way this model loads at all — the resident layout is refused
    (CUDA model arena alloc failed for tensor-span:26, 80.76 GiB vs 48 GB). So speculation is unreachable
    exactly where a drafter would help most. (Issue DSpark proposals systematically wrong under --ssd-streaming (DeepSeek-V4-Flash-0731, Metal): enabling patch observed, miss_first=100% #724's enabling patch measured the combined path as
    miss_first=100 %, so the guard looks correct today; this is only about how much this PR carries for
    consumer cards.)
  2. The four commits do not cherry-pick onto current main: applying them to 8db1d1d conflicts in
    ds4_cuda.cu (the PR was written against base 371cb40100, and main moved five days of CUDA work under
    it). We therefore build the branch head itself — worth knowing for anyone trying to rebase before merge.

Also on this build, for what it is worth: the explicit expert cache works at every size we tried
(256 / 512 / 1024 / 2048 slots — all four complete a 12,422-token prefill). Our curve is not monotonic
(512 measured faster than 1024 on single runs), unlike the GB10 table in the PR text, so it may be
workload- or staging-dependent rather than a clean capacity curve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants