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/// Generic workload class for independently addressable storage access.
411#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
412#[non_exhaustive]
413pub enum ParameterBankAccess {
414    /// A multi-row access eligible for bounded partitioning.
415    Bulk,
416    /// A latency-sensitive incremental access.
417    Incremental,
418}
419
420impl ExpertPass {
421    /// Projects text execution semantics into a generic storage access class.
422    pub const fn parameter_bank_access(self) -> ParameterBankAccess {
423        match self {
424            Self::Prefill => ParameterBankAccess::Bulk,
425            Self::Decode => ParameterBankAccess::Incremental,
426        }
427    }
428}
429
430/// Backend-neutral controls for independently addressable parameter-bank residency.
431#[derive(Debug, Clone, Copy, Eq, PartialEq)]
432pub struct ParameterBankLoadOptions {
433    /// Independent host/device budgets and eviction policy for bank members.
434    members: OffloadConfig,
435    /// Hard maximum bytes for one materialized temporary compact bank.
436    compact_bank_scratch_bytes: u64,
437    /// Soft compact-bank target used to split multi-token prefill routing.
438    prefill_compact_bank_target_bytes: u64,
439}
440
441impl ParameterBankLoadOptions {
442    /// Creates strict independently addressable bank-caching options.
443    pub fn new(
444        members: OffloadConfig,
445        compact_bank_scratch_bytes: u64,
446        prefill_compact_bank_target_bytes: u64,
447    ) -> Result<Self, WeightResidencyPolicyError> {
448        let options = Self {
449            members,
450            compact_bank_scratch_bytes,
451            prefill_compact_bank_target_bytes,
452        };
453        options.validate()?;
454        Ok(options)
455    }
456
457    /// Revalidates the complete independently addressable residency policy.
458    pub fn validate(self) -> Result<(), WeightResidencyPolicyError> {
459        if self.compact_bank_scratch_bytes == 0 {
460            return Err(WeightResidencyPolicyError::ZeroParameterBankScratchLimit);
461        }
462        if self.prefill_compact_bank_target_bytes == 0 {
463            return Err(WeightResidencyPolicyError::ZeroParameterBankPrefillTarget);
464        }
465        if self.prefill_compact_bank_target_bytes > self.compact_bank_scratch_bytes {
466            return Err(
467                WeightResidencyPolicyError::ParameterBankPrefillTargetExceedsScratch {
468                    target_bytes: self.prefill_compact_bank_target_bytes,
469                    scratch_bytes: self.compact_bank_scratch_bytes,
470                },
471            );
472        }
473        Ok(())
474    }
475
476    /// Returns the independent bank offload limits.
477    pub const fn offload(self) -> OffloadConfig {
478        self.members
479    }
480    /// Returns the hard compact-bank scratch bound.
481    pub const fn compact_bank_scratch_bytes(self) -> u64 {
482        self.compact_bank_scratch_bytes
483    }
484    /// Returns the prefill compact-bank target.
485    pub const fn prefill_compact_bank_target_bytes(self) -> u64 {
486        self.prefill_compact_bank_target_bytes
487    }
488}
489
490impl Default for ParameterBankLoadOptions {
491    fn default() -> Self {
492        Self {
493            members: OffloadConfig::default(),
494            compact_bank_scratch_bytes: u64::MAX,
495            prefill_compact_bank_target_bytes: 1 << 30,
496        }
497    }
498}
499
500/// Placement of ordinary parameters beside independently addressable banks.
501#[derive(Debug, Clone, Copy, Eq, PartialEq)]
502#[non_exhaustive]
503pub enum OrdinaryWeightResidency {
504    /// Keep every ordinary parameter resident on the execution device.
505    FullyResident,
506    /// Eagerly materialize ordinary units on host behind a device window.
507    LayerwiseHost(LayerwiseLoadOptions),
508    /// Leave ordinary units cold on disk behind finite tier caches.
509    DenseDiskStream(DenseDiskStreamLoadOptions),
510}
511
512impl OrdinaryWeightResidency {
513    /// Returns the corresponding generalized layer policy.
514    pub const fn layers(self) -> LayerWeightResidency {
515        match self {
516            Self::FullyResident => LayerWeightResidency::FullyResident,
517            Self::LayerwiseHost(options) => LayerWeightResidency::LayerwiseHost(options),
518            Self::DenseDiskStream(options) => LayerWeightResidency::DenseDiskStream(options),
519        }
520    }
521}
522
523impl From<OrdinaryWeightResidency> for LayerWeightResidency {
524    fn from(value: OrdinaryWeightResidency) -> Self {
525        value.layers()
526    }
527}
528
529/// Independently addressable bank placement relative to ordinary execution units.
530#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
531#[non_exhaustive]
532pub enum ParameterBankResidency {
533    /// Keep bank members in the ordinary unit residency allocation.
534    #[default]
535    WithLayer,
536    /// Catalog bank members as independent atomic residency units.
537    IndependentCache(ParameterBankLoadOptions),
538}
539
540/// Composable ordinary-unit and independently addressable bank placement.
541#[derive(Debug, Clone, Copy, Eq, PartialEq)]
542#[non_exhaustive]
543pub enum WeightResidency {
544    /// All parameters share the ordinary unit residency allocation.
545    Layers(LayerWeightResidency),
546    /// Addressable bank members are independent units beside bounded ordinary units.
547    #[non_exhaustive]
548    IndependentParameterBanks {
549        /// Bounded ordinary-unit policy.
550        ordinary: OrdinaryWeightResidency,
551        /// Bank-member-granular cache controls.
552        cache: ParameterBankLoadOptions,
553    },
554}
555
556impl WeightResidency {
557    /// Keeps every rank-owned parameter on the execution device.
558    pub const fn fully_resident() -> Self {
559        Self::with_layers(LayerWeightResidency::FullyResident)
560    }
561
562    /// Eagerly materializes ordinary units on host behind a bounded device window.
563    pub const fn layerwise_host(options: LayerwiseLoadOptions) -> Self {
564        Self::with_layers(LayerWeightResidency::LayerwiseHost(options))
565    }
566
567    /// Leaves ordinary units cold on disk behind finite host/device caches.
568    pub const fn dense_disk_stream(options: DenseDiskStreamLoadOptions) -> Self {
569        Self::with_layers(LayerWeightResidency::DenseDiskStream(options))
570    }
571
572    /// Keeps every owned parameter in its ordinary unit residency allocation.
573    pub const fn with_layers(layers: LayerWeightResidency) -> Self {
574        Self::Layers(layers)
575    }
576
577    /// Gives addressable parameter banks an independent cache beside ordinary units.
578    pub const fn with_independent_parameter_banks(
579        ordinary: OrdinaryWeightResidency,
580        cache: ParameterBankLoadOptions,
581    ) -> Self {
582        Self::IndependentParameterBanks { ordinary, cache }
583    }
584
585    /// Returns ordinary-unit placement.
586    pub const fn layers(self) -> LayerWeightResidency {
587        match self {
588            Self::Layers(layers) => layers,
589            Self::IndependentParameterBanks { ordinary, .. } => ordinary.layers(),
590        }
591    }
592
593    /// Returns independently addressable bank placement relative to ordinary units.
594    pub const fn parameter_banks(self) -> ParameterBankResidency {
595        match self {
596            Self::Layers(_) => ParameterBankResidency::WithLayer,
597            Self::IndependentParameterBanks { cache, .. } => {
598                ParameterBankResidency::IndependentCache(cache)
599            }
600        }
601    }
602
603    /// Returns independently cached parameter-bank controls, when selected.
604    pub const fn parameter_bank_cache(self) -> Option<ParameterBankLoadOptions> {
605        match self {
606            Self::Layers(_) => None,
607            Self::IndependentParameterBanks { cache, .. } => Some(cache),
608        }
609    }
610
611    /// Returns ordinary placement paired with an independent parameter-bank cache.
612    pub const fn ordinary_residency(self) -> Option<OrdinaryWeightResidency> {
613        match self {
614            Self::Layers(_) => None,
615            Self::IndependentParameterBanks { ordinary, .. } => Some(ordinary),
616        }
617    }
618
619    /// Returns whether every ordinary parameter remains resident.
620    pub const fn ordinary_is_fully_resident(self) -> bool {
621        matches!(
622            self,
623            Self::Layers(LayerWeightResidency::FullyResident)
624                | Self::IndependentParameterBanks {
625                    ordinary: OrdinaryWeightResidency::FullyResident,
626                    ..
627                }
628        )
629    }
630
631    /// Returns whether every ordinary unit remains resident.
632    pub const fn is_fully_resident(self) -> bool {
633        matches!(self, Self::Layers(LayerWeightResidency::FullyResident))
634    }
635
636    /// Returns the common checkpoint shard/reader cache bound.
637    pub const fn max_cached_shards(self) -> usize {
638        self.layers().max_cached_shards()
639    }
640}
641
642impl Default for WeightResidency {
643    fn default() -> Self {
644        Self::fully_resident()
645    }
646}
647
648impl LayerWeightResidency {
649    /// Returns the checkpoint shard/reader cache bound carried by this policy.
650    pub const fn max_cached_shards(self) -> usize {
651        match self {
652            Self::FullyResident => DEFAULT_MAX_CACHED_SHARDS,
653            Self::LayerwiseHost(options) => options.max_cached_shards,
654            Self::DenseDiskStream(options) => options.max_cached_shards,
655        }
656    }
657
658    /// Returns whether backend allocator memory should be sampled.
659    pub const fn sample_backend_memory(self) -> bool {
660        match self {
661            Self::FullyResident => false,
662            Self::LayerwiseHost(options) => options.sample_backend_memory,
663            Self::DenseDiskStream(options) => options.sample_backend_memory,
664        }
665    }
666
667    /// Returns whether process memory should be sampled.
668    pub const fn sample_process_memory(self) -> bool {
669        match self {
670            Self::FullyResident => false,
671            Self::LayerwiseHost(options) => options.sample_process_memory,
672            Self::DenseDiskStream(options) => options.sample_process_memory,
673        }
674    }
675
676    /// Returns the maximum execution-device unit window.
677    pub fn device_depth(self, unit_count: usize) -> usize {
678        match self {
679            Self::FullyResident => unit_count,
680            Self::LayerwiseHost(options) => options.offload.prefetch_depth(),
681            Self::DenseDiskStream(_) => unit_count.min(DENSE_TRANSFER_WINDOW),
682        }
683    }
684
685    /// Resolves this policy into the shared offload configuration.
686    pub fn offload(self) -> Result<OffloadConfig, WeightResidencyPolicyError> {
687        match self {
688            Self::FullyResident => Ok(OffloadConfig::default()),
689            Self::LayerwiseHost(options) => Ok(options.offload),
690            Self::DenseDiskStream(options) => {
691                options.validate()?;
692                Ok(OffloadConfig::new(
693                    Some(options.device_budget_bytes),
694                    Some(options.host_budget_bytes),
695                    options.host_lookahead.max(DENSE_TRANSFER_WINDOW),
696                )?
697                .with_eviction_policy(options.eviction_policy))
698            }
699        }
700    }
701
702    /// Returns dense-stream controls when this policy streams from disk.
703    pub const fn dense(self) -> Option<DenseDiskStreamLoadOptions> {
704        match self {
705            Self::DenseDiskStream(options) => Some(options),
706            Self::FullyResident | Self::LayerwiseHost(_) => None,
707        }
708    }
709
710    /// Returns whether every ordinary execution unit remains device-resident.
711    pub const fn is_fully_resident(self) -> bool {
712        matches!(self, Self::FullyResident)
713    }
714
715    /// Returns the stable physical execution classification.
716    pub const fn execution_residency(self) -> ExecutionResidency {
717        match self {
718            Self::FullyResident => ExecutionResidency::FullyResident,
719            Self::LayerwiseHost(_) => ExecutionResidency::LayerwiseHost,
720            Self::DenseDiskStream(_) => ExecutionResidency::DenseDiskStream,
721        }
722    }
723}
724
725impl From<LayerwiseLoadOptions> for LayerWeightResidency {
726    fn from(value: LayerwiseLoadOptions) -> Self {
727        Self::LayerwiseHost(value)
728    }
729}
730
731impl From<DenseDiskStreamLoadOptions> for LayerWeightResidency {
732    fn from(value: DenseDiskStreamLoadOptions) -> Self {
733        Self::DenseDiskStream(value)
734    }
735}
736
737/// Static-parameter placement used by the generalized execution engine.
738#[derive(Debug, Clone, Copy, Eq, PartialEq)]
739#[non_exhaustive]
740pub enum ExecutionResidency {
741    /// Every module is constructed once and all rank-local parameters remain on device.
742    FullyResident,
743    /// Parameters remain on host behind bounded device windows.
744    LayerwiseHost,
745    /// Parameters are materialized through bounded disk, host, and device caches.
746    DenseDiskStream,
747}
748
749/// Inspectable backend-neutral parameter-residency metadata for a layered model.
750#[derive(Debug, Clone, Eq, PartialEq)]
751pub struct LayerwiseModelMetadata {
752    effective_model_type: String,
753    quantization: Option<eredu_checkpoint::WeightQuantization>,
754    layer_count: usize,
755    static_device_bytes: u64,
756    residency: ExecutionResidency,
757    layer_parameter_bytes: u64,
758    maximum_device_layer_bytes: u64,
759    maximum_host_layer_bytes: u64,
760    device_layer_capacity: usize,
761    materialization: Option<crate::WeightMaterializationReport>,
762}
763
764impl LayerwiseModelMetadata {
765    /// Creates one complete metadata snapshot before optional materialization telemetry.
766    #[allow(clippy::too_many_arguments)]
767    pub fn new(
768        effective_model_type: impl Into<String>,
769        quantization: Option<eredu_checkpoint::WeightQuantization>,
770        layer_count: usize,
771        static_device_bytes: u64,
772        residency: ExecutionResidency,
773        layer_parameter_bytes: u64,
774        maximum_device_layer_bytes: u64,
775        maximum_host_layer_bytes: u64,
776        device_layer_capacity: usize,
777    ) -> Self {
778        Self {
779            effective_model_type: effective_model_type.into(),
780            quantization,
781            layer_count,
782            static_device_bytes,
783            residency,
784            layer_parameter_bytes,
785            maximum_device_layer_bytes,
786            maximum_host_layer_bytes,
787            device_layer_capacity,
788            materialization: None,
789        }
790    }
791
792    /// Replaces the parsed implementation identity after checkpoint resolution.
793    pub fn set_effective_model_type(&mut self, effective_model_type: impl Into<String>) {
794        self.effective_model_type = effective_model_type.into();
795    }
796
797    /// Replaces checkpoint-native packed quantization metadata.
798    pub fn set_quantization(&mut self, quantization: Option<eredu_checkpoint::WeightQuantization>) {
799        self.quantization = quantization;
800    }
801
802    /// Replaces bounded load-time materialization telemetry.
803    pub fn set_materialization(
804        &mut self,
805        materialization: Option<crate::WeightMaterializationReport>,
806    ) {
807        self.materialization = materialization;
808    }
809
810    /// Returns the parsed implementation or nested text-model type.
811    pub fn effective_model_type(&self) -> &str {
812        &self.effective_model_type
813    }
814
815    /// Returns checkpoint-native packed quantization metadata, if present.
816    pub const fn quantization(&self) -> Option<eredu_checkpoint::WeightQuantization> {
817        self.quantization
818    }
819
820    /// Returns the ordered execution-unit count.
821    pub const fn layer_count(&self) -> usize {
822        self.layer_count
823    }
824
825    /// Returns pinned static parameter bytes on the execution device.
826    pub const fn static_device_bytes(&self) -> u64 {
827        self.static_device_bytes
828    }
829
830    /// Returns the selected generalized parameter-residency policy.
831    pub const fn residency(&self) -> ExecutionResidency {
832        self.residency
833    }
834
835    /// Returns complete rank-local execution-unit parameter bytes.
836    pub const fn layer_parameter_bytes(&self) -> u64 {
837        self.layer_parameter_bytes
838    }
839
840    /// Returns the largest possible device-resident execution-unit byte total.
841    pub const fn maximum_device_layer_bytes(&self) -> u64 {
842        self.maximum_device_layer_bytes
843    }
844
845    /// Returns the charged host-transfer capacity of the largest execution unit.
846    pub const fn maximum_host_layer_bytes(&self) -> u64 {
847        self.maximum_host_layer_bytes
848    }
849
850    /// Returns the maximum number of execution units retained on device.
851    pub const fn device_layer_capacity(&self) -> usize {
852        self.device_layer_capacity
853    }
854
855    /// Returns bounded load-time materialization telemetry.
856    pub const fn materialization(&self) -> Option<&crate::WeightMaterializationReport> {
857        self.materialization.as_ref()
858    }
859}
860
861/// Invalid backend-neutral immutable-weight residency policy.
862#[derive(Debug, thiserror::Error)]
863#[non_exhaustive]
864pub enum WeightResidencyPolicyError {
865    /// Enabled host caching needs a protected current unit.
866    #[error("dense disk streaming host lookahead must be nonzero when the host budget is enabled")]
867    ZeroHostLookahead,
868    /// Enabled background work requires bounded capacity.
869    #[error("dense disk streaming background queue capacity must be nonzero when host caching is enabled")]
870    ZeroQueueCapacity,
871    /// Direct-to-device mode cannot configure host-only controls.
872    #[error("dense disk streaming with a zero host budget requires zero host lookahead and queue capacity")]
873    HostDisabledControls,
874    /// Parameter-bank scratch accounting was disabled with a zero limit.
875    #[error("parameter-bank compact scratch limit must be nonzero")]
876    ZeroParameterBankScratchLimit,
877    /// Parameter-bank prefill chunking was disabled with a zero target.
878    #[error("parameter-bank prefill target must be nonzero")]
879    ZeroParameterBankPrefillTarget,
880    /// The parameter-bank prefill target exceeded the hard scratch bound.
881    #[error("parameter-bank prefill target {target_bytes} exceeds scratch limit {scratch_bytes}")]
882    ParameterBankPrefillTargetExceedsScratch {
883        /// Requested soft prefill target.
884        target_bytes: u64,
885        /// Configured hard scratch limit.
886        scratch_bytes: u64,
887    },
888    /// The derived offload configuration was invalid.
889    #[error(transparent)]
890    Offload(#[from] OffloadError),
891}
892
893#[cfg(test)]
894mod tests {
895    use super::*;
896
897    #[test]
898    fn static_unit_bindings_are_runtime_owned_and_decomposable() {
899        let binding = WeightBinding::new(
900            "embedding",
901            "model.embedding.weight",
902            eredu_checkpoint::store::TensorSelection::Full,
903            16,
904        )
905        .unwrap();
906        let unit = StaticUnitBindings::new("static.embedding", vec![binding.clone()]).unwrap();
907
908        assert_eq!(unit.id().as_str(), "static.embedding");
909        assert_eq!(unit.bindings(), std::slice::from_ref(&binding));
910        let (id, bindings) = unit.into_parts();
911        assert_eq!(id.as_str(), "static.embedding");
912        assert_eq!(bindings, vec![binding]);
913    }
914
915    #[test]
916    fn dense_stream_controls_fail_closed_without_a_backend() {
917        assert!(matches!(
918            DenseDiskStreamLoadOptions::new(1, 1, 0, 1),
919            Err(WeightResidencyPolicyError::ZeroHostLookahead)
920        ));
921        assert!(matches!(
922            DenseDiskStreamLoadOptions::new(1, 1, 1, 0),
923            Err(WeightResidencyPolicyError::ZeroQueueCapacity)
924        ));
925        assert!(matches!(
926            DenseDiskStreamLoadOptions::new(1, 0, 1, 0),
927            Err(WeightResidencyPolicyError::HostDisabledControls)
928        ));
929        assert!(DenseDiskStreamLoadOptions::new(1, 0, 0, 0).is_ok());
930    }
931
932    #[test]
933    fn residency_policy_derives_finite_dense_windows() {
934        let options = DenseDiskStreamLoadOptions::new(32, 64, 3, 2).unwrap();
935        let policy = LayerWeightResidency::DenseDiskStream(options);
936        assert_eq!(policy.device_depth(8), DENSE_TRANSFER_WINDOW);
937        let offload = policy.offload().unwrap();
938        assert_eq!(offload.device_budget_bytes(), Some(32));
939        assert_eq!(offload.host_budget_bytes(), Some(64));
940        assert_eq!(offload.prefetch_depth(), 3);
941    }
942
943    #[test]
944    fn dense_transfer_schedule_preserves_order_and_bounded_lookahead() {
945        let mut schedule = DenseTransferSchedule::new(3..7, 2).unwrap();
946        assert_eq!(schedule.desired_indices(3), vec![3, 4, 5]);
947        assert_eq!(
948            schedule.admit(4, "wrong"),
949            Err(DenseTransferScheduleError::OutOfOrder {
950                expected: 3,
951                actual: 4,
952            })
953        );
954        schedule.admit(3, "three").unwrap();
955        schedule.admit(4, "four").unwrap();
956        assert!(!schedule.can_admit());
957        assert_eq!(schedule.desired_indices(3), vec![3, 4, 5]);
958        assert_eq!(schedule.pop_ready(), Some((3, "three")));
959        schedule.admit(5, "five").unwrap();
960        assert_eq!(schedule.desired_indices(4), vec![4, 5, 6]);
961        assert_eq!(schedule.pop_ready(), Some((4, "four")));
962        assert_eq!(schedule.pop_ready(), Some((5, "five")));
963        schedule.admit(6, "six").unwrap();
964        assert_eq!(schedule.pop_ready(), Some((6, "six")));
965        assert!(schedule.is_exhausted());
966    }
967
968    #[test]
969    fn expert_cache_controls_and_composite_placement_are_backend_neutral() {
970        let experts = ParameterBankLoadOptions::new(OffloadConfig::default(), 64, 32).unwrap();
971        let placement = WeightResidency::with_independent_parameter_banks(
972            OrdinaryWeightResidency::FullyResident,
973            experts,
974        );
975        assert_eq!(placement.parameter_bank_cache(), Some(experts));
976        assert!(placement.ordinary_is_fully_resident());
977        assert!(!placement.is_fully_resident());
978        assert!(matches!(
979            ParameterBankLoadOptions::new(OffloadConfig::default(), 64, 0),
980            Err(WeightResidencyPolicyError::ZeroParameterBankPrefillTarget)
981        ));
982        assert_eq!(
983            ParameterBankKey::new(3, 7).unit_id().as_str(),
984            "bank.unit.00003.member.00007"
985        );
986    }
987
988    #[test]
989    fn layerwise_metadata_is_runtime_owned_and_updateable() {
990        let mut metadata = LayerwiseModelMetadata::new(
991            "generic",
992            None,
993            4,
994            10,
995            ExecutionResidency::LayerwiseHost,
996            20,
997            8,
998            6,
999            2,
1000        );
1001        metadata.set_effective_model_type("llama");
1002        metadata.set_quantization(Some(eredu_checkpoint::WeightQuantization::Affine(
1003            eredu_checkpoint::AffineQuantization::default(),
1004        )));
1005
1006        assert_eq!(metadata.effective_model_type(), "llama");
1007        assert_eq!(metadata.layer_count(), 4);
1008        assert_eq!(metadata.static_device_bytes(), 10);
1009        assert_eq!(metadata.layer_parameter_bytes(), 20);
1010        assert_eq!(metadata.maximum_device_layer_bytes(), 8);
1011        assert_eq!(metadata.maximum_host_layer_bytes(), 6);
1012        assert_eq!(metadata.device_layer_capacity(), 2);
1013    }
1014}