Last updated: 2026-07-19

Inference Runtime

Forward Cuda

All API Sections

CUDA forward pass for the dense `qwen35` hybrid-SSM model (Qwen 3.5 9B).

M2 bring-up — incremental greedy decode of a single token on NVIDIA. The CUDA backend modules (device/buffer/pipeline/command) and the kernel library (`src/shaders/cuda/kernels.cu`, NVRTC-compiled) are orchestrated here into a real forward pass. Weights upload VERBATIM-quantized (Q4_K/Q5_K/Q6_K/Q8_0 blocks) via loader_cuda and are dequantized inside the DMMV kernels.

Layer schedule (qwen35, full_attention_interval=4): layer L is full attention when ((L+1) % 4 == 0) → L in {3,7,11,15,19,23,27,31}; the other 24 layers are gated-delta-net SSM. Every layer is followed by a SwiGLU FFN block.

1 exports 20 methods src/compute/forward_cuda.zig

1 exports shown

struct

ForwardCuda

#
pub const ForwardCuda = struct

Per-token GPU forward state for qwen35 greedy decode.

src/compute/forward_cuda.zig:504

Methods

20

method

ForwardCuda.init

#
pub fn init(allocator: std.mem.Allocator, model: *loader.Model, max_ctx: u32) !ForwardCuda

Compile kernels, allocate every device buffer, upload inv_freq + sinks, zero the KV cache and SSM state.

src/compute/forward_cuda.zig:763

method

ForwardCuda.allocSlotState

#
pub fn allocSlotState(self: *ForwardCuda, n_slots: u32, slot_ctx: u32) !void

Allocate per-sequence slot state for `n_slots` concurrent sequences, each with up to `slot_ctx` positions.

Attn layers get slot KV; ssm layers get per-slot conv + recurrent state (zero-initialised). Additive — production single-sequence buffers are untouched. Re-callable (frees first).

src/compute/forward_cuda.zig:1155

method

ForwardCuda.slotKvOffsetBytes

#
pub fn slotKvOffsetBytes(self: *const ForwardCuda, slot: u32, pos: u32) usize

Byte offset of slot `slot`'s K/V for position `pos` in attn layer `L`'s slot KV: (slot*slot_ctx + pos)*kv_dim*sizeof(f32).

The future per-seq kv-write + slot attention kernels (4b/4c) will use this exact indexing.

src/compute/forward_cuda.zig:1222

method

ForwardCuda.slotConvOffsetBytes

#
pub fn slotConvOffsetBytes(self: *const ForwardCuda, slot: u32) usize

Byte offset of slot `slot`'s conv ring (ssm layer): slot*conv_state_len*f4.

src/compute/forward_cuda.zig:1226

method

ForwardCuda.slotStateOffsetBytes

#
pub fn slotStateOffsetBytes(self: *const ForwardCuda, slot: u32) usize

Byte offset of slot `slot`'s recurrent state (ssm layer): slot*ssm_state_len*f4.

src/compute/forward_cuda.zig:1230

method

ForwardCuda.resetSlot

#
pub fn resetSlot(self: *ForwardCuda, slot: u32) !void

Effort 28 increment 4 (qwen serving): zero a REUSED slot's SSM conv ring + recurrent state so a new request prefilling from pos=0 into this slot starts from clean state.

The serving engine reuses slots across requests (nslots < concurrent clients) and qwen's SSM state is ACCUMULATED (not position-indexed), so without this a reused slot inherits the prior request's recurrent state → corruption. The attention slot KV needs NO reset (it is position-indexed and overwritten on prefill; reads never reach beyond the current sequence's positions — same reason gemma's slot KV reuses cleanly). No-op if slot state is unallocated. Call on ADMIT, before per-token prefill.

src/compute/forward_cuda.zig:1243

method

ForwardCuda.slotStateSmoke

#
pub fn slotStateSmoke(self: *ForwardCuda) !bool

Sub-step 4a plumbing smoke: prove the slot-offset arithmetic round-trips and that distinct slots map to NON-overlapping device regions for BOTH the attention KV and the SSM conv + recurrent state.

Requires allocSlotState with n_slots >= 2 (so slot 0 and slot n_slots-1 differ). Returns true on success.

src/compute/forward_cuda.zig:1293

method

ForwardCuda.resetState

#
pub fn resetState(self: *ForwardCuda) !void

Reset the production single-sequence recurrent state to a fresh-process start: zero every ssm layer's conv ring + recurrent state and clear the per-layer conv offset.

The attention KV cache needs no reset — it is position-indexed and each sequence overwrites from pos 0, reading only what it wrote. Used by the batch harness to make the per-sequence SERIAL reference truly single-sequence (without this, the unindexed SSM recurrent state leaks from one reference sequence into the next). Additive — never called on the real decode/prefill path; that path runs one sequence/process.

src/compute/forward_cuda.zig:1329

method

ForwardCuda.decodeStep

#
pub fn decodeStep(self: *ForwardCuda, token: u32, pos: u32, run_layers: bool) !u32

Run one greedy decode step for `token` at sequence position `pos`, returning the argmax token id.

v0: embed → final rms_norm → LM head → argmax (layers are gated by `run_layers`).

src/compute/forward_cuda.zig:1344

method

ForwardCuda.prefillStep

#
pub fn prefillStep(self: *ForwardCuda, token: u32, pos: u32) !void

Prefill helper: run every layer (build the KV cache) but SKIP the tail rms_norm + LM head + argmax.

A prompt-internal token's logits are never used — only the final prompt token's argmax seeds generation — so the (vocab x n_embd) head matvec is pure waste on T-1 of the T prompt tokens. It is a large share for the MoE models, whose active per-token forward is small next to the full-vocab head. The async layer ops are drained here (waitPending) so the ring is freed each token; bit-identical generation.

src/compute/forward_cuda.zig:1410

method

ForwardCuda.prefillBatchedSlot

#
pub fn prefillBatchedSlot(self: *ForwardCuda, tokens: []const u32, slot: u32) !u32

Batched prefill that writes KV cache + SSM state into the given slot's per-sequence region (instead of the single-seq scratch).

Used by the serve engine so server-mode prefill gets the 5× batched-GEMM speedup instead of falling back to per-token decodeBatch (B=1).

src/compute/forward_cuda.zig:1557

method

ForwardCuda.decodeBatch

#
pub fn decodeBatch(self: *ForwardCuda, tokens: []const u32, positions: []const u32, slots: []const u32, out: []u32) !void

No public doc comment yet.

src/compute/forward_cuda.zig:2232

method

ForwardCuda.readLogits

#
pub fn readLogits(self: *ForwardCuda, out: []f32) void

Download the top of the logits buffer (for top-k reporting).

src/compute/forward_cuda.zig:3633