Repository navigation
[Engine] CUDA layer split, 0.1.44.1: later-stage (CUDA1) free VRAM drops 2-6 MiB per long cold prompt and does not plateau #3470
Copy link
Copy link
Closed
Description
Setup
- Strata v0.1.44.1 (tag, built locally: CUDA 13.0 nvcc,
CMAKE_CUDA_ARCHITECTURES=120, Release), Linux, driver 595.91.07 - 2x RTX 5060 Ti 16 GB (sm_120), 48 GB RAM
- Model: Qwen3.8-Flash-Next GSQ-RCO IQ3_S pack,
--layer-split auto(K=23: CUDA0 layers 0-22, CUDA1 23-47 + head + MTP) - Args:
--expert-cache auto --prefill auto --spec 4 --mtp <rt> --max-context 262144 --kv int8 --resident-experts --resident-budget-gib 31.58 --vision --vram-reserve-mib 700 --remote-expert-opt --pcie-frac 0.35 --spec-min-p 0.70
(plus a control vector, not applied on these requests).CUDA_MODULE_LOADINGunset (EAGER forced by generate). - Startup:
CUDA1: 203 MiB of VRAM free with everything loaded - LOW ... add --vram-reserve-later-mib 1265
Observation
Free VRAM on CUDA1 (nvidia-smi -i 1 --query-gpu=memory.free) after each request, fresh process, single client:
| step | request | CUDA1 free (MiB) |
|---|---|---|
| load | - | 204 |
| 1 | 114,972-token prompt, cold | 142 (first-use verify graphs: 12 captures logged, none after) |
| 2 | same, cached | 142 |
| 3-10 | 8 distinct cold prompts, 99-120K tokens | 136, 132, 128, 126, 122, 118, 116, 112 |
| 11-16 | 114,972-token prompt: 1 cold + 5 cached | 110 |
| 17-19 | the same 113,445-token prompt, cold 3x (a 13-token request in between drops the prompt cache) | 106, 104, 98 |
- Steps 17-19 repeat an identical prompt (same length, same chunking, same window sizes) and still lose 4, 2 and 6 MiB.
- Flat while idle (an earlier process held 92 MiB overnight).
- No
capturing the N-token windowlines after the first prompt, so the verify graphs are not being recaptured. - An earlier 0.1.44.1 process without
--resident-budget-gibshowed the same trend: 189 MiB at load, 126 after the first long
prompt, 92 after 118 mixed requests. - CUDA0: 330 MiB at load, 272 after the first prompt, still 272 after step 19.
Source read (0.1.44.1)
No allocator in src/ grows with prompt length or position:
- verify graphs are keyed by window size (
verify.cpp:1881-1887), one extra set for the doorbell path; - prompt-path buffers live in borrowed cache slots (
prefill.cpp:1082-1219); - the MTP
pf_dev_buffer is a high-water mark of at most one chunk (mtp.cpp:1403); - KV is preallocated for
--max-context(generate.cpp:4446); - cuBLAS uses fixed 32 MiB workspaces (
gemm.cu:441); - there is no
cudaMallocAsyncor memory pool.
That leaves state inside the driver or cuBLAS, outside the source.
Mitigation tried: --vram-reserve-later-mib 1265
Same build and args, plus --vram-reserve-later-mib 1265 (the value the startup warning suggests), with --resident-budget-gib
raised from 31.58 to 32.14 so that the experts dropped from the GPU caches still get a RAM copy.
| without | with reserve-later 1265 | |
|---|---|---|
| layer split auto | K=23 (CUDA1 9.79 GiB free before its session carve) | K=24 (CUDA1 9.23 GiB) |
| CUDA0 expert cache | 3316 slots (8.91 GiB free) | 3200 slots (8.62 GiB free) |
| CUDA1 expert cache | 3517 slots (7.05 GiB) | 3374 slots (6.78 GiB) |
| experts on the GPUs (token graph hit path) | 8097 | 7787 (-310, -3.8 %) |
| RAM copy | 16479 experts, 31.58 GiB | 16789 experts, 32.14 GiB |
| LOW VRAM warning at startup | yes (CUDA1 203 MiB) | no |
| free VRAM at load, CUDA0 / CUDA1 | 329 / 203 MiB | 328 / 768 MiB |
| after the first 114,972-token prompt | 272 / 142 MiB | 270 / 706 MiB |
| prefill, 114,972 tokens cold | 93.1 s, 83.7 s | 92.0 s, 91.2 s, 80.8 s |
| decode after a cold prompt | 47.6, 41.7 tok/s (and 52.0, 44.8 uncapped) | 36.3, 41.8, 43.6 tok/s (median 41.8) |
| decode, same prompt cached | 45.0-47.3 tok/s | 49.1 tok/s |
- The first-use drop is the same (about 62 MiB), so the reserve adds about 565 MiB of runway on CUDA1. At the observed
2-6 MiB per long cold prompt, that is on the order of 100+ long prompts more than before. - Prefill and cached decode are unchanged.
- Decode after a cold prompt has a median of 41.8 tok/s, at the low end of the earlier cold runs (41.7-52.0). I don't
read it as a measurable cost of the 310 fewer GPU experts. - CUDA0 loses slots too, although the reserve only applies to later stages, because it changes the split search
(generate.cpp:3800-3812,stage_room). The search sees 0.56 GiB less room on CUDA1 and moves one layer to CUDA0
(K=23 -> 24). CUDA0's own expert cache, sized after the split, then has 0.29 GiB less free: -116 slots. CUDA1 loses
only 143 slots (not the ~275 the reserve alone would cost) because it no longer holds layer 23.
So for us, --vram-reserve-later-mib is a workable mitigation. It delays the problem but does not stop the drop, and we
also restart the server on a schedule when free VRAM falls below 250 MiB.
Questions
- Is a slow per-prompt growth on later stages known? What does a full later card do here: stall, OOM, or a fallback?
- Is there a way to trim it without a restart?
- Is
--vram-reserve-later-mib(placement only,generate.cpp:3808) the intended mitigation?
Metadata
Metadata
Assignees
Labels
No labels
Activity
Norhiro commentedon Oct 22, 2026
Another data point for question 1 ("what happens when a later card fills"), on 0.2.2 (engine 0.1.47, CUDA, Linux), RTX 3060 12 GB + RTX 5060 Ti 16 GB, UD-Q4_K_XL,
layer_split: auto, default reserves: the start-up log saidCUDA1: 68 MiB of VRAM free with everything loaded - LOW ... add --vram-reserve-later-mib 1400. The verify windows captured on first use took CUDA1 from 68 to 10 MiB, and the next capture failed withverify: instantiate: out of memoryand the engine exited (exit code 1; onceprefill gemm: cublasGemmEx f16: cuBLAS status 14). It did this on every start (6 starts in 11 minutes), so on this pair a full later card ends the engine rather than stalling it.--vram-reserve-later-mib 1400fixed it here: 770 MiB free on CUDA1 after loading and no failed request in 10strata_bench.py --longruns (cold and warm, 5 split configurations). It cost 236 expert-cache slots on CUDA1 (K moved 18 → 19). Details and logs: #3702.conversation copies: the private copy streams and events are handed b…
Niko1221 commentedon Oct 27, 2026
Thank you. Fixed in 0.2.7: The private copy streams and events are handed back to a pool when a thread ends (#3350, #3470). We could check it with unit tests and builds, but not on your hardware or with your workload. Please try 0.2.7 and tell us how it goes.
Release: https://github.com/Niko1221/Strata/releases/tag/v0.2.7
conversation copies: the private copy streams and events are handed b…