1use crate::checkpoint::{TensorCatalog, TensorDescriptor, TensorDtype, TensorStorage};
7pub use eredu_checkpoint::artifact::ArtifactFile;
8use eredu_checkpoint::{
9 artifact::{fingerprint_artifact_files, ArtifactFingerprintError, ArtifactMemberFingerprint},
10 safetensors::SafetensorsShards,
11 store::{SharedCheckpointSource, TensorMetadata},
12 StoredDtype,
13};
14use eredu_gguf::{Checkpoint as GgufCheckpoint, GgmlType, MetadataValue};
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::{
18 collections::{BTreeMap, BTreeSet},
19 fs::File,
20 io::Read,
21 path::{Path, PathBuf},
22 sync::Arc,
23};
24
25#[derive(Clone, Copy, Eq, Hash, PartialEq)]
31pub struct ArtifactIdentity([u8; 32]);
32
33impl ArtifactIdentity {
34 pub const fn digest(self) -> [u8; 32] {
36 self.0
37 }
38}
39
40#[derive(Clone)]
46pub struct ArtifactAdmissionToken(Arc<()>);
47
48impl ArtifactAdmissionToken {
49 fn new() -> Self {
50 Self(Arc::new(()))
51 }
52
53 pub fn same_admission(&self, other: &Self) -> bool {
55 Arc::ptr_eq(&self.0, &other.0)
56 }
57}
58
59impl std::fmt::Debug for ArtifactAdmissionToken {
60 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 formatter.write_str("ArtifactAdmissionToken(..)")
62 }
63}
64
65impl std::fmt::Display for ArtifactIdentity {
66 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(formatter, "sha256:{}", hex_digest(&self.0))
68 }
69}
70
71impl std::fmt::Debug for ArtifactIdentity {
72 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 std::fmt::Display::fmt(self, formatter)
74 }
75}
76
77#[derive(Debug, Clone, Eq, PartialEq)]
79pub struct ArtifactMemberIdentity {
80 logical_role: String,
81 length: u64,
82 digest: [u8; 32],
83}
84
85impl ArtifactMemberIdentity {
86 pub fn new(logical_role: impl Into<String>, length: u64, digest: [u8; 32]) -> Self {
88 Self {
89 logical_role: logical_role.into(),
90 length,
91 digest,
92 }
93 }
94
95 pub fn logical_role(&self) -> &str {
97 &self.logical_role
98 }
99
100 pub const fn length(&self) -> u64 {
102 self.length
103 }
104
105 pub const fn digest(&self) -> [u8; 32] {
107 self.digest
108 }
109}
110
111impl From<ArtifactMemberFingerprint> for ArtifactMemberIdentity {
112 fn from(member: ArtifactMemberFingerprint) -> Self {
113 Self::new(member.logical_role(), member.length(), member.digest())
114 }
115}
116
117pub fn fingerprint_artifact(
122 domain: &str,
123 members: impl IntoIterator<Item = ArtifactMemberIdentity>,
124) -> Result<ArtifactIdentity, ArtifactError> {
125 if domain.is_empty() {
126 return Err(ArtifactError::InvalidArtifactIdentity(
127 "artifact identity domain must not be empty".into(),
128 ));
129 }
130 let mut members = members.into_iter().collect::<Vec<_>>();
131 if members.is_empty() {
132 return Err(ArtifactError::InvalidArtifactIdentity(
133 "artifact identity requires at least one file".into(),
134 ));
135 }
136 if members.iter().any(|member| member.logical_role.is_empty()) {
137 return Err(ArtifactError::InvalidArtifactIdentity(
138 "artifact member has an empty logical role".into(),
139 ));
140 }
141 members.sort_unstable_by(|left, right| left.logical_role.cmp(&right.logical_role));
142 if let Some(pair) = members
143 .windows(2)
144 .find(|pair| pair[0].logical_role == pair[1].logical_role)
145 {
146 return Err(ArtifactError::InvalidArtifactIdentity(format!(
147 "duplicate artifact logical role {:?}",
148 pair[0].logical_role
149 )));
150 }
151
152 let mut hasher = sha2::Sha256::new();
153 hash_identity_component(&mut hasher, b"eredu-checkpoint-artifact-v2");
154 hash_identity_component(&mut hasher, domain.as_bytes());
155 use sha2::Digest as _;
156 hasher.update((members.len() as u64).to_le_bytes());
157 for member in members {
158 hash_identity_component(&mut hasher, member.logical_role.as_bytes());
159 hasher.update(member.length.to_le_bytes());
160 hasher.update(member.digest);
161 }
162 Ok(ArtifactIdentity(hasher.finalize().into()))
163}
164
165pub fn fingerprint_filesystem_artifact(
168 domain: &str,
169 files: impl IntoIterator<Item = ArtifactFile>,
170) -> Result<ArtifactIdentity, ArtifactError> {
171 fingerprint_artifact(
172 domain,
173 fingerprint_artifact_files(files)?
174 .into_iter()
175 .map(ArtifactMemberIdentity::from),
176 )
177}
178
179pub fn fingerprint_safetensors_artifact(
185 domain: &str,
186 shards: &SafetensorsShards,
187) -> Result<ArtifactIdentity, ArtifactError> {
188 fingerprint_filesystem_artifact(
189 domain,
190 shards
191 .logical_payload_paths()
192 .iter()
193 .map(|(role, path)| ArtifactFile::new(role, path)),
194 )
195}
196
197pub fn fingerprint_gguf_artifact(
202 domain: &str,
203 checkpoint: &GgufCheckpoint,
204) -> Result<ArtifactIdentity, ArtifactError> {
205 fingerprint_filesystem_artifact(
206 domain,
207 checkpoint
208 .shards()
209 .iter()
210 .map(|shard| ArtifactFile::new(format!("split/{:05}", shard.split_no()), shard.path())),
211 )
212}
213
214fn hash_identity_component(hasher: &mut sha2::Sha256, value: &[u8]) {
215 use sha2::Digest as _;
216 hasher.update((value.len() as u64).to_le_bytes());
217 hasher.update(value);
218}
219
220fn hex_digest(bytes: &[u8]) -> String {
221 const DIGITS: &[u8; 16] = b"0123456789abcdef";
222 let mut output = String::with_capacity(bytes.len() * 2);
223 for &byte in bytes {
224 output.push(DIGITS[usize::from(byte >> 4)] as char);
225 output.push(DIGITS[usize::from(byte & 0x0f)] as char);
226 }
227 output
228}
229
230#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
235#[serde(rename_all = "snake_case")]
236#[non_exhaustive]
237pub enum LoadingProtocol {
238 Model,
240 Realtime,
242}
243
244#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
246#[serde(rename_all = "snake_case")]
247#[non_exhaustive]
248pub enum ArtifactFormat {
249 SafeTensors,
251 Gguf,
253}
254
255#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
257pub struct ModelConfiguration {
258 declared_model_type: String,
260 effective_model_type: String,
262 family: String,
264 loading_protocol: LoadingProtocol,
266 #[serde(skip_serializing_if = "Option::is_none")]
268 json: Option<Value>,
269}
270
271impl ModelConfiguration {
272 pub fn new(
274 declared_model_type: impl Into<String>,
275 effective_model_type: impl Into<String>,
276 family: impl Into<String>,
277 loading_protocol: LoadingProtocol,
278 json: Option<Value>,
279 ) -> Result<Self, ArtifactError> {
280 let configuration = Self {
281 declared_model_type: declared_model_type.into(),
282 effective_model_type: effective_model_type.into(),
283 family: family.into(),
284 loading_protocol,
285 json,
286 };
287 if [
288 &configuration.declared_model_type,
289 &configuration.effective_model_type,
290 &configuration.family,
291 ]
292 .into_iter()
293 .any(|value| value.trim().is_empty())
294 {
295 return Err(ArtifactError::InvalidArtifact(
296 "model configuration identities must be non-empty".into(),
297 ));
298 }
299 Ok(configuration)
300 }
301
302 pub fn declared_model_type(&self) -> &str {
304 &self.declared_model_type
305 }
306
307 pub fn effective_model_type(&self) -> &str {
309 &self.effective_model_type
310 }
311
312 pub fn family(&self) -> &str {
314 &self.family
315 }
316
317 pub const fn loading_protocol(&self) -> LoadingProtocol {
319 self.loading_protocol
320 }
321
322 pub const fn json(&self) -> Option<&Value> {
324 self.json.as_ref()
325 }
326}
327
328#[derive(Debug, Clone)]
330pub struct ResolvedModelConfiguration<P> {
331 configuration: ModelConfiguration,
333 architecture_plan: P,
335}
336
337impl<P> ResolvedModelConfiguration<P> {
338 pub fn new(configuration: ModelConfiguration, architecture_plan: P) -> Self {
340 Self {
341 configuration,
342 architecture_plan,
343 }
344 }
345
346 pub const fn configuration(&self) -> &ModelConfiguration {
348 &self.configuration
349 }
350
351 pub const fn architecture_plan(&self) -> &P {
353 &self.architecture_plan
354 }
355
356 pub fn into_parts(self) -> (ModelConfiguration, P) {
358 (self.configuration, self.architecture_plan)
359 }
360}
361
362pub trait ModelConfigurationResolver {
367 type ArtifactPlan: Clone + std::fmt::Debug;
369
370 fn resolve_safetensors(
372 &self,
373 json: &Value,
374 ) -> Result<ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError>;
375
376 fn resolve_gguf(
378 &self,
379 architecture: &str,
380 checkpoint: &GgufCheckpoint,
381 ) -> Result<ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError>;
382
383 fn gguf_companion_requirements(
385 &self,
386 architecture: &str,
387 checkpoint: &GgufCheckpoint,
388 ) -> Result<Vec<GgufCompanionRequirement>, ArtifactError>;
389
390 fn artifact_plan(
393 &self,
394 _path: &Path,
395 _format: ArtifactFormat,
396 _configuration: &ModelConfiguration,
397 _tensors: &TensorCatalog,
398 _validated_gguf: Option<&ValidatedGguf>,
399 resolved_plan: Self::ArtifactPlan,
400 ) -> Result<Self::ArtifactPlan, ArtifactError> {
401 Ok(resolved_plan)
402 }
403}
404
405#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
407#[non_exhaustive]
408pub enum GgufCompanionRole {
409 MediaProjector,
411 Named(String),
413}
414
415#[derive(Debug, Clone, Copy, Eq, PartialEq)]
417#[non_exhaustive]
418pub enum GgufCompanionEncoding {
419 DenseRequired,
421 DensePreferred,
423}
424
425#[derive(Debug, Clone, Eq, PartialEq)]
427pub struct GgufCompanionRequirement {
428 role: GgufCompanionRole,
429 required: bool,
430 filename_prefix: String,
431 parent_search_depth: usize,
432 encoding: GgufCompanionEncoding,
433}
434
435impl GgufCompanionRequirement {
436 pub fn new(
438 role: GgufCompanionRole,
439 required: bool,
440 filename_prefix: impl Into<String>,
441 parent_search_depth: usize,
442 encoding: GgufCompanionEncoding,
443 ) -> Result<Self, ArtifactError> {
444 let filename_prefix = filename_prefix.into();
445 if filename_prefix.trim().is_empty()
446 || matches!(&role, GgufCompanionRole::Named(name) if name.trim().is_empty())
447 {
448 return Err(ArtifactError::InvalidArtifact(
449 "GGUF companion roles and filename prefixes must be non-empty".into(),
450 ));
451 }
452 Ok(Self {
453 role,
454 required,
455 filename_prefix,
456 parent_search_depth,
457 encoding,
458 })
459 }
460
461 pub fn role(&self) -> &GgufCompanionRole {
463 &self.role
464 }
465}
466
467pub fn gguf_u32_metadata_values(
469 key: &str,
470 value: Option<&MetadataValue>,
471) -> Result<Vec<u32>, ArtifactError> {
472 let Some(value) = value else {
473 return Ok(Vec::new());
474 };
475 value.to_u32_vec().ok_or_else(|| {
476 ArtifactError::InvalidArtifact(format!(
477 "GGUF metadata key {key:?} must contain an integer or integer array whose values fit in u32"
478 ))
479 })
480}
481
482#[derive(Debug, Clone)]
484pub struct ArtifactInspection<P = ()> {
485 admission_token: ArtifactAdmissionToken,
486 path: PathBuf,
487 format: ArtifactFormat,
488 configuration: ModelConfiguration,
489 tensors: TensorCatalog,
490 safetensors_shards: Option<SafetensorsShards>,
491 validated_gguf: Option<ValidatedGguf>,
492 architecture_plan: P,
493}
494
495#[derive(Debug, Clone)]
500pub struct ValidatedGguf {
501 checkpoint: GgufCheckpoint,
502 companions: BTreeMap<GgufCompanionRole, ValidatedGgufCompanion>,
503}
504
505#[derive(Debug, Clone)]
507pub struct ValidatedGgufCompanion {
508 path: PathBuf,
509 checkpoint: GgufCheckpoint,
510}
511
512impl ValidatedGgufCompanion {
513 pub fn path(&self) -> &Path {
515 &self.path
516 }
517
518 pub fn checkpoint(&self) -> &GgufCheckpoint {
520 &self.checkpoint
521 }
522}
523
524impl ValidatedGguf {
525 pub fn checkpoint(&self) -> &GgufCheckpoint {
527 &self.checkpoint
528 }
529
530 pub fn companion(&self, role: &GgufCompanionRole) -> Option<&ValidatedGgufCompanion> {
532 self.companions.get(role)
533 }
534
535 pub fn companions(
537 &self,
538 ) -> impl Iterator<Item = (&GgufCompanionRole, &ValidatedGgufCompanion)> {
539 self.companions.iter()
540 }
541
542 pub fn into_parts(
544 self,
545 ) -> (
546 GgufCheckpoint,
547 BTreeMap<GgufCompanionRole, ValidatedGgufCompanion>,
548 ) {
549 (self.checkpoint, self.companions)
550 }
551}
552
553impl<P> ArtifactInspection<P> {
554 pub fn admission_token(&self) -> ArtifactAdmissionToken {
556 self.admission_token.clone()
557 }
558 pub fn path(&self) -> &Path {
560 &self.path
561 }
562 pub const fn format(&self) -> ArtifactFormat {
564 self.format
565 }
566 pub fn configuration(&self) -> &ModelConfiguration {
568 &self.configuration
569 }
570 pub fn tensors(&self) -> &TensorCatalog {
572 &self.tensors
573 }
574 pub fn safetensors_shards(&self) -> Option<&SafetensorsShards> {
576 self.safetensors_shards.as_ref()
577 }
578 pub fn validated_gguf(&self) -> Option<&ValidatedGguf> {
580 self.validated_gguf.as_ref()
581 }
582 pub fn gguf_checkpoint(&self) -> Option<&GgufCheckpoint> {
584 self.validated_gguf().map(ValidatedGguf::checkpoint)
585 }
586 pub fn architecture_plan(&self) -> &P {
588 &self.architecture_plan
589 }
590 pub fn architecture_plan_mut(&mut self) -> &mut P {
592 &mut self.architecture_plan
593 }
594
595 pub fn map_architecture_plan<Q>(self, map: impl FnOnce(P) -> Q) -> ArtifactInspection<Q> {
600 ArtifactInspection {
601 admission_token: self.admission_token,
602 path: self.path,
603 format: self.format,
604 configuration: self.configuration,
605 tensors: self.tensors,
606 safetensors_shards: self.safetensors_shards,
607 validated_gguf: self.validated_gguf,
608 architecture_plan: map(self.architecture_plan),
609 }
610 }
611}
612
613#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
615#[serde(tag = "kind", rename_all = "snake_case")]
616#[non_exhaustive]
617pub enum QuantizationRequest {
618 Affine {
620 group_size: u32,
622 bits: u8,
624 },
625 MxFp4,
627}
628
629#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
631#[serde(rename_all = "snake_case")]
632#[non_exhaustive]
633pub enum ResidencyRequest {
634 #[default]
636 FullyResident,
637 LayerwiseHost,
639 DenseDiskStream,
641 AddressableParameterBanks,
643}
644
645#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
647pub struct PreparationPolicy {
648 quantization: Option<QuantizationRequest>,
650 residency: ResidencyRequest,
652 topology: Option<crate::topology::ParallelTopology>,
654 required_session_capabilities: crate::backend::SessionCapabilities,
656}
657
658impl PreparationPolicy {
659 pub const fn new(
661 quantization: Option<QuantizationRequest>,
662 residency: ResidencyRequest,
663 ) -> Self {
664 Self {
665 quantization,
666 residency,
667 topology: None,
668 required_session_capabilities: crate::backend::SessionCapabilities::new(
669 false, false, false,
670 ),
671 }
672 }
673
674 pub const fn quantization(self) -> Option<QuantizationRequest> {
676 self.quantization
677 }
678 pub const fn residency(self) -> ResidencyRequest {
680 self.residency
681 }
682 pub const fn topology(self) -> Option<crate::topology::ParallelTopology> {
684 self.topology
685 }
686 pub const fn required_session_capabilities(self) -> crate::backend::SessionCapabilities {
688 self.required_session_capabilities
689 }
690 pub const fn with_topology(mut self, topology: crate::topology::ParallelTopology) -> Self {
692 self.topology = Some(topology);
693 self
694 }
695 pub const fn with_required_session_capabilities(
697 mut self,
698 capabilities: crate::backend::SessionCapabilities,
699 ) -> Self {
700 self.required_session_capabilities = capabilities;
701 self
702 }
703
704 pub fn validate_session_capabilities(
706 &self,
707 available: &crate::backend::SessionCapabilities,
708 ) -> Result<(), crate::backend::SessionCapabilityError> {
709 self.required_session_capabilities.validate(available)
710 }
711}
712
713#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
715#[serde(rename_all = "snake_case")]
716#[non_exhaustive]
717pub enum MaterializationRoute {
718 Resident,
720 Layerwise,
722 AddressableParameterBanks,
724}
725
726#[derive(Debug, Clone)]
728pub struct ModelPreparationPlan<P = ()> {
729 inspection: ArtifactInspection<P>,
730 policy: PreparationPolicy,
731 route: MaterializationRoute,
732 admitted_session_capabilities: crate::backend::SessionCapabilities,
733}
734
735impl<P> ModelPreparationPlan<P> {
736 pub fn from_retained_admission(
739 inspection: ArtifactInspection<P>,
740 admission: crate::PreparationAdmission,
741 ) -> Result<Self, ArtifactError> {
742 if admission.request().format() != inspection.format() {
743 return Err(ArtifactError::InvalidArtifact(
744 "retained preparation admission has a different artifact format".into(),
745 ));
746 }
747 Ok(Self {
748 inspection,
749 policy: admission.request().policy(),
750 route: admission.route(),
751 admitted_session_capabilities: admission.session_capabilities(),
752 })
753 }
754
755 pub fn inspection(&self) -> &ArtifactInspection<P> {
757 &self.inspection
758 }
759 pub const fn policy(&self) -> PreparationPolicy {
761 self.policy
762 }
763 pub const fn route(&self) -> MaterializationRoute {
765 self.route
766 }
767 pub const fn admitted_session_capabilities(&self) -> crate::backend::SessionCapabilities {
769 self.admitted_session_capabilities
770 }
771
772 pub fn into_artifact(self) -> ModelArtifact {
779 match self.inspection.validated_gguf {
780 Some(validated) => ModelArtifact::Gguf {
781 path: self.inspection.path,
782 configuration: self.inspection.configuration,
783 tensors: self.inspection.tensors,
784 validated,
785 },
786 None => ModelArtifact::SafeTensors {
787 path: self.inspection.path,
788 configuration: self.inspection.configuration,
789 tensors: self.inspection.tensors,
790 shards: self
791 .inspection
792 .safetensors_shards
793 .expect("SafeTensors inspection retains its admitted shard set"),
794 },
795 }
796 }
797}
798
799#[derive(Debug, Clone)]
801#[non_exhaustive]
802pub enum ModelArtifact {
803 SafeTensors {
805 path: PathBuf,
807 configuration: ModelConfiguration,
809 tensors: TensorCatalog,
811 shards: SafetensorsShards,
813 },
814 Gguf {
816 path: PathBuf,
818 configuration: ModelConfiguration,
820 tensors: TensorCatalog,
822 validated: ValidatedGguf,
824 },
825}
826
827pub fn open_prepared_safetensors_artifact(
835 tensors: &TensorCatalog,
836 shards: SafetensorsShards,
837 resolution: eredu_checkpoint::validation::ResolvedCheckpointPlan,
838 max_cached_shards: usize,
839) -> Result<SharedCheckpointSource, ArtifactError> {
840 let catalog = tensors
841 .descriptors()
842 .map(|tensor| {
843 let storage = tensor.storage.as_ref().ok_or_else(|| {
844 ArtifactError::InvalidArtifact(format!(
845 "prepared SafeTensors tensor {:?} has no storage provenance",
846 tensor.name
847 ))
848 })?;
849 let stored_dtype = tensor_dtype_to_stored(&tensor.dtype);
850 Ok((
851 tensor.name.clone(),
852 TensorMetadata {
853 name: tensor.name.clone(),
854 logical_shape: tensor.shape.clone(),
855 physical_shape: tensor.shape.clone(),
856 stored_dtype,
857 encoded_byte_len: storage.length,
858 backing_shard: Some(PathBuf::from(&storage.member)),
859 },
860 ))
861 })
862 .collect::<Result<BTreeMap<_, _>, ArtifactError>>()?;
863 eredu_checkpoint::store::open_prepared_safetensors_source(
864 shards,
865 catalog,
866 resolution,
867 max_cached_shards,
868 )
869 .map_err(Into::into)
870}
871
872fn tensor_dtype_to_stored(dtype: &TensorDtype) -> StoredDtype {
873 match dtype {
874 TensorDtype::Bool => StoredDtype::Bool,
875 TensorDtype::U8 => StoredDtype::U8,
876 TensorDtype::I8 => StoredDtype::I8,
877 TensorDtype::I16 => StoredDtype::I16,
878 TensorDtype::U16 => StoredDtype::U16,
879 TensorDtype::F16 => StoredDtype::F16,
880 TensorDtype::Bf16 => StoredDtype::BF16,
881 TensorDtype::I32 => StoredDtype::I32,
882 TensorDtype::U32 => StoredDtype::U32,
883 TensorDtype::F32 => StoredDtype::F32,
884 TensorDtype::F64 => StoredDtype::F64,
885 TensorDtype::I64 => StoredDtype::I64,
886 TensorDtype::U64 => StoredDtype::U64,
887 TensorDtype::Complex64 => StoredDtype::C64,
888 TensorDtype::Encoded(name) => match name.as_str() {
889 "F8_E4M3" => StoredDtype::F8E4M3,
890 "F4" => StoredDtype::F4,
891 "F8_E8M0" => StoredDtype::F8E8M0,
892 "F8_E5M2" => StoredDtype::F8E5M2,
893 _ => StoredDtype::Other(name.clone()),
894 },
895 }
896}
897
898pub fn inspect_artifact<R: ModelConfigurationResolver>(
900 path: impl AsRef<Path>,
901 resolver: &R,
902) -> Result<ArtifactInspection<R::ArtifactPlan>, ArtifactError> {
903 let path = path.as_ref();
904 if is_gguf(path) {
905 inspect_gguf(path, resolver)
906 } else if path.is_dir() {
907 inspect_safetensors(path, resolver)
908 } else if !path.exists() {
909 Err(ArtifactError::MissingArtifact(path.to_path_buf()))
910 } else {
911 Err(ArtifactError::UnsupportedContainer(path.to_path_buf()))
912 }
913}
914
915pub fn plan_model_preparation<P>(
917 inspection: ArtifactInspection<P>,
918 policy: PreparationPolicy,
919 admitted_session_capabilities: crate::backend::SessionCapabilities,
920) -> Result<ModelPreparationPlan<P>, ArtifactError> {
921 let route = validate_preparation_policy(inspection.configuration.loading_protocol, policy)?;
922 Ok(ModelPreparationPlan {
923 inspection,
924 policy,
925 route,
926 admitted_session_capabilities,
927 })
928}
929
930pub fn validate_preparation_policy(
932 protocol: LoadingProtocol,
933 policy: PreparationPolicy,
934) -> Result<MaterializationRoute, ArtifactError> {
935 if protocol != LoadingProtocol::Model {
936 return Err(ArtifactError::UnsupportedLoadingProtocol(protocol));
937 }
938 let route = match policy.residency {
939 ResidencyRequest::FullyResident => MaterializationRoute::Resident,
940 ResidencyRequest::LayerwiseHost | ResidencyRequest::DenseDiskStream => {
941 MaterializationRoute::Layerwise
942 }
943 ResidencyRequest::AddressableParameterBanks => {
944 MaterializationRoute::AddressableParameterBanks
945 }
946 };
947 Ok(route)
948}
949
950fn inspect_gguf<R: ModelConfigurationResolver>(
951 path: &Path,
952 resolver: &R,
953) -> Result<ArtifactInspection<R::ArtifactPlan>, ArtifactError> {
954 let checkpoint = GgufCheckpoint::open(path)?;
955 let architecture_name = checkpoint
956 .metadata()
957 .get("general.architecture")
958 .and_then(MetadataValue::as_str)
959 .ok_or(ArtifactError::MissingGgufArchitecture)?;
960 let (configuration, resolved_plan) = resolver
961 .resolve_gguf(architecture_name, &checkpoint)?
962 .into_parts();
963 let requirements = resolver.gguf_companion_requirements(architecture_name, &checkpoint)?;
964 let companions = resolve_gguf_companions(path, &requirements)?;
965 validate_gguf_container(&checkpoint)?;
966 let tensors = checkpoint
967 .tensors()
968 .map(|tensor| {
969 let descriptor = tensor.descriptor();
970 let shape = descriptor
971 .dimensions
972 .iter()
973 .map(|&dimension| {
974 usize::try_from(dimension).map_err(|_| {
975 ArtifactError::InvalidArtifact(format!(
976 "GGUF tensor {:?} dimension {dimension} exceeds the host address space",
977 descriptor.name
978 ))
979 })
980 })
981 .collect::<Result<Vec<_>, _>>()?;
982 Ok(TensorDescriptor {
983 name: descriptor.name.clone(),
984 shape,
985 dtype: gguf_dtype(descriptor.ggml_type),
986 storage: None,
987 })
988 })
989 .collect::<Result<Vec<_>, ArtifactError>>()?;
990 let tensors = TensorCatalog::new(tensors)?;
991 let validated_gguf = ValidatedGguf {
992 checkpoint,
993 companions,
994 };
995 let architecture_plan = resolver.artifact_plan(
996 path,
997 ArtifactFormat::Gguf,
998 &configuration,
999 &tensors,
1000 Some(&validated_gguf),
1001 resolved_plan,
1002 )?;
1003 Ok(ArtifactInspection {
1004 admission_token: ArtifactAdmissionToken::new(),
1005 path: path.to_path_buf(),
1006 format: ArtifactFormat::Gguf,
1007 configuration,
1008 tensors,
1009 safetensors_shards: None,
1010 validated_gguf: Some(validated_gguf),
1011 architecture_plan,
1012 })
1013}
1014
1015pub fn resolve_gguf_companions(
1017 primary: &Path,
1018 requirements: &[GgufCompanionRequirement],
1019) -> Result<BTreeMap<GgufCompanionRole, ValidatedGgufCompanion>, ArtifactError> {
1020 let mut resolved = BTreeMap::new();
1021 let mut declared_roles = BTreeSet::new();
1022 for requirement in requirements {
1023 if !declared_roles.insert(requirement.role.clone()) {
1024 return Err(ArtifactError::InvalidArtifact(format!(
1025 "GGUF companion role {:?} was declared more than once",
1026 requirement.role
1027 )));
1028 }
1029 let mut directories = Vec::new();
1030 let mut directory = primary.parent().unwrap_or_else(|| Path::new("."));
1031 directories.push(directory.to_path_buf());
1032 for _ in 0..requirement.parent_search_depth {
1033 let Some(parent) = directory.parent() else {
1034 break;
1035 };
1036 if parent == directory {
1037 break;
1038 }
1039 directories.push(parent.to_path_buf());
1040 directory = parent;
1041 }
1042 let mut candidates = Vec::new();
1043 for directory in &directories {
1044 let candidate_start = candidates.len();
1045 for entry in std::fs::read_dir(directory)? {
1046 let path = entry?.path();
1047 let name = path
1048 .file_name()
1049 .and_then(|name| name.to_str())
1050 .unwrap_or_default();
1051 if path != primary
1052 && path.is_file()
1053 && name
1054 .get(..requirement.filename_prefix.len())
1055 .is_some_and(|prefix| {
1056 prefix.eq_ignore_ascii_case(&requirement.filename_prefix)
1057 })
1058 && is_gguf(&path)
1059 {
1060 let checkpoint = GgufCheckpoint::open(&path)?;
1061 if checkpoint.physical_tensor_count() == 0 {
1062 return Err(ArtifactError::InvalidArtifact(format!(
1063 "GGUF companion {} contains no tensors",
1064 path.display()
1065 )));
1066 }
1067 let dense = checkpoint.tensors().all(|tensor| {
1068 matches!(
1069 tensor.descriptor().ggml_type,
1070 eredu_gguf::GgmlType::F32
1071 | eredu_gguf::GgmlType::F16
1072 | eredu_gguf::GgmlType::Bf16
1073 )
1074 });
1075 candidates.push((path, checkpoint, dense));
1076 }
1077 }
1078 if candidates.len() != candidate_start {
1079 break;
1080 }
1081 }
1082 candidates.sort_by(|left, right| left.0.cmp(&right.0));
1083 candidates.dedup_by(|left, right| left.0 == right.0);
1084 let dense = candidates
1085 .iter()
1086 .filter(|candidate| candidate.2)
1087 .collect::<Vec<_>>();
1088 let selected = match requirement.encoding {
1089 GgufCompanionEncoding::DenseRequired => match dense.as_slice() {
1090 [candidate] => Some(*candidate),
1091 [] if candidates.is_empty() => None,
1092 [] => {
1093 return Err(ArtifactError::InvalidArtifact(format!(
1094 "GGUF companion {:?} requires dense F32, F16, or BF16 tensors, but all {} matching candidates are quantized",
1095 requirement.role,
1096 candidates.len()
1097 )))
1098 }
1099 _ => return Err(ambiguous_companion(requirement, &directories, dense.len())),
1100 },
1101 GgufCompanionEncoding::DensePreferred => match dense.as_slice() {
1102 [candidate] => Some(*candidate),
1103 [] => match candidates.as_slice() {
1104 [candidate] => Some(candidate),
1105 [] => None,
1106 _ => {
1107 return Err(ambiguous_companion(
1108 requirement,
1109 &directories,
1110 candidates.len(),
1111 ))
1112 }
1113 },
1114 _ => return Err(ambiguous_companion(requirement, &directories, dense.len())),
1115 },
1116 };
1117 match selected {
1118 Some((path, checkpoint, _)) => {
1119 resolved.insert(
1120 requirement.role.clone(),
1121 ValidatedGgufCompanion {
1122 path: path.clone(),
1123 checkpoint: checkpoint.clone(),
1124 },
1125 );
1126 }
1127 None if requirement.required => {
1128 return Err(ArtifactError::MissingRequiredGgufCompanion {
1129 role: requirement.role.clone(),
1130 filename_prefix: requirement.filename_prefix.clone(),
1131 searched_directories: directories,
1132 })
1133 }
1134 None => {}
1135 }
1136 }
1137 Ok(resolved)
1138}
1139
1140fn ambiguous_companion(
1141 requirement: &GgufCompanionRequirement,
1142 directories: &[PathBuf],
1143 candidates: usize,
1144) -> ArtifactError {
1145 ArtifactError::InvalidArtifact(format!(
1146 "GGUF companion {:?} is ambiguous: found {candidates} preferred candidates in {}",
1147 requirement.role,
1148 display_directories(directories)
1149 ))
1150}
1151
1152fn display_directories(directories: &[PathBuf]) -> String {
1153 directories
1154 .iter()
1155 .map(|directory| directory.display().to_string())
1156 .collect::<Vec<_>>()
1157 .join(", ")
1158}
1159
1160fn validate_gguf_container(checkpoint: &GgufCheckpoint) -> Result<(), ArtifactError> {
1161 if checkpoint.physical_tensor_count() == 0 {
1162 return Err(ArtifactError::InvalidArtifact(
1163 "GGUF model checkpoint contains no tensors".into(),
1164 ));
1165 }
1166 Ok(())
1167}
1168
1169fn inspect_safetensors<R: ModelConfigurationResolver>(
1170 path: &Path,
1171 resolver: &R,
1172) -> Result<ArtifactInspection<R::ArtifactPlan>, ArtifactError> {
1173 let config_path = path.join("config.json");
1174 let json: Value = serde_json::from_reader(File::open(&config_path)?)?;
1175 let (configuration, resolved_plan) = resolver.resolve_safetensors(&json)?.into_parts();
1176 let shards = SafetensorsShards::discover(path)?;
1177 let mut descriptors = Vec::new();
1178 let mut names = BTreeSet::new();
1179 for shard in shards.payload_paths() {
1180 for descriptor in inspect_safetensors_header(shard)? {
1181 if !names.insert(descriptor.name.clone()) {
1182 return Err(ArtifactError::DuplicateTensor(descriptor.name));
1183 }
1184 descriptors.push(descriptor);
1185 }
1186 }
1187 let tensors = TensorCatalog::new(descriptors)?;
1188 if tensors.is_empty() {
1189 return Err(ArtifactError::InvalidArtifact(
1190 "SafeTensors checkpoint contains no tensors".into(),
1191 ));
1192 }
1193 let architecture_plan = resolver.artifact_plan(
1194 path,
1195 ArtifactFormat::SafeTensors,
1196 &configuration,
1197 &tensors,
1198 None,
1199 resolved_plan,
1200 )?;
1201 Ok(ArtifactInspection {
1202 admission_token: ArtifactAdmissionToken::new(),
1203 path: path.to_path_buf(),
1204 format: ArtifactFormat::SafeTensors,
1205 configuration,
1206 tensors,
1207 safetensors_shards: Some(shards),
1208 validated_gguf: None,
1209 architecture_plan,
1210 })
1211}
1212
1213#[derive(Deserialize)]
1214struct RawSafetensorInfo {
1215 dtype: String,
1216 shape: Vec<usize>,
1217 data_offsets: [u64; 2],
1218}
1219
1220fn inspect_safetensors_header(path: &Path) -> Result<Vec<TensorDescriptor>, ArtifactError> {
1221 const MAX_HEADER_BYTES: u64 = 100_000_000;
1222 let mut file = File::open(path)?;
1223 let file_len = file.metadata()?.len();
1224 let mut length = [0_u8; 8];
1225 file.read_exact(&mut length)?;
1226 let header_len = u64::from_le_bytes(length);
1227 if header_len > MAX_HEADER_BYTES {
1228 return Err(ArtifactError::InvalidArtifact(format!(
1229 "SafeTensors header in {} exceeds {MAX_HEADER_BYTES} bytes",
1230 path.display()
1231 )));
1232 }
1233 let mut header = vec![
1234 0_u8;
1235 usize::try_from(header_len).map_err(|_| {
1236 ArtifactError::InvalidArtifact("SafeTensors header length overflows usize".into())
1237 })?
1238 ];
1239 file.read_exact(&mut header)?;
1240 let raw: BTreeMap<String, Value> = serde_json::from_slice(&header)?;
1241 let payload_start = 8_u64
1242 .checked_add(header_len)
1243 .ok_or_else(|| ArtifactError::InvalidArtifact("SafeTensors offset overflow".into()))?;
1244 let mut entries = raw
1245 .into_iter()
1246 .filter(|(name, _)| name != "__metadata__")
1247 .map(|(name, value)| {
1248 serde_json::from_value::<RawSafetensorInfo>(value).map(|info| (name, info))
1249 })
1250 .collect::<Result<Vec<_>, _>>()?;
1251 entries.sort_by_key(|(_, info)| info.data_offsets[0]);
1252 let mut output = Vec::with_capacity(entries.len());
1253 let mut expected_offset = 0_u64;
1254 for (name, info) in entries {
1255 if info.shape.contains(&0) {
1258 return Err(ArtifactError::InvalidArtifact(format!(
1259 "SafeTensors tensor {name:?} has an invalid shape"
1260 )));
1261 }
1262 let [start, end] = info.data_offsets;
1263 if start != expected_offset || end < start {
1264 return Err(ArtifactError::InvalidArtifact(format!(
1265 "SafeTensors tensor {name:?} has non-contiguous data offsets"
1266 )));
1267 }
1268 expected_offset = end;
1269 let absolute = payload_start
1270 .checked_add(start)
1271 .ok_or_else(|| ArtifactError::InvalidArtifact("SafeTensors offset overflow".into()))?;
1272 output.push(TensorDescriptor {
1273 name,
1274 shape: info.shape,
1275 dtype: safetensors_dtype(&info.dtype),
1276 storage: Some(TensorStorage {
1277 member: path.display().to_string(),
1278 offset: absolute,
1279 length: end - start,
1280 }),
1281 });
1282 }
1283 if payload_start
1284 .checked_add(expected_offset)
1285 .ok_or_else(|| ArtifactError::InvalidArtifact("SafeTensors length overflow".into()))?
1286 != file_len
1287 {
1288 return Err(ArtifactError::InvalidArtifact(format!(
1289 "SafeTensors payload length does not match header in {}",
1290 path.display()
1291 )));
1292 }
1293 Ok(output)
1294}
1295
1296fn safetensors_dtype(dtype: &str) -> TensorDtype {
1297 match dtype {
1298 "BOOL" => TensorDtype::Bool,
1299 "U8" => TensorDtype::U8,
1300 "I8" => TensorDtype::I8,
1301 "I16" => TensorDtype::I16,
1302 "U16" => TensorDtype::U16,
1303 "F16" => TensorDtype::F16,
1304 "BF16" => TensorDtype::Bf16,
1305 "I32" => TensorDtype::I32,
1306 "U32" => TensorDtype::U32,
1307 "F32" => TensorDtype::F32,
1308 "F64" => TensorDtype::F64,
1309 "I64" => TensorDtype::I64,
1310 "U64" => TensorDtype::U64,
1311 "C64" => TensorDtype::Complex64,
1312 other => TensorDtype::Encoded(other.into()),
1313 }
1314}
1315
1316fn gguf_dtype(dtype: GgmlType) -> TensorDtype {
1317 match dtype {
1318 GgmlType::F32 => TensorDtype::F32,
1319 GgmlType::F16 => TensorDtype::F16,
1320 GgmlType::Bf16 => TensorDtype::Bf16,
1321 GgmlType::I8 => TensorDtype::I8,
1322 GgmlType::I16 => TensorDtype::I16,
1323 GgmlType::I32 => TensorDtype::I32,
1324 GgmlType::I64 => TensorDtype::I64,
1325 GgmlType::F64 => TensorDtype::F64,
1326 encoded => TensorDtype::Encoded(format!("{encoded:?}")),
1327 }
1328}
1329
1330fn is_gguf(path: &Path) -> bool {
1331 path.extension()
1332 .and_then(|extension| extension.to_str())
1333 .is_some_and(|extension| extension.eq_ignore_ascii_case("gguf"))
1334}
1335
1336#[derive(Debug, thiserror::Error)]
1338#[non_exhaustive]
1339pub enum ArtifactError {
1340 #[error("invalid artifact identity: {0}")]
1342 InvalidArtifactIdentity(String),
1343 #[error("model artifact does not exist: {0}")]
1345 MissingArtifact(PathBuf),
1346 #[error("model artifact must be a SafeTensors directory or .gguf file: {0}")]
1348 UnsupportedContainer(PathBuf),
1349 #[error("unsupported model type: {0}")]
1351 UnsupportedModelType(String),
1352 #[error("unsupported GGUF architecture: {0}")]
1354 UnsupportedGgufArchitecture(String),
1355 #[error("GGUF metadata is missing string key \"general.architecture\"")]
1357 MissingGgufArchitecture,
1358 #[error(
1360 "required GGUF companion {role:?} matching {filename_prefix:?} was not found in {searched}",
1361 searched = display_directories(.searched_directories)
1362 )]
1363 MissingRequiredGgufCompanion {
1364 role: GgufCompanionRole,
1366 filename_prefix: String,
1368 searched_directories: Vec<PathBuf>,
1370 },
1371 #[error("invalid model artifact: {0}")]
1373 InvalidArtifact(String),
1374 #[error("invalid architecture artifact plan: {0}")]
1376 InvalidArchitecturePlan(String),
1377 #[error("duplicate checkpoint tensor {0:?}")]
1379 DuplicateTensor(String),
1380 #[error(transparent)]
1382 SafetensorsShards(#[from] eredu_checkpoint::safetensors::SafetensorsShardError),
1383 #[error(transparent)]
1385 ArtifactFingerprint(#[from] ArtifactFingerprintError),
1386 #[error(transparent)]
1388 CheckpointStore(#[from] eredu_checkpoint::store::StoreError),
1389 #[error("unsupported model quantization policy: {0}")]
1391 UnsupportedQuantizationPolicy(String),
1392 #[error("unsupported model residency policy: {0}")]
1394 UnsupportedResidencyPolicy(String),
1395 #[error("model artifact requires the {0:?} loading protocol")]
1397 UnsupportedLoadingProtocol(LoadingProtocol),
1398 #[error(transparent)]
1400 Io(#[from] std::io::Error),
1401 #[error(transparent)]
1403 Json(#[from] serde_json::Error),
1404 #[error(transparent)]
1406 Gguf(#[from] eredu_gguf::Error),
1407 #[error(transparent)]
1409 Catalog(#[from] crate::checkpoint::CatalogError),
1410}
1411
1412#[cfg(test)]
1413mod tests {
1414 use super::*;
1415 use eredu_gguf::{GgmlType, MetadataArray, TensorInput, Writer};
1416 use std::io::Write;
1417
1418 #[test]
1419 fn artifact_identity_rejects_empty_and_duplicate_layouts() {
1420 assert!(matches!(
1421 fingerprint_artifact("", [ArtifactMemberIdentity::new("weights", 7, [1; 32])]),
1422 Err(ArtifactError::InvalidArtifactIdentity(_))
1423 ));
1424 assert!(matches!(
1425 fingerprint_artifact("model", std::iter::empty::<ArtifactMemberIdentity>()),
1426 Err(ArtifactError::InvalidArtifactIdentity(_))
1427 ));
1428 assert!(matches!(
1429 fingerprint_artifact("model", [ArtifactMemberIdentity::new("", 7, [1; 32])]),
1430 Err(ArtifactError::InvalidArtifactIdentity(_))
1431 ));
1432 assert!(matches!(
1433 fingerprint_artifact(
1434 "model",
1435 [
1436 ArtifactMemberIdentity::new("weights", 7, [1; 32]),
1437 ArtifactMemberIdentity::new("weights", 7, [1; 32]),
1438 ],
1439 ),
1440 Err(ArtifactError::InvalidArtifactIdentity(_))
1441 ));
1442 }
1443
1444 #[test]
1445 fn artifact_identity_is_order_and_location_independent_but_domain_and_layout_exact() {
1446 let first = tempfile::tempdir().unwrap();
1447 let relocated = tempfile::tempdir().unwrap();
1448 for directory in [first.path(), relocated.path()] {
1449 std::fs::write(directory.join("a"), b"first").unwrap();
1450 std::fs::write(directory.join("b"), b"second").unwrap();
1451 }
1452 let fingerprint = |root: &Path, domain: &str, reversed: bool| {
1453 let mut files = vec![
1454 ArtifactFile::new("decoder", root.join("a")),
1455 ArtifactFile::new("projector", root.join("b")),
1456 ];
1457 if reversed {
1458 files.reverse();
1459 }
1460 fingerprint_filesystem_artifact(domain, files).unwrap()
1461 };
1462 let identity = fingerprint(first.path(), "model", false);
1463 assert_eq!(identity, fingerprint(first.path(), "model", true));
1464 assert_eq!(identity, fingerprint(relocated.path(), "model", false));
1465 assert_ne!(identity, fingerprint(first.path(), "assistant", false));
1466 assert_ne!(
1467 identity,
1468 fingerprint_filesystem_artifact(
1469 "model",
1470 [
1471 ArtifactFile::new("decoder-renamed", first.path().join("a")),
1472 ArtifactFile::new("projector", first.path().join("b")),
1473 ],
1474 )
1475 .unwrap()
1476 );
1477 assert_eq!(format!("{identity:?}"), identity.to_string());
1478 assert_eq!(identity.to_string().len(), 71);
1479 }
1480
1481 #[test]
1482 fn artifact_identity_includes_exact_content_and_length() {
1483 let directory = tempfile::tempdir().unwrap();
1484 let path = directory.path().join("weights");
1485 let identify = || {
1486 fingerprint_filesystem_artifact("model", [ArtifactFile::new("weights", &path)]).unwrap()
1487 };
1488 std::fs::write(&path, b"a").unwrap();
1489 let short = identify();
1490 std::fs::write(&path, b"b").unwrap();
1491 assert_ne!(short, identify());
1492 std::fs::write(&path, b"a\0").unwrap();
1493 assert_ne!(short, identify());
1494 }
1495
1496 #[test]
1497 fn admitted_safetensors_identity_is_relocation_independent_and_content_exact() {
1498 let first = tempfile::tempdir().unwrap();
1499 let relocated = tempfile::tempdir().unwrap();
1500 write_safetensors_fixture(first.path(), "llama");
1501 write_safetensors_fixture(relocated.path(), "llama");
1502 let first_path = first.path().join("original-name.safetensors");
1503 let relocated_path = relocated.path().join("renamed.safetensors");
1504 std::fs::rename(first.path().join("model.safetensors"), &first_path).unwrap();
1505 std::fs::rename(relocated.path().join("model.safetensors"), &relocated_path).unwrap();
1506 let first_shards = SafetensorsShards::discover(&first_path).unwrap();
1507 let relocated_shards = SafetensorsShards::discover(&relocated_path).unwrap();
1508 let identify = |shards: &SafetensorsShards| {
1509 fingerprint_safetensors_artifact("speculative-test", shards).unwrap()
1510 };
1511 let original = identify(&first_shards);
1512 assert_eq!(original, identify(&relocated_shards));
1513
1514 let payload = relocated_path;
1515 let mut bytes = std::fs::read(&payload).unwrap();
1516 let last = bytes.last_mut().unwrap();
1517 *last ^= 0x01;
1518 std::fs::write(&payload, bytes).unwrap();
1519 assert_ne!(original, identify(&relocated_shards));
1520 }
1521
1522 #[test]
1523 fn admitted_gguf_identity_uses_split_roles_and_exact_content() {
1524 let first = tempfile::tempdir().unwrap();
1525 let relocated = tempfile::tempdir().unwrap();
1526 let write = |path: &Path| {
1527 let data = 1.0_f32.to_le_bytes();
1528 Writer::default()
1529 .write(
1530 File::create(path).unwrap(),
1531 &BTreeMap::from([(
1532 "general.architecture".into(),
1533 MetadataValue::String("llama".into()),
1534 )]),
1535 &[TensorInput {
1536 name: "weight",
1537 dimensions: &[1],
1538 ggml_type: GgmlType::F32,
1539 data: &data,
1540 }],
1541 )
1542 .unwrap();
1543 };
1544 let first_path = first.path().join("first-name.gguf");
1545 let relocated_path = relocated.path().join("different-name.gguf");
1546 write(&first_path);
1547 write(&relocated_path);
1548 let first_checkpoint = GgufCheckpoint::open(&first_path).unwrap();
1549 let relocated_checkpoint = GgufCheckpoint::open(&relocated_path).unwrap();
1550 let original = fingerprint_gguf_artifact("speculative-test", &first_checkpoint).unwrap();
1551 assert_eq!(
1552 original,
1553 fingerprint_gguf_artifact("speculative-test", &relocated_checkpoint).unwrap()
1554 );
1555
1556 let mut bytes = std::fs::read(&relocated_path).unwrap();
1557 *bytes.last_mut().unwrap() ^= 0x01;
1558 std::fs::write(&relocated_path, bytes).unwrap();
1559 assert_ne!(
1560 original,
1561 fingerprint_gguf_artifact("speculative-test", &relocated_checkpoint).unwrap()
1562 );
1563 }
1564
1565 #[test]
1566 fn canonical_prepared_safetensors_open_rejects_substitution_without_payload_reads() {
1567 use eredu_checkpoint::{
1568 schema::{
1569 CatalogPolicy, SafetensorsCheckpointPlan, SafetensorsTensorConstraint,
1570 StoredDtypeConstraint,
1571 },
1572 store::SafetensorsWeightStore,
1573 };
1574
1575 let directory = tempfile::tempdir().unwrap();
1576 write_safetensors_fixture(directory.path(), "llama");
1577 let inspection = inspect_artifact(directory.path(), &FixtureResolver).unwrap();
1578 let shards = inspection.safetensors_shards().unwrap().clone();
1579 let plan = SafetensorsCheckpointPlan::new(
1580 "fixture",
1581 vec![SafetensorsTensorConstraint::required(
1582 "token_embd.weight",
1583 vec![2, 2],
1584 StoredDtypeConstraint::Exact(StoredDtype::F32),
1585 )],
1586 Vec::new(),
1587 CatalogPolicy::strict(),
1588 )
1589 .unwrap();
1590 let store = SafetensorsWeightStore::open_admitted(shards.clone(), 1).unwrap();
1591 let resolution =
1592 eredu_checkpoint::validation::resolve_safetensors_plan(&store, &plan).unwrap();
1593 let prepared = open_prepared_safetensors_artifact(
1594 inspection.tensors(),
1595 shards.clone(),
1596 resolution.clone(),
1597 1,
1598 )
1599 .unwrap();
1600 assert_eq!(prepared.source_diagnostics().unwrap().physical_reads, 0);
1601
1602 let header = br#"{"token_embd.weight":{"dtype":"F32","shape":[4],"data_offsets":[0,16]}}"#;
1603 let mut file = File::create(directory.path().join("model.safetensors")).unwrap();
1604 file.write_all(&(header.len() as u64).to_le_bytes())
1605 .unwrap();
1606 file.write_all(header).unwrap();
1607 file.write_all(&[0_u8; 16]).unwrap();
1608 drop(file);
1609 assert!(matches!(
1610 open_prepared_safetensors_artifact(inspection.tensors(), shards, resolution, 1,),
1611 Err(ArtifactError::CheckpointStore(
1612 eredu_checkpoint::store::StoreError::PreparedCatalogMismatch { .. }
1613 ))
1614 ));
1615 }
1616
1617 struct FixtureResolver;
1618
1619 #[derive(Debug, Clone, Default, Eq, PartialEq)]
1620 struct FixtureArtifactPlan {
1621 format: Option<ArtifactFormat>,
1622 }
1623
1624 impl ModelConfigurationResolver for FixtureResolver {
1625 type ArtifactPlan = FixtureArtifactPlan;
1626
1627 fn resolve_safetensors(
1628 &self,
1629 json: &Value,
1630 ) -> Result<ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
1631 let model_type = json
1632 .get("model_type")
1633 .and_then(Value::as_str)
1634 .ok_or_else(|| ArtifactError::InvalidArtifact("missing model_type".into()))?;
1635 let family = match model_type {
1636 "llama" => "llama",
1637 "gemma4" => "gemma4",
1638 "future" => "future_family",
1639 other => return Err(ArtifactError::UnsupportedModelType(other.into())),
1640 };
1641 Ok(ResolvedModelConfiguration::new(
1642 ModelConfiguration {
1643 declared_model_type: model_type.into(),
1644 effective_model_type: model_type.into(),
1645 family: family.into(),
1646 loading_protocol: LoadingProtocol::Model,
1647 json: Some(json.clone()),
1648 },
1649 FixtureArtifactPlan::default(),
1650 ))
1651 }
1652
1653 fn resolve_gguf(
1654 &self,
1655 architecture: &str,
1656 _checkpoint: &GgufCheckpoint,
1657 ) -> Result<ResolvedModelConfiguration<Self::ArtifactPlan>, ArtifactError> {
1658 let family = match architecture {
1659 "llama" => "llama",
1660 "future" => "future_family",
1661 other => return Err(ArtifactError::UnsupportedGgufArchitecture(other.into())),
1662 };
1663 Ok(ResolvedModelConfiguration::new(
1664 ModelConfiguration {
1665 declared_model_type: architecture.into(),
1666 effective_model_type: architecture.into(),
1667 family: family.into(),
1668 loading_protocol: LoadingProtocol::Model,
1669 json: None,
1670 },
1671 FixtureArtifactPlan::default(),
1672 ))
1673 }
1674
1675 fn gguf_companion_requirements(
1676 &self,
1677 architecture: &str,
1678 _checkpoint: &GgufCheckpoint,
1679 ) -> Result<Vec<GgufCompanionRequirement>, ArtifactError> {
1680 if architecture == "future" {
1681 return Ok(vec![GgufCompanionRequirement::new(
1682 GgufCompanionRole::MediaProjector,
1683 false,
1684 "mmproj",
1685 0,
1686 GgufCompanionEncoding::DensePreferred,
1687 )?]);
1688 }
1689 Ok(Vec::new())
1690 }
1691
1692 fn artifact_plan(
1693 &self,
1694 _path: &Path,
1695 format: ArtifactFormat,
1696 _configuration: &ModelConfiguration,
1697 _tensors: &TensorCatalog,
1698 _validated_gguf: Option<&ValidatedGguf>,
1699 _resolved_plan: Self::ArtifactPlan,
1700 ) -> Result<Self::ArtifactPlan, ArtifactError> {
1701 Ok(FixtureArtifactPlan {
1702 format: Some(format),
1703 })
1704 }
1705 }
1706
1707 fn write_safetensors_fixture(root: &Path, model_type: &str) {
1708 std::fs::write(
1709 root.join("config.json"),
1710 format!(r#"{{"model_type":"{model_type}"}}"#),
1711 )
1712 .unwrap();
1713 let header =
1714 br#"{"token_embd.weight":{"dtype":"F32","shape":[2,2],"data_offsets":[0,16]}}"#;
1715 let mut file = File::create(root.join("model.safetensors")).unwrap();
1716 file.write_all(&(header.len() as u64).to_le_bytes())
1717 .unwrap();
1718 file.write_all(header).unwrap();
1719 file.write_all(&[0_u8; 16]).unwrap();
1720 }
1721
1722 fn write_gguf_fixture(path: &Path, ggml_type: GgmlType) {
1723 let metadata = BTreeMap::from([(
1724 "general.architecture".into(),
1725 MetadataValue::String("clip".into()),
1726 )]);
1727 let (dimensions, data) = match ggml_type {
1728 GgmlType::F32 => (vec![1], 1.0_f32.to_le_bytes().to_vec()),
1729 GgmlType::Q8_0 => (vec![32], vec![0_u8; 34]),
1730 other => panic!("unsupported fixture encoding {other:?}"),
1731 };
1732 Writer::default()
1733 .write(
1734 File::create(path).unwrap(),
1735 &metadata,
1736 &[TensorInput {
1737 name: "projector.weight",
1738 dimensions: &dimensions,
1739 ggml_type,
1740 data: &data,
1741 }],
1742 )
1743 .unwrap();
1744 }
1745
1746 #[test]
1747 fn companion_planning_selects_by_catalog_encoding_not_filename() {
1748 let root = tempfile::tempdir().unwrap();
1749 let primary = root.path().join("model.gguf");
1750 write_gguf_fixture(&primary, GgmlType::F32);
1751 let quantized_name = root.path().join("mmproj-f16.gguf");
1752 let dense_name = root.path().join("mmproj-q4_k.gguf");
1753 write_gguf_fixture(&quantized_name, GgmlType::Q8_0);
1754 write_gguf_fixture(&dense_name, GgmlType::F32);
1755 let requirement = GgufCompanionRequirement::new(
1756 GgufCompanionRole::MediaProjector,
1757 true,
1758 "mmproj",
1759 1,
1760 GgufCompanionEncoding::DensePreferred,
1761 )
1762 .unwrap();
1763
1764 let companions = resolve_gguf_companions(&primary, &[requirement]).unwrap();
1765
1766 assert_eq!(
1767 companions
1768 .get(&GgufCompanionRole::MediaProjector)
1769 .unwrap()
1770 .path(),
1771 dense_name
1772 );
1773 }
1774
1775 #[test]
1776 fn dense_only_and_required_companion_policies_fail_closed() {
1777 let root = tempfile::tempdir().unwrap();
1778 let primary = root.path().join("model.gguf");
1779 write_gguf_fixture(&primary, GgmlType::F32);
1780 write_gguf_fixture(&root.path().join("mmproj.gguf"), GgmlType::Q8_0);
1781 let optional = GgufCompanionRequirement::new(
1782 GgufCompanionRole::MediaProjector,
1783 false,
1784 "mmproj",
1785 0,
1786 GgufCompanionEncoding::DenseRequired,
1787 )
1788 .unwrap();
1789 assert!(resolve_gguf_companions(&primary, &[optional]).is_err());
1790 let required = GgufCompanionRequirement::new(
1791 GgufCompanionRole::MediaProjector,
1792 true,
1793 "mmproj",
1794 0,
1795 GgufCompanionEncoding::DenseRequired,
1796 )
1797 .unwrap();
1798 assert!(resolve_gguf_companions(&primary, &[required]).is_err());
1799 }
1800
1801 #[test]
1802 fn missing_required_companion_preserves_its_semantic_role() {
1803 let root = tempfile::tempdir().unwrap();
1804 let primary = root.path().join("model.gguf");
1805 write_gguf_fixture(&primary, GgmlType::F32);
1806 let requirement = GgufCompanionRequirement::new(
1807 GgufCompanionRole::MediaProjector,
1808 true,
1809 "mmproj",
1810 0,
1811 GgufCompanionEncoding::DensePreferred,
1812 )
1813 .unwrap();
1814
1815 let error = resolve_gguf_companions(&primary, &[requirement]).unwrap_err();
1816
1817 assert!(matches!(
1818 error,
1819 ArtifactError::MissingRequiredGgufCompanion {
1820 role: GgufCompanionRole::MediaProjector,
1821 filename_prefix,
1822 searched_directories,
1823 } if filename_prefix == "mmproj" && searched_directories == [root.path()]
1824 ));
1825 }
1826
1827 #[test]
1828 fn loading_protocol_is_family_agnostic() {
1829 assert!(matches!(
1830 validate_preparation_policy(LoadingProtocol::Realtime, PreparationPolicy::default()),
1831 Err(ArtifactError::UnsupportedLoadingProtocol(
1832 LoadingProtocol::Realtime
1833 ))
1834 ));
1835 }
1836
1837 #[test]
1838 fn gguf_u32_metadata_is_lossless_and_fail_closed() {
1839 let values = MetadataValue::Array(MetadataArray::Uint64(vec![0, u32::MAX.into()]));
1840 assert_eq!(
1841 gguf_u32_metadata_values("tokenizer.ids", Some(&values)).unwrap(),
1842 vec![0, u32::MAX]
1843 );
1844 assert!(gguf_u32_metadata_values(
1845 "tokenizer.ids",
1846 Some(&MetadataValue::Uint64(u64::from(u32::MAX) + 1))
1847 )
1848 .is_err());
1849 assert!(
1850 gguf_u32_metadata_values("tokenizer.ids", Some(&MetadataValue::Int32(-1))).is_err()
1851 );
1852 assert!(gguf_u32_metadata_values(
1853 "tokenizer.ids",
1854 Some(&MetadataValue::String("1".into()))
1855 )
1856 .is_err());
1857 assert!(gguf_u32_metadata_values("tokenizer.ids", None)
1858 .unwrap()
1859 .is_empty());
1860 }
1861
1862 #[test]
1863 fn safetensors_inspection_and_planning_are_backend_neutral() {
1864 let root = tempfile::tempdir().unwrap();
1865 write_safetensors_fixture(root.path(), "llama");
1866 let inspection = inspect_artifact(root.path(), &FixtureResolver).unwrap();
1867 assert_eq!(inspection.configuration().family, "llama");
1868 assert_eq!(inspection.tensors().len(), 1);
1869 assert_eq!(
1870 inspection
1871 .safetensors_shards()
1872 .unwrap()
1873 .payload_paths()
1874 .len(),
1875 1
1876 );
1877 let plan = plan_model_preparation(
1878 inspection,
1879 PreparationPolicy::default(),
1880 crate::backend::SessionCapabilities::default(),
1881 )
1882 .unwrap();
1883 assert_eq!(plan.route(), MaterializationRoute::Resident);
1884 let architecture_plan = plan.inspection().architecture_plan().clone();
1885 let artifact = plan.into_artifact();
1886 let ModelArtifact::SafeTensors { shards, .. } = artifact else {
1887 panic!("expected SafeTensors artifact");
1888 };
1889 assert_eq!(shards.payload_paths().len(), 1);
1890 assert_eq!(architecture_plan.format, Some(ArtifactFormat::SafeTensors));
1891 }
1892
1893 #[test]
1894 fn safetensors_inspection_rejects_index_entries_missing_from_their_shard() {
1895 let root = tempfile::tempdir().unwrap();
1896 write_safetensors_fixture(root.path(), "llama");
1897 std::fs::rename(
1898 root.path().join("model.safetensors"),
1899 root.path().join("model-00001.safetensors"),
1900 )
1901 .unwrap();
1902 std::fs::write(
1903 root.path().join("model.safetensors.index.json"),
1904 r#"{"weight_map":{"missing.weight":"model-00001.safetensors"}}"#,
1905 )
1906 .unwrap();
1907
1908 assert!(matches!(
1909 inspect_artifact(root.path(), &FixtureResolver),
1910 Err(ArtifactError::SafetensorsShards(
1911 eredu_checkpoint::safetensors::SafetensorsShardError::MalformedIndex { .. }
1912 ))
1913 ));
1914 }
1915
1916 #[cfg(unix)]
1917 #[test]
1918 fn safetensors_inspection_rejects_indexed_symlinks_outside_the_access_root() {
1919 use std::os::unix::fs::symlink;
1920
1921 let parent = tempfile::tempdir().unwrap();
1922 let outside = parent.path().join("outside");
1923 std::fs::create_dir(&outside).unwrap();
1924 write_safetensors_fixture(&outside, "llama");
1925
1926 let checkpoint = parent.path().join("checkpoint");
1927 std::fs::create_dir(&checkpoint).unwrap();
1928 std::fs::write(checkpoint.join("config.json"), r#"{"model_type":"llama"}"#).unwrap();
1929 symlink(
1930 outside.join("model.safetensors"),
1931 checkpoint.join("model-00001.safetensors"),
1932 )
1933 .unwrap();
1934 std::fs::write(
1935 checkpoint.join("model.safetensors.index.json"),
1936 r#"{"weight_map":{"token_embd.weight":"model-00001.safetensors"}}"#,
1937 )
1938 .unwrap();
1939
1940 assert!(matches!(
1941 inspect_artifact(&checkpoint, &FixtureResolver),
1942 Err(ArtifactError::SafetensorsShards(
1943 eredu_checkpoint::safetensors::SafetensorsShardError::UnsafeShardPath { .. }
1944 ))
1945 ));
1946 }
1947
1948 #[test]
1949 fn safetensors_native_dtypes_remain_typed_in_the_portable_catalog() {
1950 assert_eq!(safetensors_dtype("BOOL"), TensorDtype::Bool);
1951 assert_eq!(safetensors_dtype("I64"), TensorDtype::I64);
1952 assert_eq!(safetensors_dtype("U32"), TensorDtype::U32);
1953 assert_eq!(safetensors_dtype("F64"), TensorDtype::F64);
1954 assert_eq!(safetensors_dtype("C64"), TensorDtype::Complex64);
1955 assert_eq!(
1956 safetensors_dtype("F8_E4M3"),
1957 TensorDtype::Encoded("F8_E4M3".into())
1958 );
1959 }
1960
1961 #[test]
1962 fn gguf_dense_dtypes_remain_typed_in_the_portable_catalog() {
1963 assert_eq!(gguf_dtype(GgmlType::F16), TensorDtype::F16);
1964 assert_eq!(gguf_dtype(GgmlType::Bf16), TensorDtype::Bf16);
1965 assert_eq!(gguf_dtype(GgmlType::F32), TensorDtype::F32);
1966 assert_eq!(
1967 gguf_dtype(GgmlType::Q4K),
1968 TensorDtype::Encoded("Q4K".into())
1969 );
1970 }
1971
1972 #[test]
1973 fn core_accepts_families_defined_only_by_the_resolver() {
1974 let root = tempfile::tempdir().unwrap();
1975 write_safetensors_fixture(root.path(), "future");
1976 let inspection = inspect_artifact(root.path(), &FixtureResolver).unwrap();
1977 assert_eq!(inspection.configuration().family, "future_family");
1978 assert_eq!(
1979 inspection.configuration().loading_protocol,
1980 LoadingProtocol::Model
1981 );
1982 assert!(plan_model_preparation(
1983 inspection,
1984 PreparationPolicy::default(),
1985 crate::backend::SessionCapabilities::default(),
1986 )
1987 .is_ok());
1988 }
1989
1990 #[test]
1991 fn safetensors_inspection_accepts_rank_zero_scalar_parameters() {
1992 let root = tempfile::tempdir().unwrap();
1993 std::fs::write(
1994 root.path().join("config.json"),
1995 r#"{"model_type":"gemma4"}"#,
1996 )
1997 .unwrap();
1998 let header = br#"{"clip.output_max":{"dtype":"F32","shape":[],"data_offsets":[0,4]}}"#;
1999 let mut file = File::create(root.path().join("model.safetensors")).unwrap();
2000 file.write_all(&(header.len() as u64).to_le_bytes())
2001 .unwrap();
2002 file.write_all(header).unwrap();
2003 file.write_all(&0.0_f32.to_le_bytes()).unwrap();
2004
2005 let inspection = inspect_artifact(root.path(), &FixtureResolver).unwrap();
2006 assert_eq!(
2007 inspection.tensors().get("clip.output_max").unwrap().shape,
2008 Vec::<usize>::new()
2009 );
2010 }
2011
2012 #[test]
2013 fn parallel_policy_binds_the_exact_neutral_topology() {
2014 let root = tempfile::tempdir().unwrap();
2015 write_safetensors_fixture(root.path(), "llama");
2016 let topology = crate::topology::ParallelTopology::new(2, 3, 4, 1).unwrap();
2017 let policy = PreparationPolicy {
2018 topology: Some(topology),
2019 ..PreparationPolicy::default()
2020 };
2021
2022 let plan = plan_model_preparation(
2023 inspect_artifact(root.path(), &FixtureResolver).unwrap(),
2024 policy,
2025 crate::backend::SessionCapabilities::default(),
2026 )
2027 .unwrap();
2028
2029 assert_eq!(plan.policy(), policy);
2030 assert_eq!(plan.policy().topology, Some(topology));
2031 assert_eq!(plan.route(), MaterializationRoute::Resident);
2032 }
2033
2034 #[test]
2035 fn policy_leaves_expert_cache_capability_to_architecture_and_backend() {
2036 let root = tempfile::tempdir().unwrap();
2037 write_safetensors_fixture(root.path(), "llama");
2038 let plan = plan_model_preparation(
2039 inspect_artifact(root.path(), &FixtureResolver).unwrap(),
2040 PreparationPolicy {
2041 residency: ResidencyRequest::AddressableParameterBanks,
2042 ..PreparationPolicy::default()
2043 },
2044 crate::backend::SessionCapabilities::default(),
2045 )
2046 .unwrap();
2047 assert_eq!(
2048 plan.route(),
2049 MaterializationRoute::AddressableParameterBanks
2050 );
2051 }
2052
2053 #[test]
2054 fn policy_leaves_nonresident_quantization_capability_to_architecture_and_backend() {
2055 let root = tempfile::tempdir().unwrap();
2056 write_safetensors_fixture(root.path(), "llama");
2057 let policy = PreparationPolicy {
2058 quantization: Some(QuantizationRequest::MxFp4),
2059 residency: ResidencyRequest::LayerwiseHost,
2060 ..PreparationPolicy::default()
2061 };
2062 let plan = plan_model_preparation(
2063 inspect_artifact(root.path(), &FixtureResolver).unwrap(),
2064 policy,
2065 crate::backend::SessionCapabilities::default(),
2066 )
2067 .unwrap();
2068 assert_eq!(plan.policy(), policy);
2069 assert_eq!(plan.route(), MaterializationRoute::Layerwise);
2070 }
2071
2072 #[test]
2073 fn gguf_plan_owns_the_portable_checkpoint_for_later_materialization() {
2074 let root = tempfile::tempdir().unwrap();
2075 let path = root.path().join("model.gguf");
2076 let data = [0_u8; 2];
2077 let metadata = BTreeMap::from([
2078 (
2079 "general.architecture".into(),
2080 MetadataValue::String("llama".into()),
2081 ),
2082 ("llama.block_count".into(), MetadataValue::Uint32(1)),
2083 ("llama.embedding_length".into(), MetadataValue::Uint32(1)),
2084 ]);
2085 Writer::default()
2086 .write(
2087 File::create(&path).unwrap(),
2088 &metadata,
2089 &[TensorInput {
2090 name: "token_embd.weight",
2091 dimensions: &[1],
2092 ggml_type: GgmlType::F16,
2093 data: &data,
2094 }],
2095 )
2096 .unwrap();
2097
2098 let inspection = inspect_artifact(&path, &FixtureResolver).unwrap();
2099 let validated = inspection.validated_gguf().unwrap();
2100 assert_eq!(validated.checkpoint().physical_tensor_count(), 1);
2101 assert_eq!(inspection.configuration().declared_model_type, "llama");
2102 assert_eq!(
2103 inspection.tensors().get("token_embd.weight").unwrap().dtype,
2104 TensorDtype::F16
2105 );
2106
2107 let plan = plan_model_preparation(
2108 inspection,
2109 PreparationPolicy::default(),
2110 crate::backend::SessionCapabilities::default(),
2111 )
2112 .unwrap();
2113 let architecture_plan = plan.inspection().architecture_plan().clone();
2114 let artifact = plan.into_artifact();
2115 let ModelArtifact::Gguf {
2116 configuration,
2117 validated,
2118 ..
2119 } = artifact
2120 else {
2121 panic!("expected GGUF artifact");
2122 };
2123 assert_eq!(architecture_plan.format, Some(ArtifactFormat::Gguf));
2124 assert_eq!(configuration.family, "llama");
2125 assert_eq!(validated.checkpoint().physical_tensor_count(), 1);
2126 }
2127
2128 #[test]
2129 fn core_accepts_architecture_owned_gguf_schema() {
2130 let root = tempfile::tempdir().unwrap();
2131 let path = root.path().join("model.gguf");
2132 let data = 1.0_f32.to_le_bytes();
2133 let metadata = BTreeMap::from([
2134 (
2135 "general.architecture".into(),
2136 MetadataValue::String("future".into()),
2137 ),
2138 ("future.state_width".into(), MetadataValue::Uint32(1)),
2139 ]);
2140 Writer::default()
2141 .write(
2142 File::create(&path).unwrap(),
2143 &metadata,
2144 &[TensorInput {
2145 name: "state.in_proj",
2146 dimensions: &[1],
2147 ggml_type: GgmlType::F32,
2148 data: &data,
2149 }],
2150 )
2151 .unwrap();
2152
2153 let inspection = inspect_artifact(&path, &FixtureResolver).unwrap();
2154
2155 assert_eq!(inspection.configuration().family, "future_family");
2156 assert!(inspection.tensors().get("state.in_proj").is_some());
2157 }
2158
2159 #[test]
2160 fn preparation_plan_carries_the_exact_inspected_companion() {
2161 let root = tempfile::tempdir().unwrap();
2162 let primary = root.path().join("model.gguf");
2163 let scalar = 1.0_f32.to_le_bytes();
2164 Writer::default()
2165 .write(
2166 File::create(&primary).unwrap(),
2167 &BTreeMap::from([(
2168 "general.architecture".into(),
2169 MetadataValue::String("future".into()),
2170 )]),
2171 &[TensorInput {
2172 name: "state.in_proj",
2173 dimensions: &[1],
2174 ggml_type: GgmlType::F32,
2175 data: &scalar,
2176 }],
2177 )
2178 .unwrap();
2179 let projector = root.path().join("mmproj.gguf");
2180 write_gguf_fixture(&projector, GgmlType::F32);
2181
2182 let inspection = inspect_artifact(&primary, &FixtureResolver).unwrap();
2183 assert_eq!(
2184 inspection
2185 .validated_gguf()
2186 .unwrap()
2187 .companion(&GgufCompanionRole::MediaProjector)
2188 .unwrap()
2189 .path(),
2190 projector
2191 );
2192 let ModelArtifact::Gguf { validated, .. } = plan_model_preparation(
2193 inspection,
2194 PreparationPolicy::default(),
2195 crate::backend::SessionCapabilities::default(),
2196 )
2197 .unwrap()
2198 .into_artifact() else {
2199 panic!("expected GGUF preparation artifact");
2200 };
2201 assert_eq!(
2202 validated
2203 .companion(&GgufCompanionRole::MediaProjector)
2204 .unwrap()
2205 .path(),
2206 projector
2207 );
2208 }
2209}