Last updated: 2026-07-19

CUDA Multi-Tenant Serving Engine

Cuda Serve

All API Sections

One GPU **worker thread** runs the continuous-batching loop (admit → prefill → `decodeBatch` → evict); many transport (HTTP) handler threads `submit` requests concurrently and **stream each request's own tokens incrementally** as the worker produces them. This is the threading model proven token-identical to N isolated single-sequence runs by the `dbg_cuda serve` harness (3a); 3b factors it into a reusable engine and adds the per-token streaming registry the HTTP/SSE transport needs.

ADDITIVE: the production single-sequence `decodeStep`/`prefillBatched` path is untouched. The engine REUSES `Scheduler` + `ForwardGemma.decodeBatch` (proven in increments 1+2); the only genuinely-new code here is the cross-thread result registry + the GPU worker loop.

Thread-safety model (identical to the 3a proof): * ALL GPU work (`decodeBatch`, prefill) runs ONLY on the worker thread. The CUDA shim rebinds the context per call (`cuCtxSetCurrent` at every entry), so a single GPU-owning thread needs no extra ceremony. * Cross-thread mutable state = the scheduler `pending` FIFO (handlers append via `enqueue`; worker drains via `admitNext`) + the per-request channel registry. Both are guarded by ONE `mutex`. Slot state (prefill / decode / append / release) and the scratch slices are worker-only and lock-free. * Each sequence's tokens depend only on its own slot KV + position (proven isolated in increment 1), so the nondeterministic admit/interleave ORDER across handler threads cannot change any sequence's output.

4 exports 8 methods src/server/cuda_serve.zig

4 exports shown

union

Forward

#
pub const Forward = union(enum)

Architecture-dispatched GPU forward held by the serving engine.

gemma4 dense (`ForwardGemma`) and the qwen35/36 hybrid-SSM family (`ForwardCuda`) expose the SAME batched serving primitives — `decodeBatch(tokens,positions,slots,out)` and per-sequence slot-state alloc — only under different method names, so the engine drives EITHER through this thin union. Effort 28 increment 4 (qwen serving): the Scheduler / ReqChannel / worker threading are model-agnostic; only these calls differ. The forward is owned by the caller (its frame outlives the engine).

src/server/cuda_serve.zig:42

struct

ReqChannel

#
pub const ReqChannel = struct

Per-request published-token channel.

The handler thread that submitted the request OWNS this struct (it lives on the handler's stack/frame); the worker appends generated tokens + flips `done`/`failed` under the engine mutex and broadcasts. Registered by request id in `ServeEngine.registry`. ALL fields are touched only under `ServeEngine.mutex` (the worker may realloc `tokens` while the handler drains it, so the handler must copy out under the same lock).

src/server/cuda_serve.zig:89

struct

Chunk

#
pub const Chunk = struct

Result of `nextChunk`: how many fresh tokens were copied into the caller's buffer and whether the stream is now fully drained.

src/server/cuda_serve.zig:102

struct

ServeEngine

#
pub const ServeEngine = struct

No public doc comment yet.

src/server/cuda_serve.zig:108

Methods

8

method

ServeEngine.init

#
pub fn init( allocator: std.mem.Allocator, fwd: Forward, nslots: u32, slot_ctx: u32, eos: u32, ) !ServeEngine

Allocate slot-based per-sequence state + a scheduler with `nslots` concurrent slots, each `slot_ctx` tokens deep.

The forward (`fwd`) must already be initialized; it may be EITHER a gemma or qwen forward (dispatched by `Forward`).

src/server/cuda_serve.zig:139

method

ServeEngine.shutdown

#
pub fn shutdown(self: *ServeEngine) void

Signal the worker to drain outstanding work and exit, then join it.

src/server/cuda_serve.zig:165

method

ServeEngine.submit

#
pub fn submit(self: *ServeEngine, prompt_tokens: []const u32, max_tokens: u32, chan: *ReqChannel) !u64

Submit a request: register its channel, enqueue the prompt, wake the worker.

The caller MUST keep `prompt_tokens` alive until the request finishes (the `Request` borrows the slice), and call `finish(id)` afterward.

Returns

the request id (used to wait/finish).

src/server/cuda_serve.zig:178

method

ServeEngine.nextChunk

#
pub fn nextChunk(self: *ServeEngine, chan: *ReqChannel, dst: []u32) Chunk

Block until fresh tokens are available for `chan` or it finishes, then copy up to `dst.len` newly-generated tokens into `dst` (all under the lock, since the worker may realloc `chan.tokens` concurrently).

Returns the count copied and whether the stream is now fully drained. Loop calling this until `finished`.

src/server/cuda_serve.zig:192

method

ServeEngine.finish

#
pub fn finish(self: *ServeEngine, id: u64, chan: *ReqChannel) void

Deregister + free a finished request's channel.

Call once `done`/`failed` has been observed and all tokens drained.

src/server/cuda_serve.zig:212

method

ServeEngine.statsJson

#
pub fn statsJson(self: *ServeEngine, buf: []u8) ![]const u8

Write the cumulative throughput counters as JSON into `buf`.

Lock-free (atomic loads) so `/stats` never contends the worker. The gate diffs two snapshots around each B-concurrent phase → pure decode tok/s + occupancy.

src/server/cuda_serve.zig:222