1mod continuation;
4pub use continuation::{
5 TextContinuationBoundary, TextContinuationError, TextContinuationIdentity, TextDriverIdentity,
6 TextGenerationContinuation, TextGenerationDriver,
7};
8
9use serde::{Deserialize, Serialize};
10use std::{fmt::Debug, path::Path};
11
12use crate::{
13 artifact::{
14 inspect_artifact, ArtifactError, ArtifactInspection, ModelConfigurationResolver,
15 ModelPreparationPlan,
16 },
17 capability::{
18 CapabilityError, InputTokenCount, ModelCapabilities, RuntimeStateEstimate,
19 StaticMemoryReport,
20 },
21 checkpoint::TensorDtype,
22 generation::{GenerationError, ResolvedGenerationConfig},
23 media::TokenizedMultimodalRequest,
24 observation::{InspectedOutput, ObservationRequest, ObservationSet},
25 PreparationAdmission,
26};
27
28#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
30pub struct BackendDescriptor {
31 name: String,
33 version: String,
35}
36
37impl BackendDescriptor {
38 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
40 Self {
41 name: name.into(),
42 version: version.into(),
43 }
44 }
45
46 pub fn name(&self) -> &str {
48 &self.name
49 }
50
51 pub fn version(&self) -> &str {
53 &self.version
54 }
55}
56
57#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
59pub struct DeviceDescriptor {
60 id: String,
62 name: String,
64 family: String,
66 memory_bytes: Option<u64>,
68}
69
70impl DeviceDescriptor {
71 pub fn new(
73 id: impl Into<String>,
74 name: impl Into<String>,
75 family: impl Into<String>,
76 memory_bytes: Option<u64>,
77 ) -> Self {
78 Self {
79 id: id.into(),
80 name: name.into(),
81 family: family.into(),
82 memory_bytes,
83 }
84 }
85
86 pub fn id(&self) -> &str {
88 &self.id
89 }
90 pub fn name(&self) -> &str {
92 &self.name
93 }
94 pub fn family(&self) -> &str {
96 &self.family
97 }
98 pub const fn memory_bytes(&self) -> Option<u64> {
100 self.memory_bytes
101 }
102}
103
104#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
106pub struct DeviceCapabilities {
107 exact_completion: bool,
109 transfers: bool,
111 collectives: bool,
113}
114
115impl DeviceCapabilities {
116 pub const fn new(exact_completion: bool, transfers: bool, collectives: bool) -> Self {
118 Self {
119 exact_completion,
120 transfers,
121 collectives,
122 }
123 }
124
125 pub const fn exact_completion(&self) -> bool {
127 self.exact_completion
128 }
129 pub const fn transfers(&self) -> bool {
131 self.transfers
132 }
133 pub const fn collectives(&self) -> bool {
135 self.collectives
136 }
137}
138
139#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
141pub struct SessionCapabilities {
142 persistent_cache: bool,
144 output_observation: bool,
146 activation_inspection: bool,
148}
149
150impl SessionCapabilities {
151 pub const fn new(
153 persistent_cache: bool,
154 output_observation: bool,
155 activation_inspection: bool,
156 ) -> Self {
157 Self {
158 persistent_cache,
159 output_observation,
160 activation_inspection,
161 }
162 }
163
164 pub const fn persistent_cache(self) -> bool {
166 self.persistent_cache
167 }
168 pub const fn output_observation(self) -> bool {
170 self.output_observation
171 }
172 pub const fn activation_inspection(self) -> bool {
174 self.activation_inspection
175 }
176
177 pub const fn with_persistent_cache(mut self, supported: bool) -> Self {
179 self.persistent_cache = supported;
180 self
181 }
182 pub const fn with_output_observation(mut self, supported: bool) -> Self {
184 self.output_observation = supported;
185 self
186 }
187 pub const fn with_activation_inspection(mut self, supported: bool) -> Self {
189 self.activation_inspection = supported;
190 self
191 }
192 pub fn validate(&self, available: &Self) -> Result<(), SessionCapabilityError> {
194 for (required, supported, capability) in [
195 (
196 self.persistent_cache,
197 available.persistent_cache,
198 "persistent_cache",
199 ),
200 (
201 self.output_observation,
202 available.output_observation,
203 "output_observation",
204 ),
205 (
206 self.activation_inspection,
207 available.activation_inspection,
208 "activation_inspection",
209 ),
210 ] {
211 if required && !supported {
212 return Err(SessionCapabilityError { capability });
213 }
214 }
215 Ok(())
216 }
217}
218
219#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
221#[error("prepared session does not support required capability {capability}")]
222pub struct SessionCapabilityError {
223 capability: &'static str,
224}
225
226impl SessionCapabilityError {
227 pub const fn capability(self) -> &'static str {
229 self.capability
230 }
231}
232
233#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
235pub struct DistributedCapabilities {
236 world_collectives: bool,
237 collective_groups: Vec<CollectiveGroupId>,
238 point_to_point: bool,
239 variable_all_to_all: bool,
240 exact_completion: bool,
241}
242
243impl DistributedCapabilities {
244 pub fn new(
246 world_collectives: bool,
247 collective_groups: impl IntoIterator<Item = CollectiveGroupId>,
248 point_to_point: bool,
249 variable_all_to_all: bool,
250 exact_completion: bool,
251 ) -> Self {
252 Self {
253 world_collectives,
254 collective_groups: collective_groups.into_iter().collect(),
255 point_to_point,
256 variable_all_to_all,
257 exact_completion,
258 }
259 }
260
261 pub const fn world_collectives(&self) -> bool {
263 self.world_collectives
264 }
265 pub fn collective_groups(&self) -> &[CollectiveGroupId] {
267 &self.collective_groups
268 }
269 pub const fn point_to_point(&self) -> bool {
271 self.point_to_point
272 }
273 pub const fn variable_all_to_all(&self) -> bool {
275 self.variable_all_to_all
276 }
277 pub const fn exact_completion(&self) -> bool {
279 self.exact_completion
280 }
281}
282
283#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
285#[serde(transparent)]
286pub struct CollectiveGroupId(u32);
287
288impl CollectiveGroupId {
289 pub const fn new(value: u32) -> Self {
291 Self(value)
292 }
293 pub const fn value(self) -> u32 {
295 self.0
296 }
297}
298
299#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
301pub struct CollectiveGroupDescriptor {
302 id: CollectiveGroupId,
303 members: Vec<usize>,
304 local_rank: usize,
305}
306
307impl CollectiveGroupDescriptor {
308 pub fn new(
310 id: CollectiveGroupId,
311 members: Vec<usize>,
312 local_rank: usize,
313 ) -> Result<Self, BackendError> {
314 if members.is_empty() || local_rank >= members.len() {
315 return Err(BackendError::Preparation {
316 operation: "collective group realization".into(),
317 message: "collective membership must be non-empty and contain local rank".into(),
318 });
319 }
320 let mut unique = std::collections::BTreeSet::new();
321 if !members.iter().all(|rank| unique.insert(*rank)) {
322 return Err(BackendError::Preparation {
323 operation: "collective group realization".into(),
324 message: "collective membership contains duplicate world ranks".into(),
325 });
326 }
327 Ok(Self {
328 id,
329 members,
330 local_rank,
331 })
332 }
333
334 pub const fn id(&self) -> CollectiveGroupId {
336 self.id
337 }
338 pub fn members(&self) -> &[usize] {
340 &self.members
341 }
342 pub const fn local_rank(&self) -> usize {
344 self.local_rank
345 }
346}
347
348impl<'de> Deserialize<'de> for CollectiveGroupDescriptor {
349 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
350 where
351 D: serde::Deserializer<'de>,
352 {
353 #[derive(Deserialize)]
354 struct Raw {
355 id: CollectiveGroupId,
356 members: Vec<usize>,
357 local_rank: usize,
358 }
359 let raw = Raw::deserialize(deserializer)?;
360 Self::new(raw.id, raw.members, raw.local_rank).map_err(serde::de::Error::custom)
361 }
362}
363
364#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
366#[serde(tag = "kind", content = "group", rename_all = "snake_case")]
367#[non_exhaustive]
368pub enum CollectiveScope {
369 World,
371 Group(CollectiveGroupId),
373}
374
375#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
377pub struct ValueDescriptor {
378 shape: Vec<usize>,
380 dtype: TensorDtype,
382}
383
384impl ValueDescriptor {
385 pub fn new(shape: Vec<usize>, dtype: TensorDtype) -> Result<Self, BackendError> {
387 if shape.contains(&0) {
388 return Err(BackendError::Preparation {
389 operation: "distributed value descriptor".into(),
390 message: "non-scalar distributed values require positive dimensions".into(),
391 });
392 }
393 Ok(Self { shape, dtype })
394 }
395
396 pub fn shape(&self) -> &[usize] {
398 &self.shape
399 }
400
401 pub const fn dtype(&self) -> &TensorDtype {
403 &self.dtype
404 }
405}
406
407impl<'de> Deserialize<'de> for ValueDescriptor {
408 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
409 where
410 D: serde::Deserializer<'de>,
411 {
412 #[derive(Deserialize)]
413 struct RawDescriptor {
414 shape: Vec<usize>,
415 dtype: TensorDtype,
416 }
417
418 let raw = RawDescriptor::deserialize(deserializer)?;
419 Self::new(raw.shape, raw.dtype).map_err(serde::de::Error::custom)
420 }
421}
422
423#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
425pub struct DistributedSessionDescriptor {
426 world_size: usize,
427 rank: usize,
428 groups: Vec<CollectiveGroupDescriptor>,
429}
430
431impl DistributedSessionDescriptor {
432 pub fn new(
434 world_size: usize,
435 rank: usize,
436 groups: Vec<CollectiveGroupDescriptor>,
437 ) -> Result<Self, BackendError> {
438 if world_size == 0 || rank >= world_size {
439 return Err(BackendError::Preparation {
440 operation: "distributed session realization".into(),
441 message: format!("rank {rank} is outside world size {world_size}"),
442 });
443 }
444 let mut ids = std::collections::BTreeSet::new();
445 for group in &groups {
446 if !ids.insert(group.id())
447 || group.members().iter().any(|member| *member >= world_size)
448 || group.members()[group.local_rank()] != rank
449 {
450 return Err(BackendError::Preparation {
451 operation: "distributed session realization".into(),
452 message: "collective groups must have unique IDs, in-range members, and the declared local world rank".into(),
453 });
454 }
455 }
456 Ok(Self {
457 world_size,
458 rank,
459 groups,
460 })
461 }
462
463 pub const fn world_size(&self) -> usize {
465 self.world_size
466 }
467 pub const fn rank(&self) -> usize {
469 self.rank
470 }
471 pub fn groups(&self) -> &[CollectiveGroupDescriptor] {
473 &self.groups
474 }
475}
476
477impl<'de> Deserialize<'de> for DistributedSessionDescriptor {
478 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
479 where
480 D: serde::Deserializer<'de>,
481 {
482 #[derive(Deserialize)]
483 struct RawDescriptor {
484 world_size: usize,
485 rank: usize,
486 groups: Vec<CollectiveGroupDescriptor>,
487 }
488
489 let raw = RawDescriptor::deserialize(deserializer)?;
490 Self::new(raw.world_size, raw.rank, raw.groups).map_err(serde::de::Error::custom)
491 }
492}
493
494#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
496#[non_exhaustive]
497pub enum BackendError {
498 #[error("backend {backend} does not support required capability {capability}")]
500 Unsupported {
501 backend: String,
503 capability: String,
505 },
506 #[error("backend model preparation failed during {operation}: {message}")]
508 Preparation {
509 operation: String,
511 message: String,
513 },
514 #[error("backend session {session} failed during {operation}: {message}")]
516 Execution {
517 session: String,
519 operation: String,
521 message: String,
523 },
524 #[error("backend completion observation failed: {message}")]
526 Completion {
527 message: String,
529 },
530}
531
532pub trait Completion {
534 type Error: std::error::Error + Send + Sync + 'static;
536
537 fn is_complete(&self) -> Result<bool, Self::Error>;
539
540 fn wait(&self) -> Result<(), Self::Error>;
543
544 fn resources_releasable(&self) -> bool {
558 matches!(self.is_complete(), Ok(true))
559 }
560}
561
562#[derive(
564 Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Serialize, serde::Deserialize,
565)]
566pub enum CompletionCancellationMode {
567 NativeCancel,
569 QuarantineUntilComplete,
572}
573
574#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
576#[serde(transparent)]
577pub struct DistributedCommitEpoch(u64);
578
579impl DistributedCommitEpoch {
580 pub const FIRST: Self = Self(1);
582
583 pub const fn new(value: u64) -> Option<Self> {
585 if value == 0 {
586 None
587 } else {
588 Some(Self(value))
589 }
590 }
591
592 pub const fn value(self) -> u64 {
594 self.0
595 }
596
597 pub const fn next(self) -> Option<Self> {
599 match self.0.checked_add(1) {
600 Some(value) => Some(Self(value)),
601 None => None,
602 }
603 }
604}
605
606impl<'de> Deserialize<'de> for DistributedCommitEpoch {
607 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
608 where
609 D: serde::Deserializer<'de>,
610 {
611 let value = u64::deserialize(deserializer)?;
612 Self::new(value).ok_or_else(|| serde::de::Error::custom("commit epoch must be positive"))
613 }
614}
615
616#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
618#[serde(rename_all = "snake_case")]
619pub enum DistributedCommitPhase {
620 DecisionSubmission,
622 DecisionCompletion,
624 DecisionObservation,
626}
627
628#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
630#[serde(rename_all = "snake_case")]
631pub enum DistributedCommitOutcome {
632 Committed(DistributedCommitEpoch),
634 Aborted(DistributedCommitEpoch),
636 Indeterminate {
638 epoch: DistributedCommitEpoch,
640 phase: DistributedCommitPhase,
642 },
643}
644
645impl DistributedCommitOutcome {
646 pub const fn epoch(self) -> DistributedCommitEpoch {
648 match self {
649 Self::Committed(epoch) | Self::Aborted(epoch) | Self::Indeterminate { epoch, .. } => {
650 epoch
651 }
652 }
653 }
654
655 pub const fn is_indeterminate(self) -> bool {
657 matches!(self, Self::Indeterminate { .. })
658 }
659}
660
661#[derive(Debug, Clone, Copy, Eq, PartialEq)]
663pub struct BoundedCompletionWait {
664 timeout: std::time::Duration,
665 cancellation: CompletionCancellationMode,
666}
667
668impl BoundedCompletionWait {
669 pub fn new(
671 timeout: std::time::Duration,
672 cancellation: CompletionCancellationMode,
673 ) -> Result<Self, BoundedCompletionWaitError> {
674 if timeout.is_zero() {
675 return Err(BoundedCompletionWaitError::ZeroTimeout);
676 }
677 Ok(Self {
678 timeout,
679 cancellation,
680 })
681 }
682
683 pub const fn timeout(self) -> std::time::Duration {
685 self.timeout
686 }
687
688 pub const fn cancellation(self) -> CompletionCancellationMode {
690 self.cancellation
691 }
692}
693
694#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
696pub enum BoundedCompletionWaitError {
697 #[error("bounded completion timeout must be positive")]
699 ZeroTimeout,
700}
701
702#[derive(Debug, Clone, Copy, Eq, PartialEq)]
704pub enum BoundedCompletionOutcome {
705 Completed,
707 DeadlineExceeded {
709 cancellation: CompletionCancellationMode,
711 },
712}
713
714pub trait BoundedCompletion: Completion + Sized {
725 fn supports_cancellation(_cancellation: CompletionCancellationMode) -> bool {
729 true
730 }
731
732 fn wait_bounded(
735 self,
736 policy: BoundedCompletionWait,
737 ) -> Result<BoundedCompletionOutcome, Self::Error>;
738}
739
740#[derive(Debug)]
742pub struct Submission<T, C> {
743 pub output: T,
745 pub completion: C,
747}
748
749impl<T, C> Submission<T, C>
750where
751 C: Completion,
752{
753 pub fn wait(self) -> Result<T, C::Error> {
755 self.completion.wait()?;
756 Ok(self.output)
757 }
758}
759
760impl<T, C> Submission<T, C>
761where
762 C: BoundedCompletion,
763{
764 pub fn wait_bounded(
768 self,
769 policy: BoundedCompletionWait,
770 ) -> Result<BoundedSubmissionOutcome<T>, C::Error> {
771 match self.completion.wait_bounded(policy)? {
772 BoundedCompletionOutcome::Completed => {
773 Ok(BoundedSubmissionOutcome::Completed(self.output))
774 }
775 BoundedCompletionOutcome::DeadlineExceeded { cancellation } => {
776 Ok(BoundedSubmissionOutcome::DeadlineExceeded { cancellation })
777 }
778 }
779 }
780}
781
782#[derive(Debug, Eq, PartialEq)]
784pub enum BoundedSubmissionOutcome<T> {
785 Completed(T),
787 DeadlineExceeded {
789 cancellation: CompletionCancellationMode,
791 },
792}
793
794#[derive(Debug)]
796pub struct PreparedModel<M> {
797 model: M,
798 capabilities: SessionCapabilities,
799}
800
801impl<M> PreparedModel<M> {
802 pub const fn new(model: M, capabilities: SessionCapabilities) -> Self {
804 Self {
805 model,
806 capabilities,
807 }
808 }
809 pub const fn get(&self) -> &M {
811 &self.model
812 }
813 pub fn get_mut(&mut self) -> &mut M {
815 &mut self.model
816 }
817 pub const fn capabilities(&self) -> SessionCapabilities {
819 self.capabilities
820 }
821 pub fn into_inner(self) -> M {
823 self.model
824 }
825 pub fn into_parts(self) -> (M, SessionCapabilities) {
827 (self.model, self.capabilities)
828 }
829}
830
831impl<M> std::ops::Deref for PreparedModel<M> {
832 type Target = M;
833
834 fn deref(&self) -> &Self::Target {
835 self.get()
836 }
837}
838
839impl<M> std::ops::DerefMut for PreparedModel<M> {
840 fn deref_mut(&mut self) -> &mut Self::Target {
841 self.get_mut()
842 }
843}
844
845pub trait BackendProvider: Sized {
847 type ModelConfig;
849 type Model;
851 type Session: BackendSession<Self>;
853 type Error: std::error::Error + Send + Sync + 'static;
855
856 fn descriptor(&self) -> BackendDescriptor;
858 fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error>;
860 fn prepare_model(
862 &self,
863 config: Self::ModelConfig,
864 ) -> Result<PreparedModel<Self::Model>, Self::Error>;
865 fn create_session(
871 &self,
872 model: PreparedModel<Self::Model>,
873 ) -> Result<Self::Session, Self::Error>;
874
875 fn session_capability_mismatch(
880 &self,
881 admitted: SessionCapabilities,
882 realized: SessionCapabilities,
883 ) -> Self::Error {
884 panic!("backend realized session capabilities {realized:?} after admitting {admitted:?}")
885 }
886}
887
888pub trait ModelLoadingBackend: BackendProvider {
895 type LoadOptions;
897
898 type SelectedPreparation;
901
902 type ConfigurationResolver: ModelConfigurationResolver;
904
905 fn configuration_resolver(&self) -> &Self::ConfigurationResolver;
907
908 fn select_preparation(
916 &self,
917 inspection: &ArtifactInspection<
918 <Self::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
919 >,
920 options: &Self::LoadOptions,
921 ) -> Result<Self::SelectedPreparation, Self::Error>;
922
923 fn selected_preparation_admission(
925 &self,
926 selected: &Self::SelectedPreparation,
927 ) -> PreparationAdmission;
928
929 fn model_config(
932 &self,
933 selected: SelectedModelPreparation<Self>,
934 ) -> Result<Self::ModelConfig, Self::Error>;
935}
936
937pub struct SelectedModelPreparation<B: ModelLoadingBackend> {
944 plan: ModelPreparationPlan<
945 <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
946 >,
947 selected: B::SelectedPreparation,
948}
949
950impl<B: ModelLoadingBackend> SelectedModelPreparation<B> {
951 pub(crate) fn new(
952 plan: ModelPreparationPlan<
953 <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
954 >,
955 selected: B::SelectedPreparation,
956 ) -> Self {
957 Self { plan, selected }
958 }
959
960 pub(crate) const fn plan(
961 &self,
962 ) -> &ModelPreparationPlan<<B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan>
963 {
964 &self.plan
965 }
966
967 pub fn into_parts(
969 self,
970 ) -> (
971 ModelPreparationPlan<
972 <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
973 >,
974 B::SelectedPreparation,
975 ) {
976 (self.plan, self.selected)
977 }
978}
979
980#[derive(Debug, thiserror::Error)]
982#[non_exhaustive]
983pub enum ModelLoadError<E: std::error::Error + Send + Sync + 'static> {
984 #[error(transparent)]
986 Artifact(#[from] ArtifactError),
987 #[error("selected backend failed to prepare the model: {0}")]
989 Backend(#[source] E),
990 #[error(transparent)]
992 SessionCapability(#[from] SessionCapabilityError),
993}
994
995pub fn load_model<B: ModelLoadingBackend>(
1001 backend: &B,
1002 artifact: impl AsRef<Path>,
1003 options: B::LoadOptions,
1004) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
1005 let inspection = inspect_artifact(artifact, backend.configuration_resolver())?;
1006 prepare_inspected_model(backend, inspection, options)
1007}
1008
1009pub fn prepare_inspected_model<B: ModelLoadingBackend>(
1015 backend: &B,
1016 inspection: ArtifactInspection<
1017 <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
1018 >,
1019 options: B::LoadOptions,
1020) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
1021 let selected = backend
1022 .select_preparation(&inspection, &options)
1023 .map_err(ModelLoadError::Backend)?;
1024 let admission = backend.selected_preparation_admission(&selected);
1025 let plan = ModelPreparationPlan::from_retained_admission(inspection, admission)?;
1026 prepare_selected_model(backend, SelectedModelPreparation::new(plan, selected))
1027}
1028
1029pub(crate) fn prepare_selected_model<B: ModelLoadingBackend>(
1035 backend: &B,
1036 selected: SelectedModelPreparation<B>,
1037) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
1038 let config = backend
1039 .model_config(selected)
1040 .map_err(ModelLoadError::Backend)?;
1041 backend
1042 .prepare_model(config)
1043 .map_err(ModelLoadError::Backend)
1044}
1045
1046pub trait BackendSession<B: BackendProvider> {
1052 type PrefillInput;
1054 type DecodeInput;
1056 type Output;
1058 type Completion: Completion<Error = B::Error>;
1060
1061 fn capabilities(&self) -> SessionCapabilities;
1063
1064 fn prefill(
1066 &mut self,
1067 backend: &B,
1068 input: Self::PrefillInput,
1069 ) -> Result<Submission<Self::Output, Self::Completion>, B::Error>;
1070
1071 fn decode(
1073 &mut self,
1074 backend: &B,
1075 input: Self::DecodeInput,
1076 ) -> Result<Submission<Self::Output, Self::Completion>, B::Error>;
1077
1078 fn observe_output(
1083 &self,
1084 backend: &B,
1085 output: &Self::Output,
1086 ) -> Result<ObservationSet, B::Error>;
1087}
1088
1089pub trait InspectableBackendSession<B: BackendProvider>: BackendSession<B> {
1096 fn inspect_prefill(
1098 &mut self,
1099 backend: &B,
1100 input: Self::PrefillInput,
1101 request: &ObservationRequest,
1102 ) -> Result<InspectedOutput<Self::Output>, B::Error>;
1103
1104 fn inspect_decode(
1106 &mut self,
1107 backend: &B,
1108 input: Self::DecodeInput,
1109 request: &ObservationRequest,
1110 ) -> Result<InspectedOutput<Self::Output>, B::Error>;
1111}
1112
1113pub type SessionSubmission<B> = Submission<
1115 <<B as BackendProvider>::Session as BackendSession<B>>::Output,
1116 <<B as BackendProvider>::Session as BackendSession<B>>::Completion,
1117>;
1118
1119pub struct ModelRuntime<B: BackendProvider> {
1128 backend: B,
1129 session: B::Session,
1130 admission: crate::SessionAdmission,
1131 execution_plan_target_id: Option<u64>,
1132}
1133
1134impl<B: BackendProvider> ModelRuntime<B> {
1135 pub fn prepare(backend: B, config: B::ModelConfig) -> Result<Self, B::Error> {
1137 let model = backend.prepare_model(config)?;
1138 Self::from_prepared(backend, model)
1139 }
1140
1141 pub fn from_prepared(backend: B, model: PreparedModel<B::Model>) -> Result<Self, B::Error> {
1143 Self::from_prepared_with_execution_plan_target(backend, model, None)
1144 }
1145
1146 pub(crate) fn from_prepared_execution_plan_target(
1147 backend: B,
1148 model: PreparedModel<B::Model>,
1149 execution_plan_target_id: u64,
1150 ) -> Result<Self, B::Error> {
1151 Self::from_prepared_with_execution_plan_target(
1152 backend,
1153 model,
1154 Some(execution_plan_target_id),
1155 )
1156 }
1157
1158 fn from_prepared_with_execution_plan_target(
1159 backend: B,
1160 model: PreparedModel<B::Model>,
1161 execution_plan_target_id: Option<u64>,
1162 ) -> Result<Self, B::Error> {
1163 let admitted = model.capabilities();
1164 let session = backend.create_session(model)?;
1165 let realized = session.capabilities();
1166 if crate::SessionAdmission::new(admitted)
1167 .validate(realized)
1168 .is_err()
1169 {
1170 return Err(backend.session_capability_mismatch(admitted, realized));
1171 }
1172 Ok(Self {
1173 backend,
1174 session,
1175 admission: crate::SessionAdmission::new(admitted),
1176 execution_plan_target_id,
1177 })
1178 }
1179
1180 pub(crate) const fn execution_plan_target_id(&self) -> Option<u64> {
1181 self.execution_plan_target_id
1182 }
1183
1184 pub const fn backend(&self) -> &B {
1186 &self.backend
1187 }
1188
1189 pub const fn session(&self) -> &B::Session {
1191 &self.session
1192 }
1193
1194 pub fn session_mut(&mut self) -> &mut B::Session {
1201 self.execution_plan_target_id = None;
1202 &mut self.session
1203 }
1204
1205 pub fn parts_mut(&mut self) -> (&B, &mut B::Session) {
1209 self.execution_plan_target_id = None;
1210 (&self.backend, &mut self.session)
1211 }
1212
1213 fn validate_session_admission(&self) -> Result<(), B::Error> {
1214 self.admission
1215 .validate(self.session.capabilities())
1216 .map_err(|error| {
1217 self.backend
1218 .session_capability_mismatch(error.admitted(), error.realized())
1219 })
1220 }
1221
1222 pub fn capabilities(&self) -> SessionCapabilities {
1224 self.session.capabilities()
1225 }
1226
1227 pub fn prefill(
1229 &mut self,
1230 input: <B::Session as BackendSession<B>>::PrefillInput,
1231 ) -> Result<SessionSubmission<B>, B::Error> {
1232 self.validate_session_admission()?;
1233 self.session.prefill(&self.backend, input)
1234 }
1235
1236 pub fn decode(
1238 &mut self,
1239 input: <B::Session as BackendSession<B>>::DecodeInput,
1240 ) -> Result<SessionSubmission<B>, B::Error> {
1241 self.validate_session_admission()?;
1242 self.session.decode(&self.backend, input)
1243 }
1244
1245 pub fn observe_output(
1247 &self,
1248 output: &<B::Session as BackendSession<B>>::Output,
1249 ) -> Result<ObservationSet, B::Error> {
1250 self.validate_session_admission()?;
1251 self.session.observe_output(&self.backend, output)
1252 }
1253}
1254
1255impl<B> ModelRuntime<B>
1256where
1257 B: BackendProvider,
1258 B::Session: InspectableBackendSession<B>,
1259{
1260 pub fn inspect_prefill(
1262 &mut self,
1263 input: <B::Session as BackendSession<B>>::PrefillInput,
1264 request: &ObservationRequest,
1265 ) -> Result<InspectedOutput<<B::Session as BackendSession<B>>::Output>, B::Error> {
1266 self.validate_session_admission()?;
1267 self.session.inspect_prefill(&self.backend, input, request)
1268 }
1269
1270 pub fn inspect_decode(
1272 &mut self,
1273 input: <B::Session as BackendSession<B>>::DecodeInput,
1274 request: &ObservationRequest,
1275 ) -> Result<InspectedOutput<<B::Session as BackendSession<B>>::Output>, B::Error> {
1276 self.validate_session_admission()?;
1277 self.session.inspect_decode(&self.backend, input, request)
1278 }
1279}
1280
1281impl<B: ModelLoadingBackend> ModelRuntime<B> {
1282 pub fn load(
1284 backend: B,
1285 artifact: impl AsRef<Path>,
1286 options: B::LoadOptions,
1287 ) -> Result<Self, ModelLoadError<B::Error>> {
1288 let model = load_model(&backend, artifact, options)?;
1289 Self::from_prepared(backend, model).map_err(ModelLoadError::Backend)
1290 }
1291}
1292
1293#[derive(Debug, Clone, Copy, PartialEq)]
1295pub struct TextGenerationConfig {
1296 sampling: ResolvedGenerationConfig,
1297 seed: u64,
1298 strategy: TextSamplingStrategy,
1299}
1300
1301#[derive(Debug, Clone, Copy, Default, PartialEq)]
1303pub enum TextSamplingStrategy {
1304 #[default]
1306 Standard,
1307 MirostatV2 {
1309 tau: f32,
1311 eta: f32,
1313 },
1314}
1315
1316impl TextGenerationConfig {
1317 pub const fn new(sampling: ResolvedGenerationConfig) -> Self {
1319 Self {
1320 sampling,
1321 seed: 0,
1322 strategy: TextSamplingStrategy::Standard,
1323 }
1324 }
1325
1326 pub const fn with_seed(mut self, seed: u64) -> Self {
1328 self.seed = seed;
1329 self
1330 }
1331
1332 pub fn with_mirostat_v2(mut self, tau: f32, eta: f32) -> Result<Self, GenerationError> {
1334 if !tau.is_finite() || tau <= 0.0 {
1335 return Err(GenerationError::InvalidMirostatTau(tau));
1336 }
1337 if !eta.is_finite() || eta <= 0.0 {
1338 return Err(GenerationError::InvalidMirostatEta(eta));
1339 }
1340 self.strategy = TextSamplingStrategy::MirostatV2 { tau, eta };
1341 Ok(self)
1342 }
1343
1344 pub const fn sampling(&self) -> ResolvedGenerationConfig {
1346 self.sampling
1347 }
1348
1349 pub const fn seed(&self) -> u64 {
1351 self.seed
1352 }
1353
1354 pub const fn strategy(&self) -> TextSamplingStrategy {
1356 self.strategy
1357 }
1358}
1359
1360pub trait TokenOutput: Clone {
1362 type Error: std::error::Error + Send + Sync + 'static;
1364
1365 fn token_id(&self) -> Result<u32, Self::Error>;
1367}
1368
1369impl TokenOutput for u32 {
1370 type Error = std::convert::Infallible;
1371
1372 fn token_id(&self) -> Result<u32, Self::Error> {
1373 Ok(*self)
1374 }
1375}
1376
1377#[derive(Debug, Clone, Eq, PartialEq)]
1379pub enum TokenFilter {
1380 All,
1382 Allowed(Vec<bool>),
1384}
1385
1386impl TokenFilter {
1387 pub fn allowed(mask: Vec<bool>) -> Result<Self, TokenFilterError> {
1389 if mask.is_empty() {
1390 return Err(TokenFilterError::EmptyVocabulary);
1391 }
1392 if !mask.iter().any(|allowed| *allowed) {
1393 return Err(TokenFilterError::NoAllowedToken);
1394 }
1395 Ok(Self::Allowed(mask))
1396 }
1397
1398 pub fn allowed_mask(&self) -> Option<&[bool]> {
1400 match self {
1401 Self::All => None,
1402 Self::Allowed(mask) => Some(mask),
1403 }
1404 }
1405}
1406
1407#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1409pub enum TokenFilterError {
1410 #[error("token filter vocabulary must not be empty")]
1412 EmptyVocabulary,
1413 #[error("token filter does not allow any vocabulary token")]
1415 NoAllowedToken,
1416}
1417
1418pub trait TokenFilterController {
1420 type Error: std::error::Error + Send + Sync + 'static;
1422
1423 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error>;
1425
1426 fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error>;
1428
1429 fn is_complete(&mut self) -> Result<bool, Self::Error>;
1431}
1432
1433pub trait SpeculativeTokenFilterController: TokenFilterController + Clone {
1439 fn filter_at(&self, history: &[u32]) -> Result<TokenFilter, Self::Error>;
1445
1446 fn prefix_is_complete(&self, history: &[u32]) -> Result<bool, Self::Error>;
1448}
1449
1450#[derive(Debug, Clone, Copy)]
1451struct UnconstrainedTokens;
1452
1453impl TokenFilterController for UnconstrainedTokens {
1454 type Error = std::convert::Infallible;
1455
1456 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
1457 Ok(TokenFilter::All)
1458 }
1459
1460 fn commit_token(&mut self, _: u32) -> Result<(), Self::Error> {
1461 Ok(())
1462 }
1463
1464 fn is_complete(&mut self) -> Result<bool, Self::Error> {
1465 Ok(false)
1466 }
1467}
1468
1469pub trait TextGenerationBackend: BackendProvider {
1475 type Prompt;
1477 type Token: TokenOutput<Error = Self::Error>;
1479 type TextGenerationState;
1481 type TextCompletion: Completion<Error = Self::Error>;
1483
1484 fn text_execution_control_support(
1488 _runtime: &ModelRuntime<Self>,
1489 ) -> crate::execution_control::ControlSupport {
1490 crate::execution_control::ControlSupport::Unsupported {
1491 reason: "backend has not declared completed-token control support".into(),
1492 }
1493 }
1494
1495 fn text_sampling_control_support(
1498 _runtime: &ModelRuntime<Self>,
1499 ) -> crate::execution_control::ControlSupport {
1500 crate::execution_control::ControlSupport::Unsupported {
1501 reason: "backend has no prospective sampling controls".into(),
1502 }
1503 }
1504
1505 fn intervention_discovery(
1507 _runtime: &ModelRuntime<Self>,
1508 ) -> Result<crate::intervention::InterventionDiscovery, crate::capture::CaptureError> {
1509 Err(crate::capture::CaptureError::Unsupported(
1510 "backend has no intervention discovery".into(),
1511 ))
1512 }
1513
1514 fn validate_text_interventions(
1516 runtime: &ModelRuntime<Self>,
1517 capture: &crate::capture::AdmittedCapturePlan,
1518 plan: &crate::intervention::AdmittedInterventionPlan,
1519 ) -> Result<(), crate::capture::CaptureError> {
1520 if !plan.is_empty() {
1521 return Err(crate::capture::CaptureError::Unsupported(
1522 "backend has no text interventions".into(),
1523 ));
1524 }
1525 Self::validate_text_capture(runtime, capture)
1526 }
1527
1528 fn configure_text_interventions(
1531 runtime: &ModelRuntime<Self>,
1532 state: &mut Self::TextGenerationState,
1533 capture: crate::capture::AdmittedCapturePlan,
1534 plan: crate::intervention::AdmittedInterventionPlan,
1535 ) -> Result<(), crate::capture::CaptureError> {
1536 Self::validate_text_interventions(runtime, &capture, &plan)?;
1537 Self::configure_text_capture(runtime, state, capture)
1538 }
1539
1540 fn capture_discovery(
1542 _runtime: &ModelRuntime<Self>,
1543 ) -> Result<crate::capture::CaptureDiscovery, crate::capture::CaptureError> {
1544 Err(crate::capture::CaptureError::Unsupported(
1545 "backend has no bounded capture discovery".into(),
1546 ))
1547 }
1548
1549 fn configure_text_capture(
1552 _runtime: &ModelRuntime<Self>,
1553 _state: &mut Self::TextGenerationState,
1554 plan: crate::capture::AdmittedCapturePlan,
1555 ) -> Result<(), crate::capture::CaptureError> {
1556 if plan.is_empty() {
1557 Ok(())
1558 } else {
1559 Err(crate::capture::CaptureError::Unsupported(
1560 "backend has no bounded text capture".into(),
1561 ))
1562 }
1563 }
1564
1565 fn validate_text_capture(
1568 _runtime: &ModelRuntime<Self>,
1569 plan: &crate::capture::AdmittedCapturePlan,
1570 ) -> Result<(), crate::capture::CaptureError> {
1571 if plan.is_empty() {
1572 Ok(())
1573 } else {
1574 Err(crate::capture::CaptureError::Unsupported(
1575 "backend has no bounded text capture".into(),
1576 ))
1577 }
1578 }
1579
1580 fn take_text_capture(
1583 _state: &mut Self::TextGenerationState,
1584 ) -> Option<crate::capture::CapturedStep> {
1585 None
1586 }
1587
1588 fn start_text_generation(
1590 backend: &Self,
1591 config: TextGenerationConfig,
1592 ) -> Result<Self::TextGenerationState, Self::Error>;
1593
1594 fn prepare_text_prompt(
1596 backend: &Self,
1597 prompt_token_ids: Vec<u32>,
1598 ) -> Result<Self::Prompt, Self::Error>;
1599
1600 fn submit_text_prefill(
1602 runtime: &mut ModelRuntime<Self>,
1603 prompt: Self::Prompt,
1604 filter: &TokenFilter,
1605 state: &mut Self::TextGenerationState,
1606 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error>;
1607
1608 fn submit_text_decode(
1610 runtime: &mut ModelRuntime<Self>,
1611 token: Self::Token,
1612 filter: &TokenFilter,
1613 state: &mut Self::TextGenerationState,
1614 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error>;
1615}
1616
1617#[derive(Debug, thiserror::Error)]
1619pub enum MultimodalPreparationFailure<B, T>
1620where
1621 B: std::error::Error + 'static,
1622 T: std::error::Error + 'static,
1623{
1624 #[error("backend multimodal preparation failed: {0}")]
1626 Backend(#[source] B),
1627 #[error("multimodal framing text encoding failed: {0}")]
1629 Text(#[source] T),
1630}
1631
1632pub trait MultimodalPreparationBackend: TextGenerationBackend {
1639 fn prepare_multimodal_input<E>(
1641 runtime: &ModelRuntime<Self>,
1642 request: &TokenizedMultimodalRequest,
1643 encode_backend_text: &mut dyn FnMut(&str) -> Result<Vec<u32>, E>,
1644 ) -> Result<Self::Prompt, MultimodalPreparationFailure<Self::Error, E>>
1645 where
1646 E: std::error::Error + Send + Sync + 'static;
1647}
1648
1649pub trait ModelCapabilityBackend: TextGenerationBackend {
1656 fn model_capabilities(
1658 runtime: &ModelRuntime<Self>,
1659 ) -> Result<ModelCapabilities, CapabilityError>;
1660
1661 fn count_prepared_input(
1663 runtime: &ModelRuntime<Self>,
1664 input: &Self::Prompt,
1665 ) -> Result<InputTokenCount, CapabilityError>;
1666
1667 fn estimate_runtime_state(
1669 runtime: &ModelRuntime<Self>,
1670 input: InputTokenCount,
1671 max_output_tokens: u64,
1672 batch_size: u64,
1673 ) -> Result<RuntimeStateEstimate, CapabilityError>;
1674
1675 fn static_memory(runtime: &ModelRuntime<Self>) -> Result<StaticMemoryReport, CapabilityError>;
1677}
1678
1679pub enum PendingTextInput<P, T> {
1682 Prefill(P),
1684 Decode(T),
1686}
1687
1688impl<P, T> PendingTextInput<P, T> {
1689 pub fn as_ref(&self) -> PendingTextInput<&P, &T> {
1691 match self {
1692 Self::Prefill(prompt) => PendingTextInput::Prefill(prompt),
1693 Self::Decode(token) => PendingTextInput::Decode(token),
1694 }
1695 }
1696}
1697
1698#[derive(Debug, thiserror::Error)]
1700pub enum ControlledTextGenerationError<B, C>
1701where
1702 B: std::error::Error + 'static,
1703 C: std::error::Error + 'static,
1704{
1705 #[error("backend text generation failed: {0}")]
1707 Backend(#[source] B),
1708 #[error("text generation constraint failed: {0}")]
1710 Controller(#[source] C),
1711}
1712
1713#[derive(Debug, Clone)]
1715pub struct ControlledToken<T> {
1716 output: T,
1717 token_id: u32,
1718}
1719
1720impl<T> ControlledToken<T> {
1721 pub fn token_id(&self) -> u32 {
1723 self.token_id
1724 }
1725
1726 pub const fn output(&self) -> &T {
1728 &self.output
1729 }
1730
1731 pub fn into_output(self) -> T {
1733 self.output
1734 }
1735}
1736
1737pub struct ControlledTextGeneration<'a, B, C>
1739where
1740 B: TextGenerationBackend,
1741 C: TokenFilterController,
1742{
1743 runtime: &'a mut ModelRuntime<B>,
1744 inner: TextGenerationMachine<B, C>,
1745}
1746
1747struct TextGenerationMachine<B, C>
1748where
1749 B: TextGenerationBackend,
1750 C: TokenFilterController,
1751{
1752 backend_state: B::TextGenerationState,
1753 controller: C,
1754 step: Option<PendingTextInput<B::Prompt, B::Token>>,
1755 completions: Vec<B::TextCompletion>,
1756 remaining_tokens: Option<usize>,
1757}
1758
1759type ControlledGenerationResult<B, C> = Result<
1760 <B as TextGenerationBackend>::Token,
1761 ControlledTextGenerationError<
1762 <B as BackendProvider>::Error,
1763 <C as TokenFilterController>::Error,
1764 >,
1765>;
1766
1767impl<'a, B, C> ControlledTextGeneration<'a, B, C>
1768where
1769 B: TextGenerationBackend,
1770 C: TokenFilterController,
1771{
1772 pub fn new(
1774 runtime: &'a mut ModelRuntime<B>,
1775 prompt_token_ids: Vec<u32>,
1776 config: TextGenerationConfig,
1777 controller: C,
1778 ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1779 let prompt = B::prepare_text_prompt(runtime.backend(), prompt_token_ids)
1780 .map_err(ControlledTextGenerationError::Backend)?;
1781 Self::from_prompt(runtime, prompt, config, controller)
1782 }
1783
1784 pub fn from_prompt(
1786 runtime: &'a mut ModelRuntime<B>,
1787 prompt: B::Prompt,
1788 config: TextGenerationConfig,
1789 controller: C,
1790 ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1791 let inner = TextGenerationMachine::new(runtime, prompt, config, controller)?;
1792 Ok(Self { runtime, inner })
1793 }
1794
1795 pub fn controller_mut(&mut self) -> &mut C {
1797 &mut self.inner.controller
1798 }
1799
1800 pub fn enable_capture(
1802 &mut self,
1803 plan: crate::capture::AdmittedCapturePlan,
1804 ) -> Result<(), crate::capture::CaptureError> {
1805 if !matches!(self.inner.step, Some(PendingTextInput::Prefill(_))) {
1806 return Err(crate::capture::CaptureError::Invalid(
1807 "capture must be configured before generation".into(),
1808 ));
1809 }
1810 B::configure_text_capture(self.runtime, &mut self.inner.backend_state, plan)
1811 }
1812
1813 pub fn enable_interventions(
1815 &mut self,
1816 capture: crate::capture::AdmittedCapturePlan,
1817 plan: crate::intervention::AdmittedInterventionPlan,
1818 ) -> Result<(), crate::capture::CaptureError> {
1819 if !matches!(self.inner.step, Some(PendingTextInput::Prefill(_))) {
1820 return Err(crate::capture::CaptureError::Invalid(
1821 "interventions must be configured before generation".into(),
1822 ));
1823 }
1824 B::configure_text_interventions(self.runtime, &mut self.inner.backend_state, capture, plan)
1825 }
1826
1827 pub fn take_captured_step(&mut self) -> Result<Option<crate::capture::CapturedStep>, B::Error> {
1830 self.inner.resolve_completions_before_decode()?;
1831 Ok(B::take_text_capture(&mut self.inner.backend_state))
1832 }
1833}
1834
1835impl<B, C> TextGenerationMachine<B, C>
1836where
1837 B: TextGenerationBackend,
1838 C: TokenFilterController,
1839{
1840 fn new(
1841 runtime: &ModelRuntime<B>,
1842 prompt: B::Prompt,
1843 config: TextGenerationConfig,
1844 controller: C,
1845 ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1846 let backend_state = B::start_text_generation(runtime.backend(), config)
1847 .map_err(ControlledTextGenerationError::Backend)?;
1848 Ok(Self {
1849 backend_state,
1850 controller,
1851 step: Some(PendingTextInput::Prefill(prompt)),
1852 completions: Vec::new(),
1853 remaining_tokens: config.sampling().max_new_tokens,
1854 })
1855 }
1856
1857 fn retain_completion(&mut self, completion: B::TextCompletion) -> Result<(), B::Error> {
1858 let existing = std::mem::take(&mut self.completions);
1859 let mut retained = Vec::with_capacity(existing.len() + 1);
1860 for pending in existing {
1861 match pending.is_complete() {
1862 Ok(true) => {}
1863 Ok(false) => retained.push(pending),
1864 Err(error) => {
1865 let _ = pending.wait();
1866 for retained_completion in retained.drain(..) {
1867 let _ = retained_completion.wait();
1868 }
1869 let _ = completion.wait();
1870 return Err(error);
1871 }
1872 }
1873 }
1874 retained.push(completion);
1875 self.completions = retained;
1876 Ok(())
1877 }
1878
1879 fn resolve_completions_before_decode(&mut self) -> Result<(), B::Error> {
1880 let existing = std::mem::take(&mut self.completions);
1881 let mut remaining = existing.into_iter();
1882 while let Some(completion) = remaining.next() {
1883 let result = match completion.is_complete() {
1884 Ok(true) => Ok(()),
1885 Ok(false) => completion.wait(),
1886 Err(error) => {
1887 let _ = completion.wait();
1888 Err(error)
1889 }
1890 };
1891 if let Err(error) = result {
1892 for pending in remaining {
1893 let _ = pending.wait();
1894 }
1895 return Err(error);
1896 }
1897 }
1898 Ok(())
1899 }
1900
1901 #[allow(clippy::type_complexity)]
1902 fn next_committed(
1903 &mut self,
1904 runtime: &mut ModelRuntime<B>,
1905 ) -> Option<Result<ControlledToken<B::Token>, ControlledTextGenerationError<B::Error, C::Error>>>
1906 {
1907 let token = match self.next_output(runtime)? {
1908 Ok(token) => token,
1909 Err(error) => return Some(Err(error)),
1910 };
1911 let token_id = match token.token_id() {
1912 Ok(token_id) => token_id,
1913 Err(error) => {
1914 self.step = None;
1915 return Some(Err(ControlledTextGenerationError::Backend(error)));
1916 }
1917 };
1918 if let Err(error) = self.controller.commit_token(token_id) {
1919 self.step = None;
1920 return Some(Err(ControlledTextGenerationError::Controller(error)));
1921 }
1922 Some(Ok(ControlledToken {
1923 output: token,
1924 token_id,
1925 }))
1926 }
1927
1928 fn next_output(
1929 &mut self,
1930 runtime: &mut ModelRuntime<B>,
1931 ) -> Option<ControlledGenerationResult<B, C>> {
1932 if self.remaining_tokens == Some(0) {
1933 self.step = None;
1934 return None;
1935 }
1936 let step = self.step.take()?;
1937 if matches!(step, PendingTextInput::Decode(_)) {
1938 if let Err(error) = self.resolve_completions_before_decode() {
1939 return Some(Err(ControlledTextGenerationError::Backend(error)));
1940 }
1941 }
1942 let filter = match self.controller.current_filter() {
1943 Ok(filter) => filter,
1944 Err(error) => return Some(Err(ControlledTextGenerationError::Controller(error))),
1945 };
1946 let submission = match step {
1947 PendingTextInput::Prefill(prompt) => {
1948 B::submit_text_prefill(runtime, prompt, &filter, &mut self.backend_state)
1949 }
1950 PendingTextInput::Decode(token) => {
1951 B::submit_text_decode(runtime, token, &filter, &mut self.backend_state)
1952 }
1953 };
1954 let submission = match submission {
1955 Ok(submission) => submission,
1956 Err(error) => return Some(Err(ControlledTextGenerationError::Backend(error))),
1957 };
1958 let token = submission.output;
1959 if let Err(error) = self.retain_completion(submission.completion) {
1960 return Some(Err(ControlledTextGenerationError::Backend(error)));
1961 }
1962 self.step = Some(PendingTextInput::Decode(token.clone()));
1963 if let Some(remaining_tokens) = &mut self.remaining_tokens {
1964 *remaining_tokens -= 1;
1965 }
1966 Some(Ok(token))
1967 }
1968}
1969
1970impl<B, C> Iterator for ControlledTextGeneration<'_, B, C>
1971where
1972 B: TextGenerationBackend,
1973 C: TokenFilterController,
1974{
1975 type Item =
1976 Result<ControlledToken<B::Token>, ControlledTextGenerationError<B::Error, C::Error>>;
1977
1978 fn next(&mut self) -> Option<Self::Item> {
1979 self.inner.next_committed(self.runtime)
1980 }
1981}
1982
1983impl<B, C> Drop for TextGenerationMachine<B, C>
1984where
1985 B: TextGenerationBackend,
1986 C: TokenFilterController,
1987{
1988 fn drop(&mut self) {
1989 for completion in self.completions.drain(..) {
1990 let _ = completion.wait();
1991 }
1992 }
1993}
1994
1995pub struct TextGeneration<'a, B: TextGenerationBackend> {
2002 runtime: &'a mut ModelRuntime<B>,
2003 inner: TextGenerationMachine<B, UnconstrainedTokens>,
2004}
2005
2006impl<'a, B: TextGenerationBackend> TextGeneration<'a, B> {
2007 pub fn new(
2009 runtime: &'a mut ModelRuntime<B>,
2010 prompt_token_ids: Vec<u32>,
2011 config: TextGenerationConfig,
2012 ) -> Result<Self, B::Error> {
2013 let prompt = B::prepare_text_prompt(runtime.backend(), prompt_token_ids)?;
2014 Self::from_prompt(runtime, prompt, config)
2015 }
2016
2017 pub fn from_prompt(
2019 runtime: &'a mut ModelRuntime<B>,
2020 prompt: B::Prompt,
2021 config: TextGenerationConfig,
2022 ) -> Result<Self, B::Error> {
2023 let inner = TextGenerationMachine::new(runtime, prompt, config, UnconstrainedTokens)
2024 .map_err(unreachable_unconstrained_error)?;
2025 Ok(Self { runtime, inner })
2026 }
2027}
2028
2029fn unreachable_unconstrained_error<B>(
2030 error: ControlledTextGenerationError<B, std::convert::Infallible>,
2031) -> B
2032where
2033 B: std::error::Error + 'static,
2034{
2035 match error {
2036 ControlledTextGenerationError::Backend(error) => error,
2037 ControlledTextGenerationError::Controller(error) => match error {},
2038 }
2039}
2040
2041impl<B: TextGenerationBackend> Iterator for TextGeneration<'_, B> {
2042 type Item = Result<B::Token, B::Error>;
2043
2044 fn next(&mut self) -> Option<Self::Item> {
2045 self.inner
2046 .next_output(self.runtime)
2047 .map(|result| result.map_err(unreachable_unconstrained_error))
2048 }
2049}
2050
2051pub trait DistributedSession {
2058 type Value;
2060 type Completion: Completion<Error = Self::Error>;
2062 type Error: std::error::Error + Send + Sync + 'static;
2064
2065 fn descriptor(&self) -> DistributedSessionDescriptor;
2067 fn capabilities(&self) -> DistributedCapabilities;
2069
2070 fn all_reduce_sum(
2072 &self,
2073 scope: CollectiveScope,
2074 input: &Self::Value,
2075 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2076
2077 fn all_gather(
2079 &self,
2080 scope: CollectiveScope,
2081 input: &Self::Value,
2082 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2083
2084 fn all_to_all_v(
2086 &self,
2087 scope: CollectiveScope,
2088 input: &Self::Value,
2089 send_counts: &[usize],
2090 receive_counts: &[usize],
2091 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2092
2093 fn send(
2095 &self,
2096 scope: CollectiveScope,
2097 peer: usize,
2098 input: &Self::Value,
2099 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2100
2101 fn receive(
2103 &self,
2104 scope: CollectiveScope,
2105 peer: usize,
2106 value: &ValueDescriptor,
2107 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2108
2109 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error>;
2111}
2112
2113pub trait DistributedBackend: BackendProvider {
2115 type DistributedSession: DistributedSession<Error = Self::Error>;
2117
2118 fn distributed_session(session: &Self::Session) -> Option<&Self::DistributedSession>;
2120}
2121
2122#[cfg(test)]
2123mod tests {
2124 use super::*;
2125 use std::{convert::Infallible, io::Write};
2126
2127 #[test]
2128 fn text_generation_config_validates_portable_mirostat_strategy() {
2129 let sampling = crate::generation::resolve_generation_config(
2130 None,
2131 crate::generation::GenerationConfigOverrides {
2132 temperature: Some(0.8),
2133 ..crate::generation::GenerationConfigOverrides::default()
2134 },
2135 )
2136 .unwrap();
2137 let config = TextGenerationConfig::new(sampling)
2138 .with_seed(7)
2139 .with_mirostat_v2(5.0, 0.1)
2140 .unwrap();
2141 assert_eq!(config.seed(), 7);
2142 assert_eq!(
2143 config.strategy(),
2144 TextSamplingStrategy::MirostatV2 { tau: 5.0, eta: 0.1 }
2145 );
2146 assert!(matches!(
2147 TextGenerationConfig::new(sampling).with_mirostat_v2(0.0, 0.1),
2148 Err(GenerationError::InvalidMirostatTau(0.0))
2149 ));
2150 assert!(matches!(
2151 TextGenerationConfig::new(sampling).with_mirostat_v2(5.0, f32::NAN),
2152 Err(GenerationError::InvalidMirostatEta(value)) if value.is_nan()
2153 ));
2154 }
2155
2156 #[derive(Debug, Clone)]
2157 struct Done;
2158 impl Completion for Done {
2159 type Error = Infallible;
2160 fn is_complete(&self) -> Result<bool, Self::Error> {
2161 Ok(true)
2162 }
2163 fn wait(&self) -> Result<(), Self::Error> {
2164 Ok(())
2165 }
2166 }
2167 struct Mock;
2168 impl BackendProvider for Mock {
2169 type ModelConfig = u32;
2170 type Model = u32;
2171 type Session = MockSession;
2172 type Error = Infallible;
2173 fn descriptor(&self) -> BackendDescriptor {
2174 BackendDescriptor::new("mock", "1")
2175 }
2176 fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error> {
2177 Ok(vec![])
2178 }
2179 fn prepare_model(&self, config: u32) -> Result<PreparedModel<u32>, Self::Error> {
2180 Ok(PreparedModel::new(config, SessionCapabilities::default()))
2181 }
2182 fn create_session(&self, model: PreparedModel<u32>) -> Result<MockSession, Self::Error> {
2183 Ok(MockSession {
2184 model: model.into_inner(),
2185 tokens: vec![],
2186 distributed: None,
2187 })
2188 }
2189 }
2190
2191 #[derive(Default)]
2192 struct LoadingMock {
2193 selections: std::sync::atomic::AtomicUsize,
2194 materializations: std::sync::atomic::AtomicUsize,
2195 }
2196 struct LoadingMockSession;
2197
2198 struct LoadingConfigurationResolver;
2199
2200 impl ModelConfigurationResolver for LoadingConfigurationResolver {
2201 type ArtifactPlan = ();
2202
2203 fn resolve_safetensors(
2204 &self,
2205 json: &serde_json::Value,
2206 ) -> Result<crate::ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
2207 Ok(crate::ResolvedModelConfiguration::new(
2208 crate::ModelConfiguration::new(
2209 "llama",
2210 "llama",
2211 "llama",
2212 crate::LoadingProtocol::Model,
2213 Some(json.clone()),
2214 )?,
2215 (),
2216 ))
2217 }
2218
2219 fn resolve_gguf(
2220 &self,
2221 architecture: &str,
2222 _checkpoint: &eredu_gguf::Checkpoint,
2223 ) -> Result<crate::ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
2224 if architecture != "llama" {
2225 return Err(ArtifactError::UnsupportedGgufArchitecture(
2226 architecture.into(),
2227 ));
2228 }
2229 Ok(crate::ResolvedModelConfiguration::new(
2230 crate::ModelConfiguration::new(
2231 architecture,
2232 architecture,
2233 "llama",
2234 crate::LoadingProtocol::Model,
2235 None,
2236 )?,
2237 (),
2238 ))
2239 }
2240
2241 fn gguf_companion_requirements(
2242 &self,
2243 _architecture: &str,
2244 _checkpoint: &eredu_gguf::Checkpoint,
2245 ) -> Result<Vec<crate::GgufCompanionRequirement>, ArtifactError> {
2246 Ok(Vec::new())
2247 }
2248 }
2249
2250 static LOADING_CONFIGURATION_RESOLVER: LoadingConfigurationResolver =
2251 LoadingConfigurationResolver;
2252
2253 impl BackendProvider for LoadingMock {
2254 type ModelConfig = (ModelPreparationPlan, u32);
2255 type Model = u32;
2256 type Session = LoadingMockSession;
2257 type Error = std::convert::Infallible;
2258
2259 fn descriptor(&self) -> BackendDescriptor {
2260 BackendDescriptor::new("loading-mock", "1")
2261 }
2262
2263 fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error> {
2264 Ok(Vec::new())
2265 }
2266
2267 fn prepare_model(
2268 &self,
2269 (plan, model): Self::ModelConfig,
2270 ) -> Result<PreparedModel<Self::Model>, Self::Error> {
2271 self.materializations
2272 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2273 assert_eq!(plan.inspection().configuration().family(), "llama");
2274 Ok(PreparedModel::new(
2275 model,
2276 plan.admitted_session_capabilities(),
2277 ))
2278 }
2279
2280 fn create_session(
2281 &self,
2282 _: PreparedModel<Self::Model>,
2283 ) -> Result<Self::Session, Self::Error> {
2284 Ok(LoadingMockSession)
2285 }
2286 }
2287
2288 impl BackendSession<LoadingMock> for LoadingMockSession {
2289 type PrefillInput = ();
2290 type DecodeInput = ();
2291 type Output = ();
2292 type Completion = LoadingDone;
2293
2294 fn capabilities(&self) -> SessionCapabilities {
2295 SessionCapabilities::default()
2296 }
2297
2298 fn prefill(
2299 &mut self,
2300 _: &LoadingMock,
2301 _: (),
2302 ) -> Result<Submission<(), LoadingDone>, std::convert::Infallible> {
2303 Ok(Submission {
2304 output: (),
2305 completion: LoadingDone,
2306 })
2307 }
2308
2309 fn decode(
2310 &mut self,
2311 _: &LoadingMock,
2312 _: (),
2313 ) -> Result<Submission<(), LoadingDone>, std::convert::Infallible> {
2314 Ok(Submission {
2315 output: (),
2316 completion: LoadingDone,
2317 })
2318 }
2319
2320 fn observe_output(
2321 &self,
2322 _: &LoadingMock,
2323 _: &(),
2324 ) -> Result<ObservationSet, std::convert::Infallible> {
2325 Ok(ObservationSet::new())
2326 }
2327 }
2328
2329 #[derive(Debug, Clone, Copy)]
2330 struct LoadingDone;
2331
2332 impl Completion for LoadingDone {
2333 type Error = std::convert::Infallible;
2334
2335 fn is_complete(&self) -> Result<bool, Self::Error> {
2336 Ok(true)
2337 }
2338
2339 fn wait(&self) -> Result<(), Self::Error> {
2340 Ok(())
2341 }
2342 }
2343
2344 impl ModelLoadingBackend for LoadingMock {
2345 type LoadOptions = u32;
2346 type SelectedPreparation = (u32, crate::PreparationAdmission);
2347 type ConfigurationResolver = LoadingConfigurationResolver;
2348
2349 fn configuration_resolver(&self) -> &Self::ConfigurationResolver {
2350 &LOADING_CONFIGURATION_RESOLVER
2351 }
2352
2353 fn select_preparation(
2354 &self,
2355 _: &ArtifactInspection,
2356 options: &Self::LoadOptions,
2357 ) -> Result<Self::SelectedPreparation, Self::Error> {
2358 self.selections
2359 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2360 let policy = crate::PreparationPolicy::default().with_required_session_capabilities(
2361 SessionCapabilities::default().with_activation_inspection(*options == 99),
2362 );
2363 let request = crate::PreparationAdmissionRequest::new(
2364 crate::LoadingProtocol::Model,
2365 crate::ArtifactFormat::SafeTensors,
2366 policy,
2367 crate::ArchitecturePreparationCapabilities::new(
2368 false,
2369 true,
2370 false,
2371 false,
2372 false,
2373 crate::InputModalities::TEXT,
2374 ),
2375 );
2376 let admission = crate::admit_preparation(
2377 request,
2378 crate::PreparationMechanismCapabilities::new(true, true)
2379 .with_residency(crate::ResidencyRequest::FullyResident, true)
2380 .with_input_modalities(crate::InputModalities::TEXT)
2381 .with_session(
2382 SessionCapabilities::default().with_activation_inspection(*options == 99),
2383 ),
2384 )
2385 .expect("mock admission facts are coherent");
2386 Ok((*options, admission))
2387 }
2388
2389 fn selected_preparation_admission(
2390 &self,
2391 selected: &Self::SelectedPreparation,
2392 ) -> crate::PreparationAdmission {
2393 selected.1
2394 }
2395
2396 fn model_config(
2397 &self,
2398 selected: SelectedModelPreparation<Self>,
2399 ) -> Result<Self::ModelConfig, Self::Error> {
2400 let (plan, (selected, _admission)) = selected.into_parts();
2401 Ok((plan, selected))
2402 }
2403 }
2404
2405 fn write_loading_fixture(root: &Path) {
2406 std::fs::write(root.join("config.json"), r#"{"model_type":"llama"}"#).unwrap();
2407 let header = br#"{"token_embd.weight":{"dtype":"F32","shape":[1],"data_offsets":[0,4]}}"#;
2408 let mut file = std::fs::File::create(root.join("model.safetensors")).unwrap();
2409 file.write_all(&(header.len() as u64).to_le_bytes())
2410 .unwrap();
2411 file.write_all(header).unwrap();
2412 file.write_all(&[0; 4]).unwrap();
2413 }
2414 struct MockSession {
2415 model: u32,
2416 tokens: Vec<u32>,
2417 distributed: Option<MockDistributed>,
2418 }
2419 impl BackendSession<Mock> for MockSession {
2420 type PrefillInput = Vec<u32>;
2421 type DecodeInput = u32;
2422 type Output = u32;
2423 type Completion = Done;
2424 fn capabilities(&self) -> SessionCapabilities {
2425 SessionCapabilities::default()
2426 }
2427 fn prefill(
2428 &mut self,
2429 _: &Mock,
2430 input: Vec<u32>,
2431 ) -> Result<Submission<u32, Done>, Infallible> {
2432 self.tokens.extend(input);
2433 Ok(Submission {
2434 output: self.tokens.len() as u32 + self.model,
2435 completion: Done,
2436 })
2437 }
2438 fn decode(&mut self, _: &Mock, input: u32) -> Result<Submission<u32, Done>, Infallible> {
2439 self.tokens.push(input);
2440 Ok(Submission {
2441 output: self.tokens.len() as u32 + self.model,
2442 completion: Done,
2443 })
2444 }
2445
2446 fn observe_output(&self, _: &Mock, output: &u32) -> Result<ObservationSet, Infallible> {
2447 let mut observations = ObservationSet::new();
2448 observations
2449 .insert(
2450 "mock.output",
2451 crate::ObservationValue::Unsigned(u64::from(*output)),
2452 )
2453 .unwrap();
2454 Ok(observations)
2455 }
2456 }
2457
2458 impl TextGenerationBackend for Mock {
2459 type Prompt = Vec<u32>;
2460 type Token = u32;
2461 type TextGenerationState = (u32, u64);
2462 type TextCompletion = Done;
2463
2464 fn start_text_generation(
2465 _: &Self,
2466 config: TextGenerationConfig,
2467 ) -> Result<Self::TextGenerationState, Self::Error> {
2468 Ok((config.sampling().top_k as u32, config.seed()))
2469 }
2470
2471 fn prepare_text_prompt(
2472 _: &Self,
2473 prompt_token_ids: Vec<u32>,
2474 ) -> Result<Self::Prompt, Self::Error> {
2475 Ok(prompt_token_ids)
2476 }
2477
2478 fn submit_text_prefill(
2479 runtime: &mut ModelRuntime<Self>,
2480 prompt: Self::Prompt,
2481 filter: &TokenFilter,
2482 state: &mut Self::TextGenerationState,
2483 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error> {
2484 let submission = runtime.prefill(prompt)?;
2485 Ok(Submission {
2486 output: apply_mock_filter(submission.output + state.0 + state.1 as u32, filter),
2487 completion: submission.completion,
2488 })
2489 }
2490
2491 fn submit_text_decode(
2492 runtime: &mut ModelRuntime<Self>,
2493 token: Self::Token,
2494 filter: &TokenFilter,
2495 _: &mut Self::TextGenerationState,
2496 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error> {
2497 let submission = runtime.decode(token)?;
2498 Ok(Submission {
2499 output: apply_mock_filter(submission.output, filter),
2500 completion: submission.completion,
2501 })
2502 }
2503 }
2504
2505 impl MultimodalPreparationBackend for Mock {
2506 fn prepare_multimodal_input<E>(
2507 _: &ModelRuntime<Self>,
2508 request: &TokenizedMultimodalRequest,
2509 _: &mut dyn FnMut(&str) -> Result<Vec<u32>, E>,
2510 ) -> Result<Self::Prompt, MultimodalPreparationFailure<Self::Error, E>>
2511 where
2512 E: std::error::Error + Send + Sync + 'static,
2513 {
2514 let mut prompt = Vec::new();
2515 for segment in request.segments() {
2516 match segment {
2517 crate::TokenizedMultimodalSegment::TokenIds(ids) => {
2518 prompt.extend_from_slice(ids);
2519 }
2520 crate::TokenizedMultimodalSegment::Media(crate::Media::Image(_)) => {
2521 prompt.push(1_001);
2522 }
2523 crate::TokenizedMultimodalSegment::Media(crate::Media::Video(_)) => {
2524 prompt.push(1_002);
2525 }
2526 crate::TokenizedMultimodalSegment::Media(crate::Media::Audio(_)) => {
2527 prompt.push(1_003);
2528 }
2529 }
2530 }
2531 Ok(prompt)
2532 }
2533 }
2534
2535 impl ModelCapabilityBackend for Mock {
2536 fn model_capabilities(
2537 _: &ModelRuntime<Self>,
2538 ) -> Result<ModelCapabilities, CapabilityError> {
2539 Ok(ModelCapabilities {
2540 effective_model_type: "mock".into(),
2541 native_max_context: crate::Observed::exact(64, "mock configuration"),
2542 effective_max_context: crate::Observed::exact(64, "mock configuration"),
2543 state_strategy: crate::CacheStateStrategy::FullKv,
2544 modalities: crate::InputModalities::TEXT,
2545 estimation: crate::EstimationCompleteness::Complete,
2546 })
2547 }
2548
2549 fn count_prepared_input(
2550 _: &ModelRuntime<Self>,
2551 input: &Self::Prompt,
2552 ) -> Result<InputTokenCount, CapabilityError> {
2553 Ok(InputTokenCount::text(input.len() as u64))
2554 }
2555
2556 fn estimate_runtime_state(
2557 _: &ModelRuntime<Self>,
2558 input: InputTokenCount,
2559 max_output_tokens: u64,
2560 batch_size: u64,
2561 ) -> Result<RuntimeStateEstimate, CapabilityError> {
2562 crate::estimate_runtime_state(
2563 &crate::StateMemoryLayout::new(
2564 crate::LayerSchedule::new(
2565 1,
2566 vec![crate::cache::LayerCachePolicy::key_only(
2567 crate::AttentionPolicy::Full,
2568 1,
2569 2,
2570 )
2571 .unwrap()],
2572 )
2573 .unwrap(),
2574 vec![0],
2575 1,
2576 1,
2577 crate::EstimationCompleteness::Complete,
2578 )
2579 .unwrap(),
2580 input,
2581 max_output_tokens,
2582 batch_size,
2583 std::num::NonZeroU8::new(4).unwrap(),
2584 )
2585 }
2586
2587 fn static_memory(
2588 runtime: &ModelRuntime<Self>,
2589 ) -> Result<StaticMemoryReport, CapabilityError> {
2590 let unavailable = || crate::Observed::unavailable("mock does not expose this counter");
2591 Ok(StaticMemoryReport {
2592 logical_parameter_bytes: crate::Observed::exact(
2593 u64::from(runtime.session().model),
2594 "mock model",
2595 ),
2596 current_host_resident_bytes: unavailable(),
2597 current_device_resident_bytes: unavailable(),
2598 planned_disk_backed_bytes: unavailable(),
2599 backend_active_allocation_bytes: unavailable(),
2600 backend_allocator_cache_bytes: unavailable(),
2601 physical_semantics: crate::PhysicalMemorySemantics::Unknown,
2602 currently_cached_shards: unavailable(),
2603 })
2604 }
2605 }
2606
2607 fn apply_mock_filter(candidate: u32, filter: &TokenFilter) -> u32 {
2608 let Some(allowed) = filter.allowed_mask() else {
2609 return candidate;
2610 };
2611 allowed
2612 .get(candidate as usize)
2613 .copied()
2614 .unwrap_or(false)
2615 .then_some(candidate)
2616 .or_else(|| {
2617 allowed
2618 .iter()
2619 .position(|allowed| *allowed)
2620 .map(|token| token as u32)
2621 })
2622 .expect("validated token filters allow at least one token")
2623 }
2624
2625 #[test]
2626 fn generic_loader_inspects_plans_and_prepares_on_the_selected_backend() {
2627 let root = tempfile::tempdir().unwrap();
2628 write_loading_fixture(root.path());
2629 let prepared = load_model(&LoadingMock::default(), root.path(), 41).unwrap();
2630 assert_eq!(*prepared, 41);
2631
2632 let runtime = ModelRuntime::load(LoadingMock::default(), root.path(), 7).unwrap();
2633 assert_eq!(runtime.backend().descriptor().name, "loading-mock");
2634
2635 let missing = root.path().join("missing");
2636 assert!(matches!(
2637 load_model(&LoadingMock::default(), &missing, 1),
2638 Err(ModelLoadError::Artifact(ArtifactError::MissingArtifact(path)))
2639 if path == missing
2640 ));
2641 }
2642
2643 #[test]
2644 fn session_requirement_is_retained_by_the_single_admission() {
2645 let root = tempfile::tempdir().unwrap();
2646 write_loading_fixture(root.path());
2647 let backend = LoadingMock::default();
2648
2649 let prepared = load_model(&backend, root.path(), 99).unwrap();
2650
2651 assert_eq!(*prepared, 99);
2652 assert_eq!(
2653 backend
2654 .selections
2655 .load(std::sync::atomic::Ordering::Relaxed),
2656 1
2657 );
2658 assert_eq!(
2659 backend
2660 .materializations
2661 .load(std::sync::atomic::Ordering::Relaxed),
2662 1
2663 );
2664 }
2665
2666 struct FixedController {
2667 tokens: Vec<u32>,
2668 committed: usize,
2669 }
2670
2671 impl TokenFilterController for FixedController {
2672 type Error = Infallible;
2673
2674 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
2675 let mut allowed = vec![false; 64];
2676 allowed[self.tokens[self.committed] as usize] = true;
2677 Ok(TokenFilter::allowed(allowed).unwrap())
2678 }
2679
2680 fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error> {
2681 assert_eq!(token_id, self.tokens[self.committed]);
2682 self.committed += 1;
2683 Ok(())
2684 }
2685
2686 fn is_complete(&mut self) -> Result<bool, Self::Error> {
2687 Ok(self.committed == self.tokens.len())
2688 }
2689 }
2690
2691 #[test]
2692 fn mock_prefill_and_multiple_decode_steps() {
2693 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2694 let prefill = runtime.prefill(vec![1, 2]).unwrap();
2695 assert_eq!(prefill.output, 12);
2696 assert!(prefill.completion.is_complete().unwrap());
2697 assert_eq!(runtime.decode(3).unwrap().output, 13);
2698 assert_eq!(runtime.decode(4).unwrap().output, 14);
2699 }
2700
2701 #[test]
2702 fn portable_text_generation_prefills_and_decodes_without_tensor_types() {
2703 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2704 let sampling = crate::resolve_generation_config(
2705 None,
2706 crate::GenerationConfigOverrides {
2707 max_new_tokens: Some(3),
2708 ..Default::default()
2709 },
2710 )
2711 .unwrap();
2712 let mut generation = TextGeneration::new(
2713 &mut runtime,
2714 vec![1, 2],
2715 TextGenerationConfig::new(sampling).with_seed(3),
2716 )
2717 .unwrap();
2718 assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 55);
2719 assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 13);
2720 assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 14);
2721 assert!(generation.next().is_none());
2722 }
2723
2724 #[test]
2725 fn portable_media_preparation_feeds_the_existing_generation_contract() {
2726 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2727 let request = crate::MultimodalRequest::new(vec![
2728 crate::MultimodalSegment::TokenIds(vec![7, 8]),
2729 crate::MultimodalSegment::Media(crate::Media::Image(
2730 crate::RgbImage::new(vec![5, 6, 7], 1, 1).unwrap(),
2731 )),
2732 crate::MultimodalSegment::TokenIds(vec![9]),
2733 ])
2734 .unwrap()
2735 .tokenize::<Infallible>(|_| unreachable!("request is already tokenized"))
2736 .unwrap();
2737 let prompt = Mock::prepare_multimodal_input(&runtime, &request, &mut |_| {
2738 Ok::<_, Infallible>(Vec::new())
2739 })
2740 .unwrap();
2741 assert_eq!(prompt, vec![7, 8, 1_001, 9]);
2742
2743 let sampling = crate::resolve_generation_config(
2744 None,
2745 crate::GenerationConfigOverrides {
2746 max_new_tokens: Some(2),
2747 ..Default::default()
2748 },
2749 )
2750 .unwrap();
2751 let mut generation =
2752 TextGeneration::from_prompt(&mut runtime, prompt, TextGenerationConfig::new(sampling))
2753 .unwrap();
2754 assert!(generation.next().unwrap().is_ok());
2755 assert!(generation.next().unwrap().is_ok());
2756 assert!(generation.next().is_none());
2757 }
2758
2759 #[test]
2760 fn model_capability_extension_observes_the_selected_mock_session() {
2761 let runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2762 let capabilities = Mock::model_capabilities(&runtime).unwrap();
2763 assert_eq!(capabilities.effective_model_type, "mock");
2764 let input = Mock::count_prepared_input(&runtime, &vec![1, 2, 3]).unwrap();
2765 assert_eq!(input.model_positions, 3);
2766 let state = Mock::estimate_runtime_state(&runtime, input, 2, 1).unwrap();
2767 assert_eq!(state.requested_state_bytes, 5 * 2 * 4);
2768 assert_eq!(
2769 Mock::static_memory(&runtime)
2770 .unwrap()
2771 .logical_parameter_bytes
2772 .value(),
2773 Some(&10)
2774 );
2775 }
2776
2777 #[test]
2778 fn controlled_generation_applies_portable_filters_and_commits_tokens() {
2779 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2780 let sampling = crate::resolve_generation_config(
2781 None,
2782 crate::GenerationConfigOverrides {
2783 max_new_tokens: Some(2),
2784 ..Default::default()
2785 },
2786 )
2787 .unwrap();
2788 let controller = FixedController {
2789 tokens: vec![7, 8],
2790 committed: 0,
2791 };
2792 let mut generation = ControlledTextGeneration::new(
2793 &mut runtime,
2794 vec![1, 2],
2795 TextGenerationConfig::new(sampling),
2796 controller,
2797 )
2798 .unwrap();
2799 assert_eq!(generation.next().unwrap().unwrap().token_id(), 7);
2800 assert_eq!(generation.next().unwrap().unwrap().token_id(), 8);
2801 assert!(generation.controller_mut().is_complete().unwrap());
2802 assert!(generation.next().is_none());
2803 }
2804
2805 fn continuation_config(limit: usize) -> TextGenerationConfig {
2806 TextGenerationConfig::new(
2807 crate::resolve_generation_config(
2808 None,
2809 crate::GenerationConfigOverrides {
2810 max_new_tokens: Some(limit),
2811 ..Default::default()
2812 },
2813 )
2814 .unwrap(),
2815 )
2816 }
2817
2818 #[test]
2819 fn detached_ordinary_continuation_preserves_pending_input_and_commit_order() {
2820 let controller = || FixedController {
2821 tokens: vec![7, 8, 9],
2822 committed: 0,
2823 };
2824 let mut ordinary_runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2825 let ordinary: Vec<_> = ControlledTextGeneration::new(
2826 &mut ordinary_runtime,
2827 vec![1, 2],
2828 continuation_config(3),
2829 controller(),
2830 )
2831 .unwrap()
2832 .map(|token| token.unwrap().token_id())
2833 .collect();
2834
2835 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2836 let mut driver = TextGenerationDriver::new(&mut runtime);
2837 let mut state = driver
2838 .start(vec![1, 2], continuation_config(3), controller())
2839 .unwrap();
2840 state.require_quiescent().unwrap();
2841 assert!(state.is_prefill_pending());
2842 assert!(driver.runtime().session().tokens.is_empty());
2843 let mut actual = Vec::new();
2844 for (index, expected) in ordinary.iter().enumerate() {
2845 let token = driver.advance(&mut state).unwrap().unwrap().token_id();
2846 actual.push(token);
2847 assert_eq!(token, *expected);
2848 assert_eq!(state.controller().committed, index + 1);
2849 assert_eq!(state.remaining_tokens(), Some(2 - index));
2850 let mut model_inputs = vec![1, 2];
2852 model_inputs.extend_from_slice(&ordinary[..index]);
2853 assert_eq!(driver.runtime().session().tokens, model_inputs);
2854 assert!(!state.is_prefill_pending());
2855 assert!(matches!(
2856 driver.advance(&mut state),
2857 Err(TextContinuationError::NotQuiescent)
2858 ));
2859 assert_eq!(state.controller().committed, index + 1);
2860 assert!(driver.take_completed_step(&mut state).unwrap().is_none());
2861 state.require_quiescent().unwrap();
2862 }
2863 assert!(driver.advance(&mut state).unwrap().is_none());
2864 assert_eq!(actual, ordinary);
2865 assert_eq!(
2866 driver.runtime().session().tokens,
2867 ordinary_runtime.session().tokens
2868 );
2869 }
2870
2871 #[test]
2872 fn detached_continuation_cannot_attach_to_another_driver() {
2873 let mut first = ModelRuntime::prepare(Mock, 10).unwrap();
2874 let mut other = ModelRuntime::prepare(Mock, 10).unwrap();
2875 let mut owner = TextGenerationDriver::new(&mut first);
2876 let mut state = owner
2877 .start(vec![1, 2], continuation_config(2), UnconstrainedTokens)
2878 .unwrap();
2879 let mut foreign = TextGenerationDriver::new(&mut other);
2880 assert!(matches!(
2881 foreign.advance(&mut state),
2882 Err(TextContinuationError::IncompatibleDriver)
2883 ));
2884 assert!(foreign.runtime().session().tokens.is_empty());
2885 assert!(owner.advance(&mut state).unwrap().is_some());
2886 assert!(matches!(
2887 foreign.take_completed_step(&mut state),
2888 Err(TextContinuationError::IncompatibleDriver)
2889 ));
2890 owner.take_completed_step(&mut state).unwrap();
2891 drop(owner);
2892 let mut replacement = TextGenerationDriver::new(&mut first);
2893 assert!(matches!(
2894 replacement.advance(&mut state),
2895 Err(TextContinuationError::IncompatibleDriver)
2896 ));
2897 assert_eq!(replacement.runtime().session().tokens, vec![1, 2]);
2898 }
2899
2900 #[test]
2901 fn detached_continuation_failure_remains_fenced_after_draining() {
2902 struct RejectCommit;
2903 impl TokenFilterController for RejectCommit {
2904 type Error = std::io::Error;
2905 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
2906 Ok(TokenFilter::All)
2907 }
2908 fn commit_token(&mut self, _: u32) -> Result<(), Self::Error> {
2909 Err(std::io::Error::other("commit rejected"))
2910 }
2911 fn is_complete(&mut self) -> Result<bool, Self::Error> {
2912 Ok(false)
2913 }
2914 }
2915 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2916 let mut driver = TextGenerationDriver::new(&mut runtime);
2917 let mut state = driver
2918 .start(vec![1, 2], continuation_config(3), RejectCommit)
2919 .unwrap();
2920 assert!(matches!(
2921 driver.advance(&mut state),
2922 Err(TextContinuationError::Generation(
2923 ControlledTextGenerationError::Controller(_)
2924 ))
2925 ));
2926 driver.take_completed_step(&mut state).unwrap();
2927 assert!(matches!(
2928 state.require_quiescent(),
2929 Err(TextContinuationError::Failed)
2930 ));
2931 assert!(matches!(
2932 driver.advance(&mut state),
2933 Err(TextContinuationError::Failed)
2934 ));
2935 assert_eq!(driver.runtime().session().tokens, vec![1, 2]);
2936 }
2937
2938 #[test]
2939 fn detached_continuation_caught_unwind_cannot_be_resumed() {
2940 struct PanickingFilter;
2941 impl TokenFilterController for PanickingFilter {
2942 type Error = Infallible;
2943 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
2944 panic!("filter failed while preparing the decision")
2945 }
2946 fn commit_token(&mut self, _: u32) -> Result<(), Self::Error> {
2947 Ok(())
2948 }
2949 fn is_complete(&mut self) -> Result<bool, Self::Error> {
2950 Ok(false)
2951 }
2952 }
2953 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2954 let mut driver = TextGenerationDriver::new(&mut runtime);
2955 let mut state = driver
2956 .start(vec![1, 2], continuation_config(3), PanickingFilter)
2957 .unwrap();
2958 assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2959 driver.advance(&mut state)
2960 }))
2961 .is_err());
2962 driver.take_completed_step(&mut state).unwrap();
2963 assert!(matches!(
2964 state.require_quiescent(),
2965 Err(TextContinuationError::Failed)
2966 ));
2967 assert!(matches!(
2968 driver.advance(&mut state),
2969 Err(TextContinuationError::Failed)
2970 ));
2971 assert!(driver.runtime().session().tokens.is_empty());
2972 }
2973
2974 #[derive(Debug, Clone)]
2975 struct MockDistributed {
2976 descriptor: DistributedSessionDescriptor,
2977 }
2978
2979 impl DistributedSession for MockDistributed {
2980 type Value = Vec<u32>;
2981 type Completion = Done;
2982 type Error = Infallible;
2983
2984 fn descriptor(&self) -> DistributedSessionDescriptor {
2985 self.descriptor.clone()
2986 }
2987
2988 fn capabilities(&self) -> DistributedCapabilities {
2989 DistributedCapabilities::new(true, [CollectiveGroupId::new(7)], true, true, true)
2990 }
2991
2992 fn all_reduce_sum(
2993 &self,
2994 _: CollectiveScope,
2995 input: &Vec<u32>,
2996 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2997 Ok(Submission {
2998 output: input.iter().map(|value| value * 2).collect(),
2999 completion: Done,
3000 })
3001 }
3002
3003 fn all_gather(
3004 &self,
3005 _: CollectiveScope,
3006 input: &Vec<u32>,
3007 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3008 let mut output = input.clone();
3009 output.extend(input);
3010 Ok(Submission {
3011 output,
3012 completion: Done,
3013 })
3014 }
3015
3016 fn all_to_all_v(
3017 &self,
3018 _: CollectiveScope,
3019 input: &Vec<u32>,
3020 _: &[usize],
3021 _: &[usize],
3022 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3023 Ok(Submission {
3024 output: input.clone(),
3025 completion: Done,
3026 })
3027 }
3028
3029 fn send(
3030 &self,
3031 _: CollectiveScope,
3032 _: usize,
3033 input: &Vec<u32>,
3034 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3035 Ok(Submission {
3036 output: input.clone(),
3037 completion: Done,
3038 })
3039 }
3040
3041 fn receive(
3042 &self,
3043 _: CollectiveScope,
3044 peer: usize,
3045 value: &ValueDescriptor,
3046 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3047 Ok(Submission {
3048 output: vec![peer as u32; value.shape().iter().product()],
3049 completion: Done,
3050 })
3051 }
3052
3053 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Infallible> {
3054 let mut output = local.to_vec();
3055 output.extend_from_slice(local);
3056 Ok(output)
3057 }
3058 }
3059
3060 impl DistributedBackend for Mock {
3061 type DistributedSession = MockDistributed;
3062
3063 fn distributed_session(session: &MockSession) -> Option<&Self::DistributedSession> {
3064 session.distributed.as_ref()
3065 }
3066 }
3067
3068 #[test]
3069 fn mock_distributed_session_owns_collective_and_transfer_lifecycle() {
3070 let tensor_group =
3071 CollectiveGroupDescriptor::new(CollectiveGroupId::new(7), vec![0, 1], 0).unwrap();
3072 let session = MockDistributed {
3073 descriptor: DistributedSessionDescriptor::new(2, 0, vec![tensor_group]).unwrap(),
3074 };
3075 let capabilities = session.capabilities();
3076 assert!(capabilities.exact_completion());
3077 assert_eq!(
3078 capabilities.collective_groups(),
3079 &[CollectiveGroupId::new(7)]
3080 );
3081 assert_eq!(
3082 session
3083 .all_reduce_sum(
3084 CollectiveScope::Group(CollectiveGroupId::new(7)),
3085 &vec![2, 3]
3086 )
3087 .unwrap()
3088 .wait()
3089 .unwrap(),
3090 vec![4, 6]
3091 );
3092 assert_eq!(
3093 session
3094 .receive(
3095 CollectiveScope::World,
3096 1,
3097 &ValueDescriptor::new(vec![2], TensorDtype::U32).unwrap(),
3098 )
3099 .unwrap()
3100 .wait()
3101 .unwrap(),
3102 vec![1, 1]
3103 );
3104 assert_eq!(session.all_gather_words(&[7]).unwrap(), vec![7, 7]);
3105
3106 let model_session = MockSession {
3107 model: 0,
3108 tokens: Vec::new(),
3109 distributed: Some(session.clone()),
3110 };
3111 assert_eq!(
3112 Mock::distributed_session(&model_session)
3113 .unwrap()
3114 .descriptor(),
3115 session.descriptor()
3116 );
3117 }
3118
3119 #[test]
3120 fn distributed_descriptors_round_trip_and_reject_invalid_ranks() {
3121 let descriptor = DistributedSessionDescriptor::new(
3122 6,
3123 4,
3124 vec![CollectiveGroupDescriptor::new(CollectiveGroupId::new(9), vec![1, 4], 1).unwrap()],
3125 )
3126 .unwrap();
3127 let encoded = serde_json::to_string(&descriptor).unwrap();
3128 assert_eq!(
3129 serde_json::from_str::<DistributedSessionDescriptor>(&encoded).unwrap(),
3130 descriptor
3131 );
3132 let scope = CollectiveScope::Group(CollectiveGroupId::new(9));
3133 assert_eq!(
3134 serde_json::from_str::<CollectiveScope>(&serde_json::to_string(&scope).unwrap())
3135 .unwrap(),
3136 scope
3137 );
3138 assert!(DistributedSessionDescriptor::new(descriptor.world_size(), 6, Vec::new()).is_err());
3139 assert!(serde_json::from_str::<DistributedSessionDescriptor>(
3140 r#"{"world_size":6,"rank":6,"groups":[]}"#
3141 )
3142 .is_err());
3143 }
3144
3145 #[test]
3146 fn distributed_commit_epoch_round_trips_and_rejects_zero() {
3147 let outcome = DistributedCommitOutcome::Indeterminate {
3148 epoch: DistributedCommitEpoch::new(17).unwrap(),
3149 phase: DistributedCommitPhase::DecisionCompletion,
3150 };
3151 let encoded = serde_json::to_string(&outcome).unwrap();
3152 assert_eq!(
3153 serde_json::from_str::<DistributedCommitOutcome>(&encoded).unwrap(),
3154 outcome
3155 );
3156 assert!(serde_json::from_str::<DistributedCommitEpoch>("0").is_err());
3157 }
3158}