spark_runtime/weights/gguf/
shards.rs1use anyhow::{Context, Result, bail};
6use std::path::{Path, PathBuf};
7
8use super::sidecar;
9
10pub struct GgufShardSet {
17 pub paths: Vec<PathBuf>,
19 pub total_tensors: usize,
21}
22
23pub fn find_gguf_shards(first: &Path) -> Result<GgufShardSet> {
42 let (_f, _m, g) = sidecar::open_gguf(first)?;
43 let count = match g.get_u64("split.count") {
44 None | Some(0) | Some(1) => {
45 return Ok(GgufShardSet {
46 paths: vec![first.to_path_buf()],
47 total_tensors: g.tensors.len(),
48 });
49 }
50 Some(c) => c as usize,
51 };
52 let no = g.get_u64("split.no").unwrap_or(0) as usize;
53 if no != 0 {
54 bail!(
55 "GGUF split: {} reports split.no={no}, expected shard 0. \
56 Point the loader at the `-00001-of-*` shard.",
57 first.display()
58 );
59 }
60 let total_tensors = g
61 .get_u64("split.tensors.count")
62 .context("GGUF split: shard 0 has 'split.count' but no 'split.tensors.count'")?
63 as usize;
64
65 let name = first
66 .file_name()
67 .and_then(|n| n.to_str())
68 .context("GGUF split: shard path has no filename")?;
69 let marker = format!("-00001-of-{count:05}.gguf");
72 let stem = name.strip_suffix(&marker).with_context(|| {
73 format!(
74 "GGUF split: '{name}' declares split.count={count} but does not end in '{marker}'; \
75 cannot derive sibling shard names"
76 )
77 })?;
78 let dir = first.parent().unwrap_or_else(|| Path::new("."));
79
80 let mut paths = vec![first.to_path_buf()];
81 let mut seen = vec![0usize; count];
82 seen[0] = g.tensors.len();
83 for (i, slot) in seen.iter_mut().enumerate().skip(1) {
84 let p = dir.join(format!("{stem}-{:05}-of-{count:05}.gguf", i + 1));
85 if !p.exists() {
86 bail!(
87 "GGUF split: shard {} of {count} missing: {}",
88 i + 1,
89 p.display()
90 );
91 }
92 let (_f2, _m2, g2) = sidecar::open_gguf(&p)
93 .with_context(|| format!("GGUF split: failed to open shard {}", p.display()))?;
94 let c2 = g2.get_u64("split.count").unwrap_or(0) as usize;
95 if c2 != count {
96 bail!(
97 "GGUF split: {} reports split.count={c2}, shard 0 says {count}",
98 p.display()
99 );
100 }
101 let n2 = g2.get_u64("split.no").unwrap_or(usize::MAX as u64) as usize;
102 if n2 != i {
103 bail!(
104 "GGUF split: {} reports split.no={n2}, expected {i} from its filename",
105 p.display()
106 );
107 }
108 if let Some(t2) = g2.get_u64("split.tensors.count")
109 && t2 as usize != total_tensors
110 {
111 bail!(
112 "GGUF split: {} reports split.tensors.count={t2}, shard 0 says {total_tensors}",
113 p.display()
114 );
115 }
116 *slot = g2.tensors.len();
117 paths.push(p);
118 }
119
120 let summed: usize = seen.iter().sum();
121 if summed != total_tensors {
122 bail!(
123 "GGUF split: shards hold {summed} tensors but 'split.tensors.count' is \
124 {total_tensors} — the file set is incomplete or mismatched (per-shard: {seen:?})"
125 );
126 }
127 Ok(GgufShardSet {
128 paths,
129 total_tensors,
130 })
131}