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 gather reads, index reads, ReGLU gates and boolean rounding, and
7//! appends its outputs. Operations that no requested root reads are removed.
8//! A masked sum over a declared one-hot group compiles to a gather read (see
9//! [`OneHotGroup`](crate::OneHotGroup)).
10
11use std::collections::HashMap;
12
13use crate::Error;
14use crate::circuit::{Circuit, OpKind, OutOfRange, resolve_index_position};
15use crate::gather::{GatherPlan, plan_gather_lowering};
16use crate::numeric::{Range, evaluate_boolean, relu32};
17use crate::wire::Wire;
18
19#[derive(Clone, Debug)]
20pub struct IndexReadRow {
21    pub row: u32,
22    pub index_row: u32,
23    pub below_zero: bool,
24    pub above_zero: bool,
25    /// Packed positions of the candidates.
26    pub candidate_positions: Vec<u32>,
27}
28
29/// Resolves a one-hot group's selected position once per evaluation, at the
30/// first layer that reads the group: `Σ position × bit` and `Σ bit`, exact for
31/// 0/1 bits.
32#[derive(Clone, Debug)]
33pub struct GatherCompute {
34    pub slot: u32,
35    /// Packed positions of the group's members, in group order.
36    pub bit_positions: Vec<u32>,
37}
38
39/// A lowered masked sum: the row's bias plus the selected candidate, which is
40/// first multiplied by the group's sum of bits when the group allows all zeros.
41#[derive(Clone, Debug)]
42pub struct GatherRead {
43    pub row: u32,
44    pub slot: u32,
45    pub masked: bool,
46    pub candidate_positions: Vec<u32>,
47}
48
49#[derive(Clone, Debug, Copy)]
50pub struct CompileOptions {
51    /// Lower masked sums over declared one-hot groups to gather reads. On by default.
52    pub lower_gathers: bool,
53    /// Place each operation one layer after the deepest value it reads in the compiled program, instead of at its
54    /// build-time depth, so a gather read stops waiting for the products it replaced. Off by default.
55    pub recompute_depths: bool,
56}
57
58impl Default for CompileOptions {
59    fn default() -> CompileOptions {
60        CompileOptions { lower_gathers: true, recompute_depths: false }
61    }
62}
63
64#[derive(Clone, Debug)]
65pub struct TensorLayer {
66    /// Packed positions this layer reads, in column order.
67    pub input_indices: Vec<u32>,
68    pub output_size: usize,
69    /// One bias per projection row, already rounded to f32.
70    pub bias: Vec<f32>,
71    /// CSR sparse matrix over the projection rows.
72    pub row_offsets: Vec<u32>,
73    pub columns: Vec<u32>,
74    pub weights: Vec<f32>,
75    /// Output rows whose value is multiplied by relu of gate row `output_size + k`.
76    pub gate_rows: Vec<u32>,
77    pub boolean_rows: Vec<u32>,
78    pub gather_computes: Vec<GatherCompute>,
79    pub gather_reads: Vec<GatherRead>,
80    pub index_reads: Vec<IndexReadRow>,
81}
82
83#[derive(Clone, Debug)]
84pub struct TensorProgram {
85    pub layers: Vec<TensorLayer>,
86    pub roots: Vec<Wire>,
87    pub source_size: usize,
88    /// Source cells plus live operations.
89    pub value_count: usize,
90    /// `wire_order[packed]` is the wire held at that packed position.
91    pub wire_order: Vec<Wire>,
92    /// The circuit's width, for full-width results.
93    pub width: usize,
94    /// One slot per one-hot group that gather reads use.
95    pub gather_slot_count: usize,
96    /// Each packed position's range and grid in the circuit, which hold wherever its assumptions do.
97    pub ranges: Vec<Range>,
98    pub grids: Vec<Option<f64>>,
99}
100
101impl TensorProgram {
102    pub fn stored_weight_count(&self) -> usize {
103        self.layers.iter().map(|layer| layer.weights.len()).sum()
104    }
105
106    pub fn live_operation_count(&self) -> usize {
107        self.value_count - self.source_size
108    }
109
110    pub fn gather_read_count(&self) -> usize {
111        self.layers.iter().map(|layer| layer.gather_reads.len()).sum()
112    }
113}
114
115impl Circuit {
116    /// Compile the operations that `roots` need, or every operation when
117    /// `roots` is `None`, with the default [`CompileOptions`].
118    pub fn compile(&self, roots: Option<&[Wire]>) -> TensorProgram {
119        self.compile_with(roots, CompileOptions::default())
120    }
121
122    pub fn compile_with(&self, roots: Option<&[Wire]>, options: CompileOptions) -> TensorProgram {
123        let width = self.width();
124        let source_size = self.source_size();
125        let roots: Vec<Wire> = match roots {
126            Some(roots) => roots.to_vec(),
127            None => (source_size..width).map(Wire::from_index).collect(),
128        };
129        let plans = if options.lower_gathers { plan_gather_lowering(self) } else { HashMap::new() };
130        let live = self.find_live_operations(&roots, &plans);
131        let compiled_depths = options.recompute_depths.then(|| self.compute_compiled_depths(&live, &plans));
132        let mut operations_by_layer: Vec<Vec<usize>> = Vec::new();
133        for (operation_index, operation) in self.operations.iter().enumerate() {
134            if !live[operation_index] {
135                continue;
136            }
137            let depth = compiled_depths.as_ref().map_or(operation.depth, |depths| depths[operation_index]);
138            let layer = depth as usize - 1;
139            if operations_by_layer.len() <= layer {
140                operations_by_layer.resize(layer + 1, Vec::new());
141            }
142            operations_by_layer[layer].push(operation_index);
143        }
144
145        let live_count = live.iter().filter(|&&is_live| is_live).count();
146        let value_count = source_size + live_count;
147        let mut wire_order: Vec<Wire> = (0..source_size).map(Wire::from_index).collect();
148        let mut packed_by_wire = vec![u32::MAX; width];
149        for (wire, packed) in packed_by_wire.iter_mut().enumerate().take(source_size) {
150            *packed = wire as u32;
151        }
152        // The column of each wire in the layer being compiled, reset after each layer.
153        let mut column_by_wire = vec![u32::MAX; width];
154        let mut available = source_size;
155        let mut layers = Vec::new();
156        let mut slot_by_group: HashMap<usize, u32> = HashMap::new();
157        for operation_indices in operations_by_layer.iter().filter(|indices| !indices.is_empty()) {
158            let output_size = operation_indices.len();
159            let mut input_wires: Vec<Wire> = Vec::new();
160            for &operation_index in operation_indices {
161                // Gather rows read packed values by position, not through the layer's input.
162                if plans.contains_key(&operation_index) {
163                    continue;
164                }
165                let operation = &self.operations[operation_index];
166                for &(wire, _) in operation.left_terms.iter().chain(&operation.gate_terms) {
167                    if column_by_wire[wire.index()] == u32::MAX {
168                        column_by_wire[wire.index()] = 0;
169                        input_wires.push(wire);
170                    }
171                }
172            }
173            input_wires.sort_by_key(|wire| packed_by_wire[wire.index()]);
174            for (column, wire) in input_wires.iter().enumerate() {
175                column_by_wire[wire.index()] = column as u32;
176            }
177            let gate_count =
178                operation_indices.iter().filter(|&&index| self.operations[index].kind == OpKind::Reglu).count();
179            let index_count =
180                operation_indices.iter().filter(|&&index| self.operations[index].kind == OpKind::Index).count();
181            let projection_size = output_size + gate_count + index_count;
182            let mut bias = vec![0f32; projection_size];
183            let mut projection_terms: Vec<&[(Wire, f64)]> = vec![&[]; projection_size];
184            let mut gate_rows = Vec::new();
185            let mut boolean_rows = Vec::new();
186            let mut pending_gather_reads = Vec::new();
187            let mut pending_index_reads = Vec::new();
188            let (mut gate_row, mut index_row) = (0, 0);
189            for (row, &operation_index) in operation_indices.iter().enumerate() {
190                let operation = &self.operations[operation_index];
191                let output_wire = source_size + operation_index;
192                wire_order.push(Wire::from_index(output_wire));
193                packed_by_wire[output_wire] = (available + row) as u32;
194                bias[row] = operation.left_bias as f32;
195                if let Some(plan) = plans.get(&operation_index) {
196                    // The row keeps its bias and stores no weights; the executor adds the gathered value.
197                    pending_gather_reads.push((row, plan));
198                    continue;
199                }
200                projection_terms[row] = &operation.left_terms;
201                match operation.kind {
202                    OpKind::Reglu => {
203                        let projection_row = output_size + gate_row;
204                        bias[projection_row] = operation.gate_bias as f32;
205                        projection_terms[projection_row] = &operation.gate_terms;
206                        gate_rows.push(row as u32);
207                        gate_row += 1;
208                    }
209                    OpKind::Boolean => boolean_rows.push(row as u32),
210                    OpKind::Index => {
211                        let projection_row = output_size + gate_count + index_row;
212                        bias[projection_row] = operation.gate_bias as f32;
213                        projection_terms[projection_row] = &operation.gate_terms;
214                        pending_index_reads.push((row, projection_row, operation.index_read.as_ref().unwrap()));
215                        index_row += 1;
216                    }
217                    OpKind::Affine => {}
218                }
219            }
220            let mut gather_computes = Vec::new();
221            let mut gather_reads = Vec::new();
222            for (row, plan) in pending_gather_reads {
223                let group = &self.one_hot_groups[plan.group];
224                let packed = |wire: Wire| {
225                    let position = packed_by_wire[wire.index()];
226                    assert!((position as usize) < available, "a gather's bit or candidate is not available");
227                    position
228                };
229                let next_slot = slot_by_group.len() as u32;
230                let slot = *slot_by_group.entry(plan.group).or_insert_with(|| {
231                    // The group's first read resolves its index; its bits are at earlier depths.
232                    gather_computes.push(GatherCompute {
233                        slot: next_slot,
234                        bit_positions: group.members.iter().map(|&m| packed(m)).collect(),
235                    });
236                    next_slot
237                });
238                gather_reads.push(GatherRead {
239                    row: row as u32,
240                    slot,
241                    masked: group.allow_all_zero,
242                    candidate_positions: plan.candidates.iter().map(|&candidate| packed(candidate)).collect(),
243                });
244            }
245            let index_reads = pending_index_reads
246                .into_iter()
247                .map(|(row, index_row, read)| IndexReadRow {
248                    row: row as u32,
249                    index_row: index_row as u32,
250                    below_zero: read.below == OutOfRange::Zero,
251                    above_zero: read.above == OutOfRange::Zero,
252                    candidate_positions: self.tables[read.table]
253                        .candidates
254                        .iter()
255                        .map(|candidate| {
256                            let position = packed_by_wire[candidate.index()];
257                            assert!((position as usize) < available, "index read candidate is not available");
258                            position
259                        })
260                        .collect(),
261                })
262                .collect();
263            let mut row_offsets = Vec::with_capacity(projection_size + 1);
264            let mut columns = Vec::new();
265            let mut weights = Vec::new();
266            row_offsets.push(0);
267            for terms in &projection_terms {
268                for &(wire, coefficient) in terms.iter() {
269                    let column = column_by_wire[wire.index()];
270                    assert!(column != u32::MAX, "tensor dependency is not available");
271                    columns.push(column);
272                    weights.push(coefficient as f32);
273                }
274                row_offsets.push(columns.len() as u32);
275            }
276            for wire in &input_wires {
277                column_by_wire[wire.index()] = u32::MAX;
278            }
279            layers.push(TensorLayer {
280                input_indices: input_wires.iter().map(|wire| packed_by_wire[wire.index()]).collect(),
281                output_size,
282                bias,
283                row_offsets,
284                columns,
285                weights,
286                gate_rows,
287                boolean_rows,
288                gather_computes,
289                gather_reads,
290                index_reads,
291            });
292            available += output_size;
293        }
294        let ranges = wire_order.iter().map(|&wire| self.range(wire)).collect();
295        let grids = wire_order.iter().map(|&wire| self.grid(wire)).collect();
296        TensorProgram {
297            ranges,
298            grids,
299            layers,
300            roots,
301            source_size,
302            value_count,
303            wire_order,
304            width,
305            gather_slot_count: slot_by_group.len(),
306        }
307    }
308
309    /// Each live operation's depth from what it reads in the compiled program: a lowered sum reads its group's bits
310    /// and its candidates instead of its products. Without gather plans these are the build-time depths.
311    fn compute_compiled_depths(&self, live: &[bool], plans: &HashMap<usize, GatherPlan>) -> Vec<u32> {
312        let source_size = self.source_size();
313        let mut depths = vec![0u32; self.operations.len()];
314        let mut table_depths: HashMap<usize, u32> = HashMap::new();
315        for (operation_index, operation) in self.operations.iter().enumerate() {
316            if !live[operation_index] {
317                continue;
318            }
319            let depth_of = |wire: Wire| wire.index().checked_sub(source_size).map_or(0, |index| depths[index]);
320            let depth = match plans.get(&operation_index) {
321                Some(plan) => self.one_hot_groups[plan.group]
322                    .members
323                    .iter()
324                    .chain(&plan.candidates)
325                    .map(|&wire| depth_of(wire))
326                    .max()
327                    .unwrap_or(0),
328                None => {
329                    let mut depth = operation
330                        .left_terms
331                        .iter()
332                        .chain(&operation.gate_terms)
333                        .map(|&(wire, _)| depth_of(wire))
334                        .max()
335                        .unwrap_or(0);
336                    if let Some(read) = &operation.index_read {
337                        let table_depth = match table_depths.get(&read.table) {
338                            Some(&table_depth) => table_depth,
339                            None => {
340                                let table_depth = self.tables[read.table]
341                                    .candidates
342                                    .iter()
343                                    .map(|&candidate| depth_of(candidate))
344                                    .max()
345                                    .unwrap_or(0);
346                                table_depths.insert(read.table, table_depth);
347                                table_depth
348                            }
349                        };
350                        depth = depth.max(table_depth);
351                    }
352                    depth
353                }
354            };
355            depths[operation_index] = depth + 1;
356        }
357        depths
358    }
359
360    fn find_live_operations(&self, roots: &[Wire], plans: &HashMap<usize, GatherPlan>) -> Vec<bool> {
361        let source_size = self.source_size();
362        let mut live = vec![false; self.operations.len()];
363        let mut visited_tables = vec![false; self.tables.len()];
364        let mut pending = roots.to_vec();
365        while let Some(wire) = pending.pop() {
366            self.assert_wire(wire);
367            let Some(operation_index) = wire.index().checked_sub(source_size) else { continue };
368            if live[operation_index] {
369                continue;
370            }
371            live[operation_index] = true;
372            if let Some(plan) = plans.get(&operation_index) {
373                // A lowered sum reads the group's bits and every candidate, not its products.
374                pending.extend(&self.one_hot_groups[plan.group].members);
375                pending.extend(&plan.candidates);
376                continue;
377            }
378            let operation = &self.operations[operation_index];
379            pending.extend(operation.left_terms.iter().chain(&operation.gate_terms).map(|&(wire, _)| wire));
380            if let Some(read) = &operation.index_read
381                && !visited_tables[read.table]
382            {
383                visited_tables[read.table] = true;
384                pending.extend(&self.tables[read.table].candidates);
385            }
386        }
387        live
388    }
389
390    /// Every wire's value through the compiled program, with 0 for removed wires.
391    pub fn evaluate_tensor(&self, source: &[f32], program: &TensorProgram) -> Result<Vec<f32>, Error> {
392        self.validate_source_ranges(source)?;
393        let mut executor = TensorExecutor::new(program);
394        executor.run(source);
395        let mut values = vec![0f32; program.width];
396        for (packed, wire) in program.wire_order.iter().enumerate() {
397            values[wire.index()] = executor.packed_values[packed];
398        }
399        Ok(values)
400    }
401
402    /// Check that a compiled evaluation reproduces every live value of the
403    /// reference evaluator, compared with `==` like handwired's `===`.
404    pub fn validate_tensor_evaluation(
405        &self,
406        source: &[f32],
407        program: &TensorProgram,
408        tensor_values: &[f32],
409    ) -> Result<(), String> {
410        let expected = self.evaluate_unchecked(source);
411        for wire in &program.wire_order {
412            let (got, want) = (tensor_values[wire.index()], expected[wire.index()]);
413            if got != want {
414                return Err(format!("tensor wire {} is {got}, the reference evaluator gives {want}", wire.index()));
415            }
416        }
417        Ok(())
418    }
419}
420
421/// Reusable buffers for running one program many times.
422pub struct TensorExecutor<'program> {
423    program: &'program TensorProgram,
424    /// After a run, `packed_values[k]` is the value of `program.wire_order[k]`.
425    pub packed_values: Vec<f32>,
426    input: Vec<f32>,
427    projection: Vec<f32>,
428    gather_indices: Vec<i32>,
429    gather_masses: Vec<f32>,
430    root_positions: Vec<usize>,
431    outputs: Vec<f32>,
432}
433
434impl<'program> TensorExecutor<'program> {
435    pub fn new(program: &'program TensorProgram) -> TensorExecutor<'program> {
436        let max_input = program.layers.iter().map(|layer| layer.input_indices.len()).max().unwrap_or(0);
437        let max_projection = program.layers.iter().map(|layer| layer.bias.len()).max().unwrap_or(0);
438        let root_positions = program
439            .roots
440            .iter()
441            .map(|root| program.wire_order.iter().position(|wire| wire == root).expect("root was removed"))
442            .collect();
443        TensorExecutor {
444            program,
445            packed_values: vec![0.0; program.value_count],
446            input: vec![0.0; max_input],
447            projection: vec![0.0; max_projection],
448            gather_indices: vec![0; program.gather_slot_count],
449            gather_masses: vec![0.0; program.gather_slot_count],
450            root_positions,
451            outputs: vec![0.0; program.roots.len()],
452        }
453    }
454
455    /// Evaluate the program and return the roots' values. The source is not
456    /// range-checked; use [`Circuit::validate_source_ranges`] for that.
457    pub fn run(&mut self, source: &[f32]) -> &[f32] {
458        let program = self.program;
459        assert_eq!(source.len(), program.source_size, "source table has the wrong length");
460        let values = &mut self.packed_values;
461        values[..source.len()].copy_from_slice(source);
462        let mut available = source.len();
463        for layer in &program.layers {
464            for (column, &packed) in layer.input_indices.iter().enumerate() {
465                self.input[column] = values[packed as usize];
466            }
467            let projection = &mut self.projection[..layer.bias.len()];
468            for (row, output) in projection.iter_mut().enumerate() {
469                let mut value = layer.bias[row];
470                for entry in layer.row_offsets[row] as usize..layer.row_offsets[row + 1] as usize {
471                    value += layer.weights[entry] * self.input[layer.columns[entry] as usize];
472                }
473                *output = value;
474            }
475            for compute in &layer.gather_computes {
476                let (index, mass) = resolve_gather(compute.bit_positions.iter().map(|&bit| values[bit as usize]));
477                self.gather_indices[compute.slot as usize] = index;
478                self.gather_masses[compute.slot as usize] = mass;
479            }
480            for read in &layer.gather_reads {
481                let slot = read.slot as usize;
482                let contribution = gathered(values, read, self.gather_indices[slot], self.gather_masses[slot]);
483                projection[read.row as usize] += contribution;
484            }
485            for read in &layer.index_reads {
486                let index = projection[read.index_row as usize] as f64;
487                let below = if read.below_zero { OutOfRange::Zero } else { OutOfRange::Clamp };
488                let above = if read.above_zero { OutOfRange::Zero } else { OutOfRange::Clamp };
489                projection[read.row as usize] =
490                    match resolve_index_position(index, read.candidate_positions.len(), below, above) {
491                        -2 => f32::NAN,
492                        -1 => 0.0,
493                        position => values[read.candidate_positions[position as usize] as usize],
494                    };
495            }
496            for (gate, &row) in layer.gate_rows.iter().enumerate() {
497                let gate_value = relu32(projection[layer.output_size + gate]);
498                projection[row as usize] *= gate_value;
499            }
500            for &row in &layer.boolean_rows {
501                projection[row as usize] = evaluate_boolean(projection[row as usize]);
502            }
503            values[available..available + layer.output_size].copy_from_slice(&projection[..layer.output_size]);
504            available += layer.output_size;
505        }
506        for (output, &position) in self.outputs.iter_mut().zip(&self.root_positions) {
507            *output = values[position];
508        }
509        &self.outputs
510    }
511}
512
513/// A group's selected position and its sum of bits. For exact 0/1 bits both are exact small integers.
514pub(crate) fn resolve_gather(bits: impl Iterator<Item = f32>) -> (i32, f32) {
515    let (mut index, mut mass) = (0f64, 0f64);
516    for (position, bit) in bits.enumerate() {
517        index += position as f64 * bit as f64;
518        mass += bit as f64;
519    }
520    (index as i32, mass as f32)
521}
522
523/// The selected candidate, times the group's mass for a group that allows all zeros. The clamp keeps a violated
524/// one-hot declaration inside the table.
525pub(crate) fn gathered(values: &[f32], read: &GatherRead, index: i32, mass: f32) -> f32 {
526    let index = index.clamp(0, read.candidate_positions.len() as i32 - 1) as usize;
527    let candidate = values[read.candidate_positions[index] as usize];
528    if read.masked { mass * candidate } else { candidate }
529}