Skip to content

fix(server): reserve per-device VRAM for LOADING models (#66)#148

Draft
marksverdhei wants to merge 1 commit into
htfrom
fix/66-loading-reserve
Draft

fix(server): reserve per-device VRAM for LOADING models (#66)#148
marksverdhei wants to merge 1 commit into
htfrom
fix/66-loading-reserve

Conversation

@marksverdhei

Copy link
Copy Markdown

What & why

Closes the remaining exposure of #66. #136 landed the in-router per-device admit + targeted LRU eviction (steady state), but the admit reasons about live cudaMemGetInfo only. Two same-device admits fired close together both read stale-high free memory before either child's cudaMalloc is visible, and co-load onto the same GPU → OOM. This is the exact titan --models-max 2 scenario in #66 (the under-lock re-check in load() guards the model count, not per-device memory).

The fix is the reserved[] LOADING-state accounting sketched in @marksverdhei's own #66 comment, and the residual I flagged in #146.

How

  • reserved_per_device[] on server_models (guarded by mutex, indexed in system-GPU order — identical to per_device_bytes and unload_lru's projected_free).
  • Reserve on entering LOADING (load()): applied past all throw points, just before the mapping[] commit, so it can't leak on a failed spawn — and while load() still holds the lock, so the monitoring thread can't update_status()-release it early.
  • Release on leaving LOADING (update_status()): to LOADED on success or back to UNLOADED on a failed load. Guarded on old_status == LOADING, which makes it idempotent against the double-UNLOADED path (crash handler + monitoring thread both fire) — update_status() is never called with status == LOADING (the child's "loading" state is a no-op in handle_child_state).
  • unload_lru subtracts the reserve from live free memory before the per-device fit check, so a later same-device admit sees the in-flight demand.
  • LOADING-state models are excluded from eviction — an in-flight load is never thrashed to make room for another load.

Verification

  • Compiles clean: CPU-only cmake --build --target llama-server (GGML_CUDA=OFF, server on) — server-models.cpp.o builds with no errors/warnings, llama-server links. CI will exercise the full matrix.
  • No functional change when nothing is co-loading (single-model or spare-GPU cases): reserved[] is zero, so projected_free is unchanged.

Open question for @marksverdhei (why this is a draft)

This makes unload_lru aware of in-flight loads, but it does not add a hard gate: if a device is saturated by in-flight LOADING models (which are now un-evictable), unload_lru still falls through and load() proceeds to spawn — same as today (could still OOM in that corner). The clean closure is an under-lock per-device re-check in load() (mirroring the existing count re-check at the // Re-check capacity under the lock block) that throws model_unavailable_error503, client retries. I left that out because it's a policy call — proceed-and-maybe-OOM vs. 503-retry — and model_unavailable_error/503 already exists (#41/#42). Say the word and I'll add the gate.

Also a narrow residual: two unload_lru calls that perfectly interleave before either sets LOADING won't see each other's reserve. Fully closing it means making the check-and-reserve atomic under one lock hold (folding the admit into load()'s locked region). Out of scope for this draft; happy to follow up.

Refs

The in-router admit check (#136) reasons about live cudaMemGetInfo only, so two
same-device admits fired close together both read stale-high free memory before
either child's cudaMalloc is visible and co-load onto the same GPU -> OOM. This
is the exact titan --models-max 2 scenario in #66 that #136's steady-state fix
did not close (the under-lock re-check in load() guards the model count, not
per-device memory).

Track an in-flight reserved_per_device[] (guarded by mutex, indexed in
system-GPU order like per_device_bytes): reserve a model's footprint when it
enters LOADING (applied past all throw points in load(), so it cannot leak on a
failed spawn, and while the load still holds the lock so update_status can't
release it early) and release it when it leaves LOADING to LOADED or back to
UNLOADED (in update_status, idempotent against the double-UNLOADED crash path).
unload_lru subtracts the reserve from live free memory, and LOADING-state models
are excluded from eviction so an in-flight load is never thrashed to make room
for another.
@marksverdhei

Copy link
Copy Markdown
Author

Reviewer note: reserve-leak audit (no leak found)

Verified that the reserved[] invariant — applied on enter-LOADING, released on every exit from LOADING — holds across all paths that can remove a LOADING model, so the reserve cannot leak:

  • Normal completion / failed spawn / crash — all go through update_status() (LOADED via handle_child_state, or UNLOADED via the monitoring-thread/crash handler). Release is guarded on old_status == LOADING, so it fires exactly once and is idempotent against the double-UNLOADED path (crash handler + outer thread).
  • load_models() reload — builds to_unload from any is_running() model (LOADING included) whose preset was removed/changed, calls unload() (→ update_status(UNLOADED) → release), waits for UNLOADED, joins, and only then runs the erase loop (whose GGML_ASSERT(!th.joinable()) confirms the model was already stopped). So a LOADING model dropped mid-load is released before erase.
  • remove()unload() → waits for UNLOADED/DOWNLOADED → joins → erases. Released before erase.
  • No status bypass — the only direct status = LOADING is the enter-point in load(); DOWNLOADING/DOWNLOADED are unrelated states. No path sets a LOADING model to another status outside update_status().

Net: every removal of a LOADING model is preceded by a LOADING→UNLOADED (or →LOADED) transition through update_status(), which releases the reserve. Bounds are also defensive (reserve_release clamps at 0; reserve_apply resizes).

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.

1 participant