Skip to content

fix(470): auto-recovery probe + force T3 rebuild on RPC peer reconnection - #104

Draft
hydra-z[bot] wants to merge 5 commits into
hydra-forkfrom
fix/470-auto-recovery
Draft

fix(470): auto-recovery probe + force T3 rebuild on RPC peer reconnection#104
hydra-z[bot] wants to merge 5 commits into
hydra-forkfrom
fix/470-auto-recovery

Conversation

@hydra-z

@hydra-z hydra-z Bot commented Aug 21, 2026

Copy link
Copy Markdown

What & why

Adds auto-recovery for COMBINED mode when an RPC peer (RTX 3060) restarts. Previously, a peer restart caused:

Changes

File Change
ggml/src/ggml-rpc/ggml-rpc.cpp Add ggml_backend_rpc_check_any_peer_reconnection() — probes all RPC backends for stale peer_reconnected flag
ggml/include/ggml-rpc.h Declare ggml_backend_rpc_check_any_peer_reconnection()
tools/server/server-context.cpp PREFILL handler probes RPC peers before hydra_apply_config — sets peer_reconnection_pending if any peer reconnected. Both apply_t3_rebuild() call sites (sync PREFILL + apply_pending_hydra_config) pass force=true when reconnection pending

How it works

  1. Before applying hydra_config, the PREFILL handler calls ggml_backend_rpc_check_any_peer_reconnection()
  2. If any RPC peer has the peer_reconnected flag set (from a prior failed RPC call), the PREFILL handler sets ctx_tgt->peer_reconnection_pending = true
  3. apply_t3_rebuild(force=true) skips the "model/params unchanged" early-exit and forces a full model reload
  4. The reloaded model reconnects to the fresh peer with new buffers

Known limitation

Race condition: if the peer dies AFTER the PREFILL probe but BEFORE graph_compute, the first request still fails. The update_slots reconnection check catches it on the next request. Full fix requires decode-path retry-on-fail.

Test plan

  • Peer restart → first request succeeds (when probe catches reconnection)
  • Peer restart → second request succeeds (update_slots fallback)
  • MTP works after recovery: 24 tok/s, 94.7% acceptance
  • Full 12-hour continuous run (pending)

When an RPC peer (e.g. RTX 3060) crashes and restarts, the 5060 Ti's
graph tensor data pointers become stale because the peer's new process
has empty g_hydra_server_buffers. This causes graph_compute to fail
with 'cannot reach peer' and the engine degrades to solo mode (~3 tok/s
instead of ~42 tok/s COMBINED).

This fix adds auto re-provision detection and T3 rebuild triggering:

1. ggml-rpc.cpp: Add peer_reconnected atomic flag to device context.
   Set when ggml_backend_rpc_graph_compute detects last_sock changed
   (peer restart). Return GGML_STATUS_FAILED to signal the engine.

2. ggml-rpc.cpp: Add ggml_backend_rpc_check_peer_reconnection()
   function that reads and clears the reconnection flag per device.

3. llama-context.cpp: After graph_compute failure, check all RPC
   devices for reconnection. If detected, set peer_reconnection_pending
   flag on the context.

4. llama-context.h: Add public peer_reconnection_pending flag.

5. server-context.cpp: In update_slots(), when all slots are idle and
   peer_reconnection_pending is set, call apply_t3_rebuild(force=true)
   to re-provision model layers on the fresh peer.

6. server-context.cpp: Add force parameter to apply_t3_rebuild() to
   bypass the unchanged-config optimization when needed.

The engine continues serving solo during re-provision and returns to
COMBINED mode after the T3 rebuild completes.
When an RPC peer (e.g. RTX 3060) crashes and restarts, buffer functions
(get_tensor, set_tensor, init_tensor, free_buffer, clear, get_base)
would GGML_ABORT via RPC_STATUS_ASSERT because the peer's new process
has empty g_hydra_server_buffers and can't resolve stale remote_ptrs.

This fix converts all RPC_STATUS_ASSERT calls in buffer functions to
fail-soft: instead of crashing, they set the peer_reconnected flag on
the device context and return gracefully. The engine will detect the
flag on the next graph_compute call and trigger T3 rebuild to
re-provision model layers on the fresh peer.

Changes:
- Add rpc_set_reconnect_flag() helper to set peer_reconnected atomically
- get_tensor: return without data on RPC failure (fail-soft)
- set_tensor: skip write on RPC failure (fail-soft)
- init_tensor: skip padding init on RPC failure (fail-soft)
- free_buffer: skip free on RPC failure (fail-soft)
- clear: skip clear on RPC failure (fail-soft)
- get_base: return nullptr on RPC failure (fail-soft)

This prevents the engine from crashing when a peer restarts, allowing
the auto re-provision (T3 rebuild) to restore COMBINED mode.
The reconnection check in llama_context::graph_compute was accidentally
removed during the RPC_STATUS_ASSERT fail-soft conversion. This meant
the peer_reconnection_pending flag was never set, so the T3 rebuild
in update_slots() was never triggered after a peer restart.

Restore the check: after graph_compute failure, scan all RPC devices
for reconnection. If detected, set peer_reconnection_pending = true
so update_slots() triggers apply_t3_rebuild(force=true).
- Add ggml_backend_rpc_check_any_peer_reconnection() — probes all RPC
  backends for stale peer_reconnected flag
- PREFILL handler probes RPC peers before hydra_apply_config — sets
  peer_reconnection_pending if any peer reconnected
- apply_t3_rebuild() force=true when peer_reconnection_pending is set
  (both sync PREFILL and apply_pending_hydra_config paths)
- Removes offload_kqv=false (user: KV on CPU kills speed)

WIP: race condition remains — peer can die between PREFILL probe and
graph_compute. Need decode-path retry-on-fail for full auto-recovery.
…xisted

graph_compute compared last_sock.lock() != sock and, on mismatch, set
peer_reconnected and returned GGML_STATUS_FAILED. But last_sock is an
empty weak_ptr on a fresh device context (engine start, and every T3
rebuild which recreates the context), so lock() returns nullptr and the
mismatch fired on the FIRST compute every time.

That forced a T3 rebuild on every engine start and looped: each rebuild
recreates the context (empty last_sock) so the next compute failed again
and the engine could never serve - the exact 'first request after restart
always fails' symptom PR #104 targets.

Guard the FAILED/flag path with prev_sock != nullptr so a genuine
reconnection (we held a live socket that now differs) is what triggers
re-provision, while a first connect proceeds normally.
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