Skip to main content

logicaffeine_compile/codegen_dram/
kernels.rs

1//! DRAM application kernels — the workloads in-memory compute is lightning-fast at: bitwise + COUNT
2//! over massive data, at zero data movement. Popcount is the linchpin — the reduction that turns the
3//! bitwise/XNOR machinery into counts, distances, and dot products:
4//!
5//!   • [`popcount`] — per-lane population count, folded bit-serially into an in-DRAM counter;
6//!   • [`hamming_distance`] = popcount(a XOR b) — vector similarity search (ANN over binary codes),
7//!     binary-net inference, bioinformatics distance;
8//!   • [`agreement_count`] = popcount(a XNOR b) — the 1-bit / binary neural-net DOT PRODUCT (for ±1
9//!     codes, dot = 2·agreements − K). This is how a BitNet-style matmul runs with NO multiply.
10//!
11//! All are composed from the `{And, Or, Not}` basis over a bit-serial (bitplane) layout: bitplane j
12//! is a row whose bit `i` is bit `j` of lane `i`'s value, so one kernel processes all lanes in
13//! parallel — exactly the data-parallel, bandwidth-bound shape the DRAM plane wins on.
14
15use super::dataflow::{bit_serial_sub, DramDataflow};
16
17/// Add a single-bit plane `bit` (0/1 per lane) into a per-lane counter `counter` (W planes, LSB
18/// first) — a half-adder ripple. Overflow beyond W is dropped, so size W ≥ ⌈log2(max_count+1)⌉.
19fn increment_by_bit(df: &mut DramDataflow, counter: &[usize], bit: usize) -> Vec<usize> {
20    let mut carry = bit;
21    let mut out = Vec::with_capacity(counter.len());
22    for &c in counter {
23        out.push(df.xor(c, carry)); // sum bit
24        carry = df.and(c, carry); // carry out
25    }
26    out
27}
28
29/// Per-lane POPCOUNT of `planes`: for each lane, the number of set bits across the planes, as an
30/// `out_width`-plane per-lane integer (LSB first). A reduction realized ENTIRELY in the DRAM plane by
31/// folding each plane into a bit-serial counter. `zero` is a zero row (the initial counter value).
32pub fn popcount(
33    df: &mut DramDataflow,
34    planes: &[usize],
35    out_width: usize,
36    zero: usize,
37) -> Vec<usize> {
38    let mut counter: Vec<usize> = vec![zero; out_width];
39    for &p in planes {
40        counter = increment_by_bit(df, &counter, p);
41    }
42    counter
43}
44
45/// Per-lane HAMMING DISTANCE between two bit-serial codes `a`/`b` (K planes each): popcount(a XOR b).
46/// The core kernel of vector similarity search (ANN over binary codes) and binary-net inference.
47pub fn hamming_distance(
48    df: &mut DramDataflow,
49    a: &[usize],
50    b: &[usize],
51    out_width: usize,
52    zero: usize,
53) -> Vec<usize> {
54    let xored: Vec<usize> = a.iter().zip(b).map(|(&ai, &bi)| df.xor(ai, bi)).collect();
55    popcount(df, &xored, out_width, zero)
56}
57
58/// Per-lane AGREEMENT COUNT popcount(a XNOR b) — the binary / 1-bit neural-net DOT PRODUCT. For ±1
59/// codes packed as bits, the true dot product is `2·agreements − K`; the agreement count is the
60/// in-DRAM half (XNOR + popcount, no multiply), the final affine map is a scalar host step.
61pub fn agreement_count(
62    df: &mut DramDataflow,
63    a: &[usize],
64    b: &[usize],
65    out_width: usize,
66    zero: usize,
67) -> Vec<usize> {
68    let xnored: Vec<usize> = a.iter().zip(b).map(|(&ai, &bi)| df.xnor(ai, bi)).collect();
69    popcount(df, &xnored, out_width, zero)
70}
71
72/// BitNet-style TERNARY·BINARY dot product — one matmul row, entirely in-DRAM, NO multiply. Ternary
73/// weights `w[k]` ∈ {−1,0,+1} are carried as two disjoint bitplane sets: `wp[k]`=1 iff w=+1, `wn[k]`=1
74/// iff w=−1. Binary activations `x[k]` ∈ {0,1}. Then, per lane (per output neuron),
75///   `dot = Σ w[k]·x[k] = popcount(Wp AND X) − popcount(Wn AND X)`
76/// as a two's-complement `out_width`-plane integer. `zero` is a zero row, `ones` an all-ones carry
77/// row (for the subtract). This is how a 1.58-bit LLM layer runs where the weights live: AND +
78/// popcount + subtract, no multiplier.
79pub fn ternary_binary_dot(
80    df: &mut DramDataflow,
81    wp: &[usize],
82    wn: &[usize],
83    x: &[usize],
84    out_width: usize,
85    zero: usize,
86    ones: usize,
87) -> Vec<usize> {
88    let wpx: Vec<usize> = wp.iter().zip(x).map(|(&w, &xi)| df.and(w, xi)).collect();
89    let wnx: Vec<usize> = wn.iter().zip(x).map(|(&w, &xi)| df.and(w, xi)).collect();
90    let pos = popcount(df, &wpx, out_width, zero);
91    let neg = popcount(df, &wnx, out_width, zero);
92    bit_serial_sub(df, &pos, &neg, ones)
93}
94
95#[cfg(test)]
96#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_dram/kernels/tests.rs"]
97mod tests;