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}