1use alloc::vec::Vec;
6
7use thiserror::Error;
8
9#[derive(Debug, Error, PartialEq, Eq)]
10pub enum ServingFsmError {
11 #[error("serving fsm: {attempted} is not legal from the current state")]
14 IllegalTransition { attempted: &'static str },
15}
16
17#[derive(Debug, Clone, PartialEq)]
21pub enum ServingState<Entry, Cache> {
22 Prefill { positions: Vec<Entry>, cache: Cache },
26 Decode { last: Entry, cache: Cache },
29 Verify {
33 draft: Vec<Entry>,
34 snapshot: Cache,
35 cache: Cache,
36 },
37 Accept { n: usize, next: Entry, cache: Cache },
42 Rollback { snapshot: Cache, to: Entry },
45 Done { cache: Cache },
47}
48
49impl<Entry, Cache> ServingState<Entry, Cache> {
50 pub fn start(positions: Vec<Entry>, cache: Cache) -> Self {
52 Self::Prefill { positions, cache }
53 }
54
55 pub fn advance_prefill(self, next_token: Entry, cache: Cache) -> Result<Self, ServingFsmError> {
59 match self {
60 Self::Prefill { .. } => Ok(Self::Decode {
61 last: next_token,
62 cache,
63 }),
64 _ => Err(ServingFsmError::IllegalTransition {
65 attempted: "advance_prefill",
66 }),
67 }
68 }
69
70 pub fn advance_decode(self, next_token: Entry, cache: Cache) -> Result<Self, ServingFsmError> {
72 match self {
73 Self::Decode { .. } => Ok(Self::Decode {
74 last: next_token,
75 cache,
76 }),
77 _ => Err(ServingFsmError::IllegalTransition {
78 attempted: "advance_decode",
79 }),
80 }
81 }
82
83 pub fn enter_verify(self, draft: Vec<Entry>) -> Result<Self, ServingFsmError>
86 where
87 Cache: Clone,
88 {
89 match self {
90 Self::Decode { cache, .. } => Ok(Self::Verify {
91 draft,
92 snapshot: cache.clone(),
93 cache,
94 }),
95 _ => Err(ServingFsmError::IllegalTransition {
96 attempted: "enter_verify",
97 }),
98 }
99 }
100
101 pub fn resume(self) -> Result<Self, ServingFsmError> {
104 match self {
105 Self::Accept { next, cache, .. } => Ok(Self::Decode { last: next, cache }),
106 _ => Err(ServingFsmError::IllegalTransition {
107 attempted: "resume",
108 }),
109 }
110 }
111
112 pub fn rollback(self) -> Result<Self, ServingFsmError> {
116 match self {
117 Self::Rollback { snapshot, to } => Ok(Self::Decode {
118 last: to,
119 cache: snapshot,
120 }),
121 _ => Err(ServingFsmError::IllegalTransition {
122 attempted: "rollback",
123 }),
124 }
125 }
126
127 pub fn finish(self) -> Self {
130 let cache = match self {
131 Self::Prefill { cache, .. }
132 | Self::Decode { cache, .. }
133 | Self::Verify { cache, .. }
134 | Self::Accept { cache, .. }
135 | Self::Done { cache } => cache,
136 Self::Rollback { snapshot, .. } => snapshot,
137 };
138 Self::Done { cache }
139 }
140}
141
142impl<Entry: PartialEq + Clone, Cache> ServingState<Entry, Cache> {
143 pub fn accept(
156 self,
157 row_tokens: &[Entry],
158 row_caches: Vec<Cache>,
159 ) -> Result<Self, ServingFsmError> {
160 match self {
161 Self::Verify { draft, .. } => {
162 let accepted = draft
163 .iter()
164 .zip(row_tokens.iter())
165 .take_while(|(drafted, predicted)| drafted == predicted)
166 .count();
167 Self::settle(draft, accepted, row_tokens, row_caches, "accept")
168 }
169 _ => Err(ServingFsmError::IllegalTransition {
170 attempted: "accept",
171 }),
172 }
173 }
174
175 pub fn accept_rows(
182 self,
183 accepted: usize,
184 row_tokens: &[Entry],
185 row_caches: Vec<Cache>,
186 ) -> Result<Self, ServingFsmError> {
187 match self {
188 Self::Verify { draft, .. } => {
189 Self::settle(draft, accepted, row_tokens, row_caches, "accept_rows")
190 }
191 _ => Err(ServingFsmError::IllegalTransition {
192 attempted: "accept_rows",
193 }),
194 }
195 }
196
197 fn settle(
198 draft: Vec<Entry>,
199 accepted: usize,
200 row_tokens: &[Entry],
201 row_caches: Vec<Cache>,
202 attempted: &'static str,
203 ) -> Result<Self, ServingFsmError> {
204 if draft.is_empty()
205 || (accepted < draft.len() && row_tokens.len() <= accepted)
206 || row_caches.len() != draft.len()
207 || accepted > draft.len()
208 {
209 return Err(ServingFsmError::IllegalTransition { attempted });
210 }
211 let placement_index = if accepted == draft.len() {
212 accepted - 1
213 } else {
214 accepted
215 };
216 let Some(cache) = row_caches.into_iter().nth(placement_index) else {
217 return Err(ServingFsmError::IllegalTransition { attempted });
218 };
219 if accepted == draft.len() {
220 Ok(Self::Accept {
221 n: accepted,
222 next: draft[accepted - 1].clone(),
223 cache,
224 })
225 } else {
226 Ok(Self::Rollback {
227 snapshot: cache,
228 to: row_tokens[accepted].clone(),
229 })
230 }
231 }
232}
233
234#[cfg(test)]
236#[allow(clippy::unwrap_used, clippy::expect_used)]
237pub(crate) mod action_fixture;
238
239#[cfg(test)]
240#[allow(clippy::unwrap_used, clippy::expect_used)]
244mod tests {
245 use super::action_fixture::{Action, run_sequential, run_speculative};
246 use super::*;
247
248 #[test]
249 fn serving_fsm_error_names_the_attempted_transition() {
250 let accept = ServingFsmError::IllegalTransition {
251 attempted: "accept",
252 };
253 let resume = ServingFsmError::IllegalTransition {
254 attempted: "resume",
255 };
256
257 assert_eq!(
258 alloc::format!("{}", accept),
259 "serving fsm: accept is not legal from the current state"
260 );
261 assert_ne!(accept, resume);
262 }
263
264 #[test]
265 fn walk_prefill_decode_verify_finish() {
266 let prefilled = ServingState::start(alloc::vec![1_u32, 2], 0_u8);
267 let decoding = prefilled.advance_prefill(3, 1_u8).expect("legal");
268 let decoding = decoding.advance_decode(4, 2_u8).expect("legal");
269 let verifying = decoding.enter_verify(alloc::vec![5]).expect("legal");
270
271 assert_eq!(
272 verifying,
273 ServingState::Verify {
274 draft: alloc::vec![5],
275 snapshot: 2,
276 cache: 2
277 }
278 );
279 let done = verifying.finish();
280 assert_eq!(done, ServingState::Done { cache: 2 });
281 assert_eq!(
282 done.advance_decode(6, 3),
283 Err(ServingFsmError::IllegalTransition {
284 attempted: "advance_decode"
285 })
286 );
287 }
288
289 #[derive(Debug, Clone, PartialEq)]
293 struct FakeCache {
294 conv_history_len: usize,
295 kv_len: usize,
296 }
297
298 impl FakeCache {
299 fn empty() -> Self {
300 Self {
301 conv_history_len: 0,
302 kv_len: 0,
303 }
304 }
305
306 fn advanced_by(&self, rows: usize) -> Self {
307 Self {
308 conv_history_len: self.conv_history_len + rows,
309 kv_len: self.kv_len + rows,
310 }
311 }
312 }
313
314 #[test]
319 fn serving_state_walkthrough_drives_every_legal_transition() {
320 let prompt = alloc::vec![1_u32, 2, 3];
321 let prompt_rows = prompt.len();
322 let state = ServingState::start(prompt, FakeCache::empty());
323 assert_eq!(
324 state,
325 ServingState::Prefill {
326 positions: alloc::vec![1, 2, 3],
327 cache: FakeCache::empty(),
328 }
329 );
330
331 let after_prefill_cache = FakeCache::empty().advanced_by(prompt_rows);
332 let state = state
333 .advance_prefill(4, after_prefill_cache.clone())
334 .expect("prefill -> decode is legal");
335 assert_eq!(
336 state,
337 ServingState::Decode {
338 last: 4,
339 cache: after_prefill_cache.clone(),
340 }
341 );
342
343 let after_first_decode_cache = after_prefill_cache.advanced_by(1);
344 let state = state
345 .advance_decode(5, after_first_decode_cache.clone())
346 .expect("decode -> decode is legal");
347 assert_eq!(
348 state,
349 ServingState::Decode {
350 last: 5,
351 cache: after_first_decode_cache.clone(),
352 }
353 );
354
355 let illegal_prefill = state
356 .clone()
357 .advance_prefill(6, after_first_decode_cache.clone());
358 assert_eq!(
359 illegal_prefill,
360 Err(ServingFsmError::IllegalTransition {
361 attempted: "advance_prefill"
362 })
363 );
364
365 let draft = alloc::vec![6_u32, 7];
366 let state = state
367 .enter_verify(draft.clone())
368 .expect("decode -> verify is legal");
369 assert_eq!(
370 state,
371 ServingState::Verify {
372 draft,
373 snapshot: after_first_decode_cache.clone(),
374 cache: after_first_decode_cache.clone(),
375 }
376 );
377
378 let row_one_cache = after_first_decode_cache.advanced_by(1);
382 let row_two_cache = after_first_decode_cache.advanced_by(2);
383 let state = state
384 .accept(&[6, 7], alloc::vec![row_one_cache, row_two_cache.clone()])
385 .expect("verify -> accept is legal");
386 assert_eq!(
387 state,
388 ServingState::Accept {
389 n: 2,
390 next: 7,
391 cache: row_two_cache.clone(),
392 }
393 );
394 let state = state.resume().expect("accept -> decode is legal");
395 assert_eq!(
396 state,
397 ServingState::Decode {
398 last: 7,
399 cache: row_two_cache.clone()
400 }
401 );
402
403 let illegal_resume = state.clone().resume();
404 assert_eq!(
405 illegal_resume,
406 Err(ServingFsmError::IllegalTransition {
407 attempted: "resume"
408 })
409 );
410
411 let draft = alloc::vec![100_u32, 101];
418 let state = state
419 .enter_verify(draft)
420 .expect("decode -> verify is legal");
421 let cache_after_last = row_two_cache.advanced_by(1);
422 let cache_after_draft_zero = row_two_cache.advanced_by(2);
423 let state = state
424 .accept(
425 &[100, 55],
426 alloc::vec![cache_after_last, cache_after_draft_zero.clone()],
427 )
428 .expect("verify -> rollback is legal on partial acceptance");
429 assert_eq!(
430 state,
431 ServingState::Rollback {
432 snapshot: cache_after_draft_zero.clone(),
433 to: 55,
434 }
435 );
436 let state = state.rollback().expect("rollback -> decode is legal");
437 assert_eq!(
438 state,
439 ServingState::Decode {
440 last: 55,
441 cache: cache_after_draft_zero
442 }
443 );
444
445 let done = state.finish();
446 assert!(matches!(done, ServingState::Done { .. }));
447
448 let past_done = done.advance_decode(8, FakeCache::empty());
449 assert_eq!(
450 past_done,
451 Err(ServingFsmError::IllegalTransition {
452 attempted: "advance_decode"
453 })
454 );
455 }
456
457 #[test]
462 fn serving_state_accept_rejects_mismatched_or_empty_draft() {
463 let cache = FakeCache::empty();
464 let state = ServingState::start(alloc::vec![1], cache.clone())
465 .advance_prefill(2, cache.clone())
466 .expect("prefill -> decode")
467 .enter_verify(alloc::vec![3, 4])
468 .expect("decode -> verify");
469
470 let wrong_lengths = state.clone().accept(&[3], alloc::vec![cache.clone()]);
471 assert_eq!(
472 wrong_lengths,
473 Err(ServingFsmError::IllegalTransition {
474 attempted: "accept"
475 })
476 );
477
478 let empty_draft = ServingState::start(alloc::vec![1], cache.clone())
479 .advance_prefill(2, cache.clone())
480 .expect("prefill -> decode")
481 .enter_verify(Vec::new())
482 .expect("decode -> verify")
483 .accept(&[], Vec::new());
484 assert_eq!(
485 empty_draft,
486 Err(ServingFsmError::IllegalTransition {
487 attempted: "accept"
488 })
489 );
490 }
491
492 fn draft_prompt_lookup<Entry: PartialEq + Clone>(
500 history: &[Entry],
501 ngram: usize,
502 max_k: usize,
503 ) -> Vec<Entry> {
504 if ngram == 0 || max_k == 0 || history.len() <= ngram {
505 return Vec::new();
506 }
507 let needle = &history[history.len() - ngram..];
508 let search_end = history.len() - ngram;
509 for start in (0..search_end).rev() {
510 if &history[start..start + ngram] == needle {
511 let match_end = start + ngram;
512 let available = history.len() - match_end;
513 let take = available.min(max_k);
514 return history[match_end..match_end + take].to_vec();
515 }
516 }
517 Vec::new()
518 }
519
520 #[test]
528 fn serving_state_draft_prompt_lookup_finds_the_earlier_occurrence() {
529 let history = alloc::vec![5_u32, 1, 2, 3, 9, 1, 2, 3];
530 assert_eq!(draft_prompt_lookup(&history, 2, 5), alloc::vec![9, 1, 2, 3]);
531 assert_eq!(draft_prompt_lookup(&history, 2, 2), alloc::vec![9, 1]);
532 assert_eq!(draft_prompt_lookup(&history, 2, 0), Vec::<u32>::new());
533 assert_eq!(draft_prompt_lookup(&[1, 2], 2, 5), Vec::<u32>::new());
534 assert_eq!(draft_prompt_lookup(&[9, 9, 9], 1, 1), alloc::vec![9]);
535 }
536
537 fn fake_greedy_next(input: u32, vocab: u32) -> u32 {
544 (input + 1) % vocab
545 }
546
547 #[test]
555 fn serving_state_speculation_matches_plain_greedy_for_thirty_two_tokens() {
556 const VOCAB: u32 = 11;
562 const TOKENS: usize = 32;
563 const K: usize = 5;
564 const NGRAM: usize = 2;
565 let seed_prompt = alloc::vec![3_u32, 5];
566
567 let mut plain_history = seed_prompt.clone();
568 let mut plain_state = ServingState::start(seed_prompt.clone(), FakeCache::empty());
569 let mut last = *plain_history.last().expect("seed prompt is non-empty");
570 plain_state = plain_state
571 .advance_prefill(last, FakeCache::empty())
572 .expect("prefill -> decode");
573 for _ in 0..TOKENS {
574 let next = fake_greedy_next(last, VOCAB);
575 plain_state = plain_state
576 .advance_decode(next, FakeCache::empty())
577 .expect("decode -> decode");
578 plain_history.push(next);
579 last = next;
580 }
581
582 let mut spec_history = seed_prompt.clone();
583 let mut spec_state = ServingState::start(seed_prompt.clone(), FakeCache::empty());
584 let mut last = *spec_history.last().expect("seed prompt is non-empty");
585 spec_state = spec_state
586 .advance_prefill(last, FakeCache::empty())
587 .expect("prefill -> decode");
588 let mut verify_rounds = 0usize;
589 let mut draft_tokens_accepted = 0usize;
590 while spec_history.len() - seed_prompt.len() < TOKENS {
591 let draft = draft_prompt_lookup(&spec_history, NGRAM, K);
592 if draft.is_empty() {
593 let next = fake_greedy_next(last, VOCAB);
594 spec_state = spec_state
595 .advance_decode(next, FakeCache::empty())
596 .expect("decode -> decode");
597 spec_history.push(next);
598 last = next;
599 continue;
600 }
601 let mut row_tokens = Vec::with_capacity(draft.len());
602 let mut feed = last;
603 for &drafted in &draft {
604 row_tokens.push(fake_greedy_next(feed, VOCAB));
605 feed = drafted;
606 }
607 let row_caches = alloc::vec![FakeCache::empty(); draft.len()];
608 verify_rounds += 1;
609 let verifying = spec_state
610 .enter_verify(draft.clone())
611 .expect("decode -> verify");
612 let outcome = verifying
613 .accept(&row_tokens, row_caches)
614 .expect("verify -> accept | rollback");
615 spec_state = match outcome {
616 ServingState::Accept { n, next, cache } => {
617 draft_tokens_accepted += n;
618 for token in &draft {
619 spec_history.push(*token);
620 }
621 last = next;
622 ServingState::Accept { n, next, cache }
623 .resume()
624 .expect("accept -> decode")
625 }
626 ServingState::Rollback { snapshot, to } => {
627 let accepted = draft
628 .iter()
629 .zip(row_tokens.iter())
630 .take_while(|(drafted, predicted)| drafted == predicted)
631 .count();
632 for token in &draft[..accepted] {
633 spec_history.push(*token);
634 }
635 spec_history.push(to);
636 last = to;
637 ServingState::Rollback { snapshot, to }
638 .rollback()
639 .expect("rollback -> decode")
640 }
641 _ => unreachable!("accept only ever returns Accept or Rollback"),
642 };
643 spec_history
644 .truncate(seed_prompt.len() + TOKENS.min(spec_history.len() - seed_prompt.len()));
645 }
646
647 assert!(
648 verify_rounds > 0 && draft_tokens_accepted > 0,
649 "the small vocab must make draft_prompt_lookup find real matches -- a zero count here means \
650 this run degenerated to the empty-draft fallback and never exercised accept/resume at all"
651 );
652 assert_eq!(
653 spec_history, plain_history,
654 "speculative and plain greedy must produce identical token ids"
655 );
656 assert!(matches!(plain_state.finish(), ServingState::Done { .. }));
657 assert!(matches!(spec_state.finish(), ServingState::Done { .. }));
658 }
659
660 #[test]
661 fn accept_rows_takes_declared_count_not_prefix() {
662 let state = ServingState::start(alloc::vec![1_u32], FakeCache::empty())
663 .advance_prefill(2, FakeCache::empty().advanced_by(2))
664 .expect("prefill advances")
665 .enter_verify(alloc::vec![3, 4])
666 .expect("verify enters");
667 let cache_one = FakeCache::empty().advanced_by(1);
668 let cache_two = FakeCache::empty().advanced_by(2);
669 let caches = || alloc::vec![cache_one.clone(), cache_two.clone()];
670
671 assert_eq!(
672 state.clone().accept_rows(0, &[3, 4], caches()),
673 Ok(ServingState::Rollback {
674 snapshot: cache_one.clone(),
675 to: 3
676 })
677 );
678 assert_eq!(
679 state.clone().accept_rows(2, &[3, 4], caches()),
680 Ok(ServingState::Accept {
681 n: 2,
682 next: 4,
683 cache: cache_two.clone()
684 })
685 );
686 assert_eq!(
687 state.clone().accept_rows(3, &[3, 4], caches()),
688 Err(ServingFsmError::IllegalTransition {
689 attempted: "accept_rows"
690 })
691 );
692 assert_eq!(
693 ServingState::start(alloc::vec![1_u32], FakeCache::empty()).accept_rows(
694 0,
695 &[3, 4],
696 caches()
697 ),
698 Err(ServingFsmError::IllegalTransition {
699 attempted: "accept_rows"
700 })
701 );
702 }
703
704 #[test]
705 fn accept_rows_needs_tokens_only_up_to_the_first_differing_row() {
706 let state = ServingState::start(alloc::vec![1_u32], FakeCache::empty())
707 .advance_prefill(2, FakeCache::empty().advanced_by(2))
708 .expect("prefill advances")
709 .enter_verify(alloc::vec![3, 4, 5])
710 .expect("verify enters");
711 let caches = || {
712 alloc::vec![
713 FakeCache::empty().advanced_by(1),
714 FakeCache::empty().advanced_by(2),
715 FakeCache::empty().advanced_by(3),
716 ]
717 };
718
719 assert_eq!(
720 state.clone().accept_rows(1, &[3, 9], caches()),
721 Ok(ServingState::Rollback {
722 snapshot: FakeCache::empty().advanced_by(2),
723 to: 9
724 })
725 );
726 assert_eq!(
727 state.clone().accept_rows(3, &[], caches()),
728 Ok(ServingState::Accept {
729 n: 3,
730 next: 5,
731 cache: FakeCache::empty().advanced_by(3)
732 })
733 );
734 assert_eq!(
735 state.accept_rows(1, &[3], caches()),
736 Err(ServingFsmError::IllegalTransition {
737 attempted: "accept_rows"
738 })
739 );
740 }
741
742 #[test]
743 fn action_fixture_sequential_matches_hand_derivation() {
744 let expected = [
745 (0, 1, 0),
746 (1, 1, 1),
747 (2, 1, 2),
748 (0, 2, 3),
749 (1, 2, 4),
750 (2, 2, 5),
751 (0, 4, 6),
752 (1, 4, 7),
753 (2, 4, 8),
754 (0, 3, 9),
755 (1, 3, 10),
756 (2, 3, 11),
757 ]
758 .map(|(key, delta, version)| Action {
759 key,
760 delta,
761 version,
762 });
763
764 let trace = run_sequential();
765
766 assert_eq!(trace.history, expected);
767 assert_eq!(
768 trace.environment.values,
769 alloc::collections::BTreeMap::from([(0_u8, 10_i64), (1, 10), (2, 10)])
770 );
771 assert_eq!(trace.environment.version, 12);
772 }
773
774 fn leading_equal(draft: &[Action], choices: &[Action]) -> usize {
775 draft
776 .iter()
777 .zip(choices)
778 .take_while(|(drafted, chosen)| drafted == chosen)
779 .count()
780 }
781
782 fn equality_policy(draft: &[Action], choices: &[Action]) -> usize {
783 leading_equal(draft, choices)
784 }
785
786 fn verifier_exact_policy(draft: &[Action], choices: &[Action]) -> usize {
787 leading_equal(draft, choices)
788 }
789
790 fn anchor_macro_policy(draft: &[Action], choices: &[Action]) -> usize {
791 let aligned = leading_equal(draft, choices);
792 if aligned >= 3 { aligned } else { 0 }
793 }
794
795 #[test]
796 fn action_speculation_equality_matches_sequential() {
797 let sequential = run_sequential();
798 let speculative = run_speculative(equality_policy);
799
800 assert_eq!(speculative.history, sequential.history);
801 assert_eq!(speculative.environment, sequential.environment);
802 assert_eq!(
803 (
804 speculative.rounds,
805 speculative.rejected_rounds,
806 speculative.accepted_rows
807 ),
808 (4, 4, 8)
809 );
810 }
811
812 #[test]
813 fn action_speculation_verifier_exact_matches_sequential() {
814 let sequential = run_sequential();
815 let speculative = run_speculative(verifier_exact_policy);
816
817 assert_eq!(speculative.history, sequential.history);
818 assert_eq!(speculative.environment, sequential.environment);
819 assert_eq!(
820 (
821 speculative.rounds,
822 speculative.rejected_rounds,
823 speculative.accepted_rows
824 ),
825 (4, 4, 8)
826 );
827 }
828
829 #[test]
830 fn action_speculation_anchor_macro_matches_sequential() {
831 let sequential = run_sequential();
832 let speculative = run_speculative(anchor_macro_policy);
833
834 assert_eq!(speculative.history, sequential.history);
835 assert_eq!(speculative.environment, sequential.environment);
836 assert_eq!(
837 (
838 speculative.rounds,
839 speculative.rejected_rounds,
840 speculative.accepted_rows
841 ),
842 (6, 6, 6)
843 );
844 }
845}