versorc_math/quantization/
clifford_3bit.rs1use crate::versor::DyadicWalshVersor;
17
18pub const LLOYD_MAX_3BIT_CENTROIDS: [f32; 4] = [
20 0.245_086_3, 0.756_041_4, 1.343_918_2, 2.152_014_2, ];
25
26pub const LLOYD_MAX_3BIT_BOUNDS: [f32; 3] = [
28 0.500_563_8, 1.049_979_8, 1.747_966_2, ];
32
33#[repr(transparent)]
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub struct Clifford3BitWord64(pub u64);
39
40impl Clifford3BitWord64 {
41 pub fn pack(weights: &[f32; 16], scale: f32) -> Self {
45 let inv_scale = if scale.abs() > 1e-12 { 1.0 / scale } else { 1.0 };
46 let mut bits: u64 = 0;
47
48 for (i, &w) in weights.iter().enumerate() {
49 let normalized = w * inv_scale;
50 let sign_bit = if normalized < 0.0 { 1u64 } else { 0u64 };
51 let abs_val = normalized.abs();
52
53 let mag_code = if abs_val < LLOYD_MAX_3BIT_BOUNDS[0] {
54 0u64
55 } else if abs_val < LLOYD_MAX_3BIT_BOUNDS[1] {
56 1u64
57 } else if abs_val < LLOYD_MAX_3BIT_BOUNDS[2] {
58 2u64
59 } else {
60 3u64
61 };
62
63 let code_3bit = (sign_bit << 2) | mag_code;
64 bits |= code_3bit << (i * 3);
65 }
66
67 let bf16_scale = f32_to_bf16(scale) as u64;
68 bits |= bf16_scale << 48;
69
70 Self(bits)
71 }
72
73 pub fn pack_least_squares(weights: &[f32; 16]) -> Self {
77 let sum_sq: f32 = weights.iter().map(|&w| w * w).sum();
78 let std_dev = (sum_sq / 16.0).sqrt().max(1e-8);
79 let inv_std = 1.0 / std_dev;
80
81 let mut bits: u64 = 0;
82 let mut dot_wc = 0.0f32;
83 let mut norm_c_sq = 0.0f32;
84
85 for (i, &w) in weights.iter().enumerate() {
86 let normalized = w * inv_std;
87 let sign_bit = if normalized < 0.0 { 1u64 } else { 0u64 };
88 let abs_val = normalized.abs();
89
90 let mag_code = if abs_val < LLOYD_MAX_3BIT_BOUNDS[0] {
91 0usize
92 } else if abs_val < LLOYD_MAX_3BIT_BOUNDS[1] {
93 1usize
94 } else if abs_val < LLOYD_MAX_3BIT_BOUNDS[2] {
95 2usize
96 } else {
97 3usize
98 };
99
100 let code_3bit = (sign_bit << 2) | (mag_code as u64);
101 bits |= code_3bit << (i * 3);
102
103 let sign_f = if sign_bit != 0 { -1.0f32 } else { 1.0f32 };
104 let c_norm = sign_f * LLOYD_MAX_3BIT_CENTROIDS[mag_code];
105 dot_wc += w * c_norm;
106 norm_c_sq += c_norm * c_norm;
107 }
108
109 let alpha_ls = if norm_c_sq > 1e-12 { dot_wc / norm_c_sq } else { std_dev };
110 let bf16_scale = f32_to_bf16(alpha_ls) as u64;
111 bits |= bf16_scale << 48;
112
113 Self(bits)
114 }
115
116 #[inline(always)]
118 pub fn scale(&self) -> f32 {
119 let bf16 = (self.0 >> 48) as u16;
120 bf16_to_f32(bf16)
121 }
122
123 pub fn unpack(&self, out: &mut [f32; 16]) {
125 let scale = self.scale();
126 for i in 0..16 {
127 let code = ((self.0 >> (i * 3)) & 0x7) as usize;
128 let sign = if (code & 0x4) != 0 { -1.0f32 } else { 1.0f32 };
129 let mag_idx = code & 0x3;
130 out[i] = sign * LLOYD_MAX_3BIT_CENTROIDS[mag_idx] * scale;
131 }
132 }
133
134 #[inline(always)]
137 pub fn dot_with_slice(&self, x: &[f32; 16]) -> f32 {
138 let scale = self.scale();
139 let mut sum = 0.0f32;
140 for i in 0..16 {
141 let code = ((self.0 >> (i * 3)) & 0x7) as usize;
142 let sign = if (code & 0x4) != 0 { -1.0f32 } else { 1.0f32 };
143 let mag_idx = code & 0x3;
144 let w = sign * LLOYD_MAX_3BIT_CENTROIDS[mag_idx] * scale;
145 sum += w * x[i];
146 }
147 sum
148 }
149}
150
151#[repr(C, align(64))]
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct Clifford3BitCacheLine64 {
157 pub words: [Clifford3BitWord64; 8],
158}
159
160impl Default for Clifford3BitCacheLine64 {
161 fn default() -> Self {
162 Self {
163 words: [Clifford3BitWord64(0); 8],
164 }
165 }
166}
167
168impl Clifford3BitCacheLine64 {
169 pub const CAPACITY: usize = 128;
170
171 pub fn pack_128(weights: &[f32; 128]) -> Self {
174 let mut words = [Clifford3BitWord64(0); 8];
175 for b in 0..8 {
176 let chunk_weights: &[f32; 16] = weights[b * 16..(b + 1) * 16].try_into().unwrap();
177 words[b] = Clifford3BitWord64::pack_least_squares(chunk_weights);
178 }
179 Self { words }
180 }
181
182 pub fn unpack_128(&self, out: &mut [f32; 128]) {
184 for b in 0..8 {
185 let chunk_out: &mut [f32; 16] = (&mut out[b * 16..(b + 1) * 16]).try_into().unwrap();
186 self.words[b].unpack(chunk_out);
187 }
188 }
189
190 #[inline(always)]
192 pub fn as_bytes(&self) -> &[u8; 64] {
193 unsafe { &*(self as *const Self as *const [u8; 64]) }
194 }
195
196 #[inline(always)]
198 pub fn slice_as_bytes(slice: &[Self]) -> &[u8] {
199 unsafe {
200 std::slice::from_raw_parts(slice.as_ptr() as *const u8, slice.len() * 64)
201 }
202 }
203
204 #[inline(always)]
206 pub fn slice_from_bytes(bytes: &[u8]) -> Result<&[Self], String> {
207 if bytes.len() % 64 != 0 {
208 return Err(format!("Byte slice length {} is not a multiple of 64", bytes.len()));
209 }
210 if bytes.as_ptr() as usize % 64 != 0 {
211 return Err(format!(
212 "Byte slice pointer {:p} is not 64-byte cache-line aligned",
213 bytes.as_ptr()
214 ));
215 }
216 let count = bytes.len() / 64;
217 Ok(unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const Self, count) })
218 }
219
220 #[inline(always)]
222 pub fn dot_128(&self, x: &[f32; 128]) -> f32 {
223 let mut total = 0.0f32;
224 for b in 0..8 {
225 let chunk_x: &[f32; 16] = x[b * 16..(b + 1) * 16].try_into().unwrap();
226 total += self.words[b].dot_with_slice(chunk_x);
227 }
228 total
229 }
230}
231
232#[derive(Debug, Clone)]
234pub struct Clifford3BitTensor {
235 pub rows: usize,
236 pub cols: usize,
237 pub cache_lines: Vec<Clifford3BitCacheLine64>,
238}
239
240impl Clifford3BitTensor {
241 pub fn compile_aot(weights: &[f32], rows: usize, cols: usize, versor: &DyadicWalshVersor) -> Self {
244 assert_eq!(weights.len(), rows * cols);
245 assert_eq!(cols % 128, 0, "Columns must be a multiple of 128 for cache-line alignment");
246 assert_eq!(versor.dimension(), cols);
247
248 let lines_per_row = cols / 128;
249 let mut cache_lines = Vec::with_capacity(rows * lines_per_row);
250 let mut row_buffer = vec![0.0f32; cols];
251
252 for r in 0..rows {
253 let row_slice = &weights[r * cols..(r + 1) * cols];
254 row_buffer.copy_from_slice(row_slice);
255
256 versor.apply_in_place(&mut row_buffer);
258
259 for l in 0..lines_per_row {
260 let chunk: &[f32; 128] = row_buffer[l * 128..(l + 1) * 128].try_into().unwrap();
261 cache_lines.push(Clifford3BitCacheLine64::pack_128(chunk));
262 }
263 }
264
265 Self {
266 rows,
267 cols,
268 cache_lines,
269 }
270 }
271
272 pub fn matvec(&self, x: &[f32], out: &mut [f32], versor: &DyadicWalshVersor) {
276 assert_eq!(x.len(), self.cols);
277 assert_eq!(out.len(), self.rows);
278
279 let mut x_rot = x.to_vec();
281 versor.apply_in_place(&mut x_rot);
282
283 let lines_per_row = self.cols / 128;
285 for r in 0..self.rows {
286 let mut row_dot = 0.0f32;
287 let line_offset = r * lines_per_row;
288 for l in 0..lines_per_row {
289 let cache_line = &self.cache_lines[line_offset + l];
290 let chunk_x: &[f32; 128] = x_rot[l * 128..(l + 1) * 128].try_into().unwrap();
291 row_dot += cache_line.dot_128(chunk_x);
292 }
293 out[r] = row_dot;
294 }
295 }
296}
297
298#[inline(always)]
300pub fn f32_to_bf16(val: f32) -> u16 {
301 let bits = val.to_bits();
302 let rounding_bias = 0x7FFF + ((bits >> 16) & 1);
304 ((bits.wrapping_add(rounding_bias)) >> 16) as u16
305}
306
307#[inline(always)]
309pub fn bf16_to_f32(bits: u16) -> f32 {
310 f32::from_bits((bits as u32) << 16)
311}