Skip to main content

combs_formats/
source.rs

1//! The format-agnostic [`ModelSource`] trait and tensor reader.
2
3use burn::tensor::{Device, Int, Tensor, TensorData, backend::Backend};
4
5use crate::metadata::ModelMetadata;
6use crate::tokenizer::TokenizerSpec;
7use crate::{FormatError, Result};
8
9/// The central adapter trait: a source of model weights + config, independent
10/// of the on-disk format (LiteRT-LM `ModelResources` equivalent).
11///
12/// Implementations must be cheap to query for metadata and names, and lazy /
13/// zero-copy (e.g. mmap-backed) when opening tensors.
14pub trait ModelSource: Send + Sync {
15    /// Architecture + hyperparameter metadata.
16    fn metadata(&self) -> &ModelMetadata;
17
18    /// Names of all tensors available in this source.
19    fn tensor_names(&self) -> Vec<String>;
20
21    /// Opens a tensor by name, returning a lazy reader over the raw bytes.
22    fn open_tensor(&self, name: &str) -> Result<TensorReader<'_>>;
23
24    /// Tokenizer specification (path to `tokenizer.json` + added tokens).
25    fn tokenizer(&self) -> Result<TokenizerSpec>;
26
27    /// Sampler defaults from `generation_config.json`, if present.
28    fn sampler_defaults(&self) -> Option<SamplerConfig>;
29}
30
31/// Element dtypes supported by the loaders.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum TensorDtype {
34    /// IEEE 32-bit float.
35    F32,
36    /// IEEE 16-bit half float.
37    F16,
38    /// bfloat16.
39    BF16,
40    /// Unsigned 8-bit integer (raw packed data, e.g. quantized weights).
41    U8,
42}
43
44impl TensorDtype {
45    /// Byte size of one element.
46    pub fn size(&self) -> usize {
47        match self {
48            TensorDtype::F32 => 4,
49            TensorDtype::F16 | TensorDtype::BF16 => 2,
50            TensorDtype::U8 => 1,
51        }
52    }
53}
54
55impl std::fmt::Display for TensorDtype {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            TensorDtype::F32 => write!(f, "F32"),
59            TensorDtype::F16 => write!(f, "F16"),
60            TensorDtype::BF16 => write!(f, "BF16"),
61            TensorDtype::U8 => write!(f, "U8"),
62        }
63    }
64}
65
66/// A lazy view over one tensor's raw bytes inside a [`ModelSource`].
67///
68/// The byte slice borrows from the source (e.g. an mmap region) — no copy is
69/// made until [`TensorReader::load_data`] is called. Format adapters that
70/// decode on open (e.g. GGUF quantization) use [`TensorReader::owned`].
71pub struct TensorReader<'a> {
72    name: String,
73    shape: Vec<usize>,
74    dtype: TensorDtype,
75    data: std::borrow::Cow<'a, [u8]>,
76}
77
78impl<'a> TensorReader<'a> {
79    /// Creates a reader from raw parts. `data` must be
80    /// `shape.iter().product::<usize>() * dtype.size()` little-endian bytes.
81    pub fn new(name: String, shape: Vec<usize>, dtype: TensorDtype, data: &'a [u8]) -> Self {
82        TensorReader {
83            name,
84            shape,
85            dtype,
86            data: std::borrow::Cow::Borrowed(data),
87        }
88    }
89
90    /// Creates a reader over owned (already-decoded) f32 bytes.
91    pub fn owned(name: String, shape: Vec<usize>, data: Vec<u8>) -> Self {
92        TensorReader {
93            name,
94            shape,
95            dtype: TensorDtype::F32,
96            data: std::borrow::Cow::Owned(data),
97        }
98    }
99
100    /// Element shape.
101    pub fn shape(&self) -> &[usize] {
102        &self.shape
103    }
104
105    /// On-disk dtype.
106    pub fn dtype(&self) -> TensorDtype {
107        self.dtype
108    }
109
110    /// Number of elements.
111    pub fn num_elements(&self) -> usize {
112        self.shape.iter().product()
113    }
114
115    /// Converts the raw bytes to f32 `TensorData` (F16/BF16 are widened, F32
116    /// is a straight reinterpretation of the little-endian bytes).
117    pub fn load_data(&self) -> Result<TensorData> {
118        let values: Vec<f32> = match self.dtype {
119            TensorDtype::F32 => self
120                .data
121                .chunks_exact(4)
122                .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
123                .collect(),
124            TensorDtype::F16 => self
125                .data
126                .chunks_exact(2)
127                .map(|c| half::f16::from_le_bytes([c[0], c[1]]).to_f32())
128                .collect(),
129            TensorDtype::BF16 => self
130                .data
131                .chunks_exact(2)
132                .map(|c| half::bf16::from_le_bytes([c[0], c[1]]).to_f32())
133                .collect(),
134            TensorDtype::U8 => self.data.iter().map(|&b| b as f32).collect(),
135        };
136        if values.len() != self.num_elements() {
137            return Err(FormatError::Safetensors(format!(
138                "tensor {}: expected {} elements, got {}",
139                self.name,
140                self.num_elements(),
141                values.len()
142            )));
143        }
144        Ok(TensorData::new(values, self.shape.clone()))
145    }
146
147    /// Loads the tensor onto a backend device as an f32 tensor of rank `D`.
148    pub fn load_to_tensor<B: Backend, const D: usize>(
149        &self,
150        device: &Device<B>,
151    ) -> Result<Tensor<B, D>> {
152        let data = self.load_data()?;
153        if self.shape.len() != D {
154            return Err(FormatError::Safetensors(format!(
155                "tensor {}: expected rank {D}, got {}",
156                self.name,
157                self.shape.len()
158            )));
159        }
160        Ok(Tensor::from_data(data, device))
161    }
162
163    /// Loads raw unsigned-byte data onto a backend device as an i32 tensor
164    /// of rank `D` (values 0..=255). Only valid for `U8` tensors; used to
165    /// feed packed quantized weights to GPU-side dequantization.
166    pub fn load_int_tensor<B: Backend, const D: usize>(
167        &self,
168        device: &Device<B>,
169    ) -> Result<Tensor<B, D, Int>> {
170        if self.dtype != TensorDtype::U8 {
171            return Err(FormatError::Safetensors(format!(
172                "tensor {}: load_int_tensor requires U8, got {}",
173                self.name, self.dtype
174            )));
175        }
176        if self.shape.len() != D {
177            return Err(FormatError::Safetensors(format!(
178                "tensor {}: expected rank {D}, got {}",
179                self.name,
180                self.shape.len()
181            )));
182        }
183        let values: Vec<i32> = self.data.iter().map(|&b| b as i32).collect();
184        Ok(Tensor::from_data(
185            TensorData::new(values, self.shape.clone()),
186            device,
187        ))
188    }
189
190    /// The raw little-endian bytes, borrowing from the source (zero-copy).
191    pub fn raw_bytes(&self) -> &[u8] {
192        &self.data
193    }
194}
195
196/// Blanket forwarding so `Box<dyn ModelSource>` (returned by
197/// `open_model_source`) can be passed anywhere a `&dyn ModelSource` goes.
198impl<T: ModelSource + ?Sized> ModelSource for Box<T> {
199    fn metadata(&self) -> &crate::ModelMetadata {
200        (**self).metadata()
201    }
202    fn tensor_names(&self) -> Vec<String> {
203        (**self).tensor_names()
204    }
205    fn open_tensor(&self, name: &str) -> crate::Result<TensorReader<'_>> {
206        (**self).open_tensor(name)
207    }
208    fn tokenizer(&self) -> crate::Result<TokenizerSpec> {
209        (**self).tokenizer()
210    }
211    fn sampler_defaults(&self) -> Option<SamplerConfig> {
212        (**self).sampler_defaults()
213    }
214}
215
216/// Default sampler parameters, typically from `generation_config.json`.
217#[derive(Debug, Clone, Default)]
218pub struct SamplerConfig {    /// Sampling temperature (1.0 = neutral, 0.0 = greedy).
219    pub temperature: Option<f32>,
220    /// Top-p (nucleus) threshold.
221    pub top_p: Option<f32>,
222    /// Top-k cutoff.
223    pub top_k: Option<usize>,
224    /// Repetition penalty.
225    pub repetition_penalty: Option<f32>,
226    /// Suggested maximum new tokens.
227    pub max_new_tokens: Option<usize>,
228}