Skip to main content

eredu_core/
backend.rs

1//! High-level contract implemented once per execution backend.
2
3use serde::{Deserialize, Serialize};
4use std::{fmt::Debug, path::Path};
5
6use crate::{
7    artifact::{
8        inspect_artifact, plan_model_preparation, ArtifactError, ArtifactInspection,
9        ModelConfigurationResolver, ModelPreparationPlan, PreparationPolicy,
10    },
11    capability::{
12        CapabilityError, InputTokenCount, ModelCapabilities, RuntimeStateEstimate,
13        StaticMemoryReport,
14    },
15    checkpoint::TensorDtype,
16    generation::{GenerationError, ResolvedGenerationConfig},
17    media::TokenizedMultimodalRequest,
18    observation::{InspectedOutput, ObservationRequest, ObservationSet},
19};
20
21/// Stable, extensible description of an execution backend.
22#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
23pub struct BackendDescriptor {
24    /// Backend implementation name, such as `example-backend`.
25    name: String,
26    /// Backend implementation version.
27    version: String,
28}
29
30impl BackendDescriptor {
31    /// Creates a backend identity without freezing future descriptor fields.
32    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
33        Self {
34            name: name.into(),
35            version: version.into(),
36        }
37    }
38
39    /// Returns the backend implementation name.
40    pub fn name(&self) -> &str {
41        &self.name
42    }
43
44    /// Returns the backend implementation version.
45    pub fn version(&self) -> &str {
46        &self.version
47    }
48}
49
50/// Portable description of one backend-visible device.
51#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
52pub struct DeviceDescriptor {
53    /// Backend-stable device identifier.
54    id: String,
55    /// Human-readable device name.
56    name: String,
57    /// Backend-specific device family without a closed core enum.
58    family: String,
59    /// Total memory when discoverable.
60    memory_bytes: Option<u64>,
61}
62
63impl DeviceDescriptor {
64    /// Creates a backend-stable device description.
65    pub fn new(
66        id: impl Into<String>,
67        name: impl Into<String>,
68        family: impl Into<String>,
69        memory_bytes: Option<u64>,
70    ) -> Self {
71        Self {
72            id: id.into(),
73            name: name.into(),
74            family: family.into(),
75            memory_bytes,
76        }
77    }
78
79    /// Returns the backend-stable device identifier.
80    pub fn id(&self) -> &str {
81        &self.id
82    }
83    /// Returns the human-readable device name.
84    pub fn name(&self) -> &str {
85        &self.name
86    }
87    /// Returns the backend-defined device family.
88    pub fn family(&self) -> &str {
89        &self.family
90    }
91    /// Returns total device memory when known.
92    pub const fn memory_bytes(&self) -> Option<u64> {
93        self.memory_bytes
94    }
95}
96
97/// Fail-closed capabilities discovered from a backend and device.
98#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
99pub struct DeviceCapabilities {
100    /// Supports exact completion observation for submissions.
101    exact_completion: bool,
102    /// Supports device-to-device transfer for backend-owned values.
103    transfers: bool,
104    /// Supports collective execution for a complete session.
105    collectives: bool,
106}
107
108impl DeviceCapabilities {
109    /// Creates an exact fail-closed device mechanism report.
110    pub const fn new(exact_completion: bool, transfers: bool, collectives: bool) -> Self {
111        Self {
112            exact_completion,
113            transfers,
114            collectives,
115        }
116    }
117
118    /// Returns whether exact completion observation is available.
119    pub const fn exact_completion(&self) -> bool {
120        self.exact_completion
121    }
122    /// Returns whether device transfers are available.
123    pub const fn transfers(&self) -> bool {
124        self.transfers
125    }
126    /// Returns whether collective execution is available.
127    pub const fn collectives(&self) -> bool {
128        self.collectives
129    }
130}
131
132/// Fail-closed capabilities of one exact prepared model session.
133#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
134pub struct SessionCapabilities {
135    /// Supports backend-managed persistent decode caches.
136    persistent_cache: bool,
137    /// Supports explicit host observation of completed session outputs.
138    output_observation: bool,
139    /// Supports named activation inspection for instrumented session passes.
140    activation_inspection: bool,
141}
142
143impl SessionCapabilities {
144    /// Creates an exact fail-closed session mechanism report.
145    pub const fn new(
146        persistent_cache: bool,
147        output_observation: bool,
148        activation_inspection: bool,
149    ) -> Self {
150        Self {
151            persistent_cache,
152            output_observation,
153            activation_inspection,
154        }
155    }
156
157    /// Returns whether persistent cache storage is available.
158    pub const fn persistent_cache(self) -> bool {
159        self.persistent_cache
160    }
161    /// Returns whether completed outputs may be observed on the host.
162    pub const fn output_observation(self) -> bool {
163        self.output_observation
164    }
165    /// Returns whether named activation inspection is available.
166    pub const fn activation_inspection(self) -> bool {
167        self.activation_inspection
168    }
169
170    /// Returns a report with persistent cache storage configured.
171    pub const fn with_persistent_cache(mut self, supported: bool) -> Self {
172        self.persistent_cache = supported;
173        self
174    }
175    /// Returns a report with output observation configured.
176    pub const fn with_output_observation(mut self, supported: bool) -> Self {
177        self.output_observation = supported;
178        self
179    }
180    /// Returns a report with activation inspection configured.
181    pub const fn with_activation_inspection(mut self, supported: bool) -> Self {
182        self.activation_inspection = supported;
183        self
184    }
185    /// Validates fail-closed requirements against an exact available report.
186    pub fn validate(&self, available: &Self) -> Result<(), SessionCapabilityError> {
187        for (required, supported, capability) in [
188            (
189                self.persistent_cache,
190                available.persistent_cache,
191                "persistent_cache",
192            ),
193            (
194                self.output_observation,
195                available.output_observation,
196                "output_observation",
197            ),
198            (
199                self.activation_inspection,
200                available.activation_inspection,
201                "activation_inspection",
202            ),
203        ] {
204            if required && !supported {
205                return Err(SessionCapabilityError { capability });
206            }
207        }
208        Ok(())
209    }
210}
211
212/// One unavailable exact-session requirement.
213#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
214#[error("prepared session does not support required capability {capability}")]
215pub struct SessionCapabilityError {
216    capability: &'static str,
217}
218
219impl SessionCapabilityError {
220    /// Returns the stable capability name.
221    pub const fn capability(self) -> &'static str {
222        self.capability
223    }
224}
225
226/// Fail-closed distributed operations exposed by one selected session.
227#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
228pub struct DistributedCapabilities {
229    world_collectives: bool,
230    collective_groups: Vec<CollectiveGroupId>,
231    point_to_point: bool,
232    variable_all_to_all: bool,
233    exact_completion: bool,
234}
235
236impl DistributedCapabilities {
237    /// Creates an exact mechanism capability report.
238    pub fn new(
239        world_collectives: bool,
240        collective_groups: impl IntoIterator<Item = CollectiveGroupId>,
241        point_to_point: bool,
242        variable_all_to_all: bool,
243        exact_completion: bool,
244    ) -> Self {
245        Self {
246            world_collectives,
247            collective_groups: collective_groups.into_iter().collect(),
248            point_to_point,
249            variable_all_to_all,
250            exact_completion,
251        }
252    }
253
254    /// Returns whether world-scoped collectives are available.
255    pub const fn world_collectives(&self) -> bool {
256        self.world_collectives
257    }
258    /// Returns opaque groups supporting collectives.
259    pub fn collective_groups(&self) -> &[CollectiveGroupId] {
260        &self.collective_groups
261    }
262    /// Returns whether point-to-point transfers are available.
263    pub const fn point_to_point(&self) -> bool {
264        self.point_to_point
265    }
266    /// Returns whether variable-count all-to-all is available.
267    pub const fn variable_all_to_all(&self) -> bool {
268        self.variable_all_to_all
269    }
270    /// Returns whether submissions have exact completion objects.
271    pub const fn exact_completion(&self) -> bool {
272        self.exact_completion
273    }
274}
275
276/// Opaque stable identity of a selected collective group.
277#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
278#[serde(transparent)]
279pub struct CollectiveGroupId(u32);
280
281impl CollectiveGroupId {
282    /// Creates an opaque group identity selected by architecture/runtime composition.
283    pub const fn new(value: u32) -> Self {
284        Self(value)
285    }
286    /// Returns the stable numeric representation for serialization and backend maps.
287    pub const fn value(self) -> u32 {
288        self.0
289    }
290}
291
292/// Ordered membership for one opaque collective group containing this rank.
293#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
294pub struct CollectiveGroupDescriptor {
295    id: CollectiveGroupId,
296    members: Vec<usize>,
297    local_rank: usize,
298}
299
300impl CollectiveGroupDescriptor {
301    /// Validates ordered group membership and the process-local rank.
302    pub fn new(
303        id: CollectiveGroupId,
304        members: Vec<usize>,
305        local_rank: usize,
306    ) -> Result<Self, BackendError> {
307        if members.is_empty() || local_rank >= members.len() {
308            return Err(BackendError::Preparation {
309                operation: "collective group realization".into(),
310                message: "collective membership must be non-empty and contain local rank".into(),
311            });
312        }
313        let mut unique = std::collections::BTreeSet::new();
314        if !members.iter().all(|rank| unique.insert(*rank)) {
315            return Err(BackendError::Preparation {
316                operation: "collective group realization".into(),
317                message: "collective membership contains duplicate world ranks".into(),
318            });
319        }
320        Ok(Self {
321            id,
322            members,
323            local_rank,
324        })
325    }
326
327    /// Returns the opaque group identity.
328    pub const fn id(&self) -> CollectiveGroupId {
329        self.id
330    }
331    /// Returns ordered world-rank membership.
332    pub fn members(&self) -> &[usize] {
333        &self.members
334    }
335    /// Returns this process's rank within the ordered group.
336    pub const fn local_rank(&self) -> usize {
337        self.local_rank
338    }
339}
340
341impl<'de> Deserialize<'de> for CollectiveGroupDescriptor {
342    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
343    where
344        D: serde::Deserializer<'de>,
345    {
346        #[derive(Deserialize)]
347        struct Raw {
348            id: CollectiveGroupId,
349            members: Vec<usize>,
350            local_rank: usize,
351        }
352        let raw = Raw::deserialize(deserializer)?;
353        Self::new(raw.id, raw.members, raw.local_rank).map_err(serde::de::Error::custom)
354    }
355}
356
357/// Scope of a collective or point-to-point operation.
358#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
359#[serde(tag = "kind", content = "group", rename_all = "snake_case")]
360#[non_exhaustive]
361pub enum CollectiveScope {
362    /// All ranks in the selected distributed session.
363    World,
364    /// One opaque selected collective group containing this rank.
365    Group(CollectiveGroupId),
366}
367
368/// Portable shape and element type for a backend-owned received value.
369#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
370pub struct ValueDescriptor {
371    /// Row-major logical shape. An empty shape describes a scalar.
372    shape: Vec<usize>,
373    /// Logical element type.
374    dtype: TensorDtype,
375}
376
377impl ValueDescriptor {
378    /// Validates a portable value shape and element type.
379    pub fn new(shape: Vec<usize>, dtype: TensorDtype) -> Result<Self, BackendError> {
380        if shape.contains(&0) {
381            return Err(BackendError::Preparation {
382                operation: "distributed value descriptor".into(),
383                message: "non-scalar distributed values require positive dimensions".into(),
384            });
385        }
386        Ok(Self { shape, dtype })
387    }
388
389    /// Returns the row-major logical shape.
390    pub fn shape(&self) -> &[usize] {
391        &self.shape
392    }
393
394    /// Returns the logical element type.
395    pub const fn dtype(&self) -> &TensorDtype {
396        &self.dtype
397    }
398}
399
400impl<'de> Deserialize<'de> for ValueDescriptor {
401    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
402    where
403        D: serde::Deserializer<'de>,
404    {
405        #[derive(Deserialize)]
406        struct RawDescriptor {
407            shape: Vec<usize>,
408            dtype: TensorDtype,
409        }
410
411        let raw = RawDescriptor::deserialize(deserializer)?;
412        Self::new(raw.shape, raw.dtype).map_err(serde::de::Error::custom)
413    }
414}
415
416/// Portable identity of one selected distributed session.
417#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
418pub struct DistributedSessionDescriptor {
419    world_size: usize,
420    rank: usize,
421    groups: Vec<CollectiveGroupDescriptor>,
422}
423
424impl DistributedSessionDescriptor {
425    /// Validates a mechanism-only distributed session realization.
426    pub fn new(
427        world_size: usize,
428        rank: usize,
429        groups: Vec<CollectiveGroupDescriptor>,
430    ) -> Result<Self, BackendError> {
431        if world_size == 0 || rank >= world_size {
432            return Err(BackendError::Preparation {
433                operation: "distributed session realization".into(),
434                message: format!("rank {rank} is outside world size {world_size}"),
435            });
436        }
437        let mut ids = std::collections::BTreeSet::new();
438        for group in &groups {
439            if !ids.insert(group.id())
440                || group.members().iter().any(|member| *member >= world_size)
441                || group.members()[group.local_rank()] != rank
442            {
443                return Err(BackendError::Preparation {
444                    operation: "distributed session realization".into(),
445                    message: "collective groups must have unique IDs, in-range members, and the declared local world rank".into(),
446                });
447            }
448        }
449        Ok(Self {
450            world_size,
451            rank,
452            groups,
453        })
454    }
455
456    /// Returns the total process count.
457    pub const fn world_size(&self) -> usize {
458        self.world_size
459    }
460    /// Returns this process's world rank.
461    pub const fn rank(&self) -> usize {
462        self.rank
463    }
464    /// Returns ordered opaque group realizations.
465    pub fn groups(&self) -> &[CollectiveGroupDescriptor] {
466        &self.groups
467    }
468}
469
470impl<'de> Deserialize<'de> for DistributedSessionDescriptor {
471    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
472    where
473        D: serde::Deserializer<'de>,
474    {
475        #[derive(Deserialize)]
476        struct RawDescriptor {
477            world_size: usize,
478            rank: usize,
479            groups: Vec<CollectiveGroupDescriptor>,
480        }
481
482        let raw = RawDescriptor::deserialize(deserializer)?;
483        Self::new(raw.world_size, raw.rank, raw.groups).map_err(serde::de::Error::custom)
484    }
485}
486
487/// Structured backend failure that does not expose a runtime exception type.
488#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
489#[non_exhaustive]
490pub enum BackendError {
491    /// A required capability is absent.
492    #[error("backend {backend} does not support required capability {capability}")]
493    Unsupported {
494        /// Backend implementation name.
495        backend: String,
496        /// Required capability.
497        capability: String,
498    },
499    /// Model preparation failed.
500    #[error("backend model preparation failed during {operation}: {message}")]
501    Preparation {
502        /// Preparation operation.
503        operation: String,
504        /// Backend-provided context.
505        message: String,
506    },
507    /// Session execution failed.
508    #[error("backend session {session} failed during {operation}: {message}")]
509    Execution {
510        /// Stable session identifier.
511        session: String,
512        /// Operation being executed.
513        operation: String,
514        /// Backend-provided context.
515        message: String,
516    },
517    /// Exact completion observation failed.
518    #[error("backend completion observation failed: {message}")]
519    Completion {
520        /// Backend-provided context.
521        message: String,
522    },
523}
524
525/// Exact completion owned by one backend submission.
526pub trait Completion {
527    /// Error produced while observing the completion.
528    type Error: std::error::Error + Send + Sync + 'static;
529
530    /// Nonblocking exact-completion observation.
531    fn is_complete(&self) -> Result<bool, Self::Error>;
532
533    /// Blocks on this exact completion only.
534    fn wait(&self) -> Result<(), Self::Error>;
535}
536
537/// Output and exact completion returned by a backend submission.
538#[derive(Debug)]
539pub struct Submission<T, C> {
540    /// Backend-owned output value.
541    pub output: T,
542    /// Completion retaining everything needed by the submitted work.
543    pub completion: C,
544}
545
546impl<T, C> Submission<T, C>
547where
548    C: Completion,
549{
550    /// Waits for this exact submission and returns its output.
551    pub fn wait(self) -> Result<T, C::Error> {
552        self.completion.wait()?;
553        Ok(self.output)
554    }
555}
556
557/// Marker wrapper proving that a model was prepared by a backend.
558#[derive(Debug)]
559pub struct PreparedModel<M> {
560    model: M,
561    capabilities: SessionCapabilities,
562}
563
564impl<M> PreparedModel<M> {
565    /// Wraps a backend-prepared model.
566    pub const fn new(model: M, capabilities: SessionCapabilities) -> Self {
567        Self {
568            model,
569            capabilities,
570        }
571    }
572    /// Borrows the backend model.
573    pub const fn get(&self) -> &M {
574        &self.model
575    }
576    /// Mutably borrows the backend model.
577    pub fn get_mut(&mut self) -> &mut M {
578        &mut self.model
579    }
580    /// Returns the session capabilities admitted before materialization.
581    pub const fn capabilities(&self) -> SessionCapabilities {
582        self.capabilities
583    }
584    /// Consumes the marker.
585    pub fn into_inner(self) -> M {
586        self.model
587    }
588    /// Consumes the marker into the backend model and admitted capabilities.
589    pub fn into_parts(self) -> (M, SessionCapabilities) {
590        (self.model, self.capabilities)
591    }
592}
593
594impl<M> std::ops::Deref for PreparedModel<M> {
595    type Target = M;
596
597    fn deref(&self) -> &Self::Target {
598        self.get()
599    }
600}
601
602impl<M> std::ops::DerefMut for PreparedModel<M> {
603    fn deref_mut(&mut self) -> &mut Self::Target {
604        self.get_mut()
605    }
606}
607
608/// One backend selected for an entire prepared model and all its sessions.
609pub trait BackendProvider: Sized {
610    /// Portable model preparation request.
611    type ModelConfig;
612    /// Opaque backend model/executable.
613    type Model;
614    /// Opaque backend session/cache state and execution implementation.
615    type Session: BackendSession<Self>;
616    /// Backend error.
617    type Error: std::error::Error + Send + Sync + 'static;
618
619    /// Backend identity.
620    fn descriptor(&self) -> BackendDescriptor;
621    /// Discovers devices and their fail-closed capabilities.
622    fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error>;
623    /// Loads, compiles, or materializes a model for this backend.
624    fn prepare_model(
625        &self,
626        config: Self::ModelConfig,
627    ) -> Result<PreparedModel<Self::Model>, Self::Error>;
628    /// Consumes a prepared model into one backend-owned execution session.
629    ///
630    /// The executable and its mutable cache state have one owner after this
631    /// call. This prevents callers from pairing session state with a different
632    /// model or submitting the same mutable executable through two sessions.
633    fn create_session(
634        &self,
635        model: PreparedModel<Self::Model>,
636    ) -> Result<Self::Session, Self::Error>;
637
638    /// Constructs an internal backend error for an admission/realization mismatch.
639    ///
640    /// Backends whose prepared and realized reports cannot differ may leave
641    /// this unreachable default in place.
642    fn session_capability_mismatch(
643        &self,
644        admitted: SessionCapabilities,
645        realized: SessionCapabilities,
646    ) -> Self::Error {
647        panic!("backend realized session capabilities {realized:?} after admitting {admitted:?}")
648    }
649}
650
651/// Artifact-loading extension for a selected whole-model backend.
652///
653/// Core owns checkpoint inspection and preparation planning. Implementations
654/// translate the resulting neutral plan and their associated load options into
655/// the backend's concrete [`BackendProvider::ModelConfig`]. Tensor materialization
656/// remains entirely inside [`BackendProvider::prepare_model`].
657pub trait ModelLoadingBackend: BackendProvider {
658    /// Backend load policy exposed to a generic caller.
659    type LoadOptions;
660
661    /// Exact pre-materialization realization selected from the inspected
662    /// artifact, caller request, and backend mechanisms.
663    type SelectedPreparation;
664
665    /// Architecture registry selected by this backend adapter.
666    type ConfigurationResolver: ModelConfigurationResolver;
667
668    /// Returns the architecture-owned model configuration registry.
669    fn configuration_resolver(&self) -> &Self::ConfigurationResolver;
670
671    /// Resolves backend options into the policy used during neutral planning.
672    fn preparation_policy(
673        &self,
674        options: &Self::LoadOptions,
675    ) -> Result<PreparationPolicy, Self::Error>;
676
677    /// Intersects normalized architecture requirements and the caller request
678    /// with backend support, returning the sole construction-policy handoff.
679    ///
680    /// Core deliberately does not infer architecture capabilities from a
681    /// coarse model-family identity. Implementations must fail closed for
682    /// requested routes that the exact normalized architecture or backend
683    /// cannot realize.
684    fn select_preparation(
685        &self,
686        inspection: &ArtifactInspection<
687            <Self::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
688        >,
689        options: &Self::LoadOptions,
690        policy: PreparationPolicy,
691    ) -> Result<Self::SelectedPreparation, Self::Error>;
692
693    /// Derives exact session capabilities from header/configuration state only.
694    fn session_capabilities(
695        &self,
696        inspection: &ArtifactInspection<
697            <Self::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
698        >,
699        policy: PreparationPolicy,
700    ) -> Result<SessionCapabilities, Self::Error>;
701
702    /// Binds a neutral preparation plan and its authoritative selected
703    /// realization to backend-owned materialization input.
704    fn model_config(
705        &self,
706        plan: ModelPreparationPlan<
707            <Self::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
708        >,
709        selected: Self::SelectedPreparation,
710    ) -> Result<Self::ModelConfig, Self::Error>;
711}
712
713/// Failure while inspecting, planning, or materializing a model artifact.
714#[derive(Debug, thiserror::Error)]
715#[non_exhaustive]
716pub enum ModelLoadError<E: std::error::Error + Send + Sync + 'static> {
717    /// Portable artifact inspection or preparation planning failed.
718    #[error(transparent)]
719    Artifact(#[from] ArtifactError),
720    /// The selected backend failed policy resolution or materialization.
721    #[error("selected backend failed to prepare the model: {0}")]
722    Backend(#[source] E),
723    /// The inspected architecture/load-policy/topology route lacks a requirement.
724    #[error(transparent)]
725    SessionCapability(#[from] SessionCapabilityError),
726}
727
728/// Inspects, plans, and prepares one artifact on the selected backend.
729///
730/// This is the sole generic artifact-loading entry point. The backend instance
731/// already owns its device, execution queues, transfer queues, and optional
732/// distributed communication state; none are passed separately to loading.
733pub fn load_model<B: ModelLoadingBackend>(
734    backend: &B,
735    artifact: impl AsRef<Path>,
736    options: B::LoadOptions,
737) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
738    let inspection = inspect_artifact(artifact, backend.configuration_resolver())?;
739    prepare_inspected_model(backend, inspection, options)
740}
741
742/// Plans and prepares an artifact that the caller has already inspected.
743///
744/// This is the canonical lower-level entry point for facade loaders which
745/// must derive tokenizer, chat, or other portable sidecar state from the same
746/// inspection before transferring ownership to the selected backend.
747pub fn prepare_inspected_model<B: ModelLoadingBackend>(
748    backend: &B,
749    inspection: ArtifactInspection<
750        <B::ConfigurationResolver as ModelConfigurationResolver>::ArtifactPlan,
751    >,
752    options: B::LoadOptions,
753) -> Result<PreparedModel<B::Model>, ModelLoadError<B::Error>> {
754    let policy = backend
755        .preparation_policy(&options)
756        .map_err(ModelLoadError::Backend)?;
757    let selected = backend
758        .select_preparation(&inspection, &options, policy)
759        .map_err(ModelLoadError::Backend)?;
760    let capabilities = backend
761        .session_capabilities(&inspection, policy)
762        .map_err(ModelLoadError::Backend)?;
763    policy
764        .validate_session_capabilities(&capabilities)
765        .map_err(ModelLoadError::SessionCapability)?;
766    let plan = plan_model_preparation(inspection, policy, capabilities)?;
767    let config = backend
768        .model_config(plan, selected)
769        .map_err(ModelLoadError::Backend)?;
770    backend
771        .prepare_model(config)
772        .map_err(ModelLoadError::Backend)
773}
774
775/// Prefill/decode interface for an already selected backend session.
776///
777/// The session owns its prepared executable and cache. The contract
778/// intentionally models language-model submissions rather than primitive
779/// tensor operations. Input, output, cache and completion stay opaque.
780pub trait BackendSession<B: BackendProvider> {
781    /// Backend-owned prefill input.
782    type PrefillInput;
783    /// Backend-owned decode input.
784    type DecodeInput;
785    /// Backend-owned logits/output.
786    type Output;
787    /// Exact completion type.
788    type Completion: Completion<Error = B::Error>;
789
790    /// Reports capabilities of this exact realized session.
791    fn capabilities(&self) -> SessionCapabilities;
792
793    /// Submits prompt prefill against this session.
794    fn prefill(
795        &mut self,
796        backend: &B,
797        input: Self::PrefillInput,
798    ) -> Result<Submission<Self::Output, Self::Completion>, B::Error>;
799
800    /// Submits one or more cached decode positions against this session.
801    fn decode(
802        &mut self,
803        backend: &B,
804        input: Self::DecodeInput,
805    ) -> Result<Submission<Self::Output, Self::Completion>, B::Error>;
806
807    /// Materializes an already completed opaque output into portable records.
808    ///
809    /// Calling this method is an explicit synchronization and host-transfer
810    /// boundary. Ordinary inference does not invoke it.
811    fn observe_output(
812        &self,
813        backend: &B,
814        output: &Self::Output,
815    ) -> Result<ObservationSet, B::Error>;
816}
817
818/// Optional named-activation inspection for a selected backend session.
819///
820/// This is a general diagnostics and observability capability. Implementations
821/// execute the requested operation to completion and materialize only selected
822/// observation points. It is intentionally separate from ordinary asynchronous
823/// submission so production inference pays no instrumentation cost.
824pub trait InspectableBackendSession<B: BackendProvider>: BackendSession<B> {
825    /// Executes and inspects one prompt prefill operation.
826    fn inspect_prefill(
827        &mut self,
828        backend: &B,
829        input: Self::PrefillInput,
830        request: &ObservationRequest,
831    ) -> Result<InspectedOutput<Self::Output>, B::Error>;
832
833    /// Executes and inspects one cached decode operation.
834    fn inspect_decode(
835        &mut self,
836        backend: &B,
837        input: Self::DecodeInput,
838        request: &ObservationRequest,
839    ) -> Result<InspectedOutput<Self::Output>, B::Error>;
840}
841
842/// One submission produced by the selected backend session.
843pub type SessionSubmission<B> = Submission<
844    <<B as BackendProvider>::Session as BackendSession<B>>::Output,
845    <<B as BackendProvider>::Session as BackendSession<B>>::Completion,
846>;
847
848/// A prepared model, its selected backend, and its backend-owned session.
849///
850/// This is the canonical client-side execution owner. Keeping the backend and
851/// session together makes backend selection a whole-model decision and makes
852/// it impossible to submit a session through a different backend instance.
853/// Backend-owned executable, cache, tensor, and completion types remain
854/// associated types and never enter the portable API.
855pub struct ModelRuntime<B: BackendProvider> {
856    backend: B,
857    session: B::Session,
858}
859
860impl<B: BackendProvider> ModelRuntime<B> {
861    /// Prepares `config` and creates its sole execution session.
862    pub fn prepare(backend: B, config: B::ModelConfig) -> Result<Self, B::Error> {
863        let model = backend.prepare_model(config)?;
864        Self::from_prepared(backend, model)
865    }
866
867    /// Creates the sole execution session for an already prepared model.
868    pub fn from_prepared(backend: B, model: PreparedModel<B::Model>) -> Result<Self, B::Error> {
869        let admitted = model.capabilities();
870        let session = backend.create_session(model)?;
871        let realized = session.capabilities();
872        if admitted != realized {
873            return Err(backend.session_capability_mismatch(admitted, realized));
874        }
875        Ok(Self { backend, session })
876    }
877
878    /// Returns the selected backend.
879    pub const fn backend(&self) -> &B {
880        &self.backend
881    }
882
883    /// Returns the backend-owned session for optional backend capabilities.
884    pub const fn session(&self) -> &B::Session {
885        &self.session
886    }
887
888    /// Returns the backend-owned session for optional backend capabilities.
889    pub fn session_mut(&mut self) -> &mut B::Session {
890        &mut self.session
891    }
892
893    /// Borrows the selected backend and its mutable session together.
894    pub fn parts_mut(&mut self) -> (&B, &mut B::Session) {
895        (&self.backend, &mut self.session)
896    }
897
898    /// Reports capabilities of the exact prepared model session.
899    pub fn capabilities(&self) -> SessionCapabilities {
900        self.session.capabilities()
901    }
902
903    /// Submits prompt prefill through the selected backend and session.
904    pub fn prefill(
905        &mut self,
906        input: <B::Session as BackendSession<B>>::PrefillInput,
907    ) -> Result<SessionSubmission<B>, B::Error> {
908        self.session.prefill(&self.backend, input)
909    }
910
911    /// Submits cached decode through the selected backend and session.
912    pub fn decode(
913        &mut self,
914        input: <B::Session as BackendSession<B>>::DecodeInput,
915    ) -> Result<SessionSubmission<B>, B::Error> {
916        self.session.decode(&self.backend, input)
917    }
918
919    /// Materializes portable observations from an already completed output.
920    pub fn observe_output(
921        &self,
922        output: &<B::Session as BackendSession<B>>::Output,
923    ) -> Result<ObservationSet, B::Error> {
924        self.session.observe_output(&self.backend, output)
925    }
926}
927
928impl<B> ModelRuntime<B>
929where
930    B: BackendProvider,
931    B::Session: InspectableBackendSession<B>,
932{
933    /// Executes a completed, explicitly instrumented prefill operation.
934    pub fn inspect_prefill(
935        &mut self,
936        input: <B::Session as BackendSession<B>>::PrefillInput,
937        request: &ObservationRequest,
938    ) -> Result<InspectedOutput<<B::Session as BackendSession<B>>::Output>, B::Error> {
939        self.session.inspect_prefill(&self.backend, input, request)
940    }
941
942    /// Executes a completed, explicitly instrumented decode operation.
943    pub fn inspect_decode(
944        &mut self,
945        input: <B::Session as BackendSession<B>>::DecodeInput,
946        request: &ObservationRequest,
947    ) -> Result<InspectedOutput<<B::Session as BackendSession<B>>::Output>, B::Error> {
948        self.session.inspect_decode(&self.backend, input, request)
949    }
950}
951
952impl<B: ModelLoadingBackend> ModelRuntime<B> {
953    /// Loads an artifact and creates its sole session on `backend`.
954    pub fn load(
955        backend: B,
956        artifact: impl AsRef<Path>,
957        options: B::LoadOptions,
958    ) -> Result<Self, ModelLoadError<B::Error>> {
959        let model = load_model(&backend, artifact, options)?;
960        Self::from_prepared(backend, model).map_err(ModelLoadError::Backend)
961    }
962}
963
964/// Portable sampling inputs for one text-generation session.
965#[derive(Debug, Clone, Copy, PartialEq)]
966pub struct TextGenerationConfig {
967    sampling: ResolvedGenerationConfig,
968    seed: u64,
969    strategy: TextSamplingStrategy,
970}
971
972/// Backend-neutral token-sampling strategy for one text-generation session.
973#[derive(Debug, Clone, Copy, Default, PartialEq)]
974pub enum TextSamplingStrategy {
975    /// Apply the resolved top-k, top-p, min-p, and penalty controls.
976    #[default]
977    Standard,
978    /// Adapt the surprise cutoff toward `tau` bits at rate `eta`.
979    MirostatV2 {
980        /// Target surprise in bits.
981        tau: f32,
982        /// Adaptation rate.
983        eta: f32,
984    },
985}
986
987impl TextGenerationConfig {
988    /// Uses resolved checkpoint/request sampling with deterministic seed zero.
989    pub const fn new(sampling: ResolvedGenerationConfig) -> Self {
990        Self {
991            sampling,
992            seed: 0,
993            strategy: TextSamplingStrategy::Standard,
994        }
995    }
996
997    /// Selects the deterministic root seed used by a stochastic backend.
998    pub const fn with_seed(mut self, seed: u64) -> Self {
999        self.seed = seed;
1000        self
1001    }
1002
1003    /// Selects adaptive Mirostat V2 sampling.
1004    pub fn with_mirostat_v2(mut self, tau: f32, eta: f32) -> Result<Self, GenerationError> {
1005        if !tau.is_finite() || tau <= 0.0 {
1006            return Err(GenerationError::InvalidMirostatTau(tau));
1007        }
1008        if !eta.is_finite() || eta <= 0.0 {
1009            return Err(GenerationError::InvalidMirostatEta(eta));
1010        }
1011        self.strategy = TextSamplingStrategy::MirostatV2 { tau, eta };
1012        Ok(self)
1013    }
1014
1015    /// Returns the validated sampling configuration.
1016    pub const fn sampling(&self) -> ResolvedGenerationConfig {
1017        self.sampling
1018    }
1019
1020    /// Returns the deterministic root seed.
1021    pub const fn seed(&self) -> u64 {
1022        self.seed
1023    }
1024
1025    /// Returns the selected backend-neutral sampling strategy.
1026    pub const fn strategy(&self) -> TextSamplingStrategy {
1027        self.strategy
1028    }
1029}
1030
1031/// Backend-owned generated token that exposes only its portable token id.
1032pub trait TokenOutput: Clone {
1033    /// Error produced while observing the token value.
1034    type Error: std::error::Error + Send + Sync + 'static;
1035
1036    /// Waits only as required to read this token's canonical vocabulary id.
1037    fn token_id(&self) -> Result<u32, Self::Error>;
1038}
1039
1040impl TokenOutput for u32 {
1041    type Error = std::convert::Infallible;
1042
1043    fn token_id(&self) -> Result<u32, Self::Error> {
1044        Ok(*self)
1045    }
1046}
1047
1048/// Portable vocabulary filter applied before backend-owned sampling.
1049#[derive(Debug, Clone, Eq, PartialEq)]
1050pub enum TokenFilter {
1051    /// Every vocabulary token may be selected.
1052    All,
1053    /// One boolean per canonical vocabulary id; `true` permits selection.
1054    Allowed(Vec<bool>),
1055}
1056
1057impl TokenFilter {
1058    /// Validates an explicit canonical-vocabulary allow mask.
1059    pub fn allowed(mask: Vec<bool>) -> Result<Self, TokenFilterError> {
1060        if mask.is_empty() {
1061            return Err(TokenFilterError::EmptyVocabulary);
1062        }
1063        if !mask.iter().any(|allowed| *allowed) {
1064            return Err(TokenFilterError::NoAllowedToken);
1065        }
1066        Ok(Self::Allowed(mask))
1067    }
1068
1069    /// Returns the explicit allow mask, or `None` when all tokens are allowed.
1070    pub fn allowed_mask(&self) -> Option<&[bool]> {
1071        match self {
1072            Self::All => None,
1073            Self::Allowed(mask) => Some(mask),
1074        }
1075    }
1076}
1077
1078/// Invalid portable token-filter construction.
1079#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1080pub enum TokenFilterError {
1081    /// An explicit mask must describe a nonempty vocabulary.
1082    #[error("token filter vocabulary must not be empty")]
1083    EmptyVocabulary,
1084    /// Fail closed instead of asking a backend to sample an impossible row.
1085    #[error("token filter does not allow any vocabulary token")]
1086    NoAllowedToken,
1087}
1088
1089/// Backend-independent logical controller for constrained token selection.
1090pub trait TokenFilterController {
1091    /// Constraint or grammar error.
1092    type Error: std::error::Error + Send + Sync + 'static;
1093
1094    /// Returns the filter for the current durable logical prefix.
1095    fn current_filter(&mut self) -> Result<TokenFilter, Self::Error>;
1096
1097    /// Commits one backend-selected canonical vocabulary id.
1098    fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error>;
1099
1100    /// Returns whether the committed logical prefix satisfies the constraint.
1101    fn is_complete(&mut self) -> Result<bool, Self::Error>;
1102}
1103
1104/// Portable constraint controller that can evaluate speculative token histories.
1105///
1106/// Speculative backends use these queries for discardable draft branches. The
1107/// durable controller state must not change until [`TokenFilterController::commit_token`]
1108/// is called for a target-accepted token.
1109pub trait SpeculativeTokenFilterController: TokenFilterController + Clone {
1110    /// Returns the filter at `history` without committing its uncommitted suffix.
1111    ///
1112    /// `history` contains the controller's durable prefix followed by zero or
1113    /// more speculative tokens. Implementations must reject histories that do
1114    /// not begin with the durable prefix.
1115    fn filter_at(&self, history: &[u32]) -> Result<TokenFilter, Self::Error>;
1116
1117    /// Returns whether `history` completes the constraint without committing it.
1118    fn prefix_is_complete(&self, history: &[u32]) -> Result<bool, Self::Error>;
1119}
1120
1121#[derive(Debug, Clone, Copy)]
1122struct UnconstrainedTokens;
1123
1124impl TokenFilterController for UnconstrainedTokens {
1125    type Error = std::convert::Infallible;
1126
1127    fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
1128        Ok(TokenFilter::All)
1129    }
1130
1131    fn commit_token(&mut self, _: u32) -> Result<(), Self::Error> {
1132        Ok(())
1133    }
1134
1135    fn is_complete(&mut self) -> Result<bool, Self::Error> {
1136        Ok(false)
1137    }
1138}
1139
1140/// High-level text-generation extension implemented once per backend.
1141///
1142/// The contract deliberately combines model execution and sampling. Core does
1143/// not see logits or ask a backend to implement tensor primitives. The token,
1144/// sampling state, cache state, and exact completion remain backend-owned.
1145pub trait TextGenerationBackend: BackendProvider {
1146    /// Opaque prepared prompt, including any backend-owned multimodal values.
1147    type Prompt;
1148    /// Backend-owned generated token handle.
1149    type Token: TokenOutput<Error = Self::Error>;
1150    /// Backend-owned sampler and randomness state for one sequence.
1151    type TextGenerationState;
1152    /// Exact completion retaining model execution and token sampling.
1153    type TextCompletion: Completion<Error = Self::Error>;
1154
1155    /// Creates backend sampling state for one sequence.
1156    fn start_text_generation(
1157        backend: &Self,
1158        config: TextGenerationConfig,
1159    ) -> Result<Self::TextGenerationState, Self::Error>;
1160
1161    /// Converts portable tokenizer ids into a backend-owned text prompt.
1162    fn prepare_text_prompt(
1163        backend: &Self,
1164        prompt_token_ids: Vec<u32>,
1165    ) -> Result<Self::Prompt, Self::Error>;
1166
1167    /// Submits prompt prefill followed by sampling one token.
1168    fn submit_text_prefill(
1169        runtime: &mut ModelRuntime<Self>,
1170        prompt: Self::Prompt,
1171        filter: &TokenFilter,
1172        state: &mut Self::TextGenerationState,
1173    ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error>;
1174
1175    /// Submits cached decode from the preceding token and samples its successor.
1176    fn submit_text_decode(
1177        runtime: &mut ModelRuntime<Self>,
1178        token: Self::Token,
1179        filter: &TokenFilter,
1180        state: &mut Self::TextGenerationState,
1181    ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error>;
1182}
1183
1184/// Failure from backend preprocessing or backend-requested text encoding.
1185#[derive(Debug, thiserror::Error)]
1186pub enum MultimodalPreparationFailure<B, T>
1187where
1188    B: std::error::Error + 'static,
1189    T: std::error::Error + 'static,
1190{
1191    /// The selected backend rejected or failed media preprocessing.
1192    #[error("backend multimodal preparation failed: {0}")]
1193    Backend(#[source] B),
1194    /// The facade tokenizer failed on backend-required framing text.
1195    #[error("multimodal framing text encoding failed: {0}")]
1196    Text(#[source] T),
1197}
1198
1199/// Backend preparation of portable decoded media for one selected session.
1200///
1201/// Caller text is tokenized before this boundary. Some processors introduce
1202/// checkpoint-defined framing text or video timestamps, so the callback keeps
1203/// that work on the facade's tokenizer. The selected backend returns its
1204/// existing opaque prompt type; core never observes tensors or streams.
1205pub trait MultimodalPreparationBackend: TextGenerationBackend {
1206    /// Converts ordered token and decoded-media segments into a backend prompt.
1207    fn prepare_multimodal_input<E>(
1208        runtime: &ModelRuntime<Self>,
1209        request: &TokenizedMultimodalRequest,
1210        encode_backend_text: &mut dyn FnMut(&str) -> Result<Vec<u32>, E>,
1211    ) -> Result<Self::Prompt, MultimodalPreparationFailure<Self::Error, E>>
1212    where
1213        E: std::error::Error + Send + Sync + 'static;
1214}
1215
1216/// Capability and resource observations for one selected text-model session.
1217///
1218/// Implementations inspect the opaque model and cache already owned by
1219/// [`ModelRuntime`]. The contract exposes only portable documents and the
1220/// backend's existing opaque prompt type; tensor, stream, allocator, and
1221/// executable types remain inside the adapter.
1222pub trait ModelCapabilityBackend: TextGenerationBackend {
1223    /// Reports validated model capabilities for the selected session.
1224    fn model_capabilities(
1225        runtime: &ModelRuntime<Self>,
1226    ) -> Result<ModelCapabilities, CapabilityError>;
1227
1228    /// Counts text and backend-specific model positions in a prepared prompt.
1229    fn count_prepared_input(
1230        runtime: &ModelRuntime<Self>,
1231        input: &Self::Prompt,
1232    ) -> Result<InputTokenCount, CapabilityError>;
1233
1234    /// Estimates persistent and transient request state for this session.
1235    fn estimate_runtime_state(
1236        runtime: &ModelRuntime<Self>,
1237        input: InputTokenCount,
1238        max_output_tokens: u64,
1239        batch_size: u64,
1240    ) -> Result<RuntimeStateEstimate, CapabilityError>;
1241
1242    /// Reports static model storage and current backend memory observations.
1243    fn static_memory(runtime: &ModelRuntime<Self>) -> Result<StaticMemoryReport, CapabilityError>;
1244}
1245
1246enum TextGenerationStep<P, T> {
1247    Prefill(P),
1248    Decode(T),
1249}
1250
1251/// Failure from either backend execution or portable constraint control.
1252#[derive(Debug, thiserror::Error)]
1253pub enum ControlledTextGenerationError<B, C>
1254where
1255    B: std::error::Error + 'static,
1256    C: std::error::Error + 'static,
1257{
1258    /// Backend preparation, execution, sampling, or completion failed.
1259    #[error("backend text generation failed: {0}")]
1260    Backend(#[source] B),
1261    /// Portable constraint filtering or commitment failed.
1262    #[error("text generation constraint failed: {0}")]
1263    Controller(#[source] C),
1264}
1265
1266/// One constraint-committed token and its backend-owned output handle.
1267#[derive(Debug, Clone)]
1268pub struct ControlledToken<T> {
1269    output: T,
1270    token_id: u32,
1271}
1272
1273impl<T> ControlledToken<T> {
1274    /// Returns the committed canonical vocabulary id.
1275    pub fn token_id(&self) -> u32 {
1276        self.token_id
1277    }
1278
1279    /// Borrows the backend-owned token handle.
1280    pub const fn output(&self) -> &T {
1281        &self.output
1282    }
1283
1284    /// Consumes the committed token into its backend-owned handle.
1285    pub fn into_output(self) -> T {
1286        self.output
1287    }
1288}
1289
1290/// Backend-generic generation driven by a portable token-filter controller.
1291pub struct ControlledTextGeneration<'a, B, C>
1292where
1293    B: TextGenerationBackend,
1294    C: TokenFilterController,
1295{
1296    inner: TextGenerationMachine<'a, B, C>,
1297}
1298
1299struct TextGenerationMachine<'a, B, C>
1300where
1301    B: TextGenerationBackend,
1302    C: TokenFilterController,
1303{
1304    runtime: &'a mut ModelRuntime<B>,
1305    backend_state: B::TextGenerationState,
1306    controller: C,
1307    step: Option<TextGenerationStep<B::Prompt, B::Token>>,
1308    completions: Vec<B::TextCompletion>,
1309    remaining_tokens: Option<usize>,
1310}
1311
1312type ControlledGenerationResult<B, C> = Result<
1313    <B as TextGenerationBackend>::Token,
1314    ControlledTextGenerationError<
1315        <B as BackendProvider>::Error,
1316        <C as TokenFilterController>::Error,
1317    >,
1318>;
1319
1320impl<'a, B, C> ControlledTextGeneration<'a, B, C>
1321where
1322    B: TextGenerationBackend,
1323    C: TokenFilterController,
1324{
1325    /// Starts controlled generation from portable prompt token ids.
1326    pub fn new(
1327        runtime: &'a mut ModelRuntime<B>,
1328        prompt_token_ids: Vec<u32>,
1329        config: TextGenerationConfig,
1330        controller: C,
1331    ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1332        let prompt = B::prepare_text_prompt(runtime.backend(), prompt_token_ids)
1333            .map_err(ControlledTextGenerationError::Backend)?;
1334        Self::from_prompt(runtime, prompt, config, controller)
1335    }
1336
1337    /// Starts controlled generation from an opaque backend-prepared prompt.
1338    pub fn from_prompt(
1339        runtime: &'a mut ModelRuntime<B>,
1340        prompt: B::Prompt,
1341        config: TextGenerationConfig,
1342        controller: C,
1343    ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1344        TextGenerationMachine::new(runtime, prompt, config, controller).map(|inner| Self { inner })
1345    }
1346
1347    /// Mutably borrows the canonical constraint state.
1348    pub fn controller_mut(&mut self) -> &mut C {
1349        &mut self.inner.controller
1350    }
1351}
1352
1353impl<'a, B, C> TextGenerationMachine<'a, B, C>
1354where
1355    B: TextGenerationBackend,
1356    C: TokenFilterController,
1357{
1358    fn new(
1359        runtime: &'a mut ModelRuntime<B>,
1360        prompt: B::Prompt,
1361        config: TextGenerationConfig,
1362        controller: C,
1363    ) -> Result<Self, ControlledTextGenerationError<B::Error, C::Error>> {
1364        let backend_state = B::start_text_generation(runtime.backend(), config)
1365            .map_err(ControlledTextGenerationError::Backend)?;
1366        Ok(Self {
1367            runtime,
1368            backend_state,
1369            controller,
1370            step: Some(TextGenerationStep::Prefill(prompt)),
1371            completions: Vec::new(),
1372            remaining_tokens: config.sampling().max_new_tokens,
1373        })
1374    }
1375
1376    fn retain_completion(&mut self, completion: B::TextCompletion) -> Result<(), B::Error> {
1377        let existing = std::mem::take(&mut self.completions);
1378        let mut retained = Vec::with_capacity(existing.len() + 1);
1379        for pending in existing {
1380            match pending.is_complete() {
1381                Ok(true) => {}
1382                Ok(false) => retained.push(pending),
1383                Err(error) => {
1384                    let _ = pending.wait();
1385                    for retained_completion in retained.drain(..) {
1386                        let _ = retained_completion.wait();
1387                    }
1388                    let _ = completion.wait();
1389                    return Err(error);
1390                }
1391            }
1392        }
1393        retained.push(completion);
1394        self.completions = retained;
1395        Ok(())
1396    }
1397
1398    fn next_output(&mut self) -> Option<ControlledGenerationResult<B, C>> {
1399        if self.remaining_tokens == Some(0) {
1400            self.step = None;
1401            return None;
1402        }
1403        let step = self.step.take()?;
1404        let filter = match self.controller.current_filter() {
1405            Ok(filter) => filter,
1406            Err(error) => return Some(Err(ControlledTextGenerationError::Controller(error))),
1407        };
1408        let submission = match step {
1409            TextGenerationStep::Prefill(prompt) => {
1410                B::submit_text_prefill(self.runtime, prompt, &filter, &mut self.backend_state)
1411            }
1412            TextGenerationStep::Decode(token) => {
1413                B::submit_text_decode(self.runtime, token, &filter, &mut self.backend_state)
1414            }
1415        };
1416        let submission = match submission {
1417            Ok(submission) => submission,
1418            Err(error) => return Some(Err(ControlledTextGenerationError::Backend(error))),
1419        };
1420        let token = submission.output;
1421        if let Err(error) = self.retain_completion(submission.completion) {
1422            return Some(Err(ControlledTextGenerationError::Backend(error)));
1423        }
1424        self.step = Some(TextGenerationStep::Decode(token.clone()));
1425        if let Some(remaining_tokens) = &mut self.remaining_tokens {
1426            *remaining_tokens -= 1;
1427        }
1428        Some(Ok(token))
1429    }
1430}
1431
1432impl<B, C> Iterator for ControlledTextGeneration<'_, B, C>
1433where
1434    B: TextGenerationBackend,
1435    C: TokenFilterController,
1436{
1437    type Item =
1438        Result<ControlledToken<B::Token>, ControlledTextGenerationError<B::Error, C::Error>>;
1439
1440    fn next(&mut self) -> Option<Self::Item> {
1441        let token = match self.inner.next_output()? {
1442            Ok(token) => token,
1443            Err(error) => return Some(Err(error)),
1444        };
1445        let token_id = match token.token_id() {
1446            Ok(token_id) => token_id,
1447            Err(error) => {
1448                self.inner.step = None;
1449                return Some(Err(ControlledTextGenerationError::Backend(error)));
1450            }
1451        };
1452        if let Err(error) = self.inner.controller.commit_token(token_id) {
1453            self.inner.step = None;
1454            return Some(Err(ControlledTextGenerationError::Controller(error)));
1455        }
1456        Some(Ok(ControlledToken {
1457            output: token,
1458            token_id,
1459        }))
1460    }
1461}
1462
1463impl<B, C> Drop for TextGenerationMachine<'_, B, C>
1464where
1465    B: TextGenerationBackend,
1466    C: TokenFilterController,
1467{
1468    fn drop(&mut self) {
1469        for completion in self.completions.drain(..) {
1470            let _ = completion.wait();
1471        }
1472    }
1473}
1474
1475/// Backend-generic asynchronous token-generation iterator.
1476///
1477/// Every yielded token handle may be fed into the following decode before its
1478/// id is read on the host. Exact completions are retained until finished, and
1479/// dropping the iterator waits for all still-retained submissions.
1480pub struct TextGeneration<'a, B: TextGenerationBackend> {
1481    inner: TextGenerationMachine<'a, B, UnconstrainedTokens>,
1482}
1483
1484impl<'a, B: TextGenerationBackend> TextGeneration<'a, B> {
1485    /// Starts generation from portable prompt token ids.
1486    pub fn new(
1487        runtime: &'a mut ModelRuntime<B>,
1488        prompt_token_ids: Vec<u32>,
1489        config: TextGenerationConfig,
1490    ) -> Result<Self, B::Error> {
1491        let prompt = B::prepare_text_prompt(runtime.backend(), prompt_token_ids)?;
1492        Self::from_prompt(runtime, prompt, config)
1493    }
1494
1495    /// Starts generation from an opaque backend-prepared prompt.
1496    pub fn from_prompt(
1497        runtime: &'a mut ModelRuntime<B>,
1498        prompt: B::Prompt,
1499        config: TextGenerationConfig,
1500    ) -> Result<Self, B::Error> {
1501        TextGenerationMachine::new(runtime, prompt, config, UnconstrainedTokens)
1502            .map(|inner| Self { inner })
1503            .map_err(unreachable_unconstrained_error)
1504    }
1505}
1506
1507fn unreachable_unconstrained_error<B>(
1508    error: ControlledTextGenerationError<B, std::convert::Infallible>,
1509) -> B
1510where
1511    B: std::error::Error + 'static,
1512{
1513    match error {
1514        ControlledTextGenerationError::Backend(error) => error,
1515        ControlledTextGenerationError::Controller(error) => match error {},
1516    }
1517}
1518
1519impl<B: TextGenerationBackend> Iterator for TextGeneration<'_, B> {
1520    type Item = Result<B::Token, B::Error>;
1521
1522    fn next(&mut self) -> Option<Self::Item> {
1523        self.inner
1524            .next_output()
1525            .map(|result| result.map_err(unreachable_unconstrained_error))
1526    }
1527}
1528
1529/// Optional high-level transfer and collective capability of a selected session.
1530///
1531/// This contract deliberately operates on an opaque backend value. It models
1532/// the few communication submissions needed by model execution without making
1533/// core define a tensor algebra or exposing native groups, streams, or events.
1534/// Every operation is scoped to the session selected for the complete model.
1535pub trait DistributedSession {
1536    /// Backend-owned tensor or buffer value.
1537    type Value;
1538    /// Exact completion retaining the submitted communication resources.
1539    type Completion: Completion<Error = Self::Error>;
1540    /// Structured backend error.
1541    type Error: std::error::Error + Send + Sync + 'static;
1542
1543    /// Stable topology and rank identity.
1544    fn descriptor(&self) -> DistributedSessionDescriptor;
1545    /// Fail-closed communication support.
1546    fn capabilities(&self) -> DistributedCapabilities;
1547
1548    /// Submits a sum reduction over `scope`.
1549    fn all_reduce_sum(
1550        &self,
1551        scope: CollectiveScope,
1552        input: &Self::Value,
1553    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1554
1555    /// Submits a leading-rank-axis gather over `scope`.
1556    fn all_gather(
1557        &self,
1558        scope: CollectiveScope,
1559        input: &Self::Value,
1560    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1561
1562    /// Submits a variable-count all-to-all exchange over `scope`.
1563    fn all_to_all_v(
1564        &self,
1565        scope: CollectiveScope,
1566        input: &Self::Value,
1567        send_counts: &[usize],
1568        receive_counts: &[usize],
1569    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1570
1571    /// Submits a point-to-point send to a rank within `scope`.
1572    fn send(
1573        &self,
1574        scope: CollectiveScope,
1575        peer: usize,
1576        input: &Self::Value,
1577    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1578
1579    /// Submits a point-to-point receive from a rank within `scope`.
1580    fn receive(
1581        &self,
1582        scope: CollectiveScope,
1583        peer: usize,
1584        value: &ValueDescriptor,
1585    ) -> Result<Submission<Self::Value, Self::Completion>, Self::Error>;
1586
1587    /// Synchronously gathers portable scheduler metadata across the world.
1588    fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error>;
1589}
1590
1591/// Backend extension exposing communication attached to a model session.
1592pub trait DistributedBackend: BackendProvider {
1593    /// Selected distributed session implementation.
1594    type DistributedSession: DistributedSession<Error = Self::Error>;
1595
1596    /// Returns communication for a distributed model session.
1597    fn distributed_session(session: &Self::Session) -> Option<&Self::DistributedSession>;
1598}
1599
1600#[cfg(test)]
1601mod tests {
1602    use super::*;
1603    use std::{convert::Infallible, io::Write};
1604
1605    #[test]
1606    fn text_generation_config_validates_portable_mirostat_strategy() {
1607        let sampling = crate::generation::resolve_generation_config(
1608            None,
1609            crate::generation::GenerationConfigOverrides {
1610                temperature: Some(0.8),
1611                ..crate::generation::GenerationConfigOverrides::default()
1612            },
1613        )
1614        .unwrap();
1615        let config = TextGenerationConfig::new(sampling)
1616            .with_seed(7)
1617            .with_mirostat_v2(5.0, 0.1)
1618            .unwrap();
1619        assert_eq!(config.seed(), 7);
1620        assert_eq!(
1621            config.strategy(),
1622            TextSamplingStrategy::MirostatV2 { tau: 5.0, eta: 0.1 }
1623        );
1624        assert!(matches!(
1625            TextGenerationConfig::new(sampling).with_mirostat_v2(0.0, 0.1),
1626            Err(GenerationError::InvalidMirostatTau(0.0))
1627        ));
1628        assert!(matches!(
1629            TextGenerationConfig::new(sampling).with_mirostat_v2(5.0, f32::NAN),
1630            Err(GenerationError::InvalidMirostatEta(value)) if value.is_nan()
1631        ));
1632    }
1633
1634    #[derive(Debug, Clone)]
1635    struct Done;
1636    impl Completion for Done {
1637        type Error = Infallible;
1638        fn is_complete(&self) -> Result<bool, Self::Error> {
1639            Ok(true)
1640        }
1641        fn wait(&self) -> Result<(), Self::Error> {
1642            Ok(())
1643        }
1644    }
1645    struct Mock;
1646    impl BackendProvider for Mock {
1647        type ModelConfig = u32;
1648        type Model = u32;
1649        type Session = MockSession;
1650        type Error = Infallible;
1651        fn descriptor(&self) -> BackendDescriptor {
1652            BackendDescriptor::new("mock", "1")
1653        }
1654        fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error> {
1655            Ok(vec![])
1656        }
1657        fn prepare_model(&self, config: u32) -> Result<PreparedModel<u32>, Self::Error> {
1658            Ok(PreparedModel::new(config, SessionCapabilities::default()))
1659        }
1660        fn create_session(&self, model: PreparedModel<u32>) -> Result<MockSession, Self::Error> {
1661            Ok(MockSession {
1662                model: model.into_inner(),
1663                tokens: vec![],
1664                distributed: None,
1665            })
1666        }
1667    }
1668
1669    #[derive(Default)]
1670    struct LoadingMock {
1671        materializations: std::sync::atomic::AtomicUsize,
1672    }
1673    struct LoadingMockSession;
1674
1675    struct LoadingConfigurationResolver;
1676
1677    impl ModelConfigurationResolver for LoadingConfigurationResolver {
1678        type ArtifactPlan = ();
1679
1680        fn resolve_safetensors(
1681            &self,
1682            json: &serde_json::Value,
1683        ) -> Result<crate::ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
1684            Ok(crate::ResolvedModelConfiguration::new(
1685                crate::ModelConfiguration::new(
1686                    "llama",
1687                    "llama",
1688                    "llama",
1689                    crate::LoadingProtocol::Model,
1690                    Some(json.clone()),
1691                )?,
1692                (),
1693            ))
1694        }
1695
1696        fn resolve_gguf(
1697            &self,
1698            architecture: &str,
1699            _checkpoint: &eredu_gguf::Checkpoint,
1700        ) -> Result<crate::ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
1701            if architecture != "llama" {
1702                return Err(ArtifactError::UnsupportedGgufArchitecture(
1703                    architecture.into(),
1704                ));
1705            }
1706            Ok(crate::ResolvedModelConfiguration::new(
1707                crate::ModelConfiguration::new(
1708                    architecture,
1709                    architecture,
1710                    "llama",
1711                    crate::LoadingProtocol::Model,
1712                    None,
1713                )?,
1714                (),
1715            ))
1716        }
1717
1718        fn gguf_companion_requirements(
1719            &self,
1720            _architecture: &str,
1721            _checkpoint: &eredu_gguf::Checkpoint,
1722        ) -> Result<Vec<crate::GgufCompanionRequirement>, ArtifactError> {
1723            Ok(Vec::new())
1724        }
1725    }
1726
1727    static LOADING_CONFIGURATION_RESOLVER: LoadingConfigurationResolver =
1728        LoadingConfigurationResolver;
1729
1730    impl BackendProvider for LoadingMock {
1731        type ModelConfig = (ModelPreparationPlan, u32);
1732        type Model = u32;
1733        type Session = LoadingMockSession;
1734        type Error = std::convert::Infallible;
1735
1736        fn descriptor(&self) -> BackendDescriptor {
1737            BackendDescriptor::new("loading-mock", "1")
1738        }
1739
1740        fn devices(&self) -> Result<Vec<(DeviceDescriptor, DeviceCapabilities)>, Self::Error> {
1741            Ok(Vec::new())
1742        }
1743
1744        fn prepare_model(
1745            &self,
1746            (plan, model): Self::ModelConfig,
1747        ) -> Result<PreparedModel<Self::Model>, Self::Error> {
1748            self.materializations
1749                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1750            assert_eq!(plan.inspection().configuration().family(), "llama");
1751            Ok(PreparedModel::new(
1752                model,
1753                plan.admitted_session_capabilities(),
1754            ))
1755        }
1756
1757        fn create_session(
1758            &self,
1759            _: PreparedModel<Self::Model>,
1760        ) -> Result<Self::Session, Self::Error> {
1761            Ok(LoadingMockSession)
1762        }
1763    }
1764
1765    impl BackendSession<LoadingMock> for LoadingMockSession {
1766        type PrefillInput = ();
1767        type DecodeInput = ();
1768        type Output = ();
1769        type Completion = LoadingDone;
1770
1771        fn capabilities(&self) -> SessionCapabilities {
1772            SessionCapabilities::default()
1773        }
1774
1775        fn prefill(
1776            &mut self,
1777            _: &LoadingMock,
1778            _: (),
1779        ) -> Result<Submission<(), LoadingDone>, std::convert::Infallible> {
1780            Ok(Submission {
1781                output: (),
1782                completion: LoadingDone,
1783            })
1784        }
1785
1786        fn decode(
1787            &mut self,
1788            _: &LoadingMock,
1789            _: (),
1790        ) -> Result<Submission<(), LoadingDone>, std::convert::Infallible> {
1791            Ok(Submission {
1792                output: (),
1793                completion: LoadingDone,
1794            })
1795        }
1796
1797        fn observe_output(
1798            &self,
1799            _: &LoadingMock,
1800            _: &(),
1801        ) -> Result<ObservationSet, std::convert::Infallible> {
1802            Ok(ObservationSet::new())
1803        }
1804    }
1805
1806    #[derive(Debug, Clone, Copy)]
1807    struct LoadingDone;
1808
1809    impl Completion for LoadingDone {
1810        type Error = std::convert::Infallible;
1811
1812        fn is_complete(&self) -> Result<bool, Self::Error> {
1813            Ok(true)
1814        }
1815
1816        fn wait(&self) -> Result<(), Self::Error> {
1817            Ok(())
1818        }
1819    }
1820
1821    impl ModelLoadingBackend for LoadingMock {
1822        type LoadOptions = u32;
1823        type SelectedPreparation = u32;
1824        type ConfigurationResolver = LoadingConfigurationResolver;
1825
1826        fn configuration_resolver(&self) -> &Self::ConfigurationResolver {
1827            &LOADING_CONFIGURATION_RESOLVER
1828        }
1829
1830        fn preparation_policy(
1831            &self,
1832            options: &Self::LoadOptions,
1833        ) -> Result<PreparationPolicy, Self::Error> {
1834            Ok(
1835                PreparationPolicy::default().with_required_session_capabilities(
1836                    SessionCapabilities::default().with_activation_inspection(*options == 99),
1837                ),
1838            )
1839        }
1840
1841        fn select_preparation(
1842            &self,
1843            _: &ArtifactInspection,
1844            options: &Self::LoadOptions,
1845            _: PreparationPolicy,
1846        ) -> Result<Self::SelectedPreparation, Self::Error> {
1847            Ok(*options)
1848        }
1849
1850        fn session_capabilities(
1851            &self,
1852            _: &ArtifactInspection,
1853            _: PreparationPolicy,
1854        ) -> Result<SessionCapabilities, Self::Error> {
1855            Ok(SessionCapabilities::default())
1856        }
1857
1858        fn model_config(
1859            &self,
1860            plan: ModelPreparationPlan,
1861            selected: Self::SelectedPreparation,
1862        ) -> Result<Self::ModelConfig, Self::Error> {
1863            Ok((plan, selected))
1864        }
1865    }
1866
1867    fn write_loading_fixture(root: &Path) {
1868        std::fs::write(root.join("config.json"), r#"{"model_type":"llama"}"#).unwrap();
1869        let header = br#"{"token_embd.weight":{"dtype":"F32","shape":[1],"data_offsets":[0,4]}}"#;
1870        let mut file = std::fs::File::create(root.join("model.safetensors")).unwrap();
1871        file.write_all(&(header.len() as u64).to_le_bytes())
1872            .unwrap();
1873        file.write_all(header).unwrap();
1874        file.write_all(&[0; 4]).unwrap();
1875    }
1876    struct MockSession {
1877        model: u32,
1878        tokens: Vec<u32>,
1879        distributed: Option<MockDistributed>,
1880    }
1881    impl BackendSession<Mock> for MockSession {
1882        type PrefillInput = Vec<u32>;
1883        type DecodeInput = u32;
1884        type Output = u32;
1885        type Completion = Done;
1886        fn capabilities(&self) -> SessionCapabilities {
1887            SessionCapabilities::default()
1888        }
1889        fn prefill(
1890            &mut self,
1891            _: &Mock,
1892            input: Vec<u32>,
1893        ) -> Result<Submission<u32, Done>, Infallible> {
1894            self.tokens.extend(input);
1895            Ok(Submission {
1896                output: self.tokens.len() as u32 + self.model,
1897                completion: Done,
1898            })
1899        }
1900        fn decode(&mut self, _: &Mock, input: u32) -> Result<Submission<u32, Done>, Infallible> {
1901            self.tokens.push(input);
1902            Ok(Submission {
1903                output: self.tokens.len() as u32 + self.model,
1904                completion: Done,
1905            })
1906        }
1907
1908        fn observe_output(&self, _: &Mock, output: &u32) -> Result<ObservationSet, Infallible> {
1909            let mut observations = ObservationSet::new();
1910            observations
1911                .insert(
1912                    "mock.output",
1913                    crate::ObservationValue::Unsigned(u64::from(*output)),
1914                )
1915                .unwrap();
1916            Ok(observations)
1917        }
1918    }
1919
1920    impl TextGenerationBackend for Mock {
1921        type Prompt = Vec<u32>;
1922        type Token = u32;
1923        type TextGenerationState = (u32, u64);
1924        type TextCompletion = Done;
1925
1926        fn start_text_generation(
1927            _: &Self,
1928            config: TextGenerationConfig,
1929        ) -> Result<Self::TextGenerationState, Self::Error> {
1930            Ok((config.sampling().top_k as u32, config.seed()))
1931        }
1932
1933        fn prepare_text_prompt(
1934            _: &Self,
1935            prompt_token_ids: Vec<u32>,
1936        ) -> Result<Self::Prompt, Self::Error> {
1937            Ok(prompt_token_ids)
1938        }
1939
1940        fn submit_text_prefill(
1941            runtime: &mut ModelRuntime<Self>,
1942            prompt: Self::Prompt,
1943            filter: &TokenFilter,
1944            state: &mut Self::TextGenerationState,
1945        ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error> {
1946            let submission = runtime.prefill(prompt)?;
1947            Ok(Submission {
1948                output: apply_mock_filter(submission.output + state.0 + state.1 as u32, filter),
1949                completion: submission.completion,
1950            })
1951        }
1952
1953        fn submit_text_decode(
1954            runtime: &mut ModelRuntime<Self>,
1955            token: Self::Token,
1956            filter: &TokenFilter,
1957            _: &mut Self::TextGenerationState,
1958        ) -> Result<Submission<Self::Token, Self::TextCompletion>, Self::Error> {
1959            let submission = runtime.decode(token)?;
1960            Ok(Submission {
1961                output: apply_mock_filter(submission.output, filter),
1962                completion: submission.completion,
1963            })
1964        }
1965    }
1966
1967    impl MultimodalPreparationBackend for Mock {
1968        fn prepare_multimodal_input<E>(
1969            _: &ModelRuntime<Self>,
1970            request: &TokenizedMultimodalRequest,
1971            _: &mut dyn FnMut(&str) -> Result<Vec<u32>, E>,
1972        ) -> Result<Self::Prompt, MultimodalPreparationFailure<Self::Error, E>>
1973        where
1974            E: std::error::Error + Send + Sync + 'static,
1975        {
1976            let mut prompt = Vec::new();
1977            for segment in request.segments() {
1978                match segment {
1979                    crate::TokenizedMultimodalSegment::TokenIds(ids) => {
1980                        prompt.extend_from_slice(ids);
1981                    }
1982                    crate::TokenizedMultimodalSegment::Media(crate::Media::Image(_)) => {
1983                        prompt.push(1_001);
1984                    }
1985                    crate::TokenizedMultimodalSegment::Media(crate::Media::Video(_)) => {
1986                        prompt.push(1_002);
1987                    }
1988                    crate::TokenizedMultimodalSegment::Media(crate::Media::Audio(_)) => {
1989                        prompt.push(1_003);
1990                    }
1991                }
1992            }
1993            Ok(prompt)
1994        }
1995    }
1996
1997    impl ModelCapabilityBackend for Mock {
1998        fn model_capabilities(
1999            _: &ModelRuntime<Self>,
2000        ) -> Result<ModelCapabilities, CapabilityError> {
2001            Ok(ModelCapabilities {
2002                effective_model_type: "mock".into(),
2003                native_max_context: crate::Observed::exact(64, "mock configuration"),
2004                effective_max_context: crate::Observed::exact(64, "mock configuration"),
2005                state_strategy: crate::CacheStateStrategy::FullKv,
2006                modalities: crate::InputModalities::TEXT,
2007                estimation: crate::EstimationCompleteness::Complete,
2008            })
2009        }
2010
2011        fn count_prepared_input(
2012            _: &ModelRuntime<Self>,
2013            input: &Self::Prompt,
2014        ) -> Result<InputTokenCount, CapabilityError> {
2015            Ok(InputTokenCount::text(input.len() as u64))
2016        }
2017
2018        fn estimate_runtime_state(
2019            _: &ModelRuntime<Self>,
2020            input: InputTokenCount,
2021            max_output_tokens: u64,
2022            batch_size: u64,
2023        ) -> Result<RuntimeStateEstimate, CapabilityError> {
2024            crate::estimate_runtime_state(
2025                &crate::StateMemoryLayout::new(
2026                    crate::LayerSchedule::new(
2027                        1,
2028                        vec![crate::cache::LayerCachePolicy::key_only(
2029                            crate::AttentionPolicy::Full,
2030                            1,
2031                            2,
2032                        )
2033                        .unwrap()],
2034                    )
2035                    .unwrap(),
2036                    vec![0],
2037                    1,
2038                    1,
2039                    crate::EstimationCompleteness::Complete,
2040                )
2041                .unwrap(),
2042                input,
2043                max_output_tokens,
2044                batch_size,
2045                std::num::NonZeroU8::new(4).unwrap(),
2046            )
2047        }
2048
2049        fn static_memory(
2050            runtime: &ModelRuntime<Self>,
2051        ) -> Result<StaticMemoryReport, CapabilityError> {
2052            let unavailable = || crate::Observed::unavailable("mock does not expose this counter");
2053            Ok(StaticMemoryReport {
2054                logical_parameter_bytes: crate::Observed::exact(
2055                    u64::from(runtime.session().model),
2056                    "mock model",
2057                ),
2058                current_host_resident_bytes: unavailable(),
2059                current_device_resident_bytes: unavailable(),
2060                planned_disk_backed_bytes: unavailable(),
2061                backend_active_allocation_bytes: unavailable(),
2062                backend_allocator_cache_bytes: unavailable(),
2063                physical_semantics: crate::PhysicalMemorySemantics::Unknown,
2064                currently_cached_shards: unavailable(),
2065            })
2066        }
2067    }
2068
2069    fn apply_mock_filter(candidate: u32, filter: &TokenFilter) -> u32 {
2070        let Some(allowed) = filter.allowed_mask() else {
2071            return candidate;
2072        };
2073        allowed
2074            .get(candidate as usize)
2075            .copied()
2076            .unwrap_or(false)
2077            .then_some(candidate)
2078            .or_else(|| {
2079                allowed
2080                    .iter()
2081                    .position(|allowed| *allowed)
2082                    .map(|token| token as u32)
2083            })
2084            .expect("validated token filters allow at least one token")
2085    }
2086
2087    #[test]
2088    fn generic_loader_inspects_plans_and_prepares_on_the_selected_backend() {
2089        let root = tempfile::tempdir().unwrap();
2090        write_loading_fixture(root.path());
2091        let prepared = load_model(&LoadingMock::default(), root.path(), 41).unwrap();
2092        assert_eq!(*prepared, 41);
2093
2094        let runtime = ModelRuntime::load(LoadingMock::default(), root.path(), 7).unwrap();
2095        assert_eq!(runtime.backend().descriptor().name, "loading-mock");
2096
2097        let missing = root.path().join("missing");
2098        assert!(matches!(
2099            load_model(&LoadingMock::default(), &missing, 1),
2100            Err(ModelLoadError::Artifact(ArtifactError::MissingArtifact(path)))
2101                if path == missing
2102        ));
2103    }
2104
2105    #[test]
2106    fn session_requirement_is_rejected_before_materialization() {
2107        let root = tempfile::tempdir().unwrap();
2108        write_loading_fixture(root.path());
2109        let backend = LoadingMock::default();
2110
2111        let error = load_model(&backend, root.path(), 99).unwrap_err();
2112
2113        assert!(matches!(
2114            error,
2115            ModelLoadError::SessionCapability(error)
2116                if error.capability() == "activation_inspection"
2117        ));
2118        assert_eq!(
2119            backend
2120                .materializations
2121                .load(std::sync::atomic::Ordering::Relaxed),
2122            0
2123        );
2124    }
2125
2126    struct FixedController {
2127        tokens: Vec<u32>,
2128        committed: usize,
2129    }
2130
2131    impl TokenFilterController for FixedController {
2132        type Error = Infallible;
2133
2134        fn current_filter(&mut self) -> Result<TokenFilter, Self::Error> {
2135            let mut allowed = vec![false; 64];
2136            allowed[self.tokens[self.committed] as usize] = true;
2137            Ok(TokenFilter::allowed(allowed).unwrap())
2138        }
2139
2140        fn commit_token(&mut self, token_id: u32) -> Result<(), Self::Error> {
2141            assert_eq!(token_id, self.tokens[self.committed]);
2142            self.committed += 1;
2143            Ok(())
2144        }
2145
2146        fn is_complete(&mut self) -> Result<bool, Self::Error> {
2147            Ok(self.committed == self.tokens.len())
2148        }
2149    }
2150
2151    #[test]
2152    fn mock_prefill_and_multiple_decode_steps() {
2153        let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2154        let prefill = runtime.prefill(vec![1, 2]).unwrap();
2155        assert_eq!(prefill.output, 12);
2156        assert!(prefill.completion.is_complete().unwrap());
2157        assert_eq!(runtime.decode(3).unwrap().output, 13);
2158        assert_eq!(runtime.decode(4).unwrap().output, 14);
2159    }
2160
2161    #[test]
2162    fn portable_text_generation_prefills_and_decodes_without_tensor_types() {
2163        let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2164        let sampling = crate::resolve_generation_config(
2165            None,
2166            crate::GenerationConfigOverrides {
2167                max_new_tokens: Some(3),
2168                ..Default::default()
2169            },
2170        )
2171        .unwrap();
2172        let mut generation = TextGeneration::new(
2173            &mut runtime,
2174            vec![1, 2],
2175            TextGenerationConfig::new(sampling).with_seed(3),
2176        )
2177        .unwrap();
2178        assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 55);
2179        assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 13);
2180        assert_eq!(generation.next().unwrap().unwrap().token_id().unwrap(), 14);
2181        assert!(generation.next().is_none());
2182    }
2183
2184    #[test]
2185    fn portable_media_preparation_feeds_the_existing_generation_contract() {
2186        let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2187        let request = crate::MultimodalRequest::new(vec![
2188            crate::MultimodalSegment::TokenIds(vec![7, 8]),
2189            crate::MultimodalSegment::Media(crate::Media::Image(
2190                crate::RgbImage::new(vec![5, 6, 7], 1, 1).unwrap(),
2191            )),
2192            crate::MultimodalSegment::TokenIds(vec![9]),
2193        ])
2194        .unwrap()
2195        .tokenize::<Infallible>(|_| unreachable!("request is already tokenized"))
2196        .unwrap();
2197        let prompt = Mock::prepare_multimodal_input(&runtime, &request, &mut |_| {
2198            Ok::<_, Infallible>(Vec::new())
2199        })
2200        .unwrap();
2201        assert_eq!(prompt, vec![7, 8, 1_001, 9]);
2202
2203        let sampling = crate::resolve_generation_config(
2204            None,
2205            crate::GenerationConfigOverrides {
2206                max_new_tokens: Some(2),
2207                ..Default::default()
2208            },
2209        )
2210        .unwrap();
2211        let mut generation =
2212            TextGeneration::from_prompt(&mut runtime, prompt, TextGenerationConfig::new(sampling))
2213                .unwrap();
2214        assert!(generation.next().unwrap().is_ok());
2215        assert!(generation.next().unwrap().is_ok());
2216        assert!(generation.next().is_none());
2217    }
2218
2219    #[test]
2220    fn model_capability_extension_observes_the_selected_mock_session() {
2221        let runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2222        let capabilities = Mock::model_capabilities(&runtime).unwrap();
2223        assert_eq!(capabilities.effective_model_type, "mock");
2224        let input = Mock::count_prepared_input(&runtime, &vec![1, 2, 3]).unwrap();
2225        assert_eq!(input.model_positions, 3);
2226        let state = Mock::estimate_runtime_state(&runtime, input, 2, 1).unwrap();
2227        assert_eq!(state.requested_state_bytes, 5 * 2 * 4);
2228        assert_eq!(
2229            Mock::static_memory(&runtime)
2230                .unwrap()
2231                .logical_parameter_bytes
2232                .value(),
2233            Some(&10)
2234        );
2235    }
2236
2237    #[test]
2238    fn controlled_generation_applies_portable_filters_and_commits_tokens() {
2239        let mut runtime = ModelRuntime::prepare(Mock, 10).unwrap();
2240        let sampling = crate::resolve_generation_config(
2241            None,
2242            crate::GenerationConfigOverrides {
2243                max_new_tokens: Some(2),
2244                ..Default::default()
2245            },
2246        )
2247        .unwrap();
2248        let controller = FixedController {
2249            tokens: vec![7, 8],
2250            committed: 0,
2251        };
2252        let mut generation = ControlledTextGeneration::new(
2253            &mut runtime,
2254            vec![1, 2],
2255            TextGenerationConfig::new(sampling),
2256            controller,
2257        )
2258        .unwrap();
2259        assert_eq!(generation.next().unwrap().unwrap().token_id(), 7);
2260        assert_eq!(generation.next().unwrap().unwrap().token_id(), 8);
2261        assert!(generation.controller_mut().is_complete().unwrap());
2262        assert!(generation.next().is_none());
2263    }
2264
2265    #[derive(Debug, Clone)]
2266    struct MockDistributed {
2267        descriptor: DistributedSessionDescriptor,
2268    }
2269
2270    impl DistributedSession for MockDistributed {
2271        type Value = Vec<u32>;
2272        type Completion = Done;
2273        type Error = Infallible;
2274
2275        fn descriptor(&self) -> DistributedSessionDescriptor {
2276            self.descriptor.clone()
2277        }
2278
2279        fn capabilities(&self) -> DistributedCapabilities {
2280            DistributedCapabilities::new(true, [CollectiveGroupId::new(7)], true, true, true)
2281        }
2282
2283        fn all_reduce_sum(
2284            &self,
2285            _: CollectiveScope,
2286            input: &Vec<u32>,
2287        ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2288            Ok(Submission {
2289                output: input.iter().map(|value| value * 2).collect(),
2290                completion: Done,
2291            })
2292        }
2293
2294        fn all_gather(
2295            &self,
2296            _: CollectiveScope,
2297            input: &Vec<u32>,
2298        ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2299            let mut output = input.clone();
2300            output.extend(input);
2301            Ok(Submission {
2302                output,
2303                completion: Done,
2304            })
2305        }
2306
2307        fn all_to_all_v(
2308            &self,
2309            _: CollectiveScope,
2310            input: &Vec<u32>,
2311            _: &[usize],
2312            _: &[usize],
2313        ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2314            Ok(Submission {
2315                output: input.clone(),
2316                completion: Done,
2317            })
2318        }
2319
2320        fn send(
2321            &self,
2322            _: CollectiveScope,
2323            _: usize,
2324            input: &Vec<u32>,
2325        ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2326            Ok(Submission {
2327                output: input.clone(),
2328                completion: Done,
2329            })
2330        }
2331
2332        fn receive(
2333            &self,
2334            _: CollectiveScope,
2335            peer: usize,
2336            value: &ValueDescriptor,
2337        ) -> Result<Submission<Vec<u32>, Done>, Infallible> {
2338            Ok(Submission {
2339                output: vec![peer as u32; value.shape().iter().product()],
2340                completion: Done,
2341            })
2342        }
2343
2344        fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Infallible> {
2345            let mut output = local.to_vec();
2346            output.extend_from_slice(local);
2347            Ok(output)
2348        }
2349    }
2350
2351    impl DistributedBackend for Mock {
2352        type DistributedSession = MockDistributed;
2353
2354        fn distributed_session(session: &MockSession) -> Option<&Self::DistributedSession> {
2355            session.distributed.as_ref()
2356        }
2357    }
2358
2359    #[test]
2360    fn mock_distributed_session_owns_collective_and_transfer_lifecycle() {
2361        let tensor_group =
2362            CollectiveGroupDescriptor::new(CollectiveGroupId::new(7), vec![0, 1], 0).unwrap();
2363        let session = MockDistributed {
2364            descriptor: DistributedSessionDescriptor::new(2, 0, vec![tensor_group]).unwrap(),
2365        };
2366        let capabilities = session.capabilities();
2367        assert!(capabilities.exact_completion());
2368        assert_eq!(
2369            capabilities.collective_groups(),
2370            &[CollectiveGroupId::new(7)]
2371        );
2372        assert_eq!(
2373            session
2374                .all_reduce_sum(
2375                    CollectiveScope::Group(CollectiveGroupId::new(7)),
2376                    &vec![2, 3]
2377                )
2378                .unwrap()
2379                .wait()
2380                .unwrap(),
2381            vec![4, 6]
2382        );
2383        assert_eq!(
2384            session
2385                .receive(
2386                    CollectiveScope::World,
2387                    1,
2388                    &ValueDescriptor::new(vec![2], TensorDtype::U32).unwrap(),
2389                )
2390                .unwrap()
2391                .wait()
2392                .unwrap(),
2393            vec![1, 1]
2394        );
2395        assert_eq!(session.all_gather_words(&[7]).unwrap(), vec![7, 7]);
2396
2397        let model_session = MockSession {
2398            model: 0,
2399            tokens: Vec::new(),
2400            distributed: Some(session.clone()),
2401        };
2402        assert_eq!(
2403            Mock::distributed_session(&model_session)
2404                .unwrap()
2405                .descriptor(),
2406            session.descriptor()
2407        );
2408    }
2409
2410    #[test]
2411    fn distributed_descriptors_round_trip_and_reject_invalid_ranks() {
2412        let descriptor = DistributedSessionDescriptor::new(
2413            6,
2414            4,
2415            vec![CollectiveGroupDescriptor::new(CollectiveGroupId::new(9), vec![1, 4], 1).unwrap()],
2416        )
2417        .unwrap();
2418        let encoded = serde_json::to_string(&descriptor).unwrap();
2419        assert_eq!(
2420            serde_json::from_str::<DistributedSessionDescriptor>(&encoded).unwrap(),
2421            descriptor
2422        );
2423        let scope = CollectiveScope::Group(CollectiveGroupId::new(9));
2424        assert_eq!(
2425            serde_json::from_str::<CollectiveScope>(&serde_json::to_string(&scope).unwrap())
2426                .unwrap(),
2427            scope
2428        );
2429        assert!(DistributedSessionDescriptor::new(descriptor.world_size(), 6, Vec::new()).is_err());
2430        assert!(serde_json::from_str::<DistributedSessionDescriptor>(
2431            r#"{"world_size":6,"rank":6,"groups":[]}"#
2432        )
2433        .is_err());
2434    }
2435}