Skip to content

[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

Closed

Description

@Omnicef

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_LOADING unset (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 window lines after the first prompt, so the verify graphs are not being recaptured.
  • An earlier 0.1.44.1 process without --resident-budget-gib showed 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 cudaMallocAsync or 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

  1. Is a slow per-prompt growth on later stages known? What does a full later card do here: stall, OOM, or a fallback?
  2. Is there a way to trim it without a restart?
  3. Is --vram-reserve-later-mib (placement only, generate.cpp:3808) the intended mitigation?

Activity

  1. Norhiro commented on Oct 22, 2026

    @Norhiro
    Contributor

    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 said CUDA1: 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 with verify: instantiate: out of memory and the engine exited (exit code 1; once prefill 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 1400 fixed it here: 770 MiB free on CUDA1 after loading and no failed request in 10 strata_bench.py --long runs (cold and warm, 5 split configurations). It cost 236 expert-cache slots on CUDA1 (K moved 18 → 19). Details and logs: #3702.

  2. Niko1221 commented on Oct 27, 2026

    @Niko1221
    Owner

    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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions