Skip to main content

eredu_core/
backend.rs

1//! High-level contract implemented once per execution backend.
2
3mod continuation;
4pub use continuation::{
5    TextContinuationBoundary, TextContinuationError, TextContinuationIdentity, TextDriverIdentity,
6    TextGenerationContinuation, TextGenerationDriver,
7};
8
9use serde::{Deserialize, Serialize};
10use std::{fmt::Debug, path::Path};
11
12use crate::{
13    artifact::{
14        inspect_artifact, ArtifactError, ArtifactInspection, ModelConfigurationResolver,
15        ModelPreparationPlan,
16    },
17    capability::{
18        CapabilityError, InputTokenCount, ModelCapabilities, RuntimeStateEstimate,
19        StaticMemoryReport,
20    },
21    checkpoint::TensorDtype,
22    generation::{GenerationError, ResolvedGenerationConfig},
23    media::TokenizedMultimodalRequest,
24    observation::{InspectedOutput, ObservationRequest, ObservationSet},
25    PreparationAdmission,
26};
27
28/// Stable, extensible description of an execution backend.
29#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
30pub struct BackendDescriptor {
31    /// Backend implementation name, such as `example-backend`.
32    name: String,
33    /// Backend implementation version.
34    version: String,
35}
36
37impl BackendDescriptor {
38    /// Creates a backend identity without freezing future descriptor fields.
39    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
40        Self {
41            name: name.into(),
42            version: version.into(),
43        }
44    }
45
46    /// Returns the backend implementation name.
47    pub fn name(&self) -> &str {
48        &self.name
49    }
50
51    /// Returns the backend implementation version.
52    pub fn version(&self) -> &str {
53        &self.version
54    }
55}
56
57/// Portable description of one backend-visible device.
58#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
59pub struct DeviceDescriptor {
60    /// Backend-stable device identifier.
61    id: String,
62    /// Human-readable device name.
63    name: String,
64    /// Backend-specific device family without a closed core enum.
65    family: String,
66    /// Total memory when discoverable.
67    memory_bytes: Option<u64>,
68}
69
70impl DeviceDescriptor {
71    /// Creates a backend-stable device description.
72    pub fn new(
73        id: impl Into<String>,
74        name: impl Into<String>,
75        family: impl Into<String>,
76        memory_bytes: Option<u64>,
77    ) -> Self {
78        Self {
79            id: id.into(),
80            name: name.into(),
81            family: family.into(),
82            memory_bytes,
83        }
84    }
85
86    /// Returns the backend-stable device identifier.
87    pub fn id(&self) -> &str {
88        &self.id
89    }
90    /// Returns the human-readable device name.
91    pub fn name(&self) -> &str {
92        &self.name
93    }
94    /// Returns the backend-defined device family.
95    pub fn family(&self) -> &str {
96        &self.family
97    }
98    /// Returns total device memory when known.
99    pub const fn memory_bytes(&self) -> Option<u64> {
100        self.memory_bytes
101    }
102}
103
104/// Fail-closed capabilities discovered from a backend and device.
105#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
106pub struct DeviceCapabilities {
107    /// Supports exact completion observation for submissions.
108    exact_completion: bool,
109    /// Supports device-to-device transfer for backend-owned values.
110    transfers: bool,
111    /// Supports collective execution for a complete session.
112    collectives: bool,
113}
114
115impl DeviceCapabilities {
116    /// Creates an exact fail-closed device mechanism report.
117    pub const fn new(exact_completion: bool, transfers: bool, collectives: bool) -> Self {
118        Self {
119            exact_completion,
120            transfers,
121            collectives,
122        }
123    }
124
125    /// Returns whether exact completion observation is available.
126    pub const fn exact_completion(&self) -> bool {
127        self.exact_completion
128    }
129    /// Returns whether device transfers are available.
130    pub const fn transfers(&self) -> bool {
131        self.transfers
132    }
133    /// Returns whether collective execution is available.
134    pub const fn collectives(&self) -> bool {
135        self.collectives
136    }
137}
138
139/// Fail-closed capabilities of one exact prepared model session.
140#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
141pub struct SessionCapabilities {
142    /// Supports backend-managed persistent decode caches.
143    persistent_cache: bool,
144    /// Supports explicit host observation of completed session outputs.
145    output_observation: bool,
146    /// Supports named activation inspection for instrumented session passes.
147    activation_inspection: bool,
148}
149
150impl SessionCapabilities {
151    /// Creates an exact fail-closed session mechanism report.
152    pub const fn new(
153        persistent_cache: bool,
154        output_observation: bool,
155        activation_inspection: bool,
156    ) -> Self {
157        Self {
158            persistent_cache,
159            output_observation,
160            activation_inspection,
161        }
162    }
163
164    /// Returns whether persistent cache storage is available.
165    pub const fn persistent_cache(self) -> bool {
166        self.persistent_cache
167    }
168    /// Returns whether completed outputs may be observed on the host.
169    pub const fn output_observation(self) -> bool {
170        self.output_observation
171    }
172    /// Returns whether named activation inspection is available.
173    pub const fn activation_inspection(self) -> bool {
174        self.activation_inspection
175    }
176
177    /// Returns a report with persistent cache storage configured.
178    pub const fn with_persistent_cache(mut self, supported: bool) -> Self {
179        self.persistent_cache = supported;
180        self
181    }
182    /// Returns a report with output observation configured.
183    pub const fn with_output_observation(mut self, supported: bool) -> Self {
184        self.output_observation = supported;
185        self
186    }
187    /// Returns a report with activation inspection configured.
188    pub const fn with_activation_inspection(mut self, supported: bool) -> Self {
189        self.activation_inspection = supported;
190        self
191    }
192    /// Validates fail-closed requirements against an exact available report.
193    pub fn validate(&self, available: &Self) -> Result<(), SessionCapabilityError> {
194        for (required, supported, capability) in [
195            (
196                self.persistent_cache,
197                available.persistent_cache,
198                "persistent_cache",
199            ),
200            (
201                self.output_observation,
202                available.output_observation,
203                "output_observation",
204            ),
205            (
206                self.activation_inspection,
207                available.activation_inspection,
208                "activation_inspection",
209            ),
210        ] {
211            if required && !supported {
212                return Err(SessionCapabilityError { capability });
213            }
214        }
215        Ok(())
216    }
217}
218
219/// One unavailable exact-session requirement.
220#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
221#[error("prepared session does not support required capability {capability}")]
222pub struct SessionCapabilityError {
223    capability: &'static str,
224}
225
226impl SessionCapabilityError {
227    /// Returns the stable capability name.
228    pub const fn capability(self) -> &'static str {
229        self.capability
230    }
231}
232
233/// Fail-closed distributed operations exposed by one selected session.
234#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
235pub struct DistributedCapabilities {
236    world_collectives: bool,
237    collective_groups: Vec<CollectiveGroupId>,
238    point_to_point: bool,
239    variable_all_to_all: bool,
240    exact_completion: bool,
241}
242
243impl DistributedCapabilities {
244    /// Creates an exact mechanism capability report.
245    pub fn new(
246        world_collectives: bool,
247        collective_groups: impl IntoIterator<Item = CollectiveGroupId>,
248        point_to_point: bool,
249        variable_all_to_all: bool,
250        exact_completion: bool,
251    ) -> Self {
252        Self {
253            world_collectives,
254            collective_groups: collective_groups.into_iter().collect(),
255            point_to_point,
256            variable_all_to_all,
257            exact_completion,
258        }
259    }
260
261    /// Returns whether world-scoped collectives are available.
262    pub const fn world_collectives(&self) -> bool {
263        self.world_collectives
264    }
265    /// Returns opaque groups supporting collectives.
266    pub fn collective_groups(&self) -> &[CollectiveGroupId] {
267        &self.collective_groups
268    }
269    /// Returns whether point-to-point transfers are available.
270    pub const fn point_to_point(&self) -> bool {
271        self.point_to_point
272    }
273    /// Returns whether variable-count all-to-all is available.
274    pub const fn variable_all_to_all(&self) -> bool {
275        self.variable_all_to_all
276    }
277    /// Returns whether submissions have exact completion objects.
278    pub const fn exact_completion(&self) -> bool {
279        self.exact_completion
280    }
281}
282
283/// Opaque stable identity of a selected collective group.
284#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
285#[serde(transparent)]
286pub struct CollectiveGroupId(u32);
287
288impl CollectiveGroupId {
289    /// Creates an opaque group identity selected by architecture/runtime composition.
290    pub const fn new(value: u32) -> Self {
291        Self(value)
292    }
293    /// Returns the stable numeric representation for serialization and backend maps.
294    pub const fn value(self) -> u32 {
295        self.0
296    }
297}
298
299/// Ordered membership for one opaque collective group containing this rank.
300#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
301pub struct CollectiveGroupDescriptor {
302    id: CollectiveGroupId,
303    members: Vec<usize>,
304    local_rank: usize,
305}
306
307impl CollectiveGroupDescriptor {
308    /// Validates ordered group membership and the process-local rank.
309    pub fn new(
310        id: CollectiveGroupId,
311        members: Vec<usize>,
312        local_rank: usize,
313    ) -> Result<Self, BackendError> {
314        if members.is_empty() || local_rank >= members.len() {
315            return Err(BackendError::Preparation {
316                operation: "collective group realization".into(),
317                message: "collective membership must be non-empty and contain local rank".into(),
318            });
319        }
320        let mut unique = std::collections::BTreeSet::new();
321        if !members.iter().all(|rank| unique.insert(*rank)) {
322            return Err(BackendError::Preparation {
323                operation: "collective group realization".into(),
324                message: "collective membership contains duplicate world ranks".into(),
325            });
326        }
327        Ok(Self {
328            id,
329            members,
330            local_rank,
331        })
332    }
333
334    /// Returns the opaque group identity.
335    pub const fn id(&self) -> CollectiveGroupId {
336        self.id
337    }
338    /// Returns ordered world-rank membership.
339    pub fn members(&self) -> &[usize] {
340        &self.members
341    }
342    /// Returns this process's rank within the ordered group.
343    pub const fn local_rank(&self) -> usize {
344        self.local_rank
345    }
346}
347
348impl<'de> Deserialize<'de> for CollectiveGroupDescriptor {
349    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
350    where
351        D: serde::Deserializer<'de>,
352    {
353        #[derive(Deserialize)]
354        struct Raw {
355            id: CollectiveGroupId,
356            members: Vec<usize>,
357            local_rank: usize,
358        }
359        let raw = Raw::deserialize(deserializer)?;
360        Self::new(raw.id, raw.members, raw.local_rank).map_err(serde::de::Error::custom)
361    }
362}
363
364/// Scope of a collective or point-to-point operation.
365#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
366#[serde(tag = "kind", content = "group", rename_all = "snake_case")]
367#[non_exhaustive]
368pub enum CollectiveScope {
369    /// All ranks in the selected distributed session.
370    World,
371    /// One opaque selected collective group containing this rank.
372    Group(CollectiveGroupId),
373}
374
375/// Portable shape and element type for a backend-owned received value.
376#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
377pub struct ValueDescriptor {
378    /// Row-major logical shape. An empty shape describes a scalar.
379    shape: Vec<usize>,
380    /// Logical element type.
381    dtype: TensorDtype,
382}
383
384impl ValueDescriptor {
385    /// Validates a portable value shape and element type.
386    pub fn new(shape: Vec<usize>, dtype: TensorDtype) -> Result<Self, BackendError> {
387        if shape.contains(&0) {
388            return Err(BackendError::Preparation {
389                operation: "distributed value descriptor".into(),
390                message: "non-scalar distributed values require positive dimensions".into(),
391            });
392        }
393        Ok(Self { shape, dtype })
394    }
395
396    /// Returns the row-major logical shape.
397    pub fn shape(&self) -> &[usize] {
398        &self.shape
399    }
400
401    /// Returns the logical element type.
402    pub const fn dtype(&self) -> &TensorDtype {
403        &self.dtype
404    }
405}
406
407impl<'de> Deserialize<'de> for ValueDescriptor {
408    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
409    where
410        D: serde::Deserializer<'de>,
411    {
412        #[derive(Deserialize)]
413        struct RawDescriptor {
414            shape: Vec<usize>,
415            dtype: TensorDtype,
416        }
417
418        let raw = RawDescriptor::deserialize(deserializer)?;
419        Self::new(raw.shape, raw.dtype).map_err(serde::de::Error::custom)
420    }
421}
422
423/// Portable identity of one selected distributed session.
424#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
425pub struct DistributedSessionDescriptor {
426    world_size: usize,
427    rank: usize,
428    groups: Vec<CollectiveGroupDescriptor>,
429}
430
431impl DistributedSessionDescriptor {
432    /// Validates a mechanism-only distributed session realization.
433    pub fn new(
434        world_size: usize,
435        rank: usize,
436        groups: Vec<CollectiveGroupDescriptor>,
437    ) -> Result<Self, BackendError> {
438        if world_size == 0 || rank >= world_size {
439            return Err(BackendError::Preparation {
440                operation: "distributed session realization".into(),
441                message: format!("rank {rank} is outside world size {world_size}"),
442            });
443        }
444        let mut ids = std::collections::BTreeSet::new();
445        for group in &groups {
446            if !ids.insert(group.id())
447                || group.members().iter().any(|member| *member >= world_size)
448                || group.members()[group.local_rank()] != rank
449            {
450                return Err(BackendError::Preparation {
451                    operation: "distributed session realization".into(),
452                    message: "collective groups must have unique IDs, in-range members, and the declared local world rank".into(),
453                });
454            }
455        }
456        Ok(Self {
457            world_size,
458            rank,
459            groups,
460        })
461    }
462
463    /// Returns the total process count.
464    pub const fn world_size(&self) -> usize {
465        self.world_size
466    }
467    /// Returns this process's world rank.
468    pub const fn rank(&self) -> usize {
469        self.rank
470    }
471    /// Returns ordered opaque group realizations.
472    pub fn groups(&self) -> &[CollectiveGroupDescriptor] {
473        &self.groups
474    }
475}
476
477impl<'de> Deserialize<'de> for DistributedSessionDescriptor {
478    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
479    where
480        D: serde::Deserializer<'de>,
481    {
482        #[derive(Deserialize)]
483        struct RawDescriptor {
484            world_size: usize,
485            rank: usize,
486            groups: Vec<CollectiveGroupDescriptor>,
487        }
488
489        let raw = RawDescriptor::deserialize(deserializer)?;
490        Self::new(raw.world_size, raw.rank, raw.groups).map_err(serde::de::Error::custom)
491    }
492}
493
494/// Structured backend failure that does not expose a runtime exception type.
495#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
496#[non_exhaustive]
497pub enum BackendError {
498    /// A required capability is absent.
499    #[error("backend {backend} does not support required capability {capability}")]
500    Unsupported {
501        /// Backend implementation name.
502        backend: String,
503        /// Required capability.
504        capability: String,
505    },
506    /// Model preparation failed.
507    #[error("backend model preparation failed during {operation}: {message}")]
508    Preparation {
509        /// Preparation operation.
510        operation: String,
511        /// Backend-provided context.
512        message: String,
513    },
514    /// Session execution failed.
515    #[error("backend session {session} failed during {operation}: {message}")]
516    Execution {
517        /// Stable session identifier.
518        session: String,
519        /// Operation being executed.
520        operation: String,
521        /// Backend-provided context.
522        message: String,
523    },
524    /// Exact completion observation failed.
525    #[error("backend completion observation failed: {message}")]
526    Completion {
527        /// Backend-provided context.
528        message: String,
529    },
530}
531
532/// Exact completion owned by one backend submission.
533pub trait Completion {
534    /// Error produced while observing the completion.
535    type Error: std::error::Error + Send + Sync + 'static;
536
537    /// Nonblocking exact-completion observation.
538    fn is_complete(&self) -> Result<bool, Self::Error>;
539
540    /// Blocks on this exact completion only.
541    /// An error reports the outcome, not whether native resource use has stopped.
542    fn wait(&self) -> Result<(), Self::Error>;
543
544    /// Nonblocking proof that submitted work and its consumers no longer borrow
545    /// retained resources. This is independent of successful output publication:
546    /// a failed submission may become releasable while its error remains sticky.
547    ///
548    /// `false` includes pending work, unavailable evidence, and runtime contention.
549    /// An observation error, timeout, or cancellation request is never proof of
550    /// release. Implementations must include outstanding child/observation work
551    /// and must not wait, retry failed execution, or run application destructors.
552    /// Readiness does not make a failed session reusable or validate its outputs.
553    /// Backends still own retention when the public completion is dropped.
554    ///
555    /// The default recognizes successful exact completion only. Backends that
556    /// can establish safe release after failure should override this method.
557    fn resources_releasable(&self) -> bool {
558        matches!(self.is_complete(), Ok(true))
559    }
560}
561
562/// Mechanism selected for work which has not completed by a bounded deadline.
563#[derive(
564    Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Serialize, serde::Deserialize,
565)]
566pub enum CompletionCancellationMode {
567    /// The backend can request native cancellation and safely complete its teardown.
568    NativeCancel,
569    /// Native cancellation is unavailable, so the backend retains the orphaned work
570    /// and every borrowed resource until exact completion can be observed.
571    QuarantineUntilComplete,
572}
573
574/// Monotonic identity of one distributed session transaction.
575#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
576#[serde(transparent)]
577pub struct DistributedCommitEpoch(u64);
578
579impl DistributedCommitEpoch {
580    /// First epoch allocated by a newly constructed session.
581    pub const FIRST: Self = Self(1);
582
583    /// Creates a positive durable epoch identity.
584    pub const fn new(value: u64) -> Option<Self> {
585        if value == 0 {
586            None
587        } else {
588            Some(Self(value))
589        }
590    }
591
592    /// Stable serialized epoch value.
593    pub const fn value(self) -> u64 {
594        self.0
595    }
596
597    /// Returns the next representable session epoch.
598    pub const fn next(self) -> Option<Self> {
599        match self.0.checked_add(1) {
600            Some(value) => Some(Self(value)),
601            None => None,
602        }
603    }
604}
605
606impl<'de> Deserialize<'de> for DistributedCommitEpoch {
607    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
608    where
609        D: serde::Deserializer<'de>,
610    {
611        let value = u64::deserialize(deserializer)?;
612        Self::new(value).ok_or_else(|| serde::de::Error::custom("commit epoch must be positive"))
613    }
614}
615
616/// Final decision cut reached by a distributed commit attempt.
617#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
618#[serde(rename_all = "snake_case")]
619pub enum DistributedCommitPhase {
620    /// The rank could not submit its final decision contribution.
621    DecisionSubmission,
622    /// The submitted final decision did not complete within its exact contract.
623    DecisionCompletion,
624    /// The rank observed the globally fixed decision.
625    DecisionObservation,
626}
627
628/// Honest rank-local observation of one globally identified commit attempt.
629#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
630#[serde(rename_all = "snake_case")]
631pub enum DistributedCommitOutcome {
632    /// This rank observed the globally fixed all-success decision.
633    Committed(DistributedCommitEpoch),
634    /// This rank observed the globally fixed abort decision before publication became final.
635    Aborted(DistributedCommitEpoch),
636    /// The rank may have contributed, but could not observe the fixed final decision.
637    Indeterminate {
638        /// Transaction identity retained for recovery and diagnosis.
639        epoch: DistributedCommitEpoch,
640        /// Exact final decision cut whose outcome could not be observed.
641        phase: DistributedCommitPhase,
642    },
643}
644
645impl DistributedCommitOutcome {
646    /// Transaction identity shared by every outcome variant.
647    pub const fn epoch(self) -> DistributedCommitEpoch {
648        match self {
649            Self::Committed(epoch) | Self::Aborted(epoch) | Self::Indeterminate { epoch, .. } => {
650                epoch
651            }
652        }
653    }
654
655    /// Whether further use requires external recovery of the named epoch.
656    pub const fn is_indeterminate(self) -> bool {
657        matches!(self, Self::Indeterminate { .. })
658    }
659}
660
661/// One explicit bounded-wait policy selected before a backend submission is created.
662#[derive(Debug, Clone, Copy, Eq, PartialEq)]
663pub struct BoundedCompletionWait {
664    timeout: std::time::Duration,
665    cancellation: CompletionCancellationMode,
666}
667
668impl BoundedCompletionWait {
669    /// Creates a positive relative deadline and its required safe cancellation mode.
670    pub fn new(
671        timeout: std::time::Duration,
672        cancellation: CompletionCancellationMode,
673    ) -> Result<Self, BoundedCompletionWaitError> {
674        if timeout.is_zero() {
675            return Err(BoundedCompletionWaitError::ZeroTimeout);
676        }
677        Ok(Self {
678            timeout,
679            cancellation,
680        })
681    }
682
683    /// Maximum time spent waiting for exact completion.
684    pub const fn timeout(self) -> std::time::Duration {
685        self.timeout
686    }
687
688    /// Required safe disposition for work still live at the deadline.
689    pub const fn cancellation(self) -> CompletionCancellationMode {
690        self.cancellation
691    }
692}
693
694/// Invalid bounded-completion policy.
695#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
696pub enum BoundedCompletionWaitError {
697    /// A zero duration cannot establish a meaningful completion deadline.
698    #[error("bounded completion timeout must be positive")]
699    ZeroTimeout,
700}
701
702/// Stable result of observing one completion through an explicit bounded policy.
703#[derive(Debug, Clone, Copy, Eq, PartialEq)]
704pub enum BoundedCompletionOutcome {
705    /// Exact backend completion was observed before the deadline.
706    Completed,
707    /// The deadline expired and the backend safely applied the selected disposition.
708    DeadlineExceeded {
709        /// Cancellation or retained-orphan mechanism actually applied.
710        cancellation: CompletionCancellationMode,
711    },
712}
713
714/// Completion which can enforce a selected deadline without dropping live resources.
715///
716/// Implementations must consume themselves on timeout. `NativeCancel` means native
717/// cancellation and teardown have completed before returning. `QuarantineUntilComplete`
718/// means the implementation has transferred ownership of the completion and all work
719/// resources to a safe owner which will release them only after exact completion. An
720/// `Err` return has the same ownership obligation: it may represent a terminally
721/// completed backend failure, or it must first transfer still-live work to the selected
722/// safe disposition. Implementations must never return an error by merely dropping live
723/// resources.
724pub trait BoundedCompletion: Completion + Sized {
725    /// Whether this completion implementation can honor the selected disposition.
726    /// Implementations with a restricted native mechanism set must override this
727    /// so schedulers can reject unsupported policy before submission.
728    fn supports_cancellation(_cancellation: CompletionCancellationMode) -> bool {
729        true
730    }
731
732    /// Observes exact completion until the policy deadline, then performs its selected
733    /// safe cancellation disposition.
734    fn wait_bounded(
735        self,
736        policy: BoundedCompletionWait,
737    ) -> Result<BoundedCompletionOutcome, Self::Error>;
738}
739
740/// Output and exact completion returned by a backend submission.
741#[derive(Debug)]
742pub struct Submission<T, C> {
743    /// Backend-owned output value.
744    pub output: T,
745    /// Completion retaining everything needed by the submitted work.
746    pub completion: C,
747}
748
749impl<T, C> Submission<T, C>
750where
751    C: Completion,
752{
753    /// Waits for this exact submission and returns its output.
754    pub fn wait(self) -> Result<T, C::Error> {
755        self.completion.wait()?;
756        Ok(self.output)
757    }
758}
759
760impl<T, C> Submission<T, C>
761where
762    C: BoundedCompletion,
763{
764    /// Waits through the selected bounded policy and returns output only after exact
765    /// completion. Timed-out output is dropped only after its completion has transferred
766    /// ownership of every live backend resource to its safe cancellation owner.
767    pub fn wait_bounded(
768        self,
769        policy: BoundedCompletionWait,
770    ) -> Result<BoundedSubmissionOutcome<T>, C::Error> {
771        match self.completion.wait_bounded(policy)? {
772            BoundedCompletionOutcome::Completed => {
773                Ok(BoundedSubmissionOutcome::Completed(self.output))
774            }
775            BoundedCompletionOutcome::DeadlineExceeded { cancellation } => {
776                Ok(BoundedSubmissionOutcome::DeadlineExceeded { cancellation })
777            }
778        }
779    }
780}
781
782/// Result of a bounded submission wait. Output is authoritative only on completion.
783#[derive(Debug, Eq, PartialEq)]
784pub enum BoundedSubmissionOutcome<T> {
785    /// Exact completion was observed and the output may be consumed.
786    Completed(T),
787    /// The deadline expired and live work entered its selected safe disposition.
788    DeadlineExceeded {
789        /// Cancellation or retained-orphan mechanism actually applied.
790        cancellation: CompletionCancellationMode,
791    },
792}
793
794/// Marker wrapper proving that a model was prepared by a backend.
795#[derive(Debug)]
796pub struct PreparedModel<M> {
797    model: M,
798    capabilities: SessionCapabilities,
799}
800
801impl<M> PreparedModel<M> {
802    /// Wraps a backend-prepared model.
803    pub const fn new(model: M, capabilities: SessionCapabilities) -> Self {
804        Self {
805            model,
806            capabilities,
807        }
808    }
809    /// Borrows the backend model.
810    pub const fn get(&self) -> &M {
811        &self.model
812    }
813    /// Mutably borrows the backend model.
814    pub fn get_mut(&mut self) -> &mut M {
815        &mut self.model
816    }
817    /// Returns the session capabilities admitted before materialization.
818    pub const fn capabilities(&self) -> SessionCapabilities {
819        self.capabilities
820    }
821    /// Consumes the marker.
822    pub fn into_inner(self) -> M {
823        self.model
824    }
825    /// Consumes the marker into the backend model and admitted capabilities.
826    pub fn into_parts(self) -> (M, SessionCapabilities) {
827        (self.model, self.capabilities)
828    }
829}
830
831impl<M> std::ops::Deref for PreparedModel<M> {
832    type Target = M;
833
834    fn deref(&self) -> &Self::Target {
835        self.get()
836    }
837}
838
839impl<M> std::ops::DerefMut for PreparedModel<M> {
840    fn deref_mut(&mut self) -> &mut Self::Target {
841        self.get_mut()
842    }
843}
844
845/// One backend selected for an entire prepared model and all its sessions.
846pub trait BackendProvider: Sized {
847    /// Portable model preparation request.
848    type ModelConfig;
849    /// Opaque backend model/executable.
850    type Model;
851    /// Opaque backend session/cache state and execution implementation.
852    type Session: BackendSession<Self>;
853    /// Backend error.
854    type Error: std::error::Error + Send + Sync + 'static;
855
856    /// Backend identity.
857    fn descriptor(&self) -> BackendDescriptor;
858    /// Discovers devices and their fail-closed capabilities.
859    fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error>;
860    /// Loads, compiles, or materializes a model for this backend.
861    fn prepare_model(
862        &self,
863        config: Self::ModelConfig,
864    ) -> Result<PreparedModel<Self::Model>, Self::Error>;
865    /// Consumes a prepared model into one backend-owned execution session.
866    ///
867    /// The executable and its mutable cache state have one owner after this
868    /// call. This prevents callers from pairing session state with a different
869    /// model or submitting the same mutable executable through two sessions.
870    fn create_session(
871        &self,
872        model: PreparedModel<Self::Model>,
873    ) -> Result<Self::Session, Self::Error>;
874
875    /// Constructs an internal backend error for an admission/realization mismatch.
876    ///
877    /// Backends whose prepared and realized reports cannot differ may leave
878    /// this unreachable default in place.
879    fn session_capability_mismatch(
880        &self,
881        admitted: SessionCapabilities,
882        realized: SessionCapabilities,
883    ) -> Self::Error {
884        panic!("backend realized session capabilities {realized:?} after admitting {admitted:?}")
885    }
886}
887
888/// Artifact-loading extension for a selected whole-model backend.
889///
890/// Core owns checkpoint inspection and preparation planning. Implementations
891/// translate the resulting neutral plan and their associated load options into
892/// the backend's concrete [`BackendProvider::ModelConfig`]. Tensor materialization
893/// remains entirely inside [`BackendProvider::prepare_model`].
894pub trait ModelLoadingBackend: BackendProvider {
895    /// Backend load policy exposed to a generic caller.
896    type LoadOptions;
897
898    /// Exact pre-materialization realization selected from the inspected
899    /// artifact, caller request, and backend mechanisms.
900    type SelectedPreparation;
901
902    /// Architecture registry selected by this backend adapter.
903    type ConfigurationResolver: ModelConfigurationResolver;
904
905    /// Returns the architecture-owned model configuration registry.
906    fn configuration_resolver(&self) -> &Self::ConfigurationResolver;
907
908    /// Intersects normalized architecture requirements and the caller request
909    /// with backend support, returning the sole construction-policy handoff.
910    ///
911    /// Core deliberately does not infer architecture capabilities from a
912    /// coarse model-family identity. Implementations must fail closed for
913    /// requested routes that the exact normalized architecture or backend
914    /// cannot realize.
915    fn select_preparation(
916        &self,
917        inspection: &ArtifactInspection<
918            <Self::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
919        >,
920        options: &Self::LoadOptions,
921    ) -> Result<Self::SelectedPreparation, Self::Error>;
922
923    /// Returns the exact portable admission retained by the authoritative selection.
924    fn selected_preparation_admission(
925        &self,
926        selected: &Self::SelectedPreparation,
927    ) -> PreparationAdmission;
928
929    /// Binds a neutral preparation plan and its authoritative selected
930    /// realization to backend-owned materialization input.
931    fn model_config(
932        &self,
933        selected: SelectedModelPreparation<Self>,
934    ) -> Result<Self::ModelConfig, Self::Error>;
935}
936
937/// One neutral preparation plan inseparably paired with its backend selection.
938///
939/// Core creates this value only after policy, architecture, and session-capability
940/// admission have succeeded against the same artifact inspection. Backend adapters
941/// may consume the pair, but callers cannot substitute a different plan after
942/// selection.
943pub struct SelectedModelPreparation<B: ModelLoadingBackend> {
944    plan: ModelPreparationPlan<
945        <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
946    >,
947    selected: B::SelectedPreparation,
948}
949
950impl<B: ModelLoadingBackend> SelectedModelPreparation<B> {
951    pub(crate) fn new(
952        plan: ModelPreparationPlan<
953            <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
954        >,
955        selected: B::SelectedPreparation,
956    ) -> Self {
957        Self { plan, selected }
958    }
959
960    pub(crate) const fn plan(
961        &self,
962    ) -> &ModelPreparationPlan<<B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan>
963    {
964        &self.plan
965    }
966
967    /// Consumes the binding into the exact admitted plan and backend selection.
968    pub fn into_parts(
969        self,
970    ) -> (
971        ModelPreparationPlan<
972            <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
973        >,
974        B::SelectedPreparation,
975    ) {
976        (self.plan, self.selected)
977    }
978}
979
980/// Failure while inspecting, planning, or materializing a model artifact.
981#[derive(Debug, thiserror::Error)]
982#[non_exhaustive]
983pub enum ModelLoadError<E: std::error::Error + Send + Sync + 'static> {
984    /// Portable artifact inspection or preparation planning failed.
985    #[error(transparent)]
986    Artifact(#[from] ArtifactError),
987    /// The selected backend failed policy resolution or materialization.
988    #[error("selected backend failed to prepare the model: {0}")]
989    Backend(#[source] E),
990    /// The inspected architecture/load-policy/topology route lacks a requirement.
991    #[error(transparent)]
992    SessionCapability(#[from] SessionCapabilityError),
993}
994
995/// Inspects, plans, and prepares one artifact on the selected backend.
996///
997/// This is the sole generic artifact-loading entry point. The backend instance
998/// already owns its device, execution queues, transfer queues, and optional
999/// distributed communication state; none are passed separately to loading.
1000pub fn load_model<B: ModelLoadingBackend>(
1001    backend: &B,
1002    artifact: impl AsRef<Path>,
1003    options: B::LoadOptions,
1004) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
1005    let inspection = inspect_artifact(artifact, backend.configuration_resolver())?;
1006    prepare_inspected_model(backend, inspection, options)
1007}
1008
1009/// Plans and prepares an artifact that the caller has already inspected.
1010///
1011/// This is the canonical lower-level entry point for facade loaders which
1012/// must derive tokenizer, chat, or other portable sidecar state from the same
1013/// inspection before transferring ownership to the selected backend.
1014pub fn prepare_inspected_model<B: ModelLoadingBackend>(
1015    backend: &B,
1016    inspection: ArtifactInspection<
1017        <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
1018    >,
1019    options: B::LoadOptions,
1020) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
1021    let selected = backend
1022        .select_preparation(&inspection, &options)
1023        .map_err(ModelLoadError::Backend)?;
1024    let admission = backend.selected_preparation_admission(&selected);
1025    let plan = ModelPreparationPlan::from_retained_admission(inspection, admission)?;
1026    prepare_selected_model(backend, SelectedModelPreparation::new(plan, selected))
1027}
1028
1029/// Materializes an already planned artifact with its retained backend selection.
1030///
1031/// Selection and capability admission must have completed before the backend
1032/// instance was realized. This entry point therefore performs no policy or
1033/// route selection after native resources exist.
1034pub(crate) fn prepare_selected_model<B: ModelLoadingBackend>(
1035    backend: &B,
1036    selected: SelectedModelPreparation<B>,
1037) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
1038    let config = backend
1039        .model_config(selected)
1040        .map_err(ModelLoadError::Backend)?;
1041    backend
1042        .prepare_model(config)
1043        .map_err(ModelLoadError::Backend)
1044}
1045
1046/// Prefill/decode interface for an already selected backend session.
1047///
1048/// The session owns its prepared executable and cache. The contract
1049/// intentionally models language-model submissions rather than primitive
1050/// tensor operations. Input, output, cache and completion stay opaque.
1051pub trait BackendSession<B: BackendProvider> {
1052    /// Backend-owned prefill input.
1053    type PrefillInput;
1054    /// Backend-owned decode input.
1055    type DecodeInput;
1056    /// Backend-owned logits/output.
1057    type Output;
1058    /// Exact completion type.
1059    type Completion: Completion<Error = B::Error>;
1060
1061    /// Reports capabilities of this exact realized session.
1062    fn capabilities(&self) -> SessionCapabilities;
1063
1064    /// Submits prompt prefill against this session.
1065    fn prefill(
1066        &mut self,
1067        backend: &B,
1068        input: Self::PrefillInput,
1069    ) -> Result<Submission<Self::Output, Self::Completion>, B::Error>;
1070
1071    /// Submits one or more cached decode positions against this session.
1072    fn decode(
1073        &mut self,
1074        backend: &B,
1075        input: Self::DecodeInput,
1076    ) -> Result<Submission<Self::Output, Self::Completion>, B::Error>;
1077
1078    /// Materializes an already completed opaque output into portable records.
1079    ///
1080    /// Calling this method is an explicit synchronization and host-transfer
1081    /// boundary. Ordinary inference does not invoke it.
1082    fn observe_output(
1083        &self,
1084        backend: &B,
1085        output: &Self::Output,
1086    ) -> Result<ObservationSet, B::Error>;
1087}
1088
1089/// Optional named-activation inspection for a selected backend session.
1090///
1091/// This is a general diagnostics and observability capability. Implementations
1092/// execute the requested operation to completion and materialize only selected
1093/// observation points. It is intentionally separate from ordinary asynchronous
1094/// submission so production inference pays no instrumentation cost.
1095pub trait InspectableBackendSession<B: BackendProvider>: BackendSession<B> {
1096    /// Executes and inspects one prompt prefill operation.
1097    fn inspect_prefill(
1098        &mut self,
1099        backend: &B,
1100        input: Self::PrefillInput,
1101        request: &ObservationRequest,
1102    ) -> Result<InspectedOutput<Self::Output>, B::Error>;
1103
1104    /// Executes and inspects one cached decode operation.
1105    fn inspect_decode(
1106        &mut self,
1107        backend: &B,
1108        input: Self::DecodeInput,
1109        request: &ObservationRequest,
1110    ) -> Result<InspectedOutput<Self::Output>, B::Error>;
1111}
1112
1113/// One submission produced by the selected backend session.
1114pub type SessionSubmission<B> = Submission<
1115    <<B as BackendProvider>::Session as BackendSession<B>>::Output,
1116    <<B as BackendProvider>::Session as BackendSession<B>>::Completion,
1117>;
1118
1119/// A prepared model, its selected backend, and its backend-owned session.
1120///
1121/// This is the canonical client-side execution owner. Keeping the backend and
1122/// session together makes backend selection a whole-model decision: generic
1123/// operations always supply this retained backend. Backend session hooks still
1124/// validate native identity when unrestricted session mutation is exposed.
1125/// Backend-owned executable, cache, tensor, and completion types remain
1126/// associated types and never enter the portable API.
1127pub struct ModelRuntime<B: BackendProvider> {
1128    backend: B,
1129    session: B::Session,
1130    admission: crate::SessionAdmission,
1131    execution_plan_target_id: Option<u64>,
1132}
1133
1134impl<B: BackendProvider> ModelRuntime<B> {
1135    /// Prepares `config` and creates its sole execution session.
1136    pub fn prepare(backend: B, config: B::ModelConfig) -> Result<Self, B::Error> {
1137        let model = backend.prepare_model(config)?;
1138        Self::from_prepared(backend, model)
1139    }
1140
1141    /// Creates the sole execution session for an already prepared model.
1142    pub fn from_prepared(backend: B, model: PreparedModel<B::Model>) -> Result<Self, B::Error> {
1143        Self::from_prepared_with_execution_plan_target(backend, model, None)
1144    }
1145
1146    pub(crate) fn from_prepared_execution_plan_target(
1147        backend: B,
1148        model: PreparedModel<B::Model>,
1149        execution_plan_target_id: u64,
1150    ) -> Result<Self, B::Error> {
1151        Self::from_prepared_with_execution_plan_target(
1152            backend,
1153            model,
1154            Some(execution_plan_target_id),
1155        )
1156    }
1157
1158    fn from_prepared_with_execution_plan_target(
1159        backend: B,
1160        model: PreparedModel<B::Model>,
1161        execution_plan_target_id: Option<u64>,
1162    ) -> Result<Self, B::Error> {
1163        let admitted = model.capabilities();
1164        let session = backend.create_session(model)?;
1165        let realized = session.capabilities();
1166        if crate::SessionAdmission::new(admitted)
1167            .validate(realized)
1168            .is_err()
1169        {
1170            return Err(backend.session_capability_mismatch(admitted, realized));
1171        }
1172        Ok(Self {
1173            backend,
1174            session,
1175            admission: crate::SessionAdmission::new(admitted),
1176            execution_plan_target_id,
1177        })
1178    }
1179
1180    pub(crate) const fn execution_plan_target_id(&self) -> Option<u64> {
1181        self.execution_plan_target_id
1182    }
1183
1184    /// Returns the selected backend.
1185    pub const fn backend(&self) -> &B {
1186        &self.backend
1187    }
1188
1189    /// Returns the backend-owned session for optional backend capabilities.
1190    pub const fn session(&self) -> &B::Session {
1191        &self.session
1192    }
1193
1194    /// Returns the backend-owned session for optional backend capabilities.
1195    ///
1196    /// Unrestricted mutation can replace the session, so this invalidates any
1197    /// retained execution-plan target proof. Previously selected drafting can
1198    /// no longer attach to this runtime. Generic operations still enforce the
1199    /// original exact capability admission after mutation.
1200    pub fn session_mut(&mut self) -> &mut B::Session {
1201        self.execution_plan_target_id = None;
1202        &mut self.session
1203    }
1204
1205    /// Borrows the selected backend and its mutable session together.
1206    ///
1207    /// Like [`Self::session_mut`], this invalidates retained drafting target proof.
1208    pub fn parts_mut(&mut self) -> (&B, &mut B::Session) {
1209        self.execution_plan_target_id = None;
1210        (&self.backend, &mut self.session)
1211    }
1212
1213    fn validate_session_admission(&self) -> Result<(), B::Error> {
1214        self.admission
1215            .validate(self.session.capabilities())
1216            .map_err(|error| {
1217                self.backend
1218                    .session_capability_mismatch(error.admitted(), error.realized())
1219            })
1220    }
1221
1222    /// Reports capabilities of the exact prepared model session.
1223    pub fn capabilities(&self) -> SessionCapabilities {
1224        self.session.capabilities()
1225    }
1226
1227    /// Submits prompt prefill through the selected backend and session.
1228    pub fn prefill(
1229        &mut self,
1230        input: <B::Session as BackendSession<B>>::PrefillInput,
1231    ) -> Result<SessionSubmission<B>, B::Error> {
1232        self.validate_session_admission()?;
1233        self.session.prefill(&self.backend, input)
1234    }
1235
1236    /// Submits cached decode through the selected backend and session.
1237    pub fn decode(
1238        &mut self,
1239        input: <B::Session as BackendSession<B>>::DecodeInput,
1240    ) -> Result<SessionSubmission<B>, B::Error> {
1241        self.validate_session_admission()?;
1242        self.session.decode(&self.backend, input)
1243    }
1244
1245    /// Materializes portable observations from an already completed output.
1246    pub fn observe_output(
1247        &self,
1248        output: &<B::Session as BackendSession<B>>::Output,
1249    ) -> Result<ObservationSet, B::Error> {
1250        self.validate_session_admission()?;
1251        self.session.observe_output(&self.backend, output)
1252    }
1253}
1254
1255impl<B> ModelRuntime<B>
1256where
1257    B: BackendProvider,
1258    B::Session: InspectableBackendSession<B>,
1259{
1260    /// Executes a completed, explicitly instrumented prefill operation.
1261    pub fn inspect_prefill(
1262        &mut self,
1263        input: <B::Session as BackendSession<B>>::PrefillInput,
1264        request: &ObservationRequest,
1265    ) -> Result<InspectedOutput<<B::Session as BackendSession<B>>::Output>, B::Error> {
1266        self.validate_session_admission()?;
1267        self.session.inspect_prefill(&self.backend, input, request)
1268    }
1269
1270    /// Executes a completed, explicitly instrumented decode operation.
1271    pub fn inspect_decode(
1272        &mut self,
1273        input: <B::Session as BackendSession<B>>::DecodeInput,
1274        request: &ObservationRequest,
1275    ) -> Result<InspectedOutput<<B::Session as BackendSession<B>>::Output>, B::Error> {
1276        self.validate_session_admission()?;
1277        self.session.inspect_decode(&self.backend, input, request)
1278    }
1279}
1280
1281impl<B: ModelLoadingBackend> ModelRuntime<B> {
1282    /// Loads an artifact and creates its sole session on `backend`.
1283    pub fn load(
1284        backend: B,
1285        artifact: impl AsRef<Path>,
1286        options: B::LoadOptions,
1287    ) -> Result<Self, ModelLoadError<B::Error>> {
1288        let model = load_model(&backend, artifact, options)?;
1289        Self::from_prepared(backend, model).map_err(ModelLoadError::Backend)
1290    }
1291}
1292
1293/// Portable sampling inputs for one text-generation session.
1294#[derive(Debug, Clone, Copy, PartialEq)]
1295pub struct TextGenerationConfig {
1296    sampling: ResolvedGenerationConfig,
1297    seed: u64,
1298    strategy: TextSamplingStrategy,
1299}
1300
1301/// Backend-neutral token-sampling strategy for one text-generation session.
1302#[derive(Debug, Clone, Copy, Default, PartialEq)]
1303pub enum TextSamplingStrategy {
1304    /// Apply the resolved top-k, top-p, min-p, and penalty controls.
1305    #[default]
1306    Standard,
1307    /// Adapt the surprise cutoff toward `tau` bits at rate `eta`.
1308    MirostatV2 {
1309        /// Target surprise in bits.
1310        tau: f32,
1311        /// Adaptation rate.
1312        eta: f32,
1313    },
1314}
1315
1316impl TextGenerationConfig {
1317    /// Uses resolved checkpoint/request sampling with deterministic seed zero.
1318    pub const fn new(sampling: ResolvedGenerationConfig) -> Self {
1319        Self {
1320            sampling,
1321            seed: 0,
1322            strategy: TextSamplingStrategy::Standard,
1323        }
1324    }
1325
1326    /// Selects the deterministic root seed used by a stochastic backend.
1327    pub const fn with_seed(mut self, seed: u64) -> Self {
1328        self.seed = seed;
1329        self
1330    }
1331
1332    /// Selects adaptive Mirostat V2 sampling.
1333    pub fn with_mirostat_v2(mut self, tau: f32, eta: f32) -> Result<Self, GenerationError> {
1334        if !tau.is_finite() || tau <= 0.0 {
1335            return Err(GenerationError::InvalidMirostatTau(tau));
1336        }
1337        if !eta.is_finite() || eta <= 0.0 {
1338            return Err(GenerationError::InvalidMirostatEta(eta));
1339        }
1340        self.strategy = TextSamplingStrategy::MirostatV2 { tau, eta };
1341        Ok(self)
1342    }
1343
1344    /// Returns the validated sampling configuration.
1345    pub const fn sampling(&self) -> ResolvedGenerationConfig {
1346        self.sampling
1347    }
1348
1349    /// Returns the deterministic root seed.
1350    pub const fn seed(&self) -> u64 {
1351        self.seed
1352    }
1353
1354    /// Returns the selected backend-neutral sampling strategy.
1355    pub const fn strategy(&self) -> TextSamplingStrategy {
1356        self.strategy
1357    }
1358}
1359
1360/// Backend-owned generated token that exposes only its portable token id.
1361pub trait TokenOutput: Clone {
1362    /// Error produced while observing the token value.
1363    type Error: std::error::Error + Send + Sync + 'static;
1364
1365    /// Waits only as required to read this token's canonical vocabulary id.
1366    fn token_id(&self) -> Result<u32, Self::Error>;
1367}
1368
1369impl TokenOutput for u32 {
1370    type Error = std::convert::Infallible;
1371
1372    fn token_id(&self) -> Result<u32, Self::Error> {
1373        Ok(*self)
1374    }
1375}
1376
1377/// Portable vocabulary filter applied before backend-owned sampling.
1378#[derive(Debug, Clone, Eq, PartialEq)]
1379pub enum TokenFilter {
1380    /// Every vocabulary token may be selected.
1381    All,
1382    /// One boolean per canonical vocabulary id; `true` permits selection.
1383    Allowed(Vec<bool>),
1384}
1385
1386impl TokenFilter {
1387    /// Validates an explicit canonical-vocabulary allow mask.
1388    pub fn allowed(mask: Vec<bool>) -> Result<Self, TokenFilterError> {
1389        if mask.is_empty() {
1390            return Err(TokenFilterError::EmptyVocabulary);
1391        }
1392        if !mask.iter().any(|allowed| *allowed) {
1393            return Err(TokenFilterError::NoAllowedToken);
1394        }
1395        Ok(Self::Allowed(mask))
1396    }
1397
1398    /// Returns the explicit allow mask, or `None` when all tokens are allowed.
1399    pub fn allowed_mask(&self) -> Option<&[bool]> {
1400        match self {
1401            Self::All => None,
1402            Self::Allowed(mask) => Some(mask),
1403        }
1404    }
1405}
1406
1407/// Invalid portable token-filter construction.
1408#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1409pub enum TokenFilterError {
1410    /// An explicit mask must describe a nonempty vocabulary.
1411    #[error("token filter vocabulary must not be empty")]
1412    EmptyVocabulary,
1413    /// Fail closed instead of asking a backend to sample an impossible row.
1414    #[error("token filter does not allow any vocabulary token")]
1415    NoAllowedToken,
1416}
1417
1418/// Backend-independent logical controller for constrained token selection.
1419pub trait TokenFilterController {
1420    /// Constraint or grammar error.
1421    type Error: std::error::Error + Send + Sync + 'static;
1422
1423    /// Returns the filter for the current durable logical prefix.
1424    fn current_filter(&mut self) -> Result<TokenFilter, Self::Error>;
1425
1426    /// Commits one backend-selected canonical vocabulary id.
1427    fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error>;
1428
1429    /// Returns whether the committed logical prefix satisfies the constraint.
1430    fn is_complete(&mut self) -> Result<bool, Self::Error>;
1431}
1432
1433/// Portable constraint controller that can evaluate speculative token histories.
1434///
1435/// Speculative backends use these queries for discardable draft branches. The
1436/// durable controller state must not change until [`TokenFilterController::commit_token`]
1437/// is called for a target-accepted token.
1438pub trait SpeculativeTokenFilterController: TokenFilterController + Clone {
1439    /// Returns the filter at `history` without committing its uncommitted suffix.
1440    ///
1441    /// `history` contains the controller's durable prefix followed by zero or
1442    /// more speculative tokens. Implementations must reject histories that do
1443    /// not begin with the durable prefix.
1444    fn filter_at(&self, history: &[u32]) -> Result<TokenFilter, Self::Error>;
1445
1446    /// Returns whether `history` completes the constraint without committing it.
1447    fn prefix_is_complete(&self, history: &[u32]) -> Result<bool, Self::Error>;
1448}
1449
1450#[derive(Debug, Clone, Copy)]
1451struct UnconstrainedTokens;
1452
1453impl TokenFilterController for UnconstrainedTokens {
1454    type Error = std::convert::Infallible;
1455
1456    fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
1457        Ok(TokenFilter::All)
1458    }
1459
1460    fn commit_token(&mut self, _: u32) -> Result<(), Self::Error> {
1461        Ok(())
1462    }
1463
1464    fn is_complete(&mut self) -> Result<bool, Self::Error> {
1465        Ok(false)
1466    }
1467}
1468
1469/// High-level text-generation extension implemented once per backend.
1470///
1471/// The contract deliberately combines model execution and sampling. Core does
1472/// not see logits or ask a backend to implement tensor primitives. The token,
1473/// sampling state, cache state, and exact completion remain backend-owned.
1474pub trait TextGenerationBackend: BackendProvider {
1475    /// Opaque prepared prompt, including any backend-owned multimodal values.
1476    type Prompt;
1477    /// Backend-owned generated token handle.
1478    type Token: TokenOutput<Error = Self::Error>;
1479    /// Backend-owned sampler and randomness state for one sequence.
1480    type TextGenerationState;
1481    /// Exact completion retaining model execution and token sampling.
1482    type TextCompletion: Completion<Error = Self::Error>;
1483
1484    /// Exact support for serial, completed-token control on this loaded session.
1485    /// Backends opt in only for verified ordinary single-sequence execution.
1486    /// Snapshot mechanisms and complete facade-state support are separate facts.
1487    fn text_execution_control_support(
1488        _runtime: &ModelRuntime<Self>,
1489    ) -> crate::execution_control::ControlSupport {
1490        crate::execution_control::ControlSupport::Unsupported {
1491            reason: "backend has not declared completed-token control support".into(),
1492        }
1493    }
1494
1495    /// Exact support for prospective temperature changes and explicit reseeding.
1496    /// The runtime validates policy; the adapter supplies atomic native changes.
1497    fn text_sampling_control_support(
1498        _runtime: &ModelRuntime<Self>,
1499    ) -> crate::execution_control::ControlSupport {
1500        crate::execution_control::ControlSupport::Unsupported {
1501            reason: "backend has no prospective sampling controls".into(),
1502        }
1503    }
1504
1505    /// Returns genuine mutable points for this exact loaded session.
1506    fn intervention_discovery(
1507        _runtime: &ModelRuntime<Self>,
1508    ) -> Result<crate::intervention::InterventionDiscovery, crate::capture::CaptureError> {
1509        Err(crate::capture::CaptureError::Unsupported(
1510            "backend has no intervention discovery".into(),
1511        ))
1512    }
1513
1514    /// Checks both plans without submitting work or changing model/sampler state.
1515    fn validate_text_interventions(
1516        runtime: &ModelRuntime<Self>,
1517        capture: &crate::capture::AdmittedCapturePlan,
1518        plan: &crate::intervention::AdmittedInterventionPlan,
1519    ) -> Result<(), crate::capture::CaptureError> {
1520        if !plan.is_empty() {
1521            return Err(crate::capture::CaptureError::Unsupported(
1522                "backend has no text interventions".into(),
1523            ));
1524        }
1525        Self::validate_text_capture(runtime, capture)
1526    }
1527
1528    /// Installs immutable plans before the first submission. Diagnostics and
1529    /// evidence must share capture accounting and ordinary completion ownership.
1530    fn configure_text_interventions(
1531        runtime: &ModelRuntime<Self>,
1532        state: &mut Self::TextGenerationState,
1533        capture: crate::capture::AdmittedCapturePlan,
1534        plan: crate::intervention::AdmittedInterventionPlan,
1535    ) -> Result<(), crate::capture::CaptureError> {
1536        Self::validate_text_interventions(runtime, &capture, &plan)?;
1537        Self::configure_text_capture(runtime, state, capture)
1538    }
1539
1540    /// Returns observation facts retained from this session's admitted preparation.
1541    fn capture_discovery(
1542        _runtime: &ModelRuntime<Self>,
1543    ) -> Result<crate::capture::CaptureDiscovery, crate::capture::CaptureError> {
1544        Err(crate::capture::CaptureError::Unsupported(
1545            "backend has no bounded capture discovery".into(),
1546        ))
1547    }
1548
1549    /// Enables an immutable capture plan before the first submission. Implementations
1550    /// must validate it against this session and reject unsupported combinations.
1551    fn configure_text_capture(
1552        _runtime: &ModelRuntime<Self>,
1553        _state: &mut Self::TextGenerationState,
1554        plan: crate::capture::AdmittedCapturePlan,
1555    ) -> Result<(), crate::capture::CaptureError> {
1556        if plan.is_empty() {
1557            Ok(())
1558        } else {
1559            Err(crate::capture::CaptureError::Unsupported(
1560                "backend has no bounded text capture".into(),
1561            ))
1562        }
1563    }
1564
1565    /// Validates known capture costs and execution combinations without submitting
1566    /// native work or advancing sampler state.
1567    fn validate_text_capture(
1568        _runtime: &ModelRuntime<Self>,
1569        plan: &crate::capture::AdmittedCapturePlan,
1570    ) -> Result<(), crate::capture::CaptureError> {
1571        if plan.is_empty() {
1572            Ok(())
1573        } else {
1574            Err(crate::capture::CaptureError::Unsupported(
1575                "backend has no bounded text capture".into(),
1576            ))
1577        }
1578    }
1579
1580    /// Moves out at most one completed capture step. No implementation may queue
1581    /// unconsumed steps without a separately admitted finite buffering contract.
1582    fn take_text_capture(
1583        _state: &mut Self::TextGenerationState,
1584    ) -> Option<crate::capture::CapturedStep> {
1585        None
1586    }
1587
1588    /// Creates backend sampling state for one sequence.
1589    fn start_text_generation(
1590        backend: &Self,
1591        config: TextGenerationConfig,
1592    ) -> Result<Self::TextGenerationState, Self::Error>;
1593
1594    /// Converts portable tokenizer ids into a backend-owned text prompt.
1595    fn prepare_text_prompt(
1596        backend: &Self,
1597        prompt_token_ids: Vec<u32>,
1598    ) -> Result<Self::Prompt, Self::Error>;
1599
1600    /// Submits prompt prefill followed by sampling one token.
1601    fn submit_text_prefill(
1602        runtime: &mut ModelRuntime<Self>,
1603        prompt: Self::Prompt,
1604        filter: &TokenFilter,
1605        state: &mut Self::TextGenerationState,
1606    ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error>;
1607
1608    /// Submits cached decode from the preceding token and samples its successor.
1609    fn submit_text_decode(
1610        runtime: &mut ModelRuntime<Self>,
1611        token: Self::Token,
1612        filter: &TokenFilter,
1613        state: &mut Self::TextGenerationState,
1614    ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error>;
1615}
1616
1617/// Failure from backend preprocessing or backend-requested text encoding.
1618#[derive(Debug, thiserror::Error)]
1619pub enum MultimodalPreparationFailure<B, T>
1620where
1621    B: std::error::Error + 'static,
1622    T: std::error::Error + 'static,
1623{
1624    /// The selected backend rejected or failed media preprocessing.
1625    #[error("backend multimodal preparation failed: {0}")]
1626    Backend(#[source] B),
1627    /// The facade tokenizer failed on backend-required framing text.
1628    #[error("multimodal framing text encoding failed: {0}")]
1629    Text(#[source] T),
1630}
1631
1632/// Backend preparation of portable decoded media for one selected session.
1633///
1634/// Caller text is tokenized before this boundary. Some processors introduce
1635/// checkpoint-defined framing text or video timestamps, so the callback keeps
1636/// that work on the facade's tokenizer. The selected backend returns its
1637/// existing opaque prompt type; core never observes tensors or streams.
1638pub trait MultimodalPreparationBackend: TextGenerationBackend {
1639    /// Converts ordered token and decoded-media segments into a backend prompt.
1640    fn prepare_multimodal_input<E>(
1641        runtime: &ModelRuntime<Self>,
1642        request: &TokenizedMultimodalRequest,
1643        encode_backend_text: &mut dyn FnMut(&str) -> Result<Vec<u32>, E>,
1644    ) -> Result<Self::Prompt, MultimodalPreparationFailure<Self::Error, E>>
1645    where
1646        E: std::error::Error + Send + Sync + 'static;
1647}
1648
1649/// Capability and resource observations for one selected text-model session.
1650///
1651/// Implementations inspect the opaque model and cache already owned by
1652/// [`ModelRuntime`]. The contract exposes only portable documents and the
1653/// backend's existing opaque prompt type; tensor, stream, allocator, and
1654/// executable types remain inside the adapter.
1655pub trait ModelCapabilityBackend: TextGenerationBackend {
1656    /// Reports validated model capabilities for the selected session.
1657    fn model_capabilities(
1658        runtime: &ModelRuntime<Self>,
1659    ) -> Result<ModelCapabilities, CapabilityError>;
1660
1661    /// Counts text and backend-specific model positions in a prepared prompt.
1662    fn count_prepared_input(
1663        runtime: &ModelRuntime<Self>,
1664        input: &Self::Prompt,
1665    ) -> Result<InputTokenCount, CapabilityError>;
1666
1667    /// Estimates persistent and transient request state for this session.
1668    fn estimate_runtime_state(
1669        runtime: &ModelRuntime<Self>,
1670        input: InputTokenCount,
1671        max_output_tokens: u64,
1672        batch_size: u64,
1673    ) -> Result<RuntimeStateEstimate, CapabilityError>;
1674
1675    /// Reports static model storage and current backend memory observations.
1676    fn static_memory(runtime: &ModelRuntime<Self>) -> Result<StaticMemoryReport, CapabilityError>;
1677}
1678
1679/// Input for the next ordinary prediction. A committed token may still be the
1680/// pending decode input and must not be inserted into model state until that step.
1681pub enum PendingTextInput<P, T> {
1682    /// The original prepared prompt, before the first model prediction.
1683    Prefill(P),
1684    /// The preceding committed canonical token, before its successor is predicted.
1685    Decode(T),
1686}
1687
1688impl<P, T> PendingTextInput<P, T> {
1689    /// Borrows the pending native input without cloning or evaluating it.
1690    pub fn as_ref(&self) -> PendingTextInput<&P, &T> {
1691        match self {
1692            Self::Prefill(prompt) => PendingTextInput::Prefill(prompt),
1693            Self::Decode(token) => PendingTextInput::Decode(token),
1694        }
1695    }
1696}
1697
1698/// Failure from either backend execution or portable constraint control.
1699#[derive(Debug, thiserror::Error)]
1700pub enum ControlledTextGenerationError<B, C>
1701where
1702    B: std::error::Error + 'static,
1703    C: std::error::Error + 'static,
1704{
1705    /// Backend preparation, execution, sampling, or completion failed.
1706    #[error("backend text generation failed: {0}")]
1707    Backend(#[source] B),
1708    /// Portable constraint filtering or commitment failed.
1709    #[error("text generation constraint failed: {0}")]
1710    Controller(#[source] C),
1711}
1712
1713/// One constraint-committed token and its backend-owned output handle.
1714#[derive(Debug, Clone)]
1715pub struct ControlledToken<T> {
1716    output: T,
1717    token_id: u32,
1718}
1719
1720impl<T> ControlledToken<T> {
1721    /// Returns the committed canonical vocabulary id.
1722    pub fn token_id(&self) -> u32 {
1723        self.token_id
1724    }
1725
1726    /// Borrows the backend-owned token handle.
1727    pub const fn output(&self) -> &T {
1728        &self.output
1729    }
1730
1731    /// Consumes the committed token into its backend-owned handle.
1732    pub fn into_output(self) -> T {
1733        self.output
1734    }
1735}
1736
1737/// Backend-generic generation driven by a portable token-filter controller.
1738pub struct ControlledTextGeneration<'a, B, C>
1739where
1740    B: TextGenerationBackend,
1741    C: TokenFilterController,
1742{
1743    runtime: &'a mut ModelRuntime<B>,
1744    inner: TextGenerationMachine<B, C>,
1745}
1746
1747struct TextGenerationMachine<B, C>
1748where
1749    B: TextGenerationBackend,
1750    C: TokenFilterController,
1751{
1752    backend_state: B::TextGenerationState,
1753    controller: C,
1754    step: Option<PendingTextInput<B::Prompt, B::Token>>,
1755    completions: Vec<B::TextCompletion>,
1756    remaining_tokens: Option<usize>,
1757}
1758
1759type ControlledGenerationResult<B, C> = Result<
1760    <B as TextGenerationBackend>::Token,
1761    ControlledTextGenerationError<
1762        <B as BackendProvider>::Error,
1763        <C as TokenFilterController>::Error,
1764    >,
1765>;
1766
1767impl<'a, B, C> ControlledTextGeneration<'a, B, C>
1768where
1769    B: TextGenerationBackend,
1770    C: TokenFilterController,
1771{
1772    /// Starts controlled generation from portable prompt token ids.
1773    pub fn new(
1774        runtime: &'a mut ModelRuntime<B>,
1775        prompt_token_ids: Vec<u32>,
1776        config: TextGenerationConfig,
1777        controller: C,
1778    ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1779        let prompt = B::prepare_text_prompt(runtime.backend(), prompt_token_ids)
1780            .map_err(ControlledTextGenerationError::Backend)?;
1781        Self::from_prompt(runtime, prompt, config, controller)
1782    }
1783
1784    /// Starts controlled generation from an opaque backend-prepared prompt.
1785    pub fn from_prompt(
1786        runtime: &'a mut ModelRuntime<B>,
1787        prompt: B::Prompt,
1788        config: TextGenerationConfig,
1789        controller: C,
1790    ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1791        let inner = TextGenerationMachine::new(runtime, prompt, config, controller)?;
1792        Ok(Self { runtime, inner })
1793    }
1794
1795    /// Mutably borrows the canonical constraint state.
1796    pub fn controller_mut(&mut self) -> &mut C {
1797        &mut self.inner.controller
1798    }
1799
1800    /// Installs capture while preserving the existing sampling/controller state.
1801    pub fn enable_capture(
1802        &mut self,
1803        plan: crate::capture::AdmittedCapturePlan,
1804    ) -> Result<(), crate::capture::CaptureError> {
1805        if !matches!(self.inner.step, Some(PendingTextInput::Prefill(_))) {
1806            return Err(crate::capture::CaptureError::Invalid(
1807                "capture must be configured before generation".into(),
1808            ));
1809        }
1810        B::configure_text_capture(self.runtime, &mut self.inner.backend_state, plan)
1811    }
1812
1813    /// Installs admitted interventions and captures before ordinary generation.
1814    pub fn enable_interventions(
1815        &mut self,
1816        capture: crate::capture::AdmittedCapturePlan,
1817        plan: crate::intervention::AdmittedInterventionPlan,
1818    ) -> Result<(), crate::capture::CaptureError> {
1819        if !matches!(self.inner.step, Some(PendingTextInput::Prefill(_))) {
1820            return Err(crate::capture::CaptureError::Invalid(
1821                "interventions must be configured before generation".into(),
1822            ));
1823        }
1824        B::configure_text_interventions(self.runtime, &mut self.inner.backend_state, capture, plan)
1825    }
1826
1827    /// Establishes exact completion before delivering this step's host captures.
1828    /// The ordinary machine retains authority and handles errors/drop as before.
1829    pub fn take_captured_step(&mut self) -> Result<Option<crate::capture::CapturedStep>, B::Error> {
1830        self.inner.resolve_completions_before_decode()?;
1831        Ok(B::take_text_capture(&mut self.inner.backend_state))
1832    }
1833}
1834
1835impl<B, C> TextGenerationMachine<B, C>
1836where
1837    B: TextGenerationBackend,
1838    C: TokenFilterController,
1839{
1840    fn new(
1841        runtime: &ModelRuntime<B>,
1842        prompt: B::Prompt,
1843        config: TextGenerationConfig,
1844        controller: C,
1845    ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1846        let backend_state = B::start_text_generation(runtime.backend(), config)
1847            .map_err(ControlledTextGenerationError::Backend)?;
1848        Ok(Self {
1849            backend_state,
1850            controller,
1851            step: Some(PendingTextInput::Prefill(prompt)),
1852            completions: Vec::new(),
1853            remaining_tokens: config.sampling().max_new_tokens,
1854        })
1855    }
1856
1857    fn retain_completion(&mut self, completion: B::TextCompletion) -> Result<(), B::Error> {
1858        let existing = std::mem::take(&mut self.completions);
1859        let mut retained = Vec::with_capacity(existing.len() + 1);
1860        for pending in existing {
1861            match pending.is_complete() {
1862                Ok(true) => {}
1863                Ok(false) => retained.push(pending),
1864                Err(error) => {
1865                    let _ = pending.wait();
1866                    for retained_completion in retained.drain(..) {
1867                        let _ = retained_completion.wait();
1868                    }
1869                    let _ = completion.wait();
1870                    return Err(error);
1871                }
1872            }
1873        }
1874        retained.push(completion);
1875        self.completions = retained;
1876        Ok(())
1877    }
1878
1879    fn resolve_completions_before_decode(&mut self) -> Result<(), B::Error> {
1880        let existing = std::mem::take(&mut self.completions);
1881        let mut remaining = existing.into_iter();
1882        while let Some(completion) = remaining.next() {
1883            let result = match completion.is_complete() {
1884                Ok(true) => Ok(()),
1885                Ok(false) => completion.wait(),
1886                Err(error) => {
1887                    let _ = completion.wait();
1888                    Err(error)
1889                }
1890            };
1891            if let Err(error) = result {
1892                for pending in remaining {
1893                    let _ = pending.wait();
1894                }
1895                return Err(error);
1896            }
1897        }
1898        Ok(())
1899    }
1900
1901    #[allow(clippy::type_complexity)]
1902    fn next_committed(
1903        &mut self,
1904        runtime: &mut ModelRuntime<B>,
1905    ) -> Option<Result<ControlledToken<B::Token>, ControlledTextGenerationError<B::Error, C::Error>>>
1906    {
1907        let token = match self.next_output(runtime)? {
1908            Ok(token) => token,
1909            Err(error) => return Some(Err(error)),
1910        };
1911        let token_id = match token.token_id() {
1912            Ok(token_id) => token_id,
1913            Err(error) => {
1914                self.step = None;
1915                return Some(Err(ControlledTextGenerationError::Backend(error)));
1916            }
1917        };
1918        if let Err(error) = self.controller.commit_token(token_id) {
1919            self.step = None;
1920            return Some(Err(ControlledTextGenerationError::Controller(error)));
1921        }
1922        Some(Ok(ControlledToken {
1923            output: token,
1924            token_id,
1925        }))
1926    }
1927
1928    fn next_output(
1929        &mut self,
1930        runtime: &mut ModelRuntime<B>,
1931    ) -> Option<ControlledGenerationResult<B, C>> {
1932        if self.remaining_tokens == Some(0) {
1933            self.step = None;
1934            return None;
1935        }
1936        let step = self.step.take()?;
1937        if matches!(step, PendingTextInput::Decode(_)) {
1938            if let Err(error) = self.resolve_completions_before_decode() {
1939                return Some(Err(ControlledTextGenerationError::Backend(error)));
1940            }
1941        }
1942        let filter = match self.controller.current_filter() {
1943            Ok(filter) => filter,
1944            Err(error) => return Some(Err(ControlledTextGenerationError::Controller(error))),
1945        };
1946        let submission = match step {
1947            PendingTextInput::Prefill(prompt) => {
1948                B::submit_text_prefill(runtime, prompt, &filter, &mut self.backend_state)
1949            }
1950            PendingTextInput::Decode(token) => {
1951                B::submit_text_decode(runtime, token, &filter, &mut self.backend_state)
1952            }
1953        };
1954        let submission = match submission {
1955            Ok(submission) => submission,
1956            Err(error) => return Some(Err(ControlledTextGenerationError::Backend(error))),
1957        };
1958        let token = submission.output;
1959        if let Err(error) = self.retain_completion(submission.completion) {
1960            return Some(Err(ControlledTextGenerationError::Backend(error)));
1961        }
1962        self.step = Some(PendingTextInput::Decode(token.clone()));
1963        if let Some(remaining_tokens) = &mut self.remaining_tokens {
1964            *remaining_tokens -= 1;
1965        }
1966        Some(Ok(token))
1967    }
1968}
1969
1970impl<B, C> Iterator for ControlledTextGeneration<'_, B, C>
1971where
1972    B: TextGenerationBackend,
1973    C: TokenFilterController,
1974{
1975    type Item =
1976        Result<ControlledToken<B::Token>, ControlledTextGenerationError<B::Error, C::Error>>;
1977
1978    fn next(&mut self) -> Option<Self::Item> {
1979        self.inner.next_committed(self.runtime)
1980    }
1981}
1982
1983impl<B, C> Drop for TextGenerationMachine<B, C>
1984where
1985    B: TextGenerationBackend,
1986    C: TokenFilterController,
1987{
1988    fn drop(&mut self) {
1989        for completion in self.completions.drain(..) {
1990            let _ = completion.wait();
1991        }
1992    }
1993}
1994
1995/// Backend-generic asynchronous token-generation iterator.
1996///
1997/// Every yielded token handle may be fed into the following decode without
1998/// first reading its id on the host. The preceding completion resolves before
1999/// that state-mutating decode is submitted, and dropping the iterator waits
2000/// for every still-retained submission.
2001pub struct TextGeneration<'a, B: TextGenerationBackend> {
2002    runtime: &'a mut ModelRuntime<B>,
2003    inner: TextGenerationMachine<B, UnconstrainedTokens>,
2004}
2005
2006impl<'a, B: TextGenerationBackend> TextGeneration<'a, B> {
2007    /// Starts generation from portable prompt token ids.
2008    pub fn new(
2009        runtime: &'a mut ModelRuntime<B>,
2010        prompt_token_ids: Vec<u32>,
2011        config: TextGenerationConfig,
2012    ) -> Result<Self, B::Error> {
2013        let prompt = B::prepare_text_prompt(runtime.backend(), prompt_token_ids)?;
2014        Self::from_prompt(runtime, prompt, config)
2015    }
2016
2017    /// Starts generation from an opaque backend-prepared prompt.
2018    pub fn from_prompt(
2019        runtime: &'a mut ModelRuntime<B>,
2020        prompt: B::Prompt,
2021        config: TextGenerationConfig,
2022    ) -> Result<Self, B::Error> {
2023        let inner = TextGenerationMachine::new(runtime, prompt, config, UnconstrainedTokens)
2024            .map_err(unreachable_unconstrained_error)?;
2025        Ok(Self { runtime, inner })
2026    }
2027}
2028
2029fn unreachable_unconstrained_error<B>(
2030    error: ControlledTextGenerationError<B, std::convert::Infallible>,
2031) -> B
2032where
2033    B: std::error::Error + 'static,
2034{
2035    match error {
2036        ControlledTextGenerationError::Backend(error) => error,
2037        ControlledTextGenerationError::Controller(error) => match error {},
2038    }
2039}
2040
2041impl<B: TextGenerationBackend> Iterator for TextGeneration<'_, B> {
2042    type Item = Result<B::Token, B::Error>;
2043
2044    fn next(&mut self) -> Option<Self::Item> {
2045        self.inner
2046            .next_output(self.runtime)
2047            .map(|result| result.map_err(unreachable_unconstrained_error))
2048    }
2049}
2050
2051/// Optional high-level transfer and collective capability of a selected session.
2052///
2053/// This contract deliberately operates on an opaque backend value. It models
2054/// the few communication submissions needed by model execution without making
2055/// core define a tensor algebra or exposing native groups, streams, or events.
2056/// Every operation is scoped to the session selected for the complete model.
2057pub trait DistributedSession {
2058    /// Backend-owned tensor or buffer value.
2059    type Value;
2060    /// Exact completion retaining the submitted communication resources.
2061    type Completion: Completion<Error = Self::Error>;
2062    /// Structured backend error.
2063    type Error: std::error::Error + Send + Sync + 'static;
2064
2065    /// Stable topology and rank identity.
2066    fn descriptor(&self) -> DistributedSessionDescriptor;
2067    /// Fail-closed communication support.
2068    fn capabilities(&self) -> DistributedCapabilities;
2069
2070    /// Submits a sum reduction over `scope`.
2071    fn all_reduce_sum(
2072        &self,
2073        scope: CollectiveScope,
2074        input: &Self::Value,
2075    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2076
2077    /// Submits a leading-rank-axis gather over `scope`.
2078    fn all_gather(
2079        &self,
2080        scope: CollectiveScope,
2081        input: &Self::Value,
2082    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2083
2084    /// Submits a variable-count all-to-all exchange over `scope`.
2085    fn all_to_all_v(
2086        &self,
2087        scope: CollectiveScope,
2088        input: &Self::Value,
2089        send_counts: &[usize],
2090        receive_counts: &[usize],
2091    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2092
2093    /// Submits a point-to-point send to a rank within `scope`.
2094    fn send(
2095        &self,
2096        scope: CollectiveScope,
2097        peer: usize,
2098        input: &Self::Value,
2099    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2100
2101    /// Submits a point-to-point receive from a rank within `scope`.
2102    fn receive(
2103        &self,
2104        scope: CollectiveScope,
2105        peer: usize,
2106        value: &ValueDescriptor,
2107    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
2108
2109    /// Synchronously gathers portable scheduler metadata across the world.
2110    fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error>;
2111}
2112
2113/// Backend extension exposing communication attached to a model session.
2114pub trait DistributedBackend: BackendProvider {
2115    /// Selected distributed session implementation.
2116    type DistributedSession: DistributedSession<Error = Self::Error>;
2117
2118    /// Returns communication for a distributed model session.
2119    fn distributed_session(session: &Self::Session) -> Option<&Self::DistributedSession>;
2120}
2121
2122#[cfg(test)]
2123mod tests {
2124    use super::*;
2125    use std::{convert::Infallible, io::Write};
2126
2127    #[test]
2128    fn text_generation_config_validates_portable_mirostat_strategy() {
2129        let sampling = crate::generation::resolve_generation_config(
2130            None,
2131            crate::generation::GenerationConfigOverrides {
2132                temperature: Some(0.8),
2133                ..crate::generation::GenerationConfigOverrides::default()
2134            },
2135        )
2136        .unwrap();
2137        let config = TextGenerationConfig::new(sampling)
2138            .with_seed(7)
2139            .with_mirostat_v2(5.0, 0.1)
2140            .unwrap();
2141        assert_eq!(config.seed(), 7);
2142        assert_eq!(
2143            config.strategy(),
2144            TextSamplingStrategy::MirostatV2 { tau: 5.0, eta: 0.1 }
2145        );
2146        assert!(matches!(
2147            TextGenerationConfig::new(sampling).with_mirostat_v2(0.0, 0.1),
2148            Err(GenerationError::InvalidMirostatTau(0.0))
2149        ));
2150        assert!(matches!(
2151            TextGenerationConfig::new(sampling).with_mirostat_v2(5.0, f32::NAN),
2152            Err(GenerationError::InvalidMirostatEta(value)) if value.is_nan()
2153        ));
2154    }
2155
2156    #[derive(Debug, Clone)]
2157    struct Done;
2158    impl Completion for Done {
2159        type Error = Infallible;
2160        fn is_complete(&self) -> Result<bool, Self::Error> {
2161            Ok(true)
2162        }
2163        fn wait(&self) -> Result<(), Self::Error> {
2164            Ok(())
2165        }
2166    }
2167    struct Mock;
2168    impl BackendProvider for Mock {
2169        type ModelConfig = u32;
2170        type Model = u32;
2171        type Session = MockSession;
2172        type Error = Infallible;
2173        fn descriptor(&self) -> BackendDescriptor {
2174            BackendDescriptor::new("mock", "1")
2175        }
2176        fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error> {
2177            Ok(vec![])
2178        }
2179        fn prepare_model(&self, config: u32) -> Result<PreparedModel<u32>, Self::Error> {
2180            Ok(PreparedModel::new(config, SessionCapabilities::default()))
2181        }
2182        fn create_session(&self, model: PreparedModel<u32>) -> Result<MockSession, Self::Error> {
2183            Ok(MockSession {
2184                model: model.into_inner(),
2185                tokens: vec![],
2186                distributed: None,
2187            })
2188        }
2189    }
2190
2191    #[derive(Default)]
2192    struct LoadingMock {
2193        selections: std::sync::atomic::AtomicUsize,
2194        materializations: std::sync::atomic::AtomicUsize,
2195    }
2196    struct LoadingMockSession;
2197
2198    struct LoadingConfigurationResolver;
2199
2200    impl ModelConfigurationResolver for LoadingConfigurationResolver {
2201        type ArtifactPlan = ();
2202
2203        fn resolve_safetensors(
2204            &self,
2205            json: &serde_json::Value,
2206        ) -> Result<crate::ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
2207            Ok(crate::ResolvedModelConfiguration::new(
2208                crate::ModelConfiguration::new(
2209                    "llama",
2210                    "llama",
2211                    "llama",
2212                    crate::LoadingProtocol::Model,
2213                    Some(json.clone()),
2214                )?,
2215                (),
2216            ))
2217        }
2218
2219        fn resolve_gguf(
2220            &self,
2221            architecture: &str,
2222            _checkpoint: &eredu_gguf::Checkpoint,
2223        ) -> Result<crate::ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
2224            if architecture != "llama" {
2225                return Err(ArtifactError::UnsupportedGgufArchitecture(
2226                    architecture.into(),
2227                ));
2228            }
2229            Ok(crate::ResolvedModelConfiguration::new(
2230                crate::ModelConfiguration::new(
2231                    architecture,
2232                    architecture,
2233                    "llama",
2234                    crate::LoadingProtocol::Model,
2235                    None,
2236                )?,
2237                (),
2238            ))
2239        }
2240
2241        fn gguf_companion_requirements(
2242            &self,
2243            _architecture: &str,
2244            _checkpoint: &eredu_gguf::Checkpoint,
2245        ) -> Result<Vec<crate::GgufCompanionRequirement>, ArtifactError> {
2246            Ok(Vec::new())
2247        }
2248    }
2249
2250    static LOADING_CONFIGURATION_RESOLVER: LoadingConfigurationResolver =
2251        LoadingConfigurationResolver;
2252
2253    impl BackendProvider for LoadingMock {
2254        type ModelConfig = (ModelPreparationPlan, u32);
2255        type Model = u32;
2256        type Session = LoadingMockSession;
2257        type Error = std::convert::Infallible;
2258
2259        fn descriptor(&self) -> BackendDescriptor {
2260            BackendDescriptor::new("loading-mock", "1")
2261        }
2262
2263        fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error> {
2264            Ok(Vec::new())
2265        }
2266
2267        fn prepare_model(
2268            &self,
2269            (plan, model): Self::ModelConfig,
2270        ) -> Result<PreparedModel<Self::Model>, Self::Error> {
2271            self.materializations
2272                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2273            assert_eq!(plan.inspection().configuration().family(), "llama");
2274            Ok(PreparedModel::new(
2275                model,
2276                plan.admitted_session_capabilities(),
2277            ))
2278        }
2279
2280        fn create_session(
2281            &self,
2282            _: PreparedModel<Self::Model>,
2283        ) -> Result<Self::Session, Self::Error> {
2284            Ok(LoadingMockSession)
2285        }
2286    }
2287
2288    impl BackendSession<LoadingMock> for LoadingMockSession {
2289        type PrefillInput = ();
2290        type DecodeInput = ();
2291        type Output = ();
2292        type Completion = LoadingDone;
2293
2294        fn capabilities(&self) -> SessionCapabilities {
2295            SessionCapabilities::default()
2296        }
2297
2298        fn prefill(
2299            &mut self,
2300            _: &LoadingMock,
2301            _: (),
2302        ) -> Result<Submission<(), LoadingDone>, std::convert::Infallible> {
2303            Ok(Submission {
2304                output: (),
2305                completion: LoadingDone,
2306            })
2307        }
2308
2309        fn decode(
2310            &mut self,
2311            _: &LoadingMock,
2312            _: (),
2313        ) -> Result<Submission<(), LoadingDone>, std::convert::Infallible> {
2314            Ok(Submission {
2315                output: (),
2316                completion: LoadingDone,
2317            })
2318        }
2319
2320        fn observe_output(
2321            &self,
2322            _: &LoadingMock,
2323            _: &(),
2324        ) -> Result<ObservationSet, std::convert::Infallible> {
2325            Ok(ObservationSet::new())
2326        }
2327    }
2328
2329    #[derive(Debug, Clone, Copy)]
2330    struct LoadingDone;
2331
2332    impl Completion for LoadingDone {
2333        type Error = std::convert::Infallible;
2334
2335        fn is_complete(&self) -> Result<bool, Self::Error> {
2336            Ok(true)
2337        }
2338
2339        fn wait(&self) -> Result<(), Self::Error> {
2340            Ok(())
2341        }
2342    }
2343
2344    impl ModelLoadingBackend for LoadingMock {
2345        type LoadOptions = u32;
2346        type SelectedPreparation = (u32, crate::PreparationAdmission);
2347        type ConfigurationResolver = LoadingConfigurationResolver;
2348
2349        fn configuration_resolver(&self) -> &Self::ConfigurationResolver {
2350            &LOADING_CONFIGURATION_RESOLVER
2351        }
2352
2353        fn select_preparation(
2354            &self,
2355            _: &ArtifactInspection,
2356            options: &Self::LoadOptions,
2357        ) -> Result<Self::SelectedPreparation, Self::Error> {
2358            self.selections
2359                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2360            let policy = crate::PreparationPolicy::default().with_required_session_capabilities(
2361                SessionCapabilities::default().with_activation_inspection(*options == 99),
2362            );
2363            let request = crate::PreparationAdmissionRequest::new(
2364                crate::LoadingProtocol::Model,
2365                crate::ArtifactFormat::SafeTensors,
2366                policy,
2367                crate::ArchitecturePreparationCapabilities::new(
2368                    false,
2369                    true,
2370                    false,
2371                    false,
2372                    false,
2373                    crate::InputModalities::TEXT,
2374                ),
2375            );
2376            let admission = crate::admit_preparation(
2377                request,
2378                crate::PreparationMechanismCapabilities::new(true, true)
2379                    .with_residency(crate::ResidencyRequest::FullyResident, true)
2380                    .with_input_modalities(crate::InputModalities::TEXT)
2381                    .with_session(
2382                        SessionCapabilities::default().with_activation_inspection(*options == 99),
2383                    ),
2384            )
2385            .expect("mock admission facts are coherent");
2386            Ok((*options, admission))
2387        }
2388
2389        fn selected_preparation_admission(
2390            &self,
2391            selected: &Self::SelectedPreparation,
2392        ) -> crate::PreparationAdmission {
2393            selected.1
2394        }
2395
2396        fn model_config(
2397            &self,
2398            selected: SelectedModelPreparation<Self>,
2399        ) -> Result<Self::ModelConfig, Self::Error> {
2400            let (plan, (selected, _admission)) = selected.into_parts();
2401            Ok((plan, selected))
2402        }
2403    }
2404
2405    fn write_loading_fixture(root: &Path) {
2406        std::fs::write(root.join("config.json"), r#"{"model_type":"llama"}"#).unwrap();
2407        let header = br#"{"token_embd.weight":{"dtype":"F32","shape":[1],"data_offsets":[0,4]}}"#;
2408        let mut file = std::fs::File::create(root.join("model.safetensors")).unwrap();
2409        file.write_all(&(header.len() as u64).to_le_bytes())
2410            .unwrap();
2411        file.write_all(header).unwrap();
2412        file.write_all(&[0; 4]).unwrap();
2413    }
2414    struct MockSession {
2415        model: u32,
2416        tokens: Vec<u32>,
2417        distributed: Option<MockDistributed>,
2418    }
2419    impl BackendSession<Mock> for MockSession {
2420        type PrefillInput = Vec<u32>;
2421        type DecodeInput = u32;
2422        type Output = u32;
2423        type Completion = Done;
2424        fn capabilities(&self) -> SessionCapabilities {
2425            SessionCapabilities::default()
2426        }
2427        fn prefill(
2428            &mut self,
2429            _: &Mock,
2430            input: Vec<u32>,
2431        ) -> Result<Submission<u32, Done>, Infallible> {
2432            self.tokens.extend(input);
2433            Ok(Submission {
2434                output: self.tokens.len() as u32 + self.model,
2435                completion: Done,
2436            })
2437        }
2438        fn decode(&mut self, _: &Mock, input: u32) -> Result<Submission<u32, Done>, Infallible> {
2439            self.tokens.push(input);
2440            Ok(Submission {
2441                output: self.tokens.len() as u32 + self.model,
2442                completion: Done,
2443            })
2444        }
2445
2446        fn observe_output(&self, _: &Mock, output: &u32) -> Result<ObservationSet, Infallible> {
2447            let mut observations = ObservationSet::new();
2448            observations
2449                .insert(
2450                    "mock.output",
2451                    crate::ObservationValue::Unsigned(u64::from(*output)),
2452                )
2453                .unwrap();
2454            Ok(observations)
2455        }
2456    }
2457
2458    impl TextGenerationBackend for Mock {
2459        type Prompt = Vec<u32>;
2460        type Token = u32;
2461        type TextGenerationState = (u32, u64);
2462        type TextCompletion = Done;
2463
2464        fn start_text_generation(
2465            _: &Self,
2466            config: TextGenerationConfig,
2467        ) -> Result<Self::TextGenerationState, Self::Error> {
2468            Ok((config.sampling().top_k as u32, config.seed()))
2469        }
2470
2471        fn prepare_text_prompt(
2472            _: &Self,
2473            prompt_token_ids: Vec<u32>,
2474        ) -> Result<Self::Prompt, Self::Error> {
2475            Ok(prompt_token_ids)
2476        }
2477
2478        fn submit_text_prefill(
2479            runtime: &mut ModelRuntime<Self>,
2480            prompt: Self::Prompt,
2481            filter: &TokenFilter,
2482            state: &mut Self::TextGenerationState,
2483        ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error> {
2484            let submission = runtime.prefill(prompt)?;
2485            Ok(Submission {
2486                output: apply_mock_filter(submission.output + state.0 + state.1 as u32, filter),
2487                completion: submission.completion,
2488            })
2489        }
2490
2491        fn submit_text_decode(
2492            runtime: &mut ModelRuntime<Self>,
2493            token: Self::Token,
2494            filter: &TokenFilter,
2495            _: &mut Self::TextGenerationState,
2496        ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error> {
2497            let submission = runtime.decode(token)?;
2498            Ok(Submission {
2499                output: apply_mock_filter(submission.output, filter),
2500                completion: submission.completion,
2501            })
2502        }
2503    }
2504
2505    impl MultimodalPreparationBackend for Mock {
2506        fn prepare_multimodal_input<E>(
2507            _: &ModelRuntime<Self>,
2508            request: &TokenizedMultimodalRequest,
2509            _: &mut dyn FnMut(&str) -> Result<Vec<u32>, E>,
2510        ) -> Result<Self::Prompt, MultimodalPreparationFailure<Self::Error, E>>
2511        where
2512            E: std::error::Error + Send + Sync + 'static,
2513        {
2514            let mut prompt = Vec::new();
2515            for segment in request.segments() {
2516                match segment {
2517                    crate::TokenizedMultimodalSegment::TokenIds(ids) => {
2518                        prompt.extend_from_slice(ids);
2519                    }
2520                    crate::TokenizedMultimodalSegment::Media(crate::Media::Image(_)) => {
2521                        prompt.push(1_001);
2522                    }
2523                    crate::TokenizedMultimodalSegment::Media(crate::Media::Video(_)) => {
2524                        prompt.push(1_002);
2525                    }
2526                    crate::TokenizedMultimodalSegment::Media(crate::Media::Audio(_)) => {
2527                        prompt.push(1_003);
2528                    }
2529                }
2530            }
2531            Ok(prompt)
2532        }
2533    }
2534
2535    impl ModelCapabilityBackend for Mock {
2536        fn model_capabilities(
2537            _: &ModelRuntime<Self>,
2538        ) -> Result<ModelCapabilities, CapabilityError> {
2539            Ok(ModelCapabilities {
2540                effective_model_type: "mock".into(),
2541                native_max_context: crate::Observed::exact(64, "mock configuration"),
2542                effective_max_context: crate::Observed::exact(64, "mock configuration"),
2543                state_strategy: crate::CacheStateStrategy::FullKv,
2544                modalities: crate::InputModalities::TEXT,
2545                estimation: crate::EstimationCompleteness::Complete,
2546            })
2547        }
2548
2549        fn count_prepared_input(
2550            _: &ModelRuntime<Self>,
2551            input: &Self::Prompt,
2552        ) -> Result<InputTokenCount, CapabilityError> {
2553            Ok(InputTokenCount::text(input.len() as u64))
2554        }
2555
2556        fn estimate_runtime_state(
2557            _: &ModelRuntime<Self>,
2558            input: InputTokenCount,
2559            max_output_tokens: u64,
2560            batch_size: u64,
2561        ) -> Result<RuntimeStateEstimate, CapabilityError> {
2562            crate::estimate_runtime_state(
2563                &crate::StateMemoryLayout::new(
2564                    crate::LayerSchedule::new(
2565                        1,
2566                        vec![crate::cache::LayerCachePolicy::key_only(
2567                            crate::AttentionPolicy::Full,
2568                            1,
2569                            2,
2570                        )
2571                        .unwrap()],
2572                    )
2573                    .unwrap(),
2574                    vec![0],
2575                    1,
2576                    1,
2577                    crate::EstimationCompleteness::Complete,
2578                )
2579                .unwrap(),
2580                input,
2581                max_output_tokens,
2582                batch_size,
2583                std::num::NonZeroU8::new(4).unwrap(),
2584            )
2585        }
2586
2587        fn static_memory(
2588            runtime: &ModelRuntime<Self>,
2589        ) -> Result<StaticMemoryReport, CapabilityError> {
2590            let unavailable = || crate::Observed::unavailable("mock does not expose this counter");
2591            Ok(StaticMemoryReport {
2592                logical_parameter_bytes: crate::Observed::exact(
2593                    u64::from(runtime.session().model),
2594                    "mock model",
2595                ),
2596                current_host_resident_bytes: unavailable(),
2597                current_device_resident_bytes: unavailable(),
2598                planned_disk_backed_bytes: unavailable(),
2599                backend_active_allocation_bytes: unavailable(),
2600                backend_allocator_cache_bytes: unavailable(),
2601                physical_semantics: crate::PhysicalMemorySemantics::Unknown,
2602                currently_cached_shards: unavailable(),
2603            })
2604        }
2605    }
2606
2607    fn apply_mock_filter(candidate: u32, filter: &TokenFilter) -> u32 {
2608        let Some(allowed) = filter.allowed_mask() else {
2609            return candidate;
2610        };
2611        allowed
2612            .get(candidate as usize)
2613            .copied()
2614            .unwrap_or(false)
2615            .then_some(candidate)
2616            .or_else(|| {
2617                allowed
2618                    .iter()
2619                    .position(|allowed| *allowed)
2620                    .map(|token| token as u32)
2621            })
2622            .expect("validated token filters allow at least one token")
2623    }
2624
2625    #[test]
2626    fn generic_loader_inspects_plans_and_prepares_on_the_selected_backend() {
2627        let root = tempfile::tempdir().unwrap();
2628        write_loading_fixture(root.path());
2629        let prepared = load_model(&LoadingMock::default(), root.path(), 41).unwrap();
2630        assert_eq!(*prepared, 41);
2631
2632        let runtime = ModelRuntime::load(LoadingMock::default(), root.path(), 7).unwrap();
2633        assert_eq!(runtime.backend().descriptor().name, "loading-mock");
2634
2635        let missing = root.path().join("missing");
2636        assert!(matches!(
2637            load_model(&LoadingMock::default(), &missing, 1),
2638            Err(ModelLoadError::Artifact(ArtifactError::MissingArtifact(path)))
2639                if path == missing
2640        ));
2641    }
2642
2643    #[test]
2644    fn session_requirement_is_retained_by_the_single_admission() {
2645        let root = tempfile::tempdir().unwrap();
2646        write_loading_fixture(root.path());
2647        let backend = LoadingMock::default();
2648
2649        let prepared = load_model(&backend, root.path(), 99).unwrap();
2650
2651        assert_eq!(*prepared, 99);
2652        assert_eq!(
2653            backend
2654                .selections
2655                .load(std::sync::atomic::Ordering::Relaxed),
2656            1
2657        );
2658        assert_eq!(
2659            backend
2660                .materializations
2661                .load(std::sync::atomic::Ordering::Relaxed),
2662            1
2663        );
2664    }
2665
2666    struct FixedController {
2667        tokens: Vec<u32>,
2668        committed: usize,
2669    }
2670
2671    impl TokenFilterController for FixedController {
2672        type Error = Infallible;
2673
2674        fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
2675            let mut allowed = vec![false; 64];
2676            allowed[self.tokens[self.committed] as usize] = true;
2677            Ok(TokenFilter::allowed(allowed).unwrap())
2678        }
2679
2680        fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error> {
2681            assert_eq!(token_id, self.tokens[self.committed]);
2682            self.committed += 1;
2683            Ok(())
2684        }
2685
2686        fn is_complete(&mut self) -> Result<bool, Self::Error> {
2687            Ok(self.committed == self.tokens.len())
2688        }
2689    }
2690
2691    #[test]
2692    fn mock_prefill_and_multiple_decode_steps() {
2693        let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2694        let prefill = runtime.prefill(vec![1, 2]).unwrap();
2695        assert_eq!(prefill.output, 12);
2696        assert!(prefill.completion.is_complete().unwrap());
2697        assert_eq!(runtime.decode(3).unwrap().output, 13);
2698        assert_eq!(runtime.decode(4).unwrap().output, 14);
2699    }
2700
2701    #[test]
2702    fn portable_text_generation_prefills_and_decodes_without_tensor_types() {
2703        let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2704        let sampling = crate::resolve_generation_config(
2705            None,
2706            crate::GenerationConfigOverrides {
2707                max_new_tokens: Some(3),
2708                ..Default::default()
2709            },
2710        )
2711        .unwrap();
2712        let mut generation = TextGeneration::new(
2713            &mut runtime,
2714            vec![1, 2],
2715            TextGenerationConfig::new(sampling).with_seed(3),
2716        )
2717        .unwrap();
2718        assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 55);
2719        assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 13);
2720        assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 14);
2721        assert!(generation.next().is_none());
2722    }
2723
2724    #[test]
2725    fn portable_media_preparation_feeds_the_existing_generation_contract() {
2726        let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2727        let request = crate::MultimodalRequest::new(vec![
2728            crate::MultimodalSegment::TokenIds(vec![7, 8]),
2729            crate::MultimodalSegment::Media(crate::Media::Image(
2730                crate::RgbImage::new(vec![5, 6, 7], 1, 1).unwrap(),
2731            )),
2732            crate::MultimodalSegment::TokenIds(vec![9]),
2733        ])
2734        .unwrap()
2735        .tokenize::<Infallible>(|_| unreachable!("request is already tokenized"))
2736        .unwrap();
2737        let prompt = Mock::prepare_multimodal_input(&runtime, &request, &mut |_| {
2738            Ok::<_, Infallible>(Vec::new())
2739        })
2740        .unwrap();
2741        assert_eq!(prompt, vec![7, 8, 1_001, 9]);
2742
2743        let sampling = crate::resolve_generation_config(
2744            None,
2745            crate::GenerationConfigOverrides {
2746                max_new_tokens: Some(2),
2747                ..Default::default()
2748            },
2749        )
2750        .unwrap();
2751        let mut generation =
2752            TextGeneration::from_prompt(&mut runtime, prompt, TextGenerationConfig::new(sampling))
2753                .unwrap();
2754        assert!(generation.next().unwrap().is_ok());
2755        assert!(generation.next().unwrap().is_ok());
2756        assert!(generation.next().is_none());
2757    }
2758
2759    #[test]
2760    fn model_capability_extension_observes_the_selected_mock_session() {
2761        let runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2762        let capabilities = Mock::model_capabilities(&runtime).unwrap();
2763        assert_eq!(capabilities.effective_model_type, "mock");
2764        let input = Mock::count_prepared_input(&runtime, &vec![1, 2, 3]).unwrap();
2765        assert_eq!(input.model_positions, 3);
2766        let state = Mock::estimate_runtime_state(&runtime, input, 2, 1).unwrap();
2767        assert_eq!(state.requested_state_bytes, 5 * 2 * 4);
2768        assert_eq!(
2769            Mock::static_memory(&runtime)
2770                .unwrap()
2771                .logical_parameter_bytes
2772                .value(),
2773            Some(&10)
2774        );
2775    }
2776
2777    #[test]
2778    fn controlled_generation_applies_portable_filters_and_commits_tokens() {
2779        let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2780        let sampling = crate::resolve_generation_config(
2781            None,
2782            crate::GenerationConfigOverrides {
2783                max_new_tokens: Some(2),
2784                ..Default::default()
2785            },
2786        )
2787        .unwrap();
2788        let controller = FixedController {
2789            tokens: vec![7, 8],
2790            committed: 0,
2791        };
2792        let mut generation = ControlledTextGeneration::new(
2793            &mut runtime,
2794            vec![1, 2],
2795            TextGenerationConfig::new(sampling),
2796            controller,
2797        )
2798        .unwrap();
2799        assert_eq!(generation.next().unwrap().unwrap().token_id(), 7);
2800        assert_eq!(generation.next().unwrap().unwrap().token_id(), 8);
2801        assert!(generation.controller_mut().is_complete().unwrap());
2802        assert!(generation.next().is_none());
2803    }
2804
2805    fn continuation_config(limit: usize) -> TextGenerationConfig {
2806        TextGenerationConfig::new(
2807            crate::resolve_generation_config(
2808                None,
2809                crate::GenerationConfigOverrides {
2810                    max_new_tokens: Some(limit),
2811                    ..Default::default()
2812                },
2813            )
2814            .unwrap(),
2815        )
2816    }
2817
2818    #[test]
2819    fn detached_ordinary_continuation_preserves_pending_input_and_commit_order() {
2820        let controller = || FixedController {
2821            tokens: vec![7, 8, 9],
2822            committed: 0,
2823        };
2824        let mut ordinary_runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2825        let ordinary: Vec<_> = ControlledTextGeneration::new(
2826            &mut ordinary_runtime,
2827            vec![1, 2],
2828            continuation_config(3),
2829            controller(),
2830        )
2831        .unwrap()
2832        .map(|token| token.unwrap().token_id())
2833        .collect();
2834
2835        let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2836        let mut driver = TextGenerationDriver::new(&mut runtime);
2837        let mut state = driver
2838            .start(vec![1, 2], continuation_config(3), controller())
2839            .unwrap();
2840        state.require_quiescent().unwrap();
2841        assert!(state.is_prefill_pending());
2842        assert!(driver.runtime().session().tokens.is_empty());
2843        let mut actual = Vec::new();
2844        for (index, expected) in ordinary.iter().enumerate() {
2845            let token = driver.advance(&mut state).unwrap().unwrap().token_id();
2846            actual.push(token);
2847            assert_eq!(token, *expected);
2848            assert_eq!(state.controller().committed, index + 1);
2849            assert_eq!(state.remaining_tokens(), Some(2 - index));
2850            // The newest token has been committed but is not in model state.
2851            let mut model_inputs = vec![1, 2];
2852            model_inputs.extend_from_slice(&ordinary[..index]);
2853            assert_eq!(driver.runtime().session().tokens, model_inputs);
2854            assert!(!state.is_prefill_pending());
2855            assert!(matches!(
2856                driver.advance(&mut state),
2857                Err(TextContinuationError::NotQuiescent)
2858            ));
2859            assert_eq!(state.controller().committed, index + 1);
2860            assert!(driver.take_completed_step(&mut state).unwrap().is_none());
2861            state.require_quiescent().unwrap();
2862        }
2863        assert!(driver.advance(&mut state).unwrap().is_none());
2864        assert_eq!(actual, ordinary);
2865        assert_eq!(
2866            driver.runtime().session().tokens,
2867            ordinary_runtime.session().tokens
2868        );
2869    }
2870
2871    #[test]
2872    fn detached_continuation_cannot_attach_to_another_driver() {
2873        let mut first = ModelRuntime::prepare(Mock, 10).unwrap();
2874        let mut other = ModelRuntime::prepare(Mock, 10).unwrap();
2875        let mut owner = TextGenerationDriver::new(&mut first);
2876        let mut state = owner
2877            .start(vec![1, 2], continuation_config(2), UnconstrainedTokens)
2878            .unwrap();
2879        let mut foreign = TextGenerationDriver::new(&mut other);
2880        assert!(matches!(
2881            foreign.advance(&mut state),
2882            Err(TextContinuationError::IncompatibleDriver)
2883        ));
2884        assert!(foreign.runtime().session().tokens.is_empty());
2885        assert!(owner.advance(&mut state).unwrap().is_some());
2886        assert!(matches!(
2887            foreign.take_completed_step(&mut state),
2888            Err(TextContinuationError::IncompatibleDriver)
2889        ));
2890        owner.take_completed_step(&mut state).unwrap();
2891        drop(owner);
2892        let mut replacement = TextGenerationDriver::new(&mut first);
2893        assert!(matches!(
2894            replacement.advance(&mut state),
2895            Err(TextContinuationError::IncompatibleDriver)
2896        ));
2897        assert_eq!(replacement.runtime().session().tokens, vec![1, 2]);
2898    }
2899
2900    #[test]
2901    fn detached_continuation_failure_remains_fenced_after_draining() {
2902        struct RejectCommit;
2903        impl TokenFilterController for RejectCommit {
2904            type Error = std::io::Error;
2905            fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
2906                Ok(TokenFilter::All)
2907            }
2908            fn commit_token(&mut self, _: u32) -> Result<(), Self::Error> {
2909                Err(std::io::Error::other("commit rejected"))
2910            }
2911            fn is_complete(&mut self) -> Result<bool, Self::Error> {
2912                Ok(false)
2913            }
2914        }
2915        let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2916        let mut driver = TextGenerationDriver::new(&mut runtime);
2917        let mut state = driver
2918            .start(vec![1, 2], continuation_config(3), RejectCommit)
2919            .unwrap();
2920        assert!(matches!(
2921            driver.advance(&mut state),
2922            Err(TextContinuationError::Generation(
2923                ControlledTextGenerationError::Controller(_)
2924            ))
2925        ));
2926        driver.take_completed_step(&mut state).unwrap();
2927        assert!(matches!(
2928            state.require_quiescent(),
2929            Err(TextContinuationError::Failed)
2930        ));
2931        assert!(matches!(
2932            driver.advance(&mut state),
2933            Err(TextContinuationError::Failed)
2934        ));
2935        assert_eq!(driver.runtime().session().tokens, vec![1, 2]);
2936    }
2937
2938    #[test]
2939    fn detached_continuation_caught_unwind_cannot_be_resumed() {
2940        struct PanickingFilter;
2941        impl TokenFilterController for PanickingFilter {
2942            type Error = Infallible;
2943            fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
2944                panic!("filter failed while preparing the decision")
2945            }
2946            fn commit_token(&mut self, _: u32) -> Result<(), Self::Error> {
2947                Ok(())
2948            }
2949            fn is_complete(&mut self) -> Result<bool, Self::Error> {
2950                Ok(false)
2951            }
2952        }
2953        let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2954        let mut driver = TextGenerationDriver::new(&mut runtime);
2955        let mut state = driver
2956            .start(vec![1, 2], continuation_config(3), PanickingFilter)
2957            .unwrap();
2958        assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2959            driver.advance(&mut state)
2960        }))
2961        .is_err());
2962        driver.take_completed_step(&mut state).unwrap();
2963        assert!(matches!(
2964            state.require_quiescent(),
2965            Err(TextContinuationError::Failed)
2966        ));
2967        assert!(matches!(
2968            driver.advance(&mut state),
2969            Err(TextContinuationError::Failed)
2970        ));
2971        assert!(driver.runtime().session().tokens.is_empty());
2972    }
2973
2974    #[derive(Debug, Clone)]
2975    struct MockDistributed {
2976        descriptor: DistributedSessionDescriptor,
2977    }
2978
2979    impl DistributedSession for MockDistributed {
2980        type Value = Vec<u32>;
2981        type Completion = Done;
2982        type Error = Infallible;
2983
2984        fn descriptor(&self) -> DistributedSessionDescriptor {
2985            self.descriptor.clone()
2986        }
2987
2988        fn capabilities(&self) -> DistributedCapabilities {
2989            DistributedCapabilities::new(true, [CollectiveGroupId::new(7)], true, true, true)
2990        }
2991
2992        fn all_reduce_sum(
2993            &self,
2994            _: CollectiveScope,
2995            input: &Vec<u32>,
2996        ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2997            Ok(Submission {
2998                output: input.iter().map(|value| value * 2).collect(),
2999                completion: Done,
3000            })
3001        }
3002
3003        fn all_gather(
3004            &self,
3005            _: CollectiveScope,
3006            input: &Vec<u32>,
3007        ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3008            let mut output = input.clone();
3009            output.extend(input);
3010            Ok(Submission {
3011                output,
3012                completion: Done,
3013            })
3014        }
3015
3016        fn all_to_all_v(
3017            &self,
3018            _: CollectiveScope,
3019            input: &Vec<u32>,
3020            _: &[usize],
3021            _: &[usize],
3022        ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3023            Ok(Submission {
3024                output: input.clone(),
3025                completion: Done,
3026            })
3027        }
3028
3029        fn send(
3030            &self,
3031            _: CollectiveScope,
3032            _: usize,
3033            input: &Vec<u32>,
3034        ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3035            Ok(Submission {
3036                output: input.clone(),
3037                completion: Done,
3038            })
3039        }
3040
3041        fn receive(
3042            &self,
3043            _: CollectiveScope,
3044            peer: usize,
3045            value: &ValueDescriptor,
3046        ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
3047            Ok(Submission {
3048                output: vec![peer as u32; value.shape().iter().product()],
3049                completion: Done,
3050            })
3051        }
3052
3053        fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Infallible> {
3054            let mut output = local.to_vec();
3055            output.extend_from_slice(local);
3056            Ok(output)
3057        }
3058    }
3059
3060    impl DistributedBackend for Mock {
3061        type DistributedSession = MockDistributed;
3062
3063        fn distributed_session(session: &MockSession) -> Option<&Self::DistributedSession> {
3064            session.distributed.as_ref()
3065        }
3066    }
3067
3068    #[test]
3069    fn mock_distributed_session_owns_collective_and_transfer_lifecycle() {
3070        let tensor_group =
3071            CollectiveGroupDescriptor::new(CollectiveGroupId::new(7), vec![0, 1], 0).unwrap();
3072        let session = MockDistributed {
3073            descriptor: DistributedSessionDescriptor::new(2, 0, vec![tensor_group]).unwrap(),
3074        };
3075        let capabilities = session.capabilities();
3076        assert!(capabilities.exact_completion());
3077        assert_eq!(
3078            capabilities.collective_groups(),
3079            &[CollectiveGroupId::new(7)]
3080        );
3081        assert_eq!(
3082            session
3083                .all_reduce_sum(
3084                    CollectiveScope::Group(CollectiveGroupId::new(7)),
3085                    &vec![2, 3]
3086                )
3087                .unwrap()
3088                .wait()
3089                .unwrap(),
3090            vec![4, 6]
3091        );
3092        assert_eq!(
3093            session
3094                .receive(
3095                    CollectiveScope::World,
3096                    1,
3097                    &ValueDescriptor::new(vec![2], TensorDtype::U32).unwrap(),
3098                )
3099                .unwrap()
3100                .wait()
3101                .unwrap(),
3102            vec![1, 1]
3103        );
3104        assert_eq!(session.all_gather_words(&[7]).unwrap(), vec![7, 7]);
3105
3106        let model_session = MockSession {
3107            model: 0,
3108            tokens: Vec::new(),
3109            distributed: Some(session.clone()),
3110        };
3111        assert_eq!(
3112            Mock::distributed_session(&model_session)
3113                .unwrap()
3114                .descriptor(),
3115            session.descriptor()
3116        );
3117    }
3118
3119    #[test]
3120    fn distributed_descriptors_round_trip_and_reject_invalid_ranks() {
3121        let descriptor = DistributedSessionDescriptor::new(
3122            6,
3123            4,
3124            vec![CollectiveGroupDescriptor::new(CollectiveGroupId::new(9), vec![1, 4], 1).unwrap()],
3125        )
3126        .unwrap();
3127        let encoded = serde_json::to_string(&descriptor).unwrap();
3128        assert_eq!(
3129            serde_json::from_str::<DistributedSessionDescriptor>(&encoded).unwrap(),
3130            descriptor
3131        );
3132        let scope = CollectiveScope::Group(CollectiveGroupId::new(9));
3133        assert_eq!(
3134            serde_json::from_str::<CollectiveScope>(&serde_json::to_string(&scope).unwrap())
3135                .unwrap(),
3136            scope
3137        );
3138        assert!(DistributedSessionDescriptor::new(descriptor.world_size(), 6, Vec::new()).is_err());
3139        assert!(serde_json::from_str::<DistributedSessionDescriptor>(
3140            r#"{"world_size":6,"rank":6,"groups":[]}"#
3141        )
3142        .is_err());
3143    }
3144
3145    #[test]
3146    fn distributed_commit_epoch_round_trips_and_rejects_zero() {
3147        let outcome = DistributedCommitOutcome::Indeterminate {
3148            epoch: DistributedCommitEpoch::new(17).unwrap(),
3149            phase: DistributedCommitPhase::DecisionCompletion,
3150        };
3151        let encoded = serde_json::to_string(&outcome).unwrap();
3152        assert_eq!(
3153            serde_json::from_str::<DistributedCommitOutcome>(&encoded).unwrap(),
3154            outcome
3155        );
3156        assert!(serde_json::from_str::<DistributedCommitEpoch>("0").is_err());
3157    }
3158}