qualia_core_db/inference/
ternary.rs1pub const TRITS_PER_BYTE: usize = 5;
22
23pub const GGML_TYPE_TERNARY_158: u32 = 1158;
26
27#[inline]
29pub fn packed_trit_len(count: usize) -> usize {
30 count.div_ceil(TRITS_PER_BYTE)
31}
32
33#[inline]
35pub fn ternary_blob_len(count: usize) -> usize {
36 4 + packed_trit_len(count)
37}
38
39pub fn quantize_ternary(weights: &[f32]) -> (f32, Vec<i8>) {
42 let n = weights.len();
43 if n == 0 {
44 return (0.0, Vec::new());
45 }
46 let absmean = weights.iter().map(|w| w.abs()).sum::<f32>() / n as f32;
47 if absmean == 0.0 {
48 return (0.0, vec![0i8; n]);
49 }
50 let trits = weights
51 .iter()
52 .map(|&w| (w / absmean).round().clamp(-1.0, 1.0) as i8)
53 .collect();
54 (absmean, trits)
55}
56
57pub fn pack_trits(trits: &[i8]) -> Vec<u8> {
60 let mut out = Vec::with_capacity(packed_trit_len(trits.len()));
61 for chunk in trits.chunks(TRITS_PER_BYTE) {
62 let mut byte = 0u16;
63 let mut mul = 1u16;
64 for &t in chunk {
65 byte += ((t + 1) as u16) * mul; mul *= 3;
67 }
68 out.push(byte as u8);
69 }
70 out
71}
72
73pub fn unpack_trits_into(packed: &[u8], out: &mut [i8]) -> usize {
76 let mut i = 0;
77 'outer: for &byte in packed {
78 let mut b = byte;
79 for _ in 0..TRITS_PER_BYTE {
80 if i >= out.len() {
81 break 'outer;
82 }
83 out[i] = (b % 3) as i8 - 1; b /= 3;
85 i += 1;
86 }
87 }
88 i
89}
90
91pub fn dequantize_ternary(scale: f32, packed: &[u8], out: &mut [f32]) {
94 let mut i = 0;
95 'outer: for &byte in packed {
96 let mut b = byte;
97 for _ in 0..TRITS_PER_BYTE {
98 if i >= out.len() {
99 break 'outer;
100 }
101 out[i] = scale * ((b % 3) as f32 - 1.0);
102 b /= 3;
103 i += 1;
104 }
105 }
106}
107
108pub fn ternary_blob(weights: &[f32]) -> Vec<u8> {
111 let (scale, trits) = quantize_ternary(weights);
112 let mut out = Vec::with_capacity(ternary_blob_len(weights.len()));
113 out.extend_from_slice(&scale.to_le_bytes());
114 out.extend_from_slice(&pack_trits(&trits));
115 out
116}
117
118pub const TERNARY_GEMM_WGSL: &str = include_str!("../shaders/ternary_gemm.wgsl");
125
126#[inline]
129pub fn trit_at(packed: &[u8], k: usize) -> i32 {
130 let byte = packed[k / TRITS_PER_BYTE];
131 let pos = k % TRITS_PER_BYTE;
132 let mut b = byte;
133 for _ in 0..pos {
134 b /= 3;
135 }
136 (b % 3) as i32 - 1
137}
138
139#[allow(clippy::too_many_arguments)]
144pub fn ternary_gemm_cpu(
145 activations: &[f32],
146 packed: &[u8],
147 scale: f32,
148 n_in: usize,
149 n_out: usize,
150 n_batch: usize,
151 in_row_stride: usize,
152 out_row_stride: usize,
153 out: &mut [f32],
154) {
155 let in_stride = if in_row_stride > 0 {
156 in_row_stride
157 } else {
158 n_in
159 };
160 let out_stride = if out_row_stride > 0 {
161 out_row_stride
162 } else {
163 n_out
164 };
165 for m in 0..n_batch.max(1) {
166 let in_base = m * in_stride;
167 for i in 0..n_out {
168 let row0 = i * n_in;
169 let mut acc = 0.0f32;
170 for j in 0..n_in {
171 let x = activations[in_base + j];
172 match trit_at(packed, row0 + j) {
173 t if t > 0 => acc += x,
174 t if t < 0 => acc -= x,
175 _ => {}
176 }
177 }
178 out[m * out_stride + i] = scale * acc;
179 }
180 }
181}
182
183pub const TRITS_PER_BYTE_2BIT: usize = 4;
194
195#[inline]
197fn trit_code_2bit(t: i8) -> u8 {
198 if t > 0 {
199 1
200 } else if t < 0 {
201 2
202 } else {
203 0
204 }
205}
206
207#[inline]
209pub fn packed_trit_len_2bit(count: usize) -> usize {
210 count.div_ceil(TRITS_PER_BYTE_2BIT)
211}
212
213pub fn pack_trits_2bit(trits: &[i8]) -> Vec<u8> {
215 let mut out = vec![0u8; packed_trit_len_2bit(trits.len())];
216 for (k, &t) in trits.iter().enumerate() {
217 out[k / TRITS_PER_BYTE_2BIT] |= trit_code_2bit(t) << ((k % TRITS_PER_BYTE_2BIT) * 2);
218 }
219 out
220}
221
222#[inline]
225pub fn trit_at_2bit(packed: &[u8], k: usize) -> i32 {
226 let code = (packed[k / TRITS_PER_BYTE_2BIT] >> ((k % TRITS_PER_BYTE_2BIT) * 2)) & 3;
227 (code == 1) as i32 - (code == 2) as i32
228}
229
230pub const TERNARY_GEMM_2BIT_WGSL: &str = include_str!("../shaders/ternary_gemm_2bit.wgsl");
232
233#[allow(clippy::too_many_arguments)]
236pub fn ternary_gemm_cpu_2bit(
237 activations: &[f32],
238 packed: &[u8],
239 scale: f32,
240 n_in: usize,
241 n_out: usize,
242 n_batch: usize,
243 in_row_stride: usize,
244 out_row_stride: usize,
245 out: &mut [f32],
246) {
247 let in_stride = if in_row_stride > 0 {
248 in_row_stride
249 } else {
250 n_in
251 };
252 let out_stride = if out_row_stride > 0 {
253 out_row_stride
254 } else {
255 n_out
256 };
257 for m in 0..n_batch.max(1) {
258 let in_base = m * in_stride;
259 for i in 0..n_out {
260 let row0 = i * n_in;
261 let mut acc = 0.0f32;
262 for j in 0..n_in {
263 acc += trit_at_2bit(packed, row0 + j) as f32 * activations[in_base + j];
265 }
266 out[m * out_stride + i] = scale * acc;
267 }
268 }
269}
270
271pub fn dequantize_blob(blob: &[u8], out: &mut [f32]) {
273 if blob.len() < 4 {
274 for o in out.iter_mut() {
275 *o = 0.0;
276 }
277 return;
278 }
279 let scale = f32::from_le_bytes([blob[0], blob[1], blob[2], blob[3]]);
280 dequantize_ternary(scale, &blob[4..], out);
281}
282
283pub fn rebake_ternary_blob_to_2bit(blob: &[u8], count: usize) -> (f32, Vec<u8>) {
293 if blob.len() < 4 || count == 0 {
294 return (0.0, Vec::new());
295 }
296 let scale = f32::from_le_bytes([blob[0], blob[1], blob[2], blob[3]]);
297 let mut trits = vec![0i8; count];
298 unpack_trits_into(&blob[4..], &mut trits);
299 (scale, pack_trits_2bit(&trits))
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn quantize_absmean_and_trits() {
308 let w = [2.0_f32, -2.0, 0.1, -0.1, 0.0];
310 let (scale, trits) = quantize_ternary(&w);
311 assert!((scale - 0.84).abs() < 1e-5, "scale {scale}");
312 assert_eq!(trits, vec![1, -1, 0, 0, 0]);
313 }
314
315 #[test]
316 fn pack_unpack_round_trips_exactly() {
317 let trits: Vec<i8> = [1, -1, 0, 0, 0, 1, 1, -1, 0, 1, -1]
318 .iter()
319 .copied()
320 .collect();
321 let packed = pack_trits(&trits);
322 assert_eq!(packed.len(), packed_trit_len(trits.len())); let mut back = vec![0i8; trits.len()];
324 let n = unpack_trits_into(&packed, &mut back);
325 assert_eq!(n, trits.len());
326 assert_eq!(back, trits);
327 assert_eq!(packed[0], 119);
329 }
330
331 #[test]
332 fn rebake_base3_to_2bit_preserves_gemm() {
333 let (n_out, n_in) = (3usize, 8usize);
337 let weights: Vec<f32> = (0..n_out * n_in).map(|i| (i as f32 * 0.37).sin()).collect();
338 let act: Vec<f32> = (0..n_in).map(|i| i as f32 * 0.5 - 1.0).collect();
339
340 let base3 = ternary_blob(&weights);
341 let base3_scale = f32::from_le_bytes([base3[0], base3[1], base3[2], base3[3]]);
342 let base3_packed = &base3[4..];
343
344 let (scale2, packed2) = rebake_ternary_blob_to_2bit(&base3, weights.len());
345 assert_eq!(scale2, base3_scale, "scale must be preserved");
346 for k in 0..weights.len() {
347 assert_eq!(
348 trit_at(base3_packed, k),
349 trit_at_2bit(&packed2, k),
350 "trit {k} mismatch"
351 );
352 }
353
354 let mut out_base3 = vec![0f32; n_out];
355 let mut out_2bit = vec![0f32; n_out];
356 ternary_gemm_cpu(
357 &act,
358 base3_packed,
359 base3_scale,
360 n_in,
361 n_out,
362 1,
363 0,
364 0,
365 &mut out_base3,
366 );
367 ternary_gemm_cpu_2bit(&act, &packed2, scale2, n_in, n_out, 1, 0, 0, &mut out_2bit);
368 for i in 0..n_out {
369 assert!(
370 (out_base3[i] - out_2bit[i]).abs() < 1e-6,
371 "row {i}: base3 {} vs 2bit {}",
372 out_base3[i],
373 out_2bit[i]
374 );
375 }
376 }
377
378 #[test]
379 fn blob_round_trips_with_scale() {
380 let w = [2.0_f32, -2.0, 0.1, -0.1, 0.0, 1.5, -1.5];
381 let blob = ternary_blob(&w);
382 assert_eq!(blob.len(), ternary_blob_len(w.len()));
383 let mut out = vec![0.0_f32; w.len()];
384 dequantize_blob(&blob, &mut out);
385 let (scale, _) = quantize_ternary(&w);
387 assert!((out[0] - scale).abs() < 1e-5);
388 assert!((out[1] + scale).abs() < 1e-5);
389 assert_eq!(out[4], 0.0); }
391
392 #[test]
393 fn compression_ratio_is_about_1_6_bits_per_weight() {
394 let count = 4096;
395 let f32_bytes = count * 4;
396 let ternary_bytes = ternary_blob_len(count); let ratio = f32_bytes as f64 / ternary_bytes as f64;
399 assert!(ratio > 19.0 && ratio < 21.0, "ratio {ratio}");
400 let bits_per_weight = (ternary_bytes as f64 * 8.0) / count as f64;
401 assert!(bits_per_weight < 1.7, "bits/weight {bits_per_weight}");
402 }
403
404 #[test]
405 fn ternary_gemm_cpu_matches_hand_computation() {
406 let trits: [i8; 6] = [1, -1, 0, 0, 1, 1];
408 let packed = pack_trits(&trits);
409 let act = [1.0_f32, 2.0, 3.0];
410 let mut out = [0.0_f32; 2];
411 ternary_gemm_cpu(&act, &packed, 2.0, 3, 2, 1, 0, 0, &mut out);
412 assert_eq!(out, [-2.0, 10.0]);
414 }
415
416 #[test]
417 fn ternary_gemm_equals_dense_matmul_of_dequantized_weights() {
418 let (n_in, n_out) = (7usize, 5usize);
421 let scale = 0.37_f32;
422 let trits: Vec<i8> = (0..n_in * n_out).map(|k| (k % 3) as i8 - 1).collect();
424 let packed = pack_trits(&trits);
425 let act: Vec<f32> = (0..n_in).map(|j| (j as f32) * 0.5 - 1.0).collect();
426
427 let mut got = vec![0.0_f32; n_out];
428 ternary_gemm_cpu(&act, &packed, scale, n_in, n_out, 1, 0, 0, &mut got);
429
430 for i in 0..n_out {
431 let mut dense = 0.0_f32;
432 for j in 0..n_in {
433 let w = scale * trits[i * n_in + j] as f32; dense += w * act[j];
435 }
436 assert!(
437 (got[i] - dense).abs() < 1e-5,
438 "row {i}: {} vs {}",
439 got[i],
440 dense
441 );
442 }
443 }
444
445 #[test]
446 fn ternary_gemm_wgsl_parses() {
447 naga::front::wgsl::Frontend::new()
451 .parse(TERNARY_GEMM_WGSL)
452 .unwrap_or_else(|e| panic!("ternary_gemm.wgsl parse failed: {e:?}"));
453 }
454
455 #[test]
456 fn all_zero_and_empty_are_safe() {
457 let (s, t) = quantize_ternary(&[0.0, 0.0, 0.0]);
458 assert_eq!(s, 0.0);
459 assert_eq!(t, vec![0, 0, 0]);
460 let (s2, t2) = quantize_ternary(&[]);
461 assert_eq!(s2, 0.0);
462 assert!(t2.is_empty());
463 }
464}