Skip to content

rpc: cherry-pick upstream #18626 and #26500 (real async + event APIs, RPC_PROTO 6.0) - #181

Closed
danielhanchen wants to merge 2 commits into
masterfrom
pp/rpc-proto6
Closed

rpc: cherry-pick upstream #18626 and #26500 (real async + event APIs, RPC_PROTO 6.0)#181
danielhanchen wants to merge 2 commits into
masterfrom
pp/rpc-proto6

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

rpc: cherry-pick upstream ggml-org#18626 (real async + event APIs, RPC_PROTO 6.0)

Cherry-picks upstream d0132a68 -- ggml-org/llama.cpp#18626 "rpc : implement event and
async backend APIs"
, merged 2026-08-26 --
onto master. It applies cleanly with no conflicts: 2 files, +442/-165, no fork code touched.

Why

Our fork is on RPC_PROTO 5.1. Upstream is on 6.0. The difference is not cosmetic: ggml-org#18626
gives the RPC backend a real rpc_dispatcher per endpoint with a background I/O thread,
heap-allocated commands passed as shared_ptr, and genuine event semantics, then advertises
caps.async and caps.events unconditionally.

That is what lets ggml_backend_sched actually overlap work across RPC devices. Without it,
llama-server --device CUDA0,RPC0 -sm layer splits a model across two machines but runs the
two halves strictly one after the other, so a two-node split costs slightly more than it
saves. With it, the stages overlap and multi-node prefill roughly doubles.

Measured

Two DGX Sparks (GB10, aarch64) over ConnectX-7, Qwen3.8-27B-UD-Q4_K_XL (16.4 GiB),
llama-batched-bench -c 36864 -b 2048 -ub 512 -ntg 128, both GPUs clock-capped identically,
binaries md5-verified identical on both nodes. "1 Spark" and "2 Sparks" use the same binary,
so the only variable is whether the model is split.

Prompt processing, tok/s:

prompt tokens depth 1 Spark 2 Sparks, -sm layer ratio
512 8 772.6 1331.1 1.72x
1024 8 777.0 1419.9 1.83x
2048 8 771.7 1450.2 1.88x
4096 8 767.3 1449.8 1.89x

Token generation is unchanged, as expected -- a layer split moves the same number of weight
bytes per token, so decode cannot speed up and the only question is how much the hop costs:

prompt tokens depth 1 Spark 2 Sparks ratio
512 8 62.11 59.48 0.96x
2048 8 60.53 56.36 0.93x

End to end, which is what a user waits for (4096-token prompts, 8 concurrent, 128 tokens each):
60.24 s on one Spark, 41.50 s split across two -- 1.45x.

Before this cherry-pick, on the same hardware and the same model, the same split was
0.95x-0.99x, i.e. a small loss. The commit is the entire difference.

Correctness

Checked through llama-server with the split on and off, and diffed:

  • greedy generation on a high-margin prompt
  • a prefix-cache hit versus cold recompute of the same tokens
  • a four-way mixed-length concurrent batch, each request also run alone

Every generated string is byte-identical between the single node and the split, including a
deliberately knife-edge short prompt that the unsplit node itself answers differently between
serial and batched execution. Prefix reuse is demonstrably live rather than silently disabled:
2399 ms cold versus 199 ms warm on the split.

This is unsurprising and worth saying explicitly: an RPC split has one llama_context, one KV
cache and one control plane, with the split below all of that at tensor placement. There is no
second scheduler to disagree with about seq_id-to-slot mapping.

Notes

  • Protocol version goes 5.1 -> 6.0, so both ends must be upgraded together. A 6.0 client
    against a 5.1 ggml-rpc-server will refuse to connect rather than misbehave.
  • Memory splits as expected: measured free deltas of +10.9 GiB and +11.3 GiB for a 16.4 GiB
    model, i.e. one copy spread over the pair.
  • Compile-checked on current master (CPU and CUDA), and the CUDA build was used for every
    number above.

Also cherry-picked: ggml-org#26500

d0132a68 alone leaves the fork one commit short for multi-worker setups. Upstream
#26500 rpc: avoid serializing buffers from other servers
(merged 2026-08-30) makes serialize_tensor dispatcher-aware, so a buffer pointer is only
included when the buffer belongs to the RPC socket receiving the graph. Without it, a client
driving two or more ggml-rpc-server instances serializes pointers owned by a different server
and the receiver fails with [create_node] invalid data ptr / [graph_compute] failed to create graph node 0. That is open issue ggml-org#28047.

It applies cleanly and brings the regression test upstream added with it. Run here:

$ bash tests/test-rpc-multi-server.sh build/bin/ggml-rpc-server build/bin/test-rpc-multi-server
$ echo $?
0

Two Sparks only need one RPC worker, so this is not what produced the numbers above. It is
included because three or more machines do need it, and picking one of a pair of related
upstream commits is how a fork acquires a bug nobody can reproduce upstream.

After both picks, ggml/include/ggml-rpc.h is byte-identical to upstream master and
ggml/src/ggml-rpc/ggml-rpc.cpp differs by 6 lines, all from an unrelated later ggml refactor
(ggml_backend_op_alloc_size_may_expand) that is not RPC-specific.

rgerganov and others added 2 commits September 4, 2026 00:32
* rpc : implement event and async backend APIs

* cache responses from RPC_CMD_GET_ALLOC_SIZE
* rpc: avoid serializing buffers from other servers

Only include remote buffer pointers when the buffer belongs to the RPC dispatcher receiving the graph. Add a two-server regression test for cross-server tensor serialization.

Assisted-by: Codex

* cont : add ref

---------

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0ec2378870

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +568 to +572
bool status = send_rpc_cmd(sock, msg_ptr->cmd, msg_ptr->input.get(), msg_ptr->input_size);
RPC_STATUS_ASSERT(status);
}
}
msg_ptr->completion.set_value();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Complete barriers only after remote execution

For output-less commands such as RPC_CMD_GRAPH_COMPUTE, send_rpc_cmd returns immediately after flushing the request, and this fulfills the promise without receiving any server acknowledgment. Therefore ggml_backend_rpc_synchronize and recorded events can report completion while the server is still computing, allowing the scheduler to reuse buffers or consume results on another backend prematurely; the barrier needs a round trip that completes only after the server finishes all preceding commands.

Useful? React with 👍 / 👎.

Comment on lines +843 to +849
// Cache key for calls to read the alloc_size.
// We deliberately exclude src tensor dimensions from the key because:
// 1. For CPU backends, alloc_size = ggml_nbytes(output) regardless of src shapes
// 2. For GPU backends, the reservation graph uses max dimensions, so the
// cached value from reservation is always >= any subsequent request
// 3. Including src dims causes cache misses per-ubatch (e.g. growing KV cache)
// which blocks the main thread behind in-flight GRAPH_COMPUTE commands

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include source tensors in alloc-size cache keys

When a FLASH_ATTN_EXT output with unchanged dimensions and op parameters is reused while its K/V source shapes grow, this cache returns the size calculated for the earlier sources. CUDA's ggml_cuda_flash_attn_ext_get_alloc_size explicitly adds conversion storage based on ggml_nelements(K) and ggml_nelements(V) for non-F16 sources, so a smaller first lookup can make the RPC allocator under-allocate a later decode graph and permit the conversion kernel to write past the allocation.

Useful? React with 👍 / 👎.

Comment on lines +509 to +512
void rpc_dispatcher::event_free(ggml_backend_event_t event) {
rpc_event * ev = (rpc_event *)event->context;
delete ev;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Delete the allocated event wrapper

Every event is created with both a heap-allocated rpc_event context and a heap-allocated ggml_backend_event, but this free path deletes only the context. Consequently each event owned by a parallel scheduler leaks its wrapper when ggml_backend_event_free is called, accumulating across repeated scheduler or context creation; delete event after releasing its context as the other backend implementations do.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-04T07:41:05.894898Z 0ec2378 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@danielhanchen

Copy link
Copy Markdown
Member Author

Closing this is correct, and here is the evidence so nobody reopens it by mistake.

The prebuilt release b10796-mix-659e406 is built from upstream ggml-org/llama.cpp tag b10796 (commit 9a4843cf) plus the pins in scripts/unsloth/pr-set.json, not from this repository's master. Upstream b10796 already contains both commits this PR cherry-picked: d0132a68 (ggml-org#18626) and a7cc83bb (ggml-org#26500). So every mix build on a b10796 or newer base has them by construction, and this PR would have re-applied code the base already carries.

Verified on the shipped linux-arm64 cuda13 bundle on a pair of DGX Sparks:

  • The bundle ships ggml-rpc-server and libggml-rpc.so, and every prebuilt workflow now passes -DGGML_RPC=ON.
  • A raw RPC_CMD_HELLO handshake against the shipped ggml-rpc-server returns protocol 6.0.1. A build of the upstream rpc : implement event and async backend APIs ggml-org/llama.cpp#18626 tree returns 6.0.0. The client only compares major and minor, so these interoperate.
  • Two-node layer split with only the shipped binaries logs pipeline parallelism enabled and sched copies = 4, generates correct output, and exits cleanly.

One thing worth knowing when reading this repository: master still has ggml/src/ggml-rpc/ggml-rpc.cpp at protocol 5.1 with caps.async = false, because its merge base with upstream is 11cd9884 from 2026-08-26, five commits before ggml-org#18626 landed. That tree is never what ships. To learn what a prebuilt contains, read the upstream base tag plus pr-set.json.

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.

3 participants