1use serde::{Deserialize, Serialize};
4use std::{fmt::Debug, path::Path};
5
6use crate::{
7 artifact::{
8 inspect_artifact, ArtifactError, ArtifactInspection, ModelConfigurationResolver,
9 ModelPreparationPlan,
10 },
11 capability::{
12 CapabilityError, InputTokenCount, ModelCapabilities, RuntimeStateEstimate,
13 StaticMemoryReport,
14 },
15 checkpoint::TensorDtype,
16 generation::{GenerationError, ResolvedGenerationConfig},
17 media::TokenizedMultimodalRequest,
18 observation::{InspectedOutput, ObservationRequest, ObservationSet},
19 PreparationAdmission,
20};
21
22#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
24pub struct BackendDescriptor {
25 name: String,
27 version: String,
29}
30
31impl BackendDescriptor {
32 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
34 Self {
35 name: name.into(),
36 version: version.into(),
37 }
38 }
39
40 pub fn name(&self) -> &str {
42 &self.name
43 }
44
45 pub fn version(&self) -> &str {
47 &self.version
48 }
49}
50
51#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
53pub struct DeviceDescriptor {
54 id: String,
56 name: String,
58 family: String,
60 memory_bytes: Option<u64>,
62}
63
64impl DeviceDescriptor {
65 pub fn new(
67 id: impl Into<String>,
68 name: impl Into<String>,
69 family: impl Into<String>,
70 memory_bytes: Option<u64>,
71 ) -> Self {
72 Self {
73 id: id.into(),
74 name: name.into(),
75 family: family.into(),
76 memory_bytes,
77 }
78 }
79
80 pub fn id(&self) -> &str {
82 &self.id
83 }
84 pub fn name(&self) -> &str {
86 &self.name
87 }
88 pub fn family(&self) -> &str {
90 &self.family
91 }
92 pub const fn memory_bytes(&self) -> Option<u64> {
94 self.memory_bytes
95 }
96}
97
98#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
100pub struct DeviceCapabilities {
101 exact_completion: bool,
103 transfers: bool,
105 collectives: bool,
107}
108
109impl DeviceCapabilities {
110 pub const fn new(exact_completion: bool, transfers: bool, collectives: bool) -> Self {
112 Self {
113 exact_completion,
114 transfers,
115 collectives,
116 }
117 }
118
119 pub const fn exact_completion(&self) -> bool {
121 self.exact_completion
122 }
123 pub const fn transfers(&self) -> bool {
125 self.transfers
126 }
127 pub const fn collectives(&self) -> bool {
129 self.collectives
130 }
131}
132
133#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
135pub struct SessionCapabilities {
136 persistent_cache: bool,
138 output_observation: bool,
140 activation_inspection: bool,
142}
143
144impl SessionCapabilities {
145 pub const fn new(
147 persistent_cache: bool,
148 output_observation: bool,
149 activation_inspection: bool,
150 ) -> Self {
151 Self {
152 persistent_cache,
153 output_observation,
154 activation_inspection,
155 }
156 }
157
158 pub const fn persistent_cache(self) -> bool {
160 self.persistent_cache
161 }
162 pub const fn output_observation(self) -> bool {
164 self.output_observation
165 }
166 pub const fn activation_inspection(self) -> bool {
168 self.activation_inspection
169 }
170
171 pub const fn with_persistent_cache(mut self, supported: bool) -> Self {
173 self.persistent_cache = supported;
174 self
175 }
176 pub const fn with_output_observation(mut self, supported: bool) -> Self {
178 self.output_observation = supported;
179 self
180 }
181 pub const fn with_activation_inspection(mut self, supported: bool) -> Self {
183 self.activation_inspection = supported;
184 self
185 }
186 pub fn validate(&self, available: &Self) -> Result<(), SessionCapabilityError> {
188 for (required, supported, capability) in [
189 (
190 self.persistent_cache,
191 available.persistent_cache,
192 "persistent_cache",
193 ),
194 (
195 self.output_observation,
196 available.output_observation,
197 "output_observation",
198 ),
199 (
200 self.activation_inspection,
201 available.activation_inspection,
202 "activation_inspection",
203 ),
204 ] {
205 if required && !supported {
206 return Err(SessionCapabilityError { capability });
207 }
208 }
209 Ok(())
210 }
211}
212
213#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
215#[error("prepared session does not support required capability {capability}")]
216pub struct SessionCapabilityError {
217 capability: &'static str,
218}
219
220impl SessionCapabilityError {
221 pub const fn capability(self) -> &'static str {
223 self.capability
224 }
225}
226
227#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
229pub struct DistributedCapabilities {
230 world_collectives: bool,
231 collective_groups: Vec<CollectiveGroupId>,
232 point_to_point: bool,
233 variable_all_to_all: bool,
234 exact_completion: bool,
235}
236
237impl DistributedCapabilities {
238 pub fn new(
240 world_collectives: bool,
241 collective_groups: impl IntoIterator<Item = CollectiveGroupId>,
242 point_to_point: bool,
243 variable_all_to_all: bool,
244 exact_completion: bool,
245 ) -> Self {
246 Self {
247 world_collectives,
248 collective_groups: collective_groups.into_iter().collect(),
249 point_to_point,
250 variable_all_to_all,
251 exact_completion,
252 }
253 }
254
255 pub const fn world_collectives(&self) -> bool {
257 self.world_collectives
258 }
259 pub fn collective_groups(&self) -> &[CollectiveGroupId] {
261 &self.collective_groups
262 }
263 pub const fn point_to_point(&self) -> bool {
265 self.point_to_point
266 }
267 pub const fn variable_all_to_all(&self) -> bool {
269 self.variable_all_to_all
270 }
271 pub const fn exact_completion(&self) -> bool {
273 self.exact_completion
274 }
275}
276
277#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
279#[serde(transparent)]
280pub struct CollectiveGroupId(u32);
281
282impl CollectiveGroupId {
283 pub const fn new(value: u32) -> Self {
285 Self(value)
286 }
287 pub const fn value(self) -> u32 {
289 self.0
290 }
291}
292
293#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
295pub struct CollectiveGroupDescriptor {
296 id: CollectiveGroupId,
297 members: Vec<usize>,
298 local_rank: usize,
299}
300
301impl CollectiveGroupDescriptor {
302 pub fn new(
304 id: CollectiveGroupId,
305 members: Vec<usize>,
306 local_rank: usize,
307 ) -> Result<Self, BackendError> {
308 if members.is_empty() || local_rank >= members.len() {
309 return Err(BackendError::Preparation {
310 operation: "collective group realization".into(),
311 message: "collective membership must be non-empty and contain local rank".into(),
312 });
313 }
314 let mut unique = std::collections::BTreeSet::new();
315 if !members.iter().all(|rank| unique.insert(*rank)) {
316 return Err(BackendError::Preparation {
317 operation: "collective group realization".into(),
318 message: "collective membership contains duplicate world ranks".into(),
319 });
320 }
321 Ok(Self {
322 id,
323 members,
324 local_rank,
325 })
326 }
327
328 pub const fn id(&self) -> CollectiveGroupId {
330 self.id
331 }
332 pub fn members(&self) -> &[usize] {
334 &self.members
335 }
336 pub const fn local_rank(&self) -> usize {
338 self.local_rank
339 }
340}
341
342impl<'de> Deserialize<'de> for CollectiveGroupDescriptor {
343 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
344 where
345 D: serde::Deserializer<'de>,
346 {
347 #[derive(Deserialize)]
348 struct Raw {
349 id: CollectiveGroupId,
350 members: Vec<usize>,
351 local_rank: usize,
352 }
353 let raw = Raw::deserialize(deserializer)?;
354 Self::new(raw.id, raw.members, raw.local_rank).map_err(serde::de::Error::custom)
355 }
356}
357
358#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
360#[serde(tag = "kind", content = "group", rename_all = "snake_case")]
361#[non_exhaustive]
362pub enum CollectiveScope {
363 World,
365 Group(CollectiveGroupId),
367}
368
369#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
371pub struct ValueDescriptor {
372 shape: Vec<usize>,
374 dtype: TensorDtype,
376}
377
378impl ValueDescriptor {
379 pub fn new(shape: Vec<usize>, dtype: TensorDtype) -> Result<Self, BackendError> {
381 if shape.contains(&0) {
382 return Err(BackendError::Preparation {
383 operation: "distributed value descriptor".into(),
384 message: "non-scalar distributed values require positive dimensions".into(),
385 });
386 }
387 Ok(Self { shape, dtype })
388 }
389
390 pub fn shape(&self) -> &[usize] {
392 &self.shape
393 }
394
395 pub const fn dtype(&self) -> &TensorDtype {
397 &self.dtype
398 }
399}
400
401impl<'de> Deserialize<'de> for ValueDescriptor {
402 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
403 where
404 D: serde::Deserializer<'de>,
405 {
406 #[derive(Deserialize)]
407 struct RawDescriptor {
408 shape: Vec<usize>,
409 dtype: TensorDtype,
410 }
411
412 let raw = RawDescriptor::deserialize(deserializer)?;
413 Self::new(raw.shape, raw.dtype).map_err(serde::de::Error::custom)
414 }
415}
416
417#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
419pub struct DistributedSessionDescriptor {
420 world_size: usize,
421 rank: usize,
422 groups: Vec<CollectiveGroupDescriptor>,
423}
424
425impl DistributedSessionDescriptor {
426 pub fn new(
428 world_size: usize,
429 rank: usize,
430 groups: Vec<CollectiveGroupDescriptor>,
431 ) -> Result<Self, BackendError> {
432 if world_size == 0 || rank >= world_size {
433 return Err(BackendError::Preparation {
434 operation: "distributed session realization".into(),
435 message: format!("rank {rank} is outside world size {world_size}"),
436 });
437 }
438 let mut ids = std::collections::BTreeSet::new();
439 for group in &groups {
440 if !ids.insert(group.id())
441 || group.members().iter().any(|member| *member >= world_size)
442 || group.members()[group.local_rank()] != rank
443 {
444 return Err(BackendError::Preparation {
445 operation: "distributed session realization".into(),
446 message: "collective groups must have unique IDs, in-range members, and the declared local world rank".into(),
447 });
448 }
449 }
450 Ok(Self {
451 world_size,
452 rank,
453 groups,
454 })
455 }
456
457 pub const fn world_size(&self) -> usize {
459 self.world_size
460 }
461 pub const fn rank(&self) -> usize {
463 self.rank
464 }
465 pub fn groups(&self) -> &[CollectiveGroupDescriptor] {
467 &self.groups
468 }
469}
470
471impl<'de> Deserialize<'de> for DistributedSessionDescriptor {
472 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
473 where
474 D: serde::Deserializer<'de>,
475 {
476 #[derive(Deserialize)]
477 struct RawDescriptor {
478 world_size: usize,
479 rank: usize,
480 groups: Vec<CollectiveGroupDescriptor>,
481 }
482
483 let raw = RawDescriptor::deserialize(deserializer)?;
484 Self::new(raw.world_size, raw.rank, raw.groups).map_err(serde::de::Error::custom)
485 }
486}
487
488#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
490#[non_exhaustive]
491pub enum BackendError {
492 #[error("backend {backend} does not support required capability {capability}")]
494 Unsupported {
495 backend: String,
497 capability: String,
499 },
500 #[error("backend model preparation failed during {operation}: {message}")]
502 Preparation {
503 operation: String,
505 message: String,
507 },
508 #[error("backend session {session} failed during {operation}: {message}")]
510 Execution {
511 session: String,
513 operation: String,
515 message: String,
517 },
518 #[error("backend completion observation failed: {message}")]
520 Completion {
521 message: String,
523 },
524}
525
526pub trait Completion {
528 type Error: std::error::Error + Send + Sync + 'static;
530
531 fn is_complete(&self) -> Result<bool, Self::Error>;
533
534 fn wait(&self) -> Result<(), Self::Error>;
537
538 fn resources_releasable(&self) -> bool {
552 matches!(self.is_complete(), Ok(true))
553 }
554}
555
556#[derive(
558 Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Serialize, serde::Deserialize,
559)]
560pub enum CompletionCancellationMode {
561 NativeCancel,
563 QuarantineUntilComplete,
566}
567
568#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
570#[serde(transparent)]
571pub struct DistributedCommitEpoch(u64);
572
573impl DistributedCommitEpoch {
574 pub const FIRST: Self = Self(1);
576
577 pub const fn new(value: u64) -> Option<Self> {
579 if value == 0 {
580 None
581 } else {
582 Some(Self(value))
583 }
584 }
585
586 pub const fn value(self) -> u64 {
588 self.0
589 }
590
591 pub const fn next(self) -> Option<Self> {
593 match self.0.checked_add(1) {
594 Some(value) => Some(Self(value)),
595 None => None,
596 }
597 }
598}
599
600impl<'de> Deserialize<'de> for DistributedCommitEpoch {
601 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
602 where
603 D: serde::Deserializer<'de>,
604 {
605 let value = u64::deserialize(deserializer)?;
606 Self::new(value).ok_or_else(|| serde::de::Error::custom("commit epoch must be positive"))
607 }
608}
609
610#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
612#[serde(rename_all = "snake_case")]
613pub enum DistributedCommitPhase {
614 DecisionSubmission,
616 DecisionCompletion,
618 DecisionObservation,
620}
621
622#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
624#[serde(rename_all = "snake_case")]
625pub enum DistributedCommitOutcome {
626 Committed(DistributedCommitEpoch),
628 Aborted(DistributedCommitEpoch),
630 Indeterminate {
632 epoch: DistributedCommitEpoch,
634 phase: DistributedCommitPhase,
636 },
637}
638
639impl DistributedCommitOutcome {
640 pub const fn epoch(self) -> DistributedCommitEpoch {
642 match self {
643 Self::Committed(epoch) | Self::Aborted(epoch) | Self::Indeterminate { epoch, .. } => {
644 epoch
645 }
646 }
647 }
648
649 pub const fn is_indeterminate(self) -> bool {
651 matches!(self, Self::Indeterminate { .. })
652 }
653}
654
655#[derive(Debug, Clone, Copy, Eq, PartialEq)]
657pub struct BoundedCompletionWait {
658 timeout: std::time::Duration,
659 cancellation: CompletionCancellationMode,
660}
661
662impl BoundedCompletionWait {
663 pub fn new(
665 timeout: std::time::Duration,
666 cancellation: CompletionCancellationMode,
667 ) -> Result<Self, BoundedCompletionWaitError> {
668 if timeout.is_zero() {
669 return Err(BoundedCompletionWaitError::ZeroTimeout);
670 }
671 Ok(Self {
672 timeout,
673 cancellation,
674 })
675 }
676
677 pub const fn timeout(self) -> std::time::Duration {
679 self.timeout
680 }
681
682 pub const fn cancellation(self) -> CompletionCancellationMode {
684 self.cancellation
685 }
686}
687
688#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
690pub enum BoundedCompletionWaitError {
691 #[error("bounded completion timeout must be positive")]
693 ZeroTimeout,
694}
695
696#[derive(Debug, Clone, Copy, Eq, PartialEq)]
698pub enum BoundedCompletionOutcome {
699 Completed,
701 DeadlineExceeded {
703 cancellation: CompletionCancellationMode,
705 },
706}
707
708pub trait BoundedCompletion: Completion + Sized {
719 fn supports_cancellation(_cancellation: CompletionCancellationMode) -> bool {
723 true
724 }
725
726 fn wait_bounded(
729 self,
730 policy: BoundedCompletionWait,
731 ) -> Result<BoundedCompletionOutcome, Self::Error>;
732}
733
734#[derive(Debug)]
736pub struct Submission<T, C> {
737 pub output: T,
739 pub completion: C,
741}
742
743impl<T, C> Submission<T, C>
744where
745 C: Completion,
746{
747 pub fn wait(self) -> Result<T, C::Error> {
749 self.completion.wait()?;
750 Ok(self.output)
751 }
752}
753
754impl<T, C> Submission<T, C>
755where
756 C: BoundedCompletion,
757{
758 pub fn wait_bounded(
762 self,
763 policy: BoundedCompletionWait,
764 ) -> Result<BoundedSubmissionOutcome<T>, C::Error> {
765 match self.completion.wait_bounded(policy)? {
766 BoundedCompletionOutcome::Completed => {
767 Ok(BoundedSubmissionOutcome::Completed(self.output))
768 }
769 BoundedCompletionOutcome::DeadlineExceeded { cancellation } => {
770 Ok(BoundedSubmissionOutcome::DeadlineExceeded { cancellation })
771 }
772 }
773 }
774}
775
776#[derive(Debug, Eq, PartialEq)]
778pub enum BoundedSubmissionOutcome<T> {
779 Completed(T),
781 DeadlineExceeded {
783 cancellation: CompletionCancellationMode,
785 },
786}
787
788#[derive(Debug)]
790pub struct PreparedModel<M> {
791 model: M,
792 capabilities: SessionCapabilities,
793}
794
795impl<M> PreparedModel<M> {
796 pub const fn new(model: M, capabilities: SessionCapabilities) -> Self {
798 Self {
799 model,
800 capabilities,
801 }
802 }
803 pub const fn get(&self) -> &M {
805 &self.model
806 }
807 pub fn get_mut(&mut self) -> &mut M {
809 &mut self.model
810 }
811 pub const fn capabilities(&self) -> SessionCapabilities {
813 self.capabilities
814 }
815 pub fn into_inner(self) -> M {
817 self.model
818 }
819 pub fn into_parts(self) -> (M, SessionCapabilities) {
821 (self.model, self.capabilities)
822 }
823}
824
825impl<M> std::ops::Deref for PreparedModel<M> {
826 type Target = M;
827
828 fn deref(&self) -> &Self::Target {
829 self.get()
830 }
831}
832
833impl<M> std::ops::DerefMut for PreparedModel<M> {
834 fn deref_mut(&mut self) -> &mut Self::Target {
835 self.get_mut()
836 }
837}
838
839pub trait BackendProvider: Sized {
841 type ModelConfig;
843 type Model;
845 type Session: BackendSession<Self>;
847 type Error: std::error::Error + Send + Sync + 'static;
849
850 fn descriptor(&self) -> BackendDescriptor;
852 fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error>;
854 fn prepare_model(
856 &self,
857 config: Self::ModelConfig,
858 ) -> Result<PreparedModel<Self::Model>, Self::Error>;
859 fn create_session(
865 &self,
866 model: PreparedModel<Self::Model>,
867 ) -> Result<Self::Session, Self::Error>;
868
869 fn session_capability_mismatch(
874 &self,
875 admitted: SessionCapabilities,
876 realized: SessionCapabilities,
877 ) -> Self::Error {
878 panic!("backend realized session capabilities {realized:?} after admitting {admitted:?}")
879 }
880}
881
882pub trait ModelLoadingBackend: BackendProvider {
889 type LoadOptions;
891
892 type SelectedPreparation;
895
896 type ConfigurationResolver: ModelConfigurationResolver;
898
899 fn configuration_resolver(&self) -> &Self::ConfigurationResolver;
901
902 fn select_preparation(
910 &self,
911 inspection: &ArtifactInspection<
912 <Self::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
913 >,
914 options: &Self::LoadOptions,
915 ) -> Result<Self::SelectedPreparation, Self::Error>;
916
917 fn selected_preparation_admission(
919 &self,
920 selected: &Self::SelectedPreparation,
921 ) -> PreparationAdmission;
922
923 fn model_config(
926 &self,
927 selected: SelectedModelPreparation<Self>,
928 ) -> Result<Self::ModelConfig, Self::Error>;
929}
930
931pub struct SelectedModelPreparation<B: ModelLoadingBackend> {
938 plan: ModelPreparationPlan<
939 <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
940 >,
941 selected: B::SelectedPreparation,
942}
943
944impl<B: ModelLoadingBackend> SelectedModelPreparation<B> {
945 pub(crate) fn new(
946 plan: ModelPreparationPlan<
947 <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
948 >,
949 selected: B::SelectedPreparation,
950 ) -> Self {
951 Self { plan, selected }
952 }
953
954 pub(crate) const fn plan(
955 &self,
956 ) -> &ModelPreparationPlan<<B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan>
957 {
958 &self.plan
959 }
960
961 pub fn into_parts(
963 self,
964 ) -> (
965 ModelPreparationPlan<
966 <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
967 >,
968 B::SelectedPreparation,
969 ) {
970 (self.plan, self.selected)
971 }
972}
973
974#[derive(Debug, thiserror::Error)]
976#[non_exhaustive]
977pub enum ModelLoadError<E: std::error::Error + Send + Sync + 'static> {
978 #[error(transparent)]
980 Artifact(#[from] ArtifactError),
981 #[error("selected backend failed to prepare the model: {0}")]
983 Backend(#[source] E),
984 #[error(transparent)]
986 SessionCapability(#[from] SessionCapabilityError),
987}
988
989pub fn load_model<B: ModelLoadingBackend>(
995 backend: &B,
996 artifact: impl AsRef<Path>,
997 options: B::LoadOptions,
998) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
999 let inspection = inspect_artifact(artifact, backend.configuration_resolver())?;
1000 prepare_inspected_model(backend, inspection, options)
1001}
1002
1003pub fn prepare_inspected_model<B: ModelLoadingBackend>(
1009 backend: &B,
1010 inspection: ArtifactInspection<
1011 <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
1012 >,
1013 options: B::LoadOptions,
1014) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
1015 let selected = backend
1016 .select_preparation(&inspection, &options)
1017 .map_err(ModelLoadError::Backend)?;
1018 let admission = backend.selected_preparation_admission(&selected);
1019 let plan = ModelPreparationPlan::from_retained_admission(inspection, admission)?;
1020 prepare_selected_model(backend, SelectedModelPreparation::new(plan, selected))
1021}
1022
1023pub(crate) fn prepare_selected_model<B: ModelLoadingBackend>(
1029 backend: &B,
1030 selected: SelectedModelPreparation<B>,
1031) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
1032 let config = backend
1033 .model_config(selected)
1034 .map_err(ModelLoadError::Backend)?;
1035 backend
1036 .prepare_model(config)
1037 .map_err(ModelLoadError::Backend)
1038}
1039
1040pub trait BackendSession<B: BackendProvider> {
1046 type PrefillInput;
1048 type DecodeInput;
1050 type Output;
1052 type Completion: Completion<Error = B::Error>;
1054
1055 fn capabilities(&self) -> SessionCapabilities;
1057
1058 fn prefill(
1060 &mut self,
1061 backend: &B,
1062 input: Self::PrefillInput,
1063 ) -> Result<Submission<Self::Output, Self::Completion>, B::Error>;
1064
1065 fn decode(
1067 &mut self,
1068 backend: &B,
1069 input: Self::DecodeInput,
1070 ) -> Result<Submission<Self::Output, Self::Completion>, B::Error>;
1071
1072 fn observe_output(
1077 &self,
1078 backend: &B,
1079 output: &Self::Output,
1080 ) -> Result<ObservationSet, B::Error>;
1081}
1082
1083pub trait InspectableBackendSession<B: BackendProvider>: BackendSession<B> {
1090 fn inspect_prefill(
1092 &mut self,
1093 backend: &B,
1094 input: Self::PrefillInput,
1095 request: &ObservationRequest,
1096 ) -> Result<InspectedOutput<Self::Output>, B::Error>;
1097
1098 fn inspect_decode(
1100 &mut self,
1101 backend: &B,
1102 input: Self::DecodeInput,
1103 request: &ObservationRequest,
1104 ) -> Result<InspectedOutput<Self::Output>, B::Error>;
1105}
1106
1107pub type SessionSubmission<B> = Submission<
1109 <<B as BackendProvider>::Session as BackendSession<B>>::Output,
1110 <<B as BackendProvider>::Session as BackendSession<B>>::Completion,
1111>;
1112
1113pub struct ModelRuntime<B: BackendProvider> {
1122 backend: B,
1123 session: B::Session,
1124 admission: crate::SessionAdmission,
1125 execution_plan_target_id: Option<u64>,
1126}
1127
1128impl<B: BackendProvider> ModelRuntime<B> {
1129 pub fn prepare(backend: B, config: B::ModelConfig) -> Result<Self, B::Error> {
1131 let model = backend.prepare_model(config)?;
1132 Self::from_prepared(backend, model)
1133 }
1134
1135 pub fn from_prepared(backend: B, model: PreparedModel<B::Model>) -> Result<Self, B::Error> {
1137 Self::from_prepared_with_execution_plan_target(backend, model, None)
1138 }
1139
1140 pub(crate) fn from_prepared_execution_plan_target(
1141 backend: B,
1142 model: PreparedModel<B::Model>,
1143 execution_plan_target_id: u64,
1144 ) -> Result<Self, B::Error> {
1145 Self::from_prepared_with_execution_plan_target(
1146 backend,
1147 model,
1148 Some(execution_plan_target_id),
1149 )
1150 }
1151
1152 fn from_prepared_with_execution_plan_target(
1153 backend: B,
1154 model: PreparedModel<B::Model>,
1155 execution_plan_target_id: Option<u64>,
1156 ) -> Result<Self, B::Error> {
1157 let admitted = model.capabilities();
1158 let session = backend.create_session(model)?;
1159 let realized = session.capabilities();
1160 if crate::SessionAdmission::new(admitted)
1161 .validate(realized)
1162 .is_err()
1163 {
1164 return Err(backend.session_capability_mismatch(admitted, realized));
1165 }
1166 Ok(Self {
1167 backend,
1168 session,
1169 admission: crate::SessionAdmission::new(admitted),
1170 execution_plan_target_id,
1171 })
1172 }
1173
1174 pub(crate) const fn execution_plan_target_id(&self) -> Option<u64> {
1175 self.execution_plan_target_id
1176 }
1177
1178 pub const fn backend(&self) -> &B {
1180 &self.backend
1181 }
1182
1183 pub const fn session(&self) -> &B::Session {
1185 &self.session
1186 }
1187
1188 pub fn session_mut(&mut self) -> &mut B::Session {
1195 self.execution_plan_target_id = None;
1196 &mut self.session
1197 }
1198
1199 pub fn parts_mut(&mut self) -> (&B, &mut B::Session) {
1203 self.execution_plan_target_id = None;
1204 (&self.backend, &mut self.session)
1205 }
1206
1207 fn validate_session_admission(&self) -> Result<(), B::Error> {
1208 self.admission
1209 .validate(self.session.capabilities())
1210 .map_err(|error| {
1211 self.backend
1212 .session_capability_mismatch(error.admitted(), error.realized())
1213 })
1214 }
1215
1216 pub fn capabilities(&self) -> SessionCapabilities {
1218 self.session.capabilities()
1219 }
1220
1221 pub fn prefill(
1223 &mut self,
1224 input: <B::Session as BackendSession<B>>::PrefillInput,
1225 ) -> Result<SessionSubmission<B>, B::Error> {
1226 self.validate_session_admission()?;
1227 self.session.prefill(&self.backend, input)
1228 }
1229
1230 pub fn decode(
1232 &mut self,
1233 input: <B::Session as BackendSession<B>>::DecodeInput,
1234 ) -> Result<SessionSubmission<B>, B::Error> {
1235 self.validate_session_admission()?;
1236 self.session.decode(&self.backend, input)
1237 }
1238
1239 pub fn observe_output(
1241 &self,
1242 output: &<B::Session as BackendSession<B>>::Output,
1243 ) -> Result<ObservationSet, B::Error> {
1244 self.validate_session_admission()?;
1245 self.session.observe_output(&self.backend, output)
1246 }
1247}
1248
1249impl<B> ModelRuntime<B>
1250where
1251 B: BackendProvider,
1252 B::Session: InspectableBackendSession<B>,
1253{
1254 pub fn inspect_prefill(
1256 &mut self,
1257 input: <B::Session as BackendSession<B>>::PrefillInput,
1258 request: &ObservationRequest,
1259 ) -> Result<InspectedOutput<<B::Session as BackendSession<B>>::Output>, B::Error> {
1260 self.validate_session_admission()?;
1261 self.session.inspect_prefill(&self.backend, input, request)
1262 }
1263
1264 pub fn inspect_decode(
1266 &mut self,
1267 input: <B::Session as BackendSession<B>>::DecodeInput,
1268 request: &ObservationRequest,
1269 ) -> Result<InspectedOutput<<B::Session as BackendSession<B>>::Output>, B::Error> {
1270 self.validate_session_admission()?;
1271 self.session.inspect_decode(&self.backend, input, request)
1272 }
1273}
1274
1275impl<B: ModelLoadingBackend> ModelRuntime<B> {
1276 pub fn load(
1278 backend: B,
1279 artifact: impl AsRef<Path>,
1280 options: B::LoadOptions,
1281 ) -> Result<Self, ModelLoadError<B::Error>> {
1282 let model = load_model(&backend, artifact, options)?;
1283 Self::from_prepared(backend, model).map_err(ModelLoadError::Backend)
1284 }
1285}
1286
1287#[derive(Debug, Clone, Copy, PartialEq)]
1289pub struct TextGenerationConfig {
1290 sampling: ResolvedGenerationConfig,
1291 seed: u64,
1292 strategy: TextSamplingStrategy,
1293}
1294
1295#[derive(Debug, Clone, Copy, Default, PartialEq)]
1297pub enum TextSamplingStrategy {
1298 #[default]
1300 Standard,
1301 MirostatV2 {
1303 tau: f32,
1305 eta: f32,
1307 },
1308}
1309
1310impl TextGenerationConfig {
1311 pub const fn new(sampling: ResolvedGenerationConfig) -> Self {
1313 Self {
1314 sampling,
1315 seed: 0,
1316 strategy: TextSamplingStrategy::Standard,
1317 }
1318 }
1319
1320 pub const fn with_seed(mut self, seed: u64) -> Self {
1322 self.seed = seed;
1323 self
1324 }
1325
1326 pub fn with_mirostat_v2(mut self, tau: f32, eta: f32) -> Result<Self, GenerationError> {
1328 if !tau.is_finite() || tau <= 0.0 {
1329 return Err(GenerationError::InvalidMirostatTau(tau));
1330 }
1331 if !eta.is_finite() || eta <= 0.0 {
1332 return Err(GenerationError::InvalidMirostatEta(eta));
1333 }
1334 self.strategy = TextSamplingStrategy::MirostatV2 { tau, eta };
1335 Ok(self)
1336 }
1337
1338 pub const fn sampling(&self) -> ResolvedGenerationConfig {
1340 self.sampling
1341 }
1342
1343 pub const fn seed(&self) -> u64 {
1345 self.seed
1346 }
1347
1348 pub const fn strategy(&self) -> TextSamplingStrategy {
1350 self.strategy
1351 }
1352}
1353
1354pub trait TokenOutput: Clone {
1356 type Error: std::error::Error + Send + Sync + 'static;
1358
1359 fn token_id(&self) -> Result<u32, Self::Error>;
1361}
1362
1363impl TokenOutput for u32 {
1364 type Error = std::convert::Infallible;
1365
1366 fn token_id(&self) -> Result<u32, Self::Error> {
1367 Ok(*self)
1368 }
1369}
1370
1371#[derive(Debug, Clone, Eq, PartialEq)]
1373pub enum TokenFilter {
1374 All,
1376 Allowed(Vec<bool>),
1378}
1379
1380impl TokenFilter {
1381 pub fn allowed(mask: Vec<bool>) -> Result<Self, TokenFilterError> {
1383 if mask.is_empty() {
1384 return Err(TokenFilterError::EmptyVocabulary);
1385 }
1386 if !mask.iter().any(|allowed| *allowed) {
1387 return Err(TokenFilterError::NoAllowedToken);
1388 }
1389 Ok(Self::Allowed(mask))
1390 }
1391
1392 pub fn allowed_mask(&self) -> Option<&[bool]> {
1394 match self {
1395 Self::All => None,
1396 Self::Allowed(mask) => Some(mask),
1397 }
1398 }
1399}
1400
1401#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1403pub enum TokenFilterError {
1404 #[error("token filter vocabulary must not be empty")]
1406 EmptyVocabulary,
1407 #[error("token filter does not allow any vocabulary token")]
1409 NoAllowedToken,
1410}
1411
1412pub trait TokenFilterController {
1414 type Error: std::error::Error + Send + Sync + 'static;
1416
1417 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error>;
1419
1420 fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error>;
1422
1423 fn is_complete(&mut self) -> Result<bool, Self::Error>;
1425}
1426
1427pub trait SpeculativeTokenFilterController: TokenFilterController + Clone {
1433 fn filter_at(&self, history: &[u32]) -> Result<TokenFilter, Self::Error>;
1439
1440 fn prefix_is_complete(&self, history: &[u32]) -> Result<bool, Self::Error>;
1442}
1443
1444#[derive(Debug, Clone, Copy)]
1445struct UnconstrainedTokens;
1446
1447impl TokenFilterController for UnconstrainedTokens {
1448 type Error = std::convert::Infallible;
1449
1450 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
1451 Ok(TokenFilter::All)
1452 }
1453
1454 fn commit_token(&mut self, _: u32) -> Result<(), Self::Error> {
1455 Ok(())
1456 }
1457
1458 fn is_complete(&mut self) -> Result<bool, Self::Error> {
1459 Ok(false)
1460 }
1461}
1462
1463pub trait TextGenerationBackend: BackendProvider {
1469 type Prompt;
1471 type Token: TokenOutput<Error = Self::Error>;
1473 type TextGenerationState;
1475 type TextCompletion: Completion<Error = Self::Error>;
1477
1478 fn start_text_generation(
1480 backend: &Self,
1481 config: TextGenerationConfig,
1482 ) -> Result<Self::TextGenerationState, Self::Error>;
1483
1484 fn prepare_text_prompt(
1486 backend: &Self,
1487 prompt_token_ids: Vec<u32>,
1488 ) -> Result<Self::Prompt, Self::Error>;
1489
1490 fn submit_text_prefill(
1492 runtime: &mut ModelRuntime<Self>,
1493 prompt: Self::Prompt,
1494 filter: &TokenFilter,
1495 state: &mut Self::TextGenerationState,
1496 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error>;
1497
1498 fn submit_text_decode(
1500 runtime: &mut ModelRuntime<Self>,
1501 token: Self::Token,
1502 filter: &TokenFilter,
1503 state: &mut Self::TextGenerationState,
1504 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error>;
1505}
1506
1507#[derive(Debug, thiserror::Error)]
1509pub enum MultimodalPreparationFailure<B, T>
1510where
1511 B: std::error::Error + 'static,
1512 T: std::error::Error + 'static,
1513{
1514 #[error("backend multimodal preparation failed: {0}")]
1516 Backend(#[source] B),
1517 #[error("multimodal framing text encoding failed: {0}")]
1519 Text(#[source] T),
1520}
1521
1522pub trait MultimodalPreparationBackend: TextGenerationBackend {
1529 fn prepare_multimodal_input<E>(
1531 runtime: &ModelRuntime<Self>,
1532 request: &TokenizedMultimodalRequest,
1533 encode_backend_text: &mut dyn FnMut(&str) -> Result<Vec<u32>, E>,
1534 ) -> Result<Self::Prompt, MultimodalPreparationFailure<Self::Error, E>>
1535 where
1536 E: std::error::Error + Send + Sync + 'static;
1537}
1538
1539pub trait ModelCapabilityBackend: TextGenerationBackend {
1546 fn model_capabilities(
1548 runtime: &ModelRuntime<Self>,
1549 ) -> Result<ModelCapabilities, CapabilityError>;
1550
1551 fn count_prepared_input(
1553 runtime: &ModelRuntime<Self>,
1554 input: &Self::Prompt,
1555 ) -> Result<InputTokenCount, CapabilityError>;
1556
1557 fn estimate_runtime_state(
1559 runtime: &ModelRuntime<Self>,
1560 input: InputTokenCount,
1561 max_output_tokens: u64,
1562 batch_size: u64,
1563 ) -> Result<RuntimeStateEstimate, CapabilityError>;
1564
1565 fn static_memory(runtime: &ModelRuntime<Self>) -> Result<StaticMemoryReport, CapabilityError>;
1567}
1568
1569enum TextGenerationStep<P, T> {
1570 Prefill(P),
1571 Decode(T),
1572}
1573
1574#[derive(Debug, thiserror::Error)]
1576pub enum ControlledTextGenerationError<B, C>
1577where
1578 B: std::error::Error + 'static,
1579 C: std::error::Error + 'static,
1580{
1581 #[error("backend text generation failed: {0}")]
1583 Backend(#[source] B),
1584 #[error("text generation constraint failed: {0}")]
1586 Controller(#[source] C),
1587}
1588
1589#[derive(Debug, Clone)]
1591pub struct ControlledToken<T> {
1592 output: T,
1593 token_id: u32,
1594}
1595
1596impl<T> ControlledToken<T> {
1597 pub fn token_id(&self) -> u32 {
1599 self.token_id
1600 }
1601
1602 pub const fn output(&self) -> &T {
1604 &self.output
1605 }
1606
1607 pub fn into_output(self) -> T {
1609 self.output
1610 }
1611}
1612
1613pub struct ControlledTextGeneration<'a, B, C>
1615where
1616 B: TextGenerationBackend,
1617 C: TokenFilterController,
1618{
1619 inner: TextGenerationMachine<'a, B, C>,
1620}
1621
1622struct TextGenerationMachine<'a, B, C>
1623where
1624 B: TextGenerationBackend,
1625 C: TokenFilterController,
1626{
1627 runtime: &'a mut ModelRuntime<B>,
1628 backend_state: B::TextGenerationState,
1629 controller: C,
1630 step: Option<TextGenerationStep<B::Prompt, B::Token>>,
1631 completions: Vec<B::TextCompletion>,
1632 remaining_tokens: Option<usize>,
1633}
1634
1635type ControlledGenerationResult<B, C> = Result<
1636 <B as TextGenerationBackend>::Token,
1637 ControlledTextGenerationError<
1638 <B as BackendProvider>::Error,
1639 <C as TokenFilterController>::Error,
1640 >,
1641>;
1642
1643impl<'a, B, C> ControlledTextGeneration<'a, B, C>
1644where
1645 B: TextGenerationBackend,
1646 C: TokenFilterController,
1647{
1648 pub fn new(
1650 runtime: &'a mut ModelRuntime<B>,
1651 prompt_token_ids: Vec<u32>,
1652 config: TextGenerationConfig,
1653 controller: C,
1654 ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1655 let prompt = B::prepare_text_prompt(runtime.backend(), prompt_token_ids)
1656 .map_err(ControlledTextGenerationError::Backend)?;
1657 Self::from_prompt(runtime, prompt, config, controller)
1658 }
1659
1660 pub fn from_prompt(
1662 runtime: &'a mut ModelRuntime<B>,
1663 prompt: B::Prompt,
1664 config: TextGenerationConfig,
1665 controller: C,
1666 ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1667 TextGenerationMachine::new(runtime, prompt, config, controller).map(|inner| Self { inner })
1668 }
1669
1670 pub fn controller_mut(&mut self) -> &mut C {
1672 &mut self.inner.controller
1673 }
1674}
1675
1676impl<'a, B, C> TextGenerationMachine<'a, B, C>
1677where
1678 B: TextGenerationBackend,
1679 C: TokenFilterController,
1680{
1681 fn new(
1682 runtime: &'a mut ModelRuntime<B>,
1683 prompt: B::Prompt,
1684 config: TextGenerationConfig,
1685 controller: C,
1686 ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1687 let backend_state = B::start_text_generation(runtime.backend(), config)
1688 .map_err(ControlledTextGenerationError::Backend)?;
1689 Ok(Self {
1690 runtime,
1691 backend_state,
1692 controller,
1693 step: Some(TextGenerationStep::Prefill(prompt)),
1694 completions: Vec::new(),
1695 remaining_tokens: config.sampling().max_new_tokens,
1696 })
1697 }
1698
1699 fn retain_completion(&mut self, completion: B::TextCompletion) -> Result<(), B::Error> {
1700 let existing = std::mem::take(&mut self.completions);
1701 let mut retained = Vec::with_capacity(existing.len() + 1);
1702 for pending in existing {
1703 match pending.is_complete() {
1704 Ok(true) => {}
1705 Ok(false) => retained.push(pending),
1706 Err(error) => {
1707 let _ = pending.wait();
1708 for retained_completion in retained.drain(..) {
1709 let _ = retained_completion.wait();
1710 }
1711 let _ = completion.wait();
1712 return Err(error);
1713 }
1714 }
1715 }
1716 retained.push(completion);
1717 self.completions = retained;
1718 Ok(())
1719 }
1720
1721 fn resolve_completions_before_decode(&mut self) -> Result<(), B::Error> {
1722 let existing = std::mem::take(&mut self.completions);
1723 let mut remaining = existing.into_iter();
1724 while let Some(completion) = remaining.next() {
1725 let result = match completion.is_complete() {
1726 Ok(true) => Ok(()),
1727 Ok(false) => completion.wait(),
1728 Err(error) => {
1729 let _ = completion.wait();
1730 Err(error)
1731 }
1732 };
1733 if let Err(error) = result {
1734 for pending in remaining {
1735 let _ = pending.wait();
1736 }
1737 return Err(error);
1738 }
1739 }
1740 Ok(())
1741 }
1742
1743 fn next_output(&mut self) -> Option<ControlledGenerationResult<B, C>> {
1744 if self.remaining_tokens == Some(0) {
1745 self.step = None;
1746 return None;
1747 }
1748 let step = self.step.take()?;
1749 if matches!(step, TextGenerationStep::Decode(_)) {
1750 if let Err(error) = self.resolve_completions_before_decode() {
1751 return Some(Err(ControlledTextGenerationError::Backend(error)));
1752 }
1753 }
1754 let filter = match self.controller.current_filter() {
1755 Ok(filter) => filter,
1756 Err(error) => return Some(Err(ControlledTextGenerationError::Controller(error))),
1757 };
1758 let submission = match step {
1759 TextGenerationStep::Prefill(prompt) => {
1760 B::submit_text_prefill(self.runtime, prompt, &filter, &mut self.backend_state)
1761 }
1762 TextGenerationStep::Decode(token) => {
1763 B::submit_text_decode(self.runtime, token, &filter, &mut self.backend_state)
1764 }
1765 };
1766 let submission = match submission {
1767 Ok(submission) => submission,
1768 Err(error) => return Some(Err(ControlledTextGenerationError::Backend(error))),
1769 };
1770 let token = submission.output;
1771 if let Err(error) = self.retain_completion(submission.completion) {
1772 return Some(Err(ControlledTextGenerationError::Backend(error)));
1773 }
1774 self.step = Some(TextGenerationStep::Decode(token.clone()));
1775 if let Some(remaining_tokens) = &mut self.remaining_tokens {
1776 *remaining_tokens -= 1;
1777 }
1778 Some(Ok(token))
1779 }
1780}
1781
1782impl<B, C> Iterator for ControlledTextGeneration<'_, B, C>
1783where
1784 B: TextGenerationBackend,
1785 C: TokenFilterController,
1786{
1787 type Item =
1788 Result<ControlledToken<B::Token>, ControlledTextGenerationError<B::Error, C::Error>>;
1789
1790 fn next(&mut self) -> Option<Self::Item> {
1791 let token = match self.inner.next_output()? {
1792 Ok(token) => token,
1793 Err(error) => return Some(Err(error)),
1794 };
1795 let token_id = match token.token_id() {
1796 Ok(token_id) => token_id,
1797 Err(error) => {
1798 self.inner.step = None;
1799 return Some(Err(ControlledTextGenerationError::Backend(error)));
1800 }
1801 };
1802 if let Err(error) = self.inner.controller.commit_token(token_id) {
1803 self.inner.step = None;
1804 return Some(Err(ControlledTextGenerationError::Controller(error)));
1805 }
1806 Some(Ok(ControlledToken {
1807 output: token,
1808 token_id,
1809 }))
1810 }
1811}
1812
1813impl<B, C> Drop for TextGenerationMachine<'_, B, C>
1814where
1815 B: TextGenerationBackend,
1816 C: TokenFilterController,
1817{
1818 fn drop(&mut self) {
1819 for completion in self.completions.drain(..) {
1820 let _ = completion.wait();
1821 }
1822 }
1823}
1824
1825pub struct TextGeneration<'a, B: TextGenerationBackend> {
1832 inner: TextGenerationMachine<'a, B, UnconstrainedTokens>,
1833}
1834
1835impl<'a, B: TextGenerationBackend> TextGeneration<'a, B> {
1836 pub fn new(
1838 runtime: &'a mut ModelRuntime<B>,
1839 prompt_token_ids: Vec<u32>,
1840 config: TextGenerationConfig,
1841 ) -> Result<Self, B::Error> {
1842 let prompt = B::prepare_text_prompt(runtime.backend(), prompt_token_ids)?;
1843 Self::from_prompt(runtime, prompt, config)
1844 }
1845
1846 pub fn from_prompt(
1848 runtime: &'a mut ModelRuntime<B>,
1849 prompt: B::Prompt,
1850 config: TextGenerationConfig,
1851 ) -> Result<Self, B::Error> {
1852 TextGenerationMachine::new(runtime, prompt, config, UnconstrainedTokens)
1853 .map(|inner| Self { inner })
1854 .map_err(unreachable_unconstrained_error)
1855 }
1856}
1857
1858fn unreachable_unconstrained_error<B>(
1859 error: ControlledTextGenerationError<B, std::convert::Infallible>,
1860) -> B
1861where
1862 B: std::error::Error + 'static,
1863{
1864 match error {
1865 ControlledTextGenerationError::Backend(error) => error,
1866 ControlledTextGenerationError::Controller(error) => match error {},
1867 }
1868}
1869
1870impl<B: TextGenerationBackend> Iterator for TextGeneration<'_, B> {
1871 type Item = Result<B::Token, B::Error>;
1872
1873 fn next(&mut self) -> Option<Self::Item> {
1874 self.inner
1875 .next_output()
1876 .map(|result| result.map_err(unreachable_unconstrained_error))
1877 }
1878}
1879
1880pub trait DistributedSession {
1887 type Value;
1889 type Completion: Completion<Error = Self::Error>;
1891 type Error: std::error::Error + Send + Sync + 'static;
1893
1894 fn descriptor(&self) -> DistributedSessionDescriptor;
1896 fn capabilities(&self) -> DistributedCapabilities;
1898
1899 fn all_reduce_sum(
1901 &self,
1902 scope: CollectiveScope,
1903 input: &Self::Value,
1904 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1905
1906 fn all_gather(
1908 &self,
1909 scope: CollectiveScope,
1910 input: &Self::Value,
1911 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1912
1913 fn all_to_all_v(
1915 &self,
1916 scope: CollectiveScope,
1917 input: &Self::Value,
1918 send_counts: &[usize],
1919 receive_counts: &[usize],
1920 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1921
1922 fn send(
1924 &self,
1925 scope: CollectiveScope,
1926 peer: usize,
1927 input: &Self::Value,
1928 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1929
1930 fn receive(
1932 &self,
1933 scope: CollectiveScope,
1934 peer: usize,
1935 value: &ValueDescriptor,
1936 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1937
1938 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error>;
1940}
1941
1942pub trait DistributedBackend: BackendProvider {
1944 type DistributedSession: DistributedSession<Error = Self::Error>;
1946
1947 fn distributed_session(session: &Self::Session) -> Option<&Self::DistributedSession>;
1949}
1950
1951#[cfg(test)]
1952mod tests {
1953 use super::*;
1954 use std::{convert::Infallible, io::Write};
1955
1956 #[test]
1957 fn text_generation_config_validates_portable_mirostat_strategy() {
1958 let sampling = crate::generation::resolve_generation_config(
1959 None,
1960 crate::generation::GenerationConfigOverrides {
1961 temperature: Some(0.8),
1962 ..crate::generation::GenerationConfigOverrides::default()
1963 },
1964 )
1965 .unwrap();
1966 let config = TextGenerationConfig::new(sampling)
1967 .with_seed(7)
1968 .with_mirostat_v2(5.0, 0.1)
1969 .unwrap();
1970 assert_eq!(config.seed(), 7);
1971 assert_eq!(
1972 config.strategy(),
1973 TextSamplingStrategy::MirostatV2 { tau: 5.0, eta: 0.1 }
1974 );
1975 assert!(matches!(
1976 TextGenerationConfig::new(sampling).with_mirostat_v2(0.0, 0.1),
1977 Err(GenerationError::InvalidMirostatTau(0.0))
1978 ));
1979 assert!(matches!(
1980 TextGenerationConfig::new(sampling).with_mirostat_v2(5.0, f32::NAN),
1981 Err(GenerationError::InvalidMirostatEta(value)) if value.is_nan()
1982 ));
1983 }
1984
1985 #[derive(Debug, Clone)]
1986 struct Done;
1987 impl Completion for Done {
1988 type Error = Infallible;
1989 fn is_complete(&self) -> Result<bool, Self::Error> {
1990 Ok(true)
1991 }
1992 fn wait(&self) -> Result<(), Self::Error> {
1993 Ok(())
1994 }
1995 }
1996 struct Mock;
1997 impl BackendProvider for Mock {
1998 type ModelConfig = u32;
1999 type Model = u32;
2000 type Session = MockSession;
2001 type Error = Infallible;
2002 fn descriptor(&self) -> BackendDescriptor {
2003 BackendDescriptor::new("mock", "1")
2004 }
2005 fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error> {
2006 Ok(vec![])
2007 }
2008 fn prepare_model(&self, config: u32) -> Result<PreparedModel<u32>, Self::Error> {
2009 Ok(PreparedModel::new(config, SessionCapabilities::default()))
2010 }
2011 fn create_session(&self, model: PreparedModel<u32>) -> Result<MockSession, Self::Error> {
2012 Ok(MockSession {
2013 model: model.into_inner(),
2014 tokens: vec![],
2015 distributed: None,
2016 })
2017 }
2018 }
2019
2020 #[derive(Default)]
2021 struct LoadingMock {
2022 selections: std::sync::atomic::AtomicUsize,
2023 materializations: std::sync::atomic::AtomicUsize,
2024 }
2025 struct LoadingMockSession;
2026
2027 struct LoadingConfigurationResolver;
2028
2029 impl ModelConfigurationResolver for LoadingConfigurationResolver {
2030 type ArtifactPlan = ();
2031
2032 fn resolve_safetensors(
2033 &self,
2034 json: &serde_json::Value,
2035 ) -> Result<crate::ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
2036 Ok(crate::ResolvedModelConfiguration::new(
2037 crate::ModelConfiguration::new(
2038 "llama",
2039 "llama",
2040 "llama",
2041 crate::LoadingProtocol::Model,
2042 Some(json.clone()),
2043 )?,
2044 (),
2045 ))
2046 }
2047
2048 fn resolve_gguf(
2049 &self,
2050 architecture: &str,
2051 _checkpoint: &eredu_gguf::Checkpoint,
2052 ) -> Result<crate::ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
2053 if architecture != "llama" {
2054 return Err(ArtifactError::UnsupportedGgufArchitecture(
2055 architecture.into(),
2056 ));
2057 }
2058 Ok(crate::ResolvedModelConfiguration::new(
2059 crate::ModelConfiguration::new(
2060 architecture,
2061 architecture,
2062 "llama",
2063 crate::LoadingProtocol::Model,
2064 None,
2065 )?,
2066 (),
2067 ))
2068 }
2069
2070 fn gguf_companion_requirements(
2071 &self,
2072 _architecture: &str,
2073 _checkpoint: &eredu_gguf::Checkpoint,
2074 ) -> Result<Vec<crate::GgufCompanionRequirement>, ArtifactError> {
2075 Ok(Vec::new())
2076 }
2077 }
2078
2079 static LOADING_CONFIGURATION_RESOLVER: LoadingConfigurationResolver =
2080 LoadingConfigurationResolver;
2081
2082 impl BackendProvider for LoadingMock {
2083 type ModelConfig = (ModelPreparationPlan, u32);
2084 type Model = u32;
2085 type Session = LoadingMockSession;
2086 type Error = std::convert::Infallible;
2087
2088 fn descriptor(&self) -> BackendDescriptor {
2089 BackendDescriptor::new("loading-mock", "1")
2090 }
2091
2092 fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error> {
2093 Ok(Vec::new())
2094 }
2095
2096 fn prepare_model(
2097 &self,
2098 (plan, model): Self::ModelConfig,
2099 ) -> Result<PreparedModel<Self::Model>, Self::Error> {
2100 self.materializations
2101 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2102 assert_eq!(plan.inspection().configuration().family(), "llama");
2103 Ok(PreparedModel::new(
2104 model,
2105 plan.admitted_session_capabilities(),
2106 ))
2107 }
2108
2109 fn create_session(
2110 &self,
2111 _: PreparedModel<Self::Model>,
2112 ) -> Result<Self::Session, Self::Error> {
2113 Ok(LoadingMockSession)
2114 }
2115 }
2116
2117 impl BackendSession<LoadingMock> for LoadingMockSession {
2118 type PrefillInput = ();
2119 type DecodeInput = ();
2120 type Output = ();
2121 type Completion = LoadingDone;
2122
2123 fn capabilities(&self) -> SessionCapabilities {
2124 SessionCapabilities::default()
2125 }
2126
2127 fn prefill(
2128 &mut self,
2129 _: &LoadingMock,
2130 _: (),
2131 ) -> Result<Submission<(), LoadingDone>, std::convert::Infallible> {
2132 Ok(Submission {
2133 output: (),
2134 completion: LoadingDone,
2135 })
2136 }
2137
2138 fn decode(
2139 &mut self,
2140 _: &LoadingMock,
2141 _: (),
2142 ) -> Result<Submission<(), LoadingDone>, std::convert::Infallible> {
2143 Ok(Submission {
2144 output: (),
2145 completion: LoadingDone,
2146 })
2147 }
2148
2149 fn observe_output(
2150 &self,
2151 _: &LoadingMock,
2152 _: &(),
2153 ) -> Result<ObservationSet, std::convert::Infallible> {
2154 Ok(ObservationSet::new())
2155 }
2156 }
2157
2158 #[derive(Debug, Clone, Copy)]
2159 struct LoadingDone;
2160
2161 impl Completion for LoadingDone {
2162 type Error = std::convert::Infallible;
2163
2164 fn is_complete(&self) -> Result<bool, Self::Error> {
2165 Ok(true)
2166 }
2167
2168 fn wait(&self) -> Result<(), Self::Error> {
2169 Ok(())
2170 }
2171 }
2172
2173 impl ModelLoadingBackend for LoadingMock {
2174 type LoadOptions = u32;
2175 type SelectedPreparation = (u32, crate::PreparationAdmission);
2176 type ConfigurationResolver = LoadingConfigurationResolver;
2177
2178 fn configuration_resolver(&self) -> &Self::ConfigurationResolver {
2179 &LOADING_CONFIGURATION_RESOLVER
2180 }
2181
2182 fn select_preparation(
2183 &self,
2184 _: &ArtifactInspection,
2185 options: &Self::LoadOptions,
2186 ) -> Result<Self::SelectedPreparation, Self::Error> {
2187 self.selections
2188 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2189 let policy = crate::PreparationPolicy::default().with_required_session_capabilities(
2190 SessionCapabilities::default().with_activation_inspection(*options == 99),
2191 );
2192 let request = crate::PreparationAdmissionRequest::new(
2193 crate::LoadingProtocol::Model,
2194 crate::ArtifactFormat::SafeTensors,
2195 policy,
2196 crate::ArchitecturePreparationCapabilities::new(
2197 false,
2198 true,
2199 false,
2200 false,
2201 false,
2202 crate::InputModalities::TEXT,
2203 ),
2204 );
2205 let admission = crate::admit_preparation(
2206 request,
2207 crate::PreparationMechanismCapabilities::new(true, true)
2208 .with_residency(crate::ResidencyRequest::FullyResident, true)
2209 .with_input_modalities(crate::InputModalities::TEXT)
2210 .with_session(
2211 SessionCapabilities::default().with_activation_inspection(*options == 99),
2212 ),
2213 )
2214 .expect("mock admission facts are coherent");
2215 Ok((*options, admission))
2216 }
2217
2218 fn selected_preparation_admission(
2219 &self,
2220 selected: &Self::SelectedPreparation,
2221 ) -> crate::PreparationAdmission {
2222 selected.1
2223 }
2224
2225 fn model_config(
2226 &self,
2227 selected: SelectedModelPreparation<Self>,
2228 ) -> Result<Self::ModelConfig, Self::Error> {
2229 let (plan, (selected, _admission)) = selected.into_parts();
2230 Ok((plan, selected))
2231 }
2232 }
2233
2234 fn write_loading_fixture(root: &Path) {
2235 std::fs::write(root.join("config.json"), r#"{"model_type":"llama"}"#).unwrap();
2236 let header = br#"{"token_embd.weight":{"dtype":"F32","shape":[1],"data_offsets":[0,4]}}"#;
2237 let mut file = std::fs::File::create(root.join("model.safetensors")).unwrap();
2238 file.write_all(&(header.len() as u64).to_le_bytes())
2239 .unwrap();
2240 file.write_all(header).unwrap();
2241 file.write_all(&[0; 4]).unwrap();
2242 }
2243 struct MockSession {
2244 model: u32,
2245 tokens: Vec<u32>,
2246 distributed: Option<MockDistributed>,
2247 }
2248 impl BackendSession<Mock> for MockSession {
2249 type PrefillInput = Vec<u32>;
2250 type DecodeInput = u32;
2251 type Output = u32;
2252 type Completion = Done;
2253 fn capabilities(&self) -> SessionCapabilities {
2254 SessionCapabilities::default()
2255 }
2256 fn prefill(
2257 &mut self,
2258 _: &Mock,
2259 input: Vec<u32>,
2260 ) -> Result<Submission<u32, Done>, Infallible> {
2261 self.tokens.extend(input);
2262 Ok(Submission {
2263 output: self.tokens.len() as u32 + self.model,
2264 completion: Done,
2265 })
2266 }
2267 fn decode(&mut self, _: &Mock, input: u32) -> Result<Submission<u32, Done>, Infallible> {
2268 self.tokens.push(input);
2269 Ok(Submission {
2270 output: self.tokens.len() as u32 + self.model,
2271 completion: Done,
2272 })
2273 }
2274
2275 fn observe_output(&self, _: &Mock, output: &u32) -> Result<ObservationSet, Infallible> {
2276 let mut observations = ObservationSet::new();
2277 observations
2278 .insert(
2279 "mock.output",
2280 crate::ObservationValue::Unsigned(u64::from(*output)),
2281 )
2282 .unwrap();
2283 Ok(observations)
2284 }
2285 }
2286
2287 impl TextGenerationBackend for Mock {
2288 type Prompt = Vec<u32>;
2289 type Token = u32;
2290 type TextGenerationState = (u32, u64);
2291 type TextCompletion = Done;
2292
2293 fn start_text_generation(
2294 _: &Self,
2295 config: TextGenerationConfig,
2296 ) -> Result<Self::TextGenerationState, Self::Error> {
2297 Ok((config.sampling().top_k as u32, config.seed()))
2298 }
2299
2300 fn prepare_text_prompt(
2301 _: &Self,
2302 prompt_token_ids: Vec<u32>,
2303 ) -> Result<Self::Prompt, Self::Error> {
2304 Ok(prompt_token_ids)
2305 }
2306
2307 fn submit_text_prefill(
2308 runtime: &mut ModelRuntime<Self>,
2309 prompt: Self::Prompt,
2310 filter: &TokenFilter,
2311 state: &mut Self::TextGenerationState,
2312 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error> {
2313 let submission = runtime.prefill(prompt)?;
2314 Ok(Submission {
2315 output: apply_mock_filter(submission.output + state.0 + state.1 as u32, filter),
2316 completion: submission.completion,
2317 })
2318 }
2319
2320 fn submit_text_decode(
2321 runtime: &mut ModelRuntime<Self>,
2322 token: Self::Token,
2323 filter: &TokenFilter,
2324 _: &mut Self::TextGenerationState,
2325 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error> {
2326 let submission = runtime.decode(token)?;
2327 Ok(Submission {
2328 output: apply_mock_filter(submission.output, filter),
2329 completion: submission.completion,
2330 })
2331 }
2332 }
2333
2334 impl MultimodalPreparationBackend for Mock {
2335 fn prepare_multimodal_input<E>(
2336 _: &ModelRuntime<Self>,
2337 request: &TokenizedMultimodalRequest,
2338 _: &mut dyn FnMut(&str) -> Result<Vec<u32>, E>,
2339 ) -> Result<Self::Prompt, MultimodalPreparationFailure<Self::Error, E>>
2340 where
2341 E: std::error::Error + Send + Sync + 'static,
2342 {
2343 let mut prompt = Vec::new();
2344 for segment in request.segments() {
2345 match segment {
2346 crate::TokenizedMultimodalSegment::TokenIds(ids) => {
2347 prompt.extend_from_slice(ids);
2348 }
2349 crate::TokenizedMultimodalSegment::Media(crate::Media::Image(_)) => {
2350 prompt.push(1_001);
2351 }
2352 crate::TokenizedMultimodalSegment::Media(crate::Media::Video(_)) => {
2353 prompt.push(1_002);
2354 }
2355 crate::TokenizedMultimodalSegment::Media(crate::Media::Audio(_)) => {
2356 prompt.push(1_003);
2357 }
2358 }
2359 }
2360 Ok(prompt)
2361 }
2362 }
2363
2364 impl ModelCapabilityBackend for Mock {
2365 fn model_capabilities(
2366 _: &ModelRuntime<Self>,
2367 ) -> Result<ModelCapabilities, CapabilityError> {
2368 Ok(ModelCapabilities {
2369 effective_model_type: "mock".into(),
2370 native_max_context: crate::Observed::exact(64, "mock configuration"),
2371 effective_max_context: crate::Observed::exact(64, "mock configuration"),
2372 state_strategy: crate::CacheStateStrategy::FullKv,
2373 modalities: crate::InputModalities::TEXT,
2374 estimation: crate::EstimationCompleteness::Complete,
2375 })
2376 }
2377
2378 fn count_prepared_input(
2379 _: &ModelRuntime<Self>,
2380 input: &Self::Prompt,
2381 ) -> Result<InputTokenCount, CapabilityError> {
2382 Ok(InputTokenCount::text(input.len() as u64))
2383 }
2384
2385 fn estimate_runtime_state(
2386 _: &ModelRuntime<Self>,
2387 input: InputTokenCount,
2388 max_output_tokens: u64,
2389 batch_size: u64,
2390 ) -> Result<RuntimeStateEstimate, CapabilityError> {
2391 crate::estimate_runtime_state(
2392 &crate::StateMemoryLayout::new(
2393 crate::LayerSchedule::new(
2394 1,
2395 vec![crate::cache::LayerCachePolicy::key_only(
2396 crate::AttentionPolicy::Full,
2397 1,
2398 2,
2399 )
2400 .unwrap()],
2401 )
2402 .unwrap(),
2403 vec![0],
2404 1,
2405 1,
2406 crate::EstimationCompleteness::Complete,
2407 )
2408 .unwrap(),
2409 input,
2410 max_output_tokens,
2411 batch_size,
2412 std::num::NonZeroU8::new(4).unwrap(),
2413 )
2414 }
2415
2416 fn static_memory(
2417 runtime: &ModelRuntime<Self>,
2418 ) -> Result<StaticMemoryReport, CapabilityError> {
2419 let unavailable = || crate::Observed::unavailable("mock does not expose this counter");
2420 Ok(StaticMemoryReport {
2421 logical_parameter_bytes: crate::Observed::exact(
2422 u64::from(runtime.session().model),
2423 "mock model",
2424 ),
2425 current_host_resident_bytes: unavailable(),
2426 current_device_resident_bytes: unavailable(),
2427 planned_disk_backed_bytes: unavailable(),
2428 backend_active_allocation_bytes: unavailable(),
2429 backend_allocator_cache_bytes: unavailable(),
2430 physical_semantics: crate::PhysicalMemorySemantics::Unknown,
2431 currently_cached_shards: unavailable(),
2432 })
2433 }
2434 }
2435
2436 fn apply_mock_filter(candidate: u32, filter: &TokenFilter) -> u32 {
2437 let Some(allowed) = filter.allowed_mask() else {
2438 return candidate;
2439 };
2440 allowed
2441 .get(candidate as usize)
2442 .copied()
2443 .unwrap_or(false)
2444 .then_some(candidate)
2445 .or_else(|| {
2446 allowed
2447 .iter()
2448 .position(|allowed| *allowed)
2449 .map(|token| token as u32)
2450 })
2451 .expect("validated token filters allow at least one token")
2452 }
2453
2454 #[test]
2455 fn generic_loader_inspects_plans_and_prepares_on_the_selected_backend() {
2456 let root = tempfile::tempdir().unwrap();
2457 write_loading_fixture(root.path());
2458 let prepared = load_model(&LoadingMock::default(), root.path(), 41).unwrap();
2459 assert_eq!(*prepared, 41);
2460
2461 let runtime = ModelRuntime::load(LoadingMock::default(), root.path(), 7).unwrap();
2462 assert_eq!(runtime.backend().descriptor().name, "loading-mock");
2463
2464 let missing = root.path().join("missing");
2465 assert!(matches!(
2466 load_model(&LoadingMock::default(), &missing, 1),
2467 Err(ModelLoadError::Artifact(ArtifactError::MissingArtifact(path)))
2468 if path == missing
2469 ));
2470 }
2471
2472 #[test]
2473 fn session_requirement_is_retained_by_the_single_admission() {
2474 let root = tempfile::tempdir().unwrap();
2475 write_loading_fixture(root.path());
2476 let backend = LoadingMock::default();
2477
2478 let prepared = load_model(&backend, root.path(), 99).unwrap();
2479
2480 assert_eq!(*prepared, 99);
2481 assert_eq!(
2482 backend
2483 .selections
2484 .load(std::sync::atomic::Ordering::Relaxed),
2485 1
2486 );
2487 assert_eq!(
2488 backend
2489 .materializations
2490 .load(std::sync::atomic::Ordering::Relaxed),
2491 1
2492 );
2493 }
2494
2495 struct FixedController {
2496 tokens: Vec<u32>,
2497 committed: usize,
2498 }
2499
2500 impl TokenFilterController for FixedController {
2501 type Error = Infallible;
2502
2503 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
2504 let mut allowed = vec![false; 64];
2505 allowed[self.tokens[self.committed] as usize] = true;
2506 Ok(TokenFilter::allowed(allowed).unwrap())
2507 }
2508
2509 fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error> {
2510 assert_eq!(token_id, self.tokens[self.committed]);
2511 self.committed += 1;
2512 Ok(())
2513 }
2514
2515 fn is_complete(&mut self) -> Result<bool, Self::Error> {
2516 Ok(self.committed == self.tokens.len())
2517 }
2518 }
2519
2520 #[test]
2521 fn mock_prefill_and_multiple_decode_steps() {
2522 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2523 let prefill = runtime.prefill(vec![1, 2]).unwrap();
2524 assert_eq!(prefill.output, 12);
2525 assert!(prefill.completion.is_complete().unwrap());
2526 assert_eq!(runtime.decode(3).unwrap().output, 13);
2527 assert_eq!(runtime.decode(4).unwrap().output, 14);
2528 }
2529
2530 #[test]
2531 fn portable_text_generation_prefills_and_decodes_without_tensor_types() {
2532 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2533 let sampling = crate::resolve_generation_config(
2534 None,
2535 crate::GenerationConfigOverrides {
2536 max_new_tokens: Some(3),
2537 ..Default::default()
2538 },
2539 )
2540 .unwrap();
2541 let mut generation = TextGeneration::new(
2542 &mut runtime,
2543 vec![1, 2],
2544 TextGenerationConfig::new(sampling).with_seed(3),
2545 )
2546 .unwrap();
2547 assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 55);
2548 assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 13);
2549 assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 14);
2550 assert!(generation.next().is_none());
2551 }
2552
2553 #[test]
2554 fn portable_media_preparation_feeds_the_existing_generation_contract() {
2555 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2556 let request = crate::MultimodalRequest::new(vec![
2557 crate::MultimodalSegment::TokenIds(vec![7, 8]),
2558 crate::MultimodalSegment::Media(crate::Media::Image(
2559 crate::RgbImage::new(vec![5, 6, 7], 1, 1).unwrap(),
2560 )),
2561 crate::MultimodalSegment::TokenIds(vec![9]),
2562 ])
2563 .unwrap()
2564 .tokenize::<Infallible>(|_| unreachable!("request is already tokenized"))
2565 .unwrap();
2566 let prompt = Mock::prepare_multimodal_input(&runtime, &request, &mut |_| {
2567 Ok::<_, Infallible>(Vec::new())
2568 })
2569 .unwrap();
2570 assert_eq!(prompt, vec![7, 8, 1_001, 9]);
2571
2572 let sampling = crate::resolve_generation_config(
2573 None,
2574 crate::GenerationConfigOverrides {
2575 max_new_tokens: Some(2),
2576 ..Default::default()
2577 },
2578 )
2579 .unwrap();
2580 let mut generation =
2581 TextGeneration::from_prompt(&mut runtime, prompt, TextGenerationConfig::new(sampling))
2582 .unwrap();
2583 assert!(generation.next().unwrap().is_ok());
2584 assert!(generation.next().unwrap().is_ok());
2585 assert!(generation.next().is_none());
2586 }
2587
2588 #[test]
2589 fn model_capability_extension_observes_the_selected_mock_session() {
2590 let runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2591 let capabilities = Mock::model_capabilities(&runtime).unwrap();
2592 assert_eq!(capabilities.effective_model_type, "mock");
2593 let input = Mock::count_prepared_input(&runtime, &vec![1, 2, 3]).unwrap();
2594 assert_eq!(input.model_positions, 3);
2595 let state = Mock::estimate_runtime_state(&runtime, input, 2, 1).unwrap();
2596 assert_eq!(state.requested_state_bytes, 5 * 2 * 4);
2597 assert_eq!(
2598 Mock::static_memory(&runtime)
2599 .unwrap()
2600 .logical_parameter_bytes
2601 .value(),
2602 Some(&10)
2603 );
2604 }
2605
2606 #[test]
2607 fn controlled_generation_applies_portable_filters_and_commits_tokens() {
2608 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2609 let sampling = crate::resolve_generation_config(
2610 None,
2611 crate::GenerationConfigOverrides {
2612 max_new_tokens: Some(2),
2613 ..Default::default()
2614 },
2615 )
2616 .unwrap();
2617 let controller = FixedController {
2618 tokens: vec![7, 8],
2619 committed: 0,
2620 };
2621 let mut generation = ControlledTextGeneration::new(
2622 &mut runtime,
2623 vec![1, 2],
2624 TextGenerationConfig::new(sampling),
2625 controller,
2626 )
2627 .unwrap();
2628 assert_eq!(generation.next().unwrap().unwrap().token_id(), 7);
2629 assert_eq!(generation.next().unwrap().unwrap().token_id(), 8);
2630 assert!(generation.controller_mut().is_complete().unwrap());
2631 assert!(generation.next().is_none());
2632 }
2633
2634 #[derive(Debug, Clone)]
2635 struct MockDistributed {
2636 descriptor: DistributedSessionDescriptor,
2637 }
2638
2639 impl DistributedSession for MockDistributed {
2640 type Value = Vec<u32>;
2641 type Completion = Done;
2642 type Error = Infallible;
2643
2644 fn descriptor(&self) -> DistributedSessionDescriptor {
2645 self.descriptor.clone()
2646 }
2647
2648 fn capabilities(&self) -> DistributedCapabilities {
2649 DistributedCapabilities::new(true, [CollectiveGroupId::new(7)], true, true, true)
2650 }
2651
2652 fn all_reduce_sum(
2653 &self,
2654 _: CollectiveScope,
2655 input: &Vec<u32>,
2656 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2657 Ok(Submission {
2658 output: input.iter().map(|value| value * 2).collect(),
2659 completion: Done,
2660 })
2661 }
2662
2663 fn all_gather(
2664 &self,
2665 _: CollectiveScope,
2666 input: &Vec<u32>,
2667 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2668 let mut output = input.clone();
2669 output.extend(input);
2670 Ok(Submission {
2671 output,
2672 completion: Done,
2673 })
2674 }
2675
2676 fn all_to_all_v(
2677 &self,
2678 _: CollectiveScope,
2679 input: &Vec<u32>,
2680 _: &[usize],
2681 _: &[usize],
2682 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2683 Ok(Submission {
2684 output: input.clone(),
2685 completion: Done,
2686 })
2687 }
2688
2689 fn send(
2690 &self,
2691 _: CollectiveScope,
2692 _: usize,
2693 input: &Vec<u32>,
2694 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2695 Ok(Submission {
2696 output: input.clone(),
2697 completion: Done,
2698 })
2699 }
2700
2701 fn receive(
2702 &self,
2703 _: CollectiveScope,
2704 peer: usize,
2705 value: &ValueDescriptor,
2706 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2707 Ok(Submission {
2708 output: vec![peer as u32; value.shape().iter().product()],
2709 completion: Done,
2710 })
2711 }
2712
2713 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Infallible> {
2714 let mut output = local.to_vec();
2715 output.extend_from_slice(local);
2716 Ok(output)
2717 }
2718 }
2719
2720 impl DistributedBackend for Mock {
2721 type DistributedSession = MockDistributed;
2722
2723 fn distributed_session(session: &MockSession) -> Option<&Self::DistributedSession> {
2724 session.distributed.as_ref()
2725 }
2726 }
2727
2728 #[test]
2729 fn mock_distributed_session_owns_collective_and_transfer_lifecycle() {
2730 let tensor_group =
2731 CollectiveGroupDescriptor::new(CollectiveGroupId::new(7), vec![0, 1], 0).unwrap();
2732 let session = MockDistributed {
2733 descriptor: DistributedSessionDescriptor::new(2, 0, vec![tensor_group]).unwrap(),
2734 };
2735 let capabilities = session.capabilities();
2736 assert!(capabilities.exact_completion());
2737 assert_eq!(
2738 capabilities.collective_groups(),
2739 &[CollectiveGroupId::new(7)]
2740 );
2741 assert_eq!(
2742 session
2743 .all_reduce_sum(
2744 CollectiveScope::Group(CollectiveGroupId::new(7)),
2745 &vec![2, 3]
2746 )
2747 .unwrap()
2748 .wait()
2749 .unwrap(),
2750 vec![4, 6]
2751 );
2752 assert_eq!(
2753 session
2754 .receive(
2755 CollectiveScope::World,
2756 1,
2757 &ValueDescriptor::new(vec![2], TensorDtype::U32).unwrap(),
2758 )
2759 .unwrap()
2760 .wait()
2761 .unwrap(),
2762 vec![1, 1]
2763 );
2764 assert_eq!(session.all_gather_words(&[7]).unwrap(), vec![7, 7]);
2765
2766 let model_session = MockSession {
2767 model: 0,
2768 tokens: Vec::new(),
2769 distributed: Some(session.clone()),
2770 };
2771 assert_eq!(
2772 Mock::distributed_session(&model_session)
2773 .unwrap()
2774 .descriptor(),
2775 session.descriptor()
2776 );
2777 }
2778
2779 #[test]
2780 fn distributed_descriptors_round_trip_and_reject_invalid_ranks() {
2781 let descriptor = DistributedSessionDescriptor::new(
2782 6,
2783 4,
2784 vec![CollectiveGroupDescriptor::new(CollectiveGroupId::new(9), vec![1, 4], 1).unwrap()],
2785 )
2786 .unwrap();
2787 let encoded = serde_json::to_string(&descriptor).unwrap();
2788 assert_eq!(
2789 serde_json::from_str::<DistributedSessionDescriptor>(&encoded).unwrap(),
2790 descriptor
2791 );
2792 let scope = CollectiveScope::Group(CollectiveGroupId::new(9));
2793 assert_eq!(
2794 serde_json::from_str::<CollectiveScope>(&serde_json::to_string(&scope).unwrap())
2795 .unwrap(),
2796 scope
2797 );
2798 assert!(DistributedSessionDescriptor::new(descriptor.world_size(), 6, Vec::new()).is_err());
2799 assert!(serde_json::from_str::<DistributedSessionDescriptor>(
2800 r#"{"world_size":6,"rank":6,"groups":[]}"#
2801 )
2802 .is_err());
2803 }
2804
2805 #[test]
2806 fn distributed_commit_epoch_round_trips_and_rejects_zero() {
2807 let outcome = DistributedCommitOutcome::Indeterminate {
2808 epoch: DistributedCommitEpoch::new(17).unwrap(),
2809 phase: DistributedCommitPhase::DecisionCompletion,
2810 };
2811 let encoded = serde_json::to_string(&outcome).unwrap();
2812 assert_eq!(
2813 serde_json::from_str::<DistributedCommitOutcome>(&encoded).unwrap(),
2814 outcome
2815 );
2816 assert!(serde_json::from_str::<DistributedCommitEpoch>("0").is_err());
2817 }
2818}