Skip to main content

eredu_core/
backend.rs

1//! High-level contract implemented once per execution backend.
2
3use 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/// Stable, extensible description of an execution backend.
23#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
24pub struct BackendDescriptor {
25    /// Backend implementation name, such as `example-backend`.
26    name: String,
27    /// Backend implementation version.
28    version: String,
29}
30
31impl BackendDescriptor {
32    /// Creates a backend identity without freezing future descriptor fields.
33    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    /// Returns the backend implementation name.
41    pub fn name(&self) -> &str {
42        &self.name
43    }
44
45    /// Returns the backend implementation version.
46    pub fn version(&self) -> &str {
47        &self.version
48    }
49}
50
51/// Portable description of one backend-visible device.
52#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
53pub struct DeviceDescriptor {
54    /// Backend-stable device identifier.
55    id: String,
56    /// Human-readable device name.
57    name: String,
58    /// Backend-specific device family without a closed core enum.
59    family: String,
60    /// Total memory when discoverable.
61    memory_bytes: Option<u64>,
62}
63
64impl DeviceDescriptor {
65    /// Creates a backend-stable device description.
66    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    /// Returns the backend-stable device identifier.
81    pub fn id(&self) -> &str {
82        &self.id
83    }
84    /// Returns the human-readable device name.
85    pub fn name(&self) -> &str {
86        &self.name
87    }
88    /// Returns the backend-defined device family.
89    pub fn family(&self) -> &str {
90        &self.family
91    }
92    /// Returns total device memory when known.
93    pub const fn memory_bytes(&self) -> Option<u64> {
94        self.memory_bytes
95    }
96}
97
98/// Fail-closed capabilities discovered from a backend and device.
99#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
100pub struct DeviceCapabilities {
101    /// Supports exact completion observation for submissions.
102    exact_completion: bool,
103    /// Supports device-to-device transfer for backend-owned values.
104    transfers: bool,
105    /// Supports collective execution for a complete session.
106    collectives: bool,
107}
108
109impl DeviceCapabilities {
110    /// Creates an exact fail-closed device mechanism report.
111    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    /// Returns whether exact completion observation is available.
120    pub const fn exact_completion(&self) -> bool {
121        self.exact_completion
122    }
123    /// Returns whether device transfers are available.
124    pub const fn transfers(&self) -> bool {
125        self.transfers
126    }
127    /// Returns whether collective execution is available.
128    pub const fn collectives(&self) -> bool {
129        self.collectives
130    }
131}
132
133/// Fail-closed capabilities of one exact prepared model session.
134#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
135pub struct SessionCapabilities {
136    /// Supports backend-managed persistent decode caches.
137    persistent_cache: bool,
138    /// Supports explicit host observation of completed session outputs.
139    output_observation: bool,
140    /// Supports named activation inspection for instrumented session passes.
141    activation_inspection: bool,
142}
143
144impl SessionCapabilities {
145    /// Creates an exact fail-closed session mechanism report.
146    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    /// Returns whether persistent cache storage is available.
159    pub const fn persistent_cache(self) -> bool {
160        self.persistent_cache
161    }
162    /// Returns whether completed outputs may be observed on the host.
163    pub const fn output_observation(self) -> bool {
164        self.output_observation
165    }
166    /// Returns whether named activation inspection is available.
167    pub const fn activation_inspection(self) -> bool {
168        self.activation_inspection
169    }
170
171    /// Returns a report with persistent cache storage configured.
172    pub const fn with_persistent_cache(mut self, supported: bool) -> Self {
173        self.persistent_cache = supported;
174        self
175    }
176    /// Returns a report with output observation configured.
177    pub const fn with_output_observation(mut self, supported: bool) -> Self {
178        self.output_observation = supported;
179        self
180    }
181    /// Returns a report with activation inspection configured.
182    pub const fn with_activation_inspection(mut self, supported: bool) -> Self {
183        self.activation_inspection = supported;
184        self
185    }
186    /// Validates fail-closed requirements against an exact available report.
187    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/// One unavailable exact-session requirement.
214#[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    /// Returns the stable capability name.
222    pub const fn capability(self) -> &'static str {
223        self.capability
224    }
225}
226
227/// Fail-closed distributed operations exposed by one selected session.
228#[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    /// Creates an exact mechanism capability report.
239    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    /// Returns whether world-scoped collectives are available.
256    pub const fn world_collectives(&self) -> bool {
257        self.world_collectives
258    }
259    /// Returns opaque groups supporting collectives.
260    pub fn collective_groups(&self) -> &[CollectiveGroupId] {
261        &self.collective_groups
262    }
263    /// Returns whether point-to-point transfers are available.
264    pub const fn point_to_point(&self) -> bool {
265        self.point_to_point
266    }
267    /// Returns whether variable-count all-to-all is available.
268    pub const fn variable_all_to_all(&self) -> bool {
269        self.variable_all_to_all
270    }
271    /// Returns whether submissions have exact completion objects.
272    pub const fn exact_completion(&self) -> bool {
273        self.exact_completion
274    }
275}
276
277/// Opaque stable identity of a selected collective group.
278#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
279#[serde(transparent)]
280pub struct CollectiveGroupId(u32);
281
282impl CollectiveGroupId {
283    /// Creates an opaque group identity selected by architecture/runtime composition.
284    pub const fn new(value: u32) -> Self {
285        Self(value)
286    }
287    /// Returns the stable numeric representation for serialization and backend maps.
288    pub const fn value(self) -> u32 {
289        self.0
290    }
291}
292
293/// Ordered membership for one opaque collective group containing this rank.
294#[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    /// Validates ordered group membership and the process-local rank.
303    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    /// Returns the opaque group identity.
329    pub const fn id(&self) -> CollectiveGroupId {
330        self.id
331    }
332    /// Returns ordered world-rank membership.
333    pub fn members(&self) -> &[usize] {
334        &self.members
335    }
336    /// Returns this process's rank within the ordered group.
337    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/// Scope of a collective or point-to-point operation.
359#[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    /// All ranks in the selected distributed session.
364    World,
365    /// One opaque selected collective group containing this rank.
366    Group(CollectiveGroupId),
367}
368
369/// Portable shape and element type for a backend-owned received value.
370#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
371pub struct ValueDescriptor {
372    /// Row-major logical shape. An empty shape describes a scalar.
373    shape: Vec<usize>,
374    /// Logical element type.
375    dtype: TensorDtype,
376}
377
378impl ValueDescriptor {
379    /// Validates a portable value shape and element type.
380    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    /// Returns the row-major logical shape.
391    pub fn shape(&self) -> &[usize] {
392        &self.shape
393    }
394
395    /// Returns the logical element type.
396    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/// Portable identity of one selected distributed session.
418#[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    /// Validates a mechanism-only distributed session realization.
427    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    /// Returns the total process count.
458    pub const fn world_size(&self) -> usize {
459        self.world_size
460    }
461    /// Returns this process's world rank.
462    pub const fn rank(&self) -> usize {
463        self.rank
464    }
465    /// Returns ordered opaque group realizations.
466    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/// Structured backend failure that does not expose a runtime exception type.
489#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
490#[non_exhaustive]
491pub enum BackendError {
492    /// A required capability is absent.
493    #[error("backend {backend} does not support required capability {capability}")]
494    Unsupported {
495        /// Backend implementation name.
496        backend: String,
497        /// Required capability.
498        capability: String,
499    },
500    /// Model preparation failed.
501    #[error("backend model preparation failed during {operation}: {message}")]
502    Preparation {
503        /// Preparation operation.
504        operation: String,
505        /// Backend-provided context.
506        message: String,
507    },
508    /// Session execution failed.
509    #[error("backend session {session} failed during {operation}: {message}")]
510    Execution {
511        /// Stable session identifier.
512        session: String,
513        /// Operation being executed.
514        operation: String,
515        /// Backend-provided context.
516        message: String,
517    },
518    /// Exact completion observation failed.
519    #[error("backend completion observation failed: {message}")]
520    Completion {
521        /// Backend-provided context.
522        message: String,
523    },
524}
525
526/// Exact completion owned by one backend submission.
527pub trait Completion {
528    /// Error produced while observing the completion.
529    type Error: std::error::Error + Send + Sync + 'static;
530
531    /// Nonblocking exact-completion observation.
532    fn is_complete(&self) -> Result<bool, Self::Error>;
533
534    /// Blocks on this exact completion only.
535    /// An error reports the outcome, not whether native resource use has stopped.
536    fn wait(&self) -> Result<(), Self::Error>;
537
538    /// Nonblocking proof that submitted work and its consumers no longer borrow
539    /// retained resources. This is independent of successful output publication:
540    /// a failed submission may become releasable while its error remains sticky.
541    ///
542    /// `false` includes pending work, unavailable evidence, and runtime contention.
543    /// An observation error, timeout, or cancellation request is never proof of
544    /// release. Implementations must include outstanding child/observation work
545    /// and must not wait, retry failed execution, or run application destructors.
546    /// Readiness does not make a failed session reusable or validate its outputs.
547    /// Backends still own retention when the public completion is dropped.
548    ///
549    /// The default recognizes successful exact completion only. Backends that
550    /// can establish safe release after failure should override this method.
551    fn resources_releasable(&self) -> bool {
552        matches!(self.is_complete(), Ok(true))
553    }
554}
555
556/// Mechanism selected for work which has not completed by a bounded deadline.
557#[derive(
558    Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Serialize, serde::Deserialize,
559)]
560pub enum CompletionCancellationMode {
561    /// The backend can request native cancellation and safely complete its teardown.
562    NativeCancel,
563    /// Native cancellation is unavailable, so the backend retains the orphaned work
564    /// and every borrowed resource until exact completion can be observed.
565    QuarantineUntilComplete,
566}
567
568/// Monotonic identity of one distributed session transaction.
569#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
570#[serde(transparent)]
571pub struct DistributedCommitEpoch(u64);
572
573impl DistributedCommitEpoch {
574    /// First epoch allocated by a newly constructed session.
575    pub const FIRST: Self = Self(1);
576
577    /// Creates a positive durable epoch identity.
578    pub const fn new(value: u64) -> Option<Self> {
579        if value == 0 {
580            None
581        } else {
582            Some(Self(value))
583        }
584    }
585
586    /// Stable serialized epoch value.
587    pub const fn value(self) -> u64 {
588        self.0
589    }
590
591    /// Returns the next representable session epoch.
592    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/// Final decision cut reached by a distributed commit attempt.
611#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
612#[serde(rename_all = "snake_case")]
613pub enum DistributedCommitPhase {
614    /// The rank could not submit its final decision contribution.
615    DecisionSubmission,
616    /// The submitted final decision did not complete within its exact contract.
617    DecisionCompletion,
618    /// The rank observed the globally fixed decision.
619    DecisionObservation,
620}
621
622/// Honest rank-local observation of one globally identified commit attempt.
623#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
624#[serde(rename_all = "snake_case")]
625pub enum DistributedCommitOutcome {
626    /// This rank observed the globally fixed all-success decision.
627    Committed(DistributedCommitEpoch),
628    /// This rank observed the globally fixed abort decision before publication became final.
629    Aborted(DistributedCommitEpoch),
630    /// The rank may have contributed, but could not observe the fixed final decision.
631    Indeterminate {
632        /// Transaction identity retained for recovery and diagnosis.
633        epoch: DistributedCommitEpoch,
634        /// Exact final decision cut whose outcome could not be observed.
635        phase: DistributedCommitPhase,
636    },
637}
638
639impl DistributedCommitOutcome {
640    /// Transaction identity shared by every outcome variant.
641    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    /// Whether further use requires external recovery of the named epoch.
650    pub const fn is_indeterminate(self) -> bool {
651        matches!(self, Self::Indeterminate { .. })
652    }
653}
654
655/// One explicit bounded-wait policy selected before a backend submission is created.
656#[derive(Debug, Clone, Copy, Eq, PartialEq)]
657pub struct BoundedCompletionWait {
658    timeout: std::time::Duration,
659    cancellation: CompletionCancellationMode,
660}
661
662impl BoundedCompletionWait {
663    /// Creates a positive relative deadline and its required safe cancellation mode.
664    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    /// Maximum time spent waiting for exact completion.
678    pub const fn timeout(self) -> std::time::Duration {
679        self.timeout
680    }
681
682    /// Required safe disposition for work still live at the deadline.
683    pub const fn cancellation(self) -> CompletionCancellationMode {
684        self.cancellation
685    }
686}
687
688/// Invalid bounded-completion policy.
689#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
690pub enum BoundedCompletionWaitError {
691    /// A zero duration cannot establish a meaningful completion deadline.
692    #[error("bounded completion timeout must be positive")]
693    ZeroTimeout,
694}
695
696/// Stable result of observing one completion through an explicit bounded policy.
697#[derive(Debug, Clone, Copy, Eq, PartialEq)]
698pub enum BoundedCompletionOutcome {
699    /// Exact backend completion was observed before the deadline.
700    Completed,
701    /// The deadline expired and the backend safely applied the selected disposition.
702    DeadlineExceeded {
703        /// Cancellation or retained-orphan mechanism actually applied.
704        cancellation: CompletionCancellationMode,
705    },
706}
707
708/// Completion which can enforce a selected deadline without dropping live resources.
709///
710/// Implementations must consume themselves on timeout. `NativeCancel` means native
711/// cancellation and teardown have completed before returning. `QuarantineUntilComplete`
712/// means the implementation has transferred ownership of the completion and all work
713/// resources to a safe owner which will release them only after exact completion. An
714/// `Err` return has the same ownership obligation: it may represent a terminally
715/// completed backend failure, or it must first transfer still-live work to the selected
716/// safe disposition. Implementations must never return an error by merely dropping live
717/// resources.
718pub trait BoundedCompletion: Completion + Sized {
719    /// Whether this completion implementation can honor the selected disposition.
720    /// Implementations with a restricted native mechanism set must override this
721    /// so schedulers can reject unsupported policy before submission.
722    fn supports_cancellation(_cancellation: CompletionCancellationMode) -> bool {
723        true
724    }
725
726    /// Observes exact completion until the policy deadline, then performs its selected
727    /// safe cancellation disposition.
728    fn wait_bounded(
729        self,
730        policy: BoundedCompletionWait,
731    ) -> Result<BoundedCompletionOutcome, Self::Error>;
732}
733
734/// Output and exact completion returned by a backend submission.
735#[derive(Debug)]
736pub struct Submission<T, C> {
737    /// Backend-owned output value.
738    pub output: T,
739    /// Completion retaining everything needed by the submitted work.
740    pub completion: C,
741}
742
743impl<T, C> Submission<T, C>
744where
745    C: Completion,
746{
747    /// Waits for this exact submission and returns its output.
748    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    /// Waits through the selected bounded policy and returns output only after exact
759    /// completion. Timed-out output is dropped only after its completion has transferred
760    /// ownership of every live backend resource to its safe cancellation owner.
761    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/// Result of a bounded submission wait. Output is authoritative only on completion.
777#[derive(Debug, Eq, PartialEq)]
778pub enum BoundedSubmissionOutcome<T> {
779    /// Exact completion was observed and the output may be consumed.
780    Completed(T),
781    /// The deadline expired and live work entered its selected safe disposition.
782    DeadlineExceeded {
783        /// Cancellation or retained-orphan mechanism actually applied.
784        cancellation: CompletionCancellationMode,
785    },
786}
787
788/// Marker wrapper proving that a model was prepared by a backend.
789#[derive(Debug)]
790pub struct PreparedModel<M> {
791    model: M,
792    capabilities: SessionCapabilities,
793}
794
795impl<M> PreparedModel<M> {
796    /// Wraps a backend-prepared model.
797    pub const fn new(model: M, capabilities: SessionCapabilities) -> Self {
798        Self {
799            model,
800            capabilities,
801        }
802    }
803    /// Borrows the backend model.
804    pub const fn get(&self) -> &M {
805        &self.model
806    }
807    /// Mutably borrows the backend model.
808    pub fn get_mut(&mut self) -> &mut M {
809        &mut self.model
810    }
811    /// Returns the session capabilities admitted before materialization.
812    pub const fn capabilities(&self) -> SessionCapabilities {
813        self.capabilities
814    }
815    /// Consumes the marker.
816    pub fn into_inner(self) -> M {
817        self.model
818    }
819    /// Consumes the marker into the backend model and admitted capabilities.
820    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
839/// One backend selected for an entire prepared model and all its sessions.
840pub trait BackendProvider: Sized {
841    /// Portable model preparation request.
842    type ModelConfig;
843    /// Opaque backend model/executable.
844    type Model;
845    /// Opaque backend session/cache state and execution implementation.
846    type Session: BackendSession<Self>;
847    /// Backend error.
848    type Error: std::error::Error + Send + Sync + 'static;
849
850    /// Backend identity.
851    fn descriptor(&self) -> BackendDescriptor;
852    /// Discovers devices and their fail-closed capabilities.
853    fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error>;
854    /// Loads, compiles, or materializes a model for this backend.
855    fn prepare_model(
856        &self,
857        config: Self::ModelConfig,
858    ) -> Result<PreparedModel<Self::Model>, Self::Error>;
859    /// Consumes a prepared model into one backend-owned execution session.
860    ///
861    /// The executable and its mutable cache state have one owner after this
862    /// call. This prevents callers from pairing session state with a different
863    /// model or submitting the same mutable executable through two sessions.
864    fn create_session(
865        &self,
866        model: PreparedModel<Self::Model>,
867    ) -> Result<Self::Session, Self::Error>;
868
869    /// Constructs an internal backend error for an admission/realization mismatch.
870    ///
871    /// Backends whose prepared and realized reports cannot differ may leave
872    /// this unreachable default in place.
873    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
882/// Artifact-loading extension for a selected whole-model backend.
883///
884/// Core owns checkpoint inspection and preparation planning. Implementations
885/// translate the resulting neutral plan and their associated load options into
886/// the backend's concrete [`BackendProvider::ModelConfig`]. Tensor materialization
887/// remains entirely inside [`BackendProvider::prepare_model`].
888pub trait ModelLoadingBackend: BackendProvider {
889    /// Backend load policy exposed to a generic caller.
890    type LoadOptions;
891
892    /// Exact pre-materialization realization selected from the inspected
893    /// artifact, caller request, and backend mechanisms.
894    type SelectedPreparation;
895
896    /// Architecture registry selected by this backend adapter.
897    type ConfigurationResolver: ModelConfigurationResolver;
898
899    /// Returns the architecture-owned model configuration registry.
900    fn configuration_resolver(&self) -> &Self::ConfigurationResolver;
901
902    /// Intersects normalized architecture requirements and the caller request
903    /// with backend support, returning the sole construction-policy handoff.
904    ///
905    /// Core deliberately does not infer architecture capabilities from a
906    /// coarse model-family identity. Implementations must fail closed for
907    /// requested routes that the exact normalized architecture or backend
908    /// cannot realize.
909    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    /// Returns the exact portable admission retained by the authoritative selection.
918    fn selected_preparation_admission(
919        &self,
920        selected: &Self::SelectedPreparation,
921    ) -> PreparationAdmission;
922
923    /// Binds a neutral preparation plan and its authoritative selected
924    /// realization to backend-owned materialization input.
925    fn model_config(
926        &self,
927        selected: SelectedModelPreparation<Self>,
928    ) -> Result<Self::ModelConfig, Self::Error>;
929}
930
931/// One neutral preparation plan inseparably paired with its backend selection.
932///
933/// Core creates this value only after policy, architecture, and session-capability
934/// admission have succeeded against the same artifact inspection. Backend adapters
935/// may consume the pair, but callers cannot substitute a different plan after
936/// selection.
937pub 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    /// Consumes the binding into the exact admitted plan and backend selection.
962    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/// Failure while inspecting, planning, or materializing a model artifact.
975#[derive(Debug, thiserror::Error)]
976#[non_exhaustive]
977pub enum ModelLoadError<E: std::error::Error + Send + Sync + 'static> {
978    /// Portable artifact inspection or preparation planning failed.
979    #[error(transparent)]
980    Artifact(#[from] ArtifactError),
981    /// The selected backend failed policy resolution or materialization.
982    #[error("selected backend failed to prepare the model: {0}")]
983    Backend(#[source] E),
984    /// The inspected architecture/load-policy/topology route lacks a requirement.
985    #[error(transparent)]
986    SessionCapability(#[from] SessionCapabilityError),
987}
988
989/// Inspects, plans, and prepares one artifact on the selected backend.
990///
991/// This is the sole generic artifact-loading entry point. The backend instance
992/// already owns its device, execution queues, transfer queues, and optional
993/// distributed communication state; none are passed separately to loading.
994pub 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
1003/// Plans and prepares an artifact that the caller has already inspected.
1004///
1005/// This is the canonical lower-level entry point for facade loaders which
1006/// must derive tokenizer, chat, or other portable sidecar state from the same
1007/// inspection before transferring ownership to the selected backend.
1008pub 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
1023/// Materializes an already planned artifact with its retained backend selection.
1024///
1025/// Selection and capability admission must have completed before the backend
1026/// instance was realized. This entry point therefore performs no policy or
1027/// route selection after native resources exist.
1028pub(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
1040/// Prefill/decode interface for an already selected backend session.
1041///
1042/// The session owns its prepared executable and cache. The contract
1043/// intentionally models language-model submissions rather than primitive
1044/// tensor operations. Input, output, cache and completion stay opaque.
1045pub trait BackendSession<B: BackendProvider> {
1046    /// Backend-owned prefill input.
1047    type PrefillInput;
1048    /// Backend-owned decode input.
1049    type DecodeInput;
1050    /// Backend-owned logits/output.
1051    type Output;
1052    /// Exact completion type.
1053    type Completion: Completion<Error = B::Error>;
1054
1055    /// Reports capabilities of this exact realized session.
1056    fn capabilities(&self) -> SessionCapabilities;
1057
1058    /// Submits prompt prefill against this session.
1059    fn prefill(
1060        &mut self,
1061        backend: &B,
1062        input: Self::PrefillInput,
1063    ) -> Result<Submission<Self::Output, Self::Completion>, B::Error>;
1064
1065    /// Submits one or more cached decode positions against this session.
1066    fn decode(
1067        &mut self,
1068        backend: &B,
1069        input: Self::DecodeInput,
1070    ) -> Result<Submission<Self::Output, Self::Completion>, B::Error>;
1071
1072    /// Materializes an already completed opaque output into portable records.
1073    ///
1074    /// Calling this method is an explicit synchronization and host-transfer
1075    /// boundary. Ordinary inference does not invoke it.
1076    fn observe_output(
1077        &self,
1078        backend: &B,
1079        output: &Self::Output,
1080    ) -> Result<ObservationSet, B::Error>;
1081}
1082
1083/// Optional named-activation inspection for a selected backend session.
1084///
1085/// This is a general diagnostics and observability capability. Implementations
1086/// execute the requested operation to completion and materialize only selected
1087/// observation points. It is intentionally separate from ordinary asynchronous
1088/// submission so production inference pays no instrumentation cost.
1089pub trait InspectableBackendSession<B: BackendProvider>: BackendSession<B> {
1090    /// Executes and inspects one prompt prefill operation.
1091    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    /// Executes and inspects one cached decode operation.
1099    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
1107/// One submission produced by the selected backend session.
1108pub type SessionSubmission<B> = Submission<
1109    <<B as BackendProvider>::Session as BackendSession<B>>::Output,
1110    <<B as BackendProvider>::Session as BackendSession<B>>::Completion,
1111>;
1112
1113/// A prepared model, its selected backend, and its backend-owned session.
1114///
1115/// This is the canonical client-side execution owner. Keeping the backend and
1116/// session together makes backend selection a whole-model decision: generic
1117/// operations always supply this retained backend. Backend session hooks still
1118/// validate native identity when unrestricted session mutation is exposed.
1119/// Backend-owned executable, cache, tensor, and completion types remain
1120/// associated types and never enter the portable API.
1121pub 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    /// Prepares `config` and creates its sole execution session.
1130    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    /// Creates the sole execution session for an already prepared model.
1136    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    /// Returns the selected backend.
1179    pub const fn backend(&self) -> &B {
1180        &self.backend
1181    }
1182
1183    /// Returns the backend-owned session for optional backend capabilities.
1184    pub const fn session(&self) -> &B::Session {
1185        &self.session
1186    }
1187
1188    /// Returns the backend-owned session for optional backend capabilities.
1189    ///
1190    /// Unrestricted mutation can replace the session, so this invalidates any
1191    /// retained execution-plan target proof. Previously selected drafting can
1192    /// no longer attach to this runtime. Generic operations still enforce the
1193    /// original exact capability admission after mutation.
1194    pub fn session_mut(&mut self) -> &mut B::Session {
1195        self.execution_plan_target_id = None;
1196        &mut self.session
1197    }
1198
1199    /// Borrows the selected backend and its mutable session together.
1200    ///
1201    /// Like [`Self::session_mut`], this invalidates retained drafting target proof.
1202    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    /// Reports capabilities of the exact prepared model session.
1217    pub fn capabilities(&self) -> SessionCapabilities {
1218        self.session.capabilities()
1219    }
1220
1221    /// Submits prompt prefill through the selected backend and session.
1222    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    /// Submits cached decode through the selected backend and session.
1231    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    /// Materializes portable observations from an already completed output.
1240    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    /// Executes a completed, explicitly instrumented prefill operation.
1255    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    /// Executes a completed, explicitly instrumented decode operation.
1265    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    /// Loads an artifact and creates its sole session on `backend`.
1277    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/// Portable sampling inputs for one text-generation session.
1288#[derive(Debug, Clone, Copy, PartialEq)]
1289pub struct TextGenerationConfig {
1290    sampling: ResolvedGenerationConfig,
1291    seed: u64,
1292    strategy: TextSamplingStrategy,
1293}
1294
1295/// Backend-neutral token-sampling strategy for one text-generation session.
1296#[derive(Debug, Clone, Copy, Default, PartialEq)]
1297pub enum TextSamplingStrategy {
1298    /// Apply the resolved top-k, top-p, min-p, and penalty controls.
1299    #[default]
1300    Standard,
1301    /// Adapt the surprise cutoff toward `tau` bits at rate `eta`.
1302    MirostatV2 {
1303        /// Target surprise in bits.
1304        tau: f32,
1305        /// Adaptation rate.
1306        eta: f32,
1307    },
1308}
1309
1310impl TextGenerationConfig {
1311    /// Uses resolved checkpoint/request sampling with deterministic seed zero.
1312    pub const fn new(sampling: ResolvedGenerationConfig) -> Self {
1313        Self {
1314            sampling,
1315            seed: 0,
1316            strategy: TextSamplingStrategy::Standard,
1317        }
1318    }
1319
1320    /// Selects the deterministic root seed used by a stochastic backend.
1321    pub const fn with_seed(mut self, seed: u64) -> Self {
1322        self.seed = seed;
1323        self
1324    }
1325
1326    /// Selects adaptive Mirostat V2 sampling.
1327    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    /// Returns the validated sampling configuration.
1339    pub const fn sampling(&self) -> ResolvedGenerationConfig {
1340        self.sampling
1341    }
1342
1343    /// Returns the deterministic root seed.
1344    pub const fn seed(&self) -> u64 {
1345        self.seed
1346    }
1347
1348    /// Returns the selected backend-neutral sampling strategy.
1349    pub const fn strategy(&self) -> TextSamplingStrategy {
1350        self.strategy
1351    }
1352}
1353
1354/// Backend-owned generated token that exposes only its portable token id.
1355pub trait TokenOutput: Clone {
1356    /// Error produced while observing the token value.
1357    type Error: std::error::Error + Send + Sync + 'static;
1358
1359    /// Waits only as required to read this token's canonical vocabulary id.
1360    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/// Portable vocabulary filter applied before backend-owned sampling.
1372#[derive(Debug, Clone, Eq, PartialEq)]
1373pub enum TokenFilter {
1374    /// Every vocabulary token may be selected.
1375    All,
1376    /// One boolean per canonical vocabulary id; `true` permits selection.
1377    Allowed(Vec<bool>),
1378}
1379
1380impl TokenFilter {
1381    /// Validates an explicit canonical-vocabulary allow mask.
1382    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    /// Returns the explicit allow mask, or `None` when all tokens are allowed.
1393    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/// Invalid portable token-filter construction.
1402#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1403pub enum TokenFilterError {
1404    /// An explicit mask must describe a nonempty vocabulary.
1405    #[error("token filter vocabulary must not be empty")]
1406    EmptyVocabulary,
1407    /// Fail closed instead of asking a backend to sample an impossible row.
1408    #[error("token filter does not allow any vocabulary token")]
1409    NoAllowedToken,
1410}
1411
1412/// Backend-independent logical controller for constrained token selection.
1413pub trait TokenFilterController {
1414    /// Constraint or grammar error.
1415    type Error: std::error::Error + Send + Sync + 'static;
1416
1417    /// Returns the filter for the current durable logical prefix.
1418    fn current_filter(&mut self) -> Result<TokenFilter, Self::Error>;
1419
1420    /// Commits one backend-selected canonical vocabulary id.
1421    fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error>;
1422
1423    /// Returns whether the committed logical prefix satisfies the constraint.
1424    fn is_complete(&mut self) -> Result<bool, Self::Error>;
1425}
1426
1427/// Portable constraint controller that can evaluate speculative token histories.
1428///
1429/// Speculative backends use these queries for discardable draft branches. The
1430/// durable controller state must not change until [`TokenFilterController::commit_token`]
1431/// is called for a target-accepted token.
1432pub trait SpeculativeTokenFilterController: TokenFilterController + Clone {
1433    /// Returns the filter at `history` without committing its uncommitted suffix.
1434    ///
1435    /// `history` contains the controller's durable prefix followed by zero or
1436    /// more speculative tokens. Implementations must reject histories that do
1437    /// not begin with the durable prefix.
1438    fn filter_at(&self, history: &[u32]) -> Result<TokenFilter, Self::Error>;
1439
1440    /// Returns whether `history` completes the constraint without committing it.
1441    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
1463/// High-level text-generation extension implemented once per backend.
1464///
1465/// The contract deliberately combines model execution and sampling. Core does
1466/// not see logits or ask a backend to implement tensor primitives. The token,
1467/// sampling state, cache state, and exact completion remain backend-owned.
1468pub trait TextGenerationBackend: BackendProvider {
1469    /// Opaque prepared prompt, including any backend-owned multimodal values.
1470    type Prompt;
1471    /// Backend-owned generated token handle.
1472    type Token: TokenOutput<Error = Self::Error>;
1473    /// Backend-owned sampler and randomness state for one sequence.
1474    type TextGenerationState;
1475    /// Exact completion retaining model execution and token sampling.
1476    type TextCompletion: Completion<Error = Self::Error>;
1477
1478    /// Creates backend sampling state for one sequence.
1479    fn start_text_generation(
1480        backend: &Self,
1481        config: TextGenerationConfig,
1482    ) -> Result<Self::TextGenerationState, Self::Error>;
1483
1484    /// Converts portable tokenizer ids into a backend-owned text prompt.
1485    fn prepare_text_prompt(
1486        backend: &Self,
1487        prompt_token_ids: Vec<u32>,
1488    ) -> Result<Self::Prompt, Self::Error>;
1489
1490    /// Submits prompt prefill followed by sampling one token.
1491    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    /// Submits cached decode from the preceding token and samples its successor.
1499    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/// Failure from backend preprocessing or backend-requested text encoding.
1508#[derive(Debug, thiserror::Error)]
1509pub enum MultimodalPreparationFailure<B, T>
1510where
1511    B: std::error::Error + 'static,
1512    T: std::error::Error + 'static,
1513{
1514    /// The selected backend rejected or failed media preprocessing.
1515    #[error("backend multimodal preparation failed: {0}")]
1516    Backend(#[source] B),
1517    /// The facade tokenizer failed on backend-required framing text.
1518    #[error("multimodal framing text encoding failed: {0}")]
1519    Text(#[source] T),
1520}
1521
1522/// Backend preparation of portable decoded media for one selected session.
1523///
1524/// Caller text is tokenized before this boundary. Some processors introduce
1525/// checkpoint-defined framing text or video timestamps, so the callback keeps
1526/// that work on the facade's tokenizer. The selected backend returns its
1527/// existing opaque prompt type; core never observes tensors or streams.
1528pub trait MultimodalPreparationBackend: TextGenerationBackend {
1529    /// Converts ordered token and decoded-media segments into a backend prompt.
1530    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
1539/// Capability and resource observations for one selected text-model session.
1540///
1541/// Implementations inspect the opaque model and cache already owned by
1542/// [`ModelRuntime`]. The contract exposes only portable documents and the
1543/// backend's existing opaque prompt type; tensor, stream, allocator, and
1544/// executable types remain inside the adapter.
1545pub trait ModelCapabilityBackend: TextGenerationBackend {
1546    /// Reports validated model capabilities for the selected session.
1547    fn model_capabilities(
1548        runtime: &ModelRuntime<Self>,
1549    ) -> Result<ModelCapabilities, CapabilityError>;
1550
1551    /// Counts text and backend-specific model positions in a prepared prompt.
1552    fn count_prepared_input(
1553        runtime: &ModelRuntime<Self>,
1554        input: &Self::Prompt,
1555    ) -> Result<InputTokenCount, CapabilityError>;
1556
1557    /// Estimates persistent and transient request state for this session.
1558    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    /// Reports static model storage and current backend memory observations.
1566    fn static_memory(runtime: &ModelRuntime<Self>) -> Result<StaticMemoryReport, CapabilityError>;
1567}
1568
1569enum TextGenerationStep<P, T> {
1570    Prefill(P),
1571    Decode(T),
1572}
1573
1574/// Failure from either backend execution or portable constraint control.
1575#[derive(Debug, thiserror::Error)]
1576pub enum ControlledTextGenerationError<B, C>
1577where
1578    B: std::error::Error + 'static,
1579    C: std::error::Error + 'static,
1580{
1581    /// Backend preparation, execution, sampling, or completion failed.
1582    #[error("backend text generation failed: {0}")]
1583    Backend(#[source] B),
1584    /// Portable constraint filtering or commitment failed.
1585    #[error("text generation constraint failed: {0}")]
1586    Controller(#[source] C),
1587}
1588
1589/// One constraint-committed token and its backend-owned output handle.
1590#[derive(Debug, Clone)]
1591pub struct ControlledToken<T> {
1592    output: T,
1593    token_id: u32,
1594}
1595
1596impl<T> ControlledToken<T> {
1597    /// Returns the committed canonical vocabulary id.
1598    pub fn token_id(&self) -> u32 {
1599        self.token_id
1600    }
1601
1602    /// Borrows the backend-owned token handle.
1603    pub const fn output(&self) -> &T {
1604        &self.output
1605    }
1606
1607    /// Consumes the committed token into its backend-owned handle.
1608    pub fn into_output(self) -> T {
1609        self.output
1610    }
1611}
1612
1613/// Backend-generic generation driven by a portable token-filter controller.
1614pub 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    /// Starts controlled generation from portable prompt token ids.
1649    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    /// Starts controlled generation from an opaque backend-prepared prompt.
1661    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    /// Mutably borrows the canonical constraint state.
1671    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
1825/// Backend-generic asynchronous token-generation iterator.
1826///
1827/// Every yielded token handle may be fed into the following decode without
1828/// first reading its id on the host. The preceding completion resolves before
1829/// that state-mutating decode is submitted, and dropping the iterator waits
1830/// for every still-retained submission.
1831pub struct TextGeneration<'a, B: TextGenerationBackend> {
1832    inner: TextGenerationMachine<'a, B, UnconstrainedTokens>,
1833}
1834
1835impl<'a, B: TextGenerationBackend> TextGeneration<'a, B> {
1836    /// Starts generation from portable prompt token ids.
1837    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    /// Starts generation from an opaque backend-prepared prompt.
1847    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
1880/// Optional high-level transfer and collective capability of a selected session.
1881///
1882/// This contract deliberately operates on an opaque backend value. It models
1883/// the few communication submissions needed by model execution without making
1884/// core define a tensor algebra or exposing native groups, streams, or events.
1885/// Every operation is scoped to the session selected for the complete model.
1886pub trait DistributedSession {
1887    /// Backend-owned tensor or buffer value.
1888    type Value;
1889    /// Exact completion retaining the submitted communication resources.
1890    type Completion: Completion<Error = Self::Error>;
1891    /// Structured backend error.
1892    type Error: std::error::Error + Send + Sync + 'static;
1893
1894    /// Stable topology and rank identity.
1895    fn descriptor(&self) -> DistributedSessionDescriptor;
1896    /// Fail-closed communication support.
1897    fn capabilities(&self) -> DistributedCapabilities;
1898
1899    /// Submits a sum reduction over `scope`.
1900    fn all_reduce_sum(
1901        &self,
1902        scope: CollectiveScope,
1903        input: &Self::Value,
1904    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1905
1906    /// Submits a leading-rank-axis gather over `scope`.
1907    fn all_gather(
1908        &self,
1909        scope: CollectiveScope,
1910        input: &Self::Value,
1911    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1912
1913    /// Submits a variable-count all-to-all exchange over `scope`.
1914    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    /// Submits a point-to-point send to a rank within `scope`.
1923    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    /// Submits a point-to-point receive from a rank within `scope`.
1931    fn receive(
1932        &self,
1933        scope: CollectiveScope,
1934        peer: usize,
1935        value: &ValueDescriptor,
1936    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1937
1938    /// Synchronously gathers portable scheduler metadata across the world.
1939    fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error>;
1940}
1941
1942/// Backend extension exposing communication attached to a model session.
1943pub trait DistributedBackend: BackendProvider {
1944    /// Selected distributed session implementation.
1945    type DistributedSession: DistributedSession<Error = Self::Error>;
1946
1947    /// Returns communication for a distributed model session.
1948    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}