Skip to main content

kopitiam_runtime/
bridge.rs

1//! The loader/tensor bridge: turning a [`LoadedModel`]'s raw bytes into
2//! [`Tensor`]s.
3//!
4//! `kopitiam-loader` and `kopitiam-tensor` were built concurrently and
5//! deliberately do not depend on each other (see `kopitiam-loader`'s crate
6//! docs, "Why this crate never constructs a `Tensor`"). `LoadedModel` hands
7//! back a [`TensorEntry`] (name, [`DType`], [`Shape`]) plus raw on-disk
8//! bytes via [`LoadedModel::tensor_bytes`]; this module is the one place in
9//! the workspace that combines the two into a real [`Tensor`], because
10//! `kopitiam-runtime` is the first crate that depends on both.
11//!
12//! # Byte order
13//!
14//! Every numeric format this module decodes (`f32`, raw `f16`/`bf16` bits,
15//! `i32`) is read little-endian. GGUF's spec is explicit that little-endian
16//! is the only format loaders need to support in practice (see
17//! `kopitiam_loader::gguf`'s module docs), and SafeTensors likewise stores
18//! tensors little-endian. Block-quantized formats ([`DType::is_quantized`])
19//! need no byte-order handling here at all: their bytes are handed to
20//! [`Tensor::from_quantized`] unmodified, and [`crate::quant`]-style
21//! decoding (inside `kopitiam-tensor`) owns interpreting the block layout.
22
23use kopitiam_core::{DType, Error, Result};
24use kopitiam_loader::{LoadedModel, TensorEntry};
25use kopitiam_tensor::Tensor;
26
27/// Builds a [`Tensor`] from one [`TensorEntry`]'s bytes, preserving its
28/// on-disk [`DType`] exactly (still block-quantized if `entry.dtype` is
29/// quantized, still `f16`/`bf16` if the file stored it that way).
30///
31/// Call [`Tensor::to_dtype`]`(DType::F32)` on the result to get a tensor the
32/// rest of `kopitiam-tensor`'s ops (which are `f32`-only, see that crate's
33/// docs) can actually compute on; [`load_tensor_f32`] does exactly that in
34/// one step; it is what every weight-loading call site in this crate uses.
35pub fn tensor_from_entry(model: &LoadedModel, entry: &TensorEntry) -> Result<Tensor> {
36    let bytes = model.tensor_bytes(&entry.name)?;
37    match entry.dtype {
38        DType::F32 => Tensor::from_f32(read_f32_le(bytes), entry.shape.clone()),
39        DType::F16 => Tensor::from_f16(read_u16_le(bytes), entry.shape.clone()),
40        DType::BF16 => Tensor::from_bf16(read_u16_le(bytes), entry.shape.clone()),
41        DType::I8 => Tensor::from_i8(bytes.iter().map(|&b| b as i8).collect(), entry.shape.clone()),
42        DType::I32 => Tensor::from_i32(read_i32_le(bytes), entry.shape.clone()),
43        DType::Q4_0
44        | DType::Q4_1
45        | DType::Q5_0
46        | DType::Q5_1
47        | DType::Q8_0
48        | DType::Q2_K
49        | DType::Q3_K
50        | DType::Q4_K
51        | DType::Q5_K
52        | DType::Q6_K
53        | DType::Q8_K => Tensor::from_quantized(entry.dtype, bytes.to_vec(), entry.shape.clone()),
54    }
55}
56
57/// Looks up `name` in `model`, builds a [`Tensor`] from its bytes, and
58/// dequantizes/upcasts it to `f32` — the dtype every op in `kopitiam-tensor`
59/// actually computes in. This is the call every weight-loading site in
60/// [`crate::weights`] goes through: model files may ship `f16`, `bf16`, or
61/// any of the five GGUF block-quantized formats, and this one function
62/// makes the rest of the forward pass indifferent to which.
63///
64/// # Errors
65///
66/// [`Error::MissingTensor`] if `name` is not present in `model`; whatever
67/// [`Tensor::to_dtype`] would return for a dtype this crate cannot
68/// dequantize (today, every [`DType`] this loader can produce *can* be
69/// dequantized to `f32`, so this is unreachable in practice, not a real gap
70/// — see [`Tensor::to_dtype`]'s docs).
71pub fn load_tensor_f32(model: &LoadedModel, name: &str) -> Result<Tensor> {
72    let entry = model
73        .tensor(name)
74        .ok_or_else(|| Error::MissingTensor { name: name.to_string() })?;
75    tensor_from_entry(model, entry)?.to_dtype(DType::F32)
76}
77
78/// Like [`load_tensor_f32`], but returns `Ok(None)` instead of
79/// [`Error::MissingTensor`] when `name` is absent.
80///
81/// Exists for optional weights: per-projection attention biases (present in
82/// Qwen2, absent in plain LLaMA) and the output projection (absent when it
83/// is tied to the token embedding — see [`crate::weights::ModelWeights`]'s
84/// docs on tied embeddings).
85pub fn load_tensor_f32_opt(model: &LoadedModel, name: &str) -> Result<Option<Tensor>> {
86    match model.tensor(name) {
87        Some(entry) => Ok(Some(tensor_from_entry(model, entry)?.to_dtype(DType::F32)?)),
88        None => Ok(None),
89    }
90}
91
92/// Loads `name` the way every *matmul-operand* weight
93/// (`wq`/`wk`/`wv`/`wo`, the SwiGLU MLP's three matrices, and
94/// `output.weight` — see [`crate::weights::LayerWeights`] /
95/// [`crate::weights::ModelWeights`]) should: keeping a block-quantized
96/// on-disk dtype *only when there is a fused kernel to compute on it*, and
97/// otherwise dequantizing it to `f32` at load.
98///
99/// # Why this is a separate function from [`load_tensor_f32`], not a flag
100///
101/// Every weight in a Qwen checkpoint is *not* interchangeable for this
102/// purpose. Token embeddings go through [`kopitiam_tensor::Tensor::gather_rows`]
103/// (row-indexed lookup) and norm weights go through elementwise
104/// arithmetic — both require [`kopitiam_tensor::DType::is_quantized`] to
105/// be false (see that crate's docs on why a quantized tensor cannot be
106/// indexed elementwise at all), so those call sites must keep using
107/// [`load_tensor_f32`]. Only the seven weight matrices that feed
108/// [`crate::linear::linear`] (a matmul) benefit from staying quantized, so
109/// this is a distinct, narrowly-named function rather than a boolean
110/// parameter that would let a caller accidentally quantize an embedding or a
111/// norm.
112///
113/// # Why only *some* quantized dtypes stay quantized
114///
115/// [`kopitiam_tensor::Tensor::quantized_matmul`] has a fused,
116/// dequantize-free kernel for only [`DType::Q4_0`] and [`DType::Q8_0`]
117/// today (see [`kopitiam_tensor::has_fused_matmul_kernel`], the shared
118/// predicate this function keys off so the load-time and compute-time
119/// decisions cannot drift). A weight in any *other* quantized format —
120/// `Q4_1`/`Q5_0`/`Q5_1` or a K-quant (`Q4_K`/`Q5_K`/`Q6_K`/...) — has no
121/// fused kernel, so leaving it quantized would dead-end
122/// [`crate::linear::linear`] in an [`Error::UnsupportedDType`]. Those are
123/// dequantized to `f32` here so the forward pass takes the ordinary
124/// [`kopitiam_tensor::Tensor::matmul`] path. This mirrors what happens to
125/// `f16`/`bf16` weights (also upcast to `f32`, no half-precision matmul
126/// kernel exists yet): `f32` is this crate's fallback for any dtype without
127/// a fused kernel. When a GPU backend later fuses more formats, only
128/// `has_fused_matmul_kernel` changes and those formats begin staying
129/// quantized here automatically.
130///
131/// # Errors
132///
133/// [`Error::MissingTensor`] if `name` is not present in `model`.
134pub fn load_matmul_weight(model: &LoadedModel, name: &str) -> Result<Tensor> {
135    let entry = model
136        .tensor(name)
137        .ok_or_else(|| Error::MissingTensor { name: name.to_string() })?;
138    let tensor = tensor_from_entry(model, entry)?;
139    keep_quantized_or_dequantize(tensor)
140}
141
142/// Like [`load_matmul_weight`], but returns `Ok(None)` instead of
143/// [`Error::MissingTensor`] when `name` is absent — the matmul-operand
144/// counterpart to [`load_tensor_f32_opt`], for the same optional weights
145/// (per-projection attention biases, `output.weight` when tied to the
146/// token embedding).
147pub fn load_matmul_weight_opt(model: &LoadedModel, name: &str) -> Result<Option<Tensor>> {
148    match model.tensor(name) {
149        Some(entry) => {
150            let tensor = tensor_from_entry(model, entry)?;
151            Ok(Some(keep_quantized_or_dequantize(tensor)?))
152        }
153        None => Ok(None),
154    }
155}
156
157/// Keeps `tensor` as-is when its dtype has a fused
158/// [`kopitiam_tensor::Tensor::quantized_matmul`] kernel, and otherwise
159/// dequantizes/upcasts it to `f32`. The one place [`load_matmul_weight`] and
160/// [`load_matmul_weight_opt`] encode the load-time half of the invariant
161/// [`kopitiam_tensor::has_fused_matmul_kernel`] anchors.
162fn keep_quantized_or_dequantize(tensor: Tensor) -> Result<Tensor> {
163    if kopitiam_tensor::has_fused_matmul_kernel(tensor.dtype()) {
164        Ok(tensor)
165    } else {
166        tensor.to_dtype(DType::F32)
167    }
168}
169
170fn read_f32_le(bytes: &[u8]) -> Vec<f32> {
171    bytes.chunks_exact(4).map(|c| f32::from_le_bytes(c.try_into().expect("chunks_exact(4)"))).collect()
172}
173
174fn read_u16_le(bytes: &[u8]) -> Vec<u16> {
175    bytes.chunks_exact(2).map(|c| u16::from_le_bytes(c.try_into().expect("chunks_exact(2)"))).collect()
176}
177
178fn read_i32_le(bytes: &[u8]) -> Vec<i32> {
179    bytes.chunks_exact(4).map(|c| i32::from_le_bytes(c.try_into().expect("chunks_exact(4)"))).collect()
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::test_support::synthetic_gguf::{tiny_model_bytes, write_temp_gguf};
186
187    #[test]
188    fn tensor_from_entry_round_trips_f32_bytes() {
189        let bytes = tiny_model_bytes();
190        let path = write_temp_gguf(&bytes, "bridge-round-trip");
191        let model = kopitiam_loader::load_model(&path).unwrap();
192        let entry = model.tensor("token_embd.weight").unwrap().clone();
193        let t = tensor_from_entry(&model, &entry).unwrap();
194        assert_eq!(t.dtype(), DType::F32);
195        assert_eq!(t.shape(), &entry.shape);
196    }
197
198    #[test]
199    fn load_tensor_f32_opt_is_none_for_a_missing_name() {
200        let bytes = tiny_model_bytes();
201        let path = write_temp_gguf(&bytes, "bridge-opt-missing");
202        let model = kopitiam_loader::load_model(&path).unwrap();
203        assert!(load_tensor_f32_opt(&model, "does.not.exist").unwrap().is_none());
204    }
205
206    #[test]
207    fn load_tensor_f32_errors_on_a_missing_required_name() {
208        let bytes = tiny_model_bytes();
209        let path = write_temp_gguf(&bytes, "bridge-required-missing");
210        let model = kopitiam_loader::load_model(&path).unwrap();
211        assert!(matches!(
212            load_tensor_f32(&model, "does.not.exist"),
213            Err(Error::MissingTensor { .. })
214        ));
215    }
216
217    /// A K-quant matmul weight (Q4_K) has no fused
218    /// [`kopitiam_tensor::Tensor::quantized_matmul`] kernel, so before this
219    /// fix `load_matmul_weight` left it quantized and the first
220    /// [`crate::linear::linear`] call dead-ended in an `UnsupportedDType`.
221    /// It must now be dequantized to `f32` at load and complete a forward
222    /// pass whose output equals dequantize-then-`f32`-matmul.
223    #[test]
224    fn a_q4_k_matmul_weight_is_dequantized_at_load_and_completes_a_forward_pass() {
225        use crate::test_support::synthetic_gguf::{
226            arbitrary_q4_k_blocks, single_quantized_weight_gguf, GGML_TYPE_Q4_K,
227        };
228
229        // Q4_K super-block = 256 elements, so `in_features` is one super-block
230        // per row and each of the 4 rows is exactly one super-block.
231        let (out_features, in_features) = (4, 256);
232        let bytes = arbitrary_q4_k_blocks(out_features, 0xBEEF);
233        let gguf = single_quantized_weight_gguf("test.weight", &[out_features, in_features], GGML_TYPE_Q4_K, &bytes);
234        let path = write_temp_gguf(&gguf, "q4k-load");
235        let model = kopitiam_loader::load_model(&path).unwrap();
236
237        // The whole point of the fix: a format without a fused kernel is
238        // dequantized at load, not kept quantized.
239        let w = load_matmul_weight(&model, "test.weight").unwrap();
240        assert_eq!(w.dtype(), DType::F32);
241        assert!(!w.dtype().is_quantized());
242        assert!(!kopitiam_tensor::has_fused_matmul_kernel(DType::Q4_K));
243
244        // The loaded weight is exactly the decode of the same on-disk bytes.
245        let entry = model.tensor("test.weight").unwrap().clone();
246        let reference_w = tensor_from_entry(&model, &entry).unwrap().to_dtype(DType::F32).unwrap();
247        assert_eq!(w.to_vec_f32().unwrap(), reference_w.to_vec_f32().unwrap());
248
249        // A forward pass through `linear` now completes with finite output
250        // equal to dequantize-then-f32-matmul (`x @ W^T`).
251        let x_vals: Vec<f32> = (0..in_features).map(|i| (i as f32 % 7.0 - 3.0) * 0.1).collect();
252        let x = Tensor::from_f32(x_vals, [1, in_features]).unwrap();
253        let y = crate::linear::linear(&x, &w, None).unwrap();
254        assert_eq!(y.dtype(), DType::F32);
255        let y_vals = y.to_vec_f32().unwrap();
256        assert_eq!(y_vals.len(), out_features);
257        assert!(y_vals.iter().all(|v| v.is_finite()), "forward-pass output must be finite: {y_vals:?}");
258
259        let reference = x.matmul(&reference_w.transpose(0, 1).unwrap()).unwrap().to_vec_f32().unwrap();
260        assert_eq!(y_vals, reference);
261    }
262
263    /// The no-regression counterpart: a Q4_0 matmul weight — the format the
264    /// shipped `qwen2.5-0.5b-instruct-q4_0` model uses — must still be kept
265    /// quantized at load and run through the fused kernel, never dequantized.
266    #[test]
267    fn a_q4_0_matmul_weight_stays_quantized_at_load_and_uses_the_fused_kernel() {
268        use crate::test_support::synthetic_gguf::{
269            quantize_q4_0_blocks, single_quantized_weight_gguf, GGML_TYPE_Q4_0,
270        };
271
272        let (out_features, in_features) = (3, 64); // 2 blocks of 32 per row.
273        let data: Vec<f32> = (0..out_features * in_features).map(|i| (i as f32 * 0.037).sin() * 0.5).collect();
274        let bytes = quantize_q4_0_blocks(&data);
275        let gguf = single_quantized_weight_gguf("test.weight", &[out_features, in_features], GGML_TYPE_Q4_0, &bytes);
276        let path = write_temp_gguf(&gguf, "q40-load");
277        let model = kopitiam_loader::load_model(&path).unwrap();
278
279        let w = load_matmul_weight(&model, "test.weight").unwrap();
280        assert_eq!(w.dtype(), DType::Q4_0, "Q4_0 must stay quantized for the fused kernel");
281        assert!(w.dtype().is_quantized());
282        assert!(kopitiam_tensor::has_fused_matmul_kernel(w.dtype()));
283
284        // `linear` routes it through the fused `quantized_matmul`, matching a
285        // direct fused call bit-for-bit (i.e. no dequantize slipped in).
286        let x_vals: Vec<f32> = (0..in_features).map(|i| (i as f32 % 5.0 - 2.0) * 0.1).collect();
287        let x = Tensor::from_f32(x_vals, [1, in_features]).unwrap();
288        let y = crate::linear::linear(&x, &w, None).unwrap();
289        let fused = x.quantized_matmul(&w).unwrap();
290        assert_eq!(y.to_vec_f32().unwrap(), fused.to_vec_f32().unwrap());
291        assert!(y.to_vec_f32().unwrap().iter().all(|v| v.is_finite()));
292    }
293}