Skip to content

Commit f112d81

Browse files
committed
feat: saturation for add,sub,sum
1 parent 8fcd92c commit f112d81

47 files changed

Lines changed: 1970 additions & 274 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

atlas-onnx-tracer/src/model/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ impl Model {
261261
.nodes
262262
.values()
263263
.map(|node| match &node.operator {
264+
// Saturating Add/Sub commit a one-hot clamp read-address
265+
// decomposition. Its chunks are `k_chunk`-wide just like the
266+
// activation lookups, so the largest committed poly is
267+
// `k_chunk * T` (the 64-bit address only changes the chunk
268+
// *count*, not the per-chunk size).
264269
Operator::Tanh(_)
265270
| Operator::Cos(_)
266271
| Operator::Div(_)
@@ -269,6 +274,9 @@ impl Model {
269274
| Operator::Rsqrt(_)
270275
| Operator::Sigmoid(_)
271276
| Operator::Sin(_)
277+
| Operator::Add(_)
278+
| Operator::Sub(_)
279+
| Operator::Sum(_)
272280
| Operator::SoftmaxLastAxis(_) => {
273281
LOG_K_CHUNK + log_2(node.pow2_padded_num_output_elements())
274282
}

atlas-onnx-tracer/src/ops/add.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use super::{Add, Op};
2-
use crate::tensor::{self, Tensor};
2+
use crate::tensor::Tensor;
33

44
impl Op for Add {
55
#[tracing::instrument(name = "Add::f", skip_all)]
66
fn f(&self, inputs: Vec<&Tensor<i32>>) -> Tensor<i32> {
7-
tensor::ops::add(&inputs).unwrap()
7+
super::sat_binop(inputs, "Add", |a, b| a + b)
88
}
99

1010
fn requires_shape_equality(&self) -> bool {

atlas-onnx-tracer/src/ops/mod.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,90 @@ pub trait Op {
180180
}
181181
}
182182

183+
/// Broadcast-expand `lhs` and `rhs`, apply `combine` element-wise in `i64`,
184+
/// and return the unclamped intermediate as `Tensor<i64>`.
185+
///
186+
/// This is the re-executable kernel the proof system calls to recover the
187+
/// pre-saturation intermediate without storing it in the trace.
188+
pub(super) fn sat_accumulate_pair(
189+
lhs: &crate::tensor::Tensor<i32>,
190+
rhs: &crate::tensor::Tensor<i32>,
191+
op_name: &str,
192+
combine: impl Fn(i64, i64) -> i64 + Sync,
193+
) -> crate::tensor::Tensor<i64> {
194+
use crate::tensor::get_broadcasted_shape;
195+
use common::parallel::par_enabled;
196+
use rayon::prelude::*;
197+
198+
let shape = get_broadcasted_shape(lhs.dims(), rhs.dims())
199+
.unwrap_or_else(|_| panic!("{op_name}: incompatible broadcast shapes"));
200+
let lhs_exp = lhs
201+
.expand(&shape)
202+
.unwrap_or_else(|_| panic!("{op_name}: expand lhs"));
203+
let rhs_exp = rhs
204+
.expand(&shape)
205+
.unwrap_or_else(|_| panic!("{op_name}: expand rhs"));
206+
let data: Vec<i64> = lhs_exp
207+
.data()
208+
.par_iter()
209+
.zip(rhs_exp.data().par_iter())
210+
.with_min_len(par_enabled())
211+
.map(|(&a, &b)| combine(a as i64, b as i64))
212+
.collect();
213+
crate::tensor::Tensor::new(Some(&data), &shape)
214+
.unwrap_or_else(|_| panic!("{op_name}: Tensor::new"))
215+
}
216+
217+
/// Clamp each element of a `Tensor<i64>` into `[i32::MIN, i32::MAX]`.
218+
pub(super) fn clamp_to_i32(t: &crate::tensor::Tensor<i64>) -> crate::tensor::Tensor<i32> {
219+
use common::parallel::par_enabled;
220+
use rayon::prelude::*;
221+
222+
let data: Vec<i32> = t
223+
.data()
224+
.par_iter()
225+
.with_min_len(par_enabled())
226+
.map(|&v| v.clamp(i32::MIN as i64, i32::MAX as i64) as i32)
227+
.collect();
228+
crate::tensor::Tensor::new(Some(&data), t.dims())
229+
.unwrap_or_else(|_| panic!("clamp_to_i32: Tensor::new"))
230+
}
231+
232+
/// Saturating element-wise binary operation for [`Add`] and [`Sub`].
233+
///
234+
/// For each consecutive input pair: accumulates via [`sat_accumulate_pair`]
235+
/// (in `i64`), then clamps to `i32` via [`clamp_to_i32`]. Per-step clamping
236+
/// preserves correct saturation semantics for variadic inputs.
237+
pub(super) fn sat_binop(
238+
inputs: Vec<&crate::tensor::Tensor<i32>>,
239+
op_name: &str,
240+
combine: impl Fn(i64, i64) -> i64 + Sync + Copy,
241+
) -> crate::tensor::Tensor<i32> {
242+
let mut output = inputs[0].clone();
243+
for &rhs in &inputs[1..] {
244+
output = clamp_to_i32(&sat_accumulate_pair(&output, rhs, op_name, combine));
245+
}
246+
output
247+
}
248+
249+
/// Re-execute a binary [`Add`]/[`Sub`] node's accumulation, returning the
250+
/// pre-clamp `i64` intermediate.
251+
///
252+
/// The proof system uses this to recover the accumulation (lookup-index)
253+
/// polynomial for the saturating-clamp lookup without storing it in the trace.
254+
/// Panics on non-`Add`/`Sub` operators or a non-binary operand list.
255+
pub fn sat_binop_intermediate(
256+
operator: &Operator,
257+
lhs: &crate::tensor::Tensor<i32>,
258+
rhs: &crate::tensor::Tensor<i32>,
259+
) -> crate::tensor::Tensor<i64> {
260+
match operator {
261+
Operator::Add(_) => sat_accumulate_pair(lhs, rhs, "Add", |a, b| a + b),
262+
Operator::Sub(_) => sat_accumulate_pair(lhs, rhs, "Sub", |a, b| a - b),
263+
_ => panic!("sat_binop_intermediate: expected Add or Sub, got {operator:?}"),
264+
}
265+
}
266+
183267
/// Shared evaluation for the periodic trig operators ([`Sin`], [`Cos`]).
184268
///
185269
/// Both reduce the input modulo a `4π` approximation before applying their

atlas-onnx-tracer/src/ops/sub.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use crate::{
22
ops::{Op, Sub},
3-
tensor::{self, Tensor},
3+
tensor::Tensor,
44
};
55

66
impl Op for Sub {
77
#[tracing::instrument(name = "Sub::f", skip_all)]
88
fn f(&self, inputs: Vec<&Tensor<i32>>) -> Tensor<i32> {
9-
tensor::ops::sub(&inputs).unwrap()
9+
super::sat_binop(inputs, "Sub", |a, b| a - b)
1010
}
1111

1212
fn requires_shape_equality(&self) -> bool {

atlas-onnx-tracer/src/ops/sum.rs

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use rayon::prelude::*;
12
use tract_onnx::prelude::tract_itertools::Itertools;
23

34
use crate::{
@@ -8,22 +9,21 @@ use crate::{
89
impl Op for Sum {
910
#[tracing::instrument(name = "Sum::f", skip_all)]
1011
fn f(&self, inputs: Vec<&Tensor<i32>>) -> Tensor<i32> {
11-
#[cfg(feature = "fused-ops")]
12-
{
13-
sum_axes_i32(inputs[0], &self.axes).unwrap()
14-
}
15-
#[cfg(not(feature = "fused-ops"))]
16-
{
17-
crate::tensor::ops::sum_axes(inputs[0], &self.axes).unwrap()
18-
}
12+
sum_axes_i32(inputs[0], &self.axes).unwrap()
1913
}
2014
}
2115

22-
/// Sum over specified axes of an i32 tensor, accumulating in i64 to prevent overflow,
23-
#[tracing::instrument(name = "tensor::ops::sum_axes_i32", skip_all)]
24-
pub fn sum_axes_i32(a: &Tensor<i32>, axes: &[usize]) -> Result<Tensor<i32>, TensorError> {
16+
/// Sum over specified axes, accumulating in `i64` to avoid overflow (the
17+
/// pre-clamp intermediate). Each output element is the exact integer sum over
18+
/// the reduced axes; summed axes collapse to length 1.
19+
///
20+
/// This is the polynomial the saturating-clamp proof reasons about: the proof
21+
/// system recovers it to bridge `output = SatClamp(acc)`.
22+
#[tracing::instrument(name = "tensor::ops::sum_axes_i64", skip_all)]
23+
pub fn sum_axes_i64(a: &Tensor<i32>, axes: &[usize]) -> Result<Tensor<i64>, TensorError> {
2524
if axes.is_empty() {
26-
return Ok(a.clone());
25+
let data: Vec<i64> = a.data().par_iter().map(|&v| v as i64).collect();
26+
return Tensor::new(Some(&data), a.dims());
2727
}
2828

2929
let mut new_dims = vec![];
@@ -35,15 +35,15 @@ pub fn sum_axes_i32(a: &Tensor<i32>, axes: &[usize]) -> Result<Tensor<i32>, Tens
3535
}
3636
}
3737

38-
let res = Tensor::new(None, &new_dims)?;
38+
let res = Tensor::<i64>::new(None, &new_dims)?;
3939

4040
let cartesian_coord = new_dims
4141
.iter()
4242
.map(|x| 0..*x)
4343
.multi_cartesian_product()
4444
.collect::<Vec<_>>();
4545

46-
let res = res.par_enum_map(|i, _: i32| {
46+
res.par_enum_map(|i, _: i64| {
4747
let coord = cartesian_coord[i].clone();
4848
let mut prod_dims = vec![];
4949
for (j, c) in coord.iter().enumerate() {
@@ -55,13 +55,22 @@ pub fn sum_axes_i32(a: &Tensor<i32>, axes: &[usize]) -> Result<Tensor<i32>, Tens
5555
}
5656

5757
let slice = a.get_slice(&prod_dims)?;
58-
// Accumulate in i64
5958
let mut acc: i64 = 0;
6059
let _ = slice.map(|v| acc += v as i64);
60+
Ok(acc)
61+
})
62+
}
6163

62-
// Saturate instead of wrapping — prevents sign-flip corruption
63-
Ok(acc.clamp(i32::MIN as i64, i32::MAX as i64) as i32)
64-
})?;
65-
66-
Ok(res)
64+
/// Saturating sum over specified axes: [`sum_axes_i64`] clamped back to `i32`.
65+
///
66+
/// Saturating (rather than wrapping) prevents sign-flip corruption on overflow.
67+
#[tracing::instrument(name = "tensor::ops::sum_axes_i32", skip_all)]
68+
pub fn sum_axes_i32(a: &Tensor<i32>, axes: &[usize]) -> Result<Tensor<i32>, TensorError> {
69+
let acc = sum_axes_i64(a, axes)?;
70+
let data: Vec<i32> = acc
71+
.data()
72+
.par_iter()
73+
.map(|&v| v.clamp(i32::MIN as i64, i32::MAX as i64) as i32)
74+
.collect();
75+
Tensor::new(Some(&data), acc.dims())
6776
}

common/src/lib.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ canonical_serde_enum! {
160160
/// * `0` – node index
161161
/// * `1` – decomposition index `d`
162162
SoftmaxSatDiffRaD(usize, usize),
163+
164+
/// One-hot read-address decomposition for the saturating-clamp lookup of
165+
/// a `Add`/`Sub` node. The clamp is indexed by the 64-bit accumulation, so
166+
/// there are `64 / log_k_chunk` chunks (e.g. 16 for `log_k_chunk = 4`).
167+
///
168+
/// * `0` – node index
169+
/// * `1` – decomposition index `d`
170+
ClampRaD(usize, usize),
163171
}
164172
}
165173

@@ -367,5 +375,23 @@ canonical_serde_enum! {
367375
///
368376
/// * `0` – producer node index
369377
NTEvalShiftOutput(usize),
378+
379+
// ----- Saturating clamp (Add/Sub) -----
380+
/// MLE of the pre-clamp **i64 accumulation** of a saturating `Add`/`Sub`.
381+
///
382+
/// This is the `raf` (read-address-field) polynomial of the saturating-clamp
383+
/// lookup: its values are the indices into [`SatClampTable`](joltworks). It is
384+
/// tied to the operands by the linear identity `acc(r) = left(r) ± right(r)`,
385+
/// and the lookup proves `output(r) = clamp(acc(r))`.
386+
///
387+
/// * `0` – node index
388+
ClampAcc(usize),
389+
390+
/// One-hot read-address polynomial for the saturating-clamp lookup
391+
/// (`Add`/`Sub`). Virtualized — [`CommittedPoly::ClampRaD`] commits its
392+
/// decomposition.
393+
///
394+
/// * `0` – node index
395+
ClampRa(usize),
370396
}
371397
}

0 commit comments

Comments
 (0)