Skip to main content

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}