ferrum_engine/pipeline/
chunked_prefill.rs1use ferrum_interfaces::{
11 model_executor::{PrefillInput, PrefillOutput},
12 KvCacheHandle, ModelExecutor, TensorRef,
13};
14use ferrum_models::CandleTensorWrapper;
15use ferrum_types::{FerrumError, Result, TokenId};
16use std::sync::Arc;
17use tracing::{debug, info};
18
19#[derive(Debug, Clone)]
21pub struct ChunkedPrefillConfig {
22 pub chunk_size: usize,
24 pub min_sequence_for_chunking: usize,
26 pub enable_overlap: bool,
28 pub overlap_size: usize,
30}
31
32impl Default for ChunkedPrefillConfig {
33 fn default() -> Self {
34 Self {
35 chunk_size: 512,
36 min_sequence_for_chunking: 128,
37 enable_overlap: false,
38 overlap_size: 16,
39 }
40 }
41}
42
43#[derive(Debug)]
45pub struct ChunkedPrefillState {
46 pub tokens: Vec<TokenId>,
48 pub current_chunk: usize,
50 pub total_chunks: usize,
52 pub tokens_processed: usize,
54 pub kv_cache: Option<Arc<dyn KvCacheHandle>>,
56 pub config: ChunkedPrefillConfig,
58}
59
60impl ChunkedPrefillState {
61 pub fn new(tokens: Vec<TokenId>, config: ChunkedPrefillConfig) -> Self {
63 let chunk_size = config.chunk_size;
64 let total_chunks = if tokens.len() <= config.min_sequence_for_chunking {
65 1
66 } else {
67 tokens.len().div_ceil(chunk_size)
68 };
69
70 Self {
71 tokens,
72 current_chunk: 0,
73 total_chunks,
74 tokens_processed: 0,
75 kv_cache: None,
76 config,
77 }
78 }
79
80 pub fn needs_chunking(&self) -> bool {
82 self.tokens.len() > self.config.min_sequence_for_chunking
83 }
84
85 pub fn is_complete(&self) -> bool {
87 self.tokens_processed >= self.tokens.len()
88 }
89
90 pub fn next_chunk(&self) -> Option<&[TokenId]> {
92 if self.is_complete() {
93 return None;
94 }
95
96 let start = self.tokens_processed;
97 let end = (start + self.config.chunk_size).min(self.tokens.len());
98
99 if start < self.tokens.len() {
100 Some(&self.tokens[start..end])
101 } else {
102 None
103 }
104 }
105
106 pub fn advance(&mut self, tokens_processed: usize) {
108 self.tokens_processed += tokens_processed;
109 self.current_chunk += 1;
110 }
111
112 pub fn progress(&self) -> f32 {
114 if self.tokens.is_empty() {
115 1.0
116 } else {
117 self.tokens_processed as f32 / self.tokens.len() as f32
118 }
119 }
120}
121
122pub struct ChunkedPrefillExecutor {
124 config: ChunkedPrefillConfig,
126 model_executor: Arc<dyn ModelExecutor + Send + Sync>,
128}
129
130impl ChunkedPrefillExecutor {
131 pub fn new(
133 model_executor: Arc<dyn ModelExecutor + Send + Sync>,
134 config: ChunkedPrefillConfig,
135 ) -> Self {
136 info!(
137 "Creating ChunkedPrefillExecutor: chunk_size={}, min_seq={}",
138 config.chunk_size, config.min_sequence_for_chunking
139 );
140 Self {
141 config,
142 model_executor,
143 }
144 }
145
146 pub fn with_defaults(model_executor: Arc<dyn ModelExecutor + Send + Sync>) -> Self {
148 Self::new(model_executor, ChunkedPrefillConfig::default())
149 }
150
151 pub async fn execute(&self, tokens: Vec<TokenId>) -> Result<PrefillOutput> {
153 let mut state = ChunkedPrefillState::new(tokens, self.config.clone());
154
155 if !state.needs_chunking() {
156 debug!("Processing {} tokens as single chunk", state.tokens.len());
158 return self.process_single_chunk(&state.tokens).await;
159 }
160
161 debug!(
162 "Processing {} tokens in {} chunks",
163 state.tokens.len(),
164 state.total_chunks
165 );
166
167 let mut last_output: Option<PrefillOutput> = None;
168
169 while let Some(chunk) = state.next_chunk() {
170 let chunk_len = chunk.len();
171 debug!(
172 "Processing chunk {}/{}: {} tokens (progress: {:.1}%)",
173 state.current_chunk + 1,
174 state.total_chunks,
175 chunk_len,
176 state.progress() * 100.0
177 );
178
179 let output = if state.kv_cache.is_some() {
180 self.process_continuation_chunk(chunk, state.kv_cache.as_ref().unwrap().clone())
182 .await?
183 } else {
184 self.process_single_chunk(chunk).await?
186 };
187
188 state.kv_cache = Some(output.kv_cache.clone());
189 state.advance(chunk_len);
190 last_output = Some(output);
191 }
192
193 last_output.ok_or_else(|| FerrumError::internal("No output from chunked prefill"))
194 }
195
196 async fn process_single_chunk(&self, tokens: &[TokenId]) -> Result<PrefillOutput> {
198 let token_u32s: Vec<u32> = tokens.iter().map(|t| t.get()).collect();
200 let tensor = candle_core::Tensor::new(&token_u32s[..], &candle_core::Device::Cpu)
201 .map_err(|e| FerrumError::model(format!("Tensor error: {}", e)))?
202 .unsqueeze(0)
203 .map_err(|e| FerrumError::model(format!("Unsqueeze error: {}", e)))?;
204
205 let tensor_ref: TensorRef = Arc::new(CandleTensorWrapper::new(tensor));
206 let input = PrefillInput::new(tensor_ref);
207
208 self.model_executor.prefill(&input).await
209 }
210
211 async fn process_continuation_chunk(
213 &self,
214 tokens: &[TokenId],
215 _kv_cache: Arc<dyn KvCacheHandle>,
216 ) -> Result<PrefillOutput> {
217 let token_u32s: Vec<u32> = tokens.iter().map(|t| t.get()).collect();
223 let tensor = candle_core::Tensor::new(&token_u32s[..], &candle_core::Device::Cpu)
224 .map_err(|e| FerrumError::model(format!("Tensor error: {}", e)))?
225 .unsqueeze(0)
226 .map_err(|e| FerrumError::model(format!("Unsqueeze error: {}", e)))?;
227
228 let tensor_ref: TensorRef = Arc::new(CandleTensorWrapper::new(tensor));
229
230 let input = PrefillInput::new(tensor_ref);
233 let output = self.model_executor.prefill(&input).await?;
234
235 Ok(PrefillOutput::new(output.logits, output.kv_cache))
237 }
238
239 pub fn config(&self) -> &ChunkedPrefillConfig {
241 &self.config
242 }
243}
244
245impl std::fmt::Debug for ChunkedPrefillExecutor {
246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 f.debug_struct("ChunkedPrefillExecutor")
248 .field("chunk_size", &self.config.chunk_size)
249 .field(
250 "min_sequence_for_chunking",
251 &self.config.min_sequence_for_chunking,
252 )
253 .finish()
254 }
255}
256
257#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_chunked_state_creation() {
267 let tokens: Vec<TokenId> = (0..100).map(|i| TokenId::new(i as u32)).collect();
268 let config = ChunkedPrefillConfig {
269 chunk_size: 32,
270 min_sequence_for_chunking: 50,
271 ..Default::default()
272 };
273
274 let state = ChunkedPrefillState::new(tokens, config);
275
276 assert!(state.needs_chunking());
277 assert_eq!(state.total_chunks, 4); assert!(!state.is_complete());
279 }
280
281 #[test]
282 fn test_chunked_state_no_chunking() {
283 let tokens: Vec<TokenId> = (0..30).map(|i| TokenId::new(i as u32)).collect();
284 let config = ChunkedPrefillConfig {
285 chunk_size: 32,
286 min_sequence_for_chunking: 50,
287 ..Default::default()
288 };
289
290 let state = ChunkedPrefillState::new(tokens, config);
291
292 assert!(!state.needs_chunking());
293 assert_eq!(state.total_chunks, 1);
294 }
295
296 #[test]
297 fn test_chunked_state_iteration() {
298 let tokens: Vec<TokenId> = (0..100).map(|i| TokenId::new(i as u32)).collect();
299 let config = ChunkedPrefillConfig {
300 chunk_size: 32,
301 min_sequence_for_chunking: 50,
302 ..Default::default()
303 };
304
305 let mut state = ChunkedPrefillState::new(tokens, config);
306
307 let chunk1 = state.next_chunk().unwrap();
309 assert_eq!(chunk1.len(), 32);
310 assert_eq!(chunk1[0].get(), 0);
311 state.advance(32);
312
313 let chunk2 = state.next_chunk().unwrap();
315 assert_eq!(chunk2.len(), 32);
316 assert_eq!(chunk2[0].get(), 32);
317 state.advance(32);
318
319 let chunk3 = state.next_chunk().unwrap();
321 assert_eq!(chunk3.len(), 32);
322 state.advance(32);
323
324 let chunk4 = state.next_chunk().unwrap();
326 assert_eq!(chunk4.len(), 4);
327 state.advance(4);
328
329 assert!(state.is_complete());
331 assert!(state.next_chunk().is_none());
332 }
333
334 #[test]
335 fn test_progress() {
336 let tokens: Vec<TokenId> = (0..100).map(|i| TokenId::new(i as u32)).collect();
337 let config = ChunkedPrefillConfig::default();
338 let mut state = ChunkedPrefillState::new(tokens, config);
339
340 assert_eq!(state.progress(), 0.0);
341
342 state.advance(50);
343 assert!((state.progress() - 0.5).abs() < 0.01);
344
345 state.advance(50);
346 assert!((state.progress() - 1.0).abs() < 0.01);
347 }
348}