Skip to main content

skippy_model/
gguf_writer.rs

1//! Canonical GGUF metadata and streaming tensor preparation.
2
3use 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
164/// A validated Hugging Face checkpoint prepared for direct runtime loading.
165///
166/// The object owns immutable mappings for every SafeTensors shard. Its GGUF
167/// buffer contains metadata and tensor descriptors only; tensor bytes are read
168/// from the source mappings on demand.
169pub 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    /// Open a checkpoint directory or SafeTensors file and prepare canonical
184    /// llama.cpp metadata and names.
185    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    /// Metadata-only GGUF consumed by the native model constructor.
235    pub fn metadata_gguf(&self) -> &[u8] {
236        &self.metadata_gguf
237    }
238
239    /// Number of canonical tensors exposed by this checkpoint.
240    pub fn tensor_count(&self) -> usize {
241        self.tensors.len()
242    }
243
244    /// Canonical tensor names and importance-vector widths expected by llama.cpp.
245    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    /// Decode one canonical tensor into the caller-provided F32 destination.
275    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        // The expert dimension is appended when the group becomes a GGUF
1071        // tensor. Choose precision from that merged shape so rank-one source
1072        // shards retain matrix precision while scalar shards become F32
1073        // rank-one runtime operands.
1074        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
1366/// Deinterleave alternating rows of a fused SwiGLU tensor (Inkling MTP fused
1367/// w13): parity 0 keeps even rows (gate), parity 1 keeps odd rows (up).
1368fn 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;