diff --git a/substrate/src/logic/delay.rs b/substrate/src/logic/delay.rs index a9b8641..60bd8fa 100644 --- a/substrate/src/logic/delay.rs +++ b/substrate/src/logic/delay.rs @@ -166,44 +166,60 @@ impl LogicPath { assert!(opts.lr > 0.0); assert!(opts.max_iter > 0); + let path = DensePath::new(self); + let mut values: Vec = self.variables.values().map(|s| s.value).collect(); + let n = values.len(); + let mut grad = vec![0.0; n]; + let mut partials = Vec::new(); + let mut lr = opts.lr; // Backtracking: a step that increased the delay is undone and retried at // half the size. A fixed step overshoots on the `res / size` terms near // small sizes. `delay_grad` already returns the delay, so the check is free. - let n = self.variables.len(); let mut base = vec![0.0; n]; let mut base_grad = vec![0.0; n]; let mut base_delay = f64::INFINITY; let mut step = lr; let mut iter = 0; while iter < opts.max_iter { - let mut grad = self.zero_grad(); - let delay = self.delay_grad(&mut grad); + grad.fill(0.0); + let delay = path.delay_grad(&values, &mut grad, &mut partials); // The tolerance ignores rounding noise once converged; without it, about // half of all steps near the optimum are rejected for nothing. - if delay > base_delay * (1.0 + 1e-9) { + let accepted = if delay > base_delay * (1.0 + 1e-9) { step *= 0.5; if step == 0.0 { self.set_values(&base); return; } + false } else { base_delay = delay; - for (i, (v, s)) in self.variables.iter().enumerate() { - base[i] = s.value; - base_grad[i] = grad[v]; - } + base.copy_from_slice(&values); + base_grad.copy_from_slice(&grad); step = lr; lr *= opts.lr_decay; iter += 1; - } - for (i, s) in self.variables.values_mut().enumerate() { + true + }; + let mut moved = false; + for ((value, &b), &g) in values.iter_mut().zip(&base).zip(&base_grad) { // Project back onto the feasible region. Without this, a variable // pinned at `min_var_value` keeps drifting below it (`value()` hides // this) and cannot recover if its optimum later moves above the bound. - s.value = f64::max(base[i] - step * base_grad[i], self.min_var_value); + let next = f64::max(b - step * g, self.min_var_value); + moved |= next.to_bits() != value.to_bits(); + *value = next; + } + // Every later step starts from these same values, so it has the same + // gradient and is accepted, and its step size is no larger: rounding + // returns each value to the same bits again. The remaining iterations + // would change nothing. + if accepted && !moved { + break; } } + self.set_values(&values); // The last step has not been checked yet. if self.delay() > base_delay * (1.0 + 1e-9) { self.set_values(&base); @@ -224,14 +240,6 @@ impl LogicPath { tau } - pub(crate) fn delay_grad(&self, grad: &mut Gradient) -> f64 { - let mut tau = 0.0; - for idx in 0..self.segments.len() { - tau += self.segment_delay_grad(idx, grad); - } - tau - } - fn segment_delay(&self, idx: usize) -> f64 { let seg = &self.segments[idx]; @@ -254,38 +262,6 @@ impl LogicPath { f64::max(self.variables[var].value, self.min_var_value) } - fn segment_delay_grad(&self, idx: usize, grad: &mut Gradient) -> f64 { - let seg = &self.segments[idx]; - - // Gradient of resistance and capacitance, respectively. - let mut drdv = self.zero_grad(); - let mut dcdv = self.zero_grad(); - - let (r, mut c) = match &seg.element { - Element::Resistor(r) => (*r, 0.0), - Element::SizedGate(gate) => (gate.res, gate.cout), - Element::UnsizedGate(gate, v) => { - let v = *v; - drdv[v] = -gate.res / (self.value(v) * self.value(v)); - dcdv[v] = dcdv.get(v) + gate.cout; - (gate.res / self.value(v), gate.cout * self.value(v)) - } - }; - c += seg.fixed_cap; - for (v, mult) in seg.variable_cap.iter() { - c += self.value(v) * mult; - dcdv[v] += mult; - } - c += self.elmore_input_capacitance_grad(idx + 1, &mut dcdv); - - for v in self.variables.keys() { - // Apply the product rule. - grad[v] += r * dcdv[v] + c * drdv[v]; - } - - r * c - } - fn total_output_cap(&self, segment: &Segment) -> f64 { let c = match &segment.element { Element::Resistor(_) => 0.0, @@ -296,19 +272,6 @@ impl LogicPath { c + segment.fixed_cap } - fn total_output_cap_grad(&self, segment: &Segment, grad: &mut Gradient) -> f64 { - let c = match &segment.element { - Element::Resistor(_) => 0.0, - Element::SizedGate(gate) => gate.cout, - &Element::UnsizedGate(gate, v) => { - grad[v] = grad.get(v) + gate.cout; - gate.cout * self.value(v) - } - }; - - c + segment.fixed_cap - } - fn elmore_input_capacitance(&self, mut idx: usize) -> f64 { let mut c = 0.0; loop { @@ -334,25 +297,167 @@ impl LogicPath { c } +} + +/// A [`LogicPath`] with its variables numbered in creation order, so the optimizer +/// can work on flat vectors. +/// +/// [`DensePath::delay_grad`] performs the same floating-point operations in the same +/// order as the test-only `LogicPath::delay_grad`, so it returns bit-identical results. +/// It differs only in skipping the zero partials of variables a segment does not touch. +struct DensePath { + segments: Vec, + min_var_value: f64, +} + +struct DenseSegment { + element: DenseElement, + fixed_cap: f64, + /// `(variable, multiplier)` pairs, in the iteration order of + /// [`Segment::variable_cap`], which is the order their capacitances are summed in. + variable_cap: Vec<(usize, f64)>, +} + +enum DenseElement { + SizedGate(GateModel), + UnsizedGate(GateModel, usize), + Resistor(f64), +} + +impl DensePath { + fn new(path: &LogicPath) -> Self { + let mut index = SecondaryMap::with_capacity(path.variables.len()); + for (i, v) in path.variables.keys().enumerate() { + index.insert(v, i); + } + let segments = path + .segments + .iter() + .map(|seg| DenseSegment { + element: match &seg.element { + Element::SizedGate(gate) => DenseElement::SizedGate(*gate), + &Element::UnsizedGate(gate, v) => DenseElement::UnsizedGate(gate, index[v]), + Element::Resistor(r) => DenseElement::Resistor(*r), + }, + fixed_cap: seg.fixed_cap, + variable_cap: seg + .variable_cap + .iter() + .map(|(v, &mult)| (index[v], mult)) + .collect(), + }) + .collect(); + Self { + segments, + min_var_value: path.min_var_value, + } + } + + #[inline] + fn value(&self, values: &[f64], var: usize) -> f64 { + f64::max(values[var], self.min_var_value) + } + + /// Returns the delay and adds its gradient into `grad`. + /// + /// `partials` is scratch space, reused across calls to avoid allocating. + fn delay_grad( + &self, + values: &[f64], + grad: &mut [f64], + partials: &mut Vec<(usize, f64)>, + ) -> f64 { + let mut tau = 0.0; + for idx in 0..self.segments.len() { + tau += self.segment_delay_grad(idx, values, grad, partials); + } + tau + } + + fn segment_delay_grad( + &self, + idx: usize, + values: &[f64], + grad: &mut [f64], + dcdv: &mut Vec<(usize, f64)>, + ) -> f64 { + let seg = &self.segments[idx]; + + // The capacitance's nonzero partials, as `(variable, partial)`. Only the + // element's own variable has a nonzero partial of the resistance. + dcdv.clear(); + let mut drdv = None; + + let (r, mut c) = match seg.element { + DenseElement::Resistor(r) => (r, 0.0), + DenseElement::SizedGate(gate) => (gate.res, gate.cout), + DenseElement::UnsizedGate(gate, v) => { + drdv = Some(( + v, + -gate.res / (self.value(values, v) * self.value(values, v)), + )); + *partial(dcdv, v) += gate.cout; + ( + gate.res / self.value(values, v), + gate.cout * self.value(values, v), + ) + } + }; + c += seg.fixed_cap; + for &(v, mult) in &seg.variable_cap { + c += self.value(values, v) * mult; + *partial(dcdv, v) += mult; + } + c += self.elmore_input_capacitance_grad(idx + 1, values, dcdv); + + // Apply the product rule. + for &(v, dc) in dcdv.iter() { + let dr = match drdv { + Some((u, dr)) if u == v => dr, + _ => 0.0, + }; + grad[v] += r * dc + c * dr; + } + // Every other variable gains `r * 0.0 + c * 0.0`. Gradient entries start at + // `+0.0` and only have values added to them, so they are never `-0.0`, and + // adding a zero leaves them unchanged. Only a NaN, from an infinite `r` or + // `c`, changes them. + let zero = r * 0.0 + c * 0.0; + if zero.is_nan() { + for (v, g) in grad.iter_mut().enumerate() { + if !dcdv.iter().any(|&(u, _)| u == v) { + *g += zero; + } + } + } - fn elmore_input_capacitance_grad(&self, mut idx: usize, grad: &mut Gradient) -> f64 { + r * c + } + + fn elmore_input_capacitance_grad( + &self, + mut idx: usize, + values: &[f64], + dcdv: &mut Vec<(usize, f64)>, + ) -> f64 { let mut c = 0.0; loop { if idx >= self.segments.len() { return c; } let seg = &self.segments[idx]; - match &seg.element { - Element::Resistor(_) => { - c += self.total_output_cap_grad(seg, grad); + match seg.element { + // As in `LogicPath::total_output_cap_grad`. + DenseElement::Resistor(_) => { + c += 0.0 + seg.fixed_cap; } - Element::SizedGate(gate) => { + DenseElement::SizedGate(gate) => { c += gate.cin; break; } - &Element::UnsizedGate(gate, v) => { - c += gate.cin * self.value(v); - grad[v] += gate.cin; + DenseElement::UnsizedGate(gate, v) => { + c += gate.cin * self.value(values, v); + *partial(dcdv, v) += gate.cin; break; } } @@ -361,14 +466,18 @@ impl LogicPath { c } +} - pub(crate) fn zero_grad(&self) -> Gradient { - let mut grad = Gradient(SecondaryMap::with_capacity(self.variables.len())); - for v in self.variables.keys() { - grad.0.insert(v, 0f64); +/// Returns the partial for `var`, adding it as `0.0` if absent. +fn partial(partials: &mut Vec<(usize, f64)>, var: usize) -> &mut f64 { + let i = match partials.iter().position(|&(v, _)| v == var) { + Some(i) => i, + None => { + partials.push((var, 0.0)); + partials.len() - 1 } - grad - } + }; + &mut partials[i].1 } impl Segment { @@ -426,3 +535,142 @@ impl std::ops::IndexMut for Gradient { self.0.index_mut(index) } } + +#[cfg(test)] +mod tests { + use super::*; + + // The optimizer and gradient as they were before `DensePath`, kept so tests can + // check that `size_with_opts` still returns bit-identical results. + impl LogicPath { + /// The raw variable values, in creation order. + pub(crate) fn values(&self) -> Vec { + self.variables.values().map(|s| s.value).collect() + } + + /// [`LogicPath::size_with_opts`] without [`DensePath`] or the early exit. + pub(crate) fn size_with_opts_reference(&mut self, opts: OptimizerOpts) { + let mut lr = opts.lr; + let n = self.variables.len(); + let mut base = vec![0.0; n]; + let mut base_grad = vec![0.0; n]; + let mut base_delay = f64::INFINITY; + let mut step = lr; + let mut iter = 0; + while iter < opts.max_iter { + let mut grad = self.zero_grad(); + let delay = self.delay_grad(&mut grad); + if delay > base_delay * (1.0 + 1e-9) { + step *= 0.5; + if step == 0.0 { + self.set_values(&base); + return; + } + } else { + base_delay = delay; + for (i, (v, s)) in self.variables.iter().enumerate() { + base[i] = s.value; + base_grad[i] = grad[v]; + } + step = lr; + lr *= opts.lr_decay; + iter += 1; + } + for (i, s) in self.variables.values_mut().enumerate() { + s.value = f64::max(base[i] - step * base_grad[i], self.min_var_value); + } + } + if self.delay() > base_delay * (1.0 + 1e-9) { + self.set_values(&base); + } + } + + pub(crate) fn delay_grad(&self, grad: &mut Gradient) -> f64 { + let mut tau = 0.0; + for idx in 0..self.segments.len() { + tau += self.segment_delay_grad(idx, grad); + } + tau + } + + fn segment_delay_grad(&self, idx: usize, grad: &mut Gradient) -> f64 { + let seg = &self.segments[idx]; + + // Gradient of resistance and capacitance, respectively. + let mut drdv = self.zero_grad(); + let mut dcdv = self.zero_grad(); + + let (r, mut c) = match &seg.element { + Element::Resistor(r) => (*r, 0.0), + Element::SizedGate(gate) => (gate.res, gate.cout), + Element::UnsizedGate(gate, v) => { + let v = *v; + drdv[v] = -gate.res / (self.value(v) * self.value(v)); + dcdv[v] = dcdv.get(v) + gate.cout; + (gate.res / self.value(v), gate.cout * self.value(v)) + } + }; + c += seg.fixed_cap; + for (v, mult) in seg.variable_cap.iter() { + c += self.value(v) * mult; + dcdv[v] += mult; + } + c += self.elmore_input_capacitance_grad(idx + 1, &mut dcdv); + + for v in self.variables.keys() { + // Apply the product rule. + grad[v] += r * dcdv[v] + c * drdv[v]; + } + + r * c + } + + fn total_output_cap_grad(&self, segment: &Segment, grad: &mut Gradient) -> f64 { + let c = match &segment.element { + Element::Resistor(_) => 0.0, + Element::SizedGate(gate) => gate.cout, + &Element::UnsizedGate(gate, v) => { + grad[v] = grad.get(v) + gate.cout; + gate.cout * self.value(v) + } + }; + + c + segment.fixed_cap + } + + fn elmore_input_capacitance_grad(&self, mut idx: usize, grad: &mut Gradient) -> f64 { + let mut c = 0.0; + loop { + if idx >= self.segments.len() { + return c; + } + let seg = &self.segments[idx]; + match &seg.element { + Element::Resistor(_) => { + c += self.total_output_cap_grad(seg, grad); + } + Element::SizedGate(gate) => { + c += gate.cin; + break; + } + &Element::UnsizedGate(gate, v) => { + c += gate.cin * self.value(v); + grad[v] += gate.cin; + break; + } + } + idx += 1; + } + + c + } + + pub(crate) fn zero_grad(&self) -> Gradient { + let mut grad = Gradient(SecondaryMap::with_capacity(self.variables.len())); + for v in self.variables.keys() { + grad.0.insert(v, 0f64); + } + grad + } + } +} diff --git a/substrate/src/logic/tests.rs b/substrate/src/logic/tests.rs index 73ae58d..69cb115 100644 --- a/substrate/src/logic/tests.rs +++ b/substrate/src/logic/tests.rs @@ -195,3 +195,105 @@ fn test_inv_nand3_nand2_interior() { path.value(b) ); } + +/// A xorshift generator, so the randomized test needs no extra dependencies. +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + + fn below(&mut self, n: usize) -> usize { + (self.next() % n as u64) as usize + } + + fn uniform(&mut self, lo: f64, hi: f64) -> f64 { + lo + (hi - lo) * (self.next() >> 11) as f64 / (1u64 << 53) as f64 + } + + fn gate(&mut self) -> GateModel { + GateModel { + res: self.uniform(0.5, 5.0), + cin: self.uniform(0.5, 5.0), + cout: self.uniform(0.0, 5.0), + } + } +} + +/// A random path using every kind of element and capacitance `LogicPath` supports. +fn random_path(rng: &mut Rng) -> LogicPath { + let mut path = LogicPath::new(); + // A zero bound lets sizes reach zero, making resistances infinite. + let min_var_value = [1.0, 0.5, 0.1, 0.0][rng.below(4)]; + path.set_min_var_value(min_var_value); + path.append_sized_gate(rng.gate()); + let mut vars = Vec::new(); + for _ in 0..1 + rng.below(8) { + match rng.below(6) { + 0 => path.append_resistor(rng.uniform(0.0, 2.0)), + 1 => path.append_wire(WireModel { + res: rng.uniform(0.0, 2.0), + cap: rng.uniform(0.0, 4.0), + }), + 2 => path.append_sized_gate(rng.gate()), + _ => { + let var = if !vars.is_empty() && rng.below(4) == 0 { + vars[rng.below(vars.len())] + } else { + let initial = if min_var_value == 0.0 && rng.below(4) == 0 { + 0.0 + } else { + rng.uniform(0.5, 4.0) + }; + let var = path.create_variable_with_initial(initial); + vars.push(var); + var + }; + // Branching loads, possibly several per segment and on other sizes. + for _ in 0..rng.below(4) { + let load = if rng.below(2) == 0 { + var + } else { + vars[rng.below(vars.len())] + }; + path.append_variable_capacitor(rng.uniform(0.0, 20.0), load); + } + path.append_unsized_gate(rng.gate(), var); + } + } + if rng.below(2) == 0 { + path.append_capacitor(rng.uniform(0.0, 10.0)); + } + } + path.append_capacitor(rng.uniform(1.0, 200.0)); + path +} + +#[test] +fn size_with_opts_matches_reference_bit_for_bit() { + let mut rng = Rng(0x9e37_79b9_7f4a_7c15); + for case in 0..300 { + let path = random_path(&mut rng); + let opts = OptimizerOpts { + lr: 10f64.powf(rng.uniform(-3.0, 1.0)), + // Fast decays converge within `max_iter`, exercising the early exit. + lr_decay: [1.0, 0.9999, 0.999, 0.99][rng.below(4)], + max_iter: 1 + rng.below(3_000), + }; + let mut sized = path.clone(); + sized.size_with_opts(opts); + let mut reference = path; + reference.size_with_opts_reference(opts); + let bits = |path: &LogicPath| { + path.values() + .into_iter() + .map(f64::to_bits) + .collect::>() + }; + assert_eq!(bits(&sized), bits(&reference), "case {case}: {opts:?}"); + } +}