1use 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#[derive(Debug, Default)]
24pub struct WeightStore {
25 pub weights: HashMap<ValueId, WeightRef>,
27 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 #[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 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub struct ExpertQuantization {
79 pub bits: usize,
81 pub block_size: usize,
83 pub blocks_per_row: usize,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum ExpertStorageOrder {
90 ExpertMajor,
92 Interleaved,
94}
95
96#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct ExpertTensorLayout {
99 pub version: u32,
101 pub experts: usize,
102 pub rows_per_expert: usize,
103 pub storage_elements_per_row: usize,
105 pub order: ExpertStorageOrder,
106 pub quantization: Option<ExpertQuantization>,
107}
108
109#[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#[derive(Clone, Debug, PartialEq, Eq)]
130pub enum Pageability {
131 Pageable,
132 NonPageable(NonPageableReason),
133}
134
135#[derive(Clone, Debug, PartialEq, Eq)]
137pub struct ExpertWeightRegion {
138 pub expert: usize,
139 pub offset: usize,
141 pub len: usize,
142}
143
144#[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 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 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 pub fn path(&self) -> Option<&Path> {
473 self.path.as_deref()
474 }
475
476 pub fn tensor_offset(&self) -> usize {
478 self.tensor_offset
479 }
480
481 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 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
513pub 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#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
549#[error("{0}")]
550pub struct WeightRangeError(String);
551
552pub 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
568pub 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
583pub 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
598pub 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 pub fn new() -> Self {
619 Self::default()
620 }
621
622 pub fn map_external(&mut self, path: impl AsRef<Path>) -> Result<(), LoaderError> {
632 self.mmap_file(path.as_ref())
633 }
634
635 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 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 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 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 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#[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#[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 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
793pub 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 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 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
888fn 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
897pub(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 if !proto.raw_data.is_empty() {
920 td.data = proto.raw_data.clone();
921 return Ok(td);
922 }
923
924 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 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 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 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 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 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 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 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 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}