Engine: share prefill/decode work buffers across sessions#515
Open
iCreil wants to merge 1 commit into
Open
Conversation
Split ds4_gpu_graph into per-session state (KV caches, compressor and indexer frontiers, MTP verification state) and a new engine-owned ds4_gpu_scratch holding the transient work buffers: one-token decode tensors, per-layer stage tensors, MTP draft tensors and the batched prefill buffers. Everything in the scratch is written and consumed inside a single sync/eval call, so sessions sharing an engine can share one scratch as long as they are driven from one thread at a time -- which is exactly the serialization the server's single graph worker already provides. This is what makes a second live session affordable: the prefill batch buffers dominate a session's non-KV footprint (~1.8 MiB per prefill-chunk token), and they are now allocated once per engine instead of once per session. The scratch grows on demand to the union of the attached sessions' capacities and is freed with the engine. No behavior change with a single session: tensors, sizes and the execution order are unchanged, only their ownership moved. Add tests/two_sessions_smoke: drives two sessions of one engine interleaved (one greedy token each, alternating on one thread) and checks both streams match isolated single-session runs token by token. Verified on CUDA (RTX PRO 6000, DeepSeek V4 Flash q2): MATCH for both sessions, and isolated outputs identical to the pre-refactor build.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Split
ds4_gpu_graphinto per-session state and a new engine-ownedds4_gpu_scratchthat holds the transient work buffers: the one-token decode tensors, the per-layer stage tensors, the MTP draft tensors and the batched prefill buffers. Everything in the scratch is written and consumed inside a singleds4_session_sync()/ds4_session_eval()call — no content survives across calls — so sessions sharing one engine can also share one scratch, as long as they are driven from one thread at a time. That is exactly the serialization the server's single graph worker already provides, and the contract is now documented inds4.hnext tods4_session_create().Persistent state stays where it was, in the graph owned by each session: KV caches, compressor/indexer frontiers and rows, MTP raw cache and verification/prefix1 state.
Why
The scratch is what makes a second live session affordable. The batched prefill buffers dominate a session's non-KV footprint (~1.8 MiB per prefill-chunk token, ≈3.6 GiB at
--prefill-chunk 2048), and today every session pays for its own copy. With the scratch on the engine, a second session costs only its KV cache and raw SWA rows.This is intended as the first groundwork step toward the "stateful session-based protocol" direction mentioned in the README: it makes N sessions per engine practical without touching the server at all (a follow-up PR builds on it).
How
ds4_gpu_scratchis created lazily by the firstds4_session_create()and grown to the union of the attached sessions' capacities (prefill_cap,comp_cap, MTP on/off); it is freed with the engine. A refcount tracks attached sessions (the engine warns if closed while sessions are live).metal_graph_alloc_raw_cap()takes the scratch as a parameter and only allocates per-session state; sizes and allocation order are unchanged, only ownership moved.metal_graph_decode_test,first_token_full_test,prompt_logits_test,generate_metal_graph_raw_swa,ds4_engine_collect_imatrix) use a local scratch, same lifetime as their local graph.No behavior change with a single session: same tensors, same sizes, same execution order.
Testing
tests/two_sessions_smoke.c(+ Makefile target): loads one engine, runs two prompts isolated, then runs both again on two sessions of the same engine interleaved — one greedy token each, alternating on one thread — and asserts both token streams match the isolated runs exactly.MATCHfor both sessions, and the isolated outputs are byte-identical to a pre-refactor build.make cuda-genericclean; server smoke (greedy chat completion, stream and non-stream) byte-identical to the previous build.Metal note: the split is backend-agnostic (ownership only), but I could only run Metal compile-path checks indirectly — I don't have Apple hardware. The scratch tensors are allocated with the same
ds4_gpu_tensor_alloc()calls as before.