You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
apply_t3_rebuild() in tools/server/server-context.cpp:4430 runs
synchronously inside update_slots(). The call site is:
// tools/server/server-context.cpp:4569-4577 (PR #42 head)//// NOTE: this runs synchronously inside update_slots(), blocking// the serving loop for the duration of the GGUF load + VRAM// alloc. The drain-timeout guards entry but not the reload// itself — the engine appears hung to the Coordinator for the// entire reload. A future improvement could offload this to a// background thread and gate requests until the reload completes.if (!load_model(swapped_params)) {
A full T3 model reload does:
unload_model(model_tgt) — frees the current ggml model + KV cache + tensor buffers
llama_model_load_from_file(swapped_params.model.path, ...) — reads the GGUF, allocates VRAM (multi-second for a 19 GB Q5_K_M model on a 16 GB GPU; tens of seconds for the full 35B-A3B Q3_K-mini on a 12 GB 3060 with CPU offload)
llama_new_context_with_model(model, cparams) — builds the KV cache
All six steps run on the same thread as update_slots(). During the reload, no in-flight request on any slot can be scheduled, and the engine's HTTP response stream is silent — the Coordinator sees the engine as hung.
Reproduction
# 1. Start the engine on the 5060 Ti + 3060 pair (COMBINED-static, DENSE profile)
bash scripts/deploy-hydra-head.sh rtx
# 2. Confirm the engine is serving (curl /health → 200, /v1/chat/completions returns)# 3. Run set-profile.sh moe (operator-initiated profile switch, fires a T3 CONFIGURE)
bash scripts/set-profile.sh moe
# 4. While the switch is in progress, fire a parallel chat completion:time curl -X POST http://localhost:8080/v1/chat/completions \
-d '{"model":"Q3_K-mini","messages":[{"role":"user","content":"hi"}]}' \
-H 'Content-Type: application/json'# Expected (today): the second request blocks for the full T3 reload duration# before the first reload's drain returns. No progress is visible to the# Coordinator — the engine reads as "hung" in the dashboard.# Expected (after fix): the reload runs in a background thread; the second# request is either served with the old config (Q1: old-config path, per# the recommendation in llama.cpp#40) or rejected with 503 (Q2: block path).
Impact
Multi-second engine hangs for the headline use case (set-profile.sh moe ↔ set-profile.sh dense, 35B-A3B Q3_K-mini). For the 12 GB 3060 with the full MoE, the reload can be 10-20s; the engine reads as a crash to the Coordinator.
Coordinator's item.EngineConfigTier field is set at request time but the actual model serving the request may be a different model than the one the request specified. With Q1 (old-config path), the request is correctly served with the old config, but the response trace shows the new config — the operator's telemetry is misleading.
Blocks any future feature that wants to hot-swap a model without operator intervention (e.g. context-length overflow detected at request time triggers a n_ctx change — T2 only, but the same blocking pattern applies if T2 also gets a larger refactor).
8e0122e15 correctly identifies why the fix is non-trivial:
P3b (async T3 reload) is deferred to a follow-up — load_model() replaces
nearly every member variable, making background-thread reload require
double-buffering, which is a larger architectural change.
Sketch:
Two-context double-buffer. Add a server_context_impl::pending_ctx member — a second llama_context + llama_model + per-slot state being built on a background thread. The existing context keeps serving requests until the new one is ready.
Background-thread orchestration.apply_pending_hydra_config() returns immediately after spawning the reload (the slot-free check still gates entry). The background thread runs load_model(swapped_params) + COMBINED reattach.
Atomic swap on completion. When the background thread succeeds, the swap is a pointer exchange:
pending_ctx becomes the live ctx_tgt
the old context is torn down
per-slot pointers refresh
any in-flight requests (zero, by invariant — drain) see the new context on the next update_slots()
Cancellation / failure. If the background thread fails, the rollback runs in the same background thread (don't tie up update_slots). The result is reported via the next INFO call (deferred-keys cleared, error in the response).
T2 stays synchronous for now. T2 is llama_free + llama_new_context_with_model — single-digit ms on the RTX 5060 Ti, doesn't justify the double-buffer overhead. Revisit if T2 ever includes model-graph re-derivation that takes >100ms.
Acceptance:
T3 apply no longer blocks update_slots(); the reload runs on a worker thread.
The drain-timeout semantics are unchanged (entry still gated, drain still aborts on timeout).
The Coordinator's item.EngineConfigTier is either honored (Q1: serve with old config) or the request is rejected (Q2: 503) — pick one per llama.cpp#40's Q2 and document in the design.
E2E: tests/system/test_profile_switch.py (planned in ddvnguyen/hydra_vortex#397 Phase 5) measures the reload time and asserts it's < HYDRAD_PROFILE_SWITCH_RELOAD_DEADLINE_MS from the Coordinator's view.
Effort estimate: ~1-2 weeks of C++ work, plus ~3 days of C# coordination changes in ProfileSwitcher and the new EngineConfigApplier if the swap semantic is Q1 (old-config serve) vs Q2 (reject).
Tracking
Found on: ddvnguyen/llama.cpp#42 re-review (Jul 12, 2026)
Fork PR (where the issue was found): ddvnguyen/llama.cpp#42 (T2/T3 apply path, the new commits verified)
Design question: ddvnguyen/llama.cpp#40 Q2 ("T3 race during decode") — the swap semantic must align with the chosen answer (Q1: old-config, Q2: 503 reject).
Sub-tasks (none yet)
To be split once the design is firmed up. Likely sub-tasks:
Double-buffer primitives in server_context_impl (slot, ctx_tgt, model_tgt, spec pointer — all need swap-friendly representations)
Background-thread orchestration in apply_pending_hydra_config
Atomic swap protocol (pointer exchange under the slot-free mutex)
C# ProfileSwitcher change for Q1 vs Q2 semantics
tests/system/test_profile_switch.py (lives in ddvnguyen/hydra_vortex#397 Phase 5)
Finding (P1)
apply_t3_rebuild()intools/server/server-context.cpp:4430runssynchronously inside
update_slots(). The call site is:A full T3 model reload does:
unload_model(model_tgt)— frees the current ggml model + KV cache + tensor buffersllama_model_load_from_file(swapped_params.model.path, ...)— reads the GGUF, allocates VRAM (multi-second for a 19 GB Q5_K_M model on a 16 GB GPU; tens of seconds for the full 35B-A3B Q3_K-mini on a 12 GB 3060 with CPU offload)llama_new_context_with_model(model, cparams)— builds the KV cache02c7a9d99fix)ggml_backend_sched_reset+ per-slot sampler re-initAll six steps run on the same thread as
update_slots(). During the reload, no in-flight request on any slot can be scheduled, and the engine's HTTP response stream is silent — the Coordinator sees the engine as hung.Reproduction
Impact
set-profile.sh moe↔set-profile.sh dense, 35B-A3B Q3_K-mini). For the 12 GB 3060 with the full MoE, the reload can be 10-20s; the engine reads as a crash to the Coordinator.item.EngineConfigTierfield is set at request time but the actual model serving the request may be a different model than the one the request specified. With Q1 (old-config path), the request is correctly served with the old config, but the response trace shows the new config — the operator's telemetry is misleading.n_ctxchange — T2 only, but the same blocking pattern applies if T2 also gets a larger refactor).Fix
8e0122e15correctly identifies why the fix is non-trivial:Sketch:
Two-context double-buffer. Add a
server_context_impl::pending_ctxmember — a secondllama_context+llama_model+ per-slot state being built on a background thread. The existing context keeps serving requests until the new one is ready.Background-thread orchestration.
apply_pending_hydra_config()returns immediately after spawning the reload (the slot-free check still gates entry). The background thread runsload_model(swapped_params)+ COMBINED reattach.Atomic swap on completion. When the background thread succeeds, the swap is a pointer exchange:
pending_ctxbecomes the livectx_tgtupdate_slots()Cancellation / failure. If the background thread fails, the rollback runs in the same background thread (don't tie up
update_slots). The result is reported via the next INFO call (deferred-keys cleared, error in the response).T2 stays synchronous for now. T2 is
llama_free + llama_new_context_with_model— single-digit ms on the RTX 5060 Ti, doesn't justify the double-buffer overhead. Revisit if T2 ever includes model-graph re-derivation that takes >100ms.Acceptance:
update_slots(); the reload runs on a worker thread.item.EngineConfigTieris either honored (Q1: serve with old config) or the request is rejected (Q2: 503) — pick one perllama.cpp#40's Q2 and document in the design.tests/system/test_profile_switch.py(planned inddvnguyen/hydra_vortex#397Phase 5) measures the reload time and asserts it's< HYDRAD_PROFILE_SWITCH_RELOAD_DEADLINE_MSfrom the Coordinator's view.Effort estimate: ~1-2 weeks of C++ work, plus ~3 days of C# coordination changes in
ProfileSwitcherand the newEngineConfigApplierif the swap semantic is Q1 (old-config serve) vs Q2 (reject).Tracking
ddvnguyen/llama.cpp#42re-review (Jul 12, 2026)tools/server/server-context.cpp(hydra-fork branch only)8e0122e15commit message ("P3b async T3 reload is deferred to a follow-up")Cross-repo
ddvnguyen/hydra_vortex#397(parent tracker for v4 design, Phase 2b / 4 / 5)ddvnguyen/llama.cpp#42(T2/T3 apply path, the new commits verified)ddvnguyen/llama.cpp#40Q2 ("T3 race during decode") — the swap semantic must align with the chosen answer (Q1: old-config, Q2: 503 reject).Sub-tasks (none yet)
To be split once the design is firmed up. Likely sub-tasks:
server_context_impl(slot, ctx_tgt, model_tgt, spec pointer — all need swap-friendly representations)apply_pending_hydra_configProfileSwitcherchange for Q1 vs Q2 semanticstests/system/test_profile_switch.py(lives inddvnguyen/hydra_vortex#397Phase 5)