1use std::marker::PhantomData;
4
5use eredu_nn::{NeuralBackend, Tensor};
6
7use crate::{
8 layered::{LayeredTraversalHook, LayeredTraversalPoint, LayeredUnitAction},
9 Sampler, SamplingBackend, TokenDomain,
10};
11
12#[derive(Debug, Clone, Copy, Eq, PartialEq)]
14pub enum SequentialDecisionMode {
15 TeacherForced,
17 Autoregressive,
19 PartiallyForced,
21}
22
23#[derive(Debug, Clone, Eq, PartialEq)]
25pub enum PredictionDirective<T> {
26 Sample,
28 Force(T),
30}
31
32#[derive(Debug, Clone, Eq, PartialEq)]
34pub struct SequentialDecisionPlan<T> {
35 directives: Vec<PredictionDirective<T>>,
36 retain_diagnostics: bool,
37 allow_fully_forced_tail_skip: bool,
38}
39
40impl<T> SequentialDecisionPlan<T> {
41 pub fn new(
43 directives: impl IntoIterator<Item = PredictionDirective<T>>,
44 retain_diagnostics: bool,
45 allow_fully_forced_tail_skip: bool,
46 ) -> Result<Self, SequentialDecisionPlanError> {
47 let directives = directives.into_iter().collect::<Vec<_>>();
48 if directives.is_empty() {
49 return Err(SequentialDecisionPlanError::EmptyPlan);
50 }
51 Ok(Self {
52 directives,
53 retain_diagnostics,
54 allow_fully_forced_tail_skip,
55 })
56 }
57
58 pub fn len(&self) -> usize {
60 self.directives.len()
61 }
62
63 pub fn is_empty(&self) -> bool {
65 self.directives.is_empty()
66 }
67
68 pub fn mode(&self) -> SequentialDecisionMode {
70 let forced = self
71 .directives
72 .iter()
73 .filter(|directive| matches!(directive, PredictionDirective::Force(_)))
74 .count();
75 match forced {
76 0 => SequentialDecisionMode::Autoregressive,
77 count if count == self.directives.len() => SequentialDecisionMode::TeacherForced,
78 _ => SequentialDecisionMode::PartiallyForced,
79 }
80 }
81
82 pub fn forcing_mask(&self) -> impl ExactSizeIterator<Item = bool> + '_ {
84 self.directives
85 .iter()
86 .map(|directive| matches!(directive, PredictionDirective::Force(_)))
87 }
88
89 pub const fn retains_diagnostics(&self) -> bool {
91 self.retain_diagnostics
92 }
93
94 pub const fn allows_fully_forced_tail_skip(&self) -> bool {
96 self.allow_fully_forced_tail_skip
97 }
98}
99
100#[derive(Debug, Clone, Copy, Eq, PartialEq)]
102pub enum SequentialDecisionSource {
103 Forced,
105 Sampled,
107 ForcedTailSkipped,
109}
110
111#[derive(Debug, Clone, Eq, PartialEq)]
113pub struct SequentialDecision<T> {
114 prediction: usize,
115 source: SequentialDecisionSource,
116 token: T,
117}
118
119impl<T> SequentialDecision<T> {
120 pub const fn prediction(&self) -> usize {
122 self.prediction
123 }
124
125 pub const fn source(&self) -> SequentialDecisionSource {
127 self.source
128 }
129
130 pub const fn token(&self) -> &T {
132 &self.token
133 }
134}
135
136#[derive(Debug, Clone, Eq, PartialEq)]
138pub struct SequentialDecisionDiagnostic<L> {
139 prediction: usize,
140 logits: L,
141}
142
143impl<L> SequentialDecisionDiagnostic<L> {
144 pub const fn prediction(&self) -> usize {
146 self.prediction
147 }
148
149 pub const fn logits(&self) -> &L {
151 &self.logits
152 }
153}
154
155#[derive(Debug, Clone, Copy, Eq, PartialEq)]
157pub enum FullyForcedTailDecision {
158 Execute,
160 Skip {
162 predictions: usize,
164 },
165}
166
167pub struct SequentialDecisionDriver<B, S>
172where
173 B: SamplingBackend,
174 S: Sampler<B>,
175{
176 plan: SequentialDecisionPlan<B::Token>,
177 samplers: Vec<S>,
178 temperatures: Vec<f32>,
179 random: Option<B::RandomState>,
180 decisions: Vec<SequentialDecision<B::Token>>,
181 diagnostics: Vec<SequentialDecisionDiagnostic<B::Logits>>,
182}
183
184pub type SequentialSamplingState<S, R> = (Vec<S>, Option<R>);
186
187impl<B, S> SequentialDecisionDriver<B, S>
188where
189 B: SamplingBackend,
190 S: Sampler<B>,
191{
192 pub fn new(
194 plan: SequentialDecisionPlan<B::Token>,
195 samplers: Vec<S>,
196 temperatures: Vec<f32>,
197 random: Option<B::RandomState>,
198 ) -> Result<Self, SequentialDecisionPlanError> {
199 if samplers.len() != plan.len() {
200 return Err(SequentialDecisionPlanError::SamplerCountMismatch {
201 predictions: plan.len(),
202 samplers: samplers.len(),
203 });
204 }
205 if temperatures.len() != plan.len() {
206 return Err(SequentialDecisionPlanError::TemperatureCountMismatch {
207 predictions: plan.len(),
208 temperatures: temperatures.len(),
209 });
210 }
211 if let Some((prediction, temperature)) = temperatures
212 .iter()
213 .copied()
214 .enumerate()
215 .find(|(_, temperature)| !temperature.is_finite() || *temperature < 0.0)
216 {
217 return Err(SequentialDecisionPlanError::InvalidTemperature {
218 prediction,
219 bits: temperature.to_bits(),
220 });
221 }
222 Ok(Self {
223 plan,
224 samplers,
225 temperatures,
226 random,
227 decisions: Vec::new(),
228 diagnostics: Vec::new(),
229 })
230 }
231
232 pub const fn plan(&self) -> &SequentialDecisionPlan<B::Token> {
234 &self.plan
235 }
236
237 pub fn next_prediction(&self) -> usize {
239 self.decisions.len()
240 }
241
242 pub fn decisions(&self) -> &[SequentialDecision<B::Token>] {
244 &self.decisions
245 }
246
247 pub fn diagnostics(&self) -> &[SequentialDecisionDiagnostic<B::Logits>] {
249 &self.diagnostics
250 }
251
252 pub const fn random_state(&self) -> Option<&B::RandomState> {
254 self.random.as_ref()
255 }
256
257 pub fn fully_forced_tail_decision(
259 &self,
260 prediction: usize,
261 remaining_units: usize,
262 ) -> Result<FullyForcedTailDecision, SequentialDecisionError<B::Error>> {
263 self.require_next(prediction)?;
264 let remaining_predictions = self.plan.len().saturating_sub(prediction);
265 if !self.plan.allow_fully_forced_tail_skip
266 || self.plan.retain_diagnostics
267 || remaining_predictions != remaining_units
268 || !self.plan.directives[prediction..]
269 .iter()
270 .all(|directive| matches!(directive, PredictionDirective::Force(_)))
271 {
272 return Ok(FullyForcedTailDecision::Execute);
273 }
274 Ok(FullyForcedTailDecision::Skip {
275 predictions: remaining_predictions,
276 })
277 }
278
279 pub fn forced_tail_tokens(
281 &self,
282 prediction: usize,
283 count: usize,
284 domains: impl IntoIterator<Item = TokenDomain>,
285 context: &B::Context,
286 ) -> Result<Vec<B::Token>, SequentialDecisionError<B::Error>> {
287 self.require_next(prediction)?;
288 if self.fully_forced_tail_decision(prediction, count)?
289 != (FullyForcedTailDecision::Skip { predictions: count })
290 {
291 return Err(SequentialDecisionError::InvalidTailSkip { prediction, count });
292 }
293 let domains = domains.into_iter().collect::<Vec<_>>();
294 if domains.len() != count {
295 return Err(SequentialDecisionError::TokenDomainCountMismatch {
296 prediction,
297 expected: count,
298 actual: domains.len(),
299 });
300 }
301 self.plan.directives[prediction..prediction + count]
302 .iter()
303 .zip(domains)
304 .map(|(directive, domain)| match directive {
305 PredictionDirective::Force(token) => B::validate_token(token, domain, context)
306 .map_err(SequentialDecisionError::Backend),
307 PredictionDirective::Sample => unreachable!("tail was proven fully forced"),
308 })
309 .collect()
310 }
311
312 pub fn commit_forced_tail(
314 &mut self,
315 prediction: usize,
316 tokens: Vec<B::Token>,
317 ) -> Result<(), SequentialDecisionError<B::Error>> {
318 self.require_next(prediction)?;
319 let count = tokens.len();
320 if self.fully_forced_tail_decision(prediction, count)?
321 != (FullyForcedTailDecision::Skip { predictions: count })
322 {
323 return Err(SequentialDecisionError::InvalidTailSkip { prediction, count });
324 }
325 self.decisions
326 .extend(
327 tokens
328 .into_iter()
329 .enumerate()
330 .map(|(offset, token)| SequentialDecision {
331 prediction: prediction + offset,
332 source: SequentialDecisionSource::ForcedTailSkipped,
333 token,
334 }),
335 );
336 Ok(())
337 }
338
339 pub fn resolve(
341 &mut self,
342 prediction: usize,
343 logits: &B::Logits,
344 domain: TokenDomain,
345 context: &B::Context,
346 ) -> Result<B::Token, SequentialDecisionError<B::Error>> {
347 self.require_next(prediction)?;
348 let (token, source) = match &self.plan.directives[prediction] {
349 PredictionDirective::Force(token) => (token.clone(), SequentialDecisionSource::Forced),
350 PredictionDirective::Sample => (
351 self.samplers[prediction]
352 .sample(
353 logits,
354 self.temperatures[prediction],
355 self.random.as_mut(),
356 context,
357 )
358 .map_err(SequentialDecisionError::Backend)?,
359 SequentialDecisionSource::Sampled,
360 ),
361 };
362 let token =
363 B::validate_token(&token, domain, context).map_err(SequentialDecisionError::Backend)?;
364 if self.plan.retain_diagnostics {
365 self.diagnostics.push(SequentialDecisionDiagnostic {
366 prediction,
367 logits: logits.clone(),
368 });
369 }
370 self.decisions.push(SequentialDecision {
371 prediction,
372 source,
373 token: token.clone(),
374 });
375 Ok(token)
376 }
377
378 pub fn finish(&self) -> Result<(), SequentialDecisionError<B::Error>> {
380 if self.decisions.len() != self.plan.len() {
381 return Err(SequentialDecisionError::Incomplete {
382 resolved: self.decisions.len(),
383 predictions: self.plan.len(),
384 });
385 }
386 Ok(())
387 }
388
389 pub fn finish_into_sampling_state(
396 self,
397 ) -> Result<SequentialSamplingState<S, B::RandomState>, SequentialDecisionError<B::Error>> {
398 self.finish()?;
399 Ok((self.samplers, self.random))
400 }
401
402 fn require_next(&self, prediction: usize) -> Result<(), SequentialDecisionError<B::Error>> {
403 if prediction != self.decisions.len() || prediction >= self.plan.len() {
404 return Err(SequentialDecisionError::OutOfOrder {
405 expected: self.decisions.len(),
406 actual: prediction,
407 predictions: self.plan.len(),
408 });
409 }
410 Ok(())
411 }
412}
413
414pub trait SequentialDecisionBoundary<B, C, E>
416where
417 B: SamplingBackend,
418{
419 fn prediction_at(&self, point: LayeredTraversalPoint, forward: &C) -> Option<usize>;
421
422 fn logits(
424 &mut self,
425 prediction: usize,
426 point: LayeredTraversalPoint,
427 value: &B::Logits,
428 forward: &mut C,
429 context: &B::Context,
430 ) -> Result<B::Logits, E>;
431
432 fn token_domain(
434 &mut self,
435 prediction: usize,
436 point: LayeredTraversalPoint,
437 forward: &C,
438 ) -> Result<TokenDomain, E>;
439
440 fn accept(
442 &mut self,
443 prediction: usize,
444 point: LayeredTraversalPoint,
445 token: &B::Token,
446 forward: &mut C,
447 context: &B::Context,
448 ) -> Result<(), E>;
449
450 fn decision_error(&mut self, error: SequentialDecisionError<B::Error>) -> E;
452}
453
454pub struct SequentialDecisionTraversal<'a, B, S, D, C, E>
456where
457 B: SamplingBackend,
458 S: Sampler<B>,
459 D: SequentialDecisionBoundary<B, C, E>,
460{
461 driver: &'a mut SequentialDecisionDriver<B, S>,
462 boundary: &'a mut D,
463 marker: PhantomData<fn(C) -> E>,
464}
465
466impl<'a, B, S, D, C, E> SequentialDecisionTraversal<'a, B, S, D, C, E>
467where
468 B: SamplingBackend,
469 S: Sampler<B>,
470 D: SequentialDecisionBoundary<B, C, E>,
471{
472 pub fn new(driver: &'a mut SequentialDecisionDriver<B, S>, boundary: &'a mut D) -> Self {
474 Self {
475 driver,
476 boundary,
477 marker: PhantomData,
478 }
479 }
480
481 fn process(
482 &mut self,
483 point: LayeredTraversalPoint,
484 value: &B::Logits,
485 forward: &mut C,
486 context: &B::Context,
487 ) -> Result<(), E> {
488 let Some(prediction) = self.boundary.prediction_at(point, forward) else {
489 return Ok(());
490 };
491 let logits = self
492 .boundary
493 .logits(prediction, point, value, forward, context)?;
494 let domain = self.boundary.token_domain(prediction, point, forward)?;
495 let token = self
496 .driver
497 .resolve(prediction, &logits, domain, context)
498 .map_err(|error| self.boundary.decision_error(error))?;
499 self.boundary
500 .accept(prediction, point, &token, forward, context)
501 }
502}
503
504impl<NB, B, S, D, C, E> LayeredTraversalHook<NB, C, E>
505 for SequentialDecisionTraversal<'_, B, S, D, C, E>
506where
507 NB: NeuralBackend,
508 B: SamplingBackend<Logits = NB::Tensor, Context = <NB::Tensor as Tensor>::Context>,
509 S: Sampler<B>,
510 D: SequentialDecisionBoundary<B, C, E>,
511{
512 fn before_unit(
513 &mut self,
514 group: usize,
515 index: usize,
516 remaining_units: usize,
517 _value: &NB::Tensor,
518 forward: &mut C,
519 context: &<NB::Tensor as Tensor>::Context,
520 ) -> Result<LayeredUnitAction, E> {
521 let point = LayeredTraversalPoint::Unit { group, index };
522 let Some(prediction) = self.boundary.prediction_at(point, forward) else {
523 return Ok(LayeredUnitAction::Execute);
524 };
525 let tail = self
526 .driver
527 .fully_forced_tail_decision(prediction, remaining_units)
528 .map_err(|error| self.boundary.decision_error(error))?;
529 let FullyForcedTailDecision::Skip { predictions } = tail else {
530 return Ok(LayeredUnitAction::Execute);
531 };
532 let mut domains = Vec::with_capacity(predictions);
533 for offset in 0..predictions {
534 let skipped_point = LayeredTraversalPoint::Unit {
535 group,
536 index: index + offset,
537 };
538 let actual = self.boundary.prediction_at(skipped_point, forward);
539 if actual != Some(prediction + offset) {
540 let error = SequentialDecisionError::TailBoundaryMismatch {
541 expected: prediction + offset,
542 actual,
543 };
544 return Err(self.boundary.decision_error(error));
545 }
546 domains.push(self.boundary.token_domain(
547 prediction + offset,
548 skipped_point,
549 forward,
550 )?);
551 }
552 let tokens = self
553 .driver
554 .forced_tail_tokens(prediction, predictions, domains, context)
555 .map_err(|error| self.boundary.decision_error(error))?;
556 for (offset, token) in tokens.iter().enumerate() {
557 let skipped_point = LayeredTraversalPoint::Unit {
558 group,
559 index: index + offset,
560 };
561 self.boundary
562 .accept(prediction + offset, skipped_point, token, forward, context)?;
563 }
564 self.driver
565 .commit_forced_tail(prediction, tokens)
566 .map_err(|error| self.boundary.decision_error(error))?;
567 Ok(LayeredUnitAction::SkipRemainingGroup)
568 }
569
570 fn after_unit(
571 &mut self,
572 group: usize,
573 index: usize,
574 value: &NB::Tensor,
575 forward: &mut C,
576 context: &<NB::Tensor as Tensor>::Context,
577 ) -> Result<(), E> {
578 self.process(
579 LayeredTraversalPoint::Unit { group, index },
580 value,
581 forward,
582 context,
583 )
584 }
585
586 fn after_group(
587 &mut self,
588 group: usize,
589 value: &NB::Tensor,
590 forward: &mut C,
591 context: &<NB::Tensor as Tensor>::Context,
592 ) -> Result<(), E> {
593 self.process(
594 LayeredTraversalPoint::Group { group },
595 value,
596 forward,
597 context,
598 )
599 }
600}
601
602#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
604pub enum SequentialDecisionPlanError {
605 #[error("sequential decision plan must contain at least one prediction")]
607 EmptyPlan,
608 #[error("sequential decision plan has {predictions} predictions but {samplers} samplers")]
610 SamplerCountMismatch {
611 predictions: usize,
613 samplers: usize,
615 },
616 #[error(
618 "sequential decision plan has {predictions} predictions but {temperatures} temperatures"
619 )]
620 TemperatureCountMismatch {
621 predictions: usize,
623 temperatures: usize,
625 },
626 #[error(
628 "sequential decision temperature at prediction {prediction} is invalid (bits {bits:#010x})"
629 )]
630 InvalidTemperature {
631 prediction: usize,
633 bits: u32,
635 },
636}
637
638#[derive(Debug, thiserror::Error)]
640pub enum SequentialDecisionError<E> {
641 #[error("sequential decision backend failed: {0}")]
643 Backend(E),
644 #[error(
646 "sequential decision expected prediction {expected} of {predictions}, received {actual}"
647 )]
648 OutOfOrder {
649 expected: usize,
651 actual: usize,
653 predictions: usize,
655 },
656 #[error(
658 "prediction tail beginning at {prediction} with length {count} is not safely skippable"
659 )]
660 InvalidTailSkip {
661 prediction: usize,
663 count: usize,
665 },
666 #[error(
668 "prediction tail beginning at {prediction} has {actual} token domains, expected {expected}"
669 )]
670 TokenDomainCountMismatch {
671 prediction: usize,
673 expected: usize,
675 actual: usize,
677 },
678 #[error("forced-tail boundary expected prediction {expected}, got {actual:?}")]
680 TailBoundaryMismatch {
681 expected: usize,
683 actual: Option<usize>,
685 },
686 #[error("sequential decision traversal resolved {resolved} of {predictions} predictions")]
688 Incomplete {
689 resolved: usize,
691 predictions: usize,
693 },
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699 use crate::PenaltyConfig;
700 use eredu_core::TokenFilter;
701
702 struct Backend;
703
704 impl SamplingBackend for Backend {
705 type Logits = i32;
706 type Token = i32;
707 type RandomState = i32;
708 type Context = ();
709 type Error = String;
710
711 fn error(message: String) -> Self::Error {
712 message
713 }
714
715 fn validate_token(
716 token: &Self::Token,
717 domain: TokenDomain,
718 _: &Self::Context,
719 ) -> Result<Self::Token, Self::Error> {
720 usize::try_from(*token)
721 .ok()
722 .filter(|token| *token < domain.cardinality())
723 .map(|_| *token)
724 .ok_or_else(|| "token is outside its decision domain".into())
725 }
726
727 fn scale_temperature(
728 logits: &Self::Logits,
729 _: f32,
730 _: &Self::Context,
731 ) -> Result<Self::Logits, Self::Error> {
732 Ok(*logits)
733 }
734
735 fn apply_penalties(
736 logits: &Self::Logits,
737 _: &[u32],
738 _: PenaltyConfig,
739 _: &Self::Context,
740 ) -> Result<Self::Logits, Self::Error> {
741 Ok(*logits)
742 }
743
744 fn apply_top_k(
745 logits: Self::Logits,
746 _: i32,
747 _: &Self::Context,
748 ) -> Result<Self::Logits, Self::Error> {
749 Ok(logits)
750 }
751
752 fn apply_top_p(
753 logits: Self::Logits,
754 _: f32,
755 _: &Self::Context,
756 ) -> Result<Self::Logits, Self::Error> {
757 Ok(logits)
758 }
759
760 fn apply_min_p(
761 logits: Self::Logits,
762 _: f32,
763 _: &Self::Context,
764 ) -> Result<Self::Logits, Self::Error> {
765 Ok(logits)
766 }
767
768 fn apply_token_filter(
769 logits: &Self::Logits,
770 _: &TokenFilter,
771 _: &Self::Context,
772 ) -> Result<Self::Logits, Self::Error> {
773 Ok(*logits)
774 }
775
776 fn apply_mirostat(
777 logits: &Self::Logits,
778 _: &[u32],
779 _: PenaltyConfig,
780 _: f32,
781 _: f32,
782 _: &Self::Context,
783 ) -> Result<Self::Logits, Self::Error> {
784 Ok(*logits)
785 }
786
787 fn sample_raw(
788 logits: &Self::Logits,
789 _: f32,
790 random: Option<&mut Self::RandomState>,
791 _: &Self::Context,
792 ) -> Result<Self::Token, Self::Error> {
793 if let Some(random) = random {
794 *random += 1;
795 }
796 Ok(*logits)
797 }
798
799 fn sample_processed(
800 logits: &Self::Logits,
801 temperature: f32,
802 random: Option<&mut Self::RandomState>,
803 context: &Self::Context,
804 ) -> Result<Self::Token, Self::Error> {
805 Self::sample_raw(logits, temperature, random, context)
806 }
807
808 fn token_id(token: &Self::Token, _: &Self::Context) -> Result<u32, Self::Error> {
809 u32::try_from(*token).map_err(|error| error.to_string())
810 }
811
812 fn token_probability(
813 _: &Self::Logits,
814 _: u32,
815 _: &Self::Context,
816 ) -> Result<f32, Self::Error> {
817 Ok(1.0)
818 }
819 }
820
821 #[derive(Clone)]
822 struct OffsetSampler(i32);
823
824 impl Sampler<Backend> for OffsetSampler {
825 fn sample(
826 &mut self,
827 logits: &i32,
828 _: f32,
829 random: Option<&mut i32>,
830 _: &(),
831 ) -> Result<i32, String> {
832 if let Some(random) = random {
833 *random += 1;
834 }
835 Ok(*logits + self.0)
836 }
837 }
838
839 #[test]
840 fn teacher_forcing_sampling_masks_and_diagnostics_share_one_driver() {
841 let plan = SequentialDecisionPlan::new(
842 [
843 PredictionDirective::Force(7),
844 PredictionDirective::Sample,
845 PredictionDirective::Force(9),
846 ],
847 true,
848 true,
849 )
850 .unwrap();
851 assert_eq!(plan.mode(), SequentialDecisionMode::PartiallyForced);
852 assert_eq!(plan.forcing_mask().collect::<Vec<_>>(), [true, false, true]);
853 let mut driver = SequentialDecisionDriver::<Backend, _>::new(
854 plan,
855 vec![OffsetSampler(100), OffsetSampler(10), OffsetSampler(100)],
856 vec![0.0; 3],
857 Some(4),
858 )
859 .unwrap();
860
861 let domain = TokenDomain::new(100);
862 assert_eq!(driver.resolve(0, &1, domain, &()).unwrap(), 7);
863 assert_eq!(driver.resolve(1, &2, domain, &()).unwrap(), 12);
864 assert_eq!(driver.resolve(2, &3, domain, &()).unwrap(), 9);
865 driver.finish().unwrap();
866 assert_eq!(driver.random_state(), Some(&5));
867 assert_eq!(
868 driver
869 .decisions()
870 .iter()
871 .map(|decision| (decision.source(), *decision.token()))
872 .collect::<Vec<_>>(),
873 [
874 (SequentialDecisionSource::Forced, 7),
875 (SequentialDecisionSource::Sampled, 12),
876 (SequentialDecisionSource::Forced, 9),
877 ]
878 );
879 assert_eq!(
880 driver
881 .diagnostics()
882 .iter()
883 .map(|diagnostic| (diagnostic.prediction(), *diagnostic.logits()))
884 .collect::<Vec<_>>(),
885 [(0, 1), (1, 2), (2, 3)]
886 );
887 }
888
889 #[test]
890 fn fully_forced_tail_skip_requires_exact_cardinality_and_no_diagnostics() {
891 let plan = SequentialDecisionPlan::new(
892 [
893 PredictionDirective::Sample,
894 PredictionDirective::Force(8),
895 PredictionDirective::Force(9),
896 ],
897 false,
898 true,
899 )
900 .unwrap();
901 let mut driver = SequentialDecisionDriver::<Backend, _>::new(
902 plan,
903 vec![OffsetSampler(1); 3],
904 vec![0.0; 3],
905 None,
906 )
907 .unwrap();
908 driver.resolve(0, &4, TokenDomain::new(100), &()).unwrap();
909 assert_eq!(
910 driver.fully_forced_tail_decision(1, 1).unwrap(),
911 FullyForcedTailDecision::Execute
912 );
913 assert_eq!(
914 driver.fully_forced_tail_decision(1, 2).unwrap(),
915 FullyForcedTailDecision::Skip { predictions: 2 }
916 );
917 let tokens = driver
918 .forced_tail_tokens(1, 2, [TokenDomain::new(100); 2], &())
919 .unwrap();
920 driver.commit_forced_tail(1, tokens).unwrap();
921 driver.finish().unwrap();
922 assert_eq!(
923 driver
924 .decisions()
925 .iter()
926 .map(SequentialDecision::source)
927 .collect::<Vec<_>>(),
928 [
929 SequentialDecisionSource::Sampled,
930 SequentialDecisionSource::ForcedTailSkipped,
931 SequentialDecisionSource::ForcedTailSkipped,
932 ]
933 );
934 assert!(driver.diagnostics().is_empty());
935
936 let diagnostic_plan = SequentialDecisionPlan::new(
937 [PredictionDirective::Force(1), PredictionDirective::Force(2)],
938 true,
939 true,
940 )
941 .unwrap();
942 let diagnostic = SequentialDecisionDriver::<Backend, _>::new(
943 diagnostic_plan,
944 vec![OffsetSampler(0); 2],
945 vec![0.0; 2],
946 None,
947 )
948 .unwrap();
949 assert_eq!(
950 diagnostic.fully_forced_tail_decision(0, 2).unwrap(),
951 FullyForcedTailDecision::Execute
952 );
953 }
954
955 #[test]
956 fn driver_rejects_cardinality_temperature_and_order_drift() {
957 let plan =
958 SequentialDecisionPlan::new([PredictionDirective::Sample], false, false).unwrap();
959 assert!(matches!(
960 SequentialDecisionDriver::<Backend, _>::new(
961 plan.clone(),
962 Vec::<OffsetSampler>::new(),
963 vec![0.0],
964 None
965 ),
966 Err(SequentialDecisionPlanError::SamplerCountMismatch { .. })
967 ));
968 assert!(matches!(
969 SequentialDecisionDriver::<Backend, _>::new(
970 plan.clone(),
971 vec![OffsetSampler(0)],
972 vec![f32::NAN],
973 None
974 ),
975 Err(SequentialDecisionPlanError::InvalidTemperature { .. })
976 ));
977 let mut driver = SequentialDecisionDriver::<Backend, _>::new(
978 plan,
979 vec![OffsetSampler(0)],
980 vec![0.0],
981 None,
982 )
983 .unwrap();
984 assert!(matches!(
985 driver.resolve(1, &0, TokenDomain::new(100), &()),
986 Err(SequentialDecisionError::OutOfOrder { .. })
987 ));
988 }
989
990 #[test]
991 fn token_domains_reject_sampled_executed_forcing_and_skipped_forcing_before_commit() {
992 let domain = TokenDomain::new(10);
993
994 let forced_plan =
995 SequentialDecisionPlan::new([PredictionDirective::Force(10)], false, false).unwrap();
996 let mut forced = SequentialDecisionDriver::<Backend, _>::new(
997 forced_plan,
998 vec![OffsetSampler(0)],
999 vec![0.0],
1000 None,
1001 )
1002 .unwrap();
1003 assert!(matches!(
1004 forced.resolve(0, &0, domain, &()),
1005 Err(SequentialDecisionError::Backend(_))
1006 ));
1007 assert!(forced.decisions().is_empty());
1008
1009 let sampled_plan =
1010 SequentialDecisionPlan::new([PredictionDirective::Sample], false, false).unwrap();
1011 let mut sampled = SequentialDecisionDriver::<Backend, _>::new(
1012 sampled_plan,
1013 vec![OffsetSampler(10)],
1014 vec![0.0],
1015 Some(4),
1016 )
1017 .unwrap();
1018 assert!(matches!(
1019 sampled.resolve(0, &0, domain, &()),
1020 Err(SequentialDecisionError::Backend(_))
1021 ));
1022 assert!(sampled.decisions().is_empty());
1023
1024 let tail_plan = SequentialDecisionPlan::new(
1025 [
1026 PredictionDirective::Force(1),
1027 PredictionDirective::Force(10),
1028 ],
1029 false,
1030 true,
1031 )
1032 .unwrap();
1033 let tail = SequentialDecisionDriver::<Backend, _>::new(
1034 tail_plan,
1035 vec![OffsetSampler(0); 2],
1036 vec![0.0; 2],
1037 None,
1038 )
1039 .unwrap();
1040 assert!(matches!(
1041 tail.forced_tail_tokens(0, 2, [domain; 2], &()),
1042 Err(SequentialDecisionError::Backend(_))
1043 ));
1044 assert!(tail.decisions().is_empty());
1045 }
1046}