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
39/// Quantization geometry needed to interpret one expert's packed tensor slice.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub struct ExpertQuantization {
42    /// Number of quantized weight bits per logical value.
43    pub bits: usize,
44    /// Number of logical input values sharing one scale/zero-point block.
45    pub block_size: usize,
46    /// Number of quantization blocks represented by each tensor row.
47    pub blocks_per_row: usize,
48}
49
50/// Physical storage order declared by the model/package layout descriptor.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum ExpertStorageOrder {
53    /// All rows for expert 0, then all rows for expert 1, and so on.
54    ExpertMajor,
55    /// Expert rows are interleaved and therefore cannot be represented by one range.
56    Interleaved,
57}
58
59/// Compact layout descriptor used to derive expert byte ranges.
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct ExpertTensorLayout {
62    /// Mandatory layout contract version. Phase 1 supports version 1.
63    pub version: u32,
64    pub experts: usize,
65    pub rows_per_expert: usize,
66    /// Stored elements per row (packed bytes for a `Uint8` weight tensor).
67    pub storage_elements_per_row: usize,
68    pub order: ExpertStorageOrder,
69    pub quantization: Option<ExpertQuantization>,
70}
71
72/// Why an external tensor cannot use the expert paging path.
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub enum NonPageableReason {
75    InlineTensor,
76    UnsupportedLayoutVersion(u32),
77    NotExpertMajor,
78    ShapeMismatch {
79        expected: Vec<usize>,
80        actual: Vec<usize>,
81    },
82    InvalidQuantization(String),
83    Range(String),
84    ExternalLengthMismatch {
85        expected: usize,
86        actual: usize,
87    },
88}
89
90/// Pageability classification. Non-pageable tensors remain valid and use the
91/// existing resident/materializing execution path.
92#[derive(Clone, Debug, PartialEq, Eq)]
93pub enum Pageability {
94    Pageable,
95    NonPageable(NonPageableReason),
96}
97
98/// One expert's contiguous byte window in an external initializer mapping.
99#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct ExpertWeightRegion {
101    pub expert: usize,
102    /// Absolute byte offset in the external-data file.
103    pub offset: usize,
104    pub len: usize,
105}
106
107/// Validated expert-region catalog over a [`WeightRef`] in a [`WeightStore`].
108///
109/// A pageable catalog guarantees that each expert is represented by one
110/// overflow-checked external byte range. The quantization metadata is retained
111/// so a kernel can decode one range without inspecting or materializing peers.
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct WeightRegionCatalog {
114    path: Option<PathBuf>,
115    tensor_offset: usize,
116    tensor_len: usize,
117    dtype: DataType,
118    layout: ExpertTensorLayout,
119    regions: Vec<ExpertWeightRegion>,
120    pageability: Pageability,
121}
122
123impl WeightRegionCatalog {
124    /// Classify a weight using an explicit model/package layout descriptor.
125    ///
126    /// Invalid or non-expert-major layouts produce a non-pageable catalog rather
127    /// than failing model load; callers must fall back to the resident path.
128    pub fn classify(weight: &WeightRef, layout: ExpertTensorLayout) -> Self {
129        let dtype = weight.dtype();
130        let dims = weight.dims();
131        let (path, tensor_offset, tensor_len) = match weight {
132            WeightRef::Inline(tensor) => {
133                return Self::non_pageable(
134                    None,
135                    0,
136                    tensor.data.len(),
137                    dtype,
138                    layout,
139                    NonPageableReason::InlineTensor,
140                );
141            }
142
143            WeightRef::External {
144                path,
145                offset,
146                length,
147                ..
148            } => (Some(path.clone()), *offset, *length),
149        };
150
151        if layout.version != 1 {
152            return Self::non_pageable(
153                path,
154                tensor_offset,
155                tensor_len,
156                dtype,
157                layout.clone(),
158                NonPageableReason::UnsupportedLayoutVersion(layout.version),
159            );
160        }
161        if layout.order != ExpertStorageOrder::ExpertMajor {
162            return Self::non_pageable(
163                path,
164                tensor_offset,
165                tensor_len,
166                dtype,
167                layout,
168                NonPageableReason::NotExpertMajor,
169            );
170        }
171        let expected_shape = vec![
172            layout.experts,
173            layout.rows_per_expert,
174            layout.storage_elements_per_row,
175        ];
176        if dims != expected_shape {
177            return Self::non_pageable(
178                path,
179                tensor_offset,
180                tensor_len,
181                dtype,
182                layout,
183                NonPageableReason::ShapeMismatch {
184                    expected: expected_shape,
185                    actual: dims.to_vec(),
186                },
187            );
188        }
189        if let Some(quantization) = layout.quantization
190            && (!matches!(quantization.bits, 1 | 2 | 4 | 8)
191                || quantization.block_size == 0
192                || quantization.blocks_per_row == 0)
193        {
194            return Self::non_pageable(
195                path,
196                tensor_offset,
197                tensor_len,
198                dtype,
199                layout,
200                NonPageableReason::InvalidQuantization(format!(
201                    "bits={}, block_size={}, blocks_per_row={}",
202                    quantization.bits, quantization.block_size, quantization.blocks_per_row
203                )),
204            );
205        }
206
207        let elements_per_expert = match checked_product(
208            &[layout.rows_per_expert, layout.storage_elements_per_row],
209            "per-expert element count",
210        ) {
211            Ok(value) => value,
212            Err(error) => {
213                return Self::non_pageable(
214                    path,
215                    tensor_offset,
216                    tensor_len,
217                    dtype,
218                    layout,
219                    NonPageableReason::Range(error.to_string()),
220                );
221            }
222        };
223        let bytes_per_expert =
224            match checked_storage_byte_count(dtype, elements_per_expert, "per-expert byte count") {
225                Ok(value) => value,
226                Err(error) => {
227                    return Self::non_pageable(
228                        path,
229                        tensor_offset,
230                        tensor_len,
231                        dtype,
232                        layout,
233                        NonPageableReason::Range(error.to_string()),
234                    );
235                }
236            };
237        let expected_len = match checked_byte_count(
238            layout.experts,
239            bytes_per_expert,
240            "expert tensor byte count",
241        ) {
242            Ok(value) => value,
243            Err(error) => {
244                return Self::non_pageable(
245                    path,
246                    tensor_offset,
247                    tensor_len,
248                    dtype,
249                    layout,
250                    NonPageableReason::Range(error.to_string()),
251                );
252            }
253        };
254        if expected_len != tensor_len {
255            return Self::non_pageable(
256                path,
257                tensor_offset,
258                tensor_len,
259                dtype,
260                layout,
261                NonPageableReason::ExternalLengthMismatch {
262                    expected: expected_len,
263                    actual: tensor_len,
264                },
265            );
266        }
267        let tensor_end = match tensor_offset.checked_add(tensor_len) {
268            Some(end) if end <= isize::MAX as usize => end,
269            Some(_) => {
270                return Self::non_pageable(
271                    path,
272                    tensor_offset,
273                    tensor_len,
274                    dtype,
275                    layout,
276                    NonPageableReason::Range(
277                        "expert tensor absolute endpoint exceeds isize::MAX".into(),
278                    ),
279                );
280            }
281            None => {
282                return Self::non_pageable(
283                    path,
284                    tensor_offset,
285                    tensor_len,
286                    dtype,
287                    layout,
288                    NonPageableReason::Range("expert tensor absolute endpoint overflow".into()),
289                );
290            }
291        };
292
293        let mut regions = Vec::new();
294        if let Err(error) = regions.try_reserve_exact(layout.experts) {
295            return Self::non_pageable(
296                path,
297                tensor_offset,
298                tensor_len,
299                dtype,
300                layout,
301                NonPageableReason::Range(format!("expert region allocation failed: {error}")),
302            );
303        }
304        for expert in 0..layout.experts {
305            let relative = match checked_range(expert, bytes_per_expert, "expert byte range") {
306                Ok(value) => value,
307                Err(error) => {
308                    return Self::non_pageable(
309                        path,
310                        tensor_offset,
311                        tensor_len,
312                        dtype,
313                        layout,
314                        NonPageableReason::Range(error.to_string()),
315                    );
316                }
317            };
318            let offset = match tensor_offset.checked_add(relative.start) {
319                Some(value) => value,
320                None => {
321                    return Self::non_pageable(
322                        path,
323                        tensor_offset,
324                        tensor_len,
325                        dtype,
326                        layout,
327                        NonPageableReason::Range("expert absolute offset overflow".into()),
328                    );
329                }
330            };
331            let _end = match offset.checked_add(bytes_per_expert) {
332                Some(end) if end <= isize::MAX as usize && end <= tensor_end => end,
333                Some(_) => {
334                    return Self::non_pageable(
335                        path,
336                        tensor_offset,
337                        tensor_len,
338                        dtype,
339                        layout,
340                        NonPageableReason::Range(
341                            "expert absolute endpoint exceeds validated tensor range".into(),
342                        ),
343                    );
344                }
345                None => {
346                    return Self::non_pageable(
347                        path,
348                        tensor_offset,
349                        tensor_len,
350                        dtype,
351                        layout,
352                        NonPageableReason::Range("expert absolute endpoint overflow".into()),
353                    );
354                }
355            };
356            regions.push(ExpertWeightRegion {
357                expert,
358                offset,
359                len: bytes_per_expert,
360            });
361        }
362
363        Self {
364            path,
365            tensor_offset,
366            tensor_len,
367            dtype,
368            layout,
369            regions,
370            pageability: Pageability::Pageable,
371        }
372    }
373
374    /// Build relative expert ranges for a tensor view that the caller has
375    /// already established aliases an external mmap initializer.
376    ///
377    /// This is the kernel-boundary counterpart to [`classify`](Self::classify):
378    /// it cannot re-borrow through [`WeightStore`], but applies the same shape,
379    /// layout, quantization, overflow, and `isize::MAX` validation.
380    pub fn for_mapped_tensor_view(
381        dtype: DataType,
382        dims: &[usize],
383        tensor_len: usize,
384        layout: ExpertTensorLayout,
385    ) -> Self {
386        let synthetic = WeightRef::External {
387            path: PathBuf::new(),
388            offset: 0,
389            length: tensor_len,
390            dtype,
391            dims: dims.to_vec(),
392        };
393        let mut catalog = Self::classify(&synthetic, layout);
394        catalog.path = None;
395        catalog
396    }
397
398    fn non_pageable(
399        path: Option<PathBuf>,
400        tensor_offset: usize,
401        tensor_len: usize,
402        dtype: DataType,
403        layout: ExpertTensorLayout,
404        reason: NonPageableReason,
405    ) -> Self {
406        Self {
407            path,
408            tensor_offset,
409            tensor_len,
410            dtype,
411            layout,
412            regions: Vec::new(),
413            pageability: Pageability::NonPageable(reason),
414        }
415    }
416
417    pub fn pageability(&self) -> &Pageability {
418        &self.pageability
419    }
420
421    pub fn is_pageable(&self) -> bool {
422        matches!(self.pageability, Pageability::Pageable)
423    }
424
425    pub fn layout(&self) -> &ExpertTensorLayout {
426        &self.layout
427    }
428
429    pub fn dtype(&self) -> DataType {
430        self.dtype
431    }
432
433    pub fn mapped_bytes(&self) -> usize {
434        if self.is_pageable() {
435            self.tensor_len
436        } else {
437            0
438        }
439    }
440
441    pub fn region(&self, expert: usize) -> Option<&ExpertWeightRegion> {
442        self.regions.get(expert)
443    }
444
445    pub fn relative_range(&self, expert: usize) -> Option<Range<usize>> {
446        let region = self.region(expert)?;
447        let start = region.offset.checked_sub(self.tensor_offset)?;
448        let end = start.checked_add(region.len)?;
449        Some(start..end)
450    }
451
452    /// Borrow one expert directly from the live read-only mmap.
453    pub fn expert_bytes<'a>(&self, store: &'a WeightStore, expert: usize) -> Option<&'a [u8]> {
454        let path = self.path.as_ref()?;
455        let region = self.region(expert)?;
456        store.external_bytes(path, region.offset, region.len)
457    }
458
459    pub fn tensor_offset(&self) -> usize {
460        self.tensor_offset
461    }
462}
463
464/// Overflow error shared by loader range catalogs and paging-aware kernels.
465#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
466#[error("{0}")]
467pub struct WeightRangeError(String);
468
469/// Checked product that still detects overflow when another factor is zero.
470pub fn checked_product(factors: &[usize], context: &str) -> Result<usize, WeightRangeError> {
471    let mut product = 1usize;
472    let mut has_zero = false;
473    for &factor in factors {
474        if factor == 0 {
475            has_zero = true;
476        } else {
477            product = product
478                .checked_mul(factor)
479                .ok_or_else(|| WeightRangeError(format!("{context} overflow")))?;
480        }
481    }
482    Ok(if has_zero { 0 } else { product })
483}
484
485/// Checked element-size multiplication, rejecting slices larger than `isize`.
486pub fn checked_byte_count(
487    elements: usize,
488    element_size: usize,
489    context: &str,
490) -> Result<usize, WeightRangeError> {
491    let bytes = elements
492        .checked_mul(element_size)
493        .ok_or_else(|| WeightRangeError(format!("{context} overflow")))?;
494    if bytes > isize::MAX as usize {
495        return Err(WeightRangeError(format!("{context} exceeds isize::MAX")));
496    }
497    Ok(bytes)
498}
499
500/// Checked storage byte count, including sub-byte ONNX dtypes.
501pub fn checked_storage_byte_count(
502    dtype: DataType,
503    elements: usize,
504    context: &str,
505) -> Result<usize, WeightRangeError> {
506    let bytes = dtype
507        .checked_storage_bytes(elements)
508        .ok_or_else(|| WeightRangeError(format!("{context} overflow")))?;
509    if bytes > isize::MAX as usize {
510        return Err(WeightRangeError(format!("{context} exceeds isize::MAX")));
511    }
512    Ok(bytes)
513}
514
515/// Checked fixed-width range `index * width .. start + width`.
516pub fn checked_range(
517    index: usize,
518    width: usize,
519    context: &str,
520) -> Result<Range<usize>, WeightRangeError> {
521    let start = index
522        .checked_mul(width)
523        .ok_or_else(|| WeightRangeError(format!("{context} start offset overflow")))?;
524    let end = start
525        .checked_add(width)
526        .ok_or_else(|| WeightRangeError(format!("{context} end offset overflow")))?;
527    if end > isize::MAX as usize {
528        return Err(WeightRangeError(format!("{context} exceeds isize::MAX")));
529    }
530    Ok(start..end)
531}
532
533impl WeightStore {
534    /// An empty store.
535    pub fn new() -> Self {
536        Self::default()
537    }
538
539    /// Memory-map an external-data file and register it under `path`, so any
540    /// [`WeightRef::External`] whose `path` matches resolves zero-copy via
541    /// [`bytes`](Self::bytes). Idempotent: mapping the same path twice is a
542    /// no-op. This is the programmatic counterpart to the loader path (which
543    /// maps files while resolving `TensorProto` initializers), useful when
544    /// constructing a store by hand.
545    ///
546    /// The map is read-only and kept alive for the store's lifetime; callers
547    /// must not mutate or unlink the file while the store is live.
548    pub fn map_external(&mut self, path: impl AsRef<Path>) -> Result<(), LoaderError> {
549        self.mmap_file(path.as_ref())
550    }
551
552    /// Resolve a weight descriptor to its raw little-endian bytes.
553    ///
554    /// For inline weights this borrows the stored bytes; for external weights
555    /// it slices into the memory-mapped file. Returns `None` if an external
556    /// mapping is missing or the `[offset, offset+length)` window is out of
557    /// bounds.
558    pub fn bytes<'a>(&'a self, weight: &'a WeightRef) -> Option<&'a [u8]> {
559        match weight {
560            WeightRef::Inline(t) => Some(&t.data),
561            WeightRef::External {
562                path,
563                offset,
564                length,
565                ..
566            } => {
567                let mmap = self.mmaps.get(path)?;
568                mmap.mmap.get(*offset..offset.checked_add(*length)?)
569            }
570        }
571    }
572
573    /// Return stable mmap identity and the validated absolute tensor range.
574    pub fn external_mmap_provenance(&self, weight: &WeightRef) -> Option<(usize, usize, usize)> {
575        let WeightRef::External {
576            path,
577            offset,
578            length,
579            ..
580        } = weight
581        else {
582            return None;
583        };
584        let mmap = self.mmaps.get(path)?;
585        let end = offset.checked_add(*length)?;
586        if end > mmap.mmap.len() || end > isize::MAX as usize {
587            return None;
588        }
589        Some((mmap.id, *offset, *length))
590    }
591
592    fn external_bytes(&self, path: &Path, offset: usize, length: usize) -> Option<&[u8]> {
593        let mmap = self.mmaps.get(path)?;
594        mmap.mmap.get(offset..offset.checked_add(length)?)
595    }
596
597    fn mmap_file(&mut self, path: &Path) -> Result<(), LoaderError> {
598        if self.mmaps.contains_key(path) {
599            return Ok(());
600        }
601        let file = File::open(path).map_err(|_| LoaderError::ExternalDataNotFound {
602            path: path.to_path_buf(),
603        })?;
604        // SAFETY: we hold the `File` open for the duration of the map and never
605        // expose a mutable view. The mapped bytes are treated as immutable
606        // weight storage. This is the only `unsafe` in the loader; the IR crate
607        // stays `#![forbid(unsafe_code)]`.
608        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| LoaderError::Mmap(e.to_string()))?;
609        let id = NEXT_MAPPING_ID
610            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
611            .map_err(|_| LoaderError::Mmap("external mmap identity space exhausted".into()))?;
612        self.mmaps
613            .insert(path.to_path_buf(), MappedFile { id, mmap });
614        Ok(())
615    }
616}
617
618/// Resolve all initializers, memory-mapping external data relative to
619/// `model_dir`. `name_map` maps initializer names to the graph value ids
620/// created by the [`graph_builder`](crate::graph_builder).
621pub fn load_weights(
622    model: &ModelProto,
623    model_dir: &Path,
624    name_map: &HashMap<String, ValueId>,
625) -> Result<WeightStore, LoaderError> {
626    let mut store = WeightStore::new();
627    let Some(graph) = model.graph.as_ref() else {
628        return Ok(store);
629    };
630
631    for init in &graph.initializer {
632        let Some(&vid) = name_map.get(&init.name) else {
633            // An initializer with no corresponding graph value: skip it. The
634            // builder registers a value for every initializer, so this only
635            // happens for malformed models.
636            continue;
637        };
638        let weight = resolve_initializer(&mut store, init, model_dir)?;
639        store.weights.insert(vid, weight);
640    }
641
642    Ok(store)
643}
644
645fn resolve_initializer(
646    store: &mut WeightStore,
647    init: &TensorProto,
648    model_dir: &Path,
649) -> Result<WeightRef, LoaderError> {
650    let dtype =
651        DataType::from_onnx(init.data_type).ok_or_else(|| LoaderError::UnsupportedDataType {
652            raw: init.data_type,
653            context: format!("initializer {:?}", init.name),
654        })?;
655    let dims: Vec<usize> = init.dims.iter().map(|&d| d.max(0) as usize).collect();
656
657    if init.data_location == tensor_proto::DataLocation::External as i32 {
658        let mut location = None;
659        let mut offset: usize = 0;
660        let mut length: Option<usize> = None;
661        for kv in &init.external_data {
662            match kv.key.as_str() {
663                "location" => location = Some(kv.value.clone()),
664                "offset" => offset = kv.value.parse().unwrap_or(0),
665                "length" => length = kv.value.parse().ok(),
666                _ => {}
667            }
668        }
669        let location = location.ok_or_else(|| {
670            LoaderError::GraphBuild(format!(
671                "external initializer {:?} missing 'location'",
672                init.name
673            ))
674        })?;
675        let path = resolve_external_path(model_dir, &location)?;
676        store.mmap_file(&path)?;
677        let numel: usize = dims.iter().product();
678        let length = length.unwrap_or_else(|| dtype.storage_bytes(numel));
679        // Validate the window lies within the mapped file (catches truncated or
680        // mis-described external data early).
681        if let Some(mmap) = store.mmaps.get(&path) {
682            let end = offset.checked_add(length);
683            if end.is_none_or(|e| e > mmap.mmap.len()) {
684                return Err(LoaderError::Mmap(format!(
685                    "external initializer {:?}: window [{offset}, {:?}) exceeds file {} ({} bytes)",
686                    init.name,
687                    end,
688                    path.display(),
689                    mmap.mmap.len()
690                )));
691            }
692        }
693        Ok(WeightRef::External {
694            path,
695            offset,
696            length,
697            dtype,
698            dims,
699        })
700    } else {
701        let data = tensor_data_from_proto(init, dtype, &dims)?;
702        Ok(WeightRef::Inline(data))
703    }
704}
705
706/// Join an external-data location onto `model_dir`, rejecting paths that can
707/// escape the model directory.
708fn resolve_external_path(model_dir: &Path, location: &str) -> Result<PathBuf, LoaderError> {
709    guarded_join(model_dir, location).map_err(|reason| LoaderError::ExternalDataPath {
710        path: location.to_string(),
711        reason,
712    })
713}
714
715/// Convert a `TensorProto`'s payload into an IR [`TensorData`] with raw
716/// little-endian bytes (or string payloads for `STRING` tensors).
717pub(crate) fn tensor_data_from_proto(
718    proto: &TensorProto,
719    dtype: DataType,
720    dims: &[usize],
721) -> Result<TensorData, LoaderError> {
722    let mut td = TensorData::from_raw(dtype, dims.to_vec(), Vec::new());
723    if !proto.name.is_empty() {
724        td.name = Some(proto.name.clone());
725    }
726
727    if dtype == DataType::String {
728        td.strings = proto
729            .string_data
730            .iter()
731            .map(|b| String::from_utf8_lossy(b).into_owned())
732            .collect();
733        return Ok(td);
734    }
735
736    // Prefer raw_data when present (the common, mmap-friendly encoding).
737    if !proto.raw_data.is_empty() {
738        td.data = proto.raw_data.clone();
739        return Ok(td);
740    }
741
742    // Otherwise serialise the type-specific repeated field to LE bytes.
743    td.data = match dtype {
744        DataType::Undefined => {
745            return Err(LoaderError::UnsupportedDataType {
746                raw: 0,
747                context: format!("tensor {:?}", proto.name),
748            });
749        }
750        DataType::Float32 => proto
751            .float_data
752            .iter()
753            .flat_map(|v| v.to_le_bytes())
754            .collect(),
755        DataType::Float64 => proto
756            .double_data
757            .iter()
758            .flat_map(|v| v.to_le_bytes())
759            .collect(),
760        DataType::Complex64 => proto
761            .float_data
762            .iter()
763            .flat_map(|v| v.to_le_bytes())
764            .collect(),
765        DataType::Complex128 => proto
766            .double_data
767            .iter()
768            .flat_map(|v| v.to_le_bytes())
769            .collect(),
770        DataType::Int64 => proto
771            .int64_data
772            .iter()
773            .flat_map(|v| v.to_le_bytes())
774            .collect(),
775        DataType::Uint64 | DataType::Uint32 => proto
776            .uint64_data
777            .iter()
778            .flat_map(|v| match dtype {
779                DataType::Uint32 => (*v as u32).to_le_bytes().to_vec(),
780                _ => v.to_le_bytes().to_vec(),
781            })
782            .collect(),
783        DataType::Int32 => proto
784            .int32_data
785            .iter()
786            .flat_map(|v| v.to_le_bytes())
787            .collect(),
788        // Types packed into int32_data at a narrower width.
789        DataType::Int16 | DataType::Uint16 | DataType::Float16 | DataType::BFloat16 => proto
790            .int32_data
791            .iter()
792            .flat_map(|v| (*v as u16).to_le_bytes())
793            .collect(),
794        DataType::Int8 | DataType::Uint8 | DataType::Bool => {
795            proto.int32_data.iter().map(|v| *v as u8).collect()
796        }
797        DataType::Float8E4M3FN
798        | DataType::Float8E4M3FNUZ
799        | DataType::Float8E5M2
800        | DataType::Float8E5M2FNUZ
801        | DataType::Float8E8M0
802        | DataType::Int4
803        | DataType::Uint4
804        | DataType::Float4E2M1
805        | DataType::Int2
806        | DataType::Uint2 => proto.int32_data.iter().map(|v| *v as u8).collect(),
807        DataType::String => unreachable!("STRING tensors returned above"),
808    };
809    Ok(td)
810}
811
812#[cfg(test)]
813mod tests {
814    use super::*;
815    use std::time::{SystemTime, UNIX_EPOCH};
816
817    #[test]
818    fn float8_typed_data_preserves_each_byte() {
819        let proto = TensorProto {
820            data_type: DataType::Float8E4M3FN.to_onnx(),
821            dims: vec![3],
822            int32_data: vec![0x01, 0x7f, 0xff],
823            ..Default::default()
824        };
825
826        let data =
827            tensor_data_from_proto(&proto, DataType::Float8E4M3FN, &[3]).expect("tensor data");
828        assert_eq!(data.data, [0x01, 0x7f, 0xff]);
829    }
830
831    #[test]
832    fn four_bit_typed_data_preserves_packed_nibbles() {
833        let proto = TensorProto {
834            data_type: DataType::Int4.to_onnx(),
835            dims: vec![3],
836            // ONNX stores two values per int32_data entry: first in the low
837            // nibble, second in the high nibble.
838            int32_data: vec![0x21, 0x03],
839            ..Default::default()
840        };
841
842        let data = tensor_data_from_proto(&proto, DataType::Int4, &[3]).expect("tensor data");
843        assert_eq!(data.data, [0x21, 0x03]);
844    }
845
846    #[test]
847    fn two_bit_typed_data_preserves_four_packed_elements_per_byte() {
848        let proto = TensorProto {
849            data_type: DataType::Int2.to_onnx(),
850            dims: vec![5],
851            // Elements are packed low-to-high in groups of four.
852            int32_data: vec![0b11_10_01_00, 0b0000_0001],
853            ..Default::default()
854        };
855
856        let data = tensor_data_from_proto(&proto, DataType::Int2, &[5]).expect("tensor data");
857        assert_eq!(data.data, [0b11_10_01_00, 0b0000_0001]);
858        assert_eq!(data.data.len(), DataType::Int2.storage_bytes(5));
859    }
860
861    #[test]
862    fn float8e8m0_typed_data_preserves_each_byte() {
863        let proto = TensorProto {
864            data_type: DataType::Float8E8M0.to_onnx(),
865            dims: vec![2],
866            int32_data: vec![0x7f, 0xff],
867            ..Default::default()
868        };
869
870        let data = tensor_data_from_proto(&proto, DataType::Float8E8M0, &[2]).expect("tensor data");
871        assert_eq!(data.data, [0x7f, 0xff]);
872    }
873
874    fn external_weight(path: PathBuf, length: usize, dims: Vec<usize>) -> WeightRef {
875        WeightRef::External {
876            path,
877            offset: 16,
878            length,
879            dtype: DataType::Uint8,
880            dims,
881        }
882    }
883
884    fn expert_layout(order: ExpertStorageOrder) -> ExpertTensorLayout {
885        ExpertTensorLayout {
886            version: 1,
887            experts: 3,
888            rows_per_expert: 2,
889            storage_elements_per_row: 4,
890            order,
891            quantization: Some(ExpertQuantization {
892                bits: 4,
893                block_size: 16,
894                blocks_per_row: 1,
895            }),
896        }
897    }
898
899    #[test]
900    fn expert_major_external_tensor_catalogs_contiguous_ranges() {
901        let stamp = SystemTime::now()
902            .duration_since(UNIX_EPOCH)
903            .expect("clock")
904            .as_nanos();
905        let path = std::env::current_dir()
906            .expect("cwd")
907            .join("target")
908            .join(format!(
909                "weight-region-catalog-{}-{stamp}.bin",
910                std::process::id()
911            ));
912        std::fs::create_dir_all(path.parent().expect("parent")).expect("create target");
913        let mut bytes = vec![0u8; 40];
914        for (index, byte) in bytes.iter_mut().enumerate() {
915            *byte = index as u8;
916        }
917        std::fs::write(&path, &bytes).expect("write external data");
918
919        let weight = external_weight(path.clone(), 24, vec![3, 2, 4]);
920        let catalog =
921            WeightRegionCatalog::classify(&weight, expert_layout(ExpertStorageOrder::ExpertMajor));
922        assert_eq!(catalog.pageability(), &Pageability::Pageable);
923        assert_eq!(catalog.mapped_bytes(), 24);
924        assert_eq!(
925            catalog.region(1),
926            Some(&ExpertWeightRegion {
927                expert: 1,
928                offset: 24,
929                len: 8,
930            })
931        );
932
933        let mut store = WeightStore::new();
934        store.map_external(&path).expect("map external data");
935        assert_eq!(catalog.expert_bytes(&store, 1), Some(&bytes[24..32]));
936        drop(store);
937        std::fs::remove_file(path).expect("remove external data");
938    }
939
940    #[test]
941    fn interleaved_external_tensor_is_non_pageable_without_error() {
942        let weight = external_weight(PathBuf::from("weights.bin"), 24, vec![3, 2, 4]);
943        let catalog =
944            WeightRegionCatalog::classify(&weight, expert_layout(ExpertStorageOrder::Interleaved));
945        assert_eq!(
946            catalog.pageability(),
947            &Pageability::NonPageable(NonPageableReason::NotExpertMajor)
948        );
949        assert!(catalog.region(0).is_none());
950    }
951
952    #[test]
953    fn catalog_range_math_rejects_zero_masked_overflow_and_isize_excess() {
954        let overflow = ExpertTensorLayout {
955            version: 1,
956            experts: 0,
957            rows_per_expert: usize::MAX,
958            storage_elements_per_row: 2,
959            order: ExpertStorageOrder::ExpertMajor,
960            quantization: None,
961        };
962        let weight = external_weight(PathBuf::from("weights.bin"), 0, vec![0, usize::MAX, 2]);
963        assert!(matches!(
964            WeightRegionCatalog::classify(&weight, overflow).pageability(),
965            Pageability::NonPageable(NonPageableReason::Range(message))
966                if message.contains("overflow")
967        ));
968
969        assert!(
970            checked_range(0, isize::MAX as usize + 1, "range")
971                .unwrap_err()
972                .to_string()
973                .contains("isize::MAX")
974        );
975
976        let endpoint = WeightRef::External {
977            path: PathBuf::from("weights.bin"),
978            offset: isize::MAX as usize,
979            length: 1,
980            dtype: DataType::Uint8,
981            dims: vec![1, 1, 1],
982        };
983        let layout = ExpertTensorLayout {
984            version: 1,
985            experts: 1,
986            rows_per_expert: 1,
987            storage_elements_per_row: 1,
988            order: ExpertStorageOrder::ExpertMajor,
989            quantization: None,
990        };
991        assert!(matches!(
992            WeightRegionCatalog::classify(&endpoint, layout).pageability(),
993            Pageability::NonPageable(NonPageableReason::Range(message))
994                if message.contains("endpoint")
995        ));
996    }
997}