Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c238f82
server: add --pipeline-groups to run the slots over several contexts
danielhanchen Sep 5, 2026
f96bb35
server: harden the pipeline group decode loop
danielhanchen Sep 5, 2026
2bd1359
rpc: serialise the client connection and track the stored graph per c…
danielhanchen Sep 5, 2026
4dc1b57
rpc: batch the tensor reads of a step and move the split tensors asyn…
danielhanchen Sep 5, 2026
c2f055a
rpc: keep the asynchronous copy to the backend's own buffer type
danielhanchen Sep 5, 2026
d9b25a7
ggml-backend: only ask the source backend for a copy a different back…
danielhanchen Sep 6, 2026
ed651ea
rpc: never open a second connection from the asynchronous paths
danielhanchen Sep 6, 2026
25f208c
rpc: read a batched response in one receive
danielhanchen Sep 6, 2026
2c336f3
remove the benchmark artefacts that were committed by accident
danielhanchen Sep 6, 2026
ec74a60
reduce comment volume in the rpc stage2 changes
danielhanchen Sep 8, 2026
bb0e9ac
rpc: recycle the async-copy event pool
danielhanchen Sep 9, 2026
313019c
ggml-backend: give the source side of an async copy its own interface…
danielhanchen Sep 9, 2026
433ef71
server: update the prompt cache only after waiting for the slot's group
danielhanchen Sep 9, 2026
35c4ca6
ggml-backend: bump the backend API version for the enlarged interface
danielhanchen Sep 9, 2026
7cf19c5
ggml-backend: require a real destination before delegating to the source
danielhanchen Sep 9, 2026
4ed1cd0
rpc: validate GET_TENSORS entries before allocating the response
danielhanchen Sep 9, 2026
47aaac3
rpc: advertise batched get as a server flag instead of a minor bump
danielhanchen Sep 9, 2026
3b1f523
rpc: wait for every outstanding staging event, not just the last one
danielhanchen Sep 9, 2026
6ca4b11
server: carry pipeline groups into router children and give each grou…
danielhanchen Sep 9, 2026
6e2cbe7
rpc, server: close the staging reservation gap, free staging with its…
danielhanchen Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,4 @@ a.out.*

AGENTS.local.md
.pi/SYSTEM.md
bench/
14 changes: 13 additions & 1 deletion ggml/src/ggml-backend-impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
extern "C" {
#endif

#define GGML_BACKEND_API_VERSION 2
// 3: ggml_backend_i gained cpy_tensor_from_async. A dynamically loaded backend compiled
// against version 2 supplies a struct one member short, so the version has to move with it
// for the load-time check to reject that pairing instead of reading past the end.
#define GGML_BACKEND_API_VERSION 3

//
// Backend buffer type
Expand Down Expand Up @@ -138,6 +141,15 @@ extern "C" {

// (optional) sort/optimize the nodes in the graph
void (*graph_optimize) (ggml_backend_t backend, struct ggml_cgraph * cgraph);

// (optional) copy a tensor of this backend into another backend's tensor, driven from the
// source side. Kept separate from cpy_tensor_async because every existing implementation
// of that is written as a destination side handler: it casts backend_dst to its own
// context before deciding anything, so dispatching a source side call there would
// reinterpret a foreign backend_dst, or dereference a null one. Implement this only if
// the source role is genuinely supported. Appended last so backends that list the
// members positionally are unaffected.
bool (*cpy_tensor_from_async)(ggml_backend_t backend_src, ggml_backend_t backend_dst, const struct ggml_tensor * src, struct ggml_tensor * dst);

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 Bump the backend API version for the enlarged interface

When an executable loads a dynamic backend built against the previous interface, the loader still accepts it because GGML_BACKEND_API_VERSION remains 2, but adding this pointer enlarges the embedded ggml_backend_i and moves the core's expected device and context offsets. Accessing such a backend can therefore reinterpret its old fields or call a non-function address; increment the backend API version so stale plugins are rejected.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 35c4ca6. Correct and I should have done it in the same commit that added the member: a backend compiled against version 2 hands over a struct one member short, and reading past the end of it is precisely the failure the load-time check exists to prevent. Version is now 3, with a note on the line saying what changed.

};

struct ggml_backend {
Expand Down
29 changes: 24 additions & 5 deletions ggml/src/ggml-backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,27 @@ void ggml_backend_tensor_copy(const struct ggml_tensor * src, struct ggml_tensor
}
}

// src is only asked when its implementation differs: same-type backends share one implementation that already declined.
static bool ggml_backend_cpy_tensor_async_impl(ggml_backend_t backend_src, ggml_backend_t backend_dst, const struct ggml_tensor * src, struct ggml_tensor * dst) {
if (backend_dst != NULL && backend_dst->iface.cpy_tensor_async != NULL) {
if (backend_dst->iface.cpy_tensor_async(backend_src, backend_dst, src, dst)) {
return true;
}
}

// backend_dst must be a real backend before anything is delegated to the source. An
// implementation of the source role has to inspect the destination to decide whether it can
// help, so handing it a null destination pushes that dereference into every implementer.
// Guarding here keeps the invariant in one place instead of depending on all of them.
if (backend_src != NULL && backend_dst != NULL && backend_src->iface.cpy_tensor_from_async != NULL) {
if (backend_src->iface.cpy_tensor_from_async(backend_src, backend_dst, src, dst)) {
return true;
}
}

return false;
}

void ggml_backend_tensor_copy_async(ggml_backend_t backend_src, ggml_backend_t backend_dst, const struct ggml_tensor * src, struct ggml_tensor * dst) {
GGML_ASSERT(ggml_are_same_layout(src, dst) && "cannot copy tensors with different layouts");

Expand All @@ -507,10 +528,8 @@ void ggml_backend_tensor_copy_async(ggml_backend_t backend_src, ggml_backend_t b
}

GGML_ASSERT(backend_dst);
if (backend_dst->iface.cpy_tensor_async != NULL) {
if (backend_dst->iface.cpy_tensor_async(backend_src, backend_dst, src, dst)) {
return;
}
if (ggml_backend_cpy_tensor_async_impl(backend_src, backend_dst, src, dst)) {
return;
}

// an async copy would normally happen after all the queued operations on both backends are completed
Expand Down Expand Up @@ -1728,7 +1747,7 @@ static enum ggml_status ggml_backend_sched_compute_splits(ggml_backend_sched_t s
} else {
// try async copy, but if not possible, we can still use a sync copy without synchronizing the dst backend, since we handle the synchronization here with multiple copies and events
// TODO: add public function to facilitate this, since applications do not have direct access to the backend interface
if (!split_backend->iface.cpy_tensor_async || !split_backend->iface.cpy_tensor_async(input_backend, split_backend, input, input_cpy)) {
if (!ggml_backend_cpy_tensor_async_impl(input_backend, split_backend, input, input_cpy)) {
ggml_backend_synchronize(input_backend);
if (sched->events[split_backend_id][sched->cur_copy] != NULL) {
ggml_backend_event_synchronize(sched->events[split_backend_id][sched->cur_copy]);
Expand Down
Loading