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}