Skip to main content

eredu_runtime/
weight_residency.rs

1//! Backend-neutral immutable-weight residency policy.
2
3use std::collections::VecDeque;
4
5use eredu_core::{
6    residency::{CacheEvictionPolicy, OffloadConfig, OffloadError, OffloadUnitId},
7    DEFAULT_MAX_CACHED_SHARDS,
8};
9
10use crate::WeightBinding;
11
12/// Current plus next unit retained by dense streamed execution.
13pub const DENSE_TRANSFER_WINDOW: usize = 2;
14
15/// One pinned static module and its checkpoint bindings.
16#[derive(Debug, Clone, Eq, PartialEq)]
17pub struct StaticUnitBindings {
18    id: OffloadUnitId,
19    bindings: Vec<WeightBinding>,
20}
21
22impl StaticUnitBindings {
23    /// Creates a pinned static unit definition.
24    pub fn new(id: impl Into<String>, bindings: Vec<WeightBinding>) -> Result<Self, OffloadError> {
25        Ok(Self {
26            id: OffloadUnitId::new(id.into())?,
27            bindings,
28        })
29    }
30
31    /// Returns the stable residency identifier for this static module.
32    pub const fn id(&self) -> &OffloadUnitId {
33        &self.id
34    }
35
36    /// Returns the authoritative checkpoint bindings for this static module.
37    pub fn bindings(&self) -> &[WeightBinding] {
38        &self.bindings
39    }
40
41    /// Consumes the definition into its stable identifier and checkpoint bindings.
42    pub fn into_parts(self) -> (OffloadUnitId, Vec<WeightBinding>) {
43        (self.id, self.bindings)
44    }
45}
46
47/// Ordered bounded transfer cursor used by dense streamed execution.
48#[derive(Debug)]
49pub struct DenseTransferSchedule<T> {
50    capacity: usize,
51    pending: VecDeque<usize>,
52    ready: VecDeque<(usize, T)>,
53}
54
55impl<T> DenseTransferSchedule<T> {
56    /// Creates a cursor over the remaining unit indices.
57    pub fn new(
58        pending: impl IntoIterator<Item = usize>,
59        capacity: usize,
60    ) -> Result<Self, DenseTransferScheduleError> {
61        if capacity == 0 {
62            return Err(DenseTransferScheduleError::ZeroCapacity);
63        }
64        let pending = pending.into_iter().collect::<VecDeque<_>>();
65        let mut previous = None;
66        for &index in &pending {
67            if previous.is_some_and(|previous| previous >= index) {
68                return Err(DenseTransferScheduleError::UnorderedPending {
69                    previous: previous.expect("an invalid pair has a previous index"),
70                    actual: index,
71                });
72            }
73            previous = Some(index);
74        }
75        Ok(Self {
76            capacity,
77            pending,
78            ready: VecDeque::new(),
79        })
80    }
81
82    /// Returns whether at least one submitted transfer is ready for consumption.
83    pub fn has_ready(&self) -> bool {
84        !self.ready.is_empty()
85    }
86
87    /// Returns whether no ready or pending units remain.
88    pub fn is_exhausted(&self) -> bool {
89        self.ready.is_empty() && self.pending.is_empty()
90    }
91
92    /// Returns whether another transfer may be admitted into the bounded ready window.
93    pub fn can_admit(&self) -> bool {
94        self.ready.len() < self.capacity && !self.pending.is_empty()
95    }
96
97    /// Returns the next unit which must be submitted.
98    pub fn next_pending(&self) -> Option<usize> {
99        self.pending.front().copied()
100    }
101
102    /// Returns the current ready indices followed by future indices, truncated to lookahead.
103    pub fn desired_indices(&self, lookahead: usize) -> Vec<usize> {
104        self.ready
105            .iter()
106            .map(|(index, _)| *index)
107            .chain(self.pending.iter().copied())
108            .take(lookahead)
109            .collect()
110    }
111
112    /// Commits one successfully submitted transfer in exact pending order.
113    pub fn admit(&mut self, index: usize, transfer: T) -> Result<(), DenseTransferScheduleError> {
114        if self.ready.len() >= self.capacity {
115            return Err(DenseTransferScheduleError::CapacityExceeded {
116                capacity: self.capacity,
117            });
118        }
119        let expected = self
120            .pending
121            .front()
122            .copied()
123            .ok_or(DenseTransferScheduleError::NoPendingUnit)?;
124        if expected != index {
125            return Err(DenseTransferScheduleError::OutOfOrder {
126                expected,
127                actual: index,
128            });
129        }
130        self.pending.pop_front();
131        self.ready.push_back((index, transfer));
132        Ok(())
133    }
134
135    /// Removes the oldest ready transfer for execution.
136    pub fn pop_ready(&mut self) -> Option<(usize, T)> {
137        self.ready.pop_front()
138    }
139}
140
141/// Invalid transition in a bounded dense transfer schedule.
142#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
143#[non_exhaustive]
144pub enum DenseTransferScheduleError {
145    /// A transfer window cannot have zero capacity.
146    #[error("dense transfer window capacity must be nonzero")]
147    ZeroCapacity,
148    /// Pending units were not supplied in strictly increasing order.
149    #[error("dense transfer pending unit {actual} does not follow {previous}")]
150    UnorderedPending {
151        /// Previous index.
152        previous: usize,
153        /// Invalid next index.
154        actual: usize,
155    },
156    /// Admission was attempted while the ready window was full.
157    #[error("dense transfer window exceeds its capacity of {capacity}")]
158    CapacityExceeded {
159        /// Configured ready capacity.
160        capacity: usize,
161    },
162    /// Admission was attempted after all pending units were submitted.
163    #[error("dense transfer schedule has no pending unit")]
164    NoPendingUnit,
165    /// A backend submitted transfers in a different order from the architecture sequence.
166    #[error("dense transfer schedule expected unit {expected}, received {actual}")]
167    OutOfOrder {
168        /// Required next index.
169        expected: usize,
170        /// Submitted index.
171        actual: usize,
172    },
173}
174
175/// Loader controls for a host-backed layerwise execution engine.
176#[derive(Debug, Clone, Copy, Eq, PartialEq)]
177pub struct LayerwiseLoadOptions {
178    /// Residency budgets and maximum device-unit window.
179    offload: OffloadConfig,
180    /// Maximum number of checkpoint payload shards or readers retained in cache.
181    max_cached_shards: usize,
182    /// Sample backend allocator memory when a forward pass completes.
183    sample_backend_memory: bool,
184    /// Sample process memory metrics when a forward pass completes.
185    sample_process_memory: bool,
186}
187
188impl LayerwiseLoadOptions {
189    /// Creates layerwise options with the default shard-cache bound.
190    pub fn new(offload: OffloadConfig) -> Self {
191        Self {
192            offload,
193            ..Self::default()
194        }
195    }
196
197    /// Selects the checkpoint-reader cache bound.
198    pub const fn with_max_cached_shards(mut self, maximum: usize) -> Self {
199        self.max_cached_shards = maximum;
200        self
201    }
202    /// Selects allocator and process memory sampling.
203    pub const fn with_memory_sampling(mut self, backend: bool, process: bool) -> Self {
204        self.sample_backend_memory = backend;
205        self.sample_process_memory = process;
206        self
207    }
208    /// Returns the exact offload limits.
209    pub const fn offload(self) -> OffloadConfig {
210        self.offload
211    }
212    /// Returns the checkpoint-reader cache bound.
213    pub const fn max_cached_shards(self) -> usize {
214        self.max_cached_shards
215    }
216    /// Returns whether backend allocator sampling is enabled.
217    pub const fn samples_backend_memory(self) -> bool {
218        self.sample_backend_memory
219    }
220    /// Returns whether process memory sampling is enabled.
221    pub const fn samples_process_memory(self) -> bool {
222        self.sample_process_memory
223    }
224}
225
226impl Default for LayerwiseLoadOptions {
227    fn default() -> Self {
228        Self {
229            offload: OffloadConfig::default(),
230            max_cached_shards: DEFAULT_MAX_CACHED_SHARDS,
231            sample_backend_memory: false,
232            sample_process_memory: false,
233        }
234    }
235}
236
237/// Controls for bounded dense-unit streaming from checkpoint storage.
238#[derive(Debug, Clone, Copy, Eq, PartialEq)]
239pub struct DenseDiskStreamLoadOptions {
240    /// Finite logical device parameter budget, including pinned static weights.
241    device_budget_bytes: u64,
242    /// Finite charged host-allocation budget. Zero selects direct disk-to-device loading.
243    host_budget_bytes: u64,
244    /// Number of current and imminent unit host copies protected from eviction.
245    host_lookahead: usize,
246    /// Maximum number of pending background host materializations.
247    background_queue_capacity: usize,
248    /// Deterministic ordering used when unprotected cached copies must be evicted.
249    eviction_policy: CacheEvictionPolicy,
250    /// Maximum number of checkpoint payload shards or readers retained in cache.
251    max_cached_shards: usize,
252    /// Sample backend allocator memory after a forward pass.
253    sample_backend_memory: bool,
254    /// Sample process memory and page-fault counters after a forward pass.
255    sample_process_memory: bool,
256}
257
258impl DenseDiskStreamLoadOptions {
259    /// Creates streaming options with finite tier budgets.
260    pub fn new(
261        device_budget_bytes: u64,
262        host_budget_bytes: u64,
263        host_lookahead: usize,
264        background_queue_capacity: usize,
265    ) -> Result<Self, WeightResidencyPolicyError> {
266        let options = Self {
267            device_budget_bytes,
268            host_budget_bytes,
269            host_lookahead,
270            background_queue_capacity,
271            eviction_policy: CacheEvictionPolicy::LeastRecentlyUsed,
272            max_cached_shards: DEFAULT_MAX_CACHED_SHARDS,
273            sample_backend_memory: false,
274            sample_process_memory: false,
275        };
276        options.validate()?;
277        Ok(options)
278    }
279
280    /// Revalidates the complete bounded policy.
281    pub fn validate(self) -> Result<(), WeightResidencyPolicyError> {
282        if self.host_budget_bytes == 0 {
283            if self.host_lookahead != 0 || self.background_queue_capacity != 0 {
284                return Err(WeightResidencyPolicyError::HostDisabledControls);
285            }
286        } else {
287            if self.host_lookahead == 0 {
288                return Err(WeightResidencyPolicyError::ZeroHostLookahead);
289            }
290            if self.background_queue_capacity == 0 {
291                return Err(WeightResidencyPolicyError::ZeroQueueCapacity);
292            }
293        }
294        Ok(())
295    }
296
297    /// Selects deterministic cache eviction.
298    pub const fn with_eviction_policy(mut self, policy: CacheEvictionPolicy) -> Self {
299        self.eviction_policy = policy;
300        self
301    }
302
303    /// Selects the checkpoint-reader cache bound.
304    pub const fn with_max_cached_shards(mut self, maximum: usize) -> Self {
305        self.max_cached_shards = maximum;
306        self
307    }
308    /// Selects allocator and process memory sampling.
309    pub const fn with_memory_sampling(mut self, backend: bool, process: bool) -> Self {
310        self.sample_backend_memory = backend;
311        self.sample_process_memory = process;
312        self
313    }
314    /// Returns the finite logical device budget.
315    pub const fn device_budget_bytes(self) -> u64 {
316        self.device_budget_bytes
317    }
318    /// Returns the finite charged host budget.
319    pub const fn host_budget_bytes(self) -> u64 {
320        self.host_budget_bytes
321    }
322    /// Returns the protected host lookahead.
323    pub const fn host_lookahead(self) -> usize {
324        self.host_lookahead
325    }
326    /// Returns the background materialization queue bound.
327    pub const fn background_queue_capacity(self) -> usize {
328        self.background_queue_capacity
329    }
330    /// Returns deterministic eviction ordering.
331    pub const fn eviction_policy(self) -> CacheEvictionPolicy {
332        self.eviction_policy
333    }
334    /// Returns the checkpoint-reader cache bound.
335    pub const fn max_cached_shards(self) -> usize {
336        self.max_cached_shards
337    }
338    /// Returns whether backend allocator sampling is enabled.
339    pub const fn samples_backend_memory(self) -> bool {
340        self.sample_backend_memory
341    }
342    /// Returns whether process memory sampling is enabled.
343    pub const fn samples_process_memory(self) -> bool {
344        self.sample_process_memory
345    }
346}
347
348impl Default for DenseDiskStreamLoadOptions {
349    fn default() -> Self {
350        Self::new(4 << 30, 16 << 30, 2, 2).expect("default dense disk streaming controls are valid")
351    }
352}
353
354/// Placement policy for ordinary architecture execution units.
355#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
356#[non_exhaustive]
357pub enum LayerWeightResidency {
358    /// Construct every rank-local module once and retain it on the execution device.
359    #[default]
360    FullyResident,
361    /// Eagerly materialize units on a host stream and use a bounded device window.
362    LayerwiseHost(LayerwiseLoadOptions),
363    /// Leave units cold on disk and use finite host and device caches.
364    DenseDiskStream(DenseDiskStreamLoadOptions),
365}
366
367/// Stable mechanism identity for one member of an independently addressable bank.
368#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
369pub struct ParameterBankKey {
370    unit: usize,
371    member: usize,
372}
373
374impl ParameterBankKey {
375    /// Creates one generic bank member identity after semantic translation.
376    pub const fn new(unit: usize, member: usize) -> Self {
377        Self { unit, member }
378    }
379
380    /// Returns the owning execution-unit ordinal.
381    pub const fn unit(self) -> usize {
382        self.unit
383    }
384
385    /// Returns the member ordinal within the architecture's global bank namespace.
386    pub const fn member(self) -> usize {
387        self.member
388    }
389
390    /// Returns the deterministic residency unit identifier.
391    pub fn unit_id(self) -> OffloadUnitId {
392        OffloadUnitId::new(format!(
393            "bank.unit.{:05}.member.{:05}",
394            self.unit, self.member
395        ))
396        .expect("parameter-bank unit identifier is non-empty")
397    }
398}
399
400/// Execution-path classification for routed-expert telemetry and chunking.
401#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
402#[non_exhaustive]
403pub enum ExpertPass {
404    /// Prompt processing with more than one input token.
405    Prefill,
406    /// Autoregressive processing of one input token.
407    Decode,
408}
409
410/// Backend-neutral controls for independently addressable parameter-bank residency.
411#[derive(Debug, Clone, Copy, Eq, PartialEq)]
412pub struct ParameterBankLoadOptions {
413    /// Independent host/device budgets and eviction policy for bank members.
414    members: OffloadConfig,
415    /// Hard maximum bytes for one materialized temporary compact bank.
416    compact_bank_scratch_bytes: u64,
417    /// Soft compact-bank target used to split multi-token prefill routing.
418    prefill_compact_bank_target_bytes: u64,
419}
420
421impl ParameterBankLoadOptions {
422    /// Creates strict independently addressable bank-caching options.
423    pub fn new(
424        members: OffloadConfig,
425        compact_bank_scratch_bytes: u64,
426        prefill_compact_bank_target_bytes: u64,
427    ) -> Result<Self, WeightResidencyPolicyError> {
428        let options = Self {
429            members,
430            compact_bank_scratch_bytes,
431            prefill_compact_bank_target_bytes,
432        };
433        options.validate()?;
434        Ok(options)
435    }
436
437    /// Revalidates the complete independently addressable residency policy.
438    pub fn validate(self) -> Result<(), WeightResidencyPolicyError> {
439        if self.compact_bank_scratch_bytes == 0 {
440            return Err(WeightResidencyPolicyError::ZeroParameterBankScratchLimit);
441        }
442        if self.prefill_compact_bank_target_bytes == 0 {
443            return Err(WeightResidencyPolicyError::ZeroParameterBankPrefillTarget);
444        }
445        if self.prefill_compact_bank_target_bytes > self.compact_bank_scratch_bytes {
446            return Err(
447                WeightResidencyPolicyError::ParameterBankPrefillTargetExceedsScratch {
448                    target_bytes: self.prefill_compact_bank_target_bytes,
449                    scratch_bytes: self.compact_bank_scratch_bytes,
450                },
451            );
452        }
453        Ok(())
454    }
455
456    /// Returns the independent bank offload limits.
457    pub const fn offload(self) -> OffloadConfig {
458        self.members
459    }
460    /// Returns the hard compact-bank scratch bound.
461    pub const fn compact_bank_scratch_bytes(self) -> u64 {
462        self.compact_bank_scratch_bytes
463    }
464    /// Returns the prefill compact-bank target.
465    pub const fn prefill_compact_bank_target_bytes(self) -> u64 {
466        self.prefill_compact_bank_target_bytes
467    }
468}
469
470impl Default for ParameterBankLoadOptions {
471    fn default() -> Self {
472        Self {
473            members: OffloadConfig::default(),
474            compact_bank_scratch_bytes: u64::MAX,
475            prefill_compact_bank_target_bytes: 1 << 30,
476        }
477    }
478}
479
480/// Placement of ordinary parameters beside independently addressable banks.
481#[derive(Debug, Clone, Copy, Eq, PartialEq)]
482#[non_exhaustive]
483pub enum OrdinaryWeightResidency {
484    /// Keep every ordinary parameter resident on the execution device.
485    FullyResident,
486    /// Eagerly materialize ordinary units on host behind a device window.
487    LayerwiseHost(LayerwiseLoadOptions),
488    /// Leave ordinary units cold on disk behind finite tier caches.
489    DenseDiskStream(DenseDiskStreamLoadOptions),
490}
491
492impl OrdinaryWeightResidency {
493    /// Returns the corresponding generalized layer policy.
494    pub const fn layers(self) -> LayerWeightResidency {
495        match self {
496            Self::FullyResident => LayerWeightResidency::FullyResident,
497            Self::LayerwiseHost(options) => LayerWeightResidency::LayerwiseHost(options),
498            Self::DenseDiskStream(options) => LayerWeightResidency::DenseDiskStream(options),
499        }
500    }
501}
502
503impl From<OrdinaryWeightResidency> for LayerWeightResidency {
504    fn from(value: OrdinaryWeightResidency) -> Self {
505        value.layers()
506    }
507}
508
509/// Independently addressable bank placement relative to ordinary execution units.
510#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
511#[non_exhaustive]
512pub enum ParameterBankResidency {
513    /// Keep bank members in the ordinary unit residency allocation.
514    #[default]
515    WithLayer,
516    /// Catalog bank members as independent atomic residency units.
517    IndependentCache(ParameterBankLoadOptions),
518}
519
520/// Composable ordinary-unit and independently addressable bank placement.
521#[derive(Debug, Clone, Copy, Eq, PartialEq)]
522#[non_exhaustive]
523pub enum WeightResidency {
524    /// All parameters share the ordinary unit residency allocation.
525    Layers(LayerWeightResidency),
526    /// Addressable bank members are independent units beside bounded ordinary units.
527    #[non_exhaustive]
528    IndependentParameterBanks {
529        /// Bounded ordinary-unit policy.
530        ordinary: OrdinaryWeightResidency,
531        /// Bank-member-granular cache controls.
532        cache: ParameterBankLoadOptions,
533    },
534}
535
536impl WeightResidency {
537    /// Keeps every rank-owned parameter on the execution device.
538    pub const fn fully_resident() -> Self {
539        Self::with_layers(LayerWeightResidency::FullyResident)
540    }
541
542    /// Eagerly materializes ordinary units on host behind a bounded device window.
543    pub const fn layerwise_host(options: LayerwiseLoadOptions) -> Self {
544        Self::with_layers(LayerWeightResidency::LayerwiseHost(options))
545    }
546
547    /// Leaves ordinary units cold on disk behind finite host/device caches.
548    pub const fn dense_disk_stream(options: DenseDiskStreamLoadOptions) -> Self {
549        Self::with_layers(LayerWeightResidency::DenseDiskStream(options))
550    }
551
552    /// Keeps every owned parameter in its ordinary unit residency allocation.
553    pub const fn with_layers(layers: LayerWeightResidency) -> Self {
554        Self::Layers(layers)
555    }
556
557    /// Gives addressable parameter banks an independent cache beside ordinary units.
558    pub const fn with_independent_parameter_banks(
559        ordinary: OrdinaryWeightResidency,
560        cache: ParameterBankLoadOptions,
561    ) -> Self {
562        Self::IndependentParameterBanks { ordinary, cache }
563    }
564
565    /// Returns ordinary-unit placement.
566    pub const fn layers(self) -> LayerWeightResidency {
567        match self {
568            Self::Layers(layers) => layers,
569            Self::IndependentParameterBanks { ordinary, .. } => ordinary.layers(),
570        }
571    }
572
573    /// Returns independently addressable bank placement relative to ordinary units.
574    pub const fn parameter_banks(self) -> ParameterBankResidency {
575        match self {
576            Self::Layers(_) => ParameterBankResidency::WithLayer,
577            Self::IndependentParameterBanks { cache, .. } => {
578                ParameterBankResidency::IndependentCache(cache)
579            }
580        }
581    }
582
583    /// Returns independently cached parameter-bank controls, when selected.
584    pub const fn parameter_bank_cache(self) -> Option<ParameterBankLoadOptions> {
585        match self {
586            Self::Layers(_) => None,
587            Self::IndependentParameterBanks { cache, .. } => Some(cache),
588        }
589    }
590
591    /// Returns ordinary placement paired with an independent parameter-bank cache.
592    pub const fn ordinary_residency(self) -> Option<OrdinaryWeightResidency> {
593        match self {
594            Self::Layers(_) => None,
595            Self::IndependentParameterBanks { ordinary, .. } => Some(ordinary),
596        }
597    }
598
599    /// Returns whether every ordinary parameter remains resident.
600    pub const fn ordinary_is_fully_resident(self) -> bool {
601        matches!(
602            self,
603            Self::Layers(LayerWeightResidency::FullyResident)
604                | Self::IndependentParameterBanks {
605                    ordinary: OrdinaryWeightResidency::FullyResident,
606                    ..
607                }
608        )
609    }
610
611    /// Returns whether every ordinary unit remains resident.
612    pub const fn is_fully_resident(self) -> bool {
613        matches!(self, Self::Layers(LayerWeightResidency::FullyResident))
614    }
615
616    /// Returns the common checkpoint shard/reader cache bound.
617    pub const fn max_cached_shards(self) -> usize {
618        self.layers().max_cached_shards()
619    }
620}
621
622impl Default for WeightResidency {
623    fn default() -> Self {
624        Self::fully_resident()
625    }
626}
627
628impl LayerWeightResidency {
629    /// Returns the checkpoint shard/reader cache bound carried by this policy.
630    pub const fn max_cached_shards(self) -> usize {
631        match self {
632            Self::FullyResident => DEFAULT_MAX_CACHED_SHARDS,
633            Self::LayerwiseHost(options) => options.max_cached_shards,
634            Self::DenseDiskStream(options) => options.max_cached_shards,
635        }
636    }
637
638    /// Returns whether backend allocator memory should be sampled.
639    pub const fn sample_backend_memory(self) -> bool {
640        match self {
641            Self::FullyResident => false,
642            Self::LayerwiseHost(options) => options.sample_backend_memory,
643            Self::DenseDiskStream(options) => options.sample_backend_memory,
644        }
645    }
646
647    /// Returns whether process memory should be sampled.
648    pub const fn sample_process_memory(self) -> bool {
649        match self {
650            Self::FullyResident => false,
651            Self::LayerwiseHost(options) => options.sample_process_memory,
652            Self::DenseDiskStream(options) => options.sample_process_memory,
653        }
654    }
655
656    /// Returns the maximum execution-device unit window.
657    pub fn device_depth(self, unit_count: usize) -> usize {
658        match self {
659            Self::FullyResident => unit_count,
660            Self::LayerwiseHost(options) => options.offload.prefetch_depth(),
661            Self::DenseDiskStream(_) => unit_count.min(DENSE_TRANSFER_WINDOW),
662        }
663    }
664
665    /// Resolves this policy into the shared offload configuration.
666    pub fn offload(self) -> Result<OffloadConfig, WeightResidencyPolicyError> {
667        match self {
668            Self::FullyResident => Ok(OffloadConfig::default()),
669            Self::LayerwiseHost(options) => Ok(options.offload),
670            Self::DenseDiskStream(options) => {
671                options.validate()?;
672                Ok(OffloadConfig::new(
673                    Some(options.device_budget_bytes),
674                    Some(options.host_budget_bytes),
675                    options.host_lookahead.max(DENSE_TRANSFER_WINDOW),
676                )?
677                .with_eviction_policy(options.eviction_policy))
678            }
679        }
680    }
681
682    /// Returns dense-stream controls when this policy streams from disk.
683    pub const fn dense(self) -> Option<DenseDiskStreamLoadOptions> {
684        match self {
685            Self::DenseDiskStream(options) => Some(options),
686            Self::FullyResident | Self::LayerwiseHost(_) => None,
687        }
688    }
689
690    /// Returns whether every ordinary execution unit remains device-resident.
691    pub const fn is_fully_resident(self) -> bool {
692        matches!(self, Self::FullyResident)
693    }
694
695    /// Returns the stable physical execution classification.
696    pub const fn execution_residency(self) -> ExecutionResidency {
697        match self {
698            Self::FullyResident => ExecutionResidency::FullyResident,
699            Self::LayerwiseHost(_) => ExecutionResidency::LayerwiseHost,
700            Self::DenseDiskStream(_) => ExecutionResidency::DenseDiskStream,
701        }
702    }
703}
704
705impl From<LayerwiseLoadOptions> for LayerWeightResidency {
706    fn from(value: LayerwiseLoadOptions) -> Self {
707        Self::LayerwiseHost(value)
708    }
709}
710
711impl From<DenseDiskStreamLoadOptions> for LayerWeightResidency {
712    fn from(value: DenseDiskStreamLoadOptions) -> Self {
713        Self::DenseDiskStream(value)
714    }
715}
716
717/// Static-parameter placement used by the generalized execution engine.
718#[derive(Debug, Clone, Copy, Eq, PartialEq)]
719#[non_exhaustive]
720pub enum ExecutionResidency {
721    /// Every module is constructed once and all rank-local parameters remain on device.
722    FullyResident,
723    /// Parameters remain on host behind bounded device windows.
724    LayerwiseHost,
725    /// Parameters are materialized through bounded disk, host, and device caches.
726    DenseDiskStream,
727}
728
729/// Inspectable backend-neutral parameter-residency metadata for a layered model.
730#[derive(Debug, Clone, Eq, PartialEq)]
731pub struct LayerwiseModelMetadata {
732    effective_model_type: String,
733    quantization: Option<eredu_checkpoint::WeightQuantization>,
734    layer_count: usize,
735    static_device_bytes: u64,
736    residency: ExecutionResidency,
737    layer_parameter_bytes: u64,
738    maximum_device_layer_bytes: u64,
739    maximum_host_layer_bytes: u64,
740    device_layer_capacity: usize,
741    materialization: Option<crate::WeightMaterializationReport>,
742}
743
744impl LayerwiseModelMetadata {
745    /// Creates one complete metadata snapshot before optional materialization telemetry.
746    #[allow(clippy::too_many_arguments)]
747    pub fn new(
748        effective_model_type: impl Into<String>,
749        quantization: Option<eredu_checkpoint::WeightQuantization>,
750        layer_count: usize,
751        static_device_bytes: u64,
752        residency: ExecutionResidency,
753        layer_parameter_bytes: u64,
754        maximum_device_layer_bytes: u64,
755        maximum_host_layer_bytes: u64,
756        device_layer_capacity: usize,
757    ) -> Self {
758        Self {
759            effective_model_type: effective_model_type.into(),
760            quantization,
761            layer_count,
762            static_device_bytes,
763            residency,
764            layer_parameter_bytes,
765            maximum_device_layer_bytes,
766            maximum_host_layer_bytes,
767            device_layer_capacity,
768            materialization: None,
769        }
770    }
771
772    /// Replaces the parsed implementation identity after checkpoint resolution.
773    pub fn set_effective_model_type(&mut self, effective_model_type: impl Into<String>) {
774        self.effective_model_type = effective_model_type.into();
775    }
776
777    /// Replaces checkpoint-native packed quantization metadata.
778    pub fn set_quantization(&mut self, quantization: Option<eredu_checkpoint::WeightQuantization>) {
779        self.quantization = quantization;
780    }
781
782    /// Replaces bounded load-time materialization telemetry.
783    pub fn set_materialization(
784        &mut self,
785        materialization: Option<crate::WeightMaterializationReport>,
786    ) {
787        self.materialization = materialization;
788    }
789
790    /// Returns the parsed implementation or nested text-model type.
791    pub fn effective_model_type(&self) -> &str {
792        &self.effective_model_type
793    }
794
795    /// Returns checkpoint-native packed quantization metadata, if present.
796    pub const fn quantization(&self) -> Option<eredu_checkpoint::WeightQuantization> {
797        self.quantization
798    }
799
800    /// Returns the ordered execution-unit count.
801    pub const fn layer_count(&self) -> usize {
802        self.layer_count
803    }
804
805    /// Returns pinned static parameter bytes on the execution device.
806    pub const fn static_device_bytes(&self) -> u64 {
807        self.static_device_bytes
808    }
809
810    /// Returns the selected generalized parameter-residency policy.
811    pub const fn residency(&self) -> ExecutionResidency {
812        self.residency
813    }
814
815    /// Returns complete rank-local execution-unit parameter bytes.
816    pub const fn layer_parameter_bytes(&self) -> u64 {
817        self.layer_parameter_bytes
818    }
819
820    /// Returns the largest possible device-resident execution-unit byte total.
821    pub const fn maximum_device_layer_bytes(&self) -> u64 {
822        self.maximum_device_layer_bytes
823    }
824
825    /// Returns the charged host-transfer capacity of the largest execution unit.
826    pub const fn maximum_host_layer_bytes(&self) -> u64 {
827        self.maximum_host_layer_bytes
828    }
829
830    /// Returns the maximum number of execution units retained on device.
831    pub const fn device_layer_capacity(&self) -> usize {
832        self.device_layer_capacity
833    }
834
835    /// Returns bounded load-time materialization telemetry.
836    pub const fn materialization(&self) -> Option<&crate::WeightMaterializationReport> {
837        self.materialization.as_ref()
838    }
839}
840
841/// Invalid backend-neutral immutable-weight residency policy.
842#[derive(Debug, thiserror::Error)]
843#[non_exhaustive]
844pub enum WeightResidencyPolicyError {
845    /// Enabled host caching needs a protected current unit.
846    #[error("dense disk streaming host lookahead must be nonzero when the host budget is enabled")]
847    ZeroHostLookahead,
848    /// Enabled background work requires bounded capacity.
849    #[error("dense disk streaming background queue capacity must be nonzero when host caching is enabled")]
850    ZeroQueueCapacity,
851    /// Direct-to-device mode cannot configure host-only controls.
852    #[error("dense disk streaming with a zero host budget requires zero host lookahead and queue capacity")]
853    HostDisabledControls,
854    /// Parameter-bank scratch accounting was disabled with a zero limit.
855    #[error("parameter-bank compact scratch limit must be nonzero")]
856    ZeroParameterBankScratchLimit,
857    /// Parameter-bank prefill chunking was disabled with a zero target.
858    #[error("parameter-bank prefill target must be nonzero")]
859    ZeroParameterBankPrefillTarget,
860    /// The parameter-bank prefill target exceeded the hard scratch bound.
861    #[error("parameter-bank prefill target {target_bytes} exceeds scratch limit {scratch_bytes}")]
862    ParameterBankPrefillTargetExceedsScratch {
863        /// Requested soft prefill target.
864        target_bytes: u64,
865        /// Configured hard scratch limit.
866        scratch_bytes: u64,
867    },
868    /// The derived offload configuration was invalid.
869    #[error(transparent)]
870    Offload(#[from] OffloadError),
871}
872
873#[cfg(test)]
874mod tests {
875    use super::*;
876
877    #[test]
878    fn static_unit_bindings_are_runtime_owned_and_decomposable() {
879        let binding = WeightBinding::new(
880            "embedding",
881            "model.embedding.weight",
882            eredu_checkpoint::store::TensorSelection::Full,
883            16,
884        )
885        .unwrap();
886        let unit = StaticUnitBindings::new("static.embedding", vec![binding.clone()]).unwrap();
887
888        assert_eq!(unit.id().as_str(), "static.embedding");
889        assert_eq!(unit.bindings(), std::slice::from_ref(&binding));
890        let (id, bindings) = unit.into_parts();
891        assert_eq!(id.as_str(), "static.embedding");
892        assert_eq!(bindings, vec![binding]);
893    }
894
895    #[test]
896    fn dense_stream_controls_fail_closed_without_a_backend() {
897        assert!(matches!(
898            DenseDiskStreamLoadOptions::new(1, 1, 0, 1),
899            Err(WeightResidencyPolicyError::ZeroHostLookahead)
900        ));
901        assert!(matches!(
902            DenseDiskStreamLoadOptions::new(1, 1, 1, 0),
903            Err(WeightResidencyPolicyError::ZeroQueueCapacity)
904        ));
905        assert!(matches!(
906            DenseDiskStreamLoadOptions::new(1, 0, 1, 0),
907            Err(WeightResidencyPolicyError::HostDisabledControls)
908        ));
909        assert!(DenseDiskStreamLoadOptions::new(1, 0, 0, 0).is_ok());
910    }
911
912    #[test]
913    fn residency_policy_derives_finite_dense_windows() {
914        let options = DenseDiskStreamLoadOptions::new(32, 64, 3, 2).unwrap();
915        let policy = LayerWeightResidency::DenseDiskStream(options);
916        assert_eq!(policy.device_depth(8), DENSE_TRANSFER_WINDOW);
917        let offload = policy.offload().unwrap();
918        assert_eq!(offload.device_budget_bytes(), Some(32));
919        assert_eq!(offload.host_budget_bytes(), Some(64));
920        assert_eq!(offload.prefetch_depth(), 3);
921    }
922
923    #[test]
924    fn dense_transfer_schedule_preserves_order_and_bounded_lookahead() {
925        let mut schedule = DenseTransferSchedule::new(3..7, 2).unwrap();
926        assert_eq!(schedule.desired_indices(3), vec![3, 4, 5]);
927        assert_eq!(
928            schedule.admit(4, "wrong"),
929            Err(DenseTransferScheduleError::OutOfOrder {
930                expected: 3,
931                actual: 4,
932            })
933        );
934        schedule.admit(3, "three").unwrap();
935        schedule.admit(4, "four").unwrap();
936        assert!(!schedule.can_admit());
937        assert_eq!(schedule.desired_indices(3), vec![3, 4, 5]);
938        assert_eq!(schedule.pop_ready(), Some((3, "three")));
939        schedule.admit(5, "five").unwrap();
940        assert_eq!(schedule.desired_indices(4), vec![4, 5, 6]);
941        assert_eq!(schedule.pop_ready(), Some((4, "four")));
942        assert_eq!(schedule.pop_ready(), Some((5, "five")));
943        schedule.admit(6, "six").unwrap();
944        assert_eq!(schedule.pop_ready(), Some((6, "six")));
945        assert!(schedule.is_exhausted());
946    }
947
948    #[test]
949    fn expert_cache_controls_and_composite_placement_are_backend_neutral() {
950        let experts = ParameterBankLoadOptions::new(OffloadConfig::default(), 64, 32).unwrap();
951        let placement = WeightResidency::with_independent_parameter_banks(
952            OrdinaryWeightResidency::FullyResident,
953            experts,
954        );
955        assert_eq!(placement.parameter_bank_cache(), Some(experts));
956        assert!(placement.ordinary_is_fully_resident());
957        assert!(!placement.is_fully_resident());
958        assert!(matches!(
959            ParameterBankLoadOptions::new(OffloadConfig::default(), 64, 0),
960            Err(WeightResidencyPolicyError::ZeroParameterBankPrefillTarget)
961        ));
962        assert_eq!(
963            ParameterBankKey::new(3, 7).unit_id().as_str(),
964            "bank.unit.00003.member.00007"
965        );
966    }
967
968    #[test]
969    fn layerwise_metadata_is_runtime_owned_and_updateable() {
970        let mut metadata = LayerwiseModelMetadata::new(
971            "generic",
972            None,
973            4,
974            10,
975            ExecutionResidency::LayerwiseHost,
976            20,
977            8,
978            6,
979            2,
980        );
981        metadata.set_effective_model_type("llama");
982        metadata.set_quantization(Some(eredu_checkpoint::WeightQuantization::Affine(
983            eredu_checkpoint::AffineQuantization::default(),
984        )));
985
986        assert_eq!(metadata.effective_model_type(), "llama");
987        assert_eq!(metadata.layer_count(), 4);
988        assert_eq!(metadata.static_device_bytes(), 10);
989        assert_eq!(metadata.layer_parameter_bytes(), 20);
990        assert_eq!(metadata.maximum_device_layer_bytes(), 8);
991        assert_eq!(metadata.maximum_host_layer_bytes(), 6);
992        assert_eq!(metadata.device_layer_capacity(), 2);
993    }
994}