logicaffeine_compile/codegen_sva/fusion.rs
1//! **Operand sharing** — reader fusion on the RDU, decided from the cost model.
2//!
3//! Unroll a tiled GEMM `par` ways and every instance wants the same operand panels. There are two ways to
4//! feed them and the compiler must pick one:
5//!
6//! * **Replicated** — each instance streams its own copy from DRAM. `consumers` times the memory traffic,
7//! nothing on the fabric.
8//! * **Broadcast** — one instance streams it and fans the panel out to the others. One copy from memory, plus
9//! a fan-out that occupies the vector network for as long as the data takes to cross it.
10//!
11//! Which is cheaper is a property of the shape, the mesh and the memory, never a constant: a large panel
12//! shared by many instances over a short hop is obviously worth broadcasting, and a small one over a long hop
13//! obviously is not, and the interesting cases are in between. So [`share_operand`] compares the DRAM cycles
14//! a broadcast would save against the fabric cycles it would add, and returns the cheaper arrangement with
15//! both numbers attached — the same shape of decision the Tensix lane makes for reader fusion, and with the
16//! same discipline: the function takes the machine and the shape and nothing else, so there is no way to tell
17//! the compiler the answer. `plasticine_fusion_lock` holds that both outcomes are reachable by moving only
18//! the costs, and that the roofline moves exactly the bytes the decision chose.
19
20use super::plasticine::PlasticineTarget;
21
22/// How an operand reaches the instances that want it.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
24pub enum Sharing {
25 /// Every consumer streams its own copy from memory.
26 Replicated,
27 /// One consumer streams it and fans it out across the fabric.
28 Broadcast,
29}
30
31impl Sharing {
32 pub fn label(&self) -> &'static str {
33 match self {
34 Sharing::Replicated => "replicated",
35 Sharing::Broadcast => "broadcast",
36 }
37 }
38}
39
40/// One operand panel and the instances that want it.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct OperandStream {
43 pub name: String,
44 /// Bytes in one copy of the panel.
45 pub bytes: usize,
46 /// Instances that need it.
47 pub consumers: usize,
48 /// Mesh hops a fan-out would cross to reach them.
49 pub broadcast_hops: usize,
50}
51
52/// The decision, with the two costs that produced it.
53#[derive(Clone, Debug, PartialEq, serde::Serialize)]
54pub struct SharingDecision {
55 pub sharing: Sharing,
56 /// DRAM cycles broadcasting would save: the `consumers - 1` copies it does not fetch.
57 pub dram_cycles_saved: usize,
58 /// Fabric cycles it would add: the panel crossing `broadcast_hops` links, one word per link per cycle.
59 pub fabric_cycles_added: usize,
60 /// Copies actually fetched from memory under the chosen arrangement.
61 pub copies: usize,
62 pub why: String,
63}
64
65/// Cycles to move `bytes` across `t`'s memory channels.
66fn dram_cycles(bytes: usize, t: &PlasticineTarget) -> usize {
67 (bytes as f64 * t.clock_ghz / t.dram.gbps).ceil() as usize
68}
69
70/// Choose how `stream` reaches its consumers on `t`. Takes the machine and the shape, and nothing else.
71pub fn share_operand(stream: &OperandStream, t: &PlasticineTarget) -> SharingDecision {
72 let consumers = stream.consumers.max(1);
73 // What broadcasting saves: the copies it does not fetch.
74 let saved = dram_cycles(stream.bytes * (consumers - 1), t);
75 // What it costs: the panel crossing the fan-out. The links are REGISTERED, so the panel streams — the
76 // first word arrives after one cycle per hop, and every word after it follows one cycle behind. `W`
77 // words over `H` hops therefore occupy the net for `W + H - 1` cycles, the fill plus the stream, not
78 // `W * H`.
79 //
80 // This read `words * broadcast_hops`, which is the store-and-forward cost: every word paying every hop
81 // in turn, as if each switch buffered the whole panel before passing it on. That over-charges a
82 // broadcast by about a factor of the distance and biases this decision toward replication — on a
83 // 640-cubed A panel it charged ~256,000 cycles where the honest figure is ~25,600, which was enough to
84 // flip the wide-mesh preset into fetching one shared operand up to 80 times from DRAM.
85 //
86 // `choreography.rs` already models the fabric this way (`forward_latency = stage.latency + hops`), and
87 // the credit-depth solve's two-sided tightness proof rests on it. Two passes in one compiler cannot
88 // disagree about whether a fabric pipelines.
89 let word_bytes = 4 * t.pcu_lanes.max(1);
90 let words = stream.bytes.div_ceil(word_bytes.max(1));
91 let added = words + stream.broadcast_hops.saturating_sub(1);
92 let broadcast = consumers > 1 && saved > added;
93 let sharing = if broadcast {
94 Sharing::Broadcast
95 } else {
96 Sharing::Replicated
97 };
98 let why = if consumers <= 1 {
99 "one consumer: nothing to share".to_string()
100 } else if broadcast {
101 format!("broadcasting saves {saved} DRAM cycles and costs {added} on the fabric")
102 } else {
103 format!("a fan-out would cost {added} fabric cycles to save {saved} DRAM cycles")
104 };
105 SharingDecision {
106 sharing,
107 dram_cycles_saved: saved,
108 fabric_cycles_added: added,
109 copies: if broadcast { 1 } else { consumers },
110 why,
111 }
112}