metrale_model_engine/traits.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The `Model` trait, the per-sequence state it works on (`SequenceState`), and the
4//! input and result types of its forward passes.
5//!
6//! Owner: model-engine.
7//! Invariants: none beyond the types.
8
9use metrale_gpu_runtime::gpu::DevicePtr;
10
11use metrale_model_layers::layer::LayerState;
12use metrale_model_layers::speculative::ProposerState;
13
14/// 2026-09-25: Result of a mixed forward pass (decode rows plus one prefill chunk).
15pub struct MixedForwardResult {
16 /// 2026-09-25: Logits for the decode sequences, `[N, vocab_size]`; NULL when there are none.
17 pub decode_logits: DevicePtr,
18 /// 2026-09-25: Logits for the prefill sequence's last token, `[1, vocab_size]`; NULL unless
19 /// the chunk was its last.
20 pub prefill_logits: DevicePtr,
21}
22
23/// 2026-09-25: One prefilling stream's chunk. `prefill_batch_chunk` and `mixed_forward_batch`
24/// take a `&mut [PrefillSlice<'_>]` and run every stream's chunk in one forward pass.
25pub struct PrefillSlice<'a> {
26 /// 2026-09-25: The stream's full prompt.
27 pub prompt_tokens: &'a [u32],
28 pub seq: &'a mut SequenceState,
29 /// 2026-09-25: Offset into `prompt_tokens` where this chunk starts.
30 pub chunk_start: usize,
31 pub chunk_len: usize,
32 /// 2026-09-25: Whether this is the stream's final chunk, which produces its last-token logits.
33 pub is_last_chunk: bool,
34}
35
36/// 2026-09-25: Result of a batched mixed forward pass: M decode rows plus N prefill chunks.
37pub struct MixedBatchResult {
38 /// 2026-09-25: Logits for the decode rows, `[M, vocab]`; NULL when there are none.
39 pub decode_logits: DevicePtr,
40 /// 2026-09-25: One entry per prefill stream, in input order: `[1, vocab]` logits when that
41 /// stream's chunk was its last, NULL otherwise.
42 pub prefill_logits: Vec<DevicePtr>,
43}
44
45/// 2026-09-25: Per-sequence device copies of the paged-attention metadata for chunked prefill,
46/// kept across chunks so each chunk uploads only the block-table entries it added.
47pub struct ChunkedPrefillPageMetadata {
48 /// 2026-09-25: The block table as 32-bit entries.
49 pub block_table: DevicePtr,
50 /// 2026-09-25: The running sequence length, one `u32`.
51 pub seq_len: DevicePtr,
52 /// 2026-09-25: Block-table entries allocated, sized for the whole prompt.
53 pub block_capacity: usize,
54 /// 2026-09-25: Block-table entries already uploaded.
55 pub uploaded_blocks: usize,
56}
57
58/// 2026-09-25: One sequence's state across prefill and decode.
59pub struct SequenceState {
60 /// 2026-09-25: Prompt and generated token ids processed so far.
61 pub tokens: Vec<u32>,
62 /// 2026-09-25: Physical KV blocks of the paged cache, in logical order.
63 pub block_table: Vec<u32>,
64 pub seq_len: usize,
65 pub layer_states: Vec<Box<dyn LayerState>>,
66 /// 2026-09-25: The draft proposer's per-sequence state; `None` without a proposer.
67 pub proposer_state: Option<Box<dyn ProposerState>>,
68 /// 2026-09-25: Pool slot of this sequence, taken from `ssm_slot` at `alloc_sequence`.
69 /// `compact_sequence` moves it, and `detach_slot_for_reuse` sets it to `usize::MAX`.
70 pub slot_idx: usize,
71 /// 2026-09-25: Guard that returns the SSM pool slot to the free list on drop, so the slot is
72 /// released on every exit path, including abort and unwind. The explicit free and compact
73 /// paths `take()` or `migrate()` it so the slot is released once. `None` for models
74 /// without an SSM pool.
75 pub(crate) ssm_slot: Option<crate::model::ssm_pool::SlotGuard>,
76 /// 2026-09-25: Prompt position covered by the restored SSM snapshot and cached KV, set by the
77 /// chunk-0 prefix lookup and returned again when that lookup is replayed.
78 pub marconi_skip_to: usize,
79 /// 2026-09-25: Snapshot slot when the whole prompt matched a snapshot. The last prompt token
80 /// is then re-run for logits, which advances the SSM state a second time, so
81 /// `finalize_last` restores the state from this snapshot again and takes the first token's
82 /// hidden row from it. `None` otherwise.
83 pub marconi_exact_snap: Option<usize>,
84 /// 2026-09-25: Session identity, set by the scheduler before prefill. Saved SSM snapshots are
85 /// tagged with it and a snapshot is restored only for a matching tag
86 /// (`SsmSnapshotPool::session_matches`); `0` skips the check.
87 pub session_hash: u64,
88 /// 2026-10-04: Tenant for the scheduler's cross-request prompt-lookup cache, set by the
89 /// scheduler before prefill and carried across preemption and swap. `None` keeps the
90 /// sequence out of that cache (neither read nor written).
91 pub lookup_tenant: Option<u64>,
92 /// 2026-09-25: This sequence's claim on the single shared whole-prompt hidden capture
93 /// (`mtp_prefill_hidden`). `try_mtp_prefill_capture` sets it to a new capture generation when
94 /// it writes this sequence's rows from row 0. `ensure_drafter_context` prefills the drafter
95 /// only while it equals the model's current generation, since another sequence's chunk 0 may
96 /// have restarted the capture in between. `0` means it never owned a capture.
97 pub mtp_capture_gen: u64,
98 /// 2026-09-25: This sequence's ticket for the shared hidden-row interval (`mtp_store_range`),
99 /// drawn in `alloc_sequence` from the model's counter, and separate from `mtp_capture_gen`,
100 /// which is set only by a capture that starts at row 0. `0` means it was not drawn in
101 /// `alloc_sequence`, and matches no owner.
102 pub mtp_store_gen: u64,
103 /// 2026-09-25: Prefix-cache namespace of the sequence's LoRA adapter; `0` is the base model.
104 /// Each id has its own radix root, so adapters never reuse each other's blocks. The
105 /// scheduler sets it from `Model::adapter_id_for`.
106 pub adapter_id: u64,
107 /// 2026-09-25: Paged metadata for chunked prefill, allocated by the first chunk that needs it.
108 pub chunked_prefill_meta: Option<ChunkedPrefillPageMetadata>,
109 /// 2026-09-25: Prompt tokens matched by the chunk-0 prefix-cache lookup. The block-ref
110 /// accounting reads it (the release in `free_sequence`, the prefill-end cache insert). It is
111 /// not what clients are told: a match can be found and then recomputed
112 /// (`reused_prefix_tokens`).
113 pub cached_prefix_tokens: usize,
114 /// 2026-09-25: Prompt tokens whose KV this request read from the prefix cache instead of
115 /// recomputing (`prefix_reuse::reused_prefix_tokens`). The scheduler reports it as
116 /// `usage.prompt_tokens_details.cached_tokens`.
117 pub reused_prefix_tokens: usize,
118 /// 2026-09-25: Number of `block_table` entries that came from the prefix-cache lookup
119 /// (`matched_blocks.len()`); 0 without a hit.
120 pub cached_prefix_blocks: usize,
121 /// 2026-09-25: The matched prefix tokens (`tokens[..cached_prefix_tokens]`), stored at lookup.
122 /// When a prefill matched a prefix and then failed before `tokens` was filled,
123 /// `free_sequence` releases the lookup's radix refs over these instead. Empty when nothing
124 /// matched.
125 pub prefix_ref_tokens: Vec<u32>,
126 /// 2026-09-25: Whether the chunk-0 prefix lookup has run for this sequence.
127 ///
128 /// The lookup is not idempotent: it takes radix refs, `inc_ref`s each matched block and
129 /// pushes it onto `block_table`, all before the suffix is allocated. When that allocation
130 /// fails, the scheduler's preempt-and-retry re-enters chunk 0; with this flag set the lookup
131 /// replays its first result instead of pushing the blocks and taking the refs again.
132 pub prefix_lookup_applied: bool,
133 /// 2026-09-25: The `skip` result of the chunk-0 lookup, returned again on a replay.
134 pub prefix_lookup_skip: bool,
135 /// 2026-09-25: Token count of an SSM checkpoint near the prompt's end that the pool holds for
136 /// this sequence: saved during this prefill, or the one a warm prefill restored from. Cleared
137 /// at chunk 0; `finalize_last` reads it to decide whether the prompt-end leaf gets its own
138 /// snapshot (`prefill_b::exact_leaf`).
139 pub tail_checkpoint_tokens: Option<usize>,
140 /// 2026-09-25: Length of the prefix, from position 0, whose paged KV this sequence has written
141 /// or validly reused, updated per chunk by `prefill_b_proc_range`. The prefill-end cache
142 /// insert and checkpoint save cover at most `kv_valid_tokens / block_size` complete blocks, so
143 /// a block whose KV was never written is not cached.
144 pub kv_valid_tokens: usize,
145 /// 2026-09-25: Block index (`len / block_size`) of the latest decode-time SSM checkpoint, so a
146 /// boundary is not saved twice. Prefill sets it to the prompt's block count.
147 pub last_decode_ckpt_block: usize,
148 /// 2026-09-25: Prompt length, set by prefill. `cache_sequence` passes it to the prefix-cache
149 /// insert as `matched_tokens`, so the insert takes refs only for the generated tokens.
150 pub prompt_len: usize,
151 /// 2026-09-25: `--high-speed-swap` disk block ids, one per logical block the sequence has had,
152 /// in order. When the HBM window is at its cap, the oldest `block_table` entry is freed and
153 /// its disk id stays (`ensure_blocks_through_decode`), so `block_table` holds the newest
154 /// blocks and `hss_window_start()` is the logical index of `block_table[0]`. Empty without
155 /// `--high-speed-swap`.
156 pub disk_block_ids: Vec<u32>,
157 /// 2026-09-25: For each attention layer, how many `disk_block_ids` entries it has offloaded.
158 /// A block leaves the HBM window only when every layer has offloaded it
159 /// (`check_safe_to_evict`). `TransformerModel::alloc_sequence` sizes it to the attention-layer
160 /// count whether or not `--high-speed-swap` is on.
161 pub disk_last_offloaded_per_layer: Vec<u32>,
162 /// 2026-09-25: `Some(k)` makes prefill score every prompt position with its top-`k`
163 /// alternatives (`/v1/completions` with `echo` and `logprobs`). Set by the scheduler before
164 /// prefill. Such a request bypasses the prefix cache, so every position has a hidden row.
165 pub collect_prompt_logprobs: Option<u8>,
166 /// 2026-09-25: Filled across prefill chunks: one entry per prompt position `i` scoring
167 /// `tokens[i + 1]`. The last prompt position, whose target is the first generated token, has
168 /// no entry.
169 pub prompt_logprobs: Vec<PromptTokenLogprob>,
170 /// 2026-09-25: The LoRA pool slot this sequence's requests select (not `slot_idx`, which is
171 /// the KV/SSM pool slot); `-1` selects the installed active adapter. Set by the scheduler at
172 /// prefill; the prefill, decode and verify paths route LoRA by it (`moe_lora_route`,
173 /// `upload_seq_slot_uniform`).
174 pub adapter_slot: i32,
175 /// 2026-09-25: The resolved LoRA pool slot this sequence holds a ref on, as returned by
176 /// `Model::acquire_adapter_slot`; `-1` when none. `free_sequence` releases exactly this index
177 /// and resets it to `-1`, so a rotation of the active adapter between prefill and finish
178 /// cannot release a different slot.
179 pub acquired_adapter_slot: i32,
180 /// 2026-09-25: NLLB per-request source-language token id; `0` uses the deployment default.
181 pub src_lang_id: u32,
182 /// 2026-09-25: NLLB per-request target-language token id; `0` uses the deployment default.
183 pub tgt_lang_id: u32,
184 /// 2026-09-25: NLLB beam count; `1` turns beam search off.
185 pub num_beams: u32,
186 /// 2026-09-25: NLLB length penalty: a hypothesis scores `sum_logprob / len^length_penalty`.
187 pub length_penalty: f32,
188 /// 2026-09-25: NLLB beam search: `true` stops once `num_beams` hypotheses have finished;
189 /// `false` also waits until no running beam can beat the worst finished one.
190 pub early_stopping: bool,
191}
192
193impl SequenceState {
194 /// 2026-09-25: A host-only sequence state: no GPU resources, no SSM slot, no layer states,
195 /// every counter zeroed. NLLB's `alloc_sequence` and the test models build on it;
196 /// `TransformerModel::alloc_sequence` writes its own literal. Other crates can construct a
197 /// `SequenceState` only through this, because `ssm_slot` is crate-private.
198 pub fn host_only(slot_idx: usize) -> Self {
199 SequenceState {
200 tokens: Vec::new(),
201 block_table: Vec::new(),
202 seq_len: 0,
203 layer_states: Vec::new(),
204 proposer_state: None,
205 slot_idx,
206 ssm_slot: None,
207 marconi_skip_to: 0,
208 marconi_exact_snap: None,
209 session_hash: 0,
210 lookup_tenant: None,
211 mtp_capture_gen: 0,
212 mtp_store_gen: 0,
213 adapter_id: 0,
214 chunked_prefill_meta: None,
215 cached_prefix_tokens: 0,
216 reused_prefix_tokens: 0,
217 cached_prefix_blocks: 0,
218 prefix_ref_tokens: Vec::new(),
219 prefix_lookup_applied: false,
220 tail_checkpoint_tokens: None,
221 prefix_lookup_skip: false,
222 kv_valid_tokens: 0,
223 last_decode_ckpt_block: 0,
224 prompt_len: 0,
225 disk_block_ids: Vec::new(),
226 disk_last_offloaded_per_layer: Vec::new(),
227 collect_prompt_logprobs: None,
228 prompt_logprobs: Vec::new(),
229 adapter_slot: -1,
230 acquired_adapter_slot: -1,
231 src_lang_id: 0,
232 tgt_lang_id: 0,
233 num_beams: 1,
234 length_penalty: 1.0,
235 early_stopping: false,
236 }
237 }
238
239 /// 2026-09-25: The SSM pool slot this sequence holds, or `None` without one. The scheduler
240 /// sorts decode batches by it.
241 #[inline]
242 pub fn ssm_slot_idx(&self) -> Option<usize> {
243 self.ssm_slot.as_ref().and_then(|g| g.idx())
244 }
245
246 /// 2026-09-25: Logical block index of `block_table[0]`: `disk_block_ids.len()` minus
247 /// `block_table.len()`, saturating. 0 without `--high-speed-swap`, where `disk_block_ids` is
248 /// empty.
249 #[inline]
250 pub fn hss_window_start(&self) -> usize {
251 self.disk_block_ids
252 .len()
253 .saturating_sub(self.block_table.len())
254 }
255
256 /// 2026-09-25: Physical HBM block of a logical block index, or `None` when that block has left
257 /// the HBM window (or is past the end of `block_table`).
258 #[inline]
259 pub fn physical_block_for(&self, abs_block_idx: usize) -> Option<u32> {
260 let ws = self.hss_window_start();
261 if abs_block_idx < ws {
262 return None;
263 }
264 self.block_table.get(abs_block_idx - ws).copied()
265 }
266}
267
268mod logprobs;
269mod model;
270pub use logprobs::*;
271pub use model::{
272 BeamReq, DECODE_BATCH_LADDER, EpCommandFailed, FeedSource, ForwardDisclosure, ForwardSelect,
273 Model, ModelAdapters, ModelCircuit, ModelDeviceFeed, ModelDraft, ModelEp, ModelForward,
274 ModelLifecycle, ModelLogits, ModelSsmState, ModelStreams, ModelVerify, ModelVision,
275 PrefixReclaim, RowMask, VerifyBatchedOpts, padded_batch_n,
276};