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}