metrale_model_engine/traits/model/forward.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: `ModelForward`, one of the supertraits `Model` is made of. Its methods, default
4//! bodies and docs are the ones `Model` declared before the split.
5//!
6//! Owner: model-engine.
7//! Invariants: none beyond the types.
8
9use super::{BeamReq, ModelStreams};
10use crate::traits::{MixedBatchResult, MixedForwardResult, PrefillSlice, SequenceState};
11use anyhow::{Result, bail};
12use metrale_gpu_runtime::gpu::DevicePtr;
13
14/// 2026-09-26: Prefill, decode and mixed forward passes, beam search, and the model-shape queries
15/// `is_mla`, `hc_mult` and `kv_block_size`.
16pub trait ModelForward: ModelStreams {
17 /// 2026-09-25: Whether this model implements [`Self::generate_beam_batch`]. Default `false`;
18 /// `NllbGpuModel` returns `true`.
19 fn supports_beam(&self) -> bool {
20 false
21 }
22
23 /// 2026-09-25: Run beam search to completion for each request and return each winning
24 /// hypothesis. The scheduler calls it at prefill for `num_beams > 1`, instead of the
25 /// token-by-token decode loop. Default: an error.
26 fn generate_beam_batch(&self, _reqs: &[BeamReq]) -> Result<Vec<Vec<u32>>> {
27 bail!("this model does not support beam search")
28 }
29
30 /// 2026-09-25: Prefill the whole prompt and return the logits of its last position.
31 fn prefill(&self, tokens: &[u32], seq: &mut SequenceState, stream: u64) -> Result<DevicePtr>;
32
33 /// 2026-09-25: Prefill `chunk_len` prompt tokens from `chunk_start`. Only a chunk with
34 /// `is_last_chunk` returns logits (of the last prompt position); any other chunk returns
35 /// `DevicePtr::NULL`.
36 fn prefill_chunk(
37 &self,
38 tokens: &[u32],
39 seq: &mut SequenceState,
40 chunk_start: usize,
41 chunk_len: usize,
42 is_last_chunk: bool,
43 stream: u64,
44 ) -> Result<DevicePtr>;
45
46 /// 2026-09-25: One decode step for one sequence; returns the logits for the next token.
47 fn decode(&self, token: u32, seq: &mut SequenceState, stream: u64) -> Result<DevicePtr>;
48
49 /// 2026-09-25: One decode step for each sequence; returns logits `[seqs.len(), vocab]`.
50 fn decode_batch(
51 &self,
52 tokens: &[u32],
53 seqs: &mut [&mut SequenceState],
54 stream: u64,
55 ) -> Result<DevicePtr>;
56
57 /// 2026-09-25: One decode step for each of `decode_seqs` plus one prefill chunk. Default:
58 /// `decode_batch` (skipped when there are no decode tokens), then `prefill_chunk`.
59 fn mixed_forward(
60 &self,
61 decode_tokens: &[u32],
62 decode_seqs: &mut [&mut SequenceState],
63 prefill_tokens: &[u32],
64 prefill_seq: &mut SequenceState,
65 prefill_chunk_start: usize,
66 prefill_chunk_len: usize,
67 prefill_is_last: bool,
68 stream: u64,
69 ) -> Result<MixedForwardResult> {
70 let decode_logits = if !decode_tokens.is_empty() {
71 self.decode_batch(decode_tokens, decode_seqs, stream)?
72 } else {
73 metrale_gpu_runtime::gpu::DevicePtr::NULL
74 };
75 let prefill_logits = self.prefill_chunk(
76 prefill_tokens,
77 prefill_seq,
78 prefill_chunk_start,
79 prefill_chunk_len,
80 prefill_is_last,
81 stream,
82 )?;
83 Ok(MixedForwardResult {
84 decode_logits,
85 prefill_logits,
86 })
87 }
88
89 /// 2026-09-25: Prefill one chunk for each of `streams`. Returns one entry per stream, in
90 /// order: the last-token logits when that stream's chunk is its last, else
91 /// `DevicePtr::NULL`. Default: `prefill_chunk` per stream.
92 fn prefill_batch_chunk(
93 &self,
94 streams: &mut [PrefillSlice<'_>],
95 stream: u64,
96 ) -> Result<Vec<DevicePtr>> {
97 let mut out = Vec::with_capacity(streams.len());
98 for slice in streams.iter_mut() {
99 let logits = self.prefill_chunk(
100 slice.prompt_tokens,
101 slice.seq,
102 slice.chunk_start,
103 slice.chunk_len,
104 slice.is_last_chunk,
105 stream,
106 )?;
107 out.push(logits);
108 }
109 Ok(out)
110 }
111
112 /// 2026-09-25: [`Self::prefill_batch_chunk`], with finishing stream `i`'s logits written to
113 /// row `row_base + i` of the shared logits arena instead of row `i`.
114 ///
115 /// `decode_batch` writes lane `i`'s logits to row `i` of the same arena, and in a mixed
116 /// step the caller samples the decode rows after the prefill has run. Without the shift a
117 /// finishing prefill stream overwrites a decode lane's logits, and that lane samples
118 /// another request's first-token distribution. `TransformerModel` falls back to
119 /// `row_base = 0` when `row_base + streams.len()` exceeds the arena's rows. Default: ignore
120 /// `row_base`.
121 fn prefill_batch_chunk_rows(
122 &self,
123 streams: &mut [PrefillSlice<'_>],
124 stream: u64,
125 _row_base: usize,
126 ) -> Result<Vec<DevicePtr>> {
127 self.prefill_batch_chunk(streams, stream)
128 }
129
130 /// 2026-09-25: One decode step for each of `decode_seqs` plus one prefill chunk for each of
131 /// `prefill_streams`. Default: `decode_batch`, a synchronise of the default stream, then
132 /// `prefill_batch_chunk_rows` with the prefill rows placed after the decode rows.
133 fn mixed_forward_batch(
134 &self,
135 decode_tokens: &[u32],
136 decode_seqs: &mut [&mut SequenceState],
137 prefill_streams: &mut [PrefillSlice<'_>],
138 stream: u64,
139 ) -> Result<MixedBatchResult> {
140 let decode_logits = if !decode_tokens.is_empty() {
141 let lg = self.decode_batch(decode_tokens, decode_seqs, stream)?;
142 // 2026-09-25: The batched prefill below reuses the decode pass's arena buffers on
143 // `stream`, so the decode work on the default stream must finish first.
144 self.synchronize(self.default_stream())?;
145 lg
146 } else {
147 metrale_gpu_runtime::gpu::DevicePtr::NULL
148 };
149 // 2026-09-25: Decode owns rows `0..decode_tokens.len()`, so the prefill rows start
150 // there (see `prefill_batch_chunk_rows`).
151 let prefill_logits =
152 self.prefill_batch_chunk_rows(prefill_streams, stream, decode_tokens.len())?;
153 Ok(MixedBatchResult {
154 decode_logits,
155 prefill_logits,
156 })
157 }
158
159 /// 2026-09-25: Normalise the sequence's SSM `h_state`. The scheduler calls it between
160 /// prefill chunks. Default: no-op.
161 fn normalize_ssm_states(&self, _seq: &SequenceState, _stream: u64) -> Result<()> {
162 Ok(())
163 }
164
165 /// 2026-09-25: Prefill the whole prompt in one call, with `chunk_size` as the chunk length
166 /// when an implementation splits it. Returns the last position's logits. Default: one
167 /// `prefill_chunk` over all of `tokens`.
168 fn prefill_twophase(
169 &self,
170 tokens: &[u32],
171 seq: &mut SequenceState,
172 _chunk_size: usize,
173 stream: u64,
174 ) -> Result<DevicePtr> {
175 self.prefill_chunk(tokens, seq, 0, tokens.len(), true, stream)
176 }
177
178 /// 2026-09-25: Whether chunked prefill must run as one chunk. `TransformerModel` answers
179 /// `requires_single_chunk_prefill`: MLA models (`kv_lora_rank > 0`) except `glm5_next` and
180 /// `kimi_k3`. Default `false`.
181 fn is_mla(&self) -> bool {
182 false
183 }
184
185 /// 2026-09-25: mHC hyper-connection stream count (`config.hc_mult` in `TransformerModel`);
186 /// `0` means no highway. Default `0`.
187 fn hc_mult(&self) -> usize {
188 0
189 }
190
191 /// 2026-09-25: Tokens per paged-KV block, or `None` without a paged KV cache. The scheduler
192 /// uses it to end a prefill chunk at `metrale_gpu_runtime::ssm_tail_boundary`. Default
193 /// `None`.
194 fn kv_block_size(&self) -> Option<usize> {
195 None
196 }
197
198 /// 2026-09-27: The token at which the prefill of `tokens` is split so that an SSM
199 /// snapshot lands at `prefill_plan::tail_split_point`, or `None` when this model does
200 /// not split it. The scheduler ends a non-last chunk there
201 /// (`prefill_plan::plan_chunk_len`). Default `None`.
202 fn prefill_tail_split(&self, _tokens: &[u32]) -> Option<usize> {
203 None
204 }
205}