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}