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