Skip to main content

pictor_image/
weights.rs

1//! Flat, typed weight registry for a FLUX.2 DiT (`bonsai-image`) GGUF file.
2//!
3//! [`DitWeights`] owns the file bytes (memory-mapped from disk, or an in-memory
4//! buffer) and the parsed GGUF metadata/tensor directory, and exposes every
5//! tensor with typed access. It deliberately does **not** build the nested
6//! transformer-block hierarchy or any forward pass — those belong to a later
7//! phase, which can construct block structs on top of this registry.
8//!
9//! # Two storage conventions
10//!
11//! The converter writes tensors under two conventions, which the lookups here
12//! honour transparently:
13//!
14//! 1. **Quantized linears** are stored as GGUF type `TQ2_0_g128` under their
15//!    *base* module name with the `.weight` suffix stripped, e.g.
16//!    `transformer_blocks.0.attn.to_q`. Use [`DitWeights::quantized_linear`].
17//! 2. **Plain tensors** are stored as GGUF type `BF16` under their *full*
18//!    name, e.g. `x_embedder.weight`. Use [`DitWeights::bf16_tensor`].
19//!
20//! # Reversed shapes
21//!
22//! Every tensor is stored with its logical shape reversed (outermost dimension
23//! last), so GGUF `ne[0]` is the contraction dimension. The accessors recover
24//! the logical shape by reversing `ne`; for a 2-D quantized linear this yields
25//! `(out, in) = (ne[1], ne[0])`, exposed as [`QuantizedLinear::out_features`] /
26//! [`QuantizedLinear::in_features`].
27//!
28//! # BF16 exposure
29//!
30//! BF16 tensors are kept as their raw little-endian bytes (a borrowed
31//! [`Bf16Tensor::bytes`] slice, the only true zero-copy view). On top of that,
32//! [`Bf16Tensor`] offers two decode-on-demand accessors that each allocate an
33//! owned buffer: [`Bf16Tensor::bits`] (the `u16` bit patterns) and
34//! [`Bf16Tensor::to_f32_vec`] (decoded `f32` values). Nothing is decoded until
35//! a caller asks, so opening the file is cheap.
36
37use std::path::Path;
38
39use half::bf16;
40
41use pictor_core::gguf::metadata::MetadataStore;
42use pictor_core::gguf::reader::GgufFile;
43use pictor_core::gguf::tensor_info::{TensorInfo, TensorStore};
44use pictor_core::gguf::types::GgufTensorType;
45use pictor_core::quant_ternary::{BlockTQ2_0_g128, QK_TQ2_0_G128};
46
47use crate::config::DitConfig;
48use crate::error::{DitError, DitResult};
49
50/// Suffix used by diffusers `*.weight` linear names; stripped to obtain the
51/// base name under which a quantized linear is stored.
52const WEIGHT_SUFFIX: &str = ".weight";
53
54/// Backing storage for the GGUF file bytes.
55///
56/// Owning the bytes (rather than borrowing) lets [`DitWeights`] hold the parsed
57/// metadata/tensor directory alongside the data without a self-referential
58/// borrow. The `Owned` variant supports in-memory construction (tests, or
59/// callers that already have the bytes).
60enum Backing {
61    /// Memory-mapped file.
62    Mmap(memmap2::Mmap),
63    /// In-memory byte buffer.
64    Owned(Vec<u8>),
65}
66
67impl Backing {
68    /// Borrow the backing bytes.
69    fn as_bytes(&self) -> &[u8] {
70        match self {
71            Self::Mmap(m) => &m[..],
72            Self::Owned(v) => v.as_slice(),
73        }
74    }
75}
76
77/// A quantized (`TQ2_0_g128`) linear weight, exposed as ternary blocks plus its
78/// recovered logical `(out, in)` dimensions.
79#[derive(Debug, Clone, Copy)]
80pub struct QuantizedLinear<'a> {
81    /// Out-major ternary blocks (`out * (in / 128)` of them).
82    pub blocks: &'a [BlockTQ2_0_g128],
83    /// Logical output feature count (rows).
84    pub out_features: u64,
85    /// Logical input feature count (columns, 128-divisible).
86    pub in_features: u64,
87}
88
89impl QuantizedLinear<'_> {
90    /// Number of ternary blocks expected for this linear: `out * (in / 128)`.
91    pub fn expected_block_count(&self) -> u64 {
92        self.out_features * (self.in_features / QK_TQ2_0_G128 as u64)
93    }
94}
95
96/// A plain BF16 tensor, exposed as raw bytes with typed views and a logical
97/// (reversed-`ne`) shape.
98#[derive(Debug, Clone, Copy)]
99pub struct Bf16Tensor<'a> {
100    /// Raw little-endian BF16 bytes (`2 * element_count`).
101    pub bytes: &'a [u8],
102    /// Logical shape (GGUF `ne` reversed, outermost dimension first).
103    shape_rev: &'a [u64],
104}
105
106impl<'a> Bf16Tensor<'a> {
107    /// Logical shape, outermost dimension first (GGUF `ne` reversed).
108    pub fn shape(&self) -> Vec<u64> {
109        let mut s: Vec<u64> = self.shape_rev.to_vec();
110        s.reverse();
111        s
112    }
113
114    /// Total element count.
115    pub fn element_count(&self) -> u64 {
116        self.shape_rev.iter().product()
117    }
118
119    /// Decoded copy of the raw `u16` BF16 bit patterns (allocates a `Vec`).
120    ///
121    /// This is not a borrowed view: the bytes are re-read little-endian into an
122    /// owned `Vec<u16>`, sidestepping the 2-byte alignment a `&[u16]` cast would
123    /// require on memory-mapped data. Returns `None` if the byte length is odd
124    /// (never the case for a well-formed BF16 tensor).
125    pub fn bits(&self) -> Option<Vec<u16>> {
126        if self.bytes.len() % 2 != 0 {
127            return None;
128        }
129        Some(
130            self.bytes
131                .chunks_exact(2)
132                .map(|c| u16::from_le_bytes([c[0], c[1]]))
133                .collect(),
134        )
135    }
136
137    /// Decode the tensor to an owned `Vec<f32>` (row-major, logical order).
138    pub fn to_f32_vec(&self) -> Vec<f32> {
139        self.bytes
140            .chunks_exact(2)
141            .map(|c| bf16::from_le_bytes([c[0], c[1]]).to_f32())
142            .collect()
143    }
144}
145
146/// A flat, typed registry of every tensor in a `bonsai-image` DiT GGUF file,
147/// plus the parsed [`DitConfig`].
148pub struct DitWeights {
149    backing: Backing,
150    /// Byte offset where the tensor data section begins.
151    data_offset: usize,
152    /// Parsed GGUF metadata key-value store (owned).
153    metadata: MetadataStore,
154    /// Parsed GGUF tensor directory (owned).
155    tensors: TensorStore,
156    /// Parsed DiT configuration.
157    config: DitConfig,
158}
159
160impl DitWeights {
161    /// Open a `bonsai-image` DiT GGUF file from disk (memory-mapped).
162    ///
163    /// # Errors
164    ///
165    /// Returns [`DitError::Io`] if the file cannot be opened/mapped,
166    /// [`DitError::Gguf`] on a parse failure, and a config error if the
167    /// metadata is not a valid `bonsai-image` architecture.
168    pub fn open(path: &Path) -> DitResult<Self> {
169        let file = std::fs::File::open(path).map_err(|source| DitError::Io {
170            path: path.display().to_string(),
171            source,
172        })?;
173        // SAFETY: read-only mapping; the file must not be mutated while mapped.
174        // This is the standard model-loading pattern used across Pictor.
175        let mmap = unsafe { memmap2::Mmap::map(&file) }.map_err(|source| DitError::Io {
176            path: path.display().to_string(),
177            source,
178        })?;
179        Self::from_backing(Backing::Mmap(mmap))
180    }
181
182    /// Construct from an in-memory GGUF byte buffer (no temp file needed).
183    ///
184    /// # Errors
185    ///
186    /// As [`DitWeights::open`], minus the I/O variant.
187    pub fn from_bytes(bytes: Vec<u8>) -> DitResult<Self> {
188        Self::from_backing(Backing::Owned(bytes))
189    }
190
191    /// Parse a backing buffer into a registry, dropping the transient borrow.
192    fn from_backing(backing: Backing) -> DitResult<Self> {
193        // Parse against a transient borrow, then move the owned metadata/tensor
194        // stores out and drop the borrow so `Self` is not self-referential.
195        let (metadata, tensors, data_offset) = {
196            let file = GgufFile::parse(backing.as_bytes())?;
197            (file.metadata, file.tensors, file.data_offset)
198        };
199        let config = DitConfig::from_metadata(&metadata)?;
200        Ok(Self {
201            backing,
202            data_offset,
203            metadata,
204            tensors,
205            config,
206        })
207    }
208
209    /// The parsed DiT configuration.
210    pub fn config(&self) -> &DitConfig {
211        &self.config
212    }
213
214    /// The parsed GGUF metadata store (for keys outside [`DitConfig`]).
215    pub fn metadata(&self) -> &MetadataStore {
216        &self.metadata
217    }
218
219    /// The parsed GGUF tensor directory.
220    pub fn tensors(&self) -> &TensorStore {
221        &self.tensors
222    }
223
224    /// Number of tensors in the file.
225    pub fn tensor_count(&self) -> usize {
226        self.tensors.len()
227    }
228
229    /// Names of all tensors stored as GGUF type `TQ2_0_g128` (quantized),
230    /// sorted.
231    pub fn quantized_names(&self) -> Vec<&str> {
232        self.names_of_type(GgufTensorType::TQ2_0_g128)
233    }
234
235    /// Names of all tensors stored as GGUF type `BF16` (plain), sorted.
236    pub fn bf16_names(&self) -> Vec<&str> {
237        self.names_of_type(GgufTensorType::BF16)
238    }
239
240    /// Sorted tensor names whose stored GGUF type equals `ty`.
241    fn names_of_type(&self, ty: GgufTensorType) -> Vec<&str> {
242        let mut names: Vec<&str> = self
243            .tensors
244            .iter()
245            .filter(|(_, info)| info.tensor_type == ty)
246            .map(|(name, _)| name.as_str())
247            .collect();
248        names.sort_unstable();
249        names
250    }
251
252    /// Raw bytes of the named tensor in the data section.
253    ///
254    /// Mirrors `GgufFile::tensor_data`, but against the owned backing.
255    fn raw_bytes(&self, info: &TensorInfo) -> DitResult<&[u8]> {
256        let bytes = self.backing.as_bytes();
257        let start = self.data_offset + info.offset as usize;
258        let size = info.data_size() as usize;
259        let end = start
260            .checked_add(size)
261            .ok_or_else(|| DitError::InvalidMetadata {
262                key: info.name.clone(),
263                reason: "tensor extent overflows usize".to_string(),
264            })?;
265        if end > bytes.len() {
266            return Err(DitError::Gguf(
267                pictor_core::error::BonsaiError::UnexpectedEof { offset: end as u64 },
268            ));
269        }
270        Ok(&bytes[start..end])
271    }
272
273    /// Look up a quantized (`TQ2_0_g128`) linear by its diffusers logical name.
274    ///
275    /// Accepts either the base module name (`transformer_blocks.0.attn.to_q`,
276    /// the storage convention) or a `.weight`-suffixed name, which is stripped
277    /// and retried. Returns the ternary blocks plus the recovered logical
278    /// `(out, in)` dimensions.
279    ///
280    /// # Errors
281    ///
282    /// [`DitError::Gguf`] (`TensorNotFound`) if no such tensor exists,
283    /// [`DitError::WrongTensorType`] if it is not `TQ2_0_g128`,
284    /// [`DitError::WrongRank`] if it is not 2-D, or a slice-validation error
285    /// from the core ternary block reader.
286    pub fn quantized_linear(&self, name: &str) -> DitResult<QuantizedLinear<'_>> {
287        let info = self.lookup_quantized_info(name)?;
288
289        if info.tensor_type != GgufTensorType::TQ2_0_g128 {
290            return Err(DitError::WrongTensorType {
291                name: info.name.clone(),
292                found: info.tensor_type.to_string(),
293                expected: GgufTensorType::TQ2_0_g128.to_string(),
294            });
295        }
296        if info.shape.len() != 2 {
297            return Err(DitError::WrongRank {
298                name: info.name.clone(),
299                found: info.shape.len(),
300                expected: 2,
301            });
302        }
303        // GGUF ne = [in, out]; logical (out, in) = (ne[1], ne[0]).
304        let in_features = info.shape[0];
305        let out_features = info.shape[1];
306
307        let bytes = self.raw_bytes(info)?;
308        let blocks = BlockTQ2_0_g128::slice_from_bytes(bytes)?;
309
310        Ok(QuantizedLinear {
311            blocks,
312            out_features,
313            in_features,
314        })
315    }
316
317    /// Resolve the [`TensorInfo`] for a quantized linear, honouring the
318    /// base-name convention (strip a trailing `.weight` on miss).
319    fn lookup_quantized_info(&self, name: &str) -> DitResult<&TensorInfo> {
320        if let Some(info) = self.tensors.get(name) {
321            return Ok(info);
322        }
323        if let Some(base) = name.strip_suffix(WEIGHT_SUFFIX) {
324            if let Some(info) = self.tensors.get(base) {
325                return Ok(info);
326            }
327        }
328        Err(DitError::Gguf(
329            pictor_core::error::BonsaiError::TensorNotFound {
330                name: name.to_string(),
331            },
332        ))
333    }
334
335    /// Look up a plain BF16 tensor by its full name.
336    ///
337    /// # Errors
338    ///
339    /// [`DitError::Gguf`] (`TensorNotFound`) if absent, or
340    /// [`DitError::WrongTensorType`] if it is not stored as `BF16`.
341    pub fn bf16_tensor(&self, name: &str) -> DitResult<Bf16Tensor<'_>> {
342        let info = self.tensors.require(name)?;
343        if info.tensor_type != GgufTensorType::BF16 {
344            return Err(DitError::WrongTensorType {
345                name: info.name.clone(),
346                found: info.tensor_type.to_string(),
347                expected: GgufTensorType::BF16.to_string(),
348            });
349        }
350        let bytes = self.raw_bytes(info)?;
351        Ok(Bf16Tensor {
352            bytes,
353            shape_rev: &info.shape,
354        })
355    }
356}