1use std::collections::BTreeMap;
2use std::fs;
3use std::io::{Cursor, Read};
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result, anyhow, ensure};
7
8type ByteReader<'a> = Cursor<&'a [u8]>;
9
10const GGUF_MAGIC: &[u8; 4] = b"GGUF";
11const GGUF_ALIGNMENT_DEFAULT: u64 = 32;
12const GGML_TYPE_F32: u32 = 0;
13const TENSOR_SUMS_SUFFIX: &str = ".in_sum2";
14const TENSOR_COUNTS_SUFFIX: &str = ".counts";
15
16const GGUF_TYPE_UINT32: u32 = 4;
17const GGUF_TYPE_FLOAT32: u32 = 6;
18const GGUF_TYPE_BOOL: u32 = 7;
19const GGUF_TYPE_STRING: u32 = 8;
20const GGUF_TYPE_ARRAY: u32 = 9;
21const GGUF_TYPE_UINT64: u32 = 10;
22const GGUF_TYPE_INT64: u32 = 11;
23const GGUF_TYPE_FLOAT64: u32 = 12;
24
25const KV_GENERAL_ALIGNMENT: &str = "general.alignment";
26const KV_IMATRIX_DATASETS: &str = "imatrix.datasets";
27const KV_IMATRIX_CHUNK_COUNT: &str = "imatrix.chunk_count";
28
29#[derive(Debug, Clone, PartialEq)]
30pub struct ImatrixEntry {
31 pub name: String,
32 pub values: Vec<f32>,
33}
34
35#[derive(Debug, Clone, PartialEq)]
36pub struct Imatrix {
37 entries: Vec<ImatrixEntry>,
38 source_path: PathBuf,
39 dataset: Option<String>,
40 chunk_count: i32,
41}
42
43impl Imatrix {
44 pub fn load(
45 path: &Path,
46 include_weights: &[String],
47 exclude_weights: &[String],
48 ) -> Result<Self> {
49 let bytes =
50 fs::read(path).with_context(|| format!("read imatrix file {}", path.display()))?;
51 Self::from_bytes(path, &bytes, include_weights, exclude_weights)
52 }
53
54 pub fn from_bytes(
59 path: &Path,
60 bytes: &[u8],
61 include_weights: &[String],
62 exclude_weights: &[String],
63 ) -> Result<Self> {
64 let loaded = if bytes.starts_with(GGUF_MAGIC) {
65 load_gguf_imatrix(bytes, path)?
66 } else {
67 load_legacy_imatrix(bytes, path)?
68 };
69 Self::from_loaded(path, loaded, include_weights, exclude_weights)
70 }
71
72 pub fn entries(&self) -> &[ImatrixEntry] {
73 &self.entries
74 }
75
76 pub fn source_path(&self) -> &Path {
77 &self.source_path
78 }
79
80 pub fn dataset(&self) -> Option<&str> {
81 self.dataset.as_deref()
82 }
83
84 pub fn chunk_count(&self) -> i32 {
85 self.chunk_count
86 }
87
88 pub fn entry_count(&self) -> usize {
89 self.entries.len()
90 }
91
92 fn from_loaded(
93 path: &Path,
94 loaded: LoadedImatrix,
95 include_weights: &[String],
96 exclude_weights: &[String],
97 ) -> Result<Self> {
98 let mut selected = loaded
99 .entries
100 .into_iter()
101 .filter(|entry| include_exclude_match(&entry.name, include_weights, exclude_weights))
102 .collect::<Vec<_>>();
103 selected.sort_by(|a, b| a.name.cmp(&b.name));
104 ensure!(
105 !selected.is_empty(),
106 "imatrix filters removed all entries from {}",
107 path.display()
108 );
109
110 Ok(Self {
111 entries: selected,
112 source_path: path.to_path_buf(),
113 dataset: loaded.dataset,
114 chunk_count: loaded.chunk_count,
115 })
116 }
117}
118
119struct LoadedImatrix {
120 entries: Vec<ImatrixEntry>,
121 dataset: Option<String>,
122 chunk_count: i32,
123}
124
125fn load_legacy_imatrix(bytes: &[u8], path: &Path) -> Result<LoadedImatrix> {
126 let mut reader = Cursor::new(bytes);
127 let entry_count = read_i32(&mut reader)
128 .with_context(|| format!("read imatrix entry count from {}", path.display()))?;
129 ensure!(
130 entry_count > 0,
131 "imatrix file has no entries: {}",
132 path.display()
133 );
134 let entry_count = usize::try_from(entry_count).context("imatrix entry count overflow")?;
135 ensure!(
136 entry_count <= remaining_bytes(&reader) / 12,
137 "imatrix entry count exceeds the available file data"
138 );
139 let mut entries = Vec::with_capacity(entry_count);
140 for index in 0..entry_count {
141 entries.push(read_legacy_imatrix_entry(&mut reader, index)?);
142 }
143 let (chunk_count, dataset) = read_legacy_imatrix_trailer(&mut reader)?;
144 Ok(LoadedImatrix {
145 entries,
146 dataset,
147 chunk_count,
148 })
149}
150
151fn read_legacy_imatrix_entry(reader: &mut ByteReader<'_>, index: usize) -> Result<ImatrixEntry> {
152 let name_len = read_i32(reader).with_context(|| format!("read imatrix name length {index}"))?;
153 ensure!(name_len > 0, "imatrix entry {index} has empty name");
154 let name_len = usize::try_from(name_len).context("imatrix name length overflow")?;
155 ensure!(
156 name_len <= remaining_bytes(reader),
157 "imatrix entry {index} name extends past end of file"
158 );
159 let mut name_bytes = vec![0_u8; name_len];
160 reader
161 .read_exact(&mut name_bytes)
162 .with_context(|| format!("read imatrix name {index}"))?;
163 let name = String::from_utf8(name_bytes).with_context(|| format!("imatrix name {index}"))?;
164 let ncall = read_i32(reader).with_context(|| format!("read imatrix ncall for {name}"))?;
165 let value_count =
166 read_i32(reader).with_context(|| format!("read imatrix value count for {name}"))?;
167 ensure!(value_count > 0, "imatrix entry {name} has no values");
168 let value_count = usize::try_from(value_count).context("imatrix value count overflow")?;
169 let value_bytes = value_count
170 .checked_mul(std::mem::size_of::<f32>())
171 .context("imatrix value byte count overflow")?;
172 ensure!(
173 value_bytes <= remaining_bytes(reader),
174 "imatrix entry {name} values extend past end of file"
175 );
176 let mut values = (0..value_count)
177 .map(|_| read_f32(reader))
178 .collect::<Result<Vec<_>>>()?;
179 if ncall > 0 {
180 let denom = ncall as f32;
181 for value in &mut values {
182 *value /= denom;
183 }
184 }
185 Ok(ImatrixEntry { name, values })
186}
187
188fn read_legacy_imatrix_trailer(reader: &mut ByteReader<'_>) -> Result<(i32, Option<String>)> {
189 if reader.position() as usize >= reader.get_ref().len() {
190 return Ok((0, None));
191 }
192 let chunk_count = read_i32(reader)?;
193 if reader.position() as usize >= reader.get_ref().len() {
194 return Ok((chunk_count, None));
195 }
196 let len = read_i32(reader)?;
197 if len <= 0 {
198 return Ok((chunk_count, None));
199 }
200 let len = usize::try_from(len).context("imatrix dataset length overflow")?;
201 ensure!(
202 len <= remaining_bytes(reader),
203 "imatrix dataset extends past end of file"
204 );
205 let mut dataset = vec![0_u8; len];
206 reader.read_exact(&mut dataset)?;
207 Ok((chunk_count, Some(String::from_utf8(dataset)?)))
208}
209
210fn load_gguf_imatrix(bytes: &[u8], path: &Path) -> Result<LoadedImatrix> {
211 let mut reader = Cursor::new(bytes);
212 let header = GgufHeader::read(&mut reader)?;
213 ensure!(
214 header.version >= 2,
215 "unsupported GGUF imatrix version {} in {}",
216 header.version,
217 path.display()
218 );
219 ensure!(
220 header.metadata_count <= remaining_bytes(&reader) as u64 / 12,
221 "GGUF metadata count exceeds the available file data"
222 );
223 let mut metadata = GgufMetadata::default();
224 for _ in 0..header.metadata_count {
225 let key = read_gguf_string(&mut reader)?;
226 let value_type = read_u32(&mut reader)?;
227 metadata.read_value(&mut reader, &key, value_type)?;
228 }
229
230 ensure!(
231 header.tensor_count <= remaining_bytes(&reader) as u64 / 32,
232 "GGUF tensor count exceeds the available file data"
233 );
234 let tensor_count =
235 usize::try_from(header.tensor_count).context("GGUF tensor count overflow")?;
236 let mut tensors = Vec::with_capacity(tensor_count);
237 for _ in 0..header.tensor_count {
238 tensors.push(GgufTensorInfo::read(&mut reader)?);
239 }
240 let data_start = align_to(reader.position(), metadata.alignment)
241 .context("GGUF imatrix data offset overflow")?;
242 let entries = read_gguf_entries(bytes, &tensors, data_start)?;
243 ensure!(
244 !entries.is_empty(),
245 "GGUF imatrix has no paired tensors in {}",
246 path.display()
247 );
248 Ok(LoadedImatrix {
249 entries,
250 dataset: metadata.datasets.into_iter().next(),
251 chunk_count: metadata.chunk_count.unwrap_or(0) as i32,
252 })
253}
254
255struct GgufHeader {
256 version: u32,
257 tensor_count: u64,
258 metadata_count: u64,
259}
260
261impl GgufHeader {
262 fn read(reader: &mut ByteReader<'_>) -> Result<Self> {
263 let mut magic = [0_u8; 4];
264 reader.read_exact(&mut magic)?;
265 ensure!(&magic == GGUF_MAGIC, "not a GGUF file");
266 Ok(Self {
267 version: read_u32(reader)?,
268 tensor_count: read_u64(reader)?,
269 metadata_count: read_u64(reader)?,
270 })
271 }
272}
273
274#[derive(Default)]
275struct GgufMetadata {
276 alignment: u64,
277 datasets: Vec<String>,
278 chunk_count: Option<u32>,
279}
280
281impl GgufMetadata {
282 fn read_value(
283 &mut self,
284 reader: &mut ByteReader<'_>,
285 key: &str,
286 value_type: u32,
287 ) -> Result<()> {
288 match (key, value_type) {
289 (KV_GENERAL_ALIGNMENT, GGUF_TYPE_UINT32) => {
290 self.alignment = read_u32(reader)? as u64;
291 }
292 (KV_GENERAL_ALIGNMENT, GGUF_TYPE_UINT64) => {
293 self.alignment = read_u64(reader)?;
294 }
295 (KV_IMATRIX_CHUNK_COUNT, GGUF_TYPE_UINT32) => {
296 self.chunk_count = Some(read_u32(reader)?);
297 }
298 (KV_IMATRIX_DATASETS, GGUF_TYPE_ARRAY) => {
299 self.datasets = read_string_array(reader)?;
300 }
301 _ => skip_gguf_value(reader, value_type)?,
302 }
303 if self.alignment == 0 {
304 self.alignment = GGUF_ALIGNMENT_DEFAULT;
305 }
306 Ok(())
307 }
308}
309
310struct GgufTensorInfo {
311 name: String,
312 dims: Vec<u64>,
313 tensor_type: u32,
314 offset: u64,
315}
316
317impl GgufTensorInfo {
318 fn read(reader: &mut ByteReader<'_>) -> Result<Self> {
319 let name = read_gguf_string(reader)?;
320 let n_dims = read_u32(reader)?;
321 ensure!(n_dims > 0, "GGUF tensor {name} has no dimensions");
322 ensure!(
323 n_dims as usize <= remaining_bytes(reader) / 8,
324 "GGUF tensor {name} dimensions extend past end of file"
325 );
326 let dims = (0..n_dims)
327 .map(|_| read_u64(reader))
328 .collect::<Result<Vec<_>>>()?;
329 let tensor_type = read_u32(reader)?;
330 let offset = read_u64(reader)?;
331 Ok(Self {
332 name,
333 dims,
334 tensor_type,
335 offset,
336 })
337 }
338
339 fn element_count(&self) -> Result<usize> {
340 self.dims.iter().try_fold(1_usize, |acc, dim| {
341 let dim = usize::try_from(*dim)
342 .with_context(|| format!("GGUF tensor {} dimension overflow", self.name))?;
343 acc.checked_mul(dim)
344 .with_context(|| format!("GGUF tensor {} is too large", self.name))
345 })
346 }
347}
348
349fn read_gguf_entries(
350 bytes: &[u8],
351 tensors: &[GgufTensorInfo],
352 data_start: u64,
353) -> Result<Vec<ImatrixEntry>> {
354 let mut sums = BTreeMap::<String, Vec<f32>>::new();
355 let mut counts = BTreeMap::<String, Vec<f32>>::new();
356 for tensor in tensors {
357 if tensor.tensor_type != GGML_TYPE_F32 {
358 continue;
359 }
360 let Some((base_name, kind)) = imatrix_tensor_name(&tensor.name) else {
361 continue;
362 };
363 let values = read_gguf_f32_tensor(bytes, tensor, data_start)?;
364 match kind {
365 ImatrixTensorKind::Sums => {
366 sums.insert(base_name.to_string(), values);
367 }
368 ImatrixTensorKind::Counts => {
369 counts.insert(base_name.to_string(), values);
370 }
371 }
372 }
373 let mut entries = Vec::new();
374 for (name, sum_values) in sums {
375 let count_values = counts
376 .remove(&name)
377 .with_context(|| format!("GGUF imatrix tensor {name} is missing counts"))?;
378 entries.push(ImatrixEntry {
379 name,
380 values: normalize_gguf_imatrix_values(&sum_values, &count_values)?,
381 });
382 }
383 Ok(entries)
384}
385
386enum ImatrixTensorKind {
387 Sums,
388 Counts,
389}
390
391fn imatrix_tensor_name(name: &str) -> Option<(&str, ImatrixTensorKind)> {
392 if let Some(base) = name.strip_suffix(TENSOR_SUMS_SUFFIX) {
393 return Some((base, ImatrixTensorKind::Sums));
394 }
395 name.strip_suffix(TENSOR_COUNTS_SUFFIX)
396 .map(|base| (base, ImatrixTensorKind::Counts))
397}
398
399fn normalize_gguf_imatrix_values(sums: &[f32], counts: &[f32]) -> Result<Vec<f32>> {
400 ensure!(!counts.is_empty(), "GGUF imatrix entry has no counts");
401 ensure!(
402 sums.len().is_multiple_of(counts.len()),
403 "GGUF imatrix sums/counts shape mismatch"
404 );
405 let values_per_count = sums.len() / counts.len();
406 let mut output = vec![1.0_f32; sums.len()];
407 for (count_index, count) in counts.iter().enumerate() {
408 if *count <= 0.0 {
409 continue;
410 }
411 let offset = count_index * values_per_count;
412 for i in 0..values_per_count {
413 output[offset + i] = sums[offset + i] / count;
414 }
415 }
416 Ok(output)
417}
418
419fn read_gguf_f32_tensor(
420 bytes: &[u8],
421 tensor: &GgufTensorInfo,
422 data_start: u64,
423) -> Result<Vec<f32>> {
424 let element_count = tensor.element_count()?;
425 let byte_len = element_count
426 .checked_mul(std::mem::size_of::<f32>())
427 .with_context(|| format!("GGUF tensor {} is too large", tensor.name))?;
428 let offset = data_start
429 .checked_add(tensor.offset)
430 .with_context(|| format!("GGUF tensor {} offset overflow", tensor.name))?;
431 let offset = usize::try_from(offset)
432 .with_context(|| format!("GGUF tensor {} offset exceeds address space", tensor.name))?;
433 let end = offset
434 .checked_add(byte_len)
435 .with_context(|| format!("GGUF tensor {} byte range overflow", tensor.name))?;
436 ensure!(
437 end <= bytes.len(),
438 "GGUF tensor {} extends past end of file",
439 tensor.name
440 );
441 bytes[offset..end]
442 .as_chunks::<4>()
443 .0
444 .iter()
445 .map(|chunk| Ok(f32::from_le_bytes(*chunk)))
446 .collect::<Result<Vec<_>>>()
447}
448
449fn include_exclude_match(
450 name: &str,
451 include_weights: &[String],
452 exclude_weights: &[String],
453) -> bool {
454 if !exclude_weights.is_empty() {
455 return !exclude_weights.iter().any(|filter| name.contains(filter));
456 }
457 if !include_weights.is_empty() {
458 return include_weights.iter().any(|filter| name.contains(filter));
459 }
460 true
461}
462
463fn read_string_array(reader: &mut ByteReader<'_>) -> Result<Vec<String>> {
464 let element_type = read_u32(reader)?;
465 ensure!(
466 element_type == GGUF_TYPE_STRING,
467 "expected GGUF string array, found type {element_type}"
468 );
469 let len = read_u64(reader)?;
470 ensure!(
471 len <= remaining_bytes(reader) as u64 / 8,
472 "GGUF string array count exceeds the available file data"
473 );
474 (0..len).map(|_| read_gguf_string(reader)).collect()
475}
476
477fn skip_gguf_value(reader: &mut ByteReader<'_>, value_type: u32) -> Result<()> {
478 match value_type {
479 0 | 1 => skip_bytes(reader, 1),
480 2 | 3 => skip_bytes(reader, 2),
481 GGUF_TYPE_UINT32 | 5 | GGUF_TYPE_FLOAT32 => skip_bytes(reader, 4),
482 GGUF_TYPE_BOOL => skip_bytes(reader, 1),
483 GGUF_TYPE_STRING => {
484 let _ = read_gguf_string(reader)?;
485 Ok(())
486 }
487 GGUF_TYPE_ARRAY => skip_gguf_array(reader),
488 GGUF_TYPE_UINT64 | GGUF_TYPE_INT64 | GGUF_TYPE_FLOAT64 => skip_bytes(reader, 8),
489 _ => Err(anyhow!("unsupported GGUF metadata type {value_type}")),
490 }
491}
492
493fn skip_gguf_array(reader: &mut ByteReader<'_>) -> Result<()> {
494 let element_type = read_u32(reader)?;
495 let len = read_u64(reader)?;
496 ensure!(
497 len <= remaining_bytes(reader) as u64,
498 "GGUF array count exceeds the available file data"
499 );
500 for _ in 0..len {
501 skip_gguf_value(reader, element_type)?;
502 }
503 Ok(())
504}
505
506fn skip_bytes(reader: &mut ByteReader<'_>, len: u64) -> Result<()> {
507 let position = reader.position();
508 let next = position
509 .checked_add(len)
510 .context("GGUF metadata offset overflow")?;
511 ensure!(
512 next <= reader.get_ref().len() as u64,
513 "GGUF metadata extends past end of file"
514 );
515 reader.set_position(next);
516 Ok(())
517}
518
519fn read_gguf_string(reader: &mut ByteReader<'_>) -> Result<String> {
520 let len = read_u64(reader)?;
521 let len = usize::try_from(len).context("GGUF string length overflow")?;
522 ensure!(
523 len <= remaining_bytes(reader),
524 "GGUF string extends past end of file"
525 );
526 let mut bytes = vec![0_u8; len];
527 reader.read_exact(&mut bytes)?;
528 String::from_utf8(bytes).context("GGUF string is not UTF-8")
529}
530
531fn read_i32(reader: &mut ByteReader<'_>) -> Result<i32> {
532 let mut bytes = [0_u8; 4];
533 reader.read_exact(&mut bytes)?;
534 Ok(i32::from_le_bytes(bytes))
535}
536
537fn read_u32(reader: &mut ByteReader<'_>) -> Result<u32> {
538 let mut bytes = [0_u8; 4];
539 reader.read_exact(&mut bytes)?;
540 Ok(u32::from_le_bytes(bytes))
541}
542
543fn read_u64(reader: &mut ByteReader<'_>) -> Result<u64> {
544 let mut bytes = [0_u8; 8];
545 reader.read_exact(&mut bytes)?;
546 Ok(u64::from_le_bytes(bytes))
547}
548
549fn read_f32(reader: &mut ByteReader<'_>) -> Result<f32> {
550 let mut bytes = [0_u8; 4];
551 reader.read_exact(&mut bytes)?;
552 Ok(f32::from_le_bytes(bytes))
553}
554
555fn remaining_bytes(reader: &ByteReader<'_>) -> usize {
556 reader
557 .get_ref()
558 .len()
559 .saturating_sub(reader.position() as usize)
560}
561
562fn align_to(value: u64, alignment: u64) -> Option<u64> {
563 if alignment <= 1 {
564 return Some(value);
565 }
566 value.div_ceil(alignment).checked_mul(alignment)
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 #[test]
574 fn loads_legacy_imatrix_with_include_filter() {
575 let root = unique_temp_dir();
576 fs::create_dir_all(&root).unwrap();
577 let imatrix_path = root.join("imatrix.dat");
578 write_legacy_imatrix(
579 &imatrix_path,
580 &[
581 ("blk.0.attn_q.weight", 2, &[2.0, 4.0]),
582 ("blk.0.ffn_down.weight", 1, &[9.0, 12.0]),
583 ],
584 );
585
586 let imatrix = Imatrix::load(&imatrix_path, &["attn_q".to_string()], &Vec::new()).unwrap();
587
588 assert_eq!(imatrix.entry_count(), 1);
589 assert_eq!(imatrix.entries[0].values, vec![1.0, 2.0]);
590 assert_eq!(imatrix.dataset(), Some("dataset.txt"));
591 assert_eq!(imatrix.chunk_count(), 3);
592 fs::remove_dir_all(root).unwrap();
593 }
594
595 #[test]
596 fn loads_gguf_imatrix_with_normalized_counts() {
597 let root = unique_temp_dir();
598 fs::create_dir_all(&root).unwrap();
599 let imatrix_path = root.join("imatrix.gguf");
600 write_gguf_imatrix(&imatrix_path);
601
602 let imatrix = Imatrix::load(&imatrix_path, &["attn_q".to_string()], &Vec::new()).unwrap();
603
604 assert_eq!(imatrix.entry_count(), 1);
605 assert_eq!(imatrix.entries[0].values, vec![1.0, 2.0, 1.0, 1.0]);
606 assert_eq!(imatrix.dataset(), Some("calibration.txt"));
607 assert_eq!(imatrix.chunk_count(), 7);
608 fs::remove_dir_all(root).unwrap();
609 }
610
611 #[test]
612 fn rejects_alignment_overflow() {
613 assert_eq!(align_to(u64::MAX, 32), None);
614 }
615
616 fn unique_temp_dir() -> PathBuf {
617 static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
618 let nanos = std::time::SystemTime::now()
619 .duration_since(std::time::UNIX_EPOCH)
620 .unwrap()
621 .as_nanos();
622 let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
623 std::env::temp_dir().join(format!("skippy-imatrix-{nanos}-{id}"))
624 }
625
626 fn write_legacy_imatrix(path: &Path, entries: &[(&str, i32, &[f32])]) {
627 let mut bytes = Vec::new();
628 bytes.extend_from_slice(&(entries.len() as i32).to_le_bytes());
629 for (name, ncall, values) in entries {
630 bytes.extend_from_slice(&(name.len() as i32).to_le_bytes());
631 bytes.extend_from_slice(name.as_bytes());
632 bytes.extend_from_slice(&ncall.to_le_bytes());
633 bytes.extend_from_slice(&(values.len() as i32).to_le_bytes());
634 for value in *values {
635 bytes.extend_from_slice(&value.to_le_bytes());
636 }
637 }
638 bytes.extend_from_slice(&3_i32.to_le_bytes());
639 let dataset = "dataset.txt";
640 bytes.extend_from_slice(&(dataset.len() as i32).to_le_bytes());
641 bytes.extend_from_slice(dataset.as_bytes());
642 fs::write(path, bytes).unwrap();
643 }
644
645 fn write_gguf_imatrix(path: &Path) {
646 let sums_name = "blk.0.attn_q.weight.in_sum2";
647 let counts_name = "blk.0.attn_q.weight.counts";
648 let mut bytes = Vec::new();
649 bytes.extend_from_slice(GGUF_MAGIC);
650 bytes.extend_from_slice(&3_u32.to_le_bytes());
651 bytes.extend_from_slice(&2_u64.to_le_bytes());
652 bytes.extend_from_slice(&4_u64.to_le_bytes());
653 write_gguf_kv_string(&mut bytes, "general.type", "imatrix");
654 write_gguf_kv_u32(&mut bytes, KV_GENERAL_ALIGNMENT, 32);
655 write_gguf_kv_u32(&mut bytes, KV_IMATRIX_CHUNK_COUNT, 7);
656 write_gguf_kv_string_array(&mut bytes, KV_IMATRIX_DATASETS, &["calibration.txt"]);
657 write_gguf_tensor_info(&mut bytes, sums_name, &[2, 2], GGML_TYPE_F32, 0);
658 write_gguf_tensor_info(&mut bytes, counts_name, &[1, 2], GGML_TYPE_F32, 16);
659 while bytes.len() % 32 != 0 {
660 bytes.push(0);
661 }
662 for value in [2.0_f32, 4.0, 9.0, 11.0] {
663 bytes.extend_from_slice(&value.to_le_bytes());
664 }
665 for value in [2.0_f32, 0.0] {
666 bytes.extend_from_slice(&value.to_le_bytes());
667 }
668 fs::write(path, bytes).unwrap();
669 }
670
671 fn write_gguf_kv_string(bytes: &mut Vec<u8>, key: &str, value: &str) {
672 write_gguf_string(bytes, key);
673 bytes.extend_from_slice(&GGUF_TYPE_STRING.to_le_bytes());
674 write_gguf_string(bytes, value);
675 }
676
677 fn write_gguf_kv_u32(bytes: &mut Vec<u8>, key: &str, value: u32) {
678 write_gguf_string(bytes, key);
679 bytes.extend_from_slice(&GGUF_TYPE_UINT32.to_le_bytes());
680 bytes.extend_from_slice(&value.to_le_bytes());
681 }
682
683 fn write_gguf_kv_string_array(bytes: &mut Vec<u8>, key: &str, values: &[&str]) {
684 write_gguf_string(bytes, key);
685 bytes.extend_from_slice(&GGUF_TYPE_ARRAY.to_le_bytes());
686 bytes.extend_from_slice(&GGUF_TYPE_STRING.to_le_bytes());
687 bytes.extend_from_slice(&(values.len() as u64).to_le_bytes());
688 for value in values {
689 write_gguf_string(bytes, value);
690 }
691 }
692
693 fn write_gguf_tensor_info(
694 bytes: &mut Vec<u8>,
695 name: &str,
696 dims: &[u64],
697 tensor_type: u32,
698 offset: u64,
699 ) {
700 write_gguf_string(bytes, name);
701 bytes.extend_from_slice(&(dims.len() as u32).to_le_bytes());
702 for dim in dims {
703 bytes.extend_from_slice(&dim.to_le_bytes());
704 }
705 bytes.extend_from_slice(&tensor_type.to_le_bytes());
706 bytes.extend_from_slice(&offset.to_le_bytes());
707 }
708
709 fn write_gguf_string(bytes: &mut Vec<u8>, value: &str) {
710 bytes.extend_from_slice(&(value.len() as u64).to_le_bytes());
711 bytes.extend_from_slice(value.as_bytes());
712 }
713}