use super::ops::tq19_dot;
use super::{TQ19Matrix, MAX_RAW, MIN_RAW, TRIT_DECODE_TABLE};
use crate::fixed_point::universal::fasc::stack_evaluator::BinaryStorage;
use rayon::prelude::*;
pub const NUM_PLANES: usize = 10;
pub const POW3: [i16; NUM_PLANES] = [1, 3, 9, 27, 81, 243, 729, 2187, 6561, 19683];
pub const SPARSE_DENSITY_PERCENT: u64 = 5;
const BYTE_WEIGHTS: [u8; 5] = [81, 27, 9, 3, 1];
const ZERO_BYTE: u8 = 121;
static CONTRIB_LUT: [[[i16; 8]; 256]; NUM_PLANES] = build_contrib_luts();
const fn build_contrib_luts() -> [[[i16; 8]; 256]; NUM_PLANES] {
let mut luts = [[[0i16; 8]; 256]; NUM_PLANES];
let mut k = 0;
while k < NUM_PLANES {
let w = POW3[k];
let mut b = 0usize;
while b < 243 {
let mut v = b as u16;
let mut j = 5usize;
while j > 0 {
j -= 1;
luts[k][b][j] = ((v % 3) as i16 - 1) * w;
v /= 3;
}
b += 1;
}
k += 1;
}
luts
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PlaneData {
Empty,
Dense(Vec<u8>),
Sparse {
row_ptr_pos: Vec<u32>,
cols_pos: Vec<u32>,
row_ptr_neg: Vec<u32>,
cols_neg: Vec<u32>,
},
}
impl PlaneData {
pub fn size_bytes(&self) -> usize {
match self {
PlaneData::Empty => 0,
PlaneData::Dense(bytes) => bytes.len(),
PlaneData::Sparse { row_ptr_pos, cols_pos, row_ptr_neg, cols_neg } => {
(row_ptr_pos.len() + cols_pos.len() + row_ptr_neg.len() + cols_neg.len()) * 4
}
}
}
}
#[derive(Clone, Debug)]
pub struct PlanarTQ19 {
rows: usize,
cols: usize,
planes: [PlaneData; NUM_PLANES],
}
#[inline]
fn bt_digits(raw: i16) -> [i8; NUM_PLANES] {
debug_assert!((MIN_RAW..=MAX_RAW).contains(&raw));
let mut digits = [0i8; NUM_PLANES];
let mut v = raw as i32;
let mut k = 0;
while v != 0 {
let mut d = v.rem_euclid(3);
if d == 2 {
d = -1;
}
digits[k] = d as i8;
v = (v - d) / 3;
k += 1;
}
digits
}
impl PlanarTQ19 {
pub fn from_tq19(m: &TQ19Matrix) -> Self {
let rows = m.rows();
let cols = m.cols();
let data = m.data();
let total = rows * cols;
assert!(
data.iter().all(|&raw| (MIN_RAW..=MAX_RAW).contains(&raw)),
"PlanarTQ19::from_tq19: every weight must satisfy |raw| <= {MAX_RAW} (valid TQ1.9 range)"
);
let mut nnz = [0u64; NUM_PLANES];
for &raw in data {
let digits = bt_digits(raw);
for k in 0..NUM_PLANES {
if digits[k] != 0 {
nnz[k] += 1;
}
}
}
#[derive(Clone, Copy, PartialEq)]
enum Mode {
Empty,
Sparse,
Dense,
}
let mut modes = [Mode::Empty; NUM_PLANES];
for k in 0..NUM_PLANES {
modes[k] = if nnz[k] == 0 {
Mode::Empty
} else if nnz[k] * 100 < SPARSE_DENSITY_PERCENT * total as u64 {
Mode::Sparse
} else {
Mode::Dense
};
}
let bytes_per_row = (cols + 4) / 5;
let mut dense: [Vec<u8>; NUM_PLANES] = Default::default();
let mut row_ptr_pos: [Vec<u32>; NUM_PLANES] = Default::default();
let mut cols_pos: [Vec<u32>; NUM_PLANES] = Default::default();
let mut row_ptr_neg: [Vec<u32>; NUM_PLANES] = Default::default();
let mut cols_neg: [Vec<u32>; NUM_PLANES] = Default::default();
for k in 0..NUM_PLANES {
match modes[k] {
Mode::Dense => dense[k].reserve_exact(rows * bytes_per_row),
Mode::Sparse => {
row_ptr_pos[k].reserve_exact(rows + 1);
row_ptr_neg[k].reserve_exact(rows + 1);
row_ptr_pos[k].push(0);
row_ptr_neg[k].push(0);
}
Mode::Empty => {}
}
}
let mut byte_acc = [0u8; NUM_PLANES];
for row in 0..rows {
for col in 0..cols {
let digits = bt_digits(data[row * cols + col]);
let pos_in_byte = col % 5;
for k in 0..NUM_PLANES {
match modes[k] {
Mode::Dense => {
byte_acc[k] += (digits[k] + 1) as u8 * BYTE_WEIGHTS[pos_in_byte];
if pos_in_byte == 4 {
dense[k].push(byte_acc[k]);
byte_acc[k] = 0;
}
}
Mode::Sparse => match digits[k] {
1 => cols_pos[k].push(col as u32),
-1 => cols_neg[k].push(col as u32),
_ => {}
},
Mode::Empty => {}
}
}
}
let rem = cols % 5;
if rem != 0 {
for k in 0..NUM_PLANES {
if modes[k] == Mode::Dense {
for pad in rem..5 {
byte_acc[k] += BYTE_WEIGHTS[pad];
}
dense[k].push(byte_acc[k]);
byte_acc[k] = 0;
}
}
}
for k in 0..NUM_PLANES {
if modes[k] == Mode::Sparse {
row_ptr_pos[k].push(cols_pos[k].len() as u32);
row_ptr_neg[k].push(cols_neg[k].len() as u32);
}
}
}
let mut planes: [PlaneData; NUM_PLANES] =
std::array::from_fn(|_| PlaneData::Empty);
for k in 0..NUM_PLANES {
planes[k] = match modes[k] {
Mode::Empty => PlaneData::Empty,
Mode::Dense => PlaneData::Dense(std::mem::take(&mut dense[k])),
Mode::Sparse => PlaneData::Sparse {
row_ptr_pos: std::mem::take(&mut row_ptr_pos[k]),
cols_pos: std::mem::take(&mut cols_pos[k]),
row_ptr_neg: std::mem::take(&mut row_ptr_neg[k]),
cols_neg: std::mem::take(&mut cols_neg[k]),
},
};
}
Self { rows, cols, planes }
}
pub fn to_tq19(&self) -> TQ19Matrix {
let mut data = vec![0i16; self.rows * self.cols];
let bytes_per_row = (self.cols + 4) / 5;
for (k, plane) in self.planes.iter().enumerate() {
let w = POW3[k];
match plane {
PlaneData::Empty => {}
PlaneData::Dense(bytes) => {
for row in 0..self.rows {
let base = row * bytes_per_row;
for (byte_idx, &b) in
bytes[base..base + bytes_per_row].iter().enumerate()
{
let trits = TRIT_DECODE_TABLE[b as usize];
for (j, &t) in trits.iter().enumerate() {
let col = byte_idx * 5 + j;
if col < self.cols && t != 0 {
data[row * self.cols + col] += t as i16 * w;
}
}
}
}
}
PlaneData::Sparse { row_ptr_pos, cols_pos, row_ptr_neg, cols_neg } => {
for row in 0..self.rows {
for &c in &cols_pos
[row_ptr_pos[row] as usize..row_ptr_pos[row + 1] as usize]
{
data[row * self.cols + c as usize] += w;
}
for &c in &cols_neg
[row_ptr_neg[row] as usize..row_ptr_neg[row + 1] as usize]
{
data[row * self.cols + c as usize] -= w;
}
}
}
}
}
TQ19Matrix::new(self.rows, self.cols, data)
}
pub fn rows(&self) -> usize {
self.rows
}
pub fn cols(&self) -> usize {
self.cols
}
pub fn planes(&self) -> &[PlaneData; NUM_PLANES] {
&self.planes
}
pub fn from_parts(rows: usize, cols: usize, planes: [PlaneData; NUM_PLANES]) -> Self {
Self { rows, cols, planes }
}
pub fn size_bytes(&self) -> usize {
self.planes.iter().map(PlaneData::size_bytes).sum()
}
fn reconstruct_row_into(&self, row: usize, buf: &mut [i16]) {
debug_assert!(buf.len() >= self.cols + 7);
buf.fill(0);
let bytes_per_row = (self.cols + 4) / 5;
for (k, plane) in self.planes.iter().enumerate() {
match plane {
PlaneData::Empty => {}
PlaneData::Dense(bytes) => {
let lut = &CONTRIB_LUT[k];
let base = row * bytes_per_row;
let row_bytes = &bytes[base..base + bytes_per_row];
#[cfg(target_arch = "x86_64")]
unsafe {
use std::arch::x86_64::*;
let mut j = 0usize;
for &b in row_bytes {
if b != ZERO_BYTE {
let contrib =
_mm_loadu_si128(lut[b as usize].as_ptr() as *const __m128i);
let dst = buf.as_mut_ptr().add(j) as *mut __m128i;
let cur = _mm_loadu_si128(dst as *const __m128i);
_mm_storeu_si128(dst, _mm_add_epi16(cur, contrib));
}
j += 5;
}
}
#[cfg(not(target_arch = "x86_64"))]
{
let mut j = 0usize;
for &b in row_bytes {
if b != ZERO_BYTE {
let c = &lut[b as usize];
for t in 0..5 {
buf[j + t] += c[t];
}
}
j += 5;
}
}
}
PlaneData::Sparse { row_ptr_pos, cols_pos, row_ptr_neg, cols_neg } => {
let w = POW3[k];
for &c in
&cols_pos[row_ptr_pos[row] as usize..row_ptr_pos[row + 1] as usize]
{
buf[c as usize] += w;
}
for &c in
&cols_neg[row_ptr_neg[row] as usize..row_ptr_neg[row + 1] as usize]
{
buf[c as usize] -= w;
}
}
}
}
}
#[inline]
fn buf_len(&self) -> usize {
self.cols + 8
}
pub fn matvec(&self, activations: &[BinaryStorage]) -> Vec<BinaryStorage> {
assert_eq!(
activations.len(),
self.cols,
"PlanarTQ19::matvec: activation length mismatch"
);
let mut buf = vec![0i16; self.buf_len()];
(0..self.rows)
.map(|row| {
self.reconstruct_row_into(row, &mut buf);
tq19_dot(&buf[..self.cols], activations)
})
.collect()
}
pub fn matvec_par(&self, activations: &[BinaryStorage]) -> Vec<BinaryStorage> {
assert_eq!(
activations.len(),
self.cols,
"PlanarTQ19::matvec_par: activation length mismatch"
);
(0..self.rows)
.into_par_iter()
.map_init(
|| vec![0i16; self.buf_len()],
|buf, row| {
self.reconstruct_row_into(row, buf);
tq19_dot(&buf[..self.cols], activations)
},
)
.collect()
}
pub fn matvec_batch(&self, batch: &[&[BinaryStorage]]) -> Vec<Vec<BinaryStorage>> {
for (i, v) in batch.iter().enumerate() {
assert_eq!(
v.len(),
self.cols,
"PlanarTQ19::matvec_batch: activation[{i}] length mismatch"
);
}
let mut buf = vec![0i16; self.buf_len()];
let results_by_row: Vec<Vec<BinaryStorage>> = (0..self.rows)
.map(|row| {
self.reconstruct_row_into(row, &mut buf);
let w = &buf[..self.cols];
batch.iter().map(|acts| tq19_dot(w, acts)).collect()
})
.collect();
transpose(results_by_row, batch.len())
}
pub fn matvec_batch_par(&self, batch: &[&[BinaryStorage]]) -> Vec<Vec<BinaryStorage>> {
for (i, v) in batch.iter().enumerate() {
assert_eq!(
v.len(),
self.cols,
"PlanarTQ19::matvec_batch_par: activation[{i}] length mismatch"
);
}
let results_by_row: Vec<Vec<BinaryStorage>> = (0..self.rows)
.into_par_iter()
.map_init(
|| vec![0i16; self.buf_len()],
|buf, row| {
self.reconstruct_row_into(row, buf);
let w = &buf[..self.cols];
batch.iter().map(|acts| tq19_dot(w, acts)).collect()
},
)
.collect();
transpose(results_by_row, batch.len())
}
}
fn transpose(results_by_row: Vec<Vec<BinaryStorage>>, batch_len: usize) -> Vec<Vec<BinaryStorage>> {
(0..batch_len)
.map(|b| results_by_row.iter().map(|r| r[b]).collect())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixed_point::imperative::FixedPoint;
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
self.0
}
fn raw(&mut self) -> i16 {
((self.next() % (2 * MAX_RAW as u64 + 1)) as i64 - MAX_RAW as i64) as i16
}
fn raw_small(&mut self) -> i16 {
let a = (self.next() % 2001) as i64 - 1000;
let b = (self.next() % 2001) as i64 - 1000;
(a + b) as i16
}
fn activation(&mut self) -> BinaryStorage {
let n = (self.next() % 2001) as i32 - 1000;
FixedPoint::from_int(n).raw()
}
}
#[test]
fn bt_digits_reconstruct() {
for raw in MIN_RAW..=MAX_RAW {
let digits = bt_digits(raw);
let mut v: i32 = 0;
for k in (0..NUM_PLANES).rev() {
v = v * 3 + digits[k] as i32;
}
assert_eq!(v, raw as i32, "raw {raw} did not reconstruct");
}
}
#[test]
fn roundtrip_full_range() {
let mut rng = Lcg(42);
let (rows, cols) = (17, 23); let data: Vec<i16> = (0..rows * cols).map(|_| rng.raw()).collect();
let m = TQ19Matrix::new(rows, cols, data.clone());
let planar = PlanarTQ19::from_tq19(&m);
assert_eq!(planar.to_tq19().data(), m.data());
}
#[test]
#[should_panic(expected = "valid TQ1.9 range")]
fn from_tq19_rejects_out_of_range_weight() {
let m = TQ19Matrix::new(1, 1, vec![MAX_RAW + 1]);
let _ = PlanarTQ19::from_tq19(&m);
}
#[test]
fn roundtrip_bell_distribution() {
let mut rng = Lcg(7);
let (rows, cols) = (32, 64);
let data: Vec<i16> = (0..rows * cols).map(|_| rng.raw_small()).collect();
let m = TQ19Matrix::new(rows, cols, data.clone());
let planar = PlanarTQ19::from_tq19(&m);
assert_eq!(planar.to_tq19().data(), m.data());
assert!(matches!(planar.planes()[9], PlaneData::Empty | PlaneData::Sparse { .. }));
assert!(planar.size_bytes() < rows * cols * 2, "no compression on bell data");
}
#[test]
fn matvec_bit_identical_full_range() {
let mut rng = Lcg(1234);
for &(rows, cols) in &[(3usize, 5usize), (8, 17), (16, 128), (5, 3)] {
let data: Vec<i16> = (0..rows * cols).map(|_| rng.raw()).collect();
let m = TQ19Matrix::new(rows, cols, data);
let planar = PlanarTQ19::from_tq19(&m);
let acts: Vec<BinaryStorage> = (0..cols).map(|_| rng.activation()).collect();
assert_eq!(planar.matvec(&acts), m.matvec(&acts), "{rows}x{cols} mismatch");
}
}
#[test]
fn matvec_bit_identical_bell_distribution() {
let mut rng = Lcg(99);
let (rows, cols) = (24, 96);
let data: Vec<i16> = (0..rows * cols).map(|_| rng.raw_small()).collect();
let m = TQ19Matrix::new(rows, cols, data);
let planar = PlanarTQ19::from_tq19(&m);
let acts: Vec<BinaryStorage> = (0..cols).map(|_| rng.activation()).collect();
assert_eq!(planar.matvec(&acts), m.matvec(&acts));
}
#[test]
fn matvec_bit_identical_sparse_only() {
let (rows, cols) = (6, 40);
let mut data = vec![0i16; rows * cols];
data[3] = 19683; data[45] = -6561; data[200] = 2187 + 729; let m = TQ19Matrix::new(rows, cols, data);
let planar = PlanarTQ19::from_tq19(&m);
let mut rng = Lcg(5);
let acts: Vec<BinaryStorage> = (0..cols).map(|_| rng.activation()).collect();
assert_eq!(planar.matvec(&acts), m.matvec(&acts));
assert_eq!(planar.planes()[0], PlaneData::Empty);
}
#[test]
fn parallel_and_batch_match_sequential() {
let mut rng = Lcg(2026);
let (rows, cols) = (19, 37);
let data: Vec<i16> = (0..rows * cols).map(|_| rng.raw_small()).collect();
let m = TQ19Matrix::new(rows, cols, data);
let planar = PlanarTQ19::from_tq19(&m);
let acts: Vec<BinaryStorage> = (0..cols).map(|_| rng.activation()).collect();
assert_eq!(planar.matvec_par(&acts), planar.matvec(&acts));
let batch_data: Vec<Vec<BinaryStorage>> = (0..4)
.map(|_| (0..cols).map(|_| rng.activation()).collect())
.collect();
let batch: Vec<&[BinaryStorage]> = batch_data.iter().map(|v| v.as_slice()).collect();
let expected: Vec<Vec<BinaryStorage>> = batch.iter().map(|v| m.matvec(v)).collect();
assert_eq!(planar.matvec_batch(&batch), expected);
assert_eq!(planar.matvec_batch_par(&batch), expected);
}
#[test]
fn from_parts_roundtrip() {
let mut rng = Lcg(11);
let (rows, cols) = (9, 14);
let data: Vec<i16> = (0..rows * cols).map(|_| rng.raw_small()).collect();
let m = TQ19Matrix::new(rows, cols, data);
let planar = PlanarTQ19::from_tq19(&m);
let rebuilt = PlanarTQ19::from_parts(rows, cols, planar.planes().clone());
assert_eq!(rebuilt.to_tq19().data(), m.data());
}
}