Skip to main content

apr_format/v2/
mod.rs

1//! APR Format Module (v2 `APR\0`) — sovereign leaf (issue #2231)
2//!
3//! Implements the APR v2 container format with:
4//! - 64-byte tensor alignment for zero-copy mmap
5//! - LZ4 block compression (64KB blocks)
6//! - JSON metadata section
7//! - Multi-file sharding for 10B+ parameter models
8//! - Single unified format (no versioning complexity)
9//!
10//! # Format Structure (APR)
11//!
12//! ```text
13//! ┌─────────────────────────────────────────────────────────────┐
14//! │ Header (64 bytes, 64-byte aligned)                          │
15//! │   - Magic: "APR\0" (4 bytes) - ONE format, no versioning    │
16//! │   - Version: major.minor (2 bytes)                          │
17//! │   - Flags (2 bytes)                                         │
18//! │   - Tensor count (4 bytes)                                  │
19//! │   - Metadata offset (8 bytes)                               │
20//! │   - Metadata size (4 bytes)                                 │
21//! │   - Tensor index offset (8 bytes)                           │
22//! │   - Data offset (8 bytes)                                   │
23//! │   - Checksum (4 bytes)                                      │
24//! │   - Reserved (20 bytes, zero-padded)                        │
25//! ├─────────────────────────────────────────────────────────────┤
26//! │ JSON Metadata (variable, padded to 64-byte boundary)        │
27//! ├─────────────────────────────────────────────────────────────┤
28//! │ Tensor Index (sorted by name, 64-byte aligned entries)      │
29//! ├─────────────────────────────────────────────────────────────┤
30//! │ Tensor Data (each tensor 64-byte aligned)                   │
31//! ├─────────────────────────────────────────────────────────────┤
32//! │ Footer Checksum (4 bytes)                                   │
33//! └─────────────────────────────────────────────────────────────┘
34//! ```
35//!
36//! # Example
37//!
38//! ```rust
39//! use apr_format::v2::{AprV2Header, AprV2Flags, MAGIC_V2, ALIGNMENT};
40//!
41//! let header = AprV2Header::new();
42//! assert_eq!(header.magic, MAGIC_V2);
43//! assert!(header.is_valid());
44//! ```
45//!
46//! # Sovereignty (issue #2231)
47//!
48//! This module contains ONLY the container I/O — pure bytes, shapes, and
49//! dtypes. It carries **no** ML/GPU/tokenizer dependency:
50//!   - CRC32 routes through the single [`crate::crc32::crc32`].
51//!   - f16 conversion routes through [`crate::f16`] (the IEEE-correct `half`
52//!     crate), NOT `trueno::f32_to_f16`. See the f16 note in `crate::f16`.
53//!   - The dequantizing `get_tensor_as_f32` accessor (which needs the GGUF
54//!     Q4_K/Q6_K dequant + f32 physics) is **severed** from the leaf reader and
55//!     re-attached in `aprender-core` as an extension trait (`AprV2DequantExt`).
56//!     The leaf exposes the raw bytes via [`AprV2Reader::get_tensor_data`] and
57//!     the typed-but-trivial [`AprV2Reader::get_f32_tensor`] (F32 dtype only).
58
59// ============================================================================
60// Constants
61// ============================================================================
62
63/// APR magic number: "APR\0" in ASCII (0x41505200)
64/// ONE format. No versioning. Period.
65pub const MAGIC_V2: [u8; 4] = [0x41, 0x50, 0x52, 0x00];
66
67/// Format version 2.0
68pub const VERSION_V2: (u8, u8) = (2, 0);
69
70/// Header size in bytes (64-byte aligned)
71pub const HEADER_SIZE_V2: usize = 64;
72
73/// Tensor alignment in bytes (for zero-copy mmap)
74pub const ALIGNMENT: usize = 64;
75
76/// LZ4 block size in bytes
77pub const LZ4_BLOCK_SIZE: usize = 64 * 1024; // 64KB
78
79/// Maximum metadata size (16MB)
80pub const MAX_METADATA_SIZE: usize = 16 * 1024 * 1024;
81
82/// Maximum tensor name length
83pub const MAX_TENSOR_NAME_LEN: usize = 256;
84
85// ============================================================================
86// Flags
87// ============================================================================
88
89/// APR v2 feature flags (16-bit for expanded feature set)
90#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
91pub struct AprV2Flags(u16);
92
93impl AprV2Flags {
94    /// Payload is compressed with LZ4
95    pub const LZ4_COMPRESSED: u16 = 0b0000_0000_0000_0001;
96    /// Payload is compressed with Zstd
97    pub const ZSTD_COMPRESSED: u16 = 0b0000_0000_0000_0010;
98    /// Payload is encrypted (AES-256-GCM)
99    pub const ENCRYPTED: u16 = 0b0000_0000_0000_0100;
100    /// Has digital signature (Ed25519)
101    pub const SIGNED: u16 = 0b0000_0000_0000_1000;
102    /// Model is sharded across multiple files
103    pub const SHARDED: u16 = 0b0000_0000_0001_0000;
104    /// Tensors are quantized
105    pub const QUANTIZED: u16 = 0b0000_0000_0010_0000;
106    /// Has embedded filterbank (for Whisper models)
107    pub const HAS_FILTERBANK: u16 = 0b0000_0000_0100_0000;
108    /// Has model card metadata
109    pub const HAS_MODEL_CARD: u16 = 0b0000_0000_1000_0000;
110    /// Supports streaming/chunked loading
111    pub const STREAMING: u16 = 0b0000_0001_0000_0000;
112    /// Contains vocabulary/tokenizer
113    pub const HAS_VOCAB: u16 = 0b0000_0010_0000_0000;
114
115    /// LAYOUT-002: Tensor layout is row-major (REQUIRED for valid APR files)
116    /// All APR files created after LAYOUT-002 must have this flag set.
117    /// Pre-LAYOUT-002 files without this flag are assumed row-major.
118    pub const LAYOUT_ROW_MAJOR: u16 = 0b0000_0100_0000_0000;
119
120    /// LAYOUT-002: Tensor layout is column-major (FORBIDDEN - Jidoka guard)
121    /// If this flag is set, the APR file is "dirty" and must be rejected.
122    /// This flag exists to catch improperly converted GGUF files.
123    pub const LAYOUT_COLUMN_MAJOR: u16 = 0b0000_1000_0000_0000;
124
125    /// Create new empty flags
126    #[must_use]
127    pub const fn new() -> Self {
128        Self(0)
129    }
130
131    /// Create from raw u16 value
132    #[must_use]
133    pub const fn from_bits(bits: u16) -> Self {
134        Self(bits)
135    }
136
137    /// Get raw bits
138    #[must_use]
139    pub const fn bits(self) -> u16 {
140        self.0
141    }
142
143    /// Check if flag is set
144    #[must_use]
145    pub const fn contains(self, flag: u16) -> bool {
146        (self.0 & flag) == flag
147    }
148
149    /// Set a flag
150    #[must_use]
151    pub const fn with(self, flag: u16) -> Self {
152        Self(self.0 | flag)
153    }
154
155    /// Clear a flag
156    #[must_use]
157    pub const fn without(self, flag: u16) -> Self {
158        Self(self.0 & !flag)
159    }
160
161    /// Check if LZ4 compressed
162    #[must_use]
163    pub const fn is_lz4_compressed(self) -> bool {
164        self.contains(Self::LZ4_COMPRESSED)
165    }
166
167    /// Check if Zstd compressed
168    #[must_use]
169    pub const fn is_zstd_compressed(self) -> bool {
170        self.contains(Self::ZSTD_COMPRESSED)
171    }
172
173    /// Check if encrypted
174    #[must_use]
175    pub const fn is_encrypted(self) -> bool {
176        self.contains(Self::ENCRYPTED)
177    }
178
179    /// Check if sharded
180    #[must_use]
181    pub const fn is_sharded(self) -> bool {
182        self.contains(Self::SHARDED)
183    }
184
185    /// Check if quantized
186    #[must_use]
187    pub const fn is_quantized(self) -> bool {
188        self.contains(Self::QUANTIZED)
189    }
190
191    /// LAYOUT-002: Check if row-major layout flag is set
192    #[must_use]
193    pub const fn is_row_major(self) -> bool {
194        self.contains(Self::LAYOUT_ROW_MAJOR)
195    }
196
197    /// LAYOUT-002: Check if column-major layout flag is set (should be rejected)
198    #[must_use]
199    pub const fn is_column_major(self) -> bool {
200        self.contains(Self::LAYOUT_COLUMN_MAJOR)
201    }
202
203    /// LAYOUT-002: Validate layout is safe for inference
204    /// Returns true if the file is row-major or pre-LAYOUT-002 (assumed row-major)
205    /// Returns false if explicitly marked as column-major (dirty APR file)
206    #[must_use]
207    pub const fn is_layout_valid(self) -> bool {
208        // Reject if explicitly marked as column-major
209        !self.is_column_major()
210    }
211}
212
213// ============================================================================
214// Header
215// ============================================================================
216
217/// APR file header (64 bytes)
218#[derive(Debug, Clone, Copy)]
219#[repr(C)]
220pub struct AprV2Header {
221    /// Magic number ("APR\0") - ONE format, no versioning
222    pub magic: [u8; 4],
223    /// Format version (major, minor)
224    pub version: (u8, u8),
225    /// Feature flags
226    pub flags: AprV2Flags,
227    /// Number of tensors
228    pub tensor_count: u32,
229    /// Offset to JSON metadata section
230    pub metadata_offset: u64,
231    /// Size of metadata in bytes
232    pub metadata_size: u32,
233    /// Offset to tensor index
234    pub tensor_index_offset: u64,
235    /// Offset to tensor data
236    pub data_offset: u64,
237    /// Header checksum (CRC32)
238    pub checksum: u32,
239    /// Reserved for future use (zero-padded)
240    pub reserved: [u8; 20],
241}
242
243impl Default for AprV2Header {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249// --- Split modules (was include!(); now real `mod`s, issue #2231 Stage 2) ----
250// Each formerly-`include!`d file is a real module that re-derives its own `use`
251// block and reaches the parent-scope structs/consts via `super::`. The public
252// items are re-exported into the `v2` namespace below so the historical flat
253// paths (`aprender::format::v2::AprV2Reader`, …) keep resolving unchanged.
254mod header_impl;
255mod reader_impl;
256mod streaming_writer;
257mod tensor_index_impl;
258mod v2format_error;
259mod writer;
260
261pub use header_impl::{
262    AprV2Metadata, ChatSpecialTokens, QuantizationMetadata, ShardingMetadata, TensorIndexEntry,
263};
264pub use reader_impl::{AprV2Reader, AprV2ReaderRef, ShardInfo, ShardManifest};
265pub use streaming_writer::AprV2StreamingWriter;
266pub use tensor_index_impl::{align_64, align_up, is_aligned_64, padding_to_align, TensorDType};
267pub use v2format_error::V2FormatError;
268pub use writer::AprV2Writer;
269
270// Provenance stamping — SHIP-009 full-discharge enabler (task #141).
271// Lives as a real submodule (not `include!`) so its inline tests nest
272// cleanly under `v2::stamp::tests`.
273pub mod stamp;
274pub use stamp::{stamp_provenance_bytes, ProvenancePatch};
275
276#[cfg(test)]
277mod tests;