Skip to main content

onnx_runtime_loader/
weights.rs

1//! Initializer weight resolution: inline data and external `mmap` (§19.2, §12).
2//!
3//! Turns each `TensorProto` initializer into an [`onnx_runtime_ir::WeightRef`]
4//! descriptor that the IR stores. Inline tensors keep their bytes; external
5//! tensors are described by `(path, offset, length)` and the referenced files
6//! are memory-mapped so downstream consumers get zero-copy access via
7//! [`WeightStore::bytes`].
8
9use std::collections::HashMap;
10use std::fs::File;
11use std::ops::Range;
12use std::path::{Path, PathBuf};
13use std::sync::atomic::{AtomicUsize, Ordering};
14
15use memmap2::Mmap;
16use onnx_runtime_ir::{DataType, TensorData, ValueId, WeightRef};
17
18use crate::proto::onnx::{ModelProto, TensorProto, tensor_proto};
19use crate::{LoaderError, pathsafe::guarded_join};
20
21/// A resolved set of initializer weights, keyed by the value they populate,
22/// plus the live memory maps backing any external data files.
23#[derive(Debug, Default)]
24pub struct WeightStore {
25    /// IR weight descriptors, keyed by the graph value they initialize.
26    pub weights: HashMap<ValueId, WeightRef>,
27    /// Live memory maps for external-data files, keyed by absolute path.
28    mmaps: HashMap<PathBuf, MappedFile>,
29}
30
31#[derive(Debug)]
32struct MappedFile {
33    id: usize,
34    mmap: Mmap,
35}
36
37static NEXT_MAPPING_ID: AtomicUsize = AtomicUsize::new(1);
38
39impl WeightStore {
40    /// Total bytes of external-data file currently mapped by this store.
41    ///
42    /// This is *file* size, which is only an upper bound on the weights the
43    /// graph references — see [`ReferencedWeightBytes`]. Compare the two with
44    /// [`Self::unreferenced_external_bytes`] rather than treating either alone
45    /// as "how big is this model".
46    #[must_use]
47    pub fn mapped_external_bytes(&self) -> u64 {
48        self.mmaps
49            .values()
50            .map(|mapped| mapped.mmap.len() as u64)
51            .fold(0u64, u64::saturating_add)
52    }
53
54    /// Mapped external bytes that no initializer points at.
55    ///
56    /// Nonzero means the blob carries dead weight — most commonly an orphaned
57    /// prefix left by a re-export that appended fresh tensors without
58    /// truncating the original. It is worth surfacing because **nothing fails**
59    /// when it happens: the model loads and produces byte-identical output, so
60    /// the only symptom is a file twice the size it should be. `qwen14b-zp`
61    /// carried 8.32 GB of it (50.02% of the blob), which cost double the disk,
62    /// double the page-locked host RAM on the memory-mapped weight path, and —
63    /// before #856 — a weight budget derived from file length that was wrong by
64    /// exactly 2.00x (#853).
65    ///
66    /// Saturates at zero: a store may legitimately map a file whose declared
67    /// lengths exceed what is mapped (a truncated or shared blob), and that is
68    /// a different failure with its own error path.
69    #[must_use]
70    pub fn unreferenced_external_bytes(&self, referenced: &ReferencedWeightBytes) -> u64 {
71        self.mapped_external_bytes()
72            .saturating_sub(referenced.external)
73    }
74}
75
76/// Quantization geometry needed to interpret one expert's packed tensor slice.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub struct ExpertQuantization {
79    /// Number of quantized weight bits per logical value.
80    pub bits: usize,
81    /// Number of logical input values sharing one scale/zero-point block.
82    pub block_size: usize,
83    /// Number of quantization blocks represented by each tensor row.
84    pub blocks_per_row: usize,
85}
86
87/// Physical storage order declared by the model/package layout descriptor.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum ExpertStorageOrder {
90    /// All rows for expert 0, then all rows for expert 1, and so on.
91    ExpertMajor,
92    /// Expert rows are interleaved and therefore cannot be represented by one range.
93    Interleaved,
94}
95
96/// Compact layout descriptor used to derive expert byte ranges.
97#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct ExpertTensorLayout {
99    /// Mandatory layout contract version. Phase 1 supports version 1.
100    pub version: u32,
101    pub experts: usize,
102    pub rows_per_expert: usize,
103    /// Stored elements per row (packed bytes for a `Uint8` weight tensor).
104    pub storage_elements_per_row: usize,
105    pub order: ExpertStorageOrder,
106    pub quantization: Option<ExpertQuantization>,
107}
108
109/// Why an external tensor cannot use the expert paging path.
110#[derive(Clone, Debug, PartialEq, Eq)]
111pub enum NonPageableReason {
112    InlineTensor,
113    UnsupportedLayoutVersion(u32),
114    NotExpertMajor,
115    ShapeMismatch {
116        expected: Vec<usize>,
117        actual: Vec<usize>,
118    },
119    InvalidQuantization(String),
120    Range(String),
121    ExternalLengthMismatch {
122        expected: usize,
123        actual: usize,
124    },
125}
126
127/// Pageability classification. Non-pageable tensors remain valid and use the
128/// existing resident/materializing execution path.
129#[derive(Clone, Debug, PartialEq, Eq)]
130pub enum Pageability {
131    Pageable,
132    NonPageable(NonPageableReason),
133}
134
135/// One expert's contiguous byte window in an external initializer mapping.
136#[derive(Clone, Debug, PartialEq, Eq)]
137pub struct ExpertWeightRegion {
138    pub expert: usize,
139    /// Absolute byte offset in the external-data file.
140    pub offset: usize,
141    pub len: usize,
142}
143
144/// Validated expert-region catalog over a [`WeightRef`] in a [`WeightStore`].
145///
146/// A pageable catalog guarantees that each expert is represented by one
147/// overflow-checked external byte range. The quantization metadata is retained
148/// so a kernel can decode one range without inspecting or materializing peers.
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct WeightRegionCatalog {
151    path: Option<PathBuf>,
152    tensor_offset: usize,
153    tensor_len: usize,
154    dtype: DataType,
155    layout: ExpertTensorLayout,
156    regions: Vec<ExpertWeightRegion>,
157    pageability: Pageability,
158}
159
160impl WeightRegionCatalog {
161    /// Classify a weight using an explicit model/package layout descriptor.
162    ///
163    /// Invalid or non-expert-major layouts produce a non-pageable catalog rather
164    /// than failing model load; callers must fall back to the resident path.
165    pub fn classify(weight: &WeightRef, layout: ExpertTensorLayout) -> Self {
166        let dtype = weight.dtype();
167        let dims = weight.dims();
168        let (path, tensor_offset, tensor_len) = match weight {
169            WeightRef::Inline(tensor) => {
170                return Self::non_pageable(
171                    None,
172                    0,
173                    tensor.data.len(),
174                    dtype,
175                    layout,
176                    NonPageableReason::InlineTensor,
177                );
178            }
179
180            WeightRef::External {
181                path,
182                offset,
183                length,
184                ..
185            } => (Some(path.clone()), *offset, *length),
186        };
187
188        if layout.version != 1 {
189            return Self::non_pageable(
190                path,
191                tensor_offset,
192                tensor_len,
193                dtype,
194                layout.clone(),
195                NonPageableReason::UnsupportedLayoutVersion(layout.version),
196            );
197        }
198        if layout.order != ExpertStorageOrder::ExpertMajor {
199            return Self::non_pageable(
200                path,
201                tensor_offset,
202                tensor_len,
203                dtype,
204                layout,
205                NonPageableReason::NotExpertMajor,
206            );
207        }
208        let expected_shape = vec![
209            layout.experts,
210            layout.rows_per_expert,
211            layout.storage_elements_per_row,
212        ];
213        if dims != expected_shape {
214            return Self::non_pageable(
215                path,
216                tensor_offset,
217                tensor_len,
218                dtype,
219                layout,
220                NonPageableReason::ShapeMismatch {
221                    expected: expected_shape,
222                    actual: dims.to_vec(),
223                },
224            );
225        }
226        if let Some(quantization) = layout.quantization
227            && (!matches!(quantization.bits, 1 | 2 | 4 | 8)
228                || quantization.block_size == 0
229                || quantization.blocks_per_row == 0)
230        {
231            return Self::non_pageable(
232                path,
233                tensor_offset,
234                tensor_len,
235                dtype,
236                layout,
237                NonPageableReason::InvalidQuantization(format!(
238                    "bits={}, block_size={}, blocks_per_row={}",
239                    quantization.bits, quantization.block_size, quantization.blocks_per_row
240                )),
241            );
242        }
243
244        let elements_per_expert = match checked_product(
245            &[layout.rows_per_expert, layout.storage_elements_per_row],
246            "per-expert element count",
247        ) {
248            Ok(value) => value,
249            Err(error) => {
250                return Self::non_pageable(
251                    path,
252                    tensor_offset,
253                    tensor_len,
254                    dtype,
255                    layout,
256                    NonPageableReason::Range(error.to_string()),
257                );
258            }
259        };
260        let bytes_per_expert =
261            match checked_storage_byte_count(dtype, elements_per_expert, "per-expert byte count") {
262                Ok(value) => value,
263                Err(error) => {
264                    return Self::non_pageable(
265                        path,
266                        tensor_offset,
267                        tensor_len,
268                        dtype,
269                        layout,
270                        NonPageableReason::Range(error.to_string()),
271                    );
272                }
273            };
274        let expected_len = match checked_byte_count(
275            layout.experts,
276            bytes_per_expert,
277            "expert tensor byte count",
278        ) {
279            Ok(value) => value,
280            Err(error) => {
281                return Self::non_pageable(
282                    path,
283                    tensor_offset,
284                    tensor_len,
285                    dtype,
286                    layout,
287                    NonPageableReason::Range(error.to_string()),
288                );
289            }
290        };
291        if expected_len != tensor_len {
292            return Self::non_pageable(
293                path,
294                tensor_offset,
295                tensor_len,
296                dtype,
297                layout,
298                NonPageableReason::ExternalLengthMismatch {
299                    expected: expected_len,
300                    actual: tensor_len,
301                },
302            );
303        }
304        let tensor_end = match tensor_offset.checked_add(tensor_len) {
305            Some(end) if end <= isize::MAX as usize => end,
306            Some(_) => {
307                return Self::non_pageable(
308                    path,
309                    tensor_offset,
310                    tensor_len,
311                    dtype,
312                    layout,
313                    NonPageableReason::Range(
314                        "expert tensor absolute endpoint exceeds isize::MAX".into(),
315                    ),
316                );
317            }
318            None => {
319                return Self::non_pageable(
320                    path,
321                    tensor_offset,
322                    tensor_len,
323                    dtype,
324                    layout,
325                    NonPageableReason::Range("expert tensor absolute endpoint overflow".into()),
326                );
327            }
328        };
329
330        let mut regions = Vec::new();
331        if let Err(error) = regions.try_reserve_exact(layout.experts) {
332            return Self::non_pageable(
333                path,
334                tensor_offset,
335                tensor_len,
336                dtype,
337                layout,
338                NonPageableReason::Range(format!("expert region allocation failed: {error}")),
339            );
340        }
341        for expert in 0..layout.experts {
342            let relative = match checked_range(expert, bytes_per_expert, "expert byte range") {
343                Ok(value) => value,
344                Err(error) => {
345                    return Self::non_pageable(
346                        path,
347                        tensor_offset,
348                        tensor_len,
349                        dtype,
350                        layout,
351                        NonPageableReason::Range(error.to_string()),
352                    );
353                }
354            };
355            let offset = match tensor_offset.checked_add(relative.start) {
356                Some(value) => value,
357                None => {
358                    return Self::non_pageable(
359                        path,
360                        tensor_offset,
361                        tensor_len,
362                        dtype,
363                        layout,
364                        NonPageableReason::Range("expert absolute offset overflow".into()),
365                    );
366                }
367            };
368            let _end = match offset.checked_add(bytes_per_expert) {
369                Some(end) if end <= isize::MAX as usize && end <= tensor_end => end,
370                Some(_) => {
371                    return Self::non_pageable(
372                        path,
373                        tensor_offset,
374                        tensor_len,
375                        dtype,
376                        layout,
377                        NonPageableReason::Range(
378                            "expert absolute endpoint exceeds validated tensor range".into(),
379                        ),
380                    );
381                }
382                None => {
383                    return Self::non_pageable(
384                        path,
385                        tensor_offset,
386                        tensor_len,
387                        dtype,
388                        layout,
389                        NonPageableReason::Range("expert absolute endpoint overflow".into()),
390                    );
391                }
392            };
393            regions.push(ExpertWeightRegion {
394                expert,
395                offset,
396                len: bytes_per_expert,
397            });
398        }
399
400        Self {
401            path,
402            tensor_offset,
403            tensor_len,
404            dtype,
405            layout,
406            regions,
407            pageability: Pageability::Pageable,
408        }
409    }
410
411    /// Build relative expert ranges for a tensor view that the caller has
412    /// already established aliases an external mmap initializer.
413    ///
414    /// This is the kernel-boundary counterpart to [`classify`](Self::classify):
415    /// it cannot re-borrow through [`WeightStore`], but applies the same shape,
416    /// layout, quantization, overflow, and `isize::MAX` validation.
417    pub fn for_mapped_tensor_view(
418        dtype: DataType,
419        dims: &[usize],
420        tensor_len: usize,
421        layout: ExpertTensorLayout,
422    ) -> Self {
423        let synthetic = WeightRef::External {
424            path: PathBuf::new(),
425            offset: 0,
426            length: tensor_len,
427            dtype,
428            dims: dims.to_vec(),
429        };
430        let mut catalog = Self::classify(&synthetic, layout);
431        catalog.path = None;
432        catalog
433    }
434
435    fn non_pageable(
436        path: Option<PathBuf>,
437        tensor_offset: usize,
438        tensor_len: usize,
439        dtype: DataType,
440        layout: ExpertTensorLayout,
441        reason: NonPageableReason,
442    ) -> Self {
443        Self {
444            path,
445            tensor_offset,
446            tensor_len,
447            dtype,
448            layout,
449            regions: Vec::new(),
450            pageability: Pageability::NonPageable(reason),
451        }
452    }
453
454    pub fn pageability(&self) -> &Pageability {
455        &self.pageability
456    }
457
458    pub fn is_pageable(&self) -> bool {
459        matches!(self.pageability, Pageability::Pageable)
460    }
461
462    pub fn layout(&self) -> &ExpertTensorLayout {
463        &self.layout
464    }
465
466    pub fn dtype(&self) -> DataType {
467        self.dtype
468    }
469
470    /// External-data path that supplied this catalog, when it is externally
471    /// backed.
472    pub fn path(&self) -> Option<&Path> {
473        self.path.as_deref()
474    }
475
476    /// Absolute byte offset of the tensor in its external-data mapping.
477    pub fn tensor_offset(&self) -> usize {
478        self.tensor_offset
479    }
480
481    /// Canonical tensor byte length validated by this catalog.
482    pub fn tensor_len(&self) -> usize {
483        self.tensor_len
484    }
485
486    pub fn mapped_bytes(&self) -> usize {
487        if self.is_pageable() {
488            self.tensor_len
489        } else {
490            0
491        }
492    }
493
494    pub fn region(&self, expert: usize) -> Option<&ExpertWeightRegion> {
495        self.regions.get(expert)
496    }
497
498    pub fn relative_range(&self, expert: usize) -> Option<Range<usize>> {
499        let region = self.region(expert)?;
500        let start = region.offset.checked_sub(self.tensor_offset)?;
501        let end = start.checked_add(region.len)?;
502        Some(start..end)
503    }
504
505    /// Borrow one expert directly from the live read-only mmap.
506    pub fn expert_bytes<'a>(&self, store: &'a WeightStore, expert: usize) -> Option<&'a [u8]> {
507        let path = self.path.as_ref()?;
508        let region = self.region(expert)?;
509        store.external_bytes(path, region.offset, region.len)
510    }
511}
512
513/// Derive the expert-major [`ExpertTensorLayout`] for one `com.microsoft::QMoE`
514/// input tensor (a packed weight, scale, or zero-point tensor), given the
515/// node-level quantization attributes and this tensor's own dims.
516///
517/// This is the single source of truth for "which axis is which" on a QMoE
518/// expert-bank tensor, shared by advisory host/device placement
519/// (`onnx-genai-engine::engine::placement`) and the lazy-weight paging seam
520/// (`onnx-runtime-session::executor::build`) so the two call sites cannot
521/// silently drift onto different byte-range formulas. Returns `None` when
522/// `dims` is not rank-3 (i.e. not an expert-major tensor at all); callers
523/// treat that as "not applicable", not as a paging failure.
524pub fn qmoe_expert_tensor_layout(
525    bits: usize,
526    block_size: usize,
527    blocks_per_row: usize,
528    dims: &[usize],
529) -> Option<ExpertTensorLayout> {
530    if dims.len() != 3 {
531        return None;
532    }
533    Some(ExpertTensorLayout {
534        version: 1,
535        experts: dims[0],
536        rows_per_expert: dims[1],
537        storage_elements_per_row: dims[2],
538        order: ExpertStorageOrder::ExpertMajor,
539        quantization: Some(ExpertQuantization {
540            bits,
541            block_size,
542            blocks_per_row,
543        }),
544    })
545}
546
547/// Overflow error shared by loader range catalogs and paging-aware kernels.
548#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
549#[error("{0}")]
550pub struct WeightRangeError(String);
551
552/// Checked product that still detects overflow when another factor is zero.
553pub fn checked_product(factors: &[usize], context: &str) -> Result<usize, WeightRangeError> {
554    let mut product = 1usize;
555    let mut has_zero = false;
556    for &factor in factors {
557        if factor == 0 {
558            has_zero = true;
559        } else {
560            product = product
561                .checked_mul(factor)
562                .ok_or_else(|| WeightRangeError(format!("{context} overflow")))?;
563        }
564    }
565    Ok(if has_zero { 0 } else { product })
566}
567
568/// Checked element-size multiplication, rejecting slices larger than `isize`.
569pub fn checked_byte_count(
570    elements: usize,
571    element_size: usize,
572    context: &str,
573) -> Result<usize, WeightRangeError> {
574    let bytes = elements
575        .checked_mul(element_size)
576        .ok_or_else(|| WeightRangeError(format!("{context} overflow")))?;
577    if bytes > isize::MAX as usize {
578        return Err(WeightRangeError(format!("{context} exceeds isize::MAX")));
579    }
580    Ok(bytes)
581}
582
583/// Checked storage byte count, including sub-byte ONNX dtypes.
584pub fn checked_storage_byte_count(
585    dtype: DataType,
586    elements: usize,
587    context: &str,
588) -> Result<usize, WeightRangeError> {
589    let bytes = dtype
590        .checked_storage_bytes(elements)
591        .ok_or_else(|| WeightRangeError(format!("{context} overflow")))?;
592    if bytes > isize::MAX as usize {
593        return Err(WeightRangeError(format!("{context} exceeds isize::MAX")));
594    }
595    Ok(bytes)
596}
597
598/// Checked fixed-width range `index * width .. start + width`.
599pub fn checked_range(
600    index: usize,
601    width: usize,
602    context: &str,
603) -> Result<Range<usize>, WeightRangeError> {
604    let start = index
605        .checked_mul(width)
606        .ok_or_else(|| WeightRangeError(format!("{context} start offset overflow")))?;
607    let end = start
608        .checked_add(width)
609        .ok_or_else(|| WeightRangeError(format!("{context} end offset overflow")))?;
610    if end > isize::MAX as usize {
611        return Err(WeightRangeError(format!("{context} exceeds isize::MAX")));
612    }
613    Ok(start..end)
614}
615
616impl WeightStore {
617    /// An empty store.
618    pub fn new() -> Self {
619        Self::default()
620    }
621
622    /// Memory-map an external-data file and register it under `path`, so any
623    /// [`WeightRef::External`] whose `path` matches resolves zero-copy via
624    /// [`bytes`](Self::bytes). Idempotent: mapping the same path twice is a
625    /// no-op. This is the programmatic counterpart to the loader path (which
626    /// maps files while resolving `TensorProto` initializers), useful when
627    /// constructing a store by hand.
628    ///
629    /// The map is read-only and kept alive for the store's lifetime; callers
630    /// must not mutate or unlink the file while the store is live.
631    pub fn map_external(&mut self, path: impl AsRef<Path>) -> Result<(), LoaderError> {
632        self.mmap_file(path.as_ref())
633    }
634
635    /// Resolve a weight descriptor to its raw little-endian bytes.
636    ///
637    /// For inline weights this borrows the stored bytes; for external weights
638    /// it slices into the memory-mapped file. Returns `None` if an external
639    /// mapping is missing or the `[offset, offset+length)` window is out of
640    /// bounds.
641    pub fn bytes<'a>(&'a self, weight: &'a WeightRef) -> Option<&'a [u8]> {
642        match weight {
643            WeightRef::Inline(t) => Some(&t.data),
644            WeightRef::External {
645                path,
646                offset,
647                length,
648                ..
649            } => {
650                let mmap = self.mmaps.get(path)?;
651                mmap.mmap.get(*offset..offset.checked_add(*length)?)
652            }
653        }
654    }
655
656    /// Return stable mmap identity and the validated absolute tensor range.
657    pub fn external_mmap_provenance(&self, weight: &WeightRef) -> Option<(usize, usize, usize)> {
658        let WeightRef::External {
659            path,
660            offset,
661            length,
662            ..
663        } = weight
664        else {
665            return None;
666        };
667        let mmap = self.mmaps.get(path)?;
668        let end = offset.checked_add(*length)?;
669        if end > mmap.mmap.len() || end > isize::MAX as usize {
670            return None;
671        }
672        Some((mmap.id, *offset, *length))
673    }
674
675    /// Resolve a validated external-data region by stable mmap id.
676    ///
677    /// Lazy device weight paging carries this id instead of a path so the hot
678    /// page-in path can copy directly from the live mmap. That prevents the
679    /// WDDM offload failure mode where every page-in first rebuilt an owned host
680    /// tensor and spent most of decode time in redundant CPU materialization.
681    pub fn mmap_region_bytes(&self, mapping_id: usize, offset: usize, len: usize) -> Option<&[u8]> {
682        let mmap = self.mmaps.values().find(|mapped| mapped.id == mapping_id)?;
683        mmap.mmap.get(offset..offset.checked_add(len)?)
684    }
685
686    /// Return the whole live mmap backing `mapping_id`.
687    ///
688    /// The zero-copy hybrid (#864) registers an entire mapping once with
689    /// `cuMemHostRegister(READ_ONLY | DEVICEMAP)` so that every weight's device
690    /// pointer (from `cuMemHostGetDevicePointer`) is contiguous for its full
691    /// length — a per-weight registration would only be contiguous up to the
692    /// registration boundary, so a weight spanning two registrations would read
693    /// past valid device addresses.
694    pub fn mmap_full_bytes(&self, mapping_id: usize) -> Option<&[u8]> {
695        let mmap = self.mmaps.values().find(|mapped| mapped.id == mapping_id)?;
696        Some(&mmap.mmap[..])
697    }
698
699    fn external_bytes(&self, path: &Path, offset: usize, length: usize) -> Option<&[u8]> {
700        let mmap = self.mmaps.get(path)?;
701        mmap.mmap.get(offset..offset.checked_add(length)?)
702    }
703
704    fn mmap_file(&mut self, path: &Path) -> Result<(), LoaderError> {
705        if self.mmaps.contains_key(path) {
706            return Ok(());
707        }
708        let file = File::open(path).map_err(|_| LoaderError::ExternalDataNotFound {
709            path: path.to_path_buf(),
710        })?;
711        // SAFETY: we hold the `File` open for the duration of the map and never
712        // expose a mutable view. The mapped bytes are treated as immutable
713        // weight storage. This is the only `unsafe` in the loader; the IR crate
714        // stays `#![forbid(unsafe_code)]`.
715        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| LoaderError::Mmap(e.to_string()))?;
716        let id = NEXT_MAPPING_ID
717            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
718            .map_err(|_| LoaderError::Mmap("external mmap identity space exhausted".into()))?;
719        self.mmaps
720            .insert(path.to_path_buf(), MappedFile { id, mmap });
721        Ok(())
722    }
723}
724
725/// Bytes of initializer payload the graph actually **references**, split into
726/// inline (inside the protobuf) and external (in sibling data files).
727///
728/// This exists because a model's on-disk size is only an upper bound on its
729/// weights. An external-data file may contain regions no initializer points at
730/// — most commonly when a re-export appends fresh tensors and never truncates
731/// the original, leaving an orphaned prefix. `qwen14b-zp` is exactly 50% such
732/// dead space, which made the runtime believe it had 2.00x more weights than it
733/// does and skewed every budget and residency decision derived from that number
734/// (#853).
735///
736/// Parses only the graph protobuf. It never opens, maps, or reads the external
737/// data files, so it stays cheap enough for the load-time budget path: the
738/// `.onnx` is typically a few MB even when the weights are tens of GB.
739///
740/// Initializers with an explicit `length` contribute that; external ones
741/// without a declared length fall back to the geometry implied by their shape
742/// and dtype, which is what [`resolve_initializer`] would use.
743#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
744pub struct ReferencedWeightBytes {
745    pub inline: u64,
746    pub external: u64,
747}
748
749impl ReferencedWeightBytes {
750    #[must_use]
751    pub fn total(&self) -> u64 {
752        self.inline.saturating_add(self.external)
753    }
754}
755
756/// Sum the initializer payload [`ReferencedWeightBytes`] a model's graph
757/// references. See that type for why file size is not a substitute.
758#[must_use]
759pub fn referenced_weight_bytes(model: &ModelProto) -> ReferencedWeightBytes {
760    let mut totals = ReferencedWeightBytes::default();
761    let Some(graph) = model.graph.as_ref() else {
762        return totals;
763    };
764    for init in &graph.initializer {
765        let dims: Vec<usize> = init.dims.iter().map(|&d| d.max(0) as usize).collect();
766        let declared_geometry = DataType::from_onnx(init.data_type).and_then(|dtype| {
767            TensorData::from_raw(dtype, dims, Vec::new()).checked_expected_bytes()
768        });
769        if init.data_location == tensor_proto::DataLocation::External as i32 {
770            let declared_length = init
771                .external_data
772                .iter()
773                .find(|kv| kv.key == "length")
774                .and_then(|kv| kv.value.parse::<u64>().ok());
775            let bytes = declared_length
776                .or_else(|| declared_geometry.map(|bytes| bytes as u64))
777                .unwrap_or(0);
778            totals.external = totals.external.saturating_add(bytes);
779        } else {
780            // Inline payloads are already counted by the graph file's own size,
781            // but report them so a caller can tell the two apart.
782            let bytes = if init.raw_data.is_empty() {
783                declared_geometry.map_or(0, |bytes| bytes as u64)
784            } else {
785                init.raw_data.len() as u64
786            };
787            totals.inline = totals.inline.saturating_add(bytes);
788        }
789    }
790    totals
791}
792
793/// Resolve all initializers, memory-mapping external data relative to
794/// `model_dir`. `name_map` maps initializer names to the graph value ids
795/// created by the [`graph_builder`](crate::graph_builder).
796pub fn load_weights(
797    model: &ModelProto,
798    model_dir: &Path,
799    name_map: &HashMap<String, ValueId>,
800) -> Result<WeightStore, LoaderError> {
801    let mut store = WeightStore::new();
802    let Some(graph) = model.graph.as_ref() else {
803        return Ok(store);
804    };
805
806    for init in &graph.initializer {
807        let Some(&vid) = name_map.get(&init.name) else {
808            // An initializer with no corresponding graph value: skip it. The
809            // builder registers a value for every initializer, so this only
810            // happens for malformed models.
811            continue;
812        };
813        let weight = resolve_initializer(&mut store, init, model_dir)?;
814        store.weights.insert(vid, weight);
815    }
816
817    Ok(store)
818}
819
820fn resolve_initializer(
821    store: &mut WeightStore,
822    init: &TensorProto,
823    model_dir: &Path,
824) -> Result<WeightRef, LoaderError> {
825    let dtype =
826        DataType::from_onnx(init.data_type).ok_or_else(|| LoaderError::UnsupportedDataType {
827            raw: init.data_type,
828            context: format!("initializer {:?}", init.name),
829        })?;
830    let dims: Vec<usize> = init.dims.iter().map(|&d| d.max(0) as usize).collect();
831
832    if init.data_location == tensor_proto::DataLocation::External as i32 {
833        let mut location = None;
834        let mut offset: usize = 0;
835        let mut length: Option<usize> = None;
836        for kv in &init.external_data {
837            match kv.key.as_str() {
838                "location" => location = Some(kv.value.clone()),
839                "offset" => offset = kv.value.parse().unwrap_or(0),
840                "length" => length = kv.value.parse().ok(),
841                _ => {}
842            }
843        }
844        let location = location.ok_or_else(|| {
845            LoaderError::GraphBuild(format!(
846                "external initializer {:?} missing 'location'",
847                init.name
848            ))
849        })?;
850        let path = resolve_external_path(model_dir, &location)?;
851        store.mmap_file(&path)?;
852        let expected_length = TensorData::from_raw(dtype, dims.clone(), Vec::new())
853            .checked_expected_bytes()
854            .ok_or_else(|| {
855                LoaderError::GraphBuild(format!(
856                    "external initializer {:?} geometry overflows for shape {dims:?} and dtype {dtype:?}",
857                    init.name
858                ))
859            })?;
860        let length = length.unwrap_or(expected_length);
861        // Validate the window lies within the mapped file (catches truncated or
862        // mis-described external data early).
863        if let Some(mmap) = store.mmaps.get(&path) {
864            let end = offset.checked_add(length);
865            if end.is_none_or(|e| e > mmap.mmap.len()) {
866                return Err(LoaderError::Mmap(format!(
867                    "external initializer {:?}: window [{offset}, {:?}) exceeds file {} ({} bytes)",
868                    init.name,
869                    end,
870                    path.display(),
871                    mmap.mmap.len()
872                )));
873            }
874        }
875        Ok(WeightRef::External {
876            path,
877            offset,
878            length,
879            dtype,
880            dims,
881        })
882    } else {
883        let data = tensor_data_from_proto(init, dtype, &dims)?;
884        Ok(WeightRef::Inline(data))
885    }
886}
887
888/// Join an external-data location onto `model_dir`, rejecting paths that can
889/// escape the model directory.
890fn resolve_external_path(model_dir: &Path, location: &str) -> Result<PathBuf, LoaderError> {
891    guarded_join(model_dir, location).map_err(|reason| LoaderError::ExternalDataPath {
892        path: location.to_string(),
893        reason,
894    })
895}
896
897/// Convert a `TensorProto`'s payload into an IR [`TensorData`] with raw
898/// little-endian bytes (or string payloads for `STRING` tensors).
899pub(crate) fn tensor_data_from_proto(
900    proto: &TensorProto,
901    dtype: DataType,
902    dims: &[usize],
903) -> Result<TensorData, LoaderError> {
904    let mut td = TensorData::from_raw(dtype, dims.to_vec(), Vec::new());
905    if !proto.name.is_empty() {
906        td.name = Some(proto.name.clone());
907    }
908
909    if dtype == DataType::String {
910        td.strings = proto
911            .string_data
912            .iter()
913            .map(|b| String::from_utf8_lossy(b).into_owned())
914            .collect();
915        return Ok(td);
916    }
917
918    // Prefer raw_data when present (the common, mmap-friendly encoding).
919    if !proto.raw_data.is_empty() {
920        td.data = proto.raw_data.clone();
921        return Ok(td);
922    }
923
924    // Otherwise serialise the type-specific repeated field to LE bytes.
925    td.data = match dtype {
926        DataType::Undefined => {
927            return Err(LoaderError::UnsupportedDataType {
928                raw: 0,
929                context: format!("tensor {:?}", proto.name),
930            });
931        }
932        DataType::Float32 => proto
933            .float_data
934            .iter()
935            .flat_map(|v| v.to_le_bytes())
936            .collect(),
937        DataType::Float64 => proto
938            .double_data
939            .iter()
940            .flat_map(|v| v.to_le_bytes())
941            .collect(),
942        DataType::Complex64 => proto
943            .float_data
944            .iter()
945            .flat_map(|v| v.to_le_bytes())
946            .collect(),
947        DataType::Complex128 => proto
948            .double_data
949            .iter()
950            .flat_map(|v| v.to_le_bytes())
951            .collect(),
952        DataType::Int64 => proto
953            .int64_data
954            .iter()
955            .flat_map(|v| v.to_le_bytes())
956            .collect(),
957        DataType::Uint64 | DataType::Uint32 => proto
958            .uint64_data
959            .iter()
960            .flat_map(|v| match dtype {
961                DataType::Uint32 => (*v as u32).to_le_bytes().to_vec(),
962                _ => v.to_le_bytes().to_vec(),
963            })
964            .collect(),
965        DataType::Int32 => proto
966            .int32_data
967            .iter()
968            .flat_map(|v| v.to_le_bytes())
969            .collect(),
970        // Types packed into int32_data at a narrower width.
971        DataType::Int16 | DataType::Uint16 | DataType::Float16 | DataType::BFloat16 => proto
972            .int32_data
973            .iter()
974            .flat_map(|v| (*v as u16).to_le_bytes())
975            .collect(),
976        DataType::Int8 | DataType::Uint8 | DataType::Bool => {
977            proto.int32_data.iter().map(|v| *v as u8).collect()
978        }
979        DataType::Float8E4M3FN
980        | DataType::Float8E4M3FNUZ
981        | DataType::Float8E5M2
982        | DataType::Float8E5M2FNUZ
983        | DataType::Float8E8M0
984        | DataType::Int4
985        | DataType::Uint4
986        | DataType::Float4E2M1
987        | DataType::Int2
988        | DataType::Uint2 => proto.int32_data.iter().map(|v| *v as u8).collect(),
989        DataType::String => unreachable!("STRING tensors returned above"),
990    };
991    Ok(td)
992}
993
994#[cfg(test)]
995mod tests {
996    use super::*;
997    use std::time::{SystemTime, UNIX_EPOCH};
998
999    #[test]
1000    fn float8_typed_data_preserves_each_byte() {
1001        let proto = TensorProto {
1002            data_type: DataType::Float8E4M3FN.to_onnx(),
1003            dims: vec![3],
1004            int32_data: vec![0x01, 0x7f, 0xff],
1005            ..Default::default()
1006        };
1007
1008        let data =
1009            tensor_data_from_proto(&proto, DataType::Float8E4M3FN, &[3]).expect("tensor data");
1010        assert_eq!(data.data, [0x01, 0x7f, 0xff]);
1011    }
1012
1013    #[test]
1014    fn four_bit_typed_data_preserves_packed_nibbles() {
1015        let proto = TensorProto {
1016            data_type: DataType::Int4.to_onnx(),
1017            dims: vec![3],
1018            // ONNX stores two values per int32_data entry: first in the low
1019            // nibble, second in the high nibble.
1020            int32_data: vec![0x21, 0x03],
1021            ..Default::default()
1022        };
1023
1024        let data = tensor_data_from_proto(&proto, DataType::Int4, &[3]).expect("tensor data");
1025        assert_eq!(data.data, [0x21, 0x03]);
1026    }
1027
1028    #[test]
1029    fn two_bit_typed_data_preserves_four_packed_elements_per_byte() {
1030        let proto = TensorProto {
1031            data_type: DataType::Int2.to_onnx(),
1032            dims: vec![5],
1033            // Elements are packed low-to-high in groups of four.
1034            int32_data: vec![0b11_10_01_00, 0b0000_0001],
1035            ..Default::default()
1036        };
1037
1038        let data = tensor_data_from_proto(&proto, DataType::Int2, &[5]).expect("tensor data");
1039        assert_eq!(data.data, [0b11_10_01_00, 0b0000_0001]);
1040        assert_eq!(data.data.len(), DataType::Int2.storage_bytes(5));
1041    }
1042
1043    #[test]
1044    fn external_initializer_rejects_geometry_overflow() {
1045        let stamp = SystemTime::now()
1046            .duration_since(UNIX_EPOCH)
1047            .expect("clock")
1048            .as_nanos();
1049        let model_dir = std::env::current_dir().expect("cwd").join("target");
1050        std::fs::create_dir_all(&model_dir).expect("create target");
1051        let file_name = format!(
1052            "overflowing-external-weight-{}-{stamp}.bin",
1053            std::process::id()
1054        );
1055        let path = model_dir.join(&file_name);
1056        std::fs::write(&path, [0u8]).expect("write external data");
1057
1058        let initializer = TensorProto {
1059            name: "huge".to_string(),
1060            data_type: DataType::Float32.to_onnx(),
1061            dims: vec![i64::MAX, 3],
1062            external_data: vec![crate::proto::onnx::StringStringEntryProto {
1063                key: "location".to_string(),
1064                value: file_name,
1065            }],
1066            data_location: tensor_proto::DataLocation::External as i32,
1067            ..Default::default()
1068        };
1069        let mut store = WeightStore::new();
1070        let error = resolve_initializer(&mut store, &initializer, &model_dir)
1071            .expect_err("overflowing external tensor geometry must be rejected");
1072        assert!(
1073            matches!(error, LoaderError::GraphBuild(message) if message.contains("geometry overflows"))
1074        );
1075
1076        drop(store);
1077        std::fs::remove_file(path).expect("remove external data");
1078    }
1079
1080    #[test]
1081    fn unreferenced_external_bytes_reports_an_orphaned_blob_prefix() {
1082        // The failure this guards is silent: a blob that carries a superseded
1083        // generation of weights still loads and still produces byte-identical
1084        // output, so the only symptom is a file twice the size it should be.
1085        // `qwen14b-zp` carried 8.32 GB of exactly this (50.02% of the blob) for
1086        // months, and before #856 it made the runtime believe the model was
1087        // 2.00x larger than it is (#853).
1088        //
1089        // The control that matters is the *nonzero* case: a check that only
1090        // ever returns 0 on a healthy model proves nothing, so this maps a file
1091        // whose second half is referenced and whose first half is not, and
1092        // asserts the dead prefix is reported exactly.
1093        let stamp = SystemTime::now()
1094            .duration_since(UNIX_EPOCH)
1095            .expect("clock")
1096            .as_nanos();
1097        let model_dir = std::env::current_dir().expect("cwd").join("target");
1098        std::fs::create_dir_all(&model_dir).expect("create target");
1099        let file_name = format!("orphaned-prefix-blob-{}-{stamp}.bin", std::process::id());
1100        let path = model_dir.join(&file_name);
1101
1102        // 256 bytes on disk; only the last 128 are referenced.
1103        const BLOB_LEN: usize = 256;
1104        const LIVE_LEN: usize = 128;
1105        std::fs::write(&path, vec![0u8; BLOB_LEN]).expect("write external data");
1106
1107        let initializer = TensorProto {
1108            name: "live".to_string(),
1109            data_type: DataType::Uint8.to_onnx(),
1110            dims: vec![LIVE_LEN as i64],
1111            external_data: vec![
1112                crate::proto::onnx::StringStringEntryProto {
1113                    key: "location".to_string(),
1114                    value: file_name,
1115                },
1116                crate::proto::onnx::StringStringEntryProto {
1117                    key: "offset".to_string(),
1118                    value: LIVE_LEN.to_string(),
1119                },
1120                crate::proto::onnx::StringStringEntryProto {
1121                    key: "length".to_string(),
1122                    value: LIVE_LEN.to_string(),
1123                },
1124            ],
1125            data_location: tensor_proto::DataLocation::External as i32,
1126            ..Default::default()
1127        };
1128
1129        let mut store = WeightStore::new();
1130        resolve_initializer(&mut store, &initializer, &model_dir).expect("resolve live tensor");
1131
1132        let model = ModelProto {
1133            graph: Some(crate::proto::onnx::GraphProto {
1134                initializer: vec![initializer],
1135                ..Default::default()
1136            }),
1137            ..Default::default()
1138        };
1139        let referenced = referenced_weight_bytes(&model);
1140
1141        assert_eq!(referenced.external, LIVE_LEN as u64);
1142        assert_eq!(store.mapped_external_bytes(), BLOB_LEN as u64);
1143        assert_eq!(
1144            store.unreferenced_external_bytes(&referenced),
1145            (BLOB_LEN - LIVE_LEN) as u64,
1146            "the orphaned prefix must be reported, not rounded away"
1147        );
1148
1149        drop(store);
1150        std::fs::remove_file(path).expect("remove external data");
1151    }
1152
1153    #[test]
1154    fn unreferenced_external_bytes_is_zero_for_a_fully_referenced_blob() {
1155        let stamp = SystemTime::now()
1156            .duration_since(UNIX_EPOCH)
1157            .expect("clock")
1158            .as_nanos();
1159        let model_dir = std::env::current_dir().expect("cwd").join("target");
1160        std::fs::create_dir_all(&model_dir).expect("create target");
1161        let file_name = format!("packed-blob-{}-{stamp}.bin", std::process::id());
1162        let path = model_dir.join(&file_name);
1163
1164        const BLOB_LEN: usize = 128;
1165        std::fs::write(&path, vec![0u8; BLOB_LEN]).expect("write external data");
1166
1167        let initializer = TensorProto {
1168            name: "live".to_string(),
1169            data_type: DataType::Uint8.to_onnx(),
1170            dims: vec![BLOB_LEN as i64],
1171            external_data: vec![
1172                crate::proto::onnx::StringStringEntryProto {
1173                    key: "location".to_string(),
1174                    value: file_name,
1175                },
1176                crate::proto::onnx::StringStringEntryProto {
1177                    key: "offset".to_string(),
1178                    value: "0".to_string(),
1179                },
1180                crate::proto::onnx::StringStringEntryProto {
1181                    key: "length".to_string(),
1182                    value: BLOB_LEN.to_string(),
1183                },
1184            ],
1185            data_location: tensor_proto::DataLocation::External as i32,
1186            ..Default::default()
1187        };
1188
1189        let mut store = WeightStore::new();
1190        resolve_initializer(&mut store, &initializer, &model_dir).expect("resolve live tensor");
1191
1192        let model = ModelProto {
1193            graph: Some(crate::proto::onnx::GraphProto {
1194                initializer: vec![initializer],
1195                ..Default::default()
1196            }),
1197            ..Default::default()
1198        };
1199
1200        assert_eq!(
1201            store.unreferenced_external_bytes(&referenced_weight_bytes(&model)),
1202            0
1203        );
1204
1205        drop(store);
1206        std::fs::remove_file(path).expect("remove external data");
1207    }
1208
1209    #[test]
1210    fn float8e8m0_typed_data_preserves_each_byte() {
1211        let proto = TensorProto {
1212            data_type: DataType::Float8E8M0.to_onnx(),
1213            dims: vec![2],
1214            int32_data: vec![0x7f, 0xff],
1215            ..Default::default()
1216        };
1217
1218        let data = tensor_data_from_proto(&proto, DataType::Float8E8M0, &[2]).expect("tensor data");
1219        assert_eq!(data.data, [0x7f, 0xff]);
1220    }
1221
1222    fn external_weight(path: PathBuf, length: usize, dims: Vec<usize>) -> WeightRef {
1223        WeightRef::External {
1224            path,
1225            offset: 16,
1226            length,
1227            dtype: DataType::Uint8,
1228            dims,
1229        }
1230    }
1231
1232    fn expert_layout(order: ExpertStorageOrder) -> ExpertTensorLayout {
1233        ExpertTensorLayout {
1234            version: 1,
1235            experts: 3,
1236            rows_per_expert: 2,
1237            storage_elements_per_row: 4,
1238            order,
1239            quantization: Some(ExpertQuantization {
1240                bits: 4,
1241                block_size: 16,
1242                blocks_per_row: 1,
1243            }),
1244        }
1245    }
1246
1247    #[test]
1248    fn expert_major_external_tensor_catalogs_contiguous_ranges() {
1249        let stamp = SystemTime::now()
1250            .duration_since(UNIX_EPOCH)
1251            .expect("clock")
1252            .as_nanos();
1253        let path = std::env::current_dir()
1254            .expect("cwd")
1255            .join("target")
1256            .join(format!(
1257                "weight-region-catalog-{}-{stamp}.bin",
1258                std::process::id()
1259            ));
1260        std::fs::create_dir_all(path.parent().expect("parent")).expect("create target");
1261        let mut bytes = vec![0u8; 40];
1262        for (index, byte) in bytes.iter_mut().enumerate() {
1263            *byte = index as u8;
1264        }
1265        std::fs::write(&path, &bytes).expect("write external data");
1266
1267        let weight = external_weight(path.clone(), 24, vec![3, 2, 4]);
1268        let catalog =
1269            WeightRegionCatalog::classify(&weight, expert_layout(ExpertStorageOrder::ExpertMajor));
1270        assert_eq!(catalog.pageability(), &Pageability::Pageable);
1271        assert_eq!(catalog.mapped_bytes(), 24);
1272        assert_eq!(
1273            catalog.region(1),
1274            Some(&ExpertWeightRegion {
1275                expert: 1,
1276                offset: 24,
1277                len: 8,
1278            })
1279        );
1280
1281        let mut store = WeightStore::new();
1282        store.map_external(&path).expect("map external data");
1283        assert_eq!(catalog.expert_bytes(&store, 1), Some(&bytes[24..32]));
1284        drop(store);
1285        std::fs::remove_file(path).expect("remove external data");
1286    }
1287
1288    #[test]
1289    fn interleaved_external_tensor_is_non_pageable_without_error() {
1290        let weight = external_weight(PathBuf::from("weights.bin"), 24, vec![3, 2, 4]);
1291        let catalog =
1292            WeightRegionCatalog::classify(&weight, expert_layout(ExpertStorageOrder::Interleaved));
1293        assert_eq!(
1294            catalog.pageability(),
1295            &Pageability::NonPageable(NonPageableReason::NotExpertMajor)
1296        );
1297        assert!(catalog.region(0).is_none());
1298    }
1299
1300    #[test]
1301    fn catalog_range_math_rejects_zero_masked_overflow_and_isize_excess() {
1302        let overflow = ExpertTensorLayout {
1303            version: 1,
1304            experts: 0,
1305            rows_per_expert: usize::MAX,
1306            storage_elements_per_row: 2,
1307            order: ExpertStorageOrder::ExpertMajor,
1308            quantization: None,
1309        };
1310        let weight = external_weight(PathBuf::from("weights.bin"), 0, vec![0, usize::MAX, 2]);
1311        assert!(matches!(
1312            WeightRegionCatalog::classify(&weight, overflow).pageability(),
1313            Pageability::NonPageable(NonPageableReason::Range(message))
1314                if message.contains("overflow")
1315        ));
1316
1317        assert!(
1318            checked_range(0, isize::MAX as usize + 1, "range")
1319                .unwrap_err()
1320                .to_string()
1321                .contains("isize::MAX")
1322        );
1323
1324        let endpoint = WeightRef::External {
1325            path: PathBuf::from("weights.bin"),
1326            offset: isize::MAX as usize,
1327            length: 1,
1328            dtype: DataType::Uint8,
1329            dims: vec![1, 1, 1],
1330        };
1331        let layout = ExpertTensorLayout {
1332            version: 1,
1333            experts: 1,
1334            rows_per_expert: 1,
1335            storage_elements_per_row: 1,
1336            order: ExpertStorageOrder::ExpertMajor,
1337            quantization: None,
1338        };
1339        assert!(matches!(
1340            WeightRegionCatalog::classify(&endpoint, layout).pageability(),
1341            Pageability::NonPageable(NonPageableReason::Range(message))
1342                if message.contains("endpoint")
1343        ));
1344    }
1345
1346    #[test]
1347    fn qmoe_expert_tensor_layout_rejects_non_rank3_dims() {
1348        assert_eq!(qmoe_expert_tensor_layout(4, 32, 2, &[8, 16]), None);
1349        assert_eq!(qmoe_expert_tensor_layout(4, 32, 2, &[8, 16, 4, 2]), None);
1350        assert_eq!(qmoe_expert_tensor_layout(4, 32, 2, &[]), None);
1351    }
1352
1353    #[test]
1354    fn qmoe_expert_tensor_layout_populates_expert_major_fields_for_rank3_dims() {
1355        let layout = qmoe_expert_tensor_layout(4, 32, 3, &[8, 16, 24])
1356            .expect("rank-3 dims must derive a layout");
1357        assert_eq!(layout.version, 1);
1358        assert_eq!(layout.experts, 8);
1359        assert_eq!(layout.rows_per_expert, 16);
1360        assert_eq!(layout.storage_elements_per_row, 24);
1361        assert_eq!(layout.order, ExpertStorageOrder::ExpertMajor);
1362        assert_eq!(
1363            layout.quantization,
1364            Some(ExpertQuantization {
1365                bits: 4,
1366                block_size: 32,
1367                blocks_per_row: 3,
1368            })
1369        );
1370    }
1371
1372    #[test]
1373    fn qmoe_expert_tensor_layout_classifies_as_pageable_and_partitions_the_bank() {
1374        let stamp = SystemTime::now()
1375            .duration_since(UNIX_EPOCH)
1376            .expect("clock")
1377            .as_nanos();
1378        let dir = std::env::current_dir().expect("cwd").join("target");
1379        std::fs::create_dir_all(&dir).expect("create target");
1380        let path = dir.join(format!(
1381            "qmoe-expert-region-catalog-{}-{stamp}.bin",
1382            std::process::id()
1383        ));
1384        // 4 experts * 16 rows * 24 storage elements (Uint8) per row.
1385        let tensor_len = 4 * 16 * 24;
1386        std::fs::write(&path, vec![0u8; tensor_len]).expect("write external data");
1387
1388        let weight = WeightRef::External {
1389            path,
1390            offset: 0,
1391            length: tensor_len,
1392            dtype: DataType::Uint8,
1393            dims: vec![4, 16, 24],
1394        };
1395        let layout = qmoe_expert_tensor_layout(4, 32, 3, weight.dims())
1396            .expect("rank-3 dims must derive a layout");
1397        let catalog = WeightRegionCatalog::classify(&weight, layout);
1398        assert!(catalog.is_pageable());
1399
1400        // Regions exactly partition the tensor: contiguous, non-overlapping,
1401        // and covering exactly `tensor_len` bytes across the 4 experts.
1402        let mut expected_offset = 0usize;
1403        let per_expert_len = 16 * 24;
1404        for expert in 0..4 {
1405            let range = catalog
1406                .relative_range(expert)
1407                .unwrap_or_else(|| panic!("expert {expert} must have a region"));
1408            assert_eq!(range.start, expected_offset);
1409            assert_eq!(range.end, expected_offset + per_expert_len);
1410            expected_offset = range.end;
1411        }
1412        assert_eq!(expected_offset, tensor_len);
1413        assert!(catalog.region(4).is_none());
1414
1415        std::fs::remove_file(weight_path_for_test(&catalog)).ok();
1416    }
1417
1418    /// Test-only helper: re-derive the external file path a catalog built in
1419    /// this module's own tests was classified from, so the test can clean up
1420    /// after itself without threading the path through every assertion.
1421    fn weight_path_for_test(catalog: &WeightRegionCatalog) -> PathBuf {
1422        catalog.path.clone().unwrap_or_default()
1423    }
1424
1425    #[test]
1426    fn qmoe_expert_tensor_layout_rejects_inline_tensor_with_reason() {
1427        let inline = WeightRef::Inline(TensorData::from_raw(
1428            DataType::Uint8,
1429            vec![2, 4, 8],
1430            vec![0u8; 2 * 4 * 8],
1431        ));
1432        let layout = qmoe_expert_tensor_layout(4, 32, 1, inline.dims())
1433            .expect("rank-3 dims must derive a layout");
1434        let catalog = WeightRegionCatalog::classify(&inline, layout);
1435        assert!(!catalog.is_pageable());
1436        assert_eq!(
1437            catalog.pageability(),
1438            &Pageability::NonPageable(NonPageableReason::InlineTensor)
1439        );
1440    }
1441}