1use serde::{Deserialize, Serialize};
4use std::{fmt::Debug, path::Path};
5
6use crate::{
7 artifact::{
8 inspect_artifact, plan_model_preparation, ArtifactError, ArtifactInspection,
9 ModelConfigurationResolver, ModelPreparationPlan, PreparationPolicy,
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};
20
21#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
23pub struct BackendDescriptor {
24 name: String,
26 version: String,
28}
29
30impl BackendDescriptor {
31 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
33 Self {
34 name: name.into(),
35 version: version.into(),
36 }
37 }
38
39 pub fn name(&self) -> &str {
41 &self.name
42 }
43
44 pub fn version(&self) -> &str {
46 &self.version
47 }
48}
49
50#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
52pub struct DeviceDescriptor {
53 id: String,
55 name: String,
57 family: String,
59 memory_bytes: Option<u64>,
61}
62
63impl DeviceDescriptor {
64 pub fn new(
66 id: impl Into<String>,
67 name: impl Into<String>,
68 family: impl Into<String>,
69 memory_bytes: Option<u64>,
70 ) -> Self {
71 Self {
72 id: id.into(),
73 name: name.into(),
74 family: family.into(),
75 memory_bytes,
76 }
77 }
78
79 pub fn id(&self) -> &str {
81 &self.id
82 }
83 pub fn name(&self) -> &str {
85 &self.name
86 }
87 pub fn family(&self) -> &str {
89 &self.family
90 }
91 pub const fn memory_bytes(&self) -> Option<u64> {
93 self.memory_bytes
94 }
95}
96
97#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
99pub struct DeviceCapabilities {
100 exact_completion: bool,
102 transfers: bool,
104 collectives: bool,
106}
107
108impl DeviceCapabilities {
109 pub const fn new(exact_completion: bool, transfers: bool, collectives: bool) -> Self {
111 Self {
112 exact_completion,
113 transfers,
114 collectives,
115 }
116 }
117
118 pub const fn exact_completion(&self) -> bool {
120 self.exact_completion
121 }
122 pub const fn transfers(&self) -> bool {
124 self.transfers
125 }
126 pub const fn collectives(&self) -> bool {
128 self.collectives
129 }
130}
131
132#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
134pub struct SessionCapabilities {
135 persistent_cache: bool,
137 output_observation: bool,
139 activation_inspection: bool,
141}
142
143impl SessionCapabilities {
144 pub const fn new(
146 persistent_cache: bool,
147 output_observation: bool,
148 activation_inspection: bool,
149 ) -> Self {
150 Self {
151 persistent_cache,
152 output_observation,
153 activation_inspection,
154 }
155 }
156
157 pub const fn persistent_cache(self) -> bool {
159 self.persistent_cache
160 }
161 pub const fn output_observation(self) -> bool {
163 self.output_observation
164 }
165 pub const fn activation_inspection(self) -> bool {
167 self.activation_inspection
168 }
169
170 pub const fn with_persistent_cache(mut self, supported: bool) -> Self {
172 self.persistent_cache = supported;
173 self
174 }
175 pub const fn with_output_observation(mut self, supported: bool) -> Self {
177 self.output_observation = supported;
178 self
179 }
180 pub const fn with_activation_inspection(mut self, supported: bool) -> Self {
182 self.activation_inspection = supported;
183 self
184 }
185 pub fn validate(&self, available: &Self) -> Result<(), SessionCapabilityError> {
187 for (required, supported, capability) in [
188 (
189 self.persistent_cache,
190 available.persistent_cache,
191 "persistent_cache",
192 ),
193 (
194 self.output_observation,
195 available.output_observation,
196 "output_observation",
197 ),
198 (
199 self.activation_inspection,
200 available.activation_inspection,
201 "activation_inspection",
202 ),
203 ] {
204 if required && !supported {
205 return Err(SessionCapabilityError { capability });
206 }
207 }
208 Ok(())
209 }
210}
211
212#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
214#[error("prepared session does not support required capability {capability}")]
215pub struct SessionCapabilityError {
216 capability: &'static str,
217}
218
219impl SessionCapabilityError {
220 pub const fn capability(self) -> &'static str {
222 self.capability
223 }
224}
225
226#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
228pub struct DistributedCapabilities {
229 world_collectives: bool,
230 collective_groups: Vec<CollectiveGroupId>,
231 point_to_point: bool,
232 variable_all_to_all: bool,
233 exact_completion: bool,
234}
235
236impl DistributedCapabilities {
237 pub fn new(
239 world_collectives: bool,
240 collective_groups: impl IntoIterator<Item = CollectiveGroupId>,
241 point_to_point: bool,
242 variable_all_to_all: bool,
243 exact_completion: bool,
244 ) -> Self {
245 Self {
246 world_collectives,
247 collective_groups: collective_groups.into_iter().collect(),
248 point_to_point,
249 variable_all_to_all,
250 exact_completion,
251 }
252 }
253
254 pub const fn world_collectives(&self) -> bool {
256 self.world_collectives
257 }
258 pub fn collective_groups(&self) -> &[CollectiveGroupId] {
260 &self.collective_groups
261 }
262 pub const fn point_to_point(&self) -> bool {
264 self.point_to_point
265 }
266 pub const fn variable_all_to_all(&self) -> bool {
268 self.variable_all_to_all
269 }
270 pub const fn exact_completion(&self) -> bool {
272 self.exact_completion
273 }
274}
275
276#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
278#[serde(transparent)]
279pub struct CollectiveGroupId(u32);
280
281impl CollectiveGroupId {
282 pub const fn new(value: u32) -> Self {
284 Self(value)
285 }
286 pub const fn value(self) -> u32 {
288 self.0
289 }
290}
291
292#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
294pub struct CollectiveGroupDescriptor {
295 id: CollectiveGroupId,
296 members: Vec<usize>,
297 local_rank: usize,
298}
299
300impl CollectiveGroupDescriptor {
301 pub fn new(
303 id: CollectiveGroupId,
304 members: Vec<usize>,
305 local_rank: usize,
306 ) -> Result<Self, BackendError> {
307 if members.is_empty() || local_rank >= members.len() {
308 return Err(BackendError::Preparation {
309 operation: "collective group realization".into(),
310 message: "collective membership must be non-empty and contain local rank".into(),
311 });
312 }
313 let mut unique = std::collections::BTreeSet::new();
314 if !members.iter().all(|rank| unique.insert(*rank)) {
315 return Err(BackendError::Preparation {
316 operation: "collective group realization".into(),
317 message: "collective membership contains duplicate world ranks".into(),
318 });
319 }
320 Ok(Self {
321 id,
322 members,
323 local_rank,
324 })
325 }
326
327 pub const fn id(&self) -> CollectiveGroupId {
329 self.id
330 }
331 pub fn members(&self) -> &[usize] {
333 &self.members
334 }
335 pub const fn local_rank(&self) -> usize {
337 self.local_rank
338 }
339}
340
341impl<'de> Deserialize<'de> for CollectiveGroupDescriptor {
342 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
343 where
344 D: serde::Deserializer<'de>,
345 {
346 #[derive(Deserialize)]
347 struct Raw {
348 id: CollectiveGroupId,
349 members: Vec<usize>,
350 local_rank: usize,
351 }
352 let raw = Raw::deserialize(deserializer)?;
353 Self::new(raw.id, raw.members, raw.local_rank).map_err(serde::de::Error::custom)
354 }
355}
356
357#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
359#[serde(tag = "kind", content = "group", rename_all = "snake_case")]
360#[non_exhaustive]
361pub enum CollectiveScope {
362 World,
364 Group(CollectiveGroupId),
366}
367
368#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
370pub struct ValueDescriptor {
371 shape: Vec<usize>,
373 dtype: TensorDtype,
375}
376
377impl ValueDescriptor {
378 pub fn new(shape: Vec<usize>, dtype: TensorDtype) -> Result<Self, BackendError> {
380 if shape.contains(&0) {
381 return Err(BackendError::Preparation {
382 operation: "distributed value descriptor".into(),
383 message: "non-scalar distributed values require positive dimensions".into(),
384 });
385 }
386 Ok(Self { shape, dtype })
387 }
388
389 pub fn shape(&self) -> &[usize] {
391 &self.shape
392 }
393
394 pub const fn dtype(&self) -> &TensorDtype {
396 &self.dtype
397 }
398}
399
400impl<'de> Deserialize<'de> for ValueDescriptor {
401 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
402 where
403 D: serde::Deserializer<'de>,
404 {
405 #[derive(Deserialize)]
406 struct RawDescriptor {
407 shape: Vec<usize>,
408 dtype: TensorDtype,
409 }
410
411 let raw = RawDescriptor::deserialize(deserializer)?;
412 Self::new(raw.shape, raw.dtype).map_err(serde::de::Error::custom)
413 }
414}
415
416#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
418pub struct DistributedSessionDescriptor {
419 world_size: usize,
420 rank: usize,
421 groups: Vec<CollectiveGroupDescriptor>,
422}
423
424impl DistributedSessionDescriptor {
425 pub fn new(
427 world_size: usize,
428 rank: usize,
429 groups: Vec<CollectiveGroupDescriptor>,
430 ) -> Result<Self, BackendError> {
431 if world_size == 0 || rank >= world_size {
432 return Err(BackendError::Preparation {
433 operation: "distributed session realization".into(),
434 message: format!("rank {rank} is outside world size {world_size}"),
435 });
436 }
437 let mut ids = std::collections::BTreeSet::new();
438 for group in &groups {
439 if !ids.insert(group.id())
440 || group.members().iter().any(|member| *member >= world_size)
441 || group.members()[group.local_rank()] != rank
442 {
443 return Err(BackendError::Preparation {
444 operation: "distributed session realization".into(),
445 message: "collective groups must have unique IDs, in-range members, and the declared local world rank".into(),
446 });
447 }
448 }
449 Ok(Self {
450 world_size,
451 rank,
452 groups,
453 })
454 }
455
456 pub const fn world_size(&self) -> usize {
458 self.world_size
459 }
460 pub const fn rank(&self) -> usize {
462 self.rank
463 }
464 pub fn groups(&self) -> &[CollectiveGroupDescriptor] {
466 &self.groups
467 }
468}
469
470impl<'de> Deserialize<'de> for DistributedSessionDescriptor {
471 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
472 where
473 D: serde::Deserializer<'de>,
474 {
475 #[derive(Deserialize)]
476 struct RawDescriptor {
477 world_size: usize,
478 rank: usize,
479 groups: Vec<CollectiveGroupDescriptor>,
480 }
481
482 let raw = RawDescriptor::deserialize(deserializer)?;
483 Self::new(raw.world_size, raw.rank, raw.groups).map_err(serde::de::Error::custom)
484 }
485}
486
487#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
489#[non_exhaustive]
490pub enum BackendError {
491 #[error("backend {backend} does not support required capability {capability}")]
493 Unsupported {
494 backend: String,
496 capability: String,
498 },
499 #[error("backend model preparation failed during {operation}: {message}")]
501 Preparation {
502 operation: String,
504 message: String,
506 },
507 #[error("backend session {session} failed during {operation}: {message}")]
509 Execution {
510 session: String,
512 operation: String,
514 message: String,
516 },
517 #[error("backend completion observation failed: {message}")]
519 Completion {
520 message: String,
522 },
523}
524
525pub trait Completion {
527 type Error: std::error::Error + Send + Sync + 'static;
529
530 fn is_complete(&self) -> Result<bool, Self::Error>;
532
533 fn wait(&self) -> Result<(), Self::Error>;
535}
536
537#[derive(Debug)]
539pub struct Submission<T, C> {
540 pub output: T,
542 pub completion: C,
544}
545
546impl<T, C> Submission<T, C>
547where
548 C: Completion,
549{
550 pub fn wait(self) -> Result<T, C::Error> {
552 self.completion.wait()?;
553 Ok(self.output)
554 }
555}
556
557#[derive(Debug)]
559pub struct PreparedModel<M> {
560 model: M,
561 capabilities: SessionCapabilities,
562}
563
564impl<M> PreparedModel<M> {
565 pub const fn new(model: M, capabilities: SessionCapabilities) -> Self {
567 Self {
568 model,
569 capabilities,
570 }
571 }
572 pub const fn get(&self) -> &M {
574 &self.model
575 }
576 pub fn get_mut(&mut self) -> &mut M {
578 &mut self.model
579 }
580 pub const fn capabilities(&self) -> SessionCapabilities {
582 self.capabilities
583 }
584 pub fn into_inner(self) -> M {
586 self.model
587 }
588 pub fn into_parts(self) -> (M, SessionCapabilities) {
590 (self.model, self.capabilities)
591 }
592}
593
594impl<M> std::ops::Deref for PreparedModel<M> {
595 type Target = M;
596
597 fn deref(&self) -> &Self::Target {
598 self.get()
599 }
600}
601
602impl<M> std::ops::DerefMut for PreparedModel<M> {
603 fn deref_mut(&mut self) -> &mut Self::Target {
604 self.get_mut()
605 }
606}
607
608pub trait BackendProvider: Sized {
610 type ModelConfig;
612 type Model;
614 type Session: BackendSession<Self>;
616 type Error: std::error::Error + Send + Sync + 'static;
618
619 fn descriptor(&self) -> BackendDescriptor;
621 fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error>;
623 fn prepare_model(
625 &self,
626 config: Self::ModelConfig,
627 ) -> Result<PreparedModel<Self::Model>, Self::Error>;
628 fn create_session(
634 &self,
635 model: PreparedModel<Self::Model>,
636 ) -> Result<Self::Session, Self::Error>;
637
638 fn session_capability_mismatch(
643 &self,
644 admitted: SessionCapabilities,
645 realized: SessionCapabilities,
646 ) -> Self::Error {
647 panic!("backend realized session capabilities {realized:?} after admitting {admitted:?}")
648 }
649}
650
651pub trait ModelLoadingBackend: BackendProvider {
658 type LoadOptions;
660
661 type SelectedPreparation;
664
665 type ConfigurationResolver: ModelConfigurationResolver;
667
668 fn configuration_resolver(&self) -> &Self::ConfigurationResolver;
670
671 fn preparation_policy(
673 &self,
674 options: &Self::LoadOptions,
675 ) -> Result<PreparationPolicy, Self::Error>;
676
677 fn select_preparation(
685 &self,
686 inspection: &ArtifactInspection<
687 <Self::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
688 >,
689 options: &Self::LoadOptions,
690 policy: PreparationPolicy,
691 ) -> Result<Self::SelectedPreparation, Self::Error>;
692
693 fn session_capabilities(
695 &self,
696 inspection: &ArtifactInspection<
697 <Self::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
698 >,
699 policy: PreparationPolicy,
700 ) -> Result<SessionCapabilities, Self::Error>;
701
702 fn model_config(
705 &self,
706 plan: ModelPreparationPlan<
707 <Self::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
708 >,
709 selected: Self::SelectedPreparation,
710 ) -> Result<Self::ModelConfig, Self::Error>;
711}
712
713#[derive(Debug, thiserror::Error)]
715#[non_exhaustive]
716pub enum ModelLoadError<E: std::error::Error + Send + Sync + 'static> {
717 #[error(transparent)]
719 Artifact(#[from] ArtifactError),
720 #[error("selected backend failed to prepare the model: {0}")]
722 Backend(#[source] E),
723 #[error(transparent)]
725 SessionCapability(#[from] SessionCapabilityError),
726}
727
728pub fn load_model<B: ModelLoadingBackend>(
734 backend: &B,
735 artifact: impl AsRef<Path>,
736 options: B::LoadOptions,
737) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
738 let inspection = inspect_artifact(artifact, backend.configuration_resolver())?;
739 prepare_inspected_model(backend, inspection, options)
740}
741
742pub fn prepare_inspected_model<B: ModelLoadingBackend>(
748 backend: &B,
749 inspection: ArtifactInspection<
750 <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
751 >,
752 options: B::LoadOptions,
753) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
754 let policy = backend
755 .preparation_policy(&options)
756 .map_err(ModelLoadError::Backend)?;
757 let selected = backend
758 .select_preparation(&inspection, &options, policy)
759 .map_err(ModelLoadError::Backend)?;
760 let capabilities = backend
761 .session_capabilities(&inspection, policy)
762 .map_err(ModelLoadError::Backend)?;
763 policy
764 .validate_session_capabilities(&capabilities)
765 .map_err(ModelLoadError::SessionCapability)?;
766 let plan = plan_model_preparation(inspection, policy, capabilities)?;
767 let config = backend
768 .model_config(plan, selected)
769 .map_err(ModelLoadError::Backend)?;
770 backend
771 .prepare_model(config)
772 .map_err(ModelLoadError::Backend)
773}
774
775pub trait BackendSession<B: BackendProvider> {
781 type PrefillInput;
783 type DecodeInput;
785 type Output;
787 type Completion: Completion<Error = B::Error>;
789
790 fn capabilities(&self) -> SessionCapabilities;
792
793 fn prefill(
795 &mut self,
796 backend: &B,
797 input: Self::PrefillInput,
798 ) -> Result<Submission<Self::Output, Self::Completion>, B::Error>;
799
800 fn decode(
802 &mut self,
803 backend: &B,
804 input: Self::DecodeInput,
805 ) -> Result<Submission<Self::Output, Self::Completion>, B::Error>;
806
807 fn observe_output(
812 &self,
813 backend: &B,
814 output: &Self::Output,
815 ) -> Result<ObservationSet, B::Error>;
816}
817
818pub trait InspectableBackendSession<B: BackendProvider>: BackendSession<B> {
825 fn inspect_prefill(
827 &mut self,
828 backend: &B,
829 input: Self::PrefillInput,
830 request: &ObservationRequest,
831 ) -> Result<InspectedOutput<Self::Output>, B::Error>;
832
833 fn inspect_decode(
835 &mut self,
836 backend: &B,
837 input: Self::DecodeInput,
838 request: &ObservationRequest,
839 ) -> Result<InspectedOutput<Self::Output>, B::Error>;
840}
841
842pub type SessionSubmission<B> = Submission<
844 <<B as BackendProvider>::Session as BackendSession<B>>::Output,
845 <<B as BackendProvider>::Session as BackendSession<B>>::Completion,
846>;
847
848pub struct ModelRuntime<B: BackendProvider> {
856 backend: B,
857 session: B::Session,
858}
859
860impl<B: BackendProvider> ModelRuntime<B> {
861 pub fn prepare(backend: B, config: B::ModelConfig) -> Result<Self, B::Error> {
863 let model = backend.prepare_model(config)?;
864 Self::from_prepared(backend, model)
865 }
866
867 pub fn from_prepared(backend: B, model: PreparedModel<B::Model>) -> Result<Self, B::Error> {
869 let admitted = model.capabilities();
870 let session = backend.create_session(model)?;
871 let realized = session.capabilities();
872 if admitted != realized {
873 return Err(backend.session_capability_mismatch(admitted, realized));
874 }
875 Ok(Self { backend, session })
876 }
877
878 pub const fn backend(&self) -> &B {
880 &self.backend
881 }
882
883 pub const fn session(&self) -> &B::Session {
885 &self.session
886 }
887
888 pub fn session_mut(&mut self) -> &mut B::Session {
890 &mut self.session
891 }
892
893 pub fn parts_mut(&mut self) -> (&B, &mut B::Session) {
895 (&self.backend, &mut self.session)
896 }
897
898 pub fn capabilities(&self) -> SessionCapabilities {
900 self.session.capabilities()
901 }
902
903 pub fn prefill(
905 &mut self,
906 input: <B::Session as BackendSession<B>>::PrefillInput,
907 ) -> Result<SessionSubmission<B>, B::Error> {
908 self.session.prefill(&self.backend, input)
909 }
910
911 pub fn decode(
913 &mut self,
914 input: <B::Session as BackendSession<B>>::DecodeInput,
915 ) -> Result<SessionSubmission<B>, B::Error> {
916 self.session.decode(&self.backend, input)
917 }
918
919 pub fn observe_output(
921 &self,
922 output: &<B::Session as BackendSession<B>>::Output,
923 ) -> Result<ObservationSet, B::Error> {
924 self.session.observe_output(&self.backend, output)
925 }
926}
927
928impl<B> ModelRuntime<B>
929where
930 B: BackendProvider,
931 B::Session: InspectableBackendSession<B>,
932{
933 pub fn inspect_prefill(
935 &mut self,
936 input: <B::Session as BackendSession<B>>::PrefillInput,
937 request: &ObservationRequest,
938 ) -> Result<InspectedOutput<<B::Session as BackendSession<B>>::Output>, B::Error> {
939 self.session.inspect_prefill(&self.backend, input, request)
940 }
941
942 pub fn inspect_decode(
944 &mut self,
945 input: <B::Session as BackendSession<B>>::DecodeInput,
946 request: &ObservationRequest,
947 ) -> Result<InspectedOutput<<B::Session as BackendSession<B>>::Output>, B::Error> {
948 self.session.inspect_decode(&self.backend, input, request)
949 }
950}
951
952impl<B: ModelLoadingBackend> ModelRuntime<B> {
953 pub fn load(
955 backend: B,
956 artifact: impl AsRef<Path>,
957 options: B::LoadOptions,
958 ) -> Result<Self, ModelLoadError<B::Error>> {
959 let model = load_model(&backend, artifact, options)?;
960 Self::from_prepared(backend, model).map_err(ModelLoadError::Backend)
961 }
962}
963
964#[derive(Debug, Clone, Copy, PartialEq)]
966pub struct TextGenerationConfig {
967 sampling: ResolvedGenerationConfig,
968 seed: u64,
969 strategy: TextSamplingStrategy,
970}
971
972#[derive(Debug, Clone, Copy, Default, PartialEq)]
974pub enum TextSamplingStrategy {
975 #[default]
977 Standard,
978 MirostatV2 {
980 tau: f32,
982 eta: f32,
984 },
985}
986
987impl TextGenerationConfig {
988 pub const fn new(sampling: ResolvedGenerationConfig) -> Self {
990 Self {
991 sampling,
992 seed: 0,
993 strategy: TextSamplingStrategy::Standard,
994 }
995 }
996
997 pub const fn with_seed(mut self, seed: u64) -> Self {
999 self.seed = seed;
1000 self
1001 }
1002
1003 pub fn with_mirostat_v2(mut self, tau: f32, eta: f32) -> Result<Self, GenerationError> {
1005 if !tau.is_finite() || tau <= 0.0 {
1006 return Err(GenerationError::InvalidMirostatTau(tau));
1007 }
1008 if !eta.is_finite() || eta <= 0.0 {
1009 return Err(GenerationError::InvalidMirostatEta(eta));
1010 }
1011 self.strategy = TextSamplingStrategy::MirostatV2 { tau, eta };
1012 Ok(self)
1013 }
1014
1015 pub const fn sampling(&self) -> ResolvedGenerationConfig {
1017 self.sampling
1018 }
1019
1020 pub const fn seed(&self) -> u64 {
1022 self.seed
1023 }
1024
1025 pub const fn strategy(&self) -> TextSamplingStrategy {
1027 self.strategy
1028 }
1029}
1030
1031pub trait TokenOutput: Clone {
1033 type Error: std::error::Error + Send + Sync + 'static;
1035
1036 fn token_id(&self) -> Result<u32, Self::Error>;
1038}
1039
1040impl TokenOutput for u32 {
1041 type Error = std::convert::Infallible;
1042
1043 fn token_id(&self) -> Result<u32, Self::Error> {
1044 Ok(*self)
1045 }
1046}
1047
1048#[derive(Debug, Clone, Eq, PartialEq)]
1050pub enum TokenFilter {
1051 All,
1053 Allowed(Vec<bool>),
1055}
1056
1057impl TokenFilter {
1058 pub fn allowed(mask: Vec<bool>) -> Result<Self, TokenFilterError> {
1060 if mask.is_empty() {
1061 return Err(TokenFilterError::EmptyVocabulary);
1062 }
1063 if !mask.iter().any(|allowed| *allowed) {
1064 return Err(TokenFilterError::NoAllowedToken);
1065 }
1066 Ok(Self::Allowed(mask))
1067 }
1068
1069 pub fn allowed_mask(&self) -> Option<&[bool]> {
1071 match self {
1072 Self::All => None,
1073 Self::Allowed(mask) => Some(mask),
1074 }
1075 }
1076}
1077
1078#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1080pub enum TokenFilterError {
1081 #[error("token filter vocabulary must not be empty")]
1083 EmptyVocabulary,
1084 #[error("token filter does not allow any vocabulary token")]
1086 NoAllowedToken,
1087}
1088
1089pub trait TokenFilterController {
1091 type Error: std::error::Error + Send + Sync + 'static;
1093
1094 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error>;
1096
1097 fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error>;
1099
1100 fn is_complete(&mut self) -> Result<bool, Self::Error>;
1102}
1103
1104pub trait SpeculativeTokenFilterController: TokenFilterController + Clone {
1110 fn filter_at(&self, history: &[u32]) -> Result<TokenFilter, Self::Error>;
1116
1117 fn prefix_is_complete(&self, history: &[u32]) -> Result<bool, Self::Error>;
1119}
1120
1121#[derive(Debug, Clone, Copy)]
1122struct UnconstrainedTokens;
1123
1124impl TokenFilterController for UnconstrainedTokens {
1125 type Error = std::convert::Infallible;
1126
1127 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
1128 Ok(TokenFilter::All)
1129 }
1130
1131 fn commit_token(&mut self, _: u32) -> Result<(), Self::Error> {
1132 Ok(())
1133 }
1134
1135 fn is_complete(&mut self) -> Result<bool, Self::Error> {
1136 Ok(false)
1137 }
1138}
1139
1140pub trait TextGenerationBackend: BackendProvider {
1146 type Prompt;
1148 type Token: TokenOutput<Error = Self::Error>;
1150 type TextGenerationState;
1152 type TextCompletion: Completion<Error = Self::Error>;
1154
1155 fn start_text_generation(
1157 backend: &Self,
1158 config: TextGenerationConfig,
1159 ) -> Result<Self::TextGenerationState, Self::Error>;
1160
1161 fn prepare_text_prompt(
1163 backend: &Self,
1164 prompt_token_ids: Vec<u32>,
1165 ) -> Result<Self::Prompt, Self::Error>;
1166
1167 fn submit_text_prefill(
1169 runtime: &mut ModelRuntime<Self>,
1170 prompt: Self::Prompt,
1171 filter: &TokenFilter,
1172 state: &mut Self::TextGenerationState,
1173 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error>;
1174
1175 fn submit_text_decode(
1177 runtime: &mut ModelRuntime<Self>,
1178 token: Self::Token,
1179 filter: &TokenFilter,
1180 state: &mut Self::TextGenerationState,
1181 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error>;
1182}
1183
1184#[derive(Debug, thiserror::Error)]
1186pub enum MultimodalPreparationFailure<B, T>
1187where
1188 B: std::error::Error + 'static,
1189 T: std::error::Error + 'static,
1190{
1191 #[error("backend multimodal preparation failed: {0}")]
1193 Backend(#[source] B),
1194 #[error("multimodal framing text encoding failed: {0}")]
1196 Text(#[source] T),
1197}
1198
1199pub trait MultimodalPreparationBackend: TextGenerationBackend {
1206 fn prepare_multimodal_input<E>(
1208 runtime: &ModelRuntime<Self>,
1209 request: &TokenizedMultimodalRequest,
1210 encode_backend_text: &mut dyn FnMut(&str) -> Result<Vec<u32>, E>,
1211 ) -> Result<Self::Prompt, MultimodalPreparationFailure<Self::Error, E>>
1212 where
1213 E: std::error::Error + Send + Sync + 'static;
1214}
1215
1216pub trait ModelCapabilityBackend: TextGenerationBackend {
1223 fn model_capabilities(
1225 runtime: &ModelRuntime<Self>,
1226 ) -> Result<ModelCapabilities, CapabilityError>;
1227
1228 fn count_prepared_input(
1230 runtime: &ModelRuntime<Self>,
1231 input: &Self::Prompt,
1232 ) -> Result<InputTokenCount, CapabilityError>;
1233
1234 fn estimate_runtime_state(
1236 runtime: &ModelRuntime<Self>,
1237 input: InputTokenCount,
1238 max_output_tokens: u64,
1239 batch_size: u64,
1240 ) -> Result<RuntimeStateEstimate, CapabilityError>;
1241
1242 fn static_memory(runtime: &ModelRuntime<Self>) -> Result<StaticMemoryReport, CapabilityError>;
1244}
1245
1246enum TextGenerationStep<P, T> {
1247 Prefill(P),
1248 Decode(T),
1249}
1250
1251#[derive(Debug, thiserror::Error)]
1253pub enum ControlledTextGenerationError<B, C>
1254where
1255 B: std::error::Error + 'static,
1256 C: std::error::Error + 'static,
1257{
1258 #[error("backend text generation failed: {0}")]
1260 Backend(#[source] B),
1261 #[error("text generation constraint failed: {0}")]
1263 Controller(#[source] C),
1264}
1265
1266#[derive(Debug, Clone)]
1268pub struct ControlledToken<T> {
1269 output: T,
1270 token_id: u32,
1271}
1272
1273impl<T> ControlledToken<T> {
1274 pub fn token_id(&self) -> u32 {
1276 self.token_id
1277 }
1278
1279 pub const fn output(&self) -> &T {
1281 &self.output
1282 }
1283
1284 pub fn into_output(self) -> T {
1286 self.output
1287 }
1288}
1289
1290pub struct ControlledTextGeneration<'a, B, C>
1292where
1293 B: TextGenerationBackend,
1294 C: TokenFilterController,
1295{
1296 inner: TextGenerationMachine<'a, B, C>,
1297}
1298
1299struct TextGenerationMachine<'a, B, C>
1300where
1301 B: TextGenerationBackend,
1302 C: TokenFilterController,
1303{
1304 runtime: &'a mut ModelRuntime<B>,
1305 backend_state: B::TextGenerationState,
1306 controller: C,
1307 step: Option<TextGenerationStep<B::Prompt, B::Token>>,
1308 completions: Vec<B::TextCompletion>,
1309 remaining_tokens: Option<usize>,
1310}
1311
1312type ControlledGenerationResult<B, C> = Result<
1313 <B as TextGenerationBackend>::Token,
1314 ControlledTextGenerationError<
1315 <B as BackendProvider>::Error,
1316 <C as TokenFilterController>::Error,
1317 >,
1318>;
1319
1320impl<'a, B, C> ControlledTextGeneration<'a, B, C>
1321where
1322 B: TextGenerationBackend,
1323 C: TokenFilterController,
1324{
1325 pub fn new(
1327 runtime: &'a mut ModelRuntime<B>,
1328 prompt_token_ids: Vec<u32>,
1329 config: TextGenerationConfig,
1330 controller: C,
1331 ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1332 let prompt = B::prepare_text_prompt(runtime.backend(), prompt_token_ids)
1333 .map_err(ControlledTextGenerationError::Backend)?;
1334 Self::from_prompt(runtime, prompt, config, controller)
1335 }
1336
1337 pub fn from_prompt(
1339 runtime: &'a mut ModelRuntime<B>,
1340 prompt: B::Prompt,
1341 config: TextGenerationConfig,
1342 controller: C,
1343 ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1344 TextGenerationMachine::new(runtime, prompt, config, controller).map(|inner| Self { inner })
1345 }
1346
1347 pub fn controller_mut(&mut self) -> &mut C {
1349 &mut self.inner.controller
1350 }
1351}
1352
1353impl<'a, B, C> TextGenerationMachine<'a, B, C>
1354where
1355 B: TextGenerationBackend,
1356 C: TokenFilterController,
1357{
1358 fn new(
1359 runtime: &'a mut ModelRuntime<B>,
1360 prompt: B::Prompt,
1361 config: TextGenerationConfig,
1362 controller: C,
1363 ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1364 let backend_state = B::start_text_generation(runtime.backend(), config)
1365 .map_err(ControlledTextGenerationError::Backend)?;
1366 Ok(Self {
1367 runtime,
1368 backend_state,
1369 controller,
1370 step: Some(TextGenerationStep::Prefill(prompt)),
1371 completions: Vec::new(),
1372 remaining_tokens: config.sampling().max_new_tokens,
1373 })
1374 }
1375
1376 fn retain_completion(&mut self, completion: B::TextCompletion) -> Result<(), B::Error> {
1377 let existing = std::mem::take(&mut self.completions);
1378 let mut retained = Vec::with_capacity(existing.len() + 1);
1379 for pending in existing {
1380 match pending.is_complete() {
1381 Ok(true) => {}
1382 Ok(false) => retained.push(pending),
1383 Err(error) => {
1384 let _ = pending.wait();
1385 for retained_completion in retained.drain(..) {
1386 let _ = retained_completion.wait();
1387 }
1388 let _ = completion.wait();
1389 return Err(error);
1390 }
1391 }
1392 }
1393 retained.push(completion);
1394 self.completions = retained;
1395 Ok(())
1396 }
1397
1398 fn next_output(&mut self) -> Option<ControlledGenerationResult<B, C>> {
1399 if self.remaining_tokens == Some(0) {
1400 self.step = None;
1401 return None;
1402 }
1403 let step = self.step.take()?;
1404 let filter = match self.controller.current_filter() {
1405 Ok(filter) => filter,
1406 Err(error) => return Some(Err(ControlledTextGenerationError::Controller(error))),
1407 };
1408 let submission = match step {
1409 TextGenerationStep::Prefill(prompt) => {
1410 B::submit_text_prefill(self.runtime, prompt, &filter, &mut self.backend_state)
1411 }
1412 TextGenerationStep::Decode(token) => {
1413 B::submit_text_decode(self.runtime, token, &filter, &mut self.backend_state)
1414 }
1415 };
1416 let submission = match submission {
1417 Ok(submission) => submission,
1418 Err(error) => return Some(Err(ControlledTextGenerationError::Backend(error))),
1419 };
1420 let token = submission.output;
1421 if let Err(error) = self.retain_completion(submission.completion) {
1422 return Some(Err(ControlledTextGenerationError::Backend(error)));
1423 }
1424 self.step = Some(TextGenerationStep::Decode(token.clone()));
1425 if let Some(remaining_tokens) = &mut self.remaining_tokens {
1426 *remaining_tokens -= 1;
1427 }
1428 Some(Ok(token))
1429 }
1430}
1431
1432impl<B, C> Iterator for ControlledTextGeneration<'_, B, C>
1433where
1434 B: TextGenerationBackend,
1435 C: TokenFilterController,
1436{
1437 type Item =
1438 Result<ControlledToken<B::Token>, ControlledTextGenerationError<B::Error, C::Error>>;
1439
1440 fn next(&mut self) -> Option<Self::Item> {
1441 let token = match self.inner.next_output()? {
1442 Ok(token) => token,
1443 Err(error) => return Some(Err(error)),
1444 };
1445 let token_id = match token.token_id() {
1446 Ok(token_id) => token_id,
1447 Err(error) => {
1448 self.inner.step = None;
1449 return Some(Err(ControlledTextGenerationError::Backend(error)));
1450 }
1451 };
1452 if let Err(error) = self.inner.controller.commit_token(token_id) {
1453 self.inner.step = None;
1454 return Some(Err(ControlledTextGenerationError::Controller(error)));
1455 }
1456 Some(Ok(ControlledToken {
1457 output: token,
1458 token_id,
1459 }))
1460 }
1461}
1462
1463impl<B, C> Drop for TextGenerationMachine<'_, B, C>
1464where
1465 B: TextGenerationBackend,
1466 C: TokenFilterController,
1467{
1468 fn drop(&mut self) {
1469 for completion in self.completions.drain(..) {
1470 let _ = completion.wait();
1471 }
1472 }
1473}
1474
1475pub struct TextGeneration<'a, B: TextGenerationBackend> {
1481 inner: TextGenerationMachine<'a, B, UnconstrainedTokens>,
1482}
1483
1484impl<'a, B: TextGenerationBackend> TextGeneration<'a, B> {
1485 pub fn new(
1487 runtime: &'a mut ModelRuntime<B>,
1488 prompt_token_ids: Vec<u32>,
1489 config: TextGenerationConfig,
1490 ) -> Result<Self, B::Error> {
1491 let prompt = B::prepare_text_prompt(runtime.backend(), prompt_token_ids)?;
1492 Self::from_prompt(runtime, prompt, config)
1493 }
1494
1495 pub fn from_prompt(
1497 runtime: &'a mut ModelRuntime<B>,
1498 prompt: B::Prompt,
1499 config: TextGenerationConfig,
1500 ) -> Result<Self, B::Error> {
1501 TextGenerationMachine::new(runtime, prompt, config, UnconstrainedTokens)
1502 .map(|inner| Self { inner })
1503 .map_err(unreachable_unconstrained_error)
1504 }
1505}
1506
1507fn unreachable_unconstrained_error<B>(
1508 error: ControlledTextGenerationError<B, std::convert::Infallible>,
1509) -> B
1510where
1511 B: std::error::Error + 'static,
1512{
1513 match error {
1514 ControlledTextGenerationError::Backend(error) => error,
1515 ControlledTextGenerationError::Controller(error) => match error {},
1516 }
1517}
1518
1519impl<B: TextGenerationBackend> Iterator for TextGeneration<'_, B> {
1520 type Item = Result<B::Token, B::Error>;
1521
1522 fn next(&mut self) -> Option<Self::Item> {
1523 self.inner
1524 .next_output()
1525 .map(|result| result.map_err(unreachable_unconstrained_error))
1526 }
1527}
1528
1529pub trait DistributedSession {
1536 type Value;
1538 type Completion: Completion<Error = Self::Error>;
1540 type Error: std::error::Error + Send + Sync + 'static;
1542
1543 fn descriptor(&self) -> DistributedSessionDescriptor;
1545 fn capabilities(&self) -> DistributedCapabilities;
1547
1548 fn all_reduce_sum(
1550 &self,
1551 scope: CollectiveScope,
1552 input: &Self::Value,
1553 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1554
1555 fn all_gather(
1557 &self,
1558 scope: CollectiveScope,
1559 input: &Self::Value,
1560 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1561
1562 fn all_to_all_v(
1564 &self,
1565 scope: CollectiveScope,
1566 input: &Self::Value,
1567 send_counts: &[usize],
1568 receive_counts: &[usize],
1569 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1570
1571 fn send(
1573 &self,
1574 scope: CollectiveScope,
1575 peer: usize,
1576 input: &Self::Value,
1577 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1578
1579 fn receive(
1581 &self,
1582 scope: CollectiveScope,
1583 peer: usize,
1584 value: &ValueDescriptor,
1585 ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1586
1587 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error>;
1589}
1590
1591pub trait DistributedBackend: BackendProvider {
1593 type DistributedSession: DistributedSession<Error = Self::Error>;
1595
1596 fn distributed_session(session: &Self::Session) -> Option<&Self::DistributedSession>;
1598}
1599
1600#[cfg(test)]
1601mod tests {
1602 use super::*;
1603 use std::{convert::Infallible, io::Write};
1604
1605 #[test]
1606 fn text_generation_config_validates_portable_mirostat_strategy() {
1607 let sampling = crate::generation::resolve_generation_config(
1608 None,
1609 crate::generation::GenerationConfigOverrides {
1610 temperature: Some(0.8),
1611 ..crate::generation::GenerationConfigOverrides::default()
1612 },
1613 )
1614 .unwrap();
1615 let config = TextGenerationConfig::new(sampling)
1616 .with_seed(7)
1617 .with_mirostat_v2(5.0, 0.1)
1618 .unwrap();
1619 assert_eq!(config.seed(), 7);
1620 assert_eq!(
1621 config.strategy(),
1622 TextSamplingStrategy::MirostatV2 { tau: 5.0, eta: 0.1 }
1623 );
1624 assert!(matches!(
1625 TextGenerationConfig::new(sampling).with_mirostat_v2(0.0, 0.1),
1626 Err(GenerationError::InvalidMirostatTau(0.0))
1627 ));
1628 assert!(matches!(
1629 TextGenerationConfig::new(sampling).with_mirostat_v2(5.0, f32::NAN),
1630 Err(GenerationError::InvalidMirostatEta(value)) if value.is_nan()
1631 ));
1632 }
1633
1634 #[derive(Debug, Clone)]
1635 struct Done;
1636 impl Completion for Done {
1637 type Error = Infallible;
1638 fn is_complete(&self) -> Result<bool, Self::Error> {
1639 Ok(true)
1640 }
1641 fn wait(&self) -> Result<(), Self::Error> {
1642 Ok(())
1643 }
1644 }
1645 struct Mock;
1646 impl BackendProvider for Mock {
1647 type ModelConfig = u32;
1648 type Model = u32;
1649 type Session = MockSession;
1650 type Error = Infallible;
1651 fn descriptor(&self) -> BackendDescriptor {
1652 BackendDescriptor::new("mock", "1")
1653 }
1654 fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error> {
1655 Ok(vec![])
1656 }
1657 fn prepare_model(&self, config: u32) -> Result<PreparedModel<u32>, Self::Error> {
1658 Ok(PreparedModel::new(config, SessionCapabilities::default()))
1659 }
1660 fn create_session(&self, model: PreparedModel<u32>) -> Result<MockSession, Self::Error> {
1661 Ok(MockSession {
1662 model: model.into_inner(),
1663 tokens: vec![],
1664 distributed: None,
1665 })
1666 }
1667 }
1668
1669 #[derive(Default)]
1670 struct LoadingMock {
1671 materializations: std::sync::atomic::AtomicUsize,
1672 }
1673 struct LoadingMockSession;
1674
1675 struct LoadingConfigurationResolver;
1676
1677 impl ModelConfigurationResolver for LoadingConfigurationResolver {
1678 type ArtifactPlan = ();
1679
1680 fn resolve_safetensors(
1681 &self,
1682 json: &serde_json::Value,
1683 ) -> Result<crate::ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
1684 Ok(crate::ResolvedModelConfiguration::new(
1685 crate::ModelConfiguration::new(
1686 "llama",
1687 "llama",
1688 "llama",
1689 crate::LoadingProtocol::Model,
1690 Some(json.clone()),
1691 )?,
1692 (),
1693 ))
1694 }
1695
1696 fn resolve_gguf(
1697 &self,
1698 architecture: &str,
1699 _checkpoint: &eredu_gguf::Checkpoint,
1700 ) -> Result<crate::ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
1701 if architecture != "llama" {
1702 return Err(ArtifactError::UnsupportedGgufArchitecture(
1703 architecture.into(),
1704 ));
1705 }
1706 Ok(crate::ResolvedModelConfiguration::new(
1707 crate::ModelConfiguration::new(
1708 architecture,
1709 architecture,
1710 "llama",
1711 crate::LoadingProtocol::Model,
1712 None,
1713 )?,
1714 (),
1715 ))
1716 }
1717
1718 fn gguf_companion_requirements(
1719 &self,
1720 _architecture: &str,
1721 _checkpoint: &eredu_gguf::Checkpoint,
1722 ) -> Result<Vec<crate::GgufCompanionRequirement>, ArtifactError> {
1723 Ok(Vec::new())
1724 }
1725 }
1726
1727 static LOADING_CONFIGURATION_RESOLVER: LoadingConfigurationResolver =
1728 LoadingConfigurationResolver;
1729
1730 impl BackendProvider for LoadingMock {
1731 type ModelConfig = (ModelPreparationPlan, u32);
1732 type Model = u32;
1733 type Session = LoadingMockSession;
1734 type Error = std::convert::Infallible;
1735
1736 fn descriptor(&self) -> BackendDescriptor {
1737 BackendDescriptor::new("loading-mock", "1")
1738 }
1739
1740 fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error> {
1741 Ok(Vec::new())
1742 }
1743
1744 fn prepare_model(
1745 &self,
1746 (plan, model): Self::ModelConfig,
1747 ) -> Result<PreparedModel<Self::Model>, Self::Error> {
1748 self.materializations
1749 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1750 assert_eq!(plan.inspection().configuration().family(), "llama");
1751 Ok(PreparedModel::new(
1752 model,
1753 plan.admitted_session_capabilities(),
1754 ))
1755 }
1756
1757 fn create_session(
1758 &self,
1759 _: PreparedModel<Self::Model>,
1760 ) -> Result<Self::Session, Self::Error> {
1761 Ok(LoadingMockSession)
1762 }
1763 }
1764
1765 impl BackendSession<LoadingMock> for LoadingMockSession {
1766 type PrefillInput = ();
1767 type DecodeInput = ();
1768 type Output = ();
1769 type Completion = LoadingDone;
1770
1771 fn capabilities(&self) -> SessionCapabilities {
1772 SessionCapabilities::default()
1773 }
1774
1775 fn prefill(
1776 &mut self,
1777 _: &LoadingMock,
1778 _: (),
1779 ) -> Result<Submission<(), LoadingDone>, std::convert::Infallible> {
1780 Ok(Submission {
1781 output: (),
1782 completion: LoadingDone,
1783 })
1784 }
1785
1786 fn decode(
1787 &mut self,
1788 _: &LoadingMock,
1789 _: (),
1790 ) -> Result<Submission<(), LoadingDone>, std::convert::Infallible> {
1791 Ok(Submission {
1792 output: (),
1793 completion: LoadingDone,
1794 })
1795 }
1796
1797 fn observe_output(
1798 &self,
1799 _: &LoadingMock,
1800 _: &(),
1801 ) -> Result<ObservationSet, std::convert::Infallible> {
1802 Ok(ObservationSet::new())
1803 }
1804 }
1805
1806 #[derive(Debug, Clone, Copy)]
1807 struct LoadingDone;
1808
1809 impl Completion for LoadingDone {
1810 type Error = std::convert::Infallible;
1811
1812 fn is_complete(&self) -> Result<bool, Self::Error> {
1813 Ok(true)
1814 }
1815
1816 fn wait(&self) -> Result<(), Self::Error> {
1817 Ok(())
1818 }
1819 }
1820
1821 impl ModelLoadingBackend for LoadingMock {
1822 type LoadOptions = u32;
1823 type SelectedPreparation = u32;
1824 type ConfigurationResolver = LoadingConfigurationResolver;
1825
1826 fn configuration_resolver(&self) -> &Self::ConfigurationResolver {
1827 &LOADING_CONFIGURATION_RESOLVER
1828 }
1829
1830 fn preparation_policy(
1831 &self,
1832 options: &Self::LoadOptions,
1833 ) -> Result<PreparationPolicy, Self::Error> {
1834 Ok(
1835 PreparationPolicy::default().with_required_session_capabilities(
1836 SessionCapabilities::default().with_activation_inspection(*options == 99),
1837 ),
1838 )
1839 }
1840
1841 fn select_preparation(
1842 &self,
1843 _: &ArtifactInspection,
1844 options: &Self::LoadOptions,
1845 _: PreparationPolicy,
1846 ) -> Result<Self::SelectedPreparation, Self::Error> {
1847 Ok(*options)
1848 }
1849
1850 fn session_capabilities(
1851 &self,
1852 _: &ArtifactInspection,
1853 _: PreparationPolicy,
1854 ) -> Result<SessionCapabilities, Self::Error> {
1855 Ok(SessionCapabilities::default())
1856 }
1857
1858 fn model_config(
1859 &self,
1860 plan: ModelPreparationPlan,
1861 selected: Self::SelectedPreparation,
1862 ) -> Result<Self::ModelConfig, Self::Error> {
1863 Ok((plan, selected))
1864 }
1865 }
1866
1867 fn write_loading_fixture(root: &Path) {
1868 std::fs::write(root.join("config.json"), r#"{"model_type":"llama"}"#).unwrap();
1869 let header = br#"{"token_embd.weight":{"dtype":"F32","shape":[1],"data_offsets":[0,4]}}"#;
1870 let mut file = std::fs::File::create(root.join("model.safetensors")).unwrap();
1871 file.write_all(&(header.len() as u64).to_le_bytes())
1872 .unwrap();
1873 file.write_all(header).unwrap();
1874 file.write_all(&[0; 4]).unwrap();
1875 }
1876 struct MockSession {
1877 model: u32,
1878 tokens: Vec<u32>,
1879 distributed: Option<MockDistributed>,
1880 }
1881 impl BackendSession<Mock> for MockSession {
1882 type PrefillInput = Vec<u32>;
1883 type DecodeInput = u32;
1884 type Output = u32;
1885 type Completion = Done;
1886 fn capabilities(&self) -> SessionCapabilities {
1887 SessionCapabilities::default()
1888 }
1889 fn prefill(
1890 &mut self,
1891 _: &Mock,
1892 input: Vec<u32>,
1893 ) -> Result<Submission<u32, Done>, Infallible> {
1894 self.tokens.extend(input);
1895 Ok(Submission {
1896 output: self.tokens.len() as u32 + self.model,
1897 completion: Done,
1898 })
1899 }
1900 fn decode(&mut self, _: &Mock, input: u32) -> Result<Submission<u32, Done>, Infallible> {
1901 self.tokens.push(input);
1902 Ok(Submission {
1903 output: self.tokens.len() as u32 + self.model,
1904 completion: Done,
1905 })
1906 }
1907
1908 fn observe_output(&self, _: &Mock, output: &u32) -> Result<ObservationSet, Infallible> {
1909 let mut observations = ObservationSet::new();
1910 observations
1911 .insert(
1912 "mock.output",
1913 crate::ObservationValue::Unsigned(u64::from(*output)),
1914 )
1915 .unwrap();
1916 Ok(observations)
1917 }
1918 }
1919
1920 impl TextGenerationBackend for Mock {
1921 type Prompt = Vec<u32>;
1922 type Token = u32;
1923 type TextGenerationState = (u32, u64);
1924 type TextCompletion = Done;
1925
1926 fn start_text_generation(
1927 _: &Self,
1928 config: TextGenerationConfig,
1929 ) -> Result<Self::TextGenerationState, Self::Error> {
1930 Ok((config.sampling().top_k as u32, config.seed()))
1931 }
1932
1933 fn prepare_text_prompt(
1934 _: &Self,
1935 prompt_token_ids: Vec<u32>,
1936 ) -> Result<Self::Prompt, Self::Error> {
1937 Ok(prompt_token_ids)
1938 }
1939
1940 fn submit_text_prefill(
1941 runtime: &mut ModelRuntime<Self>,
1942 prompt: Self::Prompt,
1943 filter: &TokenFilter,
1944 state: &mut Self::TextGenerationState,
1945 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error> {
1946 let submission = runtime.prefill(prompt)?;
1947 Ok(Submission {
1948 output: apply_mock_filter(submission.output + state.0 + state.1 as u32, filter),
1949 completion: submission.completion,
1950 })
1951 }
1952
1953 fn submit_text_decode(
1954 runtime: &mut ModelRuntime<Self>,
1955 token: Self::Token,
1956 filter: &TokenFilter,
1957 _: &mut Self::TextGenerationState,
1958 ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error> {
1959 let submission = runtime.decode(token)?;
1960 Ok(Submission {
1961 output: apply_mock_filter(submission.output, filter),
1962 completion: submission.completion,
1963 })
1964 }
1965 }
1966
1967 impl MultimodalPreparationBackend for Mock {
1968 fn prepare_multimodal_input<E>(
1969 _: &ModelRuntime<Self>,
1970 request: &TokenizedMultimodalRequest,
1971 _: &mut dyn FnMut(&str) -> Result<Vec<u32>, E>,
1972 ) -> Result<Self::Prompt, MultimodalPreparationFailure<Self::Error, E>>
1973 where
1974 E: std::error::Error + Send + Sync + 'static,
1975 {
1976 let mut prompt = Vec::new();
1977 for segment in request.segments() {
1978 match segment {
1979 crate::TokenizedMultimodalSegment::TokenIds(ids) => {
1980 prompt.extend_from_slice(ids);
1981 }
1982 crate::TokenizedMultimodalSegment::Media(crate::Media::Image(_)) => {
1983 prompt.push(1_001);
1984 }
1985 crate::TokenizedMultimodalSegment::Media(crate::Media::Video(_)) => {
1986 prompt.push(1_002);
1987 }
1988 crate::TokenizedMultimodalSegment::Media(crate::Media::Audio(_)) => {
1989 prompt.push(1_003);
1990 }
1991 }
1992 }
1993 Ok(prompt)
1994 }
1995 }
1996
1997 impl ModelCapabilityBackend for Mock {
1998 fn model_capabilities(
1999 _: &ModelRuntime<Self>,
2000 ) -> Result<ModelCapabilities, CapabilityError> {
2001 Ok(ModelCapabilities {
2002 effective_model_type: "mock".into(),
2003 native_max_context: crate::Observed::exact(64, "mock configuration"),
2004 effective_max_context: crate::Observed::exact(64, "mock configuration"),
2005 state_strategy: crate::CacheStateStrategy::FullKv,
2006 modalities: crate::InputModalities::TEXT,
2007 estimation: crate::EstimationCompleteness::Complete,
2008 })
2009 }
2010
2011 fn count_prepared_input(
2012 _: &ModelRuntime<Self>,
2013 input: &Self::Prompt,
2014 ) -> Result<InputTokenCount, CapabilityError> {
2015 Ok(InputTokenCount::text(input.len() as u64))
2016 }
2017
2018 fn estimate_runtime_state(
2019 _: &ModelRuntime<Self>,
2020 input: InputTokenCount,
2021 max_output_tokens: u64,
2022 batch_size: u64,
2023 ) -> Result<RuntimeStateEstimate, CapabilityError> {
2024 crate::estimate_runtime_state(
2025 &crate::StateMemoryLayout::new(
2026 crate::LayerSchedule::new(
2027 1,
2028 vec![crate::cache::LayerCachePolicy::key_only(
2029 crate::AttentionPolicy::Full,
2030 1,
2031 2,
2032 )
2033 .unwrap()],
2034 )
2035 .unwrap(),
2036 vec![0],
2037 1,
2038 1,
2039 crate::EstimationCompleteness::Complete,
2040 )
2041 .unwrap(),
2042 input,
2043 max_output_tokens,
2044 batch_size,
2045 std::num::NonZeroU8::new(4).unwrap(),
2046 )
2047 }
2048
2049 fn static_memory(
2050 runtime: &ModelRuntime<Self>,
2051 ) -> Result<StaticMemoryReport, CapabilityError> {
2052 let unavailable = || crate::Observed::unavailable("mock does not expose this counter");
2053 Ok(StaticMemoryReport {
2054 logical_parameter_bytes: crate::Observed::exact(
2055 u64::from(runtime.session().model),
2056 "mock model",
2057 ),
2058 current_host_resident_bytes: unavailable(),
2059 current_device_resident_bytes: unavailable(),
2060 planned_disk_backed_bytes: unavailable(),
2061 backend_active_allocation_bytes: unavailable(),
2062 backend_allocator_cache_bytes: unavailable(),
2063 physical_semantics: crate::PhysicalMemorySemantics::Unknown,
2064 currently_cached_shards: unavailable(),
2065 })
2066 }
2067 }
2068
2069 fn apply_mock_filter(candidate: u32, filter: &TokenFilter) -> u32 {
2070 let Some(allowed) = filter.allowed_mask() else {
2071 return candidate;
2072 };
2073 allowed
2074 .get(candidate as usize)
2075 .copied()
2076 .unwrap_or(false)
2077 .then_some(candidate)
2078 .or_else(|| {
2079 allowed
2080 .iter()
2081 .position(|allowed| *allowed)
2082 .map(|token| token as u32)
2083 })
2084 .expect("validated token filters allow at least one token")
2085 }
2086
2087 #[test]
2088 fn generic_loader_inspects_plans_and_prepares_on_the_selected_backend() {
2089 let root = tempfile::tempdir().unwrap();
2090 write_loading_fixture(root.path());
2091 let prepared = load_model(&LoadingMock::default(), root.path(), 41).unwrap();
2092 assert_eq!(*prepared, 41);
2093
2094 let runtime = ModelRuntime::load(LoadingMock::default(), root.path(), 7).unwrap();
2095 assert_eq!(runtime.backend().descriptor().name, "loading-mock");
2096
2097 let missing = root.path().join("missing");
2098 assert!(matches!(
2099 load_model(&LoadingMock::default(), &missing, 1),
2100 Err(ModelLoadError::Artifact(ArtifactError::MissingArtifact(path)))
2101 if path == missing
2102 ));
2103 }
2104
2105 #[test]
2106 fn session_requirement_is_rejected_before_materialization() {
2107 let root = tempfile::tempdir().unwrap();
2108 write_loading_fixture(root.path());
2109 let backend = LoadingMock::default();
2110
2111 let error = load_model(&backend, root.path(), 99).unwrap_err();
2112
2113 assert!(matches!(
2114 error,
2115 ModelLoadError::SessionCapability(error)
2116 if error.capability() == "activation_inspection"
2117 ));
2118 assert_eq!(
2119 backend
2120 .materializations
2121 .load(std::sync::atomic::Ordering::Relaxed),
2122 0
2123 );
2124 }
2125
2126 struct FixedController {
2127 tokens: Vec<u32>,
2128 committed: usize,
2129 }
2130
2131 impl TokenFilterController for FixedController {
2132 type Error = Infallible;
2133
2134 fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
2135 let mut allowed = vec![false; 64];
2136 allowed[self.tokens[self.committed] as usize] = true;
2137 Ok(TokenFilter::allowed(allowed).unwrap())
2138 }
2139
2140 fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error> {
2141 assert_eq!(token_id, self.tokens[self.committed]);
2142 self.committed += 1;
2143 Ok(())
2144 }
2145
2146 fn is_complete(&mut self) -> Result<bool, Self::Error> {
2147 Ok(self.committed == self.tokens.len())
2148 }
2149 }
2150
2151 #[test]
2152 fn mock_prefill_and_multiple_decode_steps() {
2153 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2154 let prefill = runtime.prefill(vec![1, 2]).unwrap();
2155 assert_eq!(prefill.output, 12);
2156 assert!(prefill.completion.is_complete().unwrap());
2157 assert_eq!(runtime.decode(3).unwrap().output, 13);
2158 assert_eq!(runtime.decode(4).unwrap().output, 14);
2159 }
2160
2161 #[test]
2162 fn portable_text_generation_prefills_and_decodes_without_tensor_types() {
2163 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2164 let sampling = crate::resolve_generation_config(
2165 None,
2166 crate::GenerationConfigOverrides {
2167 max_new_tokens: Some(3),
2168 ..Default::default()
2169 },
2170 )
2171 .unwrap();
2172 let mut generation = TextGeneration::new(
2173 &mut runtime,
2174 vec![1, 2],
2175 TextGenerationConfig::new(sampling).with_seed(3),
2176 )
2177 .unwrap();
2178 assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 55);
2179 assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 13);
2180 assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 14);
2181 assert!(generation.next().is_none());
2182 }
2183
2184 #[test]
2185 fn portable_media_preparation_feeds_the_existing_generation_contract() {
2186 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2187 let request = crate::MultimodalRequest::new(vec![
2188 crate::MultimodalSegment::TokenIds(vec![7, 8]),
2189 crate::MultimodalSegment::Media(crate::Media::Image(
2190 crate::RgbImage::new(vec![5, 6, 7], 1, 1).unwrap(),
2191 )),
2192 crate::MultimodalSegment::TokenIds(vec![9]),
2193 ])
2194 .unwrap()
2195 .tokenize::<Infallible>(|_| unreachable!("request is already tokenized"))
2196 .unwrap();
2197 let prompt = Mock::prepare_multimodal_input(&runtime, &request, &mut |_| {
2198 Ok::<_, Infallible>(Vec::new())
2199 })
2200 .unwrap();
2201 assert_eq!(prompt, vec![7, 8, 1_001, 9]);
2202
2203 let sampling = crate::resolve_generation_config(
2204 None,
2205 crate::GenerationConfigOverrides {
2206 max_new_tokens: Some(2),
2207 ..Default::default()
2208 },
2209 )
2210 .unwrap();
2211 let mut generation =
2212 TextGeneration::from_prompt(&mut runtime, prompt, TextGenerationConfig::new(sampling))
2213 .unwrap();
2214 assert!(generation.next().unwrap().is_ok());
2215 assert!(generation.next().unwrap().is_ok());
2216 assert!(generation.next().is_none());
2217 }
2218
2219 #[test]
2220 fn model_capability_extension_observes_the_selected_mock_session() {
2221 let runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2222 let capabilities = Mock::model_capabilities(&runtime).unwrap();
2223 assert_eq!(capabilities.effective_model_type, "mock");
2224 let input = Mock::count_prepared_input(&runtime, &vec![1, 2, 3]).unwrap();
2225 assert_eq!(input.model_positions, 3);
2226 let state = Mock::estimate_runtime_state(&runtime, input, 2, 1).unwrap();
2227 assert_eq!(state.requested_state_bytes, 5 * 2 * 4);
2228 assert_eq!(
2229 Mock::static_memory(&runtime)
2230 .unwrap()
2231 .logical_parameter_bytes
2232 .value(),
2233 Some(&10)
2234 );
2235 }
2236
2237 #[test]
2238 fn controlled_generation_applies_portable_filters_and_commits_tokens() {
2239 let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2240 let sampling = crate::resolve_generation_config(
2241 None,
2242 crate::GenerationConfigOverrides {
2243 max_new_tokens: Some(2),
2244 ..Default::default()
2245 },
2246 )
2247 .unwrap();
2248 let controller = FixedController {
2249 tokens: vec![7, 8],
2250 committed: 0,
2251 };
2252 let mut generation = ControlledTextGeneration::new(
2253 &mut runtime,
2254 vec![1, 2],
2255 TextGenerationConfig::new(sampling),
2256 controller,
2257 )
2258 .unwrap();
2259 assert_eq!(generation.next().unwrap().unwrap().token_id(), 7);
2260 assert_eq!(generation.next().unwrap().unwrap().token_id(), 8);
2261 assert!(generation.controller_mut().is_complete().unwrap());
2262 assert!(generation.next().is_none());
2263 }
2264
2265 #[derive(Debug, Clone)]
2266 struct MockDistributed {
2267 descriptor: DistributedSessionDescriptor,
2268 }
2269
2270 impl DistributedSession for MockDistributed {
2271 type Value = Vec<u32>;
2272 type Completion = Done;
2273 type Error = Infallible;
2274
2275 fn descriptor(&self) -> DistributedSessionDescriptor {
2276 self.descriptor.clone()
2277 }
2278
2279 fn capabilities(&self) -> DistributedCapabilities {
2280 DistributedCapabilities::new(true, [CollectiveGroupId::new(7)], true, true, true)
2281 }
2282
2283 fn all_reduce_sum(
2284 &self,
2285 _: CollectiveScope,
2286 input: &Vec<u32>,
2287 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2288 Ok(Submission {
2289 output: input.iter().map(|value| value * 2).collect(),
2290 completion: Done,
2291 })
2292 }
2293
2294 fn all_gather(
2295 &self,
2296 _: CollectiveScope,
2297 input: &Vec<u32>,
2298 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2299 let mut output = input.clone();
2300 output.extend(input);
2301 Ok(Submission {
2302 output,
2303 completion: Done,
2304 })
2305 }
2306
2307 fn all_to_all_v(
2308 &self,
2309 _: CollectiveScope,
2310 input: &Vec<u32>,
2311 _: &[usize],
2312 _: &[usize],
2313 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2314 Ok(Submission {
2315 output: input.clone(),
2316 completion: Done,
2317 })
2318 }
2319
2320 fn send(
2321 &self,
2322 _: CollectiveScope,
2323 _: usize,
2324 input: &Vec<u32>,
2325 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2326 Ok(Submission {
2327 output: input.clone(),
2328 completion: Done,
2329 })
2330 }
2331
2332 fn receive(
2333 &self,
2334 _: CollectiveScope,
2335 peer: usize,
2336 value: &ValueDescriptor,
2337 ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2338 Ok(Submission {
2339 output: vec![peer as u32; value.shape().iter().product()],
2340 completion: Done,
2341 })
2342 }
2343
2344 fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Infallible> {
2345 let mut output = local.to_vec();
2346 output.extend_from_slice(local);
2347 Ok(output)
2348 }
2349 }
2350
2351 impl DistributedBackend for Mock {
2352 type DistributedSession = MockDistributed;
2353
2354 fn distributed_session(session: &MockSession) -> Option<&Self::DistributedSession> {
2355 session.distributed.as_ref()
2356 }
2357 }
2358
2359 #[test]
2360 fn mock_distributed_session_owns_collective_and_transfer_lifecycle() {
2361 let tensor_group =
2362 CollectiveGroupDescriptor::new(CollectiveGroupId::new(7), vec![0, 1], 0).unwrap();
2363 let session = MockDistributed {
2364 descriptor: DistributedSessionDescriptor::new(2, 0, vec![tensor_group]).unwrap(),
2365 };
2366 let capabilities = session.capabilities();
2367 assert!(capabilities.exact_completion());
2368 assert_eq!(
2369 capabilities.collective_groups(),
2370 &[CollectiveGroupId::new(7)]
2371 );
2372 assert_eq!(
2373 session
2374 .all_reduce_sum(
2375 CollectiveScope::Group(CollectiveGroupId::new(7)),
2376 &vec![2, 3]
2377 )
2378 .unwrap()
2379 .wait()
2380 .unwrap(),
2381 vec![4, 6]
2382 );
2383 assert_eq!(
2384 session
2385 .receive(
2386 CollectiveScope::World,
2387 1,
2388 &ValueDescriptor::new(vec![2], TensorDtype::U32).unwrap(),
2389 )
2390 .unwrap()
2391 .wait()
2392 .unwrap(),
2393 vec![1, 1]
2394 );
2395 assert_eq!(session.all_gather_words(&[7]).unwrap(), vec![7, 7]);
2396
2397 let model_session = MockSession {
2398 model: 0,
2399 tokens: Vec::new(),
2400 distributed: Some(session.clone()),
2401 };
2402 assert_eq!(
2403 Mock::distributed_session(&model_session)
2404 .unwrap()
2405 .descriptor(),
2406 session.descriptor()
2407 );
2408 }
2409
2410 #[test]
2411 fn distributed_descriptors_round_trip_and_reject_invalid_ranks() {
2412 let descriptor = DistributedSessionDescriptor::new(
2413 6,
2414 4,
2415 vec![CollectiveGroupDescriptor::new(CollectiveGroupId::new(9), vec![1, 4], 1).unwrap()],
2416 )
2417 .unwrap();
2418 let encoded = serde_json::to_string(&descriptor).unwrap();
2419 assert_eq!(
2420 serde_json::from_str::<DistributedSessionDescriptor>(&encoded).unwrap(),
2421 descriptor
2422 );
2423 let scope = CollectiveScope::Group(CollectiveGroupId::new(9));
2424 assert_eq!(
2425 serde_json::from_str::<CollectiveScope>(&serde_json::to_string(&scope).unwrap())
2426 .unwrap(),
2427 scope
2428 );
2429 assert!(DistributedSessionDescriptor::new(descriptor.world_size(), 6, Vec::new()).is_err());
2430 assert!(serde_json::from_str::<DistributedSessionDescriptor>(
2431 r#"{"world_size":6,"rank":6,"groups":[]}"#
2432 )
2433 .is_err());
2434 }
2435}