1use std::collections::{BTreeMap, btree_map::Entry};
4use std::fs::{self, File};
5use std::io::{Seek, Write};
6use std::path::Path;
7
8use anyhow::{Context, Result, ensure};
9use serde::Serialize;
10use serde_json::Value;
11
12use crate::ConvertOutputType;
13use crate::float_convert::{
14 FloatDType, convert_float_chunk, read_float_element, target_dtype_for_tensor,
15 write_float_element,
16};
17pub use crate::gguf_metadata::GgufKv;
18use crate::gguf_metadata::write_kv;
19#[cfg(test)]
20use crate::gguf_metadata::{
21 GGUF_TYPE_ARRAY, GGUF_TYPE_BOOL, GGUF_TYPE_FLOAT32, GGUF_TYPE_INT32, GGUF_TYPE_STRING,
22 GGUF_TYPE_UINT16, GGUF_TYPE_UINT32, GGUF_TYPE_UINT64,
23};
24use crate::gguf_template::{metadata_from_hf_config, mtp_layer_start_from_hf_config};
25use crate::hf_checkpoint::{SafetensorFile, SafetensorTensorInfo, open_safetensor_files};
26use crate::tensor_map::{
27 TensorNameMap, hf_layer_id, inkling_mtp_depth, is_inkling_fused_w13, is_mtp_source_tensor,
28 is_shared_mtp_context_tensor,
29};
30
31mod glm_dsa;
32
33use glm_dsa::{
34 GlmDsaKvBSplitMode, TensorTransform, enrich_glm_dsa_indexshare_metadata, glm_dsa_kv_b_layer,
35 glm_dsa_kv_b_split_mode, stream_transformed_segment,
36};
37
38const GGUF_MAGIC: &[u8; 4] = b"GGUF";
39const GGUF_VERSION: u32 = 3;
40const GGUF_ALIGNMENT: u64 = 32;
41const GGML_TYPE_F32: u32 = 0;
42const GGML_TYPE_F16: u32 = 1;
43const GGML_TYPE_BF16: u32 = 30;
44#[derive(Debug, Clone)]
45pub struct RawGgufWriteOptions {
46 pub buffer_size: usize,
47 pub metadata: Option<Vec<GgufKv>>,
48 pub tensor_name_map: TensorNameMap,
49 pub split: Option<GgufSplit>,
50 pub output_type: Option<ConvertOutputType>,
51 pub tensor_selection: TensorSelection,
52}
53
54#[derive(Debug, Clone, Copy, Default)]
55pub enum TensorSelection {
56 #[default]
57 All,
58 ExcludeMtp {
59 layer_start: u32,
60 },
61 MtpOnly {
62 layer_start: u32,
63 },
64}
65
66#[derive(Debug, Clone, Copy)]
67pub struct GgufSplit {
68 pub split_index: u32,
69 pub split_count: u32,
70}
71
72pub fn write_raw_safetensors_gguf(
73 source: &Path,
74 output: &Path,
75 options: RawGgufWriteOptions,
76) -> Result<()> {
77 let PreparedGgufWrite {
78 files,
79 tensors,
80 metadata,
81 } = prepare_raw_safetensors_gguf(source, &options)?;
82 if let Some(parent) = output.parent() {
83 fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
84 }
85 let mut writer =
86 File::create(output).with_context(|| format!("create {}", output.display()))?;
87 write_header_and_tensor_table(&mut writer, &metadata, &tensors)?;
88 stream_tensor_data(&mut writer, &files, &tensors, options.buffer_size)
89}
90
91pub fn validate_raw_safetensors_gguf(
92 source: &Path,
93 options: RawGgufWriteOptions,
94) -> Result<RawGgufValidation> {
95 let PreparedGgufWrite {
96 tensors, metadata, ..
97 } = prepare_raw_safetensors_gguf(source, &options)?;
98 Ok(RawGgufValidation {
99 selected_tensor_count: tensors.len(),
100 selected_tensor_bytes: tensors.iter().map(|tensor| tensor.byte_len).sum(),
101 metadata_count: metadata.len(),
102 output_type: options.output_type.map(|kind| kind.as_arg().to_string()),
103 })
104}
105
106pub fn recommended_raw_safetensors_gguf_split_count(
107 source: &Path,
108 mut options: RawGgufWriteOptions,
109 max_tensor_bytes: u64,
110) -> Result<u32> {
111 ensure!(
112 max_tensor_bytes > 0,
113 "split maximum tensor bytes must be greater than zero"
114 );
115 options.split = None;
116 let PreparedGgufWrite { tensors, .. } = prepare_raw_safetensors_gguf(source, &options)?;
117 let largest_tensor_bytes = tensors
118 .iter()
119 .map(|tensor| tensor.byte_len)
120 .max()
121 .unwrap_or_default();
122 ensure!(
123 largest_tensor_bytes <= max_tensor_bytes,
124 "largest selected tensor is {largest_tensor_bytes} bytes, exceeding split maximum {max_tensor_bytes} bytes"
125 );
126 let total_tensor_bytes = tensors
127 .iter()
128 .try_fold(0_u64, |total, tensor| total.checked_add(tensor.byte_len));
129 let total_tensor_bytes = total_tensor_bytes.context("selected tensor byte total overflow")?;
130 let minimum_count = total_tensor_bytes
131 .div_ceil(max_tensor_bytes)
132 .max(1)
133 .min(tensors.len() as u64);
134 let minimum_count = u32::try_from(minimum_count).context("split count does not fit u32")?;
135 let maximum_count = u32::try_from(tensors.len()).context("tensor count does not fit u32")?;
136
137 for split_count in minimum_count..=maximum_count {
138 let split = GgufSplit {
139 split_index: 1,
140 split_count,
141 };
142 let boundaries = byte_balanced_split_boundaries(&tensors, split)?;
143 let every_split_fits = boundaries.windows(2).all(|range| {
144 tensors[range[0]..range[1]]
145 .iter()
146 .map(|tensor| tensor.byte_len)
147 .sum::<u64>()
148 <= max_tensor_bytes
149 });
150 if every_split_fits {
151 return Ok(split_count);
152 }
153 }
154
155 anyhow::bail!("could not partition selected tensors within the split maximum")
156}
157
158struct PreparedGgufWrite {
159 files: Vec<SafetensorFile>,
160 tensors: Vec<TensorSource>,
161 metadata: Vec<GgufKv>,
162}
163
164pub struct DirectCheckpoint {
170 files: Vec<SafetensorFile>,
171 tensors: Vec<TensorSource>,
172 metadata_gguf: Vec<u8>,
173 buffer_size: usize,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct ImatrixTensorLayout {
178 pub name: String,
179 pub value_count: usize,
180}
181
182impl DirectCheckpoint {
183 pub fn open(source: &Path, buffer_size: usize) -> Result<Self> {
186 let source = if source.is_file() {
187 ensure!(
188 source.extension().and_then(|value| value.to_str()) == Some("safetensors"),
189 "checkpoint file must use the .safetensors extension: {}",
190 source.display()
191 );
192 source
193 .parent()
194 .with_context(|| format!("checkpoint file has no parent: {}", source.display()))?
195 } else {
196 source
197 };
198 let files = open_safetensor_files(source)?;
199 ensure!(
200 !files.is_empty(),
201 "no safetensors files found under {}",
202 source.display()
203 );
204 let tensor_count = files.iter().map(|file| file.tensors().len()).sum();
205 let mtp_layer_start = mtp_layer_start_from_hf_config(source)?;
206 let tensor_name_map = mtp_layer_start
207 .map(|layer_start| TensorNameMap::HfToGgufWithMtp { layer_start })
208 .unwrap_or(TensorNameMap::HfToGguf);
209 let prepared = prepare_raw_safetensors_gguf_with_files(
210 source,
211 &RawGgufWriteOptions {
212 buffer_size,
213 metadata: Some(metadata_from_hf_config(source, tensor_count)?),
214 tensor_name_map,
215 split: None,
216 output_type: None,
217 tensor_selection: TensorSelection::All,
218 },
219 files,
220 )?;
221 let mut metadata_gguf = Vec::new();
222 write_header_and_tensor_table(&mut metadata_gguf, &prepared.metadata, &prepared.tensors)?;
223 let aligned_len = usize::try_from(align_to(metadata_gguf.len() as u64, GGUF_ALIGNMENT))
224 .context("metadata GGUF length does not fit usize")?;
225 metadata_gguf.resize(aligned_len, 0);
226 Ok(Self {
227 files: prepared.files,
228 tensors: prepared.tensors,
229 metadata_gguf,
230 buffer_size,
231 })
232 }
233
234 pub fn metadata_gguf(&self) -> &[u8] {
236 &self.metadata_gguf
237 }
238
239 pub fn tensor_count(&self) -> usize {
241 self.tensors.len()
242 }
243
244 pub fn imatrix_layout(&self) -> Result<Vec<ImatrixTensorLayout>> {
246 self.tensors
247 .iter()
248 .filter(|tensor| tensor.dims.len() >= 2)
249 .map(|tensor| {
250 let ne0 = usize::try_from(tensor.dims[0]).with_context(|| {
251 format!("imatrix width does not fit usize for {}", tensor.name)
252 })?;
253 let ne2 = tensor
254 .dims
255 .get(2)
256 .copied()
257 .map(usize::try_from)
258 .transpose()
259 .with_context(|| {
260 format!("imatrix depth does not fit usize for {}", tensor.name)
261 })?
262 .unwrap_or(1);
263 let value_count = ne0.checked_mul(ne2).with_context(|| {
264 format!("imatrix value count overflows usize for {}", tensor.name)
265 })?;
266 Ok(ImatrixTensorLayout {
267 name: tensor.name.clone(),
268 value_count,
269 })
270 })
271 .collect()
272 }
273
274 pub fn read_tensor_f32(&self, name: &str, destination: &mut [f32]) -> Result<()> {
276 let tensor = self
277 .tensors
278 .iter()
279 .find(|tensor| tensor.name == name)
280 .with_context(|| format!("canonical tensor {name} not found in checkpoint"))?;
281 let target_dtype = tensor
282 .segments
283 .first()
284 .context("tensor has no source segments")?
285 .target_dtype;
286 ensure!(
287 tensor
288 .segments
289 .iter()
290 .all(|segment| segment.target_dtype == target_dtype),
291 "tensor {name} has mixed target dtypes"
292 );
293 let expected_elements = tensor.byte_len / target_dtype.byte_size();
294 ensure!(
295 destination.len() as u64 == expected_elements,
296 "tensor {name} destination has {} elements, expected {expected_elements}",
297 destination.len()
298 );
299 let mut bytes = Vec::with_capacity(
300 usize::try_from(tensor.byte_len).context("tensor byte length does not fit usize")?,
301 );
302 for segment in &tensor.segments {
303 stream_segment(
304 &mut bytes,
305 &self.files[segment.file_index],
306 segment,
307 self.buffer_size,
308 )?;
309 }
310 ensure!(
311 bytes.len() as u64 == tensor.byte_len,
312 "decoded {} bytes for {name}, expected {}",
313 bytes.len(),
314 tensor.byte_len
315 );
316 for (index, value) in destination.iter_mut().enumerate() {
317 *value = read_float_element(&bytes, target_dtype, index);
318 }
319 Ok(())
320 }
321}
322
323#[derive(Debug, Serialize)]
324pub struct RawGgufValidation {
325 pub selected_tensor_count: usize,
326 pub selected_tensor_bytes: u64,
327 pub metadata_count: usize,
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub output_type: Option<String>,
330}
331
332fn prepare_raw_safetensors_gguf(
333 source: &Path,
334 options: &RawGgufWriteOptions,
335) -> Result<PreparedGgufWrite> {
336 let files = open_safetensor_files(source)?;
337 prepare_raw_safetensors_gguf_with_files(source, options, files)
338}
339
340fn prepare_raw_safetensors_gguf_with_files(
341 source: &Path,
342 options: &RawGgufWriteOptions,
343 files: Vec<SafetensorFile>,
344) -> Result<PreparedGgufWrite> {
345 ensure!(
346 options.buffer_size > 0,
347 "buffer_size must be greater than zero"
348 );
349 ensure!(
350 !files.is_empty(),
351 "no safetensors files found under {}",
352 source.display()
353 );
354 let metadata_seed = options.metadata.clone();
355 let glm_dsa_kv_b_split = glm_dsa_kv_b_split_mode(metadata_seed.as_deref())?;
356 let hf_layout = HfTensorLayout::from_checkpoint(source, options.tensor_name_map)?;
357 let tensors = collect_tensor_sources(
358 &files,
359 options.tensor_name_map,
360 options.output_type,
361 options.tensor_selection,
362 glm_dsa_kv_b_split,
363 hf_layout,
364 )?;
365 ensure!(
366 !tensors.is_empty(),
367 "no tensors found under {}",
368 source.display()
369 );
370 let total_tensor_count = tensors.len();
371 let mut metadata = metadata_seed.unwrap_or_else(|| raw_metadata(source, total_tensor_count));
372 enrich_glm_dsa_indexshare_metadata(&mut metadata, &tensors)?;
373 let mut tensors = select_split_tensors(tensors, options.split)?;
374 assign_gguf_offsets(&mut tensors)?;
375 let metadata = split_metadata(metadata, options.split, total_tensor_count)?;
376 Ok(PreparedGgufWrite {
377 files,
378 tensors,
379 metadata,
380 })
381}
382
383fn select_split_tensors(
384 tensors: Vec<TensorSource>,
385 split: Option<GgufSplit>,
386) -> Result<Vec<TensorSource>> {
387 let Some(split) = split else {
388 return Ok(tensors);
389 };
390 split.validate()?;
391 let total_tensors = tensors.len();
392 ensure!(
393 usize::try_from(split.split_count).is_ok_and(|count| count <= total_tensors),
394 "split_count {} cannot exceed tensor count {}",
395 split.split_count,
396 total_tensors
397 );
398 let split_index =
399 usize::try_from(split.split_index).context("split_index does not fit usize")?;
400 let boundaries = byte_balanced_split_boundaries(&tensors, split)?;
401 let start = boundaries[split_index - 1];
402 let end = boundaries[split_index];
403 ensure!(
404 start < end,
405 "split {} of {} would contain no tensors",
406 split.split_index,
407 split.split_count
408 );
409 Ok(tensors
410 .into_iter()
411 .enumerate()
412 .filter_map(|(index, tensor)| (start <= index && index < end).then_some(tensor))
413 .collect())
414}
415
416fn byte_balanced_split_boundaries(
417 tensors: &[TensorSource],
418 split: GgufSplit,
419) -> Result<Vec<usize>> {
420 split.validate()?;
421 let split_count =
422 usize::try_from(split.split_count).context("split_count does not fit usize")?;
423 ensure!(
424 split_count <= tensors.len(),
425 "split_count {} cannot exceed tensor count {}",
426 split.split_count,
427 tensors.len()
428 );
429 let total_bytes = tensors
430 .iter()
431 .try_fold(0_u128, |acc, tensor| {
432 acc.checked_add(tensor.byte_len as u128)
433 })
434 .context("split tensor byte total overflow")?;
435 let mut boundaries = vec![0_usize];
436 let mut accumulated = 0_u128;
437 for (index, tensor) in tensors.iter().enumerate() {
438 accumulated = accumulated
439 .checked_add(tensor.byte_len as u128)
440 .context("split tensor byte total overflow")?;
441 let remaining_tensors = tensors.len() - (index + 1);
442 let remaining_splits = split_count - boundaries.len();
443 if boundaries.len() < split_count && remaining_tensors >= remaining_splits {
444 let target = total_bytes
445 .checked_mul(boundaries.len() as u128)
446 .context("split target byte overflow")?
447 / split_count as u128;
448 if accumulated >= target {
449 boundaries.push(index + 1);
450 }
451 }
452 }
453 while boundaries.len() < split_count {
454 let next = boundaries.last().copied().unwrap_or(0) + 1;
455 boundaries.push(next);
456 }
457 boundaries.push(tensors.len());
458 Ok(boundaries)
459}
460
461fn assign_gguf_offsets(tensors: &mut [TensorSource]) -> Result<()> {
462 let mut offset = 0_u64;
463 for tensor in tensors {
464 offset = align_to(offset, GGUF_ALIGNMENT);
465 tensor.gguf_offset = offset;
466 offset = offset
467 .checked_add(tensor.byte_len)
468 .with_context(|| format!("GGUF data offset overflow after {}", tensor.name))?;
469 }
470 Ok(())
471}
472
473fn split_metadata(
474 mut metadata: Vec<GgufKv>,
475 split: Option<GgufSplit>,
476 total_tensor_count: usize,
477) -> Result<Vec<GgufKv>> {
478 let Some(split) = split else {
479 return Ok(metadata);
480 };
481 split.validate()?;
482 metadata.push(GgufKv::u16(
483 "split.no",
484 u16::try_from(split.split_index - 1).context("split index does not fit uint16")?,
485 ));
486 metadata.push(GgufKv::u16(
487 "split.count",
488 u16::try_from(split.split_count).context("split count does not fit uint16")?,
489 ));
490 metadata.push(GgufKv::i32(
491 "split.tensors.count",
492 i32::try_from(total_tensor_count).context("tensor count does not fit int32")?,
493 ));
494 Ok(metadata)
495}
496
497impl GgufSplit {
498 fn validate(self) -> Result<()> {
499 ensure!(
500 self.split_count > 0,
501 "split_count must be greater than zero"
502 );
503 ensure!(
504 self.split_index > 0,
505 "split_index is 1-based and cannot be zero"
506 );
507 ensure!(
508 self.split_index <= self.split_count,
509 "split_index {} exceeds split_count {}",
510 self.split_index,
511 self.split_count
512 );
513 ensure!(
514 u16::try_from(self.split_count).is_ok(),
515 "split_count {} exceeds GGUF uint16 split metadata",
516 self.split_count
517 );
518 Ok(())
519 }
520}
521
522fn collect_tensor_sources(
523 files: &[SafetensorFile],
524 tensor_name_map: TensorNameMap,
525 output_type: Option<ConvertOutputType>,
526 tensor_selection: TensorSelection,
527 glm_dsa_kv_b_split: GlmDsaKvBSplitMode,
528 hf_layout: HfTensorLayout,
529) -> Result<Vec<TensorSource>> {
530 let mut tensors = Vec::new();
531 let mut expert_groups = BTreeMap::<ExpertGroupKey, ExpertGroup>::new();
532 for (file_index, file) in files.iter().enumerate() {
533 for tensor in file.tensors().values() {
534 if !tensor_selection.includes(tensor.name())? {
535 continue;
536 }
537 if is_inkling_fused_w13(tensor.name()) {
538 tensors.extend(inkling_w13_tensor_sources(
539 file_index,
540 tensor,
541 tensor_name_map,
542 output_type,
543 )?);
544 continue;
545 }
546 if matches!(hf_layout, HfTensorLayout::GraniteHybrid { .. })
547 && tensor.name().ends_with("shared_mlp.input_linear.weight")
548 {
549 tensors.extend(granite_shared_mlp_tensor_sources(
550 file_index,
551 tensor,
552 output_type,
553 )?);
554 continue;
555 }
556 if matches!(
557 tensor_name_map,
558 TensorNameMap::HfToGguf | TensorNameMap::HfToGgufWithMtp { .. }
559 ) && let Some(expert) = ExpertSourceTensor::parse(tensor.name())?
560 {
561 match expert_groups.entry(expert.group_key()) {
562 Entry::Vacant(entry) => {
563 entry
564 .insert(ExpertGroup::new(expert.group_key(), tensor, output_type)?)
565 .push(file_index, tensor, expert.expert_id)?;
566 }
567 Entry::Occupied(mut entry) => {
568 entry.get_mut().push(file_index, tensor, expert.expert_id)?;
569 }
570 }
571 continue;
572 }
573 if let Some(layer) = glm_dsa_kv_b_layer(tensor.name())? {
574 match glm_dsa_kv_b_split {
575 GlmDsaKvBSplitMode::Config(split) => {
576 tensors.extend(TensorSource::from_glm_dsa_kv_b_split(
577 file_index,
578 tensor,
579 layer,
580 split,
581 output_type,
582 )?);
583 continue;
584 }
585 GlmDsaKvBSplitMode::MissingMetadata => {
586 anyhow::bail!(
587 "GLM-DSA tensor {} requires attention head/value/rope/kv_lora metadata for kv_b split",
588 tensor.name()
589 );
590 }
591 GlmDsaKvBSplitMode::Disabled => {}
592 }
593 }
594 tensors.push(TensorSource::from_safetensor(
595 file_index,
596 tensor,
597 tensor_name_map,
598 output_type,
599 hf_layout,
600 )?);
601 }
602 }
603 for group in expert_groups.into_values() {
604 tensors.push(group.into_tensor_source()?);
605 }
606 tensors.sort_by(|a, b| a.name.cmp(&b.name));
607 Ok(tensors)
608}
609
610impl TensorSelection {
611 fn includes(self, name: &str) -> Result<bool> {
612 let is_mtp = match self {
613 Self::All => return Ok(true),
614 Self::ExcludeMtp { layer_start } | Self::MtpOnly { layer_start } => {
615 is_mtp_source_tensor(name)
616 || hf_layer_id(name)?.is_some_and(|layer| layer >= layer_start)
617 }
618 };
619 match self {
620 Self::All => Ok(true),
621 Self::ExcludeMtp { .. } => Ok(!is_mtp),
622 Self::MtpOnly { .. } => Ok(is_mtp || is_shared_mtp_context_tensor(name)),
623 }
624 }
625}
626
627struct TensorSource {
628 segments: Vec<TensorSegment>,
629 name: String,
630 dims: Vec<u64>,
631 ggml_type: u32,
632 byte_len: u64,
633 gguf_offset: u64,
634}
635
636impl TensorSource {
637 fn from_safetensor(
638 file_index: usize,
639 tensor: &SafetensorTensorInfo,
640 tensor_name_map: TensorNameMap,
641 output_type: Option<ConvertOutputType>,
642 hf_layout: HfTensorLayout,
643 ) -> Result<Self> {
644 let source_dtype = FloatDType::from_safetensor(tensor.dtype()).with_context(|| {
645 format!("unsupported dtype {} for {}", tensor.dtype(), tensor.name())
646 })?;
647 let name = tensor_name_map.map_tensor_name(tensor.name())?;
648 let target_dtype =
649 target_dtype_for_mapped_tensor(source_dtype, output_type, tensor.shape(), &name)?;
650 let element_count = tensor_element_count(tensor)?;
651 let dims = mapped_tensor_dims(tensor.shape(), &name, hf_layout)?;
652 let transform = tensor_transform(tensor, &name, hf_layout)?;
653 Ok(Self {
654 segments: vec![TensorSegment {
655 file_index,
656 source_name: tensor.name().to_string(),
657 source_dtype,
658 target_dtype,
659 element_count,
660 source_byte_len: tensor.byte_len(),
661 target_byte_len: tensor_byte_len(element_count, target_dtype)?,
662 transform,
663 }],
664 name,
665 dims,
666 ggml_type: ggml_type_for_dtype(target_dtype),
667 byte_len: tensor_byte_len(element_count, target_dtype)?,
668 gguf_offset: 0,
669 })
670 }
671}
672
673fn target_dtype_for_mapped_tensor(
674 source_dtype: FloatDType,
675 output_type: Option<ConvertOutputType>,
676 shape: &[u64],
677 mapped_name: &str,
678) -> Result<FloatDType> {
679 if mapped_name.ends_with("attn_rel_proj.weight")
680 || mapped_name.contains(".shortconv_")
681 || mapped_name.ends_with(".ssm_conv1d.weight")
682 {
683 return Ok(FloatDType::F32);
684 }
685 target_dtype_for_tensor(source_dtype, output_type, shape)
686}
687
688fn mapped_tensor_dims(
689 shape: &[u64],
690 mapped_name: &str,
691 hf_layout: HfTensorLayout,
692) -> Result<Vec<u64>> {
693 if mapped_name.contains(".shortconv_") {
694 ensure!(
695 shape.len() == 3 && shape[1] == 1,
696 "Inkling shortconv tensor {mapped_name} must have shape [channels, 1, kernel], got {shape:?}"
697 );
698 return Ok(vec![shape[2], shape[0]]);
699 }
700 if matches!(hf_layout, HfTensorLayout::GraniteHybrid { .. })
701 && mapped_name.ends_with(".ssm_conv1d.weight")
702 {
703 ensure!(
704 shape.len() == 3 && shape[1] == 1,
705 "Granite Hybrid SSM convolution {mapped_name} must have shape [channels, 1, kernel], got {shape:?}"
706 );
707 return Ok(vec![shape[2], shape[0]]);
708 }
709 if matches!(hf_layout, HfTensorLayout::GraniteHybrid { .. })
710 && (mapped_name.ends_with(".ssm_a") || mapped_name.ends_with(".ssm_d"))
711 {
712 ensure!(
713 shape.len() == 1,
714 "Granite Hybrid {mapped_name} must be rank 1, got {shape:?}"
715 );
716 return Ok(vec![1, shape[0]]);
717 }
718 if let HfTensorLayout::GraniteHybrid {
719 ssm_group_count, ..
720 } = hf_layout
721 && mapped_name.ends_with(".ssm_norm.weight")
722 {
723 ensure!(
724 shape.len() == 1 && shape[0].is_multiple_of(ssm_group_count),
725 "Granite Hybrid {mapped_name} shape {shape:?} is not divisible by SSM group count {ssm_group_count}"
726 );
727 return Ok(vec![shape[0] / ssm_group_count, ssm_group_count]);
728 }
729 Ok(shape.iter().rev().copied().collect())
730}
731
732fn tensor_transform(
733 tensor: &SafetensorTensorInfo,
734 mapped_name: &str,
735 hf_layout: HfTensorLayout,
736) -> Result<TensorTransform> {
737 if matches!(hf_layout, HfTensorLayout::GraniteHybrid { .. }) && mapped_name.ends_with(".ssm_a")
738 {
739 return Ok(TensorTransform::NegativeExp);
740 }
741 let head_count = match (hf_layout, mapped_name) {
742 (
743 HfTensorLayout::LlamaLike {
744 head_count,
745 kv_head_count: _,
746 }
747 | HfTensorLayout::GraniteHybrid {
748 head_count,
749 kv_head_count: _,
750 ssm_group_count: _,
751 },
752 name,
753 ) if name.ends_with(".attn_q.weight") || name.ends_with(".attn_q.bias") => Some(head_count),
754 (
755 HfTensorLayout::LlamaLike { kv_head_count, .. }
756 | HfTensorLayout::GraniteHybrid { kv_head_count, .. },
757 name,
758 ) if name.ends_with(".attn_k.weight") || name.ends_with(".attn_k.bias") => {
759 Some(kv_head_count)
760 }
761 _ => None,
762 };
763 let Some(head_count) = head_count else {
764 return Ok(TensorTransform::Identity);
765 };
766 let row_count = *tensor
767 .shape()
768 .first()
769 .with_context(|| format!("RoPE tensor {} has no dimensions", tensor.name()))?;
770 let row_elements = tensor.shape()[1..].iter().try_fold(1_u64, |acc, dim| {
771 acc.checked_mul(*dim)
772 .with_context(|| format!("RoPE row width overflow for {}", tensor.name()))
773 })?;
774 ensure!(
775 head_count > 0 && row_count.is_multiple_of(head_count * 2),
776 "RoPE tensor {} row count {row_count} must be divisible by twice the head count {head_count}",
777 tensor.name()
778 );
779 Ok(TensorTransform::RopePermutation {
780 head_count,
781 row_count,
782 row_elements,
783 })
784}
785
786#[derive(Debug, Clone, Copy, Default)]
787enum HfTensorLayout {
788 #[default]
789 Generic,
790 LlamaLike {
791 head_count: u64,
792 kv_head_count: u64,
793 },
794 GraniteHybrid {
795 head_count: u64,
796 kv_head_count: u64,
797 ssm_group_count: u64,
798 },
799}
800
801impl HfTensorLayout {
802 fn from_checkpoint(source: &Path, tensor_name_map: TensorNameMap) -> Result<Self> {
803 if !matches!(
804 tensor_name_map,
805 TensorNameMap::HfToGguf | TensorNameMap::HfToGgufWithMtp { .. }
806 ) {
807 return Ok(Self::Generic);
808 }
809 let path = source.join("config.json");
810 if !path.is_file() {
811 return Ok(Self::Generic);
812 }
813 let config: Value = serde_json::from_slice(
814 &fs::read(&path).with_context(|| format!("read {}", path.display()))?,
815 )
816 .with_context(|| format!("parse {}", path.display()))?;
817 let model_type = config
818 .get("model_type")
819 .and_then(Value::as_str)
820 .unwrap_or_default();
821 let value_u64 = |key: &str| -> Result<u64> {
822 config
823 .get(key)
824 .and_then(Value::as_u64)
825 .with_context(|| format!("config missing integer {key}"))
826 };
827 let head_count = || value_u64("num_attention_heads");
828 let kv_head_count = || {
829 config
830 .get("num_key_value_heads")
831 .and_then(Value::as_u64)
832 .map(Ok)
833 .unwrap_or_else(head_count)
834 };
835 match model_type {
836 "llama" | "mistral" => Ok(Self::LlamaLike {
837 head_count: head_count()?,
838 kv_head_count: kv_head_count()?,
839 }),
840 "granitemoehybrid" => Ok(Self::GraniteHybrid {
841 head_count: head_count()?,
842 kv_head_count: kv_head_count()?,
843 ssm_group_count: value_u64("mamba_n_groups")?,
844 }),
845 _ => Ok(Self::Generic),
846 }
847 }
848}
849
850fn granite_shared_mlp_tensor_sources(
851 file_index: usize,
852 tensor: &SafetensorTensorInfo,
853 output_type: Option<ConvertOutputType>,
854) -> Result<Vec<TensorSource>> {
855 ensure!(
856 tensor.shape().len() == 2 && tensor.shape()[0].is_multiple_of(2),
857 "Granite Hybrid shared MLP input {} must have shape [2 * intermediate, hidden], got {:?}",
858 tensor.name(),
859 tensor.shape()
860 );
861 let layer = hf_layer_id(tensor.name())?
862 .with_context(|| format!("missing Granite Hybrid layer id in {}", tensor.name()))?;
863 let source_dtype = FloatDType::from_safetensor(tensor.dtype())
864 .with_context(|| format!("unsupported dtype {} for {}", tensor.dtype(), tensor.name()))?;
865 let row_count = tensor.shape()[0] / 2;
866 let row_elements = tensor.shape()[1];
867 let output_shape = [row_count, row_elements];
868 let target_dtype = target_dtype_for_tensor(source_dtype, output_type, &output_shape)?;
869 let element_count = row_count
870 .checked_mul(row_elements)
871 .context("Granite Hybrid shared MLP element count overflow")?;
872 let target_byte_len = tensor_byte_len(element_count, target_dtype)?;
873 let dims = vec![row_elements, row_count];
874 Ok([("ffn_gate", 0_u64), ("ffn_up", row_count)]
875 .into_iter()
876 .map(|(projection, row_start)| TensorSource {
877 segments: vec![TensorSegment {
878 file_index,
879 source_name: tensor.name().to_string(),
880 source_dtype,
881 target_dtype,
882 element_count,
883 source_byte_len: tensor.byte_len(),
884 target_byte_len,
885 transform: TensorTransform::ContiguousRows {
886 row_start,
887 row_count,
888 row_elements,
889 },
890 }],
891 name: format!("blk.{layer}.{projection}.weight"),
892 dims: dims.clone(),
893 ggml_type: ggml_type_for_dtype(target_dtype),
894 byte_len: target_byte_len,
895 gguf_offset: 0,
896 })
897 .collect())
898}
899
900fn inkling_w13_tensor_sources(
901 file_index: usize,
902 tensor: &SafetensorTensorInfo,
903 tensor_name_map: TensorNameMap,
904 output_type: Option<ConvertOutputType>,
905) -> Result<Vec<TensorSource>> {
906 let layer = if let Some(depth) = inkling_mtp_depth(tensor.name())? {
907 let TensorNameMap::HfToGgufWithMtp { layer_start } = tensor_name_map else {
908 anyhow::bail!("Inkling MTP conversion requires an MTP-aware tensor name map");
909 };
910 layer_start
911 .checked_add(depth)
912 .context("Inkling MTP layer id overflow")?
913 } else {
914 ensure!(
915 matches!(
916 tensor_name_map,
917 TensorNameMap::HfToGguf | TensorNameMap::HfToGgufWithMtp { .. }
918 ),
919 "Inkling fused w13 conversion requires an HF tensor name map"
920 );
921 hf_layer_id(tensor.name())?
922 .with_context(|| format!("missing Inkling layer id in {}", tensor.name()))?
923 };
924 ensure!(
925 tensor.shape().len() == 2,
926 "Inkling MTP fused w13 tensor {} must be rank 2, got {:?}",
927 tensor.name(),
928 tensor.shape()
929 );
930 ensure!(
931 tensor.shape()[0].is_multiple_of(2),
932 "Inkling MTP fused w13 tensor {} must have an even row count",
933 tensor.name()
934 );
935 let source_dtype = FloatDType::from_safetensor(tensor.dtype())
936 .with_context(|| format!("unsupported dtype {} for {}", tensor.dtype(), tensor.name()))?;
937 let output_shape = [tensor.shape()[0] / 2, tensor.shape()[1]];
938 let target_dtype = target_dtype_for_tensor(source_dtype, output_type, &output_shape)?;
939 let element_count = output_shape[0]
940 .checked_mul(output_shape[1])
941 .context("Inkling MTP w13 output element count overflow")?;
942 let target_byte_len = tensor_byte_len(element_count, target_dtype)?;
943 let dims = output_shape.iter().rev().copied().collect::<Vec<_>>();
944 Ok([("ffn_gate", 0_u64), ("ffn_up", 1_u64)]
945 .into_iter()
946 .map(|(projection, parity)| TensorSource {
947 segments: vec![TensorSegment {
948 file_index,
949 source_name: tensor.name().to_string(),
950 source_dtype,
951 target_dtype,
952 element_count,
953 source_byte_len: tensor.byte_len(),
954 target_byte_len,
955 transform: TensorTransform::AlternatingRows {
956 parity,
957 row_elements: tensor.shape()[1],
958 },
959 }],
960 name: format!("blk.{layer}.{projection}.weight"),
961 dims: dims.clone(),
962 ggml_type: ggml_type_for_dtype(target_dtype),
963 byte_len: target_byte_len,
964 gguf_offset: 0,
965 })
966 .collect())
967}
968
969struct TensorSegment {
970 file_index: usize,
971 source_name: String,
972 source_dtype: FloatDType,
973 target_dtype: FloatDType,
974 element_count: u64,
975 source_byte_len: u64,
976 target_byte_len: u64,
977 transform: TensorTransform,
978}
979
980#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
981struct ExpertGroupKey {
982 layer: u32,
983 projection: ExpertProjection,
984}
985
986#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
987enum ExpertProjection {
988 Down,
989 Gate,
990 Up,
991}
992
993impl ExpertProjection {
994 fn gguf_name(self, layer: u32) -> String {
995 match self {
996 Self::Down => format!("blk.{layer}.ffn_down_exps.weight"),
997 Self::Gate => format!("blk.{layer}.ffn_gate_exps.weight"),
998 Self::Up => format!("blk.{layer}.ffn_up_exps.weight"),
999 }
1000 }
1001}
1002
1003#[derive(Debug, Clone, Copy)]
1004struct ExpertSourceTensor {
1005 layer: u32,
1006 expert_id: u32,
1007 projection: ExpertProjection,
1008}
1009
1010impl ExpertSourceTensor {
1011 fn parse(name: &str) -> Result<Option<Self>> {
1012 let Some(rest) = name.strip_prefix("model.layers.") else {
1013 return Ok(None);
1014 };
1015 let Some((layer, suffix)) = rest.split_once('.') else {
1016 return Ok(None);
1017 };
1018 let Some(expert_suffix) = suffix.strip_prefix("mlp.experts.") else {
1019 return Ok(None);
1020 };
1021 let Some((expert_id, projection_suffix)) = expert_suffix.split_once('.') else {
1022 return Ok(None);
1023 };
1024 let layer = layer
1025 .parse::<u32>()
1026 .with_context(|| format!("parse expert layer id in {name}"))?;
1027 let expert_id = expert_id
1028 .parse::<u32>()
1029 .with_context(|| format!("parse expert id in {name}"))?;
1030 let projection = match projection_suffix {
1031 "down_proj.weight" => ExpertProjection::Down,
1032 "gate_proj.weight" => ExpertProjection::Gate,
1033 "up_proj.weight" => ExpertProjection::Up,
1034 _ => return Ok(None),
1035 };
1036 Ok(Some(Self {
1037 layer,
1038 expert_id,
1039 projection,
1040 }))
1041 }
1042
1043 fn group_key(self) -> ExpertGroupKey {
1044 ExpertGroupKey {
1045 layer: self.layer,
1046 projection: self.projection,
1047 }
1048 }
1049}
1050
1051struct ExpertGroup {
1052 key: ExpertGroupKey,
1053 source_dtype: FloatDType,
1054 target_dtype: FloatDType,
1055 shape: Vec<u64>,
1056 source_byte_len_per_expert: u64,
1057 target_byte_len_per_expert: u64,
1058 experts: BTreeMap<u32, TensorSegment>,
1059}
1060
1061impl ExpertGroup {
1062 fn new(
1063 key: ExpertGroupKey,
1064 tensor: &SafetensorTensorInfo,
1065 output_type: Option<ConvertOutputType>,
1066 ) -> Result<Self> {
1067 let source_dtype = FloatDType::from_safetensor(tensor.dtype()).with_context(|| {
1068 format!("unsupported dtype {} for {}", tensor.dtype(), tensor.name())
1069 })?;
1070 let mut merged_shape = Vec::with_capacity(tensor.shape().len() + 1);
1075 merged_shape.push(1);
1076 merged_shape.extend_from_slice(tensor.shape());
1077 let target_dtype = target_dtype_for_tensor(source_dtype, output_type, &merged_shape)?;
1078 let element_count = tensor_element_count(tensor)?;
1079 Ok(Self {
1080 key,
1081 source_dtype,
1082 target_dtype,
1083 shape: tensor.shape().to_vec(),
1084 source_byte_len_per_expert: tensor.byte_len(),
1085 target_byte_len_per_expert: tensor_byte_len(element_count, target_dtype)?,
1086 experts: BTreeMap::new(),
1087 })
1088 }
1089
1090 fn push(
1091 &mut self,
1092 file_index: usize,
1093 tensor: &SafetensorTensorInfo,
1094 expert_id: u32,
1095 ) -> Result<()> {
1096 ensure!(
1097 FloatDType::from_safetensor(tensor.dtype()) == Some(self.source_dtype),
1098 "expert tensor {} dtype {} does not match group dtype {:?}",
1099 tensor.name(),
1100 tensor.dtype(),
1101 self.source_dtype
1102 );
1103 ensure!(
1104 tensor.shape() == self.shape,
1105 "expert tensor {} shape {:?} does not match group shape {:?}",
1106 tensor.name(),
1107 tensor.shape(),
1108 self.shape
1109 );
1110 ensure!(
1111 tensor.byte_len() == self.source_byte_len_per_expert,
1112 "expert tensor {} byte length {} does not match group byte length {}",
1113 tensor.name(),
1114 tensor.byte_len(),
1115 self.source_byte_len_per_expert
1116 );
1117 let element_count = tensor_element_count(tensor)?;
1118 let previous = self.experts.insert(
1119 expert_id,
1120 TensorSegment {
1121 file_index,
1122 source_name: tensor.name().to_string(),
1123 source_dtype: self.source_dtype,
1124 target_dtype: self.target_dtype,
1125 element_count,
1126 source_byte_len: tensor.byte_len(),
1127 target_byte_len: tensor_byte_len(element_count, self.target_dtype)?,
1128 transform: TensorTransform::Identity,
1129 },
1130 );
1131 ensure!(
1132 previous.is_none(),
1133 "duplicate expert tensor id {expert_id} for {}",
1134 self.key.projection.gguf_name(self.key.layer)
1135 );
1136 Ok(())
1137 }
1138
1139 fn into_tensor_source(self) -> Result<TensorSource> {
1140 ensure!(
1141 !self.experts.is_empty(),
1142 "expert group {} has no tensors",
1143 self.key.projection.gguf_name(self.key.layer)
1144 );
1145 for (expected, actual) in self.experts.keys().copied().enumerate() {
1146 ensure!(
1147 expected as u32 == actual,
1148 "expert group {} is missing expert id {}",
1149 self.key.projection.gguf_name(self.key.layer),
1150 expected
1151 );
1152 }
1153 let expert_count = self.experts.len() as u64;
1154 let mut dims = self.shape.iter().rev().copied().collect::<Vec<_>>();
1155 dims.push(expert_count);
1156 let byte_len = self
1157 .target_byte_len_per_expert
1158 .checked_mul(expert_count)
1159 .with_context(|| {
1160 format!(
1161 "expert group {} byte length overflow",
1162 self.key.projection.gguf_name(self.key.layer)
1163 )
1164 })?;
1165 Ok(TensorSource {
1166 segments: self.experts.into_values().collect(),
1167 name: self.key.projection.gguf_name(self.key.layer),
1168 dims,
1169 ggml_type: ggml_type_for_dtype(self.target_dtype),
1170 byte_len,
1171 gguf_offset: 0,
1172 })
1173 }
1174}
1175
1176fn tensor_element_count(tensor: &SafetensorTensorInfo) -> Result<u64> {
1177 tensor.shape().iter().try_fold(1_u64, |acc, dim| {
1178 acc.checked_mul(*dim)
1179 .with_context(|| format!("tensor {} element count overflow", tensor.name()))
1180 })
1181}
1182
1183fn tensor_byte_len(element_count: u64, dtype: FloatDType) -> Result<u64> {
1184 element_count
1185 .checked_mul(dtype.byte_size())
1186 .context("target tensor byte length overflow")
1187}
1188
1189fn ggml_type_for_dtype(dtype: FloatDType) -> u32 {
1190 match dtype {
1191 FloatDType::F32 => GGML_TYPE_F32,
1192 FloatDType::F16 => GGML_TYPE_F16,
1193 FloatDType::Bf16 => GGML_TYPE_BF16,
1194 }
1195}
1196
1197fn raw_metadata(source: &Path, tensor_count: usize) -> Vec<GgufKv> {
1198 vec![
1199 GgufKv::string("general.architecture", "raw-safetensors"),
1200 GgufKv::string(
1201 "general.name",
1202 source
1203 .file_name()
1204 .and_then(|value| value.to_str())
1205 .unwrap_or("checkpoint"),
1206 ),
1207 GgufKv::bool("skippy.convert.raw_safetensors", true),
1208 GgufKv::u64("skippy.convert.tensor_count", tensor_count as u64),
1209 ]
1210}
1211
1212fn write_header_and_tensor_table<W: Write>(
1213 writer: &mut W,
1214 metadata: &[GgufKv],
1215 tensors: &[TensorSource],
1216) -> Result<()> {
1217 writer.write_all(GGUF_MAGIC)?;
1218 write_u32(writer, GGUF_VERSION)?;
1219 write_u64(writer, tensors.len() as u64)?;
1220 write_u64(writer, metadata.len() as u64)?;
1221 for kv in metadata {
1222 write_kv(writer, kv)?;
1223 }
1224 for tensor in tensors {
1225 write_string(writer, &tensor.name)?;
1226 write_u32(writer, tensor.dims.len() as u32)?;
1227 for dim in &tensor.dims {
1228 write_u64(writer, *dim)?;
1229 }
1230 write_u32(writer, tensor.ggml_type)?;
1231 write_u64(writer, tensor.gguf_offset)?;
1232 }
1233 Ok(())
1234}
1235
1236fn stream_tensor_data(
1237 writer: &mut File,
1238 files: &[SafetensorFile],
1239 tensors: &[TensorSource],
1240 buffer_size: usize,
1241) -> Result<()> {
1242 pad_writer_to_alignment(writer, GGUF_ALIGNMENT)?;
1243 let data_start = writer.stream_position()?;
1244 for tensor in tensors {
1245 let expected_position = data_start + tensor.gguf_offset;
1246 pad_writer_to_position(writer, expected_position)?;
1247 let mut copied = 0_u64;
1248 for segment in &tensor.segments {
1249 let segment_copied =
1250 stream_segment(writer, &files[segment.file_index], segment, buffer_size)?;
1251 ensure!(
1252 segment_copied == segment.target_byte_len,
1253 "copied {} bytes for {}, expected {}",
1254 segment_copied,
1255 segment.source_name,
1256 segment.target_byte_len
1257 );
1258 copied += segment_copied;
1259 }
1260 ensure!(
1261 copied == tensor.byte_len,
1262 "copied {} bytes for {}, expected {}",
1263 copied,
1264 tensor.name,
1265 tensor.byte_len
1266 );
1267 }
1268 Ok(())
1269}
1270
1271fn stream_segment<W: Write>(
1272 writer: &mut W,
1273 file: &SafetensorFile,
1274 segment: &TensorSegment,
1275 buffer_size: usize,
1276) -> Result<u64> {
1277 if let TensorTransform::AlternatingRows {
1278 parity,
1279 row_elements,
1280 } = segment.transform
1281 {
1282 return stream_alternating_rows(writer, file, segment, buffer_size, parity, row_elements);
1283 }
1284 if let TensorTransform::ContiguousRows {
1285 row_start,
1286 row_count,
1287 row_elements,
1288 } = segment.transform
1289 {
1290 return stream_contiguous_rows(
1291 writer,
1292 file,
1293 segment,
1294 buffer_size,
1295 row_start,
1296 row_count,
1297 row_elements,
1298 );
1299 }
1300 if let TensorTransform::RopePermutation {
1301 head_count,
1302 row_count,
1303 row_elements,
1304 } = segment.transform
1305 {
1306 return stream_rope_permutation(
1307 writer,
1308 file,
1309 segment,
1310 buffer_size,
1311 head_count,
1312 row_count,
1313 row_elements,
1314 );
1315 }
1316 if matches!(segment.transform, TensorTransform::NegativeExp) {
1317 return stream_negative_exp(writer, file, segment, buffer_size);
1318 }
1319 if let Some(written) = stream_transformed_segment(writer, file, segment, buffer_size)? {
1320 return Ok(written);
1321 }
1322
1323 if segment.source_dtype == segment.target_dtype {
1324 let copied = file.stream_tensor(&segment.source_name, writer, buffer_size)?;
1325 ensure!(
1326 copied == segment.source_byte_len,
1327 "read {} bytes for {}, expected {}",
1328 copied,
1329 segment.source_name,
1330 segment.source_byte_len
1331 );
1332 return Ok(copied);
1333 }
1334
1335 let source_element_size = usize::try_from(segment.source_dtype.byte_size())
1336 .context("source dtype byte size does not fit usize")?;
1337 let chunk_size = aligned_chunk_size(buffer_size, source_element_size);
1338 let mut output_bytes = 0_u64;
1339 let mut source_bytes = 0_u64;
1340 file.stream_tensor_chunks(&segment.source_name, chunk_size, |chunk| {
1341 ensure!(
1342 chunk.len() % source_element_size == 0,
1343 "chunk for {} split an element boundary",
1344 segment.source_name
1345 );
1346 source_bytes += chunk.len() as u64;
1347 output_bytes +=
1348 convert_float_chunk(chunk, segment.source_dtype, segment.target_dtype, writer)?;
1349 Ok(())
1350 })?;
1351 ensure!(
1352 source_bytes == segment.source_byte_len,
1353 "read {} bytes for {}, expected {}",
1354 source_bytes,
1355 segment.source_name,
1356 segment.source_byte_len
1357 );
1358 ensure!(
1359 source_bytes / segment.source_dtype.byte_size() == segment.element_count,
1360 "read element count mismatch for {}",
1361 segment.source_name
1362 );
1363 Ok(output_bytes)
1364}
1365
1366fn stream_alternating_rows<W: Write>(
1369 writer: &mut W,
1370 file: &SafetensorFile,
1371 segment: &TensorSegment,
1372 buffer_size: usize,
1373 parity: u64,
1374 row_elements: u64,
1375) -> Result<u64> {
1376 ensure!(parity < 2, "alternating-row parity must be zero or one");
1377 ensure!(row_elements > 0, "alternating-row width must be non-zero");
1378 let row_bytes = row_elements
1379 .checked_mul(segment.source_dtype.byte_size())
1380 .context("alternating-row byte length overflow")?;
1381 let row_bytes = usize::try_from(row_bytes).context("row byte length does not fit usize")?;
1382 let chunk_size = aligned_chunk_size(buffer_size, row_bytes);
1383 let mut source_bytes = 0_u64;
1384 let mut output_bytes = 0_u64;
1385 let mut row_index = 0_u64;
1386 file.stream_tensor_chunks(&segment.source_name, chunk_size, |chunk| {
1387 ensure!(
1388 chunk.len() % row_bytes == 0,
1389 "chunk for {} split a fused SwiGLU row",
1390 segment.source_name
1391 );
1392 source_bytes += chunk.len() as u64;
1393 for row in chunk.chunks_exact(row_bytes) {
1394 if row_index % 2 == parity {
1395 output_bytes +=
1396 convert_float_chunk(row, segment.source_dtype, segment.target_dtype, writer)?;
1397 }
1398 row_index += 1;
1399 }
1400 Ok(())
1401 })?;
1402 ensure!(
1403 source_bytes == segment.source_byte_len,
1404 "read {} bytes for {}, expected {}",
1405 source_bytes,
1406 segment.source_name,
1407 segment.source_byte_len
1408 );
1409 ensure!(
1410 output_bytes == segment.target_byte_len,
1411 "deinterleaved {} bytes for {}, expected {}",
1412 output_bytes,
1413 segment.source_name,
1414 segment.target_byte_len
1415 );
1416 Ok(output_bytes)
1417}
1418
1419fn stream_contiguous_rows<W: Write>(
1420 writer: &mut W,
1421 file: &SafetensorFile,
1422 segment: &TensorSegment,
1423 buffer_size: usize,
1424 row_start: u64,
1425 row_count: u64,
1426 row_elements: u64,
1427) -> Result<u64> {
1428 ensure!(row_count > 0, "contiguous-row count must be non-zero");
1429 ensure!(row_elements > 0, "contiguous-row width must be non-zero");
1430 let source = read_segment_source(file, segment, buffer_size)?;
1431 let source_width = segment.source_dtype.byte_size();
1432 let start_element = row_start
1433 .checked_mul(row_elements)
1434 .context("contiguous-row start overflow")?;
1435 let element_count = row_count
1436 .checked_mul(row_elements)
1437 .context("contiguous-row element count overflow")?;
1438 let start_byte = start_element
1439 .checked_mul(source_width)
1440 .context("contiguous-row byte start overflow")?;
1441 let byte_len = element_count
1442 .checked_mul(source_width)
1443 .context("contiguous-row byte length overflow")?;
1444 let end_byte = start_byte
1445 .checked_add(byte_len)
1446 .context("contiguous-row byte end overflow")?;
1447 let start = usize::try_from(start_byte).context("contiguous-row start does not fit usize")?;
1448 let end = usize::try_from(end_byte).context("contiguous-row end does not fit usize")?;
1449 ensure!(
1450 end <= source.len(),
1451 "contiguous-row range {start}..{end} exceeds {} source bytes for {}",
1452 source.len(),
1453 segment.source_name
1454 );
1455 let written = if segment.source_dtype == segment.target_dtype {
1456 writer.write_all(&source[start..end])?;
1457 byte_len
1458 } else {
1459 convert_float_chunk(
1460 &source[start..end],
1461 segment.source_dtype,
1462 segment.target_dtype,
1463 writer,
1464 )?
1465 };
1466 ensure!(
1467 written == segment.target_byte_len,
1468 "wrote {written} bytes for contiguous rows of {}, expected {}",
1469 segment.source_name,
1470 segment.target_byte_len
1471 );
1472 Ok(written)
1473}
1474
1475fn stream_rope_permutation<W: Write>(
1476 writer: &mut W,
1477 file: &SafetensorFile,
1478 segment: &TensorSegment,
1479 buffer_size: usize,
1480 head_count: u64,
1481 row_count: u64,
1482 row_elements: u64,
1483) -> Result<u64> {
1484 ensure!(head_count > 0, "RoPE head count must be non-zero");
1485 ensure!(row_elements > 0, "RoPE row width must be non-zero");
1486 ensure!(
1487 row_count.is_multiple_of(head_count * 2),
1488 "RoPE row count {row_count} must be divisible by twice head count {head_count}"
1489 );
1490 let source = read_segment_source(file, segment, buffer_size)?;
1491 let head_dim = row_count / head_count;
1492 let source_element_count = row_count
1493 .checked_mul(row_elements)
1494 .context("RoPE tensor element count overflow")?;
1495 ensure!(
1496 source_element_count == segment.element_count,
1497 "RoPE transform element count {source_element_count} does not match {} for {}",
1498 segment.element_count,
1499 segment.source_name
1500 );
1501 let flush_limit = buffer_size.max(segment.target_dtype.byte_size() as usize);
1502 let mut output = Vec::with_capacity(flush_limit);
1503 let mut written = 0_u64;
1504 for head in 0..head_count {
1505 for target_row_in_head in 0..head_dim {
1506 let source_row_in_head =
1507 (target_row_in_head % 2) * (head_dim / 2) + target_row_in_head / 2;
1508 let source_row = head * head_dim + source_row_in_head;
1509 for column in 0..row_elements {
1510 let source_index = source_row
1511 .checked_mul(row_elements)
1512 .and_then(|value| value.checked_add(column))
1513 .context("RoPE source index overflow")?;
1514 let source_index = usize::try_from(source_index)
1515 .context("RoPE source index does not fit usize")?;
1516 ensure!(
1517 source_index < segment.element_count as usize,
1518 "RoPE source index {source_index} exceeds {} elements",
1519 segment.element_count
1520 );
1521 write_float_element(
1522 &mut output,
1523 segment.target_dtype,
1524 read_float_element(&source, segment.source_dtype, source_index),
1525 );
1526 written += segment.target_dtype.byte_size();
1527 if output.len() >= flush_limit {
1528 writer.write_all(&output)?;
1529 output.clear();
1530 }
1531 }
1532 }
1533 }
1534 writer.write_all(&output)?;
1535 ensure!(
1536 written == segment.target_byte_len,
1537 "wrote {written} bytes for RoPE permutation of {}, expected {}",
1538 segment.source_name,
1539 segment.target_byte_len
1540 );
1541 Ok(written)
1542}
1543
1544fn stream_negative_exp<W: Write>(
1545 writer: &mut W,
1546 file: &SafetensorFile,
1547 segment: &TensorSegment,
1548 buffer_size: usize,
1549) -> Result<u64> {
1550 let source = read_segment_source(file, segment, buffer_size)?;
1551 let flush_limit = buffer_size.max(segment.target_dtype.byte_size() as usize);
1552 let mut output = Vec::with_capacity(flush_limit);
1553 let mut written = 0_u64;
1554 let element_count = usize::try_from(segment.element_count)
1555 .context("negative-exp element count does not fit usize")?;
1556 for index in 0..element_count {
1557 let value = -read_float_element(&source, segment.source_dtype, index).exp();
1558 write_float_element(&mut output, segment.target_dtype, value);
1559 written += segment.target_dtype.byte_size();
1560 if output.len() >= flush_limit {
1561 writer.write_all(&output)?;
1562 output.clear();
1563 }
1564 }
1565 writer.write_all(&output)?;
1566 ensure!(
1567 written == segment.target_byte_len,
1568 "wrote {written} bytes for negative-exp transform of {}, expected {}",
1569 segment.source_name,
1570 segment.target_byte_len
1571 );
1572 Ok(written)
1573}
1574
1575fn read_segment_source(
1576 file: &SafetensorFile,
1577 segment: &TensorSegment,
1578 buffer_size: usize,
1579) -> Result<Vec<u8>> {
1580 let mut source = Vec::with_capacity(
1581 usize::try_from(segment.source_byte_len)
1582 .context("transformed source byte length does not fit usize")?,
1583 );
1584 file.stream_tensor_chunks(&segment.source_name, buffer_size, |chunk| {
1585 source.extend_from_slice(chunk);
1586 Ok(())
1587 })?;
1588 ensure!(
1589 source.len() as u64 == segment.source_byte_len,
1590 "read {} bytes for {}, expected {}",
1591 source.len(),
1592 segment.source_name,
1593 segment.source_byte_len
1594 );
1595 Ok(source)
1596}
1597
1598fn aligned_chunk_size(buffer_size: usize, element_size: usize) -> usize {
1599 let aligned = buffer_size - (buffer_size % element_size);
1600 aligned.max(element_size)
1601}
1602
1603fn pad_writer_to_alignment(writer: &mut File, alignment: u64) -> Result<()> {
1604 let position = writer.stream_position()?;
1605 pad_writer_to_position(writer, align_to(position, alignment))
1606}
1607
1608fn pad_writer_to_position(writer: &mut File, position: u64) -> Result<()> {
1609 let current = writer.stream_position()?;
1610 ensure!(
1611 current <= position,
1612 "writer is past requested output position {position}"
1613 );
1614 let mut remaining = position - current;
1615 let zeros = [0_u8; 4096];
1616 while remaining > 0 {
1617 let write_len = zeros.len().min(remaining as usize);
1618 writer.write_all(&zeros[..write_len])?;
1619 remaining -= write_len as u64;
1620 }
1621 Ok(())
1622}
1623
1624fn align_to(value: u64, alignment: u64) -> u64 {
1625 if alignment <= 1 {
1626 return value;
1627 }
1628 value.div_ceil(alignment) * alignment
1629}
1630
1631fn write_string<W: Write>(writer: &mut W, value: &str) -> Result<()> {
1632 write_u64(writer, value.len() as u64)?;
1633 writer.write_all(value.as_bytes())?;
1634 Ok(())
1635}
1636
1637fn write_u32<W: Write>(writer: &mut W, value: u32) -> Result<()> {
1638 writer.write_all(&value.to_le_bytes())?;
1639 Ok(())
1640}
1641
1642fn write_u64<W: Write>(writer: &mut W, value: u64) -> Result<()> {
1643 writer.write_all(&value.to_le_bytes())?;
1644 Ok(())
1645}
1646
1647#[cfg(test)]
1648#[path = "gguf_writer_tests.rs"]
1649mod tests;