Skip to content

Commit 6a99b2d

Browse files
committed
Add lax::scheduler module for dynamic ordering...
... of lax open hypergraphs. This is a slightly less 'batteries included' version of the SSA module used in catgrad and other projects.
1 parent bfde2ad commit 6a99b2d

5 files changed

Lines changed: 252 additions & 0 deletions

File tree

src/lax/hypergraph.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,9 @@ impl<O: Clone, A: Clone> Hypergraph<O, A> {
382382
pub fn to_hypergraph(&self) -> crate::strict::Hypergraph<VecKind, O, A> {
383383
make_hypergraph(self)
384384
}
385+
}
385386

387+
impl<O, A> Hypergraph<O, A> {
386388
pub fn coequalizer(&self) -> FiniteFunction<VecKind> {
387389
// Compute the coequalizer (connected components) of the quotient graph
388390
let s: FiniteFunction<VecKind> = FiniteFunction {

src/lax/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,5 @@ pub use open_hypergraph::*;
8383

8484
pub mod optic;
8585
pub mod var;
86+
87+
pub mod scheduler;

src/lax/scheduler.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
//! # Scheduling (dynamic ordering) of acyclic lax open hypergraph operations
2+
use crate::array::vec::{VecArray, VecKind};
3+
use crate::array::NaturalArray;
4+
use crate::finite_function::FiniteFunction;
5+
use crate::indexed_coproduct::IndexedCoproduct;
6+
use crate::lax::{EdgeId, OpenHypergraph};
7+
use crate::strict::graph;
8+
use std::collections::HashSet;
9+
10+
/// Errors when constructing or advancing a topological ordering.
11+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12+
pub enum TopologicalSchedulerError {
13+
/// The hypergraph has a directed cycle, so no topological order exists.
14+
Cycle,
15+
/// The requested edge id is outside the operation set.
16+
UnknownEdge(EdgeId),
17+
/// The requested edge is not currently available to pop.
18+
NotAvailable(EdgeId),
19+
/// The requested pop subset contains the same edge more than once.
20+
Duplicate(EdgeId),
21+
}
22+
23+
/// Stateful topological scheduler over operation edges.
24+
///
25+
/// At each step, [`available`](Self::available) returns operations with zero current indegree.
26+
/// Calling [`pop_subset`](Self::pop_subset) marks any subset of those operations as processed.
27+
pub struct TopologicalScheduler {
28+
adjacency: IndexedCoproduct<VecKind, FiniteFunction<VecKind>>,
29+
indegree: Vec<usize>,
30+
available: HashSet<EdgeId>,
31+
popped: HashSet<EdgeId>,
32+
remaining: usize,
33+
}
34+
35+
impl TopologicalScheduler {
36+
/// Build a scheduler for all operations in `f`.
37+
///
38+
/// Returns [`TopologicalSchedulerError::Cycle`] if `f` has a directed cycle.
39+
pub fn new<O, A>(f: &OpenHypergraph<O, A>) -> Result<Self, TopologicalSchedulerError> {
40+
let adjacency = operation_adjacency(f);
41+
42+
let (_, unvisited) = graph::kahn(&adjacency);
43+
if unvisited.0.contains(&1) {
44+
return Err(TopologicalSchedulerError::Cycle);
45+
}
46+
47+
let indegree_ff = graph::indegree(&adjacency);
48+
let mut available = HashSet::new();
49+
for &edge_ix in indegree_ff.table.zero().iter() {
50+
available.insert(EdgeId(edge_ix));
51+
}
52+
53+
let edge_count = indegree_ff.table.len();
54+
Ok(Self {
55+
adjacency,
56+
indegree: indegree_ff.table.0,
57+
available,
58+
popped: HashSet::new(),
59+
remaining: edge_count,
60+
})
61+
}
62+
63+
/// Operations currently available to be popped.
64+
pub fn available(&self) -> &HashSet<EdgeId> {
65+
&self.available
66+
}
67+
68+
/// Number of operations that have not yet been popped.
69+
pub fn remaining(&self) -> usize {
70+
self.remaining
71+
}
72+
73+
/// Whether all operations have been popped.
74+
pub fn is_complete(&self) -> bool {
75+
self.remaining == 0
76+
}
77+
78+
/// Pop any subset of currently available operations.
79+
///
80+
/// This advances the scheduler by updating indegrees and frontier according to Kahn's
81+
/// algorithm.
82+
pub fn pop_subset(&mut self, edges: &[EdgeId]) -> Result<(), TopologicalSchedulerError> {
83+
if edges.is_empty() {
84+
return Ok(());
85+
}
86+
87+
let mut seen = HashSet::new();
88+
let edge_count = self.indegree.len();
89+
for &edge in edges {
90+
if edge.0 >= edge_count {
91+
return Err(TopologicalSchedulerError::UnknownEdge(edge));
92+
}
93+
if !seen.insert(edge.0) {
94+
return Err(TopologicalSchedulerError::Duplicate(edge));
95+
}
96+
if !self.available.contains(&edge) {
97+
return Err(TopologicalSchedulerError::NotAvailable(edge));
98+
}
99+
}
100+
101+
for &edge in edges {
102+
self.available.remove(&edge);
103+
self.popped.insert(edge);
104+
self.remaining -= 1;
105+
}
106+
107+
let frontier = FiniteFunction::new(
108+
VecArray(edges.iter().map(|e| e.0).collect()),
109+
self.adjacency.len(),
110+
)
111+
.expect("subset validated to be in range");
112+
113+
let (reachable_ix, reachable_count) =
114+
graph::sparse_relative_indegree(&self.adjacency, &frontier);
115+
for (&edge_ix, &count) in reachable_ix.table.iter().zip(reachable_count.table.iter()) {
116+
self.indegree[edge_ix] -= count;
117+
let edge = EdgeId(edge_ix);
118+
if self.indegree[edge_ix] == 0 && !self.popped.contains(&edge) {
119+
self.available.insert(edge);
120+
}
121+
}
122+
123+
Ok(())
124+
}
125+
}
126+
127+
fn operation_adjacency<O, A>(
128+
f: &OpenHypergraph<O, A>,
129+
) -> IndexedCoproduct<VecKind, FiniteFunction<VecKind>> {
130+
let edge_count = f.hypergraph.edges.len();
131+
let quotient = f.hypergraph.coequalizer();
132+
let class_count = quotient.target;
133+
134+
// For each quotiented node-class, all source incidences (consumers) at that class.
135+
let mut consumers: Vec<Vec<usize>> = vec![Vec::new(); class_count];
136+
for (edge_ix, edge) in f.hypergraph.adjacency.iter().enumerate() {
137+
for source in &edge.sources {
138+
let class_ix = quotient.table[source.0];
139+
consumers[class_ix].push(edge_ix);
140+
}
141+
}
142+
143+
let mut lengths = Vec::with_capacity(edge_count);
144+
let mut values = Vec::<usize>::new();
145+
for edge in &f.hypergraph.adjacency {
146+
let start = values.len();
147+
for target in &edge.targets {
148+
let class_ix = quotient.table[target.0];
149+
values.extend_from_slice(&consumers[class_ix]);
150+
}
151+
lengths.push(values.len() - start);
152+
}
153+
154+
IndexedCoproduct::new(
155+
FiniteFunction::new(VecArray(lengths), values.len() + 1).expect("valid by construction"),
156+
FiniteFunction::new(VecArray(values), edge_count).expect("valid by construction"),
157+
)
158+
.expect("valid by construction")
159+
}

tests/lax/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ pub mod eval;
22
pub mod functor;
33
pub mod hypergraph;
44
pub mod open_hypergraph;
5+
pub mod scheduler;

tests/lax/scheduler.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use open_hypergraphs::category::Arrow;
2+
use open_hypergraphs::lax::scheduler::{TopologicalScheduler, TopologicalSchedulerError};
3+
use open_hypergraphs::lax::{EdgeId, OpenHypergraph};
4+
5+
#[derive(Clone, Debug, PartialEq, Eq)]
6+
enum Obj {
7+
A,
8+
}
9+
10+
#[derive(Clone, Debug, PartialEq, Eq)]
11+
enum Op {
12+
Copy,
13+
Mul,
14+
F,
15+
G,
16+
}
17+
18+
#[test]
19+
fn test_topological_order_sequential() {
20+
let copy = OpenHypergraph::singleton(Op::Copy, vec![Obj::A], vec![Obj::A, Obj::A]);
21+
let mul = OpenHypergraph::singleton(Op::Mul, vec![Obj::A, Obj::A], vec![Obj::A]);
22+
let composed = copy.compose(&mul).expect("copy ; mul should typecheck");
23+
24+
let mut order = TopologicalScheduler::new(&composed).expect("acyclic");
25+
assert_eq!(order.available().len(), 1);
26+
assert!(order.available().contains(&EdgeId(0)));
27+
28+
order.pop_subset(&[EdgeId(0)]).expect("edge 0 available");
29+
assert_eq!(order.available().len(), 1);
30+
assert!(order.available().contains(&EdgeId(1)));
31+
assert_eq!(order.remaining(), 1);
32+
33+
order.pop_subset(&[EdgeId(1)]).expect("edge 1 available");
34+
assert!(order.is_complete());
35+
assert!(order.available().is_empty());
36+
}
37+
38+
#[test]
39+
fn test_topological_order_parallel_pop_subset() {
40+
let f = OpenHypergraph::singleton(Op::Copy, vec![Obj::A], vec![Obj::A, Obj::A]);
41+
let g = OpenHypergraph::singleton(Op::Mul, vec![Obj::A, Obj::A], vec![Obj::A]);
42+
let parallel = f.tensor(&g);
43+
44+
let mut order = TopologicalScheduler::new(&parallel).expect("acyclic");
45+
assert_eq!(order.available().len(), 2);
46+
assert!(order.available().contains(&EdgeId(0)));
47+
assert!(order.available().contains(&EdgeId(1)));
48+
49+
order.pop_subset(&[EdgeId(1)]).expect("edge 1 available");
50+
assert_eq!(order.available().len(), 1);
51+
assert!(order.available().contains(&EdgeId(0)));
52+
assert_eq!(order.remaining(), 1);
53+
54+
order.pop_subset(&[EdgeId(0)]).expect("edge 0 available");
55+
assert!(order.is_complete());
56+
}
57+
58+
#[test]
59+
fn test_topological_order_pop_rejects_unavailable() {
60+
let copy = OpenHypergraph::singleton(Op::Copy, vec![Obj::A], vec![Obj::A, Obj::A]);
61+
let mul = OpenHypergraph::singleton(Op::Mul, vec![Obj::A, Obj::A], vec![Obj::A]);
62+
let composed = copy.compose(&mul).expect("copy ; mul should typecheck");
63+
64+
let mut order = TopologicalScheduler::new(&composed).expect("acyclic");
65+
let err = order
66+
.pop_subset(&[EdgeId(1)])
67+
.expect_err("edge 1 is not initially available");
68+
assert_eq!(err, TopologicalSchedulerError::NotAvailable(EdgeId(1)));
69+
}
70+
71+
#[test]
72+
fn test_topological_order_unquotiented_respects_quotient_dependency() {
73+
let mut f = OpenHypergraph::empty();
74+
let (_, (_, f_targets)) = f.new_operation(Op::F, vec![Obj::A], vec![Obj::A]);
75+
let (_, (g_sources, _)) = f.new_operation(Op::G, vec![Obj::A], vec![Obj::A]);
76+
77+
// Keep the graph unquotiented, but record that output(F) and input(G) are the same node.
78+
f.unify(f_targets[0], g_sources[0]);
79+
assert_eq!(f.hypergraph.quotient.0.len(), 1);
80+
81+
let mut order = TopologicalScheduler::new(&f).expect("acyclic");
82+
assert_eq!(order.available().len(), 1);
83+
assert!(order.available().contains(&EdgeId(0)));
84+
85+
order.pop_subset(&[EdgeId(0)]).expect("edge 0 available");
86+
assert_eq!(order.available().len(), 1);
87+
assert!(order.available().contains(&EdgeId(1)));
88+
}

0 commit comments

Comments
 (0)