calyx_opt/analysis/control_order.rs
1use crate::analysis::{PromotionAnalysis, ReadWriteSet};
2use calyx_ir as ir;
3use calyx_utils::{CalyxResult, Error};
4use ir::RRC;
5use itertools::Itertools;
6use petgraph::{
7 algo,
8 graph::{DiGraph, NodeIndex},
9};
10use std::collections::{HashMap, HashSet};
11use std::fmt::Write as _;
12
13/// Extract the dependency order of a list of control programs.
14/// Dependencies are defined using read/write sets used in the control program.
15/// The read/write sets ignore ports on constants and ThisComponent.
16///
17/// For example, if we have control programs C1 and C2 with read sets R1 and
18/// R2 and write sets W1 and W2 respectively, we can define an order relationship:
19///
20/// C1 < C2 if (R1 subset of W2) and (R2 disjoint W1)
21/// C1 > C2 if (R2 subset of W1) and (R1 disjoint W2)
22/// C1 =!= if (R1 subset of W2) and (R2 subset of W1)
23///
24/// Setting `BETTER_ERR` turns on additional machinery to generate an explanation for what caused
25/// the error but may require expensive computations. Turn on when cycles should be a hard error.
26pub struct ControlOrder<const BETTER_ERR: bool>;
27
28impl<const BETTER_ERR: bool> ControlOrder<BETTER_ERR> {
29 fn get_cells(ports: Vec<RRC<ir::Port>>) -> impl Iterator<Item = ir::Id> {
30 ports
31 .into_iter()
32 .filter_map(|p| {
33 let cr = p.borrow().cell_parent();
34 let cell = cr.borrow();
35 match cell.prototype {
36 // Ignore constants and _this
37 ir::CellType::Constant { .. } => None,
38 ir::CellType::ThisComponent => None,
39 _ => Some(cell.name()),
40 }
41 })
42 .unique()
43 }
44
45 // Filters out the constants from `cells`, while mapping the remaining `ir:Cell`s
46 // to their cell name.
47 fn filter_out_constants(
48 cells: &[RRC<ir::Cell>],
49 ) -> impl Iterator<Item = ir::Id> + '_ {
50 cells
51 .iter()
52 .filter_map(|cell| match cell.borrow().prototype {
53 ir::CellType::Constant { .. } => None,
54 ir::CellType::Component { .. }
55 | ir::CellType::Primitive { .. }
56 | ir::CellType::ThisComponent { .. } => {
57 Some(cell.borrow().name())
58 }
59 })
60 .unique()
61 }
62
63 /// Return a total order for the control programs.
64 /// Returns an error if there is a cycle
65 pub fn get_total_order(
66 stmts: impl Iterator<Item = ir::Control>,
67 ) -> CalyxResult<Vec<ir::Control>> {
68 // Directed graph where edges means that a control program must be run before.
69 let mut gr: DiGraph<Option<ir::Control>, ()> = DiGraph::new();
70
71 // Mapping name of cell to all the indices that read or write to it.
72 let mut reads: HashMap<ir::Id, Vec<NodeIndex>> = HashMap::default();
73 let mut writes: HashMap<ir::Id, Vec<NodeIndex>> = HashMap::default();
74
75 let add_cells =
76 |idx: NodeIndex,
77 ports: Vec<RRC<ir::Port>>,
78 map: &mut HashMap<ir::Id, Vec<NodeIndex>>| {
79 let cells = Self::get_cells(ports);
80 for cell in cells {
81 map.entry(cell).or_default().push(idx);
82 }
83 };
84
85 // Compute read/write sets and add them to the maps
86 for c in stmts {
87 let (port_reads, port_writes) =
88 ReadWriteSet::control_port_read_write_set::<true>(&c);
89 let idx = gr.add_node(Some(c));
90 add_cells(idx, port_reads, &mut reads);
91 add_cells(idx, port_writes, &mut writes);
92 }
93
94 // Add edges between read and writes
95 for (cell, r_idxs) in reads {
96 if let Some(wr_idxs) = writes.get(&cell) {
97 wr_idxs.iter().cartesian_product(r_idxs.iter()).for_each(
98 |(wr, r)| {
99 if wr != r {
100 gr.add_edge(*r, *wr, ());
101 }
102 },
103 );
104 }
105 }
106
107 if let Ok(order) = algo::toposort(&gr, None) {
108 let assigns = order
109 .into_iter()
110 .map(|idx| gr[idx].take().unwrap())
111 .collect_vec();
112 Ok(assigns)
113 } else {
114 let mut msg = "".to_string();
115 if BETTER_ERR {
116 // Compute strongly connected component of the graph
117 let sccs = algo::kosaraju_scc(&gr);
118 let scc = sccs.iter().find(|cc| cc.len() > 1).unwrap();
119 msg = scc
120 .iter()
121 .map(|idx| {
122 let con = gr[*idx].as_ref().unwrap();
123 let mut msg = ir::Printer::control_to_str(con);
124 let (port_reads, port_writes) =
125 ReadWriteSet::control_port_read_write_set::<true>(
126 con,
127 );
128 write!(
129 msg,
130 " which reads: {}\n and writes: {}",
131 Self::get_cells(port_reads)
132 .map(|c| c.id)
133 .join(", "),
134 Self::get_cells(port_writes)
135 .map(|c| c.id)
136 .join(", "),
137 )
138 .unwrap();
139 msg
140 })
141 .join("\n");
142 }
143 Err(Error::misc(format!("No possible sequential ordering. Control programs exhibit data race:\n{}", msg)))
144 }
145 }
146
147 // Returns a graph of dependency for input programs.
148 // IMPORTANT: we ignore assignments to done ports.
149 // Input control programs are considered to have data dependency if:
150 // 1. subsequent program writes to cells that previous program reads from
151 // 2. subsequent program writes to cells that previous program writes to
152 // 3. subsequent program reads from cells that previous program writes to
153 // Furthermore, we add dependencies due to continuous assignments as well. If:
154 // 4. Program writes to cell that a continuous assignment writes to or reads from.
155 // 5. Program reads from a cell that a continuous assignment writes to.
156 // Then the program "touches" the continuous assignments, and therefore depends
157 // on all previous programs that "touched" continuous assignments as well.
158 // In short, we treat continuous assignments as one big cell.
159 pub fn get_dependency_graph_seq(
160 stmts: impl Iterator<Item = ir::Control>,
161 (cont_reads, cont_writes): (
162 &Vec<ir::RRC<ir::Cell>>,
163 &Vec<ir::RRC<ir::Cell>>,
164 ),
165 dependency: &mut HashMap<NodeIndex, Vec<NodeIndex>>,
166 latency_map: &mut HashMap<NodeIndex, u64>,
167 ) -> DiGraph<Option<ir::Control>, ()> {
168 // The names of the cells that are read/written in continuous assignments
169 let cont_read_cell_names =
170 Self::filter_out_constants(cont_reads).collect_vec();
171 let cont_write_cell_names =
172 Self::filter_out_constants(cont_writes).collect_vec();
173
174 // Directed graph where edges means that a control program must be run before.
175 let mut gr: DiGraph<Option<ir::Control>, ()> = DiGraph::new();
176
177 // Mapping name of cell to all the indices that read or write to it.
178 let mut reads: HashMap<ir::Id, Vec<NodeIndex>> = HashMap::default();
179 let mut writes: HashMap<ir::Id, Vec<NodeIndex>> = HashMap::default();
180
181 // Stores the nodes (i.e., control stmts) that are affected by continuous
182 // assignments
183 let mut continuous_idxs: HashSet<NodeIndex> = HashSet::new();
184
185 for c in stmts {
186 let (cell_reads, cell_writes) =
187 ReadWriteSet::control_read_write_set::<false>(&c);
188 let r_cell_names = Self::filter_out_constants(&cell_reads);
189 let w_cell_names = Self::filter_out_constants(&cell_writes);
190 let latency = PromotionAnalysis::get_inferred_latency(&c);
191 let idx = gr.add_node(Some(c));
192 dependency.insert(idx, Vec::new());
193 latency_map.insert(idx, latency);
194
195 for cell in r_cell_names {
196 // Checking: 3. subsequent program reads from cells that previous program writes to
197 if let Some(wr_idxs) = writes.get(&cell) {
198 for wr_idx in wr_idxs {
199 if !wr_idx.eq(&idx) {
200 gr.add_edge(*wr_idx, idx, ());
201 dependency.entry(idx).or_default().push(*wr_idx);
202 }
203 }
204 }
205
206 // Checking: 5. Program reads from a cell that a continuous
207 // assignment writes to.
208 if cont_write_cell_names.contains(&cell) {
209 for cur_idx in continuous_idxs.iter() {
210 if !cur_idx.eq(&idx) {
211 gr.add_edge(*cur_idx, idx, ());
212 dependency.entry(idx).or_default().push(*cur_idx);
213 }
214 }
215 continuous_idxs.insert(idx);
216 }
217 reads.entry(cell).or_default().push(idx);
218 }
219
220 for cell in w_cell_names {
221 // Checking: 2. subsequent program writes to cells that previous program writes to
222 if let Some(wr_idxs) = writes.get(&cell) {
223 for wr_idx in wr_idxs {
224 if !wr_idx.eq(&idx) {
225 gr.add_edge(*wr_idx, idx, ());
226 dependency.entry(idx).or_default().push(*wr_idx);
227 }
228 }
229 }
230
231 // Checking: 1. subsequent program writes to cells that previous program reads from
232 if let Some(r_idxs) = reads.get(&cell) {
233 for r_idx in r_idxs {
234 if !r_idx.eq(&idx) {
235 gr.add_edge(*r_idx, idx, ());
236 dependency.entry(idx).or_default().push(*r_idx);
237 }
238 }
239 }
240
241 // Checking: 4. Program writes to cell that a continuous assignment
242 // writes to or reads from.
243 if cont_write_cell_names.contains(&cell)
244 || cont_read_cell_names.contains(&cell)
245 {
246 for cur_idx in continuous_idxs.iter() {
247 if !cur_idx.eq(&idx) {
248 gr.add_edge(*cur_idx, idx, ());
249 dependency.entry(idx).or_default().push(*cur_idx);
250 }
251 }
252 continuous_idxs.insert(idx);
253 }
254
255 writes.entry(cell).or_default().push(idx);
256 }
257 }
258 gr
259 }
260}