Skip to main content

logicaffeine_proof/
cycle_ratio.rs

1//! The maximum cycle ratio of a weighted digraph, exact and certified: the bound a loop puts on a clock period.
2//!
3//! In a circuit's retiming graph (Leiserson & Saxe, *Retiming synchronous circuitry*, 1991) an edge carries the
4//! combinational delay of its source and the registers on it. A cycle of total delay `D` through `R` registers
5//! cannot run faster than `D / R` a clock, however the registers are moved, because retiming keeps every cycle's
6//! register count; so the largest `D / R` over every cycle is the period floor no retiming or pipelining of the
7//! loop's own logic can beat, and the cycle reaching it names the loop to re-architect. A cycle of no register is a
8//! combinational loop and has no period at all.
9//!
10//! [`max_cycle_ratio`] splits the graph into strongly connected components, since every cycle lies in one, and runs
11//! Howard's policy iteration on each (Cochet-Terrasson, Cohen, Gaubert, Mc Gettrick & Quadrat, *Numerical
12//! computation of spectral elements in max-plus algebra*, 1998), the fastest method in practice (Dasdan,
13//! *Experimental analysis of the fastest optimum cycle ratio and mean algorithms*, 2004). A policy picks one edge out
14//! of every node; its cycles have exact ratios `p/q` in lowest terms, and each node takes the ratio of the cycle its
15//! policy path reaches and a bias `x` with `x(u) = q·delay − p·registers + x(v)` along its edge. A node switches to
16//! an edge reaching a larger ratio, or, when none does, to an edge of the same ratio and a larger
17//! `q·delay − p·registers + x(v)`; when no node can switch, every node of the component has the component's largest
18//! ratio. All of it is integer arithmetic.
19//!
20//! The largest ratio `D/R` over the components is the bound, and a longest-path pass over the integer weights
21//! `R·delay − D·registers`, which no cycle makes positive, gives a potential `π` with
22//! `π(to) ≥ π(from) + R·delay − D·registers` on every edge. Summed around any cycle that says no cycle's ratio
23//! exceeds `D/R`, and the cycle found reaches it: the [`Bound::Ratio`] is that cycle and that potential, and [`check`]
24//! replays both in exact integer arithmetic, trusting nothing the search did.
25//!
26//! Every delay and register count is below [`LIMIT`] and a graph has at most [`NODES`] nodes, so every sum and
27//! product the search and the checker form fits `i128` exactly.
28
29use std::cmp::Ordering;
30use std::collections::VecDeque;
31
32/// The bound on an edge's delay and register count.
33pub const LIMIT: i64 = 1 << 24;
34
35/// The bound on the node count.
36pub const NODES: usize = 1 << 31;
37
38/// An edge of the graph: `delay` of combinational logic and `registers` on it, both counts.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub struct Edge {
41    pub from: usize,
42    pub to: usize,
43    pub delay: i64,
44    pub registers: i64,
45}
46
47/// What bounds the period.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum Bound {
50    /// No cycle: nothing bounds the period but the longest path, which retiming can cut.
51    Acyclic,
52    /// A cycle through no register, its edges in order.
53    CombinationalLoop(Vec<usize>),
54    /// The largest `delay / registers` over every cycle: the sums of the cycle reaching it, its edges in order, and a
55    /// potential over the nodes under which no cycle exceeds it.
56    Ratio { delay: i64, registers: i64, cycle: Vec<usize>, potential: Vec<i128> },
57}
58
59impl Bound {
60    /// The bound as a fraction in lowest terms, `delay / registers`.
61    pub fn period(&self) -> Option<(i64, i64)> {
62        match self {
63            Bound::Ratio { delay, registers, .. } => {
64                let g = gcd(*delay, *registers).max(1);
65                Some((delay / g, registers / g))
66            }
67            _ => None,
68        }
69    }
70}
71
72fn gcd(mut a: i64, mut b: i64) -> i64 {
73    while b != 0 {
74        (a, b) = (b, a % b);
75    }
76    a.abs()
77}
78
79/// Why a graph has no bound computed.
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub enum GraphError {
82    /// An edge names a node past the count.
83    OutOfRange { edge: usize },
84    /// An edge's delay or register count is negative.
85    Negative { edge: usize },
86    /// An edge's delay or register count is [`LIMIT`] or more.
87    TooLarge { edge: usize },
88    /// More than [`NODES`] nodes.
89    TooManyNodes,
90}
91
92fn validate(nodes: usize, edges: &[Edge]) -> Result<(), GraphError> {
93    if nodes > NODES {
94        return Err(GraphError::TooManyNodes);
95    }
96    for (k, e) in edges.iter().enumerate() {
97        if e.from >= nodes || e.to >= nodes {
98            return Err(GraphError::OutOfRange { edge: k });
99        }
100        if e.delay < 0 || e.registers < 0 {
101            return Err(GraphError::Negative { edge: k });
102        }
103        if e.delay >= LIMIT || e.registers >= LIMIT {
104            return Err(GraphError::TooLarge { edge: k });
105        }
106    }
107    Ok(())
108}
109
110/// The edges out of every node, in edge order.
111struct Out {
112    start: Vec<usize>,
113    list: Vec<usize>,
114}
115
116impl Out {
117    fn new(nodes: usize, edges: &[Edge]) -> Out {
118        let mut start = vec![0usize; nodes + 1];
119        for e in edges {
120            start[e.from + 1] += 1;
121        }
122        for n in 0..nodes {
123            start[n + 1] += start[n];
124        }
125        let mut fill = start.clone();
126        let mut list = vec![0usize; edges.len()];
127        for (k, e) in edges.iter().enumerate() {
128            list[fill[e.from]] = k;
129            fill[e.from] += 1;
130        }
131        Out { start, list }
132    }
133
134    fn of(&self, n: usize) -> &[usize] {
135        &self.list[self.start[n]..self.start[n + 1]]
136    }
137}
138
139/// A cycle of the subgraph `keep` admits, its edges in order, by depth-first search.
140fn find_cycle(nodes: usize, edges: &[Edge], keep: impl Fn(&Edge) -> bool) -> Option<Vec<usize>> {
141    let out = Out::new(nodes, edges);
142    // 0 unvisited, 1 on the stack, 2 done.
143    let mut state = vec![0u8; nodes];
144    let mut via: Vec<usize> = vec![usize::MAX; nodes];
145    for root in 0..nodes {
146        if state[root] != 0 {
147            continue;
148        }
149        let mut stack: Vec<(usize, usize)> = vec![(root, 0)];
150        state[root] = 1;
151        while let Some(top) = stack.last_mut() {
152            let (n, i) = *top;
153            let Some(&k) = out.of(n).get(i) else {
154                state[n] = 2;
155                stack.pop();
156                continue;
157            };
158            top.1 += 1;
159            if !keep(&edges[k]) {
160                continue;
161            }
162            let t = edges[k].to;
163            match state[t] {
164                0 => {
165                    state[t] = 1;
166                    via[t] = k;
167                    stack.push((t, 0));
168                }
169                1 => {
170                    let mut cycle = vec![k];
171                    let mut at = n;
172                    while at != t {
173                        let e = via[at];
174                        cycle.push(e);
175                        at = edges[e].from;
176                    }
177                    cycle.reverse();
178                    return Some(cycle);
179                }
180                _ => {}
181            }
182        }
183    }
184    None
185}
186
187/// The strongly connected components, sources first, by Tarjan's algorithm without recursion; and each node's
188/// component.
189fn components(nodes: usize, edges: &[Edge], out: &Out) -> (Vec<Vec<usize>>, Vec<usize>) {
190    let mut index = vec![usize::MAX; nodes];
191    let mut low = vec![0usize; nodes];
192    let mut on = vec![false; nodes];
193    let mut stack: Vec<usize> = Vec::new();
194    let mut comps: Vec<Vec<usize>> = Vec::new();
195    let mut next = 0;
196    for root in 0..nodes {
197        if index[root] != usize::MAX {
198            continue;
199        }
200        let mut call: Vec<(usize, usize)> = vec![(root, 0)];
201        index[root] = next;
202        low[root] = next;
203        next += 1;
204        stack.push(root);
205        on[root] = true;
206        while let Some(top) = call.last_mut() {
207            let (v, i) = *top;
208            if let Some(&k) = out.of(v).get(i) {
209                top.1 += 1;
210                let w = edges[k].to;
211                if index[w] == usize::MAX {
212                    index[w] = next;
213                    low[w] = next;
214                    next += 1;
215                    stack.push(w);
216                    on[w] = true;
217                    call.push((w, 0));
218                } else if on[w] {
219                    low[v] = low[v].min(index[w]);
220                }
221                continue;
222            }
223            call.pop();
224            if let Some(&(u, _)) = call.last() {
225                low[u] = low[u].min(low[v]);
226            }
227            if low[v] == index[v] {
228                let mut members = Vec::new();
229                loop {
230                    let w = stack.pop().expect("v is on the stack");
231                    on[w] = false;
232                    members.push(w);
233                    if w == v {
234                        break;
235                    }
236                }
237                comps.push(members);
238            }
239        }
240    }
241    comps.reverse();
242    let mut comp = vec![0usize; nodes];
243    for (c, members) in comps.iter().enumerate() {
244        for &v in members {
245            comp[v] = c;
246        }
247    }
248    (comps, comp)
249}
250
251/// A ratio `p/q` in lowest terms, `q > 0`.
252#[derive(Clone, Copy, Debug, PartialEq, Eq)]
253struct Ratio {
254    p: i64,
255    q: i64,
256}
257
258impl Ratio {
259    fn new(p: i64, q: i64) -> Ratio {
260        let g = gcd(p, q).max(1);
261        Ratio { p: p / g, q: q / g }
262    }
263
264    fn weight(self, e: &Edge) -> i128 {
265        self.q as i128 * e.delay as i128 - self.p as i128 * e.registers as i128
266    }
267}
268
269impl Ord for Ratio {
270    fn cmp(&self, other: &Ratio) -> Ordering {
271        (self.p as i128 * other.q as i128).cmp(&(other.p as i128 * self.q as i128))
272    }
273}
274
275impl PartialOrd for Ratio {
276    fn partial_cmp(&self, other: &Ratio) -> Option<Ordering> {
277        Some(self.cmp(other))
278    }
279}
280
281fn sums(edges: &[Edge], cycle: &[usize]) -> (i64, i64) {
282    cycle.iter().fold((0, 0), |(d, r), &k| (d + edges[k].delay, r + edges[k].registers))
283}
284
285/// Howard's policy iteration on component `c`, whose every cycle holds a register: its largest ratio and a cycle
286/// reaching it.
287fn howard(edges: &[Edge], out: &Out, members: &[usize], comp: &[usize], c: usize) -> (Ratio, Vec<usize>) {
288    let inside = |k: &&usize| comp[edges[**k].to] == c;
289    let mut policy: Vec<usize> = vec![usize::MAX; comp.len()];
290    for &v in members {
291        policy[v] = *out.of(v).iter().filter(inside).max_by_key(|&&k| edges[k].delay).expect("a component's node has an edge in it");
292    }
293    let mut ratio: Vec<Ratio> = vec![Ratio { p: 0, q: 1 }; comp.len()];
294    let mut bias: Vec<i128> = vec![0; comp.len()];
295    let mut state: Vec<u8> = vec![0; comp.len()];
296    let mut path: Vec<usize> = Vec::new();
297    loop {
298        // Value determination: 0 unseen, 1 on the walk, 2 valued.
299        for &v in members {
300            state[v] = 0;
301        }
302        let mut found: Option<(Ratio, Vec<usize>)> = None;
303        for &start in members {
304            if state[start] != 0 {
305                continue;
306            }
307            path.clear();
308            let mut at = start;
309            while state[at] == 0 {
310                state[at] = 1;
311                path.push(at);
312                at = edges[policy[at]].to;
313            }
314            let mut tail = path.len();
315            if state[at] == 1 {
316                let pos = path.iter().position(|&n| n == at).expect("the walk met itself");
317                let cycle = &path[pos..];
318                let edges_of: Vec<usize> = cycle.iter().map(|&n| policy[n]).collect();
319                let (d, r) = sums(edges, &edges_of);
320                let lambda = Ratio::new(d, r);
321                let root = (0..cycle.len()).min_by_key(|&i| cycle[i]).expect("a cycle has a node");
322                bias[cycle[root]] = 0;
323                ratio[cycle[root]] = lambda;
324                state[cycle[root]] = 2;
325                for k in 1..cycle.len() {
326                    let i = (root + cycle.len() - k) % cycle.len();
327                    let u = cycle[i];
328                    let t = cycle[(i + 1) % cycle.len()];
329                    bias[u] = lambda.weight(&edges[policy[u]]) + bias[t];
330                    ratio[u] = lambda;
331                    state[u] = 2;
332                }
333                if found.as_ref().map_or(true, |(best, _)| lambda > *best) {
334                    found = Some((lambda, edges_of));
335                }
336                tail = pos;
337            }
338            for &u in path[..tail].iter().rev() {
339                let t = edges[policy[u]].to;
340                ratio[u] = ratio[t];
341                bias[u] = ratio[u].weight(&edges[policy[u]]) + bias[t];
342                state[u] = 2;
343            }
344        }
345        // Improvement: a larger ratio first; else, at the same ratio, a larger bias.
346        let mut changed = false;
347        for &u in members {
348            let mut best = ratio[u];
349            for &k in out.of(u).iter().filter(inside) {
350                let r = ratio[edges[k].to];
351                if r > best {
352                    best = r;
353                    policy[u] = k;
354                    changed = true;
355                }
356            }
357        }
358        if !changed {
359            for &u in members {
360                let mut best = bias[u];
361                for &k in out.of(u).iter().filter(inside) {
362                    let t = edges[k].to;
363                    if ratio[t] != ratio[u] {
364                        continue;
365                    }
366                    let value = ratio[u].weight(&edges[k]) + bias[t];
367                    if value > best {
368                        best = value;
369                        policy[u] = k;
370                        changed = true;
371                    }
372                }
373            }
374        }
375        if !changed {
376            return found.expect("a component with an edge has a policy cycle");
377        }
378    }
379}
380
381/// Longest paths from a virtual source to every node under the weights `lambda.q·delay − lambda.p·registers`, which
382/// no cycle makes positive, one component at a time in topological order.
383fn potential(nodes: usize, edges: &[Edge], out: &Out, comps: &[Vec<usize>], comp: &[usize], lambda: Ratio) -> Vec<i128> {
384    let mut dist = vec![0i128; nodes];
385    let mut queued = vec![false; nodes];
386    let mut queue: VecDeque<usize> = VecDeque::new();
387    for (c, members) in comps.iter().enumerate() {
388        queue.extend(members.iter().copied());
389        for &v in members {
390            queued[v] = true;
391        }
392        while let Some(u) = queue.pop_front() {
393            queued[u] = false;
394            for &k in out.of(u) {
395                let e = &edges[k];
396                if comp[e.to] != c {
397                    continue;
398                }
399                let d = dist[u] + lambda.weight(e);
400                if d > dist[e.to] {
401                    dist[e.to] = d;
402                    if !queued[e.to] {
403                        queued[e.to] = true;
404                        queue.push_back(e.to);
405                    }
406                }
407            }
408        }
409        for &u in members {
410            for &k in out.of(u) {
411                let e = &edges[k];
412                if comp[e.to] != c {
413                    dist[e.to] = dist[e.to].max(dist[u] + lambda.weight(e));
414                }
415            }
416        }
417    }
418    dist
419}
420
421/// The bound on the period the graph's cycles put.
422pub fn max_cycle_ratio(nodes: usize, edges: &[Edge]) -> Result<Bound, GraphError> {
423    validate(nodes, edges)?;
424    if let Some(cycle) = find_cycle(nodes, edges, |e| e.registers == 0) {
425        return Ok(Bound::CombinationalLoop(cycle));
426    }
427    let out = Out::new(nodes, edges);
428    let (comps, comp) = components(nodes, edges, &out);
429    let mut best: Option<(Ratio, Vec<usize>)> = None;
430    for (c, members) in comps.iter().enumerate() {
431        let cyclic = members.len() > 1 || out.of(members[0]).iter().any(|&k| edges[k].to == members[0]);
432        if !cyclic {
433            continue;
434        }
435        let (lambda, cycle) = howard(edges, &out, members, &comp, c);
436        if best.as_ref().map_or(true, |(b, _)| lambda > *b) {
437            best = Some((lambda, cycle));
438        }
439    }
440    let Some((lambda, cycle)) = best else {
441        return Ok(Bound::Acyclic);
442    };
443    let (delay, registers) = sums(edges, &cycle);
444    let scale = (registers / lambda.q) as i128;
445    let potential = potential(nodes, edges, &out, &comps, &comp, lambda).into_iter().map(|d| d * scale).collect();
446    Ok(Bound::Ratio { delay, registers, cycle, potential })
447}
448
449/// Whether `bound` is the graph's: a combinational loop is a simple cycle of no register; a ratio's cycle is a simple
450/// cycle with those sums and some register, and its potential satisfies every edge; acyclic means the graph has no
451/// cycle. The graph itself must be well formed.
452pub fn check(nodes: usize, edges: &[Edge], bound: &Bound) -> bool {
453    if validate(nodes, edges).is_err() {
454        return false;
455    }
456    let is_cycle = |cycle: &[usize]| {
457        let mut seen = vec![false; nodes];
458        !cycle.is_empty()
459            && cycle.iter().all(|&k| k < edges.len() && !std::mem::replace(&mut seen[edges[k].from], true))
460            && cycle.iter().enumerate().all(|(i, &k)| edges[k].to == edges[cycle[(i + 1) % cycle.len()]].from)
461    };
462    match bound {
463        Bound::Acyclic => find_cycle(nodes, edges, |_| true).is_none(),
464        Bound::CombinationalLoop(cycle) => is_cycle(cycle) && cycle.iter().all(|&k| edges[k].registers == 0),
465        Bound::Ratio { delay, registers, cycle, potential } => {
466            if !is_cycle(cycle) || sums(edges, cycle) != (*delay, *registers) || *registers <= 0 || potential.len() != nodes {
467                return false;
468            }
469            edges.iter().all(|e| {
470                let w = *registers as i128 * e.delay as i128 - *delay as i128 * e.registers as i128;
471                match potential[e.from].checked_add(w) {
472                    Some(reach) => potential[e.to] >= reach,
473                    None => w < 0,
474                }
475            })
476        }
477    }
478}
479
480#[cfg(test)]
481#[path = "../../logicaffeine_tests/unit/logicaffeine_proof/cycle_ratio/tests.rs"]
482mod tests;