Skip to content

rpc : implement event and async backend APIs - #18626

Merged
ggerganov merged 2 commits into
ggml-org:masterfrom
rgerganov:rpc-async
Aug 26, 2026
Merged

rpc : implement event and async backend APIs#18626
ggerganov merged 2 commits into
ggml-org:masterfrom
rgerganov:rpc-async

Conversation

@rgerganov

Copy link
Copy Markdown
Member

This patch implements the event and async APIs from the backend interface. The intent is to enable pipeline parallelism with the RPC backend.

Here is a summary of the changes:

  • Introduce rpc_dispatcher which allows sending commands both synchronously and asynchronously to an RPC endpoint. There is one dispatcher per RPC endpoint and every dispatcher is using a background thread for its network communication.
  • All RPC commands are allocated on the heap and passed to rpc_dispatcher with shared_ptrs to avoid making copies of the input data.
  • Creating an event adds a dummy message in the dispatcher's queue and synchronizing the event waits for this dummy message to be processed by the background thread. Not sure if this is equivalent to how CUDA events work which seem to be the inspiration of the backend API.
  • Changed RPC_CMD_FREE_BUFFER, RPC_CMD_BUFFER_CLEAR, RPC_CMD_INIT_TENSOR to not send any response which simplifies the logic in the dispatcher. Hence the protocol version bump (3.7.0).

TODO:

  • Verify event implementation; Are event_synchronize() and event_wait() equivalent here?
  • Implement cpy_tensor_async(). This is a hard one as RPC servers need to to talk to each other (see PR rpc : copy tensors across servers #8032). Pipeline parallelism relies on both graph_compute() and cpy_tensor_async() being async and I am not sure if we are going to see improvements only with the former.
  • Split RPC backend into several files (e.g. client, server, common). This is left for a future refactoring PR with no functional changes.

@ggerganov I will appreciate some early feedback on this.

Comment thread ggml/src/ggml-rpc/ggml-rpc.cpp
@github-actions github-actions Bot added the ggml changes relating to the ggml tensor library for machine learning label Jan 5, 2026
Comment thread ggml/src/ggml-rpc/ggml-rpc.cpp
@ggerganov

Copy link
Copy Markdown
Member

Verify event implementation; Are event_synchronize() and event_wait() equivalent here?

I think we have the following analogy between CUDA and RPC:

  • CUDA device -> RPC endpoint
  • CUDA stream -> nothing

If this is correct, then I believe the event_record() and event_wait() API should never be utilized because the RPC backend does not have a notion of "stream". Is this correct?

@rgerganov

Copy link
Copy Markdown
Member Author

If this is correct, then I believe the event_record() and event_wait() API should never be utilized because the RPC backend does not have a notion of "stream". Is this correct?

According to this comment from @slaren, ggml_backend_t is supposed to represent a stream or queue and this is exactly how I implement the client side.

The server side is a bit tricky as we may create multiple ggml_backend_t from multiple devices and expose them through a single endpoint. But for now my goal is to prove that we can get PP improvement in the simple case when every endpoint exposes a single device with a single ggml_backend_t

@ggerganov ggerganov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AFAIU since all communication for a given endpoint is funneled through the same socket, then effectively the RPC backends for a given endpoint on the client are all associated with a single shared "stream" (i.e. the network socket). So if this is correct, then my guess would be that event_wait() would have to be a noop. Doing dispatcher->event_synchronize() would not make it incorrect, though probably it's not needed.

From reading how the CUDA backend and toolkit work, I think that event_wait() only makes sense with parallel streams. It is effectively a mechanism to tell one stream to wait for another stream. So in the context of the RPC backend, it should be a noop.

Apart from that, I think the approach is OK.

Comment thread ggml/src/ggml-rpc/ggml-rpc.cpp
@wallentri88

Copy link
Copy Markdown
Contributor

I think it will be useful to check that extra heap allocation of each request and dispatching logic doesn't hurt TG speed when pipeline parallelism is not needed/disabled

@xbezdick

Copy link
Copy Markdown

Seems to work fine, together with #16753 I'm getting nice speed improvement.

@aoprea1982

Copy link
Copy Markdown

Apologize. Know everyone is busy but any updates on this implementation? Thank you!

@rgerganov

Copy link
Copy Markdown
Member Author

Apologize. Know everyone is busy but any updates on this implementation? Thank you!

I'm working on the implementation of cpy_tensor_async, I hope to post it for review soon

@Stoney49th

Copy link
Copy Markdown

thanks for all the good work! @rgerganov

Let me know when testing is required, I have a real franken-setup here with 2xB50 and a 6900XT (via RPC) - can assist in testing once the async work is ready.

@Stoney49th

Copy link
Copy Markdown

maybe a stupid question, but would this be an enabler for tensor parallel via RPC as well?

@sredman

sredman commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

@rgerganov -- No big comments from me. You and I converged on the same design, so implicit +1 from me there :)

I see the same speedup as on my branch with my hardware, so functionally it seems good.

YvanDaSilva pushed a commit to YvanDaSilva/llama.cpp that referenced this pull request Aug 22, 2026
@rgerganov

Copy link
Copy Markdown
Member Author

Here are some performance results coming from Nvidia 4xA100 HF space:

./llama-bench -rpc localhost:50052,localhost:50053,localhost:50054,localhost:50055 -hf unsloth/Phi-4-mini-instruct-GGUF,ggml-org/Qwen3.8-27B-GGUF:Q8_0,ggml-org/gemma-4-31B-it-GGUF:Q8_0 --split-mode layer -ngl 99 --ubatch-size 256 --batch-size 2048 --device RPC0,RPC0/RPC1,RPC0/RPC1/RPC2,RPC0/RPC1/RPC2/RPC3 -p 4096 -n 0
model size params backend ngl n_ubatch dev test t/s
phi3 3B Q4_K - Medium 2.31 GiB 3.84 B CUDA,RPC 99 256 RPC0 pp4096 7557.80 ± 25.49
phi3 3B Q4_K - Medium 2.31 GiB 3.84 B CUDA,RPC 99 256 RPC0/RPC1 pp4096 9552.90 ± 61.77
phi3 3B Q4_K - Medium 2.31 GiB 3.84 B CUDA,RPC 99 256 RPC0/RPC1/RPC2 pp4096 6542.86 ± 158.67
phi3 3B Q4_K - Medium 2.31 GiB 3.84 B CUDA,RPC 99 256 RPC0/RPC1/RPC2/RPC3 pp4096 5007.86 ± 132.02
qwen35 27B Q8_0 26.62 GiB 26.90 B CUDA,RPC 99 256 RPC0 pp4096 1173.31 ± 0.55
qwen35 27B Q8_0 26.62 GiB 26.90 B CUDA,RPC 99 256 RPC0/RPC1 pp4096 1929.63 ± 2.53
qwen35 27B Q8_0 26.62 GiB 26.90 B CUDA,RPC 99 256 RPC0/RPC1/RPC2 pp4096 1474.27 ± 2.49
qwen35 27B Q8_0 26.62 GiB 26.90 B CUDA,RPC 99 256 RPC0/RPC1/RPC2/RPC3 pp4096 1306.18 ± 1.48
gemma4 31B Q8_0 30.38 GiB 30.70 B CUDA,RPC 99 256 RPC0 pp4096 1095.35 ± 0.82
gemma4 31B Q8_0 30.38 GiB 30.70 B CUDA,RPC 99 256 RPC0/RPC1 pp4096 1802.49 ± 6.14
gemma4 31B Q8_0 30.38 GiB 30.70 B CUDA,RPC 99 256 RPC0/RPC1/RPC2 pp4096 1375.12 ± 4.08
gemma4 31B Q8_0 30.38 GiB 30.70 B CUDA,RPC 99 256 RPC0/RPC1/RPC2/RPC3 pp4096 1219.50 ± 2.60

Adding a second RPC server running on localhost gives us 30-70% PP improvement, depending on the model. I expect these numbers to be very similar with servers running on different hosts, using RDMA transport. Adding a third RPC server makes things worse, I guess due to the communication overhead between the nodes. Nevertheless, I think these results justify the complexity being added in the RPC backend and there is still room for improvement (e.g. implementing cpy_tensor_async).

One mistake I made when I've been testing this in the past was the assumption that I can get performance improvements with two RPC servers using the same physical GPU accelerator. This is simply not the case (at least with CUDA).

@sredman

sredman commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

I recently upgraded to four AMD V620. I also see negative speedup with more than two RPC GPUs. They're all on the same host, so it's kind of a bummer.
I did see near-linear speed improvement with up to four CPU RPC devices on different hosts (over Ethernet), so I think we have done this work correctly.

@ggerganov

Copy link
Copy Markdown
Member

@rgerganov Can you do a test with --ubatch-size 256 --batch-size 8192 -p 8192? I'm wondering if you don't see scaling beyond 2 RPC devices simply because of the small logical batch size.

@ggerganov ggerganov self-assigned this Aug 26, 2026
@rgerganov

Copy link
Copy Markdown
Member Author

@rgerganov Can you do a test with --ubatch-size 256 --batch-size 8192 -p 8192? I'm wondering if you don't see scaling beyond 2 RPC devices simply because of the small logical batch size.

These are the results:

model size params backend ngl n_batch n_ubatch dev test t/s
phi3 3B Q4_K - Medium 2.31 GiB 3.84 B CUDA,RPC 99 8192 256 RPC0 pp8192 7098.79 ± 13.02
phi3 3B Q4_K - Medium 2.31 GiB 3.84 B CUDA,RPC 99 8192 256 RPC0/RPC1 pp8192 9323.24 ± 49.93
phi3 3B Q4_K - Medium 2.31 GiB 3.84 B CUDA,RPC 99 8192 256 RPC0/RPC1/RPC2 pp8192 6897.75 ± 26.01
phi3 3B Q4_K - Medium 2.31 GiB 3.84 B CUDA,RPC 99 8192 256 RPC0/RPC1/RPC2/RPC3 pp8192 5782.01 ± 15.19
qwen35 27B Q8_0 26.62 GiB 26.90 B CUDA,RPC 99 8192 256 RPC0 pp8192 1171.64 ± 0.51
qwen35 27B Q8_0 26.62 GiB 26.90 B CUDA,RPC 99 8192 256 RPC0/RPC1 pp8192 1970.64 ± 7.53
qwen35 27B Q8_0 26.62 GiB 26.90 B CUDA,RPC 99 8192 256 RPC0/RPC1/RPC2 pp8192 1488.06 ± 2.11
qwen35 27B Q8_0 26.62 GiB 26.90 B CUDA,RPC 99 8192 256 RPC0/RPC1/RPC2/RPC3 pp8192 1312.52 ± 2.40
gemma4 31B Q8_0 30.38 GiB 30.70 B CUDA,RPC 99 8192 256 RPC0 pp8192 1081.42 ± 0.75
gemma4 31B Q8_0 30.38 GiB 30.70 B CUDA,RPC 99 8192 256 RPC0/RPC1 pp8192 1823.52 ± 2.67
gemma4 31B Q8_0 30.38 GiB 30.70 B CUDA,RPC 99 8192 256 RPC0/RPC1/RPC2 pp8192 1387.17 ± 2.49
gemma4 31B Q8_0 30.38 GiB 30.70 B CUDA,RPC 99 8192 256 RPC0/RPC1/RPC2/RPC3 pp8192 1212.99 ± 3.04

@ggerganov

Copy link
Copy Markdown
Member

I see - not much difference.

Good to merge?

@rgerganov

Copy link
Copy Markdown
Member Author

Good to merge?

I am not happy with the GET_ALLOC_SIZE cache but I will need more time to rework this and I don't want to hold this anymore. Let's merge and improve in follow ups.

@rgerganov rgerganov added the merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. label Aug 26, 2026
@ggerganov
ggerganov merged commit d0132a6 into ggml-org:master Aug 26, 2026
31 of 34 checks passed
ppenatra pushed a commit to ppenatra/llama.cpp that referenced this pull request Aug 27, 2026
* rpc : implement event and async backend APIs

* cache responses from RPC_CMD_GET_ALLOC_SIZE
MarkShark2 added a commit to MarkShark2/llama.cpp that referenced this pull request Aug 27, 2026
34 commits, 9 conflicted files. Resolutions worth knowing:

- ggml-rpc: KEPT THE FORK'S RPC LAYER, deferred upstream ggml-org#18626. Upstream
  replaced the client socket model with an rpc_dispatcher + message queue and
  bumped the wire to 6.0.0. The fork has 4035 lines layered on the old model
  (full-duplex lanes, peer push, proto-v5 graph cache, imatrix sqsum, bf16
  wire); porting them is a project, not a merge step, so ggml-rpc.{h,cpp} stay
  on the fork's side at proto 5.1.3. Client and servers deploy together, so
  the fabric stays self-consistent.

- Vulkan lightning_indexer: both sides had a complete implementation and BOTH
  SURVIVED the automerge (duplicate structs, two dispatch functions, two
  switch cases, two shader registrations -- the documented "compiles clean
  with both copies" seam). Kept upstream's shader and its k-type array, which
  supports 9 K types instead of f32/f16, and layered the fork's measured
  fast_f16 path back on top: it keeps its own push-constant struct (renamed
  vk_op_lightning_indexer_fast_push_constants), its own dispatch branch, and
  the -inf-masked-row skip that fixed the DSV4 cold request (942c45c).

- qwen4exp (new upstream arch): read hparams.n_ff_exp as a plain field. It is
  a per-layer accessor here; raw field is n_ff_exp_impl. The recurring seam.

- server-context: n_ctx_slot became a method (--kv-unified-per-slot).
- speculative: DFlash2's !is_dflash2 masking kept alongside the fork's Laguna
  causal-attention probe; DFlash2 is never "laguna" so it keeps non-causal.
- llama-model: kept upstream's M-RoPE branch ahead of the fork's DFLASH
  dsv4-backbone rope selection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwEmGb6MgaXhJVy46bKNLk
YvanDaSilva pushed a commit to YvanDaSilva/llama.cpp that referenced this pull request Aug 31, 2026
YvanDaSilva pushed a commit to YvanDaSilva/llama.cpp that referenced this pull request Aug 31, 2026
danielhanchen added a commit to unslothai/unsloth that referenced this pull request Sep 4, 2026
The planner shipped one number for splitting a model that already fits on one
node: 0.92x, a loss, always. That is still correct for a llama.cpp whose RPC
backend predates ggml-org/llama.cpp#18626 ("rpc: implement event and async
backend APIs", merged 2026-08-26), which is what the fork currently builds.
Without that commit the RPC backend advertises neither async nor events,
ggml_backend_sched refuses to pipeline across RPC devices, and the two halves
run strictly one after the other.

With it the answer stops being a constant, because what overlaps is prefill.
Measured end to end on two Sparks, Qwen3-27B Q4_K_XL, same binary in both arms
so the only variable is whether the model is split:

  prompt tokens |  c=1    c=4    c=8
          128   | 0.94x  0.95x  0.95x
          256   | 0.98x  1.00x  1.00x     <- break-even
          512   | 0.96x  1.05x  1.07x
         1024   | 1.02x  1.12x  1.17x
         2048   | 1.07x  1.23x  1.29x
         4096   | 1.11x  1.35x  1.45x

Decode is 0.93-0.98x throughout and cannot be otherwise: a layer split moves the
same weight bytes per token, so the entire gain is prefill and the entire
question is prompt length. Below ~256 tokens splitting costs 2-6%; above ~1024
it wins, growing with both prompt length and concurrency. So it is a loss for
chat-shaped traffic and a win for prompt-heavy work, which is the opposite of a
single verdict.

`layer_split_speedup()` defaults to `async_rpc=False` and returns the
conservative 0.92x, so nothing promises users a speedup their build cannot
deliver. With `async_rpc=True` and no prompt length it returns None rather than
guessing, and it snaps down to the nearest measured row rather than
interpolating: these are six measured points, not a fitted curve.

The old constant is kept and still used, so no existing caller changes
behaviour.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ggml changes relating to the ggml tensor library for machine learning merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants