qualia_core_db/inference/runtime/scheduler/
request_table.rs1use crate::inference::runtime::graph_assist::PrefixIdentity;
2use crate::inference::runtime::kv::paged::{
3 BlockPool, CopyOnWrite, SequenceBlockTable, TableError,
4};
5use crate::inference::runtime::kv::prefix::{PrefixKvError, PrefixKvStore};
6
7use super::batch::{
8 RaggedBackendError, RaggedBatchItem, RaggedBatchOutput, RaggedBatchReceipt, RaggedDecodeBackend,
9};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum RequestState {
13 Empty,
14 Prefill,
15 Decode,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct Admission {
20 pub slot: u16,
21 pub prefix_hit: bool,
22 pub prefix_tokens: u32,
23}
24
25#[repr(C)]
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct RequestView {
28 pub request_id: u64,
29 pub slot: u16,
30 pub state: RequestState,
31 pub token_count: u32,
32 pub prefix_tokens: u32,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum SchedulerError {
37 DuplicateRequest,
38 Full,
39 UnknownRequest,
40 OutputTooSmall,
41 Prefix(PrefixKvError),
42 Table(TableError),
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum DecodeRoundError {
47 Scheduler(SchedulerError),
48 ItemBufferTooSmall,
49 BlockTableBufferTooSmall,
50 OutputBufferTooSmall,
51 MissingInputToken,
52 Backend(RaggedBackendError),
53 BackendLaunchCount,
54 OutputIdentityMismatch,
55}
56
57impl From<SchedulerError> for DecodeRoundError {
58 fn from(value: SchedulerError) -> Self {
59 Self::Scheduler(value)
60 }
61}
62
63impl From<RaggedBackendError> for DecodeRoundError {
64 fn from(value: RaggedBackendError) -> Self {
65 Self::Backend(value)
66 }
67}
68
69impl From<PrefixKvError> for SchedulerError {
70 fn from(value: PrefixKvError) -> Self {
71 Self::Prefix(value)
72 }
73}
74
75impl From<TableError> for SchedulerError {
76 fn from(value: TableError) -> Self {
77 Self::Table(value)
78 }
79}
80
81struct RequestSlot {
82 request_id: u64,
83 state: RequestState,
84 token_count: u32,
85 prefix_tokens: u32,
86 next_token_id: u32,
87 token_ready: bool,
88 blocks: SequenceBlockTable,
89}
90
91impl RequestSlot {
92 fn new(logical_pages: u32) -> Self {
93 Self {
94 request_id: 0,
95 state: RequestState::Empty,
96 token_count: 0,
97 prefix_tokens: 0,
98 next_token_id: 0,
99 token_ready: false,
100 blocks: SequenceBlockTable::new(logical_pages),
101 }
102 }
103
104 fn clear(&mut self) {
105 self.request_id = 0;
106 self.state = RequestState::Empty;
107 self.token_count = 0;
108 self.prefix_tokens = 0;
109 self.next_token_id = 0;
110 self.token_ready = false;
111 }
112}
113
114pub struct RequestScheduler<const REQUESTS: usize> {
116 slots: [RequestSlot; REQUESTS],
117}
118
119impl<const REQUESTS: usize> RequestScheduler<REQUESTS> {
120 pub fn new(logical_pages: u32) -> Self {
122 Self {
123 slots: std::array::from_fn(|_| RequestSlot::new(logical_pages)),
124 }
125 }
126
127 pub fn active_count(&self) -> usize {
128 self.slots
129 .iter()
130 .filter(|slot| slot.state != RequestState::Empty)
131 .count()
132 }
133
134 pub fn admit_with_prefix<const ENTRIES: usize, const PAGES: usize>(
136 &mut self,
137 request_id: u64,
138 identity: Option<PrefixIdentity>,
139 prefixes: &PrefixKvStore<ENTRIES, PAGES>,
140 pool: &mut BlockPool,
141 ) -> Result<Admission, SchedulerError> {
142 if self
143 .slots
144 .iter()
145 .any(|slot| slot.state != RequestState::Empty && slot.request_id == request_id)
146 {
147 return Err(SchedulerError::DuplicateRequest);
148 }
149 let index = self
150 .slots
151 .iter()
152 .position(|slot| slot.state == RequestState::Empty)
153 .ok_or(SchedulerError::Full)?;
154 let prefix_tokens = match identity {
155 Some(identity) => prefixes
156 .attach(identity, &mut self.slots[index].blocks, pool)?
157 .unwrap_or(0),
158 None => 0,
159 };
160 let slot = &mut self.slots[index];
161 slot.request_id = request_id;
162 slot.state = if prefix_tokens == 0 {
163 RequestState::Prefill
164 } else {
165 RequestState::Decode
166 };
167 slot.token_count = prefix_tokens;
168 slot.prefix_tokens = prefix_tokens;
169 Ok(Admission {
170 slot: index as u16,
171 prefix_hit: prefix_tokens != 0,
172 prefix_tokens,
173 })
174 }
175
176 pub fn ensure_writable(
179 &mut self,
180 request_id: u64,
181 logical_page: u32,
182 pool: &mut BlockPool,
183 ) -> Result<CopyOnWrite, SchedulerError> {
184 let slot = self.find_mut(request_id)?;
185 slot.blocks
186 .ensure_writable(logical_page, pool)
187 .map_err(Into::into)
188 }
189
190 pub fn mark_prefill_complete(&mut self, request_id: u64) -> Result<(), SchedulerError> {
191 self.find_mut(request_id)?.state = RequestState::Decode;
192 Ok(())
193 }
194
195 pub fn seed_decode_token(
197 &mut self,
198 request_id: u64,
199 token_id: u32,
200 ) -> Result<(), SchedulerError> {
201 let slot = self.find_mut(request_id)?;
202 slot.next_token_id = token_id;
203 slot.token_ready = true;
204 Ok(())
205 }
206
207 pub fn record_token(&mut self, request_id: u64) -> Result<u32, SchedulerError> {
208 let slot = self.find_mut(request_id)?;
209 slot.token_count = slot.token_count.saturating_add(1);
210 Ok(slot.token_count)
211 }
212
213 pub fn runnable_into(&self, out: &mut [RequestView]) -> Result<usize, SchedulerError> {
215 let required = self.active_count();
216 if out.len() < required {
217 return Err(SchedulerError::OutputTooSmall);
218 }
219 let mut written = 0usize;
220 for (index, slot) in self.slots.iter().enumerate() {
221 if slot.state == RequestState::Empty {
222 continue;
223 }
224 out[written] = RequestView {
225 request_id: slot.request_id,
226 slot: index as u16,
227 state: slot.state,
228 token_count: slot.token_count,
229 prefix_tokens: slot.prefix_tokens,
230 };
231 written += 1;
232 }
233 Ok(written)
234 }
235
236 pub fn execute_decode_round<B: RaggedDecodeBackend>(
242 &mut self,
243 backend: &mut B,
244 item_scratch: &mut [RaggedBatchItem],
245 block_table_scratch: &mut [u32],
246 output_scratch: &mut [RaggedBatchOutput],
247 ) -> Result<RaggedBatchReceipt, DecodeRoundError> {
248 let decode_count = self
249 .slots
250 .iter()
251 .filter(|slot| slot.state == RequestState::Decode)
252 .count();
253 if item_scratch.len() < decode_count {
254 return Err(DecodeRoundError::ItemBufferTooSmall);
255 }
256 if output_scratch.len() < decode_count {
257 return Err(DecodeRoundError::OutputBufferTooSmall);
258 }
259 let required_table_entries = self
260 .slots
261 .iter()
262 .filter(|slot| slot.state == RequestState::Decode)
263 .map(|slot| slot.blocks.entries().len())
264 .sum::<usize>();
265 if block_table_scratch.len() < required_table_entries {
266 return Err(DecodeRoundError::BlockTableBufferTooSmall);
267 }
268
269 let mut item_count = 0usize;
270 let mut table_offset = 0usize;
271 for (index, slot) in self.slots.iter().enumerate() {
272 if slot.state != RequestState::Decode {
273 continue;
274 }
275 if !slot.token_ready {
276 return Err(DecodeRoundError::MissingInputToken);
277 }
278 let table = slot.blocks.entries();
279 block_table_scratch[table_offset..table_offset + table.len()].copy_from_slice(table);
280 item_scratch[item_count] = RaggedBatchItem {
281 request_id: slot.request_id,
282 slot: index as u32,
283 token_id: slot.next_token_id,
284 position: slot.token_count,
285 block_table_offset: table_offset as u32,
286 logical_pages: table.len() as u32,
287 _reserved: 0,
288 };
289 item_count += 1;
290 table_offset += table.len();
291 }
292 if item_count == 0 {
293 return Ok(RaggedBatchReceipt::default());
294 }
295
296 let receipt = backend.execute_ragged(
297 &item_scratch[..item_count],
298 &block_table_scratch[..table_offset],
299 &mut output_scratch[..item_count],
300 )?;
301 if receipt.batch_size != item_count as u32 {
302 return Err(DecodeRoundError::OutputIdentityMismatch);
303 }
304 if receipt.backend_launches != 1 {
305 return Err(DecodeRoundError::BackendLaunchCount);
306 }
307 for (item, output) in item_scratch[..item_count]
308 .iter()
309 .zip(&output_scratch[..item_count])
310 {
311 if output.request_id != item.request_id || output.slot != item.slot {
312 return Err(DecodeRoundError::OutputIdentityMismatch);
313 }
314 }
315 for output in &output_scratch[..item_count] {
316 let slot = &mut self.slots[output.slot as usize];
317 slot.next_token_id = output.next_token_id;
318 slot.token_count = slot.token_count.saturating_add(1);
319 slot.token_ready = true;
320 }
321 Ok(receipt)
322 }
323
324 pub fn finish(&mut self, request_id: u64, pool: &mut BlockPool) -> Result<(), SchedulerError> {
326 let slot = self.find_mut(request_id)?;
327 slot.blocks.release_all(pool)?;
328 slot.clear();
329 Ok(())
330 }
331
332 fn find_mut(&mut self, request_id: u64) -> Result<&mut RequestSlot, SchedulerError> {
333 self.slots
334 .iter_mut()
335 .find(|slot| slot.state != RequestState::Empty && slot.request_id == request_id)
336 .ok_or(SchedulerError::UnknownRequest)
337 }
338}