calyx_opt/passes/
cell_share.rs

1use crate::analysis::{
2    AssignmentAnalysis, GraphColoring, LiveRangeAnalysis, ShareSet,
3    StaticParTiming,
4};
5use crate::traversal::{
6    Action, ConstructVisitor, Named, ParseVal, PassOpt, VisResult, Visitor,
7};
8use calyx_ir::{self as ir, Id};
9use calyx_ir::{BoolAttr, rewriter};
10use calyx_utils::{CalyxResult, OutputFile};
11use itertools::Itertools;
12use serde::Serialize;
13use serde_json::{Value, json};
14use std::collections::{BTreeMap, HashMap, HashSet};
15
16// function to turn cell types to string when we are building the json for
17// share_freqs
18fn cell_type_to_string(cell_type: &ir::CellType) -> String {
19    match cell_type {
20        ir::CellType::Primitive {
21            name,
22            param_binding,
23            ..
24        } => {
25            let param_str = param_binding
26                .iter()
27                .map(|(id, val)| format!("{id}_{val}"))
28                .join("_");
29            format!("{name}_{param_str}")
30        }
31        ir::CellType::Component { name } => name.to_string(),
32        ir::CellType::ThisComponent => "ThisComponent".to_string(),
33        ir::CellType::Constant { val, width } => {
34            format!("Const_{val}_{width}")
35        }
36    }
37}
38
39#[derive(PartialEq, Eq, Hash, Clone, Serialize)]
40struct ShareEntry {
41    #[serde(serialize_with = "id_serialize_passthrough")]
42    original: Id, // cell to be replaced
43    #[serde(serialize_with = "id_serialize_passthrough")]
44    new: Id, // replacement cell (shared)
45    cell_type: String,
46}
47
48fn id_serialize_passthrough<S>(id: &Id, ser: S) -> Result<S::Ok, S::Error>
49where
50    S: serde::Serializer,
51{
52    id.to_string().serialize(ser)
53}
54
55/// Given a [LiveRangeAnalysis] that specifies the "share" and "state_share" cells
56/// alive at each group, minimizes the cells used for each component.
57///
58/// This works by constructing an interference graph for each alive "state_share" cell.
59/// If two cells are ever alive at the same time, then there is an edge
60/// between them in the interference graph. Additionally, if two cells
61/// are different prototypes, then there is an edge between them.
62///
63/// A greedy graph coloring algorithm on the interference graph
64/// is used to assign each cell a name.
65///
66/// By default, this pass will share a given cell as many times as possible. However,
67/// by passing a command line argument, we can limit the number of times a given
68/// cell is reused. The rationale behind this option is that, in certain cases,
69/// if you share a given component too much, the logic to determine when that
70/// component should be activated ends up being more expensive than just using
71/// a separate component. To pass this command line argument, you give three numbers:
72/// The number of times a given combinational component can be shared, the number
73/// of times a given register can be shared, and the number of times all other
74/// components can be shared. Generally we would want settings such that 1 < 2 < 3,
75/// since a given share of a 3) would save more hardware than a share of a 2), and
76/// a share of a 2) would save more hardware than a share of a 1).
77/// The exact command line syntax to use: if we had a file, "x.futil" and ran:
78/// `cargo run x.futil -x cell-share:bounds=2,4,8`, then we would only share a
79/// given combinational component at most twice, a given register at most 4 times,
80/// and all other components at most 8 times. If you wanted to do something with
81/// fud then run `fud e ... -s calyx.flags " -x cell-share:bounds=2,4,8"`. Finally
82/// if you do not want to bound the sharing for a particular cell type,
83/// you can pass -1 as a bound. So for example if you passed
84/// `-x cell-share:bounds=2,-1,3` this means that you will always share registers.
85/// Note: *The no spaces are important.*
86/// Also, if you pass the following flag: `-x cell-share:print-share-freqs=file-name`
87/// this pass will write a json to `file-name`. If want to print into stdout
88/// then just set the file-name to be "stdout" (you don't need the quotes
89/// when you actually pass in the argument, so run `-x cell-share:print-share-freqs=stdout`),
90/// and if you want to print to stderr then just set the file-name to be "stderr".
91/// The json will map an integer (say n) to the number of cells in the new design (i.e.,
92/// the design after sharing has been performed) that were shared
93/// exactly n times. So the key n = 2 will be mapped to the number of cells in the
94/// new design that are shared exactly twice.
95///
96/// Other flags:
97/// print_par_timing: prints the par-timing-map
98/// calyx_2020: shares using the Calyx 2020 settings: unlimited sharing of combinational
99/// components and registers, but no sharing of anything else
100///
101///
102/// This pass only renames uses of cells. [crate::passes::DeadCellRemoval] should be run after this
103/// to actually remove the definitions.
104pub struct CellShare {
105    live: LiveRangeAnalysis,
106    rewrites: HashMap<ir::Id, ir::RRC<ir::Cell>>,
107    /// Set of state shareable components (as type names)
108    state_shareable: ShareSet,
109
110    /// Set of shareable components (as type names)
111    shareable: ShareSet,
112
113    /// Cell active in continuous assignments, or ref cells (we want to ignore both)
114    cont_ref_cells: HashSet<ir::Id>,
115
116    /// Maps cell types to the corresponding pdf. Each pdf is a hashmap which maps
117    /// the number of times a given cell name reused (i.e., shared) to the
118    /// number of cells that have been shared that many times times.
119    share_freqs: HashMap<ir::Id, HashMap<ir::CellType, HashMap<i64, i64>>>,
120
121    /// maps the ids of groups to a set of tuples (i,j), the clock cycles (relative
122    /// to the start of the par) that group is live
123    par_timing_map: StaticParTiming,
124
125    //name of main (we know main will only be run once)
126    main: ir::Id,
127
128    // ========= Pass Options ============
129    /// The number of times a given class of cell can be shared. bounds should be
130    /// length 3 to hold the 3 classes: comb cells, registers, and everything else
131    bounds: [Option<i64>; 3],
132
133    /// executes cell share pass using Calyx 2020 benchmarks: no component
134    /// sharing, and only sharing registers and combinational components
135    calyx_2020: bool,
136
137    /// whether or not to print the share_freqs
138    print_share_freqs: Option<OutputFile>,
139    print_par_timing: Option<OutputFile>,
140
141    /// whether or not to print the share mappings
142    emit_share_map: Option<OutputFile>,
143    /// Bookkeeping for share mappings, using a BTreeMap for output determinism.
144    shared_cells: BTreeMap<String, Vec<ShareEntry>>,
145}
146
147impl Named for CellShare {
148    fn name() -> &'static str {
149        "cell-share"
150    }
151    fn description() -> &'static str {
152        "use the fewest possible cells"
153    }
154
155    fn opts() -> Vec<PassOpt> {
156        vec![
157            PassOpt::new(
158                "print-share-freqs",
159                "print sharing frequencies",
160                ParseVal::OutStream(OutputFile::Null),
161                PassOpt::parse_outstream,
162            ),
163            PassOpt::new(
164                "emit-share-map",
165                "emit json file of shared cells",
166                ParseVal::OutStream(OutputFile::Null),
167                PassOpt::parse_outstream,
168            ),
169            PassOpt::new(
170                "bounds",
171                "maximum amount of sharing for combinational components, registers, and other components. Negative valye means no bound",
172                ParseVal::List(vec![]),
173                PassOpt::parse_num_list,
174            ),
175            PassOpt::new(
176                "print-par-timing",
177                "print timing information for `par` blocks",
178                ParseVal::OutStream(OutputFile::Null),
179                PassOpt::parse_outstream,
180            ),
181            PassOpt::new(
182                "calyx-2020",
183                "share using the Calyx 2020 settings: no component sharing, only share registers/combinational components",
184                ParseVal::Bool(false),
185                PassOpt::parse_bool,
186            ),
187        ]
188    }
189}
190
191impl ConstructVisitor for CellShare {
192    fn from(ctx: &ir::Context) -> CalyxResult<Self> {
193        let state_shareable = ShareSet::from_context::<true>(ctx);
194        let shareable = ShareSet::from_context::<false>(ctx);
195        let opts = Self::get_opts(ctx);
196
197        Ok(CellShare {
198            live: LiveRangeAnalysis::default(),
199            rewrites: HashMap::new(),
200            cont_ref_cells: HashSet::new(),
201            state_shareable,
202            shareable,
203            par_timing_map: StaticParTiming::default(),
204            main: ctx.entrypoint,
205            share_freqs: HashMap::new(),
206            calyx_2020: opts["calyx-2020"].bool(),
207            bounds: opts["bounds"].num_list_exact::<3>(),
208            print_par_timing: opts["print-par-timing"].not_null_outstream(),
209            print_share_freqs: opts["print-share-freqs"].not_null_outstream(),
210            emit_share_map: opts["emit-share-map"].not_null_outstream(),
211            shared_cells: BTreeMap::new(),
212        })
213    }
214
215    fn clear_data(&mut self) {
216        self.rewrites = HashMap::new();
217        self.live = LiveRangeAnalysis::default();
218        self.cont_ref_cells = HashSet::new();
219    }
220}
221
222impl CellShare {
223    fn initialize(
224        &mut self,
225        comp: &ir::Component,
226        _sigs: &ir::LibrarySignatures,
227    ) {
228        //add cont cells
229        self.cont_ref_cells = comp
230            .continuous_assignments
231            .iter()
232            .analysis()
233            .cell_uses()
234            .map(|cr| cr.borrow().name())
235            .collect();
236        //add ref cells
237        self.cont_ref_cells.extend(
238            comp.cells
239                .iter()
240                .filter(|cell| cell.borrow().is_reference())
241                .map(|cell| cell.borrow().name()),
242        );
243
244        // We know main will only ever execute once
245        // If the component is shareable, then we know it completley overwrites
246        // state at each invocation and is therefore fine to treat as if it
247        // runs once (i.e., state doesn't live beyond a single invocation).
248        let only_run_once = comp.name == self.main
249            || comp.attributes.has(ir::BoolAttr::StateShare);
250
251        // TODO(rachit): Pass cont_ref_cells to LiveRangeAnalysis so that it ignores unneccessary
252        // cells.
253        self.live = LiveRangeAnalysis::new(
254            &mut comp.control.borrow_mut(),
255            self.state_shareable.clone(),
256            self.shareable.clone(),
257            only_run_once,
258        );
259
260        self.par_timing_map = StaticParTiming::new(
261            &mut comp.control.borrow_mut(),
262            comp.name,
263            &self.live,
264        );
265        if let Some(stream) = &mut self.print_par_timing {
266            write!(stream.get_write(), "{:?}", self.par_timing_map).unwrap();
267        }
268    }
269
270    fn cell_filter(&self, cell: &ir::Cell) -> bool {
271        // Cells used in continuous assignments cannot be shared, nor can ref cells.
272        if self.cont_ref_cells.contains(&cell.name()) {
273            return false;
274        }
275        // Cells that have @protected cannot be shared (even if they have share/state_share attributes)
276        if cell.attributes.has(BoolAttr::Protected) {
277            return false;
278        }
279        if let Some(ref name) = cell.type_name() {
280            self.state_shareable.contains(name) || self.shareable.contains(name)
281        } else {
282            false
283        }
284    }
285
286    // prints the json if self.print_share_freqs is not None
287    fn print_share_json(&mut self) {
288        if let Some(file) = &mut self.print_share_freqs {
289            let printable_share_freqs: HashMap<String, HashMap<String, _>> =
290                self.share_freqs
291                    .iter()
292                    .map(|(id, freq_map)| {
293                        (
294                            id.to_string(),
295                            freq_map
296                                .iter()
297                                .map(|(cell_type, pdf)| {
298                                    (cell_type_to_string(cell_type), pdf)
299                                })
300                                .collect(),
301                        )
302                    })
303                    .collect();
304            let json_share_freqs: Value = json!(printable_share_freqs);
305            write!(file.get_write(), "{json_share_freqs}").unwrap()
306        }
307    }
308}
309
310/// The algorithm that runs is:
311///  - instantiate conflict graph using all component cells that satisfy `cell_filter`
312///  - use [ScheduleConflicts] to find groups/invokes that run in parallel with each other
313///  - for each tuple combination of cells that return true on cell_filter(), c1 and c2
314///  - first determine if their live ranges overlap. If so, then insert a conflict between
315///    c1 and c2
316///  - if c1 and c2 don't have overlapping live ranges, check if c1 and c2 are ever
317///    live at within the same par block, and they are live at different children
318///    of the par block. If the parent par is not static, then add a conflict.
319///    If the parent par is static, then we can use the static_par_timing analysis
320///    to check whether the cells' liveness actually overlaps.
321///  - perform graph coloring using `self.ordering` to define the order of the greedy coloring
322///  - use coloring to rewrite group assignments, continuous assignments, and conditional ports.
323impl Visitor for CellShare {
324    fn start(
325        &mut self,
326        comp: &mut ir::Component,
327        sigs: &ir::LibrarySignatures,
328        _comps: &[ir::Component],
329    ) -> VisResult {
330        self.initialize(comp, sigs);
331
332        let cells = comp.cells.iter().filter(|c| self.cell_filter(&c.borrow()));
333
334        // Mapping from cell type to names of all cells of that type.
335        let mut cells_by_type: HashMap<ir::CellType, Vec<ir::Id>> =
336            HashMap::new();
337        for cell in cells {
338            cells_by_type
339                .entry(cell.borrow().prototype.clone())
340                .or_default()
341                .push(cell.borrow().name());
342        }
343
344        // List of cell maps
345        let mut cell_map_list = Vec::new();
346
347        // Maps cell type to conflict graph (will be used to perform coloring)
348        let mut graphs_by_type: HashMap<ir::CellType, GraphColoring<ir::Id>> =
349            cells_by_type
350                .clone()
351                .into_iter()
352                .map(|(key, cell_names)| {
353                    (key, GraphColoring::from(cell_names.into_iter()))
354                })
355                .collect();
356
357        // We assume unique ids have already been computed by LiveRangeAnalysis
358
359        // live_once_map maps celltypes to maps that map cells to control statements
360        // in which the cell was live for at least one group/invoke. Furthermore,
361        // only control statements that are direct children of par blocks
362        // are included in this map.
363        let mut live_once_map = HashMap::new();
364        // Maps every control statement that is a direct child of a par block to
365        // its parent par block. (maps id number to id number)
366        let mut par_thread_map = HashMap::new();
367        // Maps celltype to map that maps cells to groups/invokes in which the cell is live.
368        let mut live_cell_map = HashMap::new();
369        // build live_once_map and par_thread_map
370        self.live.get_live_control_data(
371            &mut live_once_map,
372            &mut par_thread_map,
373            &mut live_cell_map,
374            &HashSet::new(),
375            &comp.control.borrow(),
376        );
377
378        // Adding the conflicts
379        for (cell_type, cells) in &cells_by_type {
380            // Run remove_dead_cells before this cell-share pass.
381            let g = graphs_by_type.get_mut(cell_type).unwrap();
382
383            // mapping from cell names to the enables/invokes in which it is live
384            let cell_to_nodes =
385                live_cell_map.entry(cell_type.clone()).or_default();
386            // mapping of cell names to the control statements in which it is live
387            // at least once. Only control statements that are direct children of
388            // par blocks are included
389            let cell_to_control =
390                live_once_map.entry(cell_type.clone()).or_default();
391            for (a, b) in cells.iter().tuple_combinations() {
392                // checking if live ranges overlap
393                // nodes (groups/invokes) in which a is live
394                if let Some(live_a) = cell_to_nodes.get(a) {
395                    if let Some(live_b) = cell_to_nodes.get(b) {
396                        if !live_a.is_disjoint(live_b) {
397                            g.insert_conflict(a, b);
398                            continue;
399                        }
400                    }
401                }
402                // checking if b is live at any groups/invokes running in parallel
403                // to groups/invokes live at a
404                // get the children of pars in which a was alive "at least once"
405                if let Some(live_once_a) = cell_to_control.get(a) {
406                    // get the children of pars in which b was alive "at least once"
407                    if let Some(live_once_b) = cell_to_control.get(b) {
408                        'outer: for live_a in live_once_a {
409                            for live_b in live_once_b {
410                                // a and b are live within the same par block but not within
411                                // the same child thread, then insert conflict.
412                                let parent_a =
413                                    par_thread_map.get(live_a).unwrap();
414                                let parent_b =
415                                    par_thread_map.get(live_b).unwrap();
416                                if live_a != live_b && parent_a == parent_b {
417                                    // We have to check par_timing_map
418                                    // to see whether liveness overlaps.
419                                    // For dynamic pars, liveness_overlaps() returns
420                                    // true no matter what.
421                                    if self.par_timing_map.liveness_overlaps(
422                                        parent_a, live_a, live_b, a, b,
423                                    ) {
424                                        g.insert_conflict(a, b);
425                                        break 'outer;
426                                    }
427                                }
428                            }
429                        }
430                    }
431                }
432            }
433        }
434
435        // perform graph coloring to rename the cells
436        let mut coloring: rewriter::RewriteMap<ir::Cell> = HashMap::new();
437        let mut comp_share_freqs: HashMap<ir::CellType, HashMap<i64, i64>> =
438            HashMap::new();
439        let [comb_bound, reg_bound, other_bound] = &self.bounds;
440        for (cell_type, mut graph) in graphs_by_type {
441            // getting bound, based on self.bounds and cell_type
442            let bound = {
443                if let Some(ref name) = cell_type.get_name() {
444                    let is_comb = self.shareable.contains(name);
445                    let is_reg = name == "std_reg";
446                    // if self.calyx_2020, then set bounds based on that
447                    // otherwise, look at the actual self.bounds values to
448                    // get the bounds
449                    if self.calyx_2020 {
450                        if is_comb || is_reg { &None } else { &Some(1) }
451                    } else if is_comb {
452                        comb_bound
453                    } else if is_reg {
454                        reg_bound
455                    } else {
456                        other_bound
457                    }
458                } else {
459                    // sharing bound doesn't really matter for ThisComponent/Constants
460                    &None
461                }
462            };
463            if graph.has_nodes() {
464                coloring.extend(
465                    graph
466                        .color_greedy(*bound, false)
467                        .iter()
468                        .map(|(a, b)| (*a, comp.find_cell(*b).unwrap())),
469                );
470                // only generate share-freqs if we're going to use them.
471                if self.print_share_freqs.is_some() {
472                    // must accumulate sharing numbers for share_freqs
473                    comp_share_freqs.insert(cell_type, graph.get_share_freqs());
474                }
475            }
476        }
477
478        // add the sharing freqs for the component we just analyzed
479        if self.print_share_freqs.is_some() {
480            // must accumulate sharing numbers for share_freqs
481            self.share_freqs.insert(comp.name, comp_share_freqs);
482            // print share freqs json if self.print_share_freqs is not none
483            self.print_share_json();
484        }
485
486        for (id, cell_ref) in coloring.iter() {
487            cell_map_list.push(ShareEntry {
488                original: *id,
489                new: cell_ref.borrow().name(),
490                cell_type: cell_type_to_string(&cell_ref.borrow().prototype),
491            });
492        }
493        // sort entries by original cell name to avoid output nondeterminism
494        cell_map_list.sort_by(|c1, c2| c1.original.cmp(&c2.original));
495        self.shared_cells
496            .insert(comp.name.to_string(), cell_map_list);
497
498        // Rewrite assignments using the coloring generated.
499        let rewriter = ir::Rewriter {
500            cell_map: coloring,
501            ..Default::default()
502        };
503        comp.for_each_assignment(|assign| {
504            assign.for_each_port(|port| rewriter.get(port));
505        });
506        comp.for_each_static_assignment(|assign| {
507            assign.for_each_port(|port| rewriter.get(port));
508        });
509
510        // Rewrite control uses of ports
511        rewriter.rewrite_control(&mut comp.control.borrow_mut());
512
513        Ok(Action::Stop)
514    }
515
516    /// If requested, emit share map json after all components are processed
517    fn finish_context(&mut self, _ctx: &mut calyx_ir::Context) -> VisResult {
518        if let Some(json_out_file) = &mut self.emit_share_map {
519            let _ = serde_json::to_writer_pretty(
520                json_out_file.get_write(),
521                &self.shared_cells,
522            );
523        }
524        Ok(Action::Stop)
525    }
526}