fix: peer perf regression — bounded-pool bypass + TCP_NODELAY (closes the 178/3 tok/s gap)#39
Conversation
The new merged server's peer path (started by PR #37) goes through the same dispatch pipeline as the head: accept() + MSG_PEEK + bounded thread pool + dispatch. This works for the head (where the pool bounds public-internet DoS risk) but throttles the peer in a layer-split COMBINE, where the head sends thousands of compute ops per prefill that the pool serializes behind the accept+enqueue+worker-pickup pipeline. This commit adds a peer_mode flag to the settings struct. When set (only the no-model compute-only server sets it), the accept loop skips both the bounded thread pool and the MSG_PEEK+dispatch path and calls ggml_backend_rpc_handle_client directly from a freshly detached std::thread, matching upstream llama.cpp's rpc-server.cpp behavior byte-for-byte. This restores the head's compute graph throughput on the peer path. Note: live benchmark on the 27B COMBINE at 25/40 / 96K / q8/q8 / MTP still showed 178 tok/s prefill vs 525 tok/s with the upstream rpc-server (3x slower). The remaining gap is in other code paths beyond the dispatch pipeline (likely the peer's ggml-rpc handler overhead and the head's per-op scheduling). A follow-up investigation is needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hydra_rpc.cpp's accept_loop() calls raw ::accept() and hands the fd straight to ggml_backend_rpc_handle_client(), which wraps it via socket_t::from_fd() (ggml-rpc/transport.cpp) — a bare wrapper that sets no socket options. Upstream's own server loop goes through socket_t::accept() instead, which sets TCP_NODELAY + SO_KEEPALIVE on every accepted connection. The merged accept loop never got that ported over, in either the pooled/MSG_PEEK path (#37) or this branch's peer_mode fast path. Without TCP_NODELAY, Nagle's algorithm batches the small, latency-sensitive ggml-rpc request/response messages that dominate prefill and (especially) decode — this is why the peer_mode change alone didn't move the earlier 178/3 tok/s benchmark at all. Validated on RTX 5060 Ti + RTX 3060 (Dense 27B Q5_K_M, 25/40 layer-split, ctx 65536, q8/q8 KV, MTP draft-n-max 3), bare-metal head+peer processes talking directly to /v1/completions: | Test | Fixed fork peer | Upstream rpc-server peer (ceiling) | |-----------------------------|-----------------|-------------------------------------| | 2K prompt / 256 predict | 504 pp / 29.2 tg | 490 pp / 29.2 tg | | 8K prompt / 256 predict | 554 pp / 24.3 tg | 556 pp / 24.3 tg | | 30 tok / 512 predict (MTP) | 32 pp / 26.9 tg | 31 pp / 26.8 tg | Fixed build matches the upstream-peer ceiling within noise on every test (previous regression was 178 pp / 3 tg). No RPC errors (GET_TENSOR FAILED etc.) in any run; test_phase0_scheduler_feasibility.py passes against a hydra-dev build with this fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root cause found + fixed (commit 158c0cf)The
Fix: Validated on hardware (RTX 5060 Ti + RTX 3060)Dense 27B Q5_K_M, 25/40 layer-split, ctx 65536, q8/q8 KV, MTP draft-n-max 3.
Fixed build matches the upstream-peer ceiling within noise on every test — the Review notes
|
Follow-up to #37 (Phase 1, merged 16b65ce).
The new merged server's peer path goes through the same dispatch pipeline as the head: accept() + MSG_PEEK + bounded thread pool + dispatch. This works for the head but throttles the peer in a layer-split COMBINE, where the head sends thousands of compute ops per prefill that the pool serializes behind the accept+enqueue+worker-pickup pipeline.
This PR adds a peer_mode flag to the settings struct. When set (only the no-model compute-only server sets it), the accept loop skips both the bounded thread pool and the MSG_PEEK+dispatch path and calls ggml_backend_rpc_handle_client directly from a freshly detached std::thread, matching upstream llama.cpp's rpc-server.cpp behavior.
Benchmark (27B COMBINE at 25/40 / 96K / q8/q8 / MTP, Phase 1 binary 16b65ce, on 5060 Ti + 3060)
The fix attempt removes the bounded-thread-pool and MSG_PEEK overhead in the peer path, matching the upstream's rpc-server.cpp behavior. However, the benchmark numbers are unchanged — the remaining 3x prefill / 9x decode gap is in other code paths (likely the peer's ggml-rpc handler overhead and the head's per-op scheduling in the new merged server).
The previous PR #37 had a comment claiming 525 / 32 tok/s for the 'real COMBINE' test. That comment was misleading — the 525 / 32 came from using the upstream rpc-server as the peer, not the Phase 1 binary as peer. With Phase 1 binary as peer, the numbers are 178 / 3 tok/s. See ddvnguyen/hydra_vortex#398 for the full investigation.
This PR is offered as a partial fix attempt. The remaining gap needs a follow-up investigation.
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com