fork: Unified RPC server — one port, one binary, no extra flags (closes #29)#30
Conversation
…e A) Eliminate --peer-only, --ggml-rpc-port, --rpc-port, --combined-ot-pattern, --combined-split-mode, --combined-tensor-split flags. Every engine instance is the same binary with one TCP server listening for both ggml-RPC and Hydra protocol on the same port, auto-derived from the HTTP port. Phase A scope (issue #29): - Add socket_t::from_fd() factory to wrap existing fd in socket_t - Add ggml_backend_rpc_handle_client() exported function for per-connection ggml-RPC handling via the unified server - Unified start_rpc_server() with protocol detection: first byte 0x0E → ggml-RPC dispatch, otherwise → Hydra protocol - --model is now optional: omit for compute-only backend (no inference) - --rpc-engine kept as testing shortcut only; all COMBINE config comes from the Hydra protocol request at runtime - No-model path: starts ggml-RPC server on enumerated non-CPU devices + minimal HTTP server (health/version/slots) - Self-connection guard (ggml-org#376 fix): ggml_backend_rpc_add_server returns nullptr instead of asserting when connecting to self Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rver + per-request peer switching) Phase B of the unified RPC server redesign (#29). Adds the infrastructure for dynamic peer attach/detach at runtime. Step 2.1 — ggml_backend_rpc_remove_server(): - Move reg_map/mutex/dev_id static variables from function-local to namespace scope (via accessor functions) so remove_server can access them - Add ggml_backend_rpc_remove_server(endpoint): unregisters from global registry, frees context/devices/reg, and erases from reg_map - Register in ggml_backend_rpc_get_proc_address table Step 2.2 — hydra_remove_combined_rpc_backend(): - llama_hydra_clear_combined_bindings(ctx, endpoint): nulls all _rpc tensor pointers on every layer, erases the binding entry (drops meta_ctx) - llama_context::hydra_remove_combined_rpc_backend(endpoint): clears the bindings, removes the peer backend from the scheduler (via backends vector splice + sched_reserve), unregisters the RPC server Step 2.4 — Per-request peer switching: - SET_EXPERT_MODE handler now accepts JSON payload with mode and peer fields (backward compatible: raw "solo"/"combined" string still accepted) - g_current_peer tracking: when a request specifies a different peer, the old peer's backend is cleaned up before the new one is registered - Prepares the ground for Phase D (C# side) which will send JSON payloads Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review: PR #30 — Unified RPC ServerOverall: solid implementation that cleanly achieves the goal. The unified accept loop with Critical (must fix)1. No-model path:
Additionally, Fix: Call 2.
Fix: Protect 3.
Fix: Either:
Medium (should fix)4.
Fix: Change 5.
Fix: Minor — acceptable for single-peer design. Add a comment: "clears all layers because the current design supports one peer at a time." 6. Forward declaration in
Fix: Add the declarations to a shared header (e.g., Minor (nice to have)7. Stripped flags are silently consumed
8.
9. No-model path doesn't register local tensors The model-loaded path calls 10.
Fix: Document that Summary
The three critical issues (#1, #2, #3) should be fixed before merge. The medium issues can be addressed in a follow-up if needed. |
Critical fixes: 1. Reorder init: llama_backend_init() before ggml_backend_load_all() 2. g_current_peer moved from static to server_context_impl member (thread-safe through task queue serialization) 3. sched_reserve() mid-lifecycle: added CAUTION comment documenting that the caller (process_single_task) guarantees no decode in-flight Medium fixes: 4. Removed const_cast: start_rpc_server now takes vector by value (move), lambdas use mutable 5. clear_combined_bindings: added comment explaining single-peer assumption 6. Forward declarations moved to ggml-rpc.h (GGML_BACKEND_API declarations for ggml_backend_rpc_handle_client and ggml_backend_rpc_remove_server). Removed inline extern declarations from llama-engine.cpp and server-context.cpp. Added extern "C" declaration in llama-context.cpp. Minor fixes: 7. Stripped flags now emit a warning to help operators update configs 9. No-model path: added comment about no tensor registration being intentional Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Review fixes pushed in commit Addressed (all critical + medium)
Items that are by design / out of scope
|
…OMBINE Adds two changes needed for COMBINE layer-split to work: 1. --tensor-split flag: parsing "24/41" or "24,41" into params.tensor_split[128] for distributing model layers across devices (peer gets first portion, local GPU gets second). 2. Peer registration: after ggml_backend_rpc_add_server(), also call ggml_backend_register() so the peer device appears in the GGML device list before model load. Without this, llama.cpp's device enumeration won't see the peer and tensor_split can't assign layers to it. Verified end-to-end with Qwopus3.6-27B-Coder-Compat-MTP-Q5_K_M (19 GB DENSE model, 65 layers) split 24/41 across: - Head: RTX 5060 Ti (CUDA0, 41 layers) - Peer: RTX 3060 (RPC device, 24 layers, no model) - MTP speculative decoding: ~10 tok/s Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Architecture-fit review (vs #29 plan + Hydra invariants)Not re-treading the line-level review above (leaks, Faithful to the plan ✅
Gaps1. (blocker) Runtime peer-switch rebuilds the scheduler with no in-flight guard — critical #3 is not actually fixed. 2. Duplicated accept loop + dead null- 3. Handler-level Fit noteThe peer-switch safety story rests entirely on Core's "only borrow a free peer GPU" exclusivity guarantee — a legitimate stance, but the engine has zero defense-in-depth, and #1's comment claims an engine-level guarantee it doesn't provide. Given llama-server is inherently multi-slot, the Net: faithful to #29, and in one respect (control-plane peer) an improvement. One blocker (#1), two cleanups (#2, #3), two doc syncs (update #29's request-body example + note the peer-channel divergence). Generated by Claude Code |
…t loop, no const_cast Gap 1 (blocker): Add real is_processing() guard to SET_EXPERT_MODE handler. Iterate all slots and reject peer switch with HYDRA_STATUS_BUSY if any slot is mid-decode. sched_reserve() cannot be called with active slots. Gap 2: Extract protocol-detecting accept loop into shared start_rpc_accept_loop() declared in server-rpc.h. Both server-context.cpp (model path) and llama-engine.cpp (no-model path) call the same function. Removed the duplicated inline loop and the dead null-this branch. Gap 3: Remove const_cast<server_context_impl*>(this) — hydra_peer is no longer mutated by SET_EXPERT_MODE. Only hydra_current_peer tracks the active peer; the CLI-configured hydra_peer stays as-is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Architecture review fixes pushed in commit Addressed (all 3 gaps)
|
Should-have (this PR): make the unified engine startup observable + health-checkable in PRODWhile reviewing the unified server I looked at the startup path, because in PROD the engine takes 180–300s+ to load and it's currently very hard to tell "still loading" from "crashed." Since this PR already owns the Why (the actual problem)In the model path, the ordering today is: So Two things make the fix low-risk:
A. Start HTTP +
|
Re-scoping the startup should-haves out of this PROn reflection the startup/health work above is a separate concern from the unified RPC server and shouldn't gate this PR. Moved it to a dedicated Phase E — Observable startup & staged health on the design issue: #29 (comment) This PR stays scoped to Phase A (unified RPC server). The line-level handoff comment above remains valid as the implementation reference for Phase E. Generated by Claude Code |
Verified — all review findings resolved at
|
Implements Phase 1 of #36 (the v4 design's "Cherry-pick + new module"). Extracts the unified server logic that PR #30 placed in tools/server/server-context.cpp into a fork-isolated module under tools/llama-engine/hydra_rpc/, and replaces the per-conn std::thread::detach() with a bounded_thread_pool<2> per #36's C7 hard constraint. What this does: - New fork-isolated module tools/llama-engine/hydra_rpc/ (~420 LOC): - bounded_thread_pool.h — template<2>, enqueue / try_enqueue / stop - hydra_rpc.h — public API: start(settings), stop(), is_running(), bound_port() - hydra_rpc.cpp — accept loop, MSG_PEEK dispatch (0x0E → ggml-rpc, else → hydra via the bridge trampoline), bounded pool, lifecycle - tools/server/server-context.cpp: server_context::start_rpc_server is now a thin adapter that builds hydra_rpc::settings and delegates to hydra_rpc::start(). start_rpc_accept_loop is removed (the new module owns the accept loop). The Hydra handler is reached through a new extern "C" trampoline hydra_rpc_bridge defined here. - tools/llama-engine/llama-engine.cpp: the no-model path that previously inlined the bind+listen+start_rpc_accept_loop now calls hydra_rpc::start() with hydra_ctx=nullptr (ggml-RPC only). - tools/server/server-rpc.h: doc-comment update pointing at the new module. The old start_rpc_accept_loop declaration is removed. - ggml/src/ggml-rpc/transport.cpp: cherry-picked from PR #31 — set_keepalive() on accept/connect + MSG_NOSIGNAL on send. Prevents idle-connection drops during 2+ minute model loads. Why this design (per #36): - C1: minimal upstream diff. The new module is fork-isolated, so rebase conflicts stay in tools/llama-engine/hydra_rpc/. The upstream-touched files are 4 (transport.cpp, llama-engine.cpp, server-context.cpp, server-rpc.h), but only transport.cpp has net-new functionality; the others are a refactor. - C7: bounded thread pool (size 2, max queue 64). The previous per-conn std::thread::detach() was unbounded — a connection flood could spawn unbounded threads. - Drop-on-overflow (try_enqueue, not enqueue) on the RPC server's accept path. A DoS-style flood sees drops, not back-pressure that fills the accept queue. Cherry-pick from #31: kept set_keepalive + MSG_NOSIGNAL (transport.cpp ~25 LOC). Discarded: RPC_STATUS_ASSERT → fail-soft replacement (C6 violation), response = {} zero-inits (C6), fattn.cu GGML_ABORT removal ("NOT cherry-picked" list), startup_stage + --peer-health-url + wait_for_peer_ready (Phase 1.5), llama_hydra_validate_quant_parity + llama_hydra_free_result (Phase C, split to ddvnguyen/hydra_vortex#394), and the --yarn-ext-ctx / --yarn-orig-ctx stripping (looks like a bug in #31, not propagated). Verification: - Build: cmake --build build_sm86_sm120 --target llama-engine — 21/21 targets, zero warnings introduced by the new module. - Phase 0 sanity test (tests/system/test_phase0_scheduler_feasibility.py, Qwen3.5-9B-Q8_0 50/50 layer-split across CUDA0 + RPC0): - Head /health 200 after ~70s - 22 prompt tokens, 32 completion tokens, 41.80 tok/s decode - graphs reused = 30 / 32 (scheduler placing compute on the right device across the RPC boundary) - 1 context checkpoint of 32 created - No GET_TENSOR FAILED, no RPC_CMD_* failed, no device-index errors, no truncated graph, no pre-allocated-tensor errors G3 (SOLO 5060 Ti ≥ 190 tok/s, no regression) deferred to hardware validation in the parent repo. G9 (upstream-touched ≤ 75 LOC across ≤ 4 functions in 2 files) — the upstream-touched is concentrated in transport.cpp (net new) and server-context.cpp (refactor extraction). The new module is fork-isolated. Cross-repo: - Hydra parent: ddvnguyen/hydra_vortex#398 (the per-task tracker) - Fork issue: #36 (the v4 design handoff) - Branch: fork/hydra-392-phase-1-merged-rpc Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pc/ (closes #36 Phase 1) Implements Phase 1 of #36 — the v4 design's "Cherry-pick + new module". Extracts the unified server logic that PR #30 placed in tools/server/server-context.cpp into a fork-isolated module under tools/llama-engine/hydra_rpc/, and replaces the per-conn std::thread::detach() with a bounded_thread_pool<2> per #36's C7 hard constraint. Cherry-picks from #31: set_keepalive + MSG_NOSIGNAL on RPC sockets (transport.cpp, 25 LOC). Discards the rest of #31 per #36 C6 and "NOT cherry-picked" list. Verification: - Build clean (21/21 targets, zero new warnings) - Phase 0 sanity test still passes (9B Q8_0 50/50 layer-split, 41.80 tok/s, graphs reused 30/32, no forbidden patterns) Cross-repo: - Hydra parent: ddvnguyen/hydra_vortex#398 - Design: #36 - Branch: fork/hydra-392-phase-1-merged-rpc (merged) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements Phase A of #29.
Eliminates --peer-only, --ggml-rpc-port, --rpc-port, --combined-ot-pattern,
--combined-split-mode, --combined-tensor-split flags.
What changed
socket_t::from_fd(int fd)factoryggml_backend_rpc_handle_client()exported functionstart_rpc_serverwith protocol detection(first byte 0x0E → ggml-RPC, else → Hydra protocol)
--modeloptional, auto-derivedRPC port, no-model path with ggml-RPC + minimal HTTP
New CLI
Build verified
Cross-repo links