Skip to main content

handwired_rs/
tensor.rs

1//! Compilation into layers of sparse matrix multiplications, and a CPU executor.
2//!
3//! Independent operations at the same dependency depth share a layer. A layer
4//! gathers its earlier inputs, evaluates one sparse affine projection (one row
5//! per output, then one gate row per ReGLU, then one index row per index read),
6//! applies index reads, ReGLU gates and boolean rounding, and appends its
7//! outputs. Operations that no requested root reads are removed.
8//!
9//! Not yet ported from handwired: lowering one-hot masked sums to gathers, and
10//! recomputing depths after lowering.
11
12use crate::Error;
13use crate::circuit::{Circuit, OpKind, OutOfRange, resolve_index_position};
14use crate::numeric::{evaluate_boolean, relu32};
15use crate::wire::Wire;
16
17#[derive(Clone, Debug)]
18pub struct IndexReadRow {
19    pub row: u32,
20    pub index_row: u32,
21    pub below_zero: bool,
22    pub above_zero: bool,
23    /// Packed positions of the candidates.
24    pub candidate_positions: Vec<u32>,
25}
26
27#[derive(Clone, Debug)]
28pub struct TensorLayer {
29    /// Packed positions this layer reads, in column order.
30    pub input_indices: Vec<u32>,
31    pub output_size: usize,
32    /// One bias per projection row, already rounded to f32.
33    pub bias: Vec<f32>,
34    /// CSR sparse matrix over the projection rows.
35    pub row_offsets: Vec<u32>,
36    pub columns: Vec<u32>,
37    pub weights: Vec<f32>,
38    /// Output rows whose value is multiplied by relu of gate row `output_size + k`.
39    pub gate_rows: Vec<u32>,
40    pub boolean_rows: Vec<u32>,
41    pub index_reads: Vec<IndexReadRow>,
42}
43
44#[derive(Clone, Debug)]
45pub struct TensorProgram {
46    pub layers: Vec<TensorLayer>,
47    pub roots: Vec<Wire>,
48    pub source_size: usize,
49    /// Source cells plus live operations.
50    pub value_count: usize,
51    /// `wire_order[packed]` is the wire held at that packed position.
52    pub wire_order: Vec<Wire>,
53    /// The circuit's width, for full-width results.
54    pub width: usize,
55}
56
57impl TensorProgram {
58    pub fn stored_weight_count(&self) -> usize {
59        self.layers.iter().map(|layer| layer.weights.len()).sum()
60    }
61
62    pub fn live_operation_count(&self) -> usize {
63        self.value_count - self.source_size
64    }
65}
66
67impl Circuit {
68    /// Compile the operations that `roots` need, or every operation when
69    /// `roots` is `None`.
70    pub fn compile(&self, roots: Option<&[Wire]>) -> TensorProgram {
71        let width = self.width();
72        let source_size = self.source_size();
73        let roots: Vec<Wire> = match roots {
74            Some(roots) => roots.to_vec(),
75            None => (source_size..width).map(Wire::from_index).collect(),
76        };
77        let live = self.find_live_operations(&roots);
78        let mut operations_by_layer: Vec<Vec<usize>> = Vec::new();
79        for (operation_index, operation) in self.operations.iter().enumerate() {
80            if !live[operation_index] {
81                continue;
82            }
83            let layer = operation.depth as usize - 1;
84            if operations_by_layer.len() <= layer {
85                operations_by_layer.resize(layer + 1, Vec::new());
86            }
87            operations_by_layer[layer].push(operation_index);
88        }
89
90        let live_count = live.iter().filter(|&&is_live| is_live).count();
91        let value_count = source_size + live_count;
92        let mut wire_order: Vec<Wire> = (0..source_size).map(Wire::from_index).collect();
93        let mut packed_by_wire = vec![u32::MAX; width];
94        for (wire, packed) in packed_by_wire.iter_mut().enumerate().take(source_size) {
95            *packed = wire as u32;
96        }
97        // The column of each wire in the layer being compiled, reset after each layer.
98        let mut column_by_wire = vec![u32::MAX; width];
99        let mut available = source_size;
100        let mut layers = Vec::new();
101        for operation_indices in operations_by_layer.iter().filter(|indices| !indices.is_empty()) {
102            let output_size = operation_indices.len();
103            let mut input_wires: Vec<Wire> = Vec::new();
104            for &operation_index in operation_indices {
105                let operation = &self.operations[operation_index];
106                for &(wire, _) in operation.left_terms.iter().chain(&operation.gate_terms) {
107                    if column_by_wire[wire.index()] == u32::MAX {
108                        column_by_wire[wire.index()] = 0;
109                        input_wires.push(wire);
110                    }
111                }
112            }
113            input_wires.sort_by_key(|wire| packed_by_wire[wire.index()]);
114            for (column, wire) in input_wires.iter().enumerate() {
115                column_by_wire[wire.index()] = column as u32;
116            }
117            let gate_count =
118                operation_indices.iter().filter(|&&index| self.operations[index].kind == OpKind::Reglu).count();
119            let index_count =
120                operation_indices.iter().filter(|&&index| self.operations[index].kind == OpKind::Index).count();
121            let projection_size = output_size + gate_count + index_count;
122            let mut bias = vec![0f32; projection_size];
123            let mut projection_terms: Vec<&[(Wire, f64)]> = vec![&[]; projection_size];
124            let mut gate_rows = Vec::new();
125            let mut boolean_rows = Vec::new();
126            let mut pending_index_reads = Vec::new();
127            let (mut gate_row, mut index_row) = (0, 0);
128            for (row, &operation_index) in operation_indices.iter().enumerate() {
129                let operation = &self.operations[operation_index];
130                let output_wire = source_size + operation_index;
131                wire_order.push(Wire::from_index(output_wire));
132                packed_by_wire[output_wire] = (available + row) as u32;
133                bias[row] = operation.left_bias as f32;
134                projection_terms[row] = &operation.left_terms;
135                match operation.kind {
136                    OpKind::Reglu => {
137                        let projection_row = output_size + gate_row;
138                        bias[projection_row] = operation.gate_bias as f32;
139                        projection_terms[projection_row] = &operation.gate_terms;
140                        gate_rows.push(row as u32);
141                        gate_row += 1;
142                    }
143                    OpKind::Boolean => boolean_rows.push(row as u32),
144                    OpKind::Index => {
145                        let projection_row = output_size + gate_count + index_row;
146                        bias[projection_row] = operation.gate_bias as f32;
147                        projection_terms[projection_row] = &operation.gate_terms;
148                        pending_index_reads.push((row, projection_row, operation.index_read.as_ref().unwrap()));
149                        index_row += 1;
150                    }
151                    OpKind::Affine => {}
152                }
153            }
154            let index_reads = pending_index_reads
155                .into_iter()
156                .map(|(row, index_row, read)| IndexReadRow {
157                    row: row as u32,
158                    index_row: index_row as u32,
159                    below_zero: read.below == OutOfRange::Zero,
160                    above_zero: read.above == OutOfRange::Zero,
161                    candidate_positions: self.tables[read.table]
162                        .candidates
163                        .iter()
164                        .map(|candidate| {
165                            let position = packed_by_wire[candidate.index()];
166                            assert!((position as usize) < available, "index read candidate is not available");
167                            position
168                        })
169                        .collect(),
170                })
171                .collect();
172            let mut row_offsets = Vec::with_capacity(projection_size + 1);
173            let mut columns = Vec::new();
174            let mut weights = Vec::new();
175            row_offsets.push(0);
176            for terms in &projection_terms {
177                for &(wire, coefficient) in terms.iter() {
178                    let column = column_by_wire[wire.index()];
179                    assert!(column != u32::MAX, "tensor dependency is not available");
180                    columns.push(column);
181                    weights.push(coefficient as f32);
182                }
183                row_offsets.push(columns.len() as u32);
184            }
185            for wire in &input_wires {
186                column_by_wire[wire.index()] = u32::MAX;
187            }
188            layers.push(TensorLayer {
189                input_indices: input_wires.iter().map(|wire| packed_by_wire[wire.index()]).collect(),
190                output_size,
191                bias,
192                row_offsets,
193                columns,
194                weights,
195                gate_rows,
196                boolean_rows,
197                index_reads,
198            });
199            available += output_size;
200        }
201        TensorProgram { layers, roots, source_size, value_count, wire_order, width }
202    }
203
204    fn find_live_operations(&self, roots: &[Wire]) -> Vec<bool> {
205        let source_size = self.source_size();
206        let mut live = vec![false; self.operations.len()];
207        let mut visited_tables = vec![false; self.tables.len()];
208        let mut pending = roots.to_vec();
209        while let Some(wire) = pending.pop() {
210            self.assert_wire(wire);
211            let Some(operation_index) = wire.index().checked_sub(source_size) else { continue };
212            if live[operation_index] {
213                continue;
214            }
215            live[operation_index] = true;
216            let operation = &self.operations[operation_index];
217            pending.extend(operation.left_terms.iter().chain(&operation.gate_terms).map(|&(wire, _)| wire));
218            if let Some(read) = &operation.index_read
219                && !visited_tables[read.table]
220            {
221                visited_tables[read.table] = true;
222                pending.extend(&self.tables[read.table].candidates);
223            }
224        }
225        live
226    }
227
228    /// Every wire's value through the compiled program, with 0 for removed wires.
229    pub fn evaluate_tensor(&self, source: &[f32], program: &TensorProgram) -> Result<Vec<f32>, Error> {
230        self.validate_source_ranges(source)?;
231        let mut executor = TensorExecutor::new(program);
232        executor.run(source);
233        let mut values = vec![0f32; program.width];
234        for (packed, wire) in program.wire_order.iter().enumerate() {
235            values[wire.index()] = executor.packed_values[packed];
236        }
237        Ok(values)
238    }
239
240    /// Check that a compiled evaluation reproduces every live value of the
241    /// reference evaluator, compared with `==` like handwired's `===`.
242    pub fn validate_tensor_evaluation(
243        &self,
244        source: &[f32],
245        program: &TensorProgram,
246        tensor_values: &[f32],
247    ) -> Result<(), String> {
248        let expected = self.evaluate_unchecked(source);
249        for wire in &program.wire_order {
250            let (got, want) = (tensor_values[wire.index()], expected[wire.index()]);
251            if got != want {
252                return Err(format!("tensor wire {} is {got}, the reference evaluator gives {want}", wire.index()));
253            }
254        }
255        Ok(())
256    }
257}
258
259/// Reusable buffers for running one program many times.
260pub struct TensorExecutor<'program> {
261    program: &'program TensorProgram,
262    /// After a run, `packed_values[k]` is the value of `program.wire_order[k]`.
263    pub packed_values: Vec<f32>,
264    input: Vec<f32>,
265    projection: Vec<f32>,
266    root_positions: Vec<usize>,
267    outputs: Vec<f32>,
268}
269
270impl<'program> TensorExecutor<'program> {
271    pub fn new(program: &'program TensorProgram) -> TensorExecutor<'program> {
272        let max_input = program.layers.iter().map(|layer| layer.input_indices.len()).max().unwrap_or(0);
273        let max_projection = program.layers.iter().map(|layer| layer.bias.len()).max().unwrap_or(0);
274        let root_positions = program
275            .roots
276            .iter()
277            .map(|root| program.wire_order.iter().position(|wire| wire == root).expect("root was removed"))
278            .collect();
279        TensorExecutor {
280            program,
281            packed_values: vec![0.0; program.value_count],
282            input: vec![0.0; max_input],
283            projection: vec![0.0; max_projection],
284            root_positions,
285            outputs: vec![0.0; program.roots.len()],
286        }
287    }
288
289    /// Evaluate the program and return the roots' values. The source is not
290    /// range-checked; use [`Circuit::validate_source_ranges`] for that.
291    pub fn run(&mut self, source: &[f32]) -> &[f32] {
292        let program = self.program;
293        assert_eq!(source.len(), program.source_size, "source table has the wrong length");
294        let values = &mut self.packed_values;
295        values[..source.len()].copy_from_slice(source);
296        let mut available = source.len();
297        for layer in &program.layers {
298            for (column, &packed) in layer.input_indices.iter().enumerate() {
299                self.input[column] = values[packed as usize];
300            }
301            let projection = &mut self.projection[..layer.bias.len()];
302            for (row, output) in projection.iter_mut().enumerate() {
303                let mut value = layer.bias[row];
304                for entry in layer.row_offsets[row] as usize..layer.row_offsets[row + 1] as usize {
305                    value += layer.weights[entry] * self.input[layer.columns[entry] as usize];
306                }
307                *output = value;
308            }
309            for read in &layer.index_reads {
310                let index = projection[read.index_row as usize] as f64;
311                let below = if read.below_zero { OutOfRange::Zero } else { OutOfRange::Clamp };
312                let above = if read.above_zero { OutOfRange::Zero } else { OutOfRange::Clamp };
313                projection[read.row as usize] =
314                    match resolve_index_position(index, read.candidate_positions.len(), below, above) {
315                        -2 => f32::NAN,
316                        -1 => 0.0,
317                        position => values[read.candidate_positions[position as usize] as usize],
318                    };
319            }
320            for (gate, &row) in layer.gate_rows.iter().enumerate() {
321                let gate_value = relu32(projection[layer.output_size + gate]);
322                projection[row as usize] *= gate_value;
323            }
324            for &row in &layer.boolean_rows {
325                projection[row as usize] = evaluate_boolean(projection[row as usize]);
326            }
327            values[available..available + layer.output_size].copy_from_slice(&projection[..layer.output_size]);
328            available += layer.output_size;
329        }
330        for (output, &position) in self.outputs.iter_mut().zip(&self.root_positions) {
331            *output = values[position];
332        }
333        &self.outputs
334    }
335}