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>),
1386}
1387
1388impl TokenFilter {
1389 pub fn allowed(mask: Vec<bool>) -> Result<Self, TokenFilterError> {
1391 if mask.is_empty() {
1392 return Err(TokenFilterError::EmptyVocabulary);
1393 }
1394 if !mask.iter().any(|allowed| *allowed) {
1395 return Err(TokenFilterError::NoAllowedToken);
1396 }
1397 Ok(Self::Allowed(mask))
1398 }
1399
1400 pub fn allowed_mask(&self) -> Option<&[bool]> {
1402 match self {
1403 Self::All => None,
1404 Self::Allowed(mask) => Some(mask),
1405 }
1406 }
1407
1408 pub fn allows(&self, token: u32) -> bool {
1410 self.allowed_mask()
1411 .is_none_or(|mask| mask.get(token as usize).copied().unwrap_or(false))
1412 }
1413
1414 pub fn intersection(&self, other: &Self) -> Result<Self, TokenFilterError> {
1416 match (self.allowed_mask(), other.allowed_mask()) {
1417 (None, None) => Ok(Self::All),
1418 (Some(mask), None) | (None, Some(mask)) => Self::allowed(mask.to_vec()),
1419 (Some(left), Some(right)) => {
1420 Self::allowed(left.iter().zip(right).map(|(a, b)| *a && *b).collect())
1421 }
1422 }
1423 }
1424
1425 pub fn allowed_mask_for(
1428 &self,
1429 output_width: usize,
1430 ) -> Result<Option<std::borrow::Cow<'_, [bool]>>, TokenFilterError> {
1431 if output_width == 0 {
1432 return Err(TokenFilterError::EmptyVocabulary);
1433 }
1434 let Some(mask) = self.allowed_mask() else {
1435 return Ok(None);
1436 };
1437 let prefix = &mask[..mask.len().min(output_width)];
1438 if !prefix.iter().any(|allowed| *allowed) {
1439 return Err(TokenFilterError::NoExecutableToken { output_width });
1440 }
1441 Ok(Some(if mask.len() >= output_width {
1442 std::borrow::Cow::Borrowed(prefix)
1443 } else {
1444 let mut mask = mask.to_vec();
1445 mask.resize(output_width, false);
1446 std::borrow::Cow::Owned(mask)
1447 }))
1448 }
1449}
1450
1451#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1453pub enum TokenFilterError {
1454 #[error("token filter vocabulary must not be empty")]
1456 EmptyVocabulary,
1457 #[error("token filter does not allow any vocabulary token")]
1459 NoAllowedToken,
1460 #[error("token filter permits no token in the model output vocabulary of size {output_width}")]
1462 NoExecutableToken {
1463 output_width: usize,
1465 },
1466}
1467
1468pub trait TokenFilterController {
1470 type Error: std::error::Error + Send + Sync + 'static;
1472
1473 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error>;
1475
1476 fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error>;
1478
1479 fn is_complete(&mut self) -> Result<bool, Self::Error>;
1481}
1482
1483pub trait SpeculativeTokenFilterController: TokenFilterController + Clone {
1489 fn filter_at(&self, history: &[u32]) -> Result<TokenFilter, Self::Error>;
1495
1496 fn prefix_is_complete(&self, history: &[u32]) -> Result<bool, Self::Error>;
1498}
1499
1500#[derive(Debug, Clone)]
1501struct FixedTokenFilter(TokenFilter);
1502
1503impl TokenFilterController for FixedTokenFilter {
1504 type Error = std::convert::Infallible;
1505
1506 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
1507 Ok(self.0.clone())
1508 }
1509
1510 fn commit_token(&mut self, _: u32) -> Result<(), Self::Error> {
1511 Ok(())
1512 }
1513
1514 fn is_complete(&mut self) -> Result<bool, Self::Error> {
1515 Ok(false)
1516 }
1517}
1518
1519pub trait TextGenerationBackend: BackendProvider {
1525 type Prompt;
1527 type Token: TokenOutput<Error = Self::Error>;
1529 type TextGenerationState;
1531 type TextCompletion: Completion<Error = Self::Error>;
1533
1534 fn text_execution_control_support(
1538 _runtime: &ModelRuntime<Self>,
1539 ) -> crate::execution_control::ControlSupport {
1540 crate::execution_control::ControlSupport::Unsupported {
1541 reason: "backend has not declared completed-token control support".into(),
1542 }
1543 }
1544
1545 fn text_sampling_control_support(
1548 _runtime: &ModelRuntime<Self>,
1549 ) -> crate::execution_control::ControlSupport {
1550 crate::execution_control::ControlSupport::Unsupported {
1551 reason: "backend has no prospective sampling controls".into(),
1552 }
1553 }
1554
1555 fn intervention_discovery(
1557 _runtime: &ModelRuntime<Self>,
1558 ) -> Result<crate::intervention::InterventionDiscovery, crate::capture::CaptureError> {
1559 Err(crate::capture::CaptureError::Unsupported(
1560 "backend has no intervention discovery".into(),
1561 ))
1562 }
1563
1564 fn validate_text_interventions(
1566 runtime: &ModelRuntime<Self>,
1567 capture: &crate::capture::AdmittedCapturePlan,
1568 plan: &crate::intervention::AdmittedInterventionPlan,
1569 ) -> Result<(), crate::capture::CaptureError> {
1570 if !plan.is_empty() {
1571 return Err(crate::capture::CaptureError::Unsupported(
1572 "backend has no text interventions".into(),
1573 ));
1574 }
1575 Self::validate_text_capture(runtime, capture)
1576 }
1577
1578 fn configure_text_interventions(
1581 runtime: &ModelRuntime<Self>,
1582 state: &mut Self::TextGenerationState,
1583 capture: crate::capture::AdmittedCapturePlan,
1584 plan: crate::intervention::AdmittedInterventionPlan,
1585 ) -> Result<(), crate::capture::CaptureError> {
1586 Self::validate_text_interventions(runtime, &capture, &plan)?;
1587 Self::configure_text_capture(runtime, state, capture)
1588 }
1589
1590 fn capture_discovery(
1592 _runtime: &ModelRuntime<Self>,
1593 ) -> Result<crate::capture::CaptureDiscovery, crate::capture::CaptureError> {
1594 Err(crate::capture::CaptureError::Unsupported(
1595 "backend has no bounded capture discovery".into(),
1596 ))
1597 }
1598
1599 fn configure_text_capture(
1602 _runtime: &ModelRuntime<Self>,
1603 _state: &mut Self::TextGenerationState,
1604 plan: crate::capture::AdmittedCapturePlan,
1605 ) -> Result<(), crate::capture::CaptureError> {
1606 if plan.is_empty() {
1607 Ok(())
1608 } else {
1609 Err(crate::capture::CaptureError::Unsupported(
1610 "backend has no bounded text capture".into(),
1611 ))
1612 }
1613 }
1614
1615 fn validate_text_capture(
1618 _runtime: &ModelRuntime<Self>,
1619 plan: &crate::capture::AdmittedCapturePlan,
1620 ) -> Result<(), crate::capture::CaptureError> {
1621 if plan.is_empty() {
1622 Ok(())
1623 } else {
1624 Err(crate::capture::CaptureError::Unsupported(
1625 "backend has no bounded text capture".into(),
1626 ))
1627 }
1628 }
1629
1630 fn take_text_capture(
1633 _state: &mut Self::TextGenerationState,
1634 ) -> Option<crate::capture::CapturedStep> {
1635 None
1636 }
1637
1638 fn start_text_generation(
1640 backend: &Self,
1641 config: TextGenerationConfig,
1642 ) -> Result<Self::TextGenerationState, Self::Error>;
1643
1644 fn prepare_text_prompt(
1646 backend: &Self,
1647 prompt_token_ids: Vec<u32>,
1648 ) -> Result<Self::Prompt, Self::Error>;
1649
1650 fn submit_text_prefill(
1652 runtime: &mut ModelRuntime<Self>,
1653 prompt: Self::Prompt,
1654 filter: &TokenFilter,
1655 state: &mut Self::TextGenerationState,
1656 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error>;
1657
1658 fn submit_text_decode(
1660 runtime: &mut ModelRuntime<Self>,
1661 token: Self::Token,
1662 filter: &TokenFilter,
1663 state: &mut Self::TextGenerationState,
1664 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error>;
1665}
1666
1667#[derive(Debug, thiserror::Error)]
1669pub enum MultimodalPreparationFailure<B, T>
1670where
1671 B: std::error::Error + 'static,
1672 T: std::error::Error + 'static,
1673{
1674 #[error("backend multimodal preparation failed: {0}")]
1676 Backend(#[source] B),
1677 #[error("multimodal framing text encoding failed: {0}")]
1679 Text(#[source] T),
1680}
1681
1682pub trait MultimodalPreparationBackend: TextGenerationBackend {
1689 fn prepare_multimodal_input<E>(
1691 runtime: &ModelRuntime<Self>,
1692 request: &TokenizedMultimodalRequest,
1693 encode_backend_text: &mut dyn FnMut(&str) -> Result<Vec<u32>, E>,
1694 ) -> Result<Self::Prompt, MultimodalPreparationFailure<Self::Error, E>>
1695 where
1696 E: std::error::Error + Send + Sync + 'static;
1697}
1698
1699pub trait ModelCapabilityBackend: TextGenerationBackend {
1706 fn model_capabilities(
1708 runtime: &ModelRuntime<Self>,
1709 ) -> Result<ModelCapabilities, CapabilityError>;
1710
1711 fn count_prepared_input(
1713 runtime: &ModelRuntime<Self>,
1714 input: &Self::Prompt,
1715 ) -> Result<InputTokenCount, CapabilityError>;
1716
1717 fn estimate_runtime_state(
1719 runtime: &ModelRuntime<Self>,
1720 input: InputTokenCount,
1721 max_output_tokens: u64,
1722 batch_size: u64,
1723 ) -> Result<RuntimeStateEstimate, CapabilityError>;
1724
1725 fn static_memory(runtime: &ModelRuntime<Self>) -> Result<StaticMemoryReport, CapabilityError>;
1727}
1728
1729pub enum PendingTextInput<P, T> {
1732 Prefill(P),
1734 Decode(T),
1736}
1737
1738impl<P, T> PendingTextInput<P, T> {
1739 pub fn as_ref(&self) -> PendingTextInput<&P, &T> {
1741 match self {
1742 Self::Prefill(prompt) => PendingTextInput::Prefill(prompt),
1743 Self::Decode(token) => PendingTextInput::Decode(token),
1744 }
1745 }
1746}
1747
1748#[derive(Debug, thiserror::Error)]
1750pub enum ControlledTextGenerationError<B, C>
1751where
1752 B: std::error::Error + 'static,
1753 C: std::error::Error + 'static,
1754{
1755 #[error("backend text generation failed: {0}")]
1757 Backend(#[source] B),
1758 #[error("text generation constraint failed: {0}")]
1760 Controller(#[source] C),
1761}
1762
1763#[derive(Debug, Clone)]
1765pub struct ControlledToken<T> {
1766 output: T,
1767 token_id: u32,
1768}
1769
1770impl<T> ControlledToken<T> {
1771 pub fn token_id(&self) -> u32 {
1773 self.token_id
1774 }
1775
1776 pub const fn output(&self) -> &T {
1778 &self.output
1779 }
1780
1781 pub fn into_output(self) -> T {
1783 self.output
1784 }
1785}
1786
1787pub struct ControlledTextGeneration<'a, B, C>
1789where
1790 B: TextGenerationBackend,
1791 C: TokenFilterController,
1792{
1793 runtime: &'a mut ModelRuntime<B>,
1794 inner: TextGenerationMachine<B, C>,
1795}
1796
1797struct TextGenerationMachine<B, C>
1798where
1799 B: TextGenerationBackend,
1800 C: TokenFilterController,
1801{
1802 backend_state: B::TextGenerationState,
1803 controller: C,
1804 step: Option<PendingTextInput<B::Prompt, B::Token>>,
1805 completions: Vec<B::TextCompletion>,
1806 remaining_tokens: Option<usize>,
1807}
1808
1809type ControlledGenerationResult<B, C> = Result<
1810 <B as TextGenerationBackend>::Token,
1811 ControlledTextGenerationError<
1812 <B as BackendProvider>::Error,
1813 <C as TokenFilterController>::Error,
1814 >,
1815>;
1816
1817impl<'a, B, C> ControlledTextGeneration<'a, B, C>
1818where
1819 B: TextGenerationBackend,
1820 C: TokenFilterController,
1821{
1822 pub fn new(
1824 runtime: &'a mut ModelRuntime<B>,
1825 prompt_token_ids: Vec<u32>,
1826 config: TextGenerationConfig,
1827 controller: C,
1828 ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1829 let prompt = B::prepare_text_prompt(runtime.backend(), prompt_token_ids)
1830 .map_err(ControlledTextGenerationError::Backend)?;
1831 Self::from_prompt(runtime, prompt, config, controller)
1832 }
1833
1834 pub fn from_prompt(
1836 runtime: &'a mut ModelRuntime<B>,
1837 prompt: B::Prompt,
1838 config: TextGenerationConfig,
1839 controller: C,
1840 ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1841 let inner = TextGenerationMachine::new(runtime, prompt, config, controller)?;
1842 Ok(Self { runtime, inner })
1843 }
1844
1845 pub fn controller_mut(&mut self) -> &mut C {
1847 &mut self.inner.controller
1848 }
1849
1850 pub fn enable_capture(
1852 &mut self,
1853 plan: crate::capture::AdmittedCapturePlan,
1854 ) -> Result<(), crate::capture::CaptureError> {
1855 if !matches!(self.inner.step, Some(PendingTextInput::Prefill(_))) {
1856 return Err(crate::capture::CaptureError::Invalid(
1857 "capture must be configured before generation".into(),
1858 ));
1859 }
1860 B::configure_text_capture(self.runtime, &mut self.inner.backend_state, plan)
1861 }
1862
1863 pub fn enable_interventions(
1865 &mut self,
1866 capture: crate::capture::AdmittedCapturePlan,
1867 plan: crate::intervention::AdmittedInterventionPlan,
1868 ) -> Result<(), crate::capture::CaptureError> {
1869 if !matches!(self.inner.step, Some(PendingTextInput::Prefill(_))) {
1870 return Err(crate::capture::CaptureError::Invalid(
1871 "interventions must be configured before generation".into(),
1872 ));
1873 }
1874 B::configure_text_interventions(self.runtime, &mut self.inner.backend_state, capture, plan)
1875 }
1876
1877 pub fn take_captured_step(&mut self) -> Result<Option<crate::capture::CapturedStep>, B::Error> {
1880 self.inner.resolve_completions_before_decode()?;
1881 Ok(B::take_text_capture(&mut self.inner.backend_state))
1882 }
1883}
1884
1885impl<B, C> TextGenerationMachine<B, C>
1886where
1887 B: TextGenerationBackend,
1888 C: TokenFilterController,
1889{
1890 fn new(
1891 runtime: &ModelRuntime<B>,
1892 prompt: B::Prompt,
1893 config: TextGenerationConfig,
1894 controller: C,
1895 ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1896 let backend_state = B::start_text_generation(runtime.backend(), config)
1897 .map_err(ControlledTextGenerationError::Backend)?;
1898 Ok(Self {
1899 backend_state,
1900 controller,
1901 step: Some(PendingTextInput::Prefill(prompt)),
1902 completions: Vec::new(),
1903 remaining_tokens: config.sampling().max_new_tokens,
1904 })
1905 }
1906
1907 fn retain_completion(&mut self, completion: B::TextCompletion) -> Result<(), B::Error> {
1908 let existing = std::mem::take(&mut self.completions);
1909 let mut retained = Vec::with_capacity(existing.len() + 1);
1910 for pending in existing {
1911 match pending.is_complete() {
1912 Ok(true) => {}
1913 Ok(false) => retained.push(pending),
1914 Err(error) => {
1915 let _ = pending.wait();
1916 for retained_completion in retained.drain(..) {
1917 let _ = retained_completion.wait();
1918 }
1919 let _ = completion.wait();
1920 return Err(error);
1921 }
1922 }
1923 }
1924 retained.push(completion);
1925 self.completions = retained;
1926 Ok(())
1927 }
1928
1929 fn resolve_completions_before_decode(&mut self) -> Result<(), B::Error> {
1930 let existing = std::mem::take(&mut self.completions);
1931 let mut remaining = existing.into_iter();
1932 while let Some(completion) = remaining.next() {
1933 let result = match completion.is_complete() {
1934 Ok(true) => Ok(()),
1935 Ok(false) => completion.wait(),
1936 Err(error) => {
1937 let _ = completion.wait();
1938 Err(error)
1939 }
1940 };
1941 if let Err(error) = result {
1942 for pending in remaining {
1943 let _ = pending.wait();
1944 }
1945 return Err(error);
1946 }
1947 }
1948 Ok(())
1949 }
1950
1951 #[allow(clippy::type_complexity)]
1952 fn next_committed(
1953 &mut self,
1954 runtime: &mut ModelRuntime<B>,
1955 ) -> Option<Result<ControlledToken<B::Token>, ControlledTextGenerationError<B::Error, C::Error>>>
1956 {
1957 let token = match self.next_output(runtime)? {
1958 Ok(token) => token,
1959 Err(error) => return Some(Err(error)),
1960 };
1961 let token_id = match token.token_id() {
1962 Ok(token_id) => token_id,
1963 Err(error) => {
1964 self.step = None;
1965 return Some(Err(ControlledTextGenerationError::Backend(error)));
1966 }
1967 };
1968 if let Err(error) = self.controller.commit_token(token_id) {
1969 self.step = None;
1970 return Some(Err(ControlledTextGenerationError::Controller(error)));
1971 }
1972 Some(Ok(ControlledToken {
1973 output: token,
1974 token_id,
1975 }))
1976 }
1977
1978 fn next_output(
1979 &mut self,
1980 runtime: &mut ModelRuntime<B>,
1981 ) -> Option<ControlledGenerationResult<B, C>> {
1982 if self.remaining_tokens == Some(0) {
1983 self.step = None;
1984 return None;
1985 }
1986 let step = self.step.take()?;
1987 if matches!(step, PendingTextInput::Decode(_)) {
1988 if let Err(error) = self.resolve_completions_before_decode() {
1989 return Some(Err(ControlledTextGenerationError::Backend(error)));
1990 }
1991 }
1992 let filter = match self.controller.current_filter() {
1993 Ok(filter) => filter,
1994 Err(error) => return Some(Err(ControlledTextGenerationError::Controller(error))),
1995 };
1996 let submission = match step {
1997 PendingTextInput::Prefill(prompt) => {
1998 B::submit_text_prefill(runtime, prompt, &filter, &mut self.backend_state)
1999 }
2000 PendingTextInput::Decode(token) => {
2001 B::submit_text_decode(runtime, token, &filter, &mut self.backend_state)
2002 }
2003 };
2004 let submission = match submission {
2005 Ok(submission) => submission,
2006 Err(error) => return Some(Err(ControlledTextGenerationError::Backend(error))),
2007 };
2008 let token = submission.output;
2009 if let Err(error) = self.retain_completion(submission.completion) {
2010 return Some(Err(ControlledTextGenerationError::Backend(error)));
2011 }
2012 self.step = Some(PendingTextInput::Decode(token.clone()));
2013 if let Some(remaining_tokens) = &mut self.remaining_tokens {
2014 *remaining_tokens -= 1;
2015 }
2016 Some(Ok(token))
2017 }
2018}
2019
2020impl<B, C> Iterator for ControlledTextGeneration<'_, B, C>
2021where
2022 B: TextGenerationBackend,
2023 C: TokenFilterController,
2024{
2025 type Item =
2026 Result<ControlledToken<B::Token>, ControlledTextGenerationError<B::Error, C::Error>>;
2027
2028 fn next(&mut self) -> Option<Self::Item> {
2029 self.inner.next_committed(self.runtime)
2030 }
2031}
2032
2033impl<B, C> Drop for TextGenerationMachine<B, C>
2034where
2035 B: TextGenerationBackend,
2036 C: TokenFilterController,
2037{
2038 fn drop(&mut self) {
2039 for completion in self.completions.drain(..) {
2040 let _ = completion.wait();
2041 }
2042 }
2043}
2044
2045pub struct TextGeneration<'a, B: TextGenerationBackend> {
2052 runtime: &'a mut ModelRuntime<B>,
2053 inner: TextGenerationMachine<B, FixedTokenFilter>,
2054}
2055
2056impl<'a, B: TextGenerationBackend> TextGeneration<'a, B> {
2057 pub fn new(
2059 runtime: &'a mut ModelRuntime<B>,
2060 prompt_token_ids: Vec<u32>,
2061 config: TextGenerationConfig,
2062 ) -> Result<Self, B::Error> {
2063 Self::with_token_filter(runtime, prompt_token_ids, config, TokenFilter::All)
2064 }
2065
2066 pub fn with_token_filter(
2070 runtime: &'a mut ModelRuntime<B>,
2071 prompt_token_ids: Vec<u32>,
2072 config: TextGenerationConfig,
2073 filter: TokenFilter,
2074 ) -> Result<Self, B::Error> {
2075 let prompt = B::prepare_text_prompt(runtime.backend(), prompt_token_ids)?;
2076 let inner = TextGenerationMachine::new(runtime, prompt, config, FixedTokenFilter(filter))
2077 .map_err(unreachable_unconstrained_error)?;
2078 Ok(Self { runtime, inner })
2079 }
2080
2081 pub fn from_prompt(
2083 runtime: &'a mut ModelRuntime<B>,
2084 prompt: B::Prompt,
2085 config: TextGenerationConfig,
2086 ) -> Result<Self, B::Error> {
2087 let inner =
2088 TextGenerationMachine::new(runtime, prompt, config, FixedTokenFilter(TokenFilter::All))
2089 .map_err(unreachable_unconstrained_error)?;
2090 Ok(Self { runtime, inner })
2091 }
2092}
2093
2094fn unreachable_unconstrained_error<B>(
2095 error: ControlledTextGenerationError<B, std::convert::Infallible>,
2096) -> B
2097where
2098 B: std::error::Error + 'static,
2099{
2100 match error {
2101 ControlledTextGenerationError::Backend(error) => error,
2102 ControlledTextGenerationError::Controller(error) => match error {},
2103 }
2104}
2105
2106impl<B: TextGenerationBackend> Iterator for TextGeneration<'_, B> {
2107 type Item = Result<B::Token, B::Error>;
2108
2109 fn next(&mut self) -> Option<Self::Item> {
2110 self.inner
2111 .next_output(self.runtime)
2112 .map(|result| result.map_err(unreachable_unconstrained_error))
2113 }
2114}
2115
2116pub trait DistributedSession {
2123 type Value;
2125 type Completion: Completion<Error = Self::Error>;
2127 type Error: std::error::Error + Send + Sync + 'static;
2129
2130 fn descriptor(&self) -> DistributedSessionDescriptor;
2132 fn capabilities(&self) -> DistributedCapabilities;
2134
2135 fn all_reduce_sum(
2137 &self,
2138 scope: CollectiveScope,
2139 input: &Self::Value,
2140 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2141
2142 fn all_gather(
2144 &self,
2145 scope: CollectiveScope,
2146 input: &Self::Value,
2147 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2148
2149 fn all_to_all_v(
2151 &self,
2152 scope: CollectiveScope,
2153 input: &Self::Value,
2154 send_counts: &[usize],
2155 receive_counts: &[usize],
2156 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2157
2158 fn send(
2160 &self,
2161 scope: CollectiveScope,
2162 peer: usize,
2163 input: &Self::Value,
2164 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2165
2166 fn receive(
2168 &self,
2169 scope: CollectiveScope,
2170 peer: usize,
2171 value: &ValueDescriptor,
2172 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2173
2174 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error>;
2176}
2177
2178pub trait DistributedBackend: BackendProvider {
2180 type DistributedSession: DistributedSession<Error = Self::Error>;
2182
2183 fn distributed_session(session: &Self::Session) -> Option<&Self::DistributedSession>;
2185}
2186
2187#[cfg(test)]
2188mod tests {
2189 #[test]
2190 fn closed_token_sets_intersect_and_project_to_executable_output_width() {
2191 use super::{TokenFilter, TokenFilterError};
2192 let valid = TokenFilter::allowed(vec![false, true, false, true]).unwrap();
2193 assert_eq!(
2194 valid.allowed_mask_for(6).unwrap().unwrap().as_ref(),
2195 &[false, true, false, true, false, false]
2196 );
2197 assert_eq!(
2198 valid.allowed_mask_for(3).unwrap().unwrap().as_ref(),
2199 &[false, true, false]
2200 );
2201 assert_eq!(
2202 valid.allowed_mask_for(1),
2203 Err(TokenFilterError::NoExecutableToken { output_width: 1 })
2204 );
2205 assert_eq!(
2206 valid.allowed_mask_for(0),
2207 Err(TokenFilterError::EmptyVocabulary)
2208 );
2209 assert!(!valid.allows(4));
2210 let grammar = TokenFilter::allowed(vec![true, true]).unwrap();
2211 assert_eq!(
2212 valid.intersection(&grammar).unwrap(),
2213 TokenFilter::Allowed(vec![false, true])
2214 );
2215 assert_eq!(valid.intersection(&TokenFilter::All).unwrap(), valid);
2216 assert_eq!(
2217 valid.intersection(&TokenFilter::Allowed(vec![true])),
2218 Err(TokenFilterError::NoAllowedToken)
2219 );
2220 assert!(TokenFilter::Allowed(vec![]).allowed_mask_for(2).is_err());
2221 }
2222
2223 use super::*;
2224 use std::{convert::Infallible, io::Write};
2225
2226 #[test]
2227 fn text_generation_config_validates_portable_mirostat_strategy() {
2228 let sampling = crate::generation::resolve_generation_config(
2229 None,
2230 crate::generation::GenerationConfigOverrides {
2231 temperature: Some(0.8),
2232 ..crate::generation::GenerationConfigOverrides::default()
2233 },
2234 )
2235 .unwrap();
2236 let config = TextGenerationConfig::new(sampling)
2237 .with_seed(7)
2238 .with_mirostat_v2(5.0, 0.1)
2239 .unwrap();
2240 assert_eq!(config.seed(), 7);
2241 assert_eq!(
2242 config.strategy(),
2243 TextSamplingStrategy::MirostatV2 { tau: 5.0, eta: 0.1 }
2244 );
2245 assert!(matches!(
2246 TextGenerationConfig::new(sampling).with_mirostat_v2(0.0, 0.1),
2247 Err(GenerationError::InvalidMirostatTau(0.0))
2248 ));
2249 assert!(matches!(
2250 TextGenerationConfig::new(sampling).with_mirostat_v2(5.0, f32::NAN),
2251 Err(GenerationError::InvalidMirostatEta(value)) if value.is_nan()
2252 ));
2253 }
2254
2255 #[derive(Debug, Clone)]
2256 struct Done;
2257 impl Completion for Done {
2258 type Error = Infallible;
2259 fn is_complete(&self) -> Result<bool, Self::Error> {
2260 Ok(true)
2261 }
2262 fn wait(&self) -> Result<(), Self::Error> {
2263 Ok(())
2264 }
2265 }
2266 struct Mock;
2267 impl BackendProvider for Mock {
2268 type ModelConfig = u32;
2269 type Model = u32;
2270 type Session = MockSession;
2271 type Error = Infallible;
2272 fn descriptor(&self) -> BackendDescriptor {
2273 BackendDescriptor::new("mock", "1")
2274 }
2275 fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error> {
2276 Ok(vec![])
2277 }
2278 fn prepare_model(&self, config: u32) -> Result<PreparedModel<u32>, Self::Error> {
2279 Ok(PreparedModel::new(config, SessionCapabilities::default()))
2280 }
2281 fn create_session(&self, model: PreparedModel<u32>) -> Result<MockSession, Self::Error> {
2282 Ok(MockSession {
2283 model: model.into_inner(),
2284 tokens: vec![],
2285 distributed: None,
2286 })
2287 }
2288 }
2289
2290 #[derive(Default)]
2291 struct LoadingMock {
2292 selections: std::sync::atomic::AtomicUsize,
2293 materializations: std::sync::atomic::AtomicUsize,
2294 }
2295 struct LoadingMockSession;
2296
2297 struct LoadingConfigurationResolver;
2298
2299 impl ModelConfigurationResolver for LoadingConfigurationResolver {
2300 type ArtifactPlan = ();
2301
2302 fn resolve_safetensors(
2303 &self,
2304 json: &serde_json::Value,
2305 ) -> Result<crate::ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
2306 Ok(crate::ResolvedModelConfiguration::new(
2307 crate::ModelConfiguration::new(
2308 "llama",
2309 "llama",
2310 "llama",
2311 crate::LoadingProtocol::Model,
2312 Some(json.clone()),
2313 )?,
2314 (),
2315 ))
2316 }
2317
2318 fn resolve_gguf(
2319 &self,
2320 architecture: &str,
2321 _checkpoint: &eredu_gguf::Checkpoint,
2322 ) -> Result<crate::ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
2323 if architecture != "llama" {
2324 return Err(ArtifactError::UnsupportedGgufArchitecture(
2325 architecture.into(),
2326 ));
2327 }
2328 Ok(crate::ResolvedModelConfiguration::new(
2329 crate::ModelConfiguration::new(
2330 architecture,
2331 architecture,
2332 "llama",
2333 crate::LoadingProtocol::Model,
2334 None,
2335 )?,
2336 (),
2337 ))
2338 }
2339
2340 fn gguf_companion_requirements(
2341 &self,
2342 _architecture: &str,
2343 _checkpoint: &eredu_gguf::Checkpoint,
2344 ) -> Result<Vec<crate::GgufCompanionRequirement>, ArtifactError> {
2345 Ok(Vec::new())
2346 }
2347 }
2348
2349 static LOADING_CONFIGURATION_RESOLVER: LoadingConfigurationResolver =
2350 LoadingConfigurationResolver;
2351
2352 impl BackendProvider for LoadingMock {
2353 type ModelConfig = (ModelPreparationPlan, u32);
2354 type Model = u32;
2355 type Session = LoadingMockSession;
2356 type Error = std::convert::Infallible;
2357
2358 fn descriptor(&self) -> BackendDescriptor {
2359 BackendDescriptor::new("loading-mock", "1")
2360 }
2361
2362 fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error> {
2363 Ok(Vec::new())
2364 }
2365
2366 fn prepare_model(
2367 &self,
2368 (plan, model): Self::ModelConfig,
2369 ) -> Result<PreparedModel<Self::Model>, Self::Error> {
2370 self.materializations
2371 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2372 assert_eq!(plan.inspection().configuration().family(), "llama");
2373 Ok(PreparedModel::new(
2374 model,
2375 plan.admitted_session_capabilities(),
2376 ))
2377 }
2378
2379 fn create_session(
2380 &self,
2381 _: PreparedModel<Self::Model>,
2382 ) -> Result<Self::Session, Self::Error> {
2383 Ok(LoadingMockSession)
2384 }
2385 }
2386
2387 impl BackendSession<LoadingMock> for LoadingMockSession {
2388 type PrefillInput = ();
2389 type DecodeInput = ();
2390 type Output = ();
2391 type Completion = LoadingDone;
2392
2393 fn capabilities(&self) -> SessionCapabilities {
2394 SessionCapabilities::default()
2395 }
2396
2397 fn prefill(
2398 &mut self,
2399 _: &LoadingMock,
2400 _: (),
2401 ) -> Result<Submission<(), LoadingDone>, std::convert::Infallible> {
2402 Ok(Submission {
2403 output: (),
2404 completion: LoadingDone,
2405 })
2406 }
2407
2408 fn decode(
2409 &mut self,
2410 _: &LoadingMock,
2411 _: (),
2412 ) -> Result<Submission<(), LoadingDone>, std::convert::Infallible> {
2413 Ok(Submission {
2414 output: (),
2415 completion: LoadingDone,
2416 })
2417 }
2418
2419 fn observe_output(
2420 &self,
2421 _: &LoadingMock,
2422 _: &(),
2423 ) -> Result<ObservationSet, std::convert::Infallible> {
2424 Ok(ObservationSet::new())
2425 }
2426 }
2427
2428 #[derive(Debug, Clone, Copy)]
2429 struct LoadingDone;
2430
2431 impl Completion for LoadingDone {
2432 type Error = std::convert::Infallible;
2433
2434 fn is_complete(&self) -> Result<bool, Self::Error> {
2435 Ok(true)
2436 }
2437
2438 fn wait(&self) -> Result<(), Self::Error> {
2439 Ok(())
2440 }
2441 }
2442
2443 impl ModelLoadingBackend for LoadingMock {
2444 type LoadOptions = u32;
2445 type SelectedPreparation = (u32, crate::PreparationAdmission);
2446 type ConfigurationResolver = LoadingConfigurationResolver;
2447
2448 fn configuration_resolver(&self) -> &Self::ConfigurationResolver {
2449 &LOADING_CONFIGURATION_RESOLVER
2450 }
2451
2452 fn select_preparation(
2453 &self,
2454 _: &ArtifactInspection,
2455 options: &Self::LoadOptions,
2456 ) -> Result<Self::SelectedPreparation, Self::Error> {
2457 self.selections
2458 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2459 let policy = crate::PreparationPolicy::default().with_required_session_capabilities(
2460 SessionCapabilities::default().with_activation_inspection(*options == 99),
2461 );
2462 let request = crate::PreparationAdmissionRequest::new(
2463 crate::LoadingProtocol::Model,
2464 crate::ArtifactFormat::SafeTensors,
2465 policy,
2466 crate::ArchitecturePreparationCapabilities::new(
2467 false,
2468 true,
2469 false,
2470 false,
2471 false,
2472 crate::InputModalities::TEXT,
2473 ),
2474 );
2475 let admission = crate::admit_preparation(
2476 request,
2477 crate::PreparationMechanismCapabilities::new(true, true)
2478 .with_residency(crate::ResidencyRequest::FullyResident, true)
2479 .with_input_modalities(crate::InputModalities::TEXT)
2480 .with_session(
2481 SessionCapabilities::default().with_activation_inspection(*options == 99),
2482 ),
2483 )
2484 .expect("mock admission facts are coherent");
2485 Ok((*options, admission))
2486 }
2487
2488 fn selected_preparation_admission(
2489 &self,
2490 selected: &Self::SelectedPreparation,
2491 ) -> crate::PreparationAdmission {
2492 selected.1
2493 }
2494
2495 fn model_config(
2496 &self,
2497 selected: SelectedModelPreparation<Self>,
2498 ) -> Result<Self::ModelConfig, Self::Error> {
2499 let (plan, (selected, _admission)) = selected.into_parts();
2500 Ok((plan, selected))
2501 }
2502 }
2503
2504 fn write_loading_fixture(root: &Path) {
2505 std::fs::write(root.join("config.json"), r#"{"model_type":"llama"}"#).unwrap();
2506 let header = br#"{"token_embd.weight":{"dtype":"F32","shape":[1],"data_offsets":[0,4]}}"#;
2507 let mut file = std::fs::File::create(root.join("model.safetensors")).unwrap();
2508 file.write_all(&(header.len() as u64).to_le_bytes())
2509 .unwrap();
2510 file.write_all(header).unwrap();
2511 file.write_all(&[0; 4]).unwrap();
2512 }
2513 struct MockSession {
2514 model: u32,
2515 tokens: Vec<u32>,
2516 distributed: Option<MockDistributed>,
2517 }
2518 impl BackendSession<Mock> for MockSession {
2519 type PrefillInput = Vec<u32>;
2520 type DecodeInput = u32;
2521 type Output = u32;
2522 type Completion = Done;
2523 fn capabilities(&self) -> SessionCapabilities {
2524 SessionCapabilities::default()
2525 }
2526 fn prefill(
2527 &mut self,
2528 _: &Mock,
2529 input: Vec<u32>,
2530 ) -> Result<Submission<u32, Done>, Infallible> {
2531 self.tokens.extend(input);
2532 Ok(Submission {
2533 output: self.tokens.len() as u32 + self.model,
2534 completion: Done,
2535 })
2536 }
2537 fn decode(&mut self, _: &Mock, input: u32) -> Result<Submission<u32, Done>, Infallible> {
2538 self.tokens.push(input);
2539 Ok(Submission {
2540 output: self.tokens.len() as u32 + self.model,
2541 completion: Done,
2542 })
2543 }
2544
2545 fn observe_output(&self, _: &Mock, output: &u32) -> Result<ObservationSet, Infallible> {
2546 let mut observations = ObservationSet::new();
2547 observations
2548 .insert(
2549 "mock.output",
2550 crate::ObservationValue::Unsigned(u64::from(*output)),
2551 )
2552 .unwrap();
2553 Ok(observations)
2554 }
2555 }
2556
2557 impl TextGenerationBackend for Mock {
2558 type Prompt = Vec<u32>;
2559 type Token = u32;
2560 type TextGenerationState = (u32, u64);
2561 type TextCompletion = Done;
2562
2563 fn start_text_generation(
2564 _: &Self,
2565 config: TextGenerationConfig,
2566 ) -> Result<Self::TextGenerationState, Self::Error> {
2567 Ok((config.sampling().top_k as u32, config.seed()))
2568 }
2569
2570 fn prepare_text_prompt(
2571 _: &Self,
2572 prompt_token_ids: Vec<u32>,
2573 ) -> Result<Self::Prompt, Self::Error> {
2574 Ok(prompt_token_ids)
2575 }
2576
2577 fn submit_text_prefill(
2578 runtime: &mut ModelRuntime<Self>,
2579 prompt: Self::Prompt,
2580 filter: &TokenFilter,
2581 state: &mut Self::TextGenerationState,
2582 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error> {
2583 let submission = runtime.prefill(prompt)?;
2584 Ok(Submission {
2585 output: apply_mock_filter(submission.output + state.0 + state.1 as u32, filter),
2586 completion: submission.completion,
2587 })
2588 }
2589
2590 fn submit_text_decode(
2591 runtime: &mut ModelRuntime<Self>,
2592 token: Self::Token,
2593 filter: &TokenFilter,
2594 _: &mut Self::TextGenerationState,
2595 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error> {
2596 let submission = runtime.decode(token)?;
2597 Ok(Submission {
2598 output: apply_mock_filter(submission.output, filter),
2599 completion: submission.completion,
2600 })
2601 }
2602 }
2603
2604 impl MultimodalPreparationBackend for Mock {
2605 fn prepare_multimodal_input<E>(
2606 _: &ModelRuntime<Self>,
2607 request: &TokenizedMultimodalRequest,
2608 _: &mut dyn FnMut(&str) -> Result<Vec<u32>, E>,
2609 ) -> Result<Self::Prompt, MultimodalPreparationFailure<Self::Error, E>>
2610 where
2611 E: std::error::Error + Send + Sync + 'static,
2612 {
2613 let mut prompt = Vec::new();
2614 for segment in request.segments() {
2615 match segment {
2616 crate::TokenizedMultimodalSegment::TokenIds(ids) => {
2617 prompt.extend_from_slice(ids);
2618 }
2619 crate::TokenizedMultimodalSegment::Media(crate::Media::Image(_)) => {
2620 prompt.push(1_001);
2621 }
2622 crate::TokenizedMultimodalSegment::Media(crate::Media::Video(_)) => {
2623 prompt.push(1_002);
2624 }
2625 crate::TokenizedMultimodalSegment::Media(crate::Media::Audio(_)) => {
2626 prompt.push(1_003);
2627 }
2628 }
2629 }
2630 Ok(prompt)
2631 }
2632 }
2633
2634 impl ModelCapabilityBackend for Mock {
2635 fn model_capabilities(
2636 _: &ModelRuntime<Self>,
2637 ) -> Result<ModelCapabilities, CapabilityError> {
2638 Ok(ModelCapabilities {
2639 effective_model_type: "mock".into(),
2640 native_max_context: crate::Observed::exact(64, "mock configuration"),
2641 effective_max_context: crate::Observed::exact(64, "mock configuration"),
2642 state_strategy: crate::CacheStateStrategy::FullKv,
2643 modalities: crate::InputModalities::TEXT,
2644 estimation: crate::EstimationCompleteness::Complete,
2645 })
2646 }
2647
2648 fn count_prepared_input(
2649 _: &ModelRuntime<Self>,
2650 input: &Self::Prompt,
2651 ) -> Result<InputTokenCount, CapabilityError> {
2652 Ok(InputTokenCount::text(input.len() as u64))
2653 }
2654
2655 fn estimate_runtime_state(
2656 _: &ModelRuntime<Self>,
2657 input: InputTokenCount,
2658 max_output_tokens: u64,
2659 batch_size: u64,
2660 ) -> Result<RuntimeStateEstimate, CapabilityError> {
2661 crate::estimate_runtime_state(
2662 &crate::StateMemoryLayout::new(
2663 crate::LayerSchedule::new(
2664 1,
2665 vec![crate::cache::LayerCachePolicy::key_only(
2666 crate::AttentionPolicy::Full,
2667 1,
2668 2,
2669 )
2670 .unwrap()],
2671 )
2672 .unwrap(),
2673 vec![0],
2674 1,
2675 1,
2676 crate::EstimationCompleteness::Complete,
2677 )
2678 .unwrap(),
2679 input,
2680 max_output_tokens,
2681 batch_size,
2682 std::num::NonZeroU8::new(4).unwrap(),
2683 )
2684 }
2685
2686 fn static_memory(
2687 runtime: &ModelRuntime<Self>,
2688 ) -> Result<StaticMemoryReport, CapabilityError> {
2689 let unavailable = || crate::Observed::unavailable("mock does not expose this counter");
2690 Ok(StaticMemoryReport {
2691 logical_parameter_bytes: crate::Observed::exact(
2692 u64::from(runtime.session().model),
2693 "mock model",
2694 ),
2695 current_host_resident_bytes: unavailable(),
2696 current_device_resident_bytes: unavailable(),
2697 planned_disk_backed_bytes: unavailable(),
2698 backend_active_allocation_bytes: unavailable(),
2699 backend_allocator_cache_bytes: unavailable(),
2700 physical_semantics: crate::PhysicalMemorySemantics::Unknown,
2701 currently_cached_shards: unavailable(),
2702 })
2703 }
2704 }
2705
2706 fn apply_mock_filter(candidate: u32, filter: &TokenFilter) -> u32 {
2707 let Some(allowed) = filter.allowed_mask() else {
2708 return candidate;
2709 };
2710 allowed
2711 .get(candidate as usize)
2712 .copied()
2713 .unwrap_or(false)
2714 .then_some(candidate)
2715 .or_else(|| {
2716 allowed
2717 .iter()
2718 .position(|allowed| *allowed)
2719 .map(|token| token as u32)
2720 })
2721 .expect("validated token filters allow at least one token")
2722 }
2723
2724 #[test]
2725 fn generic_loader_inspects_plans_and_prepares_on_the_selected_backend() {
2726 let root = tempfile::tempdir().unwrap();
2727 write_loading_fixture(root.path());
2728 let prepared = load_model(&LoadingMock::default(), root.path(), 41).unwrap();
2729 assert_eq!(*prepared, 41);
2730
2731 let runtime = ModelRuntime::load(LoadingMock::default(), root.path(), 7).unwrap();
2732 assert_eq!(runtime.backend().descriptor().name, "loading-mock");
2733
2734 let missing = root.path().join("missing");
2735 assert!(matches!(
2736 load_model(&LoadingMock::default(), &missing, 1),
2737 Err(ModelLoadError::Artifact(ArtifactError::MissingArtifact(path)))
2738 if path == missing
2739 ));
2740 }
2741
2742 #[test]
2743 fn session_requirement_is_retained_by_the_single_admission() {
2744 let root = tempfile::tempdir().unwrap();
2745 write_loading_fixture(root.path());
2746 let backend = LoadingMock::default();
2747
2748 let prepared = load_model(&backend, root.path(), 99).unwrap();
2749
2750 assert_eq!(*prepared, 99);
2751 assert_eq!(
2752 backend
2753 .selections
2754 .load(std::sync::atomic::Ordering::Relaxed),
2755 1
2756 );
2757 assert_eq!(
2758 backend
2759 .materializations
2760 .load(std::sync::atomic::Ordering::Relaxed),
2761 1
2762 );
2763 }
2764
2765 struct FixedController {
2766 tokens: Vec<u32>,
2767 committed: usize,
2768 }
2769
2770 impl TokenFilterController for FixedController {
2771 type Error = Infallible;
2772
2773 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
2774 let mut allowed = vec![false; 64];
2775 allowed[self.tokens[self.committed] as usize] = true;
2776 Ok(TokenFilter::allowed(allowed).unwrap())
2777 }
2778
2779 fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error> {
2780 assert_eq!(token_id, self.tokens[self.committed]);
2781 self.committed += 1;
2782 Ok(())
2783 }
2784
2785 fn is_complete(&mut self) -> Result<bool, Self::Error> {
2786 Ok(self.committed == self.tokens.len())
2787 }
2788 }
2789
2790 #[test]
2791 fn mock_prefill_and_multiple_decode_steps() {
2792 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2793 let prefill = runtime.prefill(vec![1, 2]).unwrap();
2794 assert_eq!(prefill.output, 12);
2795 assert!(prefill.completion.is_complete().unwrap());
2796 assert_eq!(runtime.decode(3).unwrap().output, 13);
2797 assert_eq!(runtime.decode(4).unwrap().output, 14);
2798 }
2799
2800 #[test]
2801 fn portable_text_generation_prefills_and_decodes_without_tensor_types() {
2802 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2803 let sampling = crate::resolve_generation_config(
2804 None,
2805 crate::GenerationConfigOverrides {
2806 max_new_tokens: Some(3),
2807 ..Default::default()
2808 },
2809 )
2810 .unwrap();
2811 let mut generation = TextGeneration::new(
2812 &mut runtime,
2813 vec![1, 2],
2814 TextGenerationConfig::new(sampling).with_seed(3),
2815 )
2816 .unwrap();
2817 assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 55);
2818 assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 13);
2819 assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 14);
2820 assert!(generation.next().is_none());
2821 }
2822
2823 #[test]
2824 fn portable_media_preparation_feeds_the_existing_generation_contract() {
2825 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2826 let request = crate::MultimodalRequest::new(vec![
2827 crate::MultimodalSegment::TokenIds(vec![7, 8]),
2828 crate::MultimodalSegment::Media(crate::Media::Image(
2829 crate::RgbImage::new(vec![5, 6, 7], 1, 1).unwrap(),
2830 )),
2831 crate::MultimodalSegment::TokenIds(vec![9]),
2832 ])
2833 .unwrap()
2834 .tokenize::<Infallible>(|_| unreachable!("request is already tokenized"))
2835 .unwrap();
2836 let prompt = Mock::prepare_multimodal_input(&runtime, &request, &mut |_| {
2837 Ok::<_, Infallible>(Vec::new())
2838 })
2839 .unwrap();
2840 assert_eq!(prompt, vec![7, 8, 1_001, 9]);
2841
2842 let sampling = crate::resolve_generation_config(
2843 None,
2844 crate::GenerationConfigOverrides {
2845 max_new_tokens: Some(2),
2846 ..Default::default()
2847 },
2848 )
2849 .unwrap();
2850 let mut generation =
2851 TextGeneration::from_prompt(&mut runtime, prompt, TextGenerationConfig::new(sampling))
2852 .unwrap();
2853 assert!(generation.next().unwrap().is_ok());
2854 assert!(generation.next().unwrap().is_ok());
2855 assert!(generation.next().is_none());
2856 }
2857
2858 #[test]
2859 fn model_capability_extension_observes_the_selected_mock_session() {
2860 let runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2861 let capabilities = Mock::model_capabilities(&runtime).unwrap();
2862 assert_eq!(capabilities.effective_model_type, "mock");
2863 let input = Mock::count_prepared_input(&runtime, &vec![1, 2, 3]).unwrap();
2864 assert_eq!(input.model_positions, 3);
2865 let state = Mock::estimate_runtime_state(&runtime, input, 2, 1).unwrap();
2866 assert_eq!(state.requested_state_bytes, 5 * 2 * 4);
2867 assert_eq!(
2868 Mock::static_memory(&runtime)
2869 .unwrap()
2870 .logical_parameter_bytes
2871 .value(),
2872 Some(&10)
2873 );
2874 }
2875
2876 #[test]
2877 fn controlled_generation_applies_portable_filters_and_commits_tokens() {
2878 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2879 let sampling = crate::resolve_generation_config(
2880 None,
2881 crate::GenerationConfigOverrides {
2882 max_new_tokens: Some(2),
2883 ..Default::default()
2884 },
2885 )
2886 .unwrap();
2887 let controller = FixedController {
2888 tokens: vec![7, 8],
2889 committed: 0,
2890 };
2891 let mut generation = ControlledTextGeneration::new(
2892 &mut runtime,
2893 vec![1, 2],
2894 TextGenerationConfig::new(sampling),
2895 controller,
2896 )
2897 .unwrap();
2898 assert_eq!(generation.next().unwrap().unwrap().token_id(), 7);
2899 assert_eq!(generation.next().unwrap().unwrap().token_id(), 8);
2900 assert!(generation.controller_mut().is_complete().unwrap());
2901 assert!(generation.next().is_none());
2902 }
2903
2904 fn continuation_config(limit: usize) -> TextGenerationConfig {
2905 TextGenerationConfig::new(
2906 crate::resolve_generation_config(
2907 None,
2908 crate::GenerationConfigOverrides {
2909 max_new_tokens: Some(limit),
2910 ..Default::default()
2911 },
2912 )
2913 .unwrap(),
2914 )
2915 }
2916
2917 #[test]
2918 fn detached_ordinary_continuation_preserves_pending_input_and_commit_order() {
2919 let controller = || FixedController {
2920 tokens: vec![7, 8, 9],
2921 committed: 0,
2922 };
2923 let mut ordinary_runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2924 let ordinary: Vec<_> = ControlledTextGeneration::new(
2925 &mut ordinary_runtime,
2926 vec![1, 2],
2927 continuation_config(3),
2928 controller(),
2929 )
2930 .unwrap()
2931 .map(|token| token.unwrap().token_id())
2932 .collect();
2933
2934 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2935 let mut driver = TextGenerationDriver::new(&mut runtime);
2936 let mut state = driver
2937 .start(vec![1, 2], continuation_config(3), controller())
2938 .unwrap();
2939 state.require_quiescent().unwrap();
2940 assert!(state.is_prefill_pending());
2941 assert!(driver.runtime().session().tokens.is_empty());
2942 let mut actual = Vec::new();
2943 for (index, expected) in ordinary.iter().enumerate() {
2944 let token = driver.advance(&mut state).unwrap().unwrap().token_id();
2945 actual.push(token);
2946 assert_eq!(token, *expected);
2947 assert_eq!(state.controller().committed, index + 1);
2948 assert_eq!(state.remaining_tokens(), Some(2 - index));
2949 let mut model_inputs = vec![1, 2];
2951 model_inputs.extend_from_slice(&ordinary[..index]);
2952 assert_eq!(driver.runtime().session().tokens, model_inputs);
2953 assert!(!state.is_prefill_pending());
2954 assert!(matches!(
2955 driver.advance(&mut state),
2956 Err(TextContinuationError::NotQuiescent)
2957 ));
2958 assert_eq!(state.controller().committed, index + 1);
2959 assert!(driver.take_completed_step(&mut state).unwrap().is_none());
2960 state.require_quiescent().unwrap();
2961 }
2962 assert!(driver.advance(&mut state).unwrap().is_none());
2963 assert_eq!(actual, ordinary);
2964 assert_eq!(
2965 driver.runtime().session().tokens,
2966 ordinary_runtime.session().tokens
2967 );
2968 }
2969
2970 #[test]
2971 fn detached_continuation_cannot_attach_to_another_driver() {
2972 let mut first = ModelRuntime::prepare(Mock, 10).unwrap();
2973 let mut other = ModelRuntime::prepare(Mock, 10).unwrap();
2974 let mut owner = TextGenerationDriver::new(&mut first);
2975 let mut state = owner
2976 .start(
2977 vec![1, 2],
2978 continuation_config(2),
2979 FixedTokenFilter(TokenFilter::All),
2980 )
2981 .unwrap();
2982 let mut foreign = TextGenerationDriver::new(&mut other);
2983 assert!(matches!(
2984 foreign.advance(&mut state),
2985 Err(TextContinuationError::IncompatibleDriver)
2986 ));
2987 assert!(foreign.runtime().session().tokens.is_empty());
2988 assert!(owner.advance(&mut state).unwrap().is_some());
2989 assert!(matches!(
2990 foreign.take_completed_step(&mut state),
2991 Err(TextContinuationError::IncompatibleDriver)
2992 ));
2993 owner.take_completed_step(&mut state).unwrap();
2994 drop(owner);
2995 let mut replacement = TextGenerationDriver::new(&mut first);
2996 assert!(matches!(
2997 replacement.advance(&mut state),
2998 Err(TextContinuationError::IncompatibleDriver)
2999 ));
3000 assert_eq!(replacement.runtime().session().tokens, vec![1, 2]);
3001 }
3002
3003 #[test]
3004 fn detached_continuation_failure_remains_fenced_after_draining() {
3005 struct RejectCommit;
3006 impl TokenFilterController for RejectCommit {
3007 type Error = std::io::Error;
3008 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
3009 Ok(TokenFilter::All)
3010 }
3011 fn commit_token(&mut self, _: u32) -> Result<(), Self::Error> {
3012 Err(std::io::Error::other("commit rejected"))
3013 }
3014 fn is_complete(&mut self) -> Result<bool, Self::Error> {
3015 Ok(false)
3016 }
3017 }
3018 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
3019 let mut driver = TextGenerationDriver::new(&mut runtime);
3020 let mut state = driver
3021 .start(vec![1, 2], continuation_config(3), RejectCommit)
3022 .unwrap();
3023 assert!(matches!(
3024 driver.advance(&mut state),
3025 Err(TextContinuationError::Generation(
3026 ControlledTextGenerationError::Controller(_)
3027 ))
3028 ));
3029 driver.take_completed_step(&mut state).unwrap();
3030 assert!(matches!(
3031 state.require_quiescent(),
3032 Err(TextContinuationError::Failed)
3033 ));
3034 assert!(matches!(
3035 driver.advance(&mut state),
3036 Err(TextContinuationError::Failed)
3037 ));
3038 assert_eq!(driver.runtime().session().tokens, vec![1, 2]);
3039 }
3040
3041 #[test]
3042 fn detached_continuation_caught_unwind_cannot_be_resumed() {
3043 struct PanickingFilter;
3044 impl TokenFilterController for PanickingFilter {
3045 type Error = Infallible;
3046 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
3047 panic!("filter failed while preparing the decision")
3048 }
3049 fn commit_token(&mut self, _: u32) -> Result<(), Self::Error> {
3050 Ok(())
3051 }
3052 fn is_complete(&mut self) -> Result<bool, Self::Error> {
3053 Ok(false)
3054 }
3055 }
3056 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
3057 let mut driver = TextGenerationDriver::new(&mut runtime);
3058 let mut state = driver
3059 .start(vec![1, 2], continuation_config(3), PanickingFilter)
3060 .unwrap();
3061 assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3062 driver.advance(&mut state)
3063 }))
3064 .is_err());
3065 driver.take_completed_step(&mut state).unwrap();
3066 assert!(matches!(
3067 state.require_quiescent(),
3068 Err(TextContinuationError::Failed)
3069 ));
3070 assert!(matches!(
3071 driver.advance(&mut state),
3072 Err(TextContinuationError::Failed)
3073 ));
3074 assert!(driver.runtime().session().tokens.is_empty());
3075 }
3076
3077 #[derive(Debug, Clone)]
3078 struct MockDistributed {
3079 descriptor: DistributedSessionDescriptor,
3080 }
3081
3082 impl DistributedSession for MockDistributed {
3083 type Value = Vec<u32>;
3084 type Completion = Done;
3085 type Error = Infallible;
3086
3087 fn descriptor(&self) -> DistributedSessionDescriptor {
3088 self.descriptor.clone()
3089 }
3090
3091 fn capabilities(&self) -> DistributedCapabilities {
3092 DistributedCapabilities::new(true, [CollectiveGroupId::new(7)], true, true, true)
3093 }
3094
3095 fn all_reduce_sum(
3096 &self,
3097 _: CollectiveScope,
3098 input: &Vec<u32>,
3099 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3100 Ok(Submission {
3101 output: input.iter().map(|value| value * 2).collect(),
3102 completion: Done,
3103 })
3104 }
3105
3106 fn all_gather(
3107 &self,
3108 _: CollectiveScope,
3109 input: &Vec<u32>,
3110 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3111 let mut output = input.clone();
3112 output.extend(input);
3113 Ok(Submission {
3114 output,
3115 completion: Done,
3116 })
3117 }
3118
3119 fn all_to_all_v(
3120 &self,
3121 _: CollectiveScope,
3122 input: &Vec<u32>,
3123 _: &[usize],
3124 _: &[usize],
3125 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3126 Ok(Submission {
3127 output: input.clone(),
3128 completion: Done,
3129 })
3130 }
3131
3132 fn send(
3133 &self,
3134 _: CollectiveScope,
3135 _: usize,
3136 input: &Vec<u32>,
3137 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3138 Ok(Submission {
3139 output: input.clone(),
3140 completion: Done,
3141 })
3142 }
3143
3144 fn receive(
3145 &self,
3146 _: CollectiveScope,
3147 peer: usize,
3148 value: &ValueDescriptor,
3149 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3150 Ok(Submission {
3151 output: vec![peer as u32; value.shape().iter().product()],
3152 completion: Done,
3153 })
3154 }
3155
3156 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Infallible> {
3157 let mut output = local.to_vec();
3158 output.extend_from_slice(local);
3159 Ok(output)
3160 }
3161 }
3162
3163 impl DistributedBackend for Mock {
3164 type DistributedSession = MockDistributed;
3165
3166 fn distributed_session(session: &MockSession) -> Option<&Self::DistributedSession> {
3167 session.distributed.as_ref()
3168 }
3169 }
3170
3171 #[test]
3172 fn mock_distributed_session_owns_collective_and_transfer_lifecycle() {
3173 let tensor_group =
3174 CollectiveGroupDescriptor::new(CollectiveGroupId::new(7), vec![0, 1], 0).unwrap();
3175 let session = MockDistributed {
3176 descriptor: DistributedSessionDescriptor::new(2, 0, vec![tensor_group]).unwrap(),
3177 };
3178 let capabilities = session.capabilities();
3179 assert!(capabilities.exact_completion());
3180 assert_eq!(
3181 capabilities.collective_groups(),
3182 &[CollectiveGroupId::new(7)]
3183 );
3184 assert_eq!(
3185 session
3186 .all_reduce_sum(
3187 CollectiveScope::Group(CollectiveGroupId::new(7)),
3188 &vec![2, 3]
3189 )
3190 .unwrap()
3191 .wait()
3192 .unwrap(),
3193 vec![4, 6]
3194 );
3195 assert_eq!(
3196 session
3197 .receive(
3198 CollectiveScope::World,
3199 1,
3200 &ValueDescriptor::new(vec![2], TensorDtype::U32).unwrap(),
3201 )
3202 .unwrap()
3203 .wait()
3204 .unwrap(),
3205 vec![1, 1]
3206 );
3207 assert_eq!(session.all_gather_words(&[7]).unwrap(), vec![7, 7]);
3208
3209 let model_session = MockSession {
3210 model: 0,
3211 tokens: Vec::new(),
3212 distributed: Some(session.clone()),
3213 };
3214 assert_eq!(
3215 Mock::distributed_session(&model_session)
3216 .unwrap()
3217 .descriptor(),
3218 session.descriptor()
3219 );
3220 }
3221
3222 #[test]
3223 fn distributed_descriptors_round_trip_and_reject_invalid_ranks() {
3224 let descriptor = DistributedSessionDescriptor::new(
3225 6,
3226 4,
3227 vec![CollectiveGroupDescriptor::new(CollectiveGroupId::new(9), vec![1, 4], 1).unwrap()],
3228 )
3229 .unwrap();
3230 let encoded = serde_json::to_string(&descriptor).unwrap();
3231 assert_eq!(
3232 serde_json::from_str::<DistributedSessionDescriptor>(&encoded).unwrap(),
3233 descriptor
3234 );
3235 let scope = CollectiveScope::Group(CollectiveGroupId::new(9));
3236 assert_eq!(
3237 serde_json::from_str::<CollectiveScope>(&serde_json::to_string(&scope).unwrap())
3238 .unwrap(),
3239 scope
3240 );
3241 assert!(DistributedSessionDescriptor::new(descriptor.world_size(), 6, Vec::new()).is_err());
3242 assert!(serde_json::from_str::<DistributedSessionDescriptor>(
3243 r#"{"world_size":6,"rank":6,"groups":[]}"#
3244 )
3245 .is_err());
3246 }
3247
3248 #[test]
3249 fn distributed_commit_epoch_round_trips_and_rejects_zero() {
3250 let outcome = DistributedCommitOutcome::Indeterminate {
3251 epoch: DistributedCommitEpoch::new(17).unwrap(),
3252 phase: DistributedCommitPhase::DecisionCompletion,
3253 };
3254 let encoded = serde_json::to_string(&outcome).unwrap();
3255 assert_eq!(
3256 serde_json::from_str::<DistributedCommitOutcome>(&encoded).unwrap(),
3257 outcome
3258 );
3259 assert!(serde_json::from_str::<DistributedCommitEpoch>("0").is_err());
3260 }
3261}