metrale_circuit/
planner.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-28: The buffer planner: an arena offset for every materialised edge of a plan,
4//! sized once for the widest padded row count, so no buffer is reallocated after boot (the
5//! CUDA-graph constraint: captured pointers must stay valid).
6//!
7//! An executor may add a [`Layout`]: edges it binds to buffers of its own, outputs a kernel
8//! writes in place over one of its inputs, and edges a kernel writes back to back.
9//!
10//! Owner: metrale-circuit.
11//! Invariants:
12//! - Two edges whose live ranges overlap never share a byte, unless a [`Layout`] alias joins
13//!   them; the edges of one alias class share their bytes by construction.
14//! - An edge is live from the group that writes it to the last group that reads it; an edge
15//!   read or written outside the plan (its inputs from another section, and the declared
16//!   outputs) is live for the whole plan. An alias class is live over the union of its edges.
17//! - The edges of a pack are placed back to back, in pack order, with no padding between.
18//! - The edges of a row pack share every row: row `i` of member `j` sits at the pack's base
19//!   plus `i` pack rows plus the widths of members `0..j`; each member's `row_stride` is the
20//!   pack row. A row pack placed `over` an edge occupies exactly that edge's bytes.
21//! - Offsets of unpacked storage are aligned to [`ALIGN`] bytes; a pack's first edge is.
22
23use std::collections::{BTreeMap, BTreeSet};
24
25use crate::fuser::{EdgeState, FusionPlan};
26use crate::ir::{Circuit, EdgeIdx};
27
28/// 2026-09-28: Offset alignment, in bytes.
29pub const ALIGN: u64 = 256;
30
31/// 2026-09-28: One placed edge.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Slot {
34    /// 2026-09-28: The edge.
35    pub edge: EdgeIdx,
36    /// 2026-09-28: Byte offset in the arena.
37    pub offset: u64,
38    /// 2026-09-28: Bytes at the sized row count.
39    pub bytes: u64,
40    /// 2026-09-28: First group index it is live in.
41    pub start: usize,
42    /// 2026-09-28: Last group index it is live in.
43    pub end: usize,
44    /// 2026-09-28: Bytes from one row of the edge to the next: its own row width, or the
45    /// whole row of an interleaved pack it belongs to.
46    pub row_stride: u64,
47}
48
49/// 2026-09-28: The arena layout of one plan.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct BufferPlan {
52    /// 2026-09-28: Placed edges, by edge index.
53    pub slots: Vec<Slot>,
54    /// 2026-09-28: Arena size in bytes.
55    pub arena_bytes: u64,
56    /// 2026-09-28: Sum of the materialised edges' sizes, before reuse.
57    pub materialized_bytes: u64,
58}
59
60/// 2026-09-28: Why a plan could not be laid out.
61#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
62pub enum PlanError {
63    /// 2026-09-28: A row or byte count overflowed, or a dim is not a multiple of a scale group.
64    #[error("edge `{0}` has no byte size at the sized row count")]
65    Size(String),
66    /// 2026-09-28: A [`Layout`] constraint the plan cannot honour.
67    #[error("layout: {0}")]
68    Layout(String),
69}
70
71/// 2026-09-28: Storage constraints an executor puts on a plan's materialised edges.
72#[derive(Debug, Clone, Default, PartialEq, Eq)]
73pub struct Layout {
74    /// 2026-09-28: Edges bound to the executor's own buffers; the planner places none of them.
75    pub external: BTreeSet<EdgeIdx>,
76    /// 2026-09-28: `(output, input)`: a kernel writes `output` in place over `input`, so the
77    /// two share storage. They must have the same size.
78    pub aliases: Vec<(EdgeIdx, EdgeIdx)>,
79    /// 2026-09-28: Edges a kernel writes back to back, in this order, from one pointer.
80    pub packs: Vec<Vec<EdgeIdx>>,
81    /// 2026-09-28: Edges a kernel writes side by side within each row.
82    pub row_packs: Vec<RowPack>,
83}
84
85/// 2026-09-28: Edges laid side by side within each row: row `i` holds row `i` of every member,
86/// in order. The members must have one row count. With `over`, the pack occupies that edge's
87/// bytes (a kernel rewrites the edge in place into the members), which must be exactly the
88/// pack's size.
89#[derive(Debug, Clone, Default, PartialEq, Eq)]
90pub struct RowPack {
91    /// 2026-09-28: The members, in row order.
92    pub members: Vec<EdgeIdx>,
93    /// 2026-09-28: The edge the pack is laid over, if any.
94    pub over: Option<EdgeIdx>,
95}
96
97/// 2026-09-28: Live range of every materialised edge in `plan`, keyed by edge.
98pub fn live_ranges(circuit: &Circuit, plan: &FusionPlan) -> BTreeMap<EdgeIdx, (usize, usize)> {
99    let mut group_of = vec![None; circuit.nodes.len()];
100    for (g, grp) in plan.groups.iter().enumerate() {
101        for &n in &grp.nodes {
102            group_of[n] = Some(g);
103        }
104    }
105    let last = plan.groups.len().saturating_sub(1);
106    let mut out = BTreeMap::new();
107    for (e, edge) in circuit.edges.iter().enumerate() {
108        if plan.edge_states[e] != Some(EdgeState::Materialized) {
109            continue;
110        }
111        let start = edge.producer.and_then(|p| group_of[p]).unwrap_or(0);
112        let end = if edge.is_output {
113            last
114        } else {
115            edge.consumers
116                .iter()
117                .filter_map(|&c| group_of[c])
118                .max()
119                .unwrap_or(last)
120                .max(start)
121        };
122        out.insert(e, (start, end));
123    }
124    out
125}
126
127/// 2026-09-28: Place every materialised edge of `plan`, sized for `max_rows` rows, with no
128/// layout constraints.
129pub fn plan_buffers(
130    circuit: &Circuit,
131    plan: &FusionPlan,
132    max_rows: u64,
133) -> Result<BufferPlan, PlanError> {
134    plan_buffers_with(circuit, plan, max_rows, &Layout::default())
135}
136
137/// 2026-09-28: One unit of placement: alias classes laid back to back.
138struct Item {
139    /// 2026-09-28: `(class root, offset within the item)`.
140    classes: Vec<(EdgeIdx, u64)>,
141    bytes: u64,
142    start: usize,
143    end: usize,
144    /// 2026-09-28: Lowest edge index in the item: the deterministic tie-break.
145    key: EdgeIdx,
146}
147
148fn root(parent: &mut BTreeMap<EdgeIdx, EdgeIdx>, e: EdgeIdx) -> EdgeIdx {
149    let mut r = e;
150    while parent[&r] != r {
151        r = parent[&r];
152    }
153    parent.insert(e, r);
154    r
155}
156
157/// 2026-09-28: Place every materialised edge of `plan` that `layout` does not bind, sized for
158/// `max_rows` rows.
159///
160/// Aliased edges form one class; packs lay classes back to back into one item; every other
161/// class is an item of its own. Items are placed first fit, sorted by (start, larger first,
162/// lowest edge): each takes the lowest aligned offset that overlaps no placed item whose live
163/// range meets its own.
164pub fn plan_buffers_with(
165    circuit: &Circuit,
166    plan: &FusionPlan,
167    max_rows: u64,
168    layout: &Layout,
169) -> Result<BufferPlan, PlanError> {
170    let mut dims = circuit.dims.clone();
171    dims.insert("n".into(), max_rows);
172    let ranges = live_ranges(circuit, plan);
173    let id = |e: EdgeIdx| circuit.edges.get(e).map_or("?", |x| x.id.as_str());
174    let layout_err = |msg: String| PlanError::Layout(msg);
175    for &e in layout
176        .external
177        .iter()
178        .chain(layout.aliases.iter().flat_map(|(a, b)| [a, b]))
179        .chain(layout.packs.iter().flatten())
180        .chain(
181            layout
182                .row_packs
183                .iter()
184                .flat_map(|p| p.members.iter().chain(&p.over)),
185        )
186    {
187        if !ranges.contains_key(&e) {
188            return Err(layout_err(format!("edge `{}` is not materialised", id(e))));
189        }
190    }
191    let mut size = BTreeMap::new();
192    let mut rows_of = BTreeMap::new();
193    let mut materialized_bytes = 0u64;
194    for &e in ranges.keys() {
195        let edge = &circuit.edges[e];
196        let size_err = || PlanError::Size(edge.id.clone());
197        let rows = edge.rows.eval(&dims).map_err(|_| size_err())?;
198        let bytes = plan.edge_formats[e]
199            .bytes(rows, edge.dim_value)
200            .ok_or_else(size_err)?;
201        materialized_bytes = materialized_bytes.checked_add(bytes).ok_or_else(size_err)?;
202        if !layout.external.contains(&e) {
203            size.insert(e, bytes);
204            rows_of.insert(e, rows.max(1));
205        }
206    }
207    let mut parent: BTreeMap<EdgeIdx, EdgeIdx> = size.keys().map(|&e| (e, e)).collect();
208    for &(out, inp) in &layout.aliases {
209        if layout.external.contains(&out) || layout.external.contains(&inp) {
210            return Err(layout_err(format!(
211                "alias `{}` over `{}` names an external edge",
212                id(out),
213                id(inp)
214            )));
215        }
216        if size[&out] != size[&inp] {
217            return Err(layout_err(format!(
218                "alias `{}` ({} B) over `{}` ({} B): sizes differ",
219                id(out),
220                size[&out],
221                id(inp),
222                size[&inp]
223            )));
224        }
225        let (a, b) = (root(&mut parent, out), root(&mut parent, inp));
226        parent.insert(a.max(b), a.min(b));
227    }
228    let edges: Vec<EdgeIdx> = size.keys().copied().collect();
229    let mut class_range: BTreeMap<EdgeIdx, (usize, usize, EdgeIdx)> = BTreeMap::new();
230    for &e in &edges {
231        let r = root(&mut parent, e);
232        let (s, t) = ranges[&e];
233        let c = class_range.entry(r).or_insert((s, t, e));
234        *c = (c.0.min(s), c.1.max(t), c.2.min(e));
235    }
236    let mut packed = BTreeSet::new();
237    let mut items = Vec::new();
238    let mut stride_of: BTreeMap<EdgeIdx, u64> = BTreeMap::new();
239    for pack in &layout.row_packs {
240        let rows = pack
241            .members
242            .first()
243            .map_or(0, |e| rows_of.get(e).copied().unwrap_or(0));
244        let mut item = Item {
245            classes: Vec::new(),
246            bytes: 0,
247            start: usize::MAX,
248            end: 0,
249            key: EdgeIdx::MAX,
250        };
251        let mut row_bytes = 0u64;
252        for &e in pack.members.iter().chain(&pack.over) {
253            if layout.external.contains(&e) {
254                return Err(layout_err(format!(
255                    "row pack names external edge `{}`",
256                    id(e)
257                )));
258            }
259            if rows_of[&e] != rows {
260                return Err(layout_err(format!(
261                    "row-packed edge `{}` has another row count",
262                    id(e)
263                )));
264            }
265            let r = root(&mut parent, e);
266            if !packed.insert(r) {
267                return Err(layout_err(format!("edge `{}` is packed twice", id(e))));
268            }
269            let (s, t, k) = class_range[&r];
270            let within = if Some(e) == pack.over { 0 } else { row_bytes };
271            item.classes.push((r, within));
272            if Some(e) != pack.over {
273                row_bytes += size[&e] / rows;
274            }
275            item.start = item.start.min(s);
276            item.end = item.end.max(t);
277            item.key = item.key.min(k);
278        }
279        item.bytes = row_bytes * rows;
280        if let Some(o) = pack.over
281            && size[&o] != item.bytes
282        {
283            return Err(layout_err(format!(
284                "row pack over `{}` ({} B) holds {} B",
285                id(o),
286                size[&o],
287                item.bytes
288            )));
289        }
290        for &(r, _) in &item.classes {
291            stride_of.insert(r, row_bytes);
292        }
293        items.push(item);
294    }
295    for pack in &layout.packs {
296        let mut item = Item {
297            classes: Vec::new(),
298            bytes: 0,
299            start: usize::MAX,
300            end: 0,
301            key: EdgeIdx::MAX,
302        };
303        for &e in pack {
304            if layout.external.contains(&e) {
305                return Err(layout_err(format!("pack names external edge `{}`", id(e))));
306            }
307            let r = root(&mut parent, e);
308            if !packed.insert(r) {
309                return Err(layout_err(format!("edge `{}` is packed twice", id(e))));
310            }
311            let (s, t, k) = class_range[&r];
312            item.classes.push((r, item.bytes));
313            item.bytes += size[&e];
314            item.start = item.start.min(s);
315            item.end = item.end.max(t);
316            item.key = item.key.min(k);
317        }
318        items.push(item);
319    }
320    for (&r, &(s, t, k)) in &class_range {
321        if !packed.contains(&r) {
322            items.push(Item {
323                classes: vec![(r, 0)],
324                bytes: size[&r],
325                start: s,
326                end: t,
327                key: k,
328            });
329        }
330    }
331    items.sort_by(|a, b| {
332        a.start
333            .cmp(&b.start)
334            .then(b.bytes.cmp(&a.bytes))
335            .then(a.key.cmp(&b.key))
336    });
337    let mut placed: Vec<(u64, u64, usize, usize)> = Vec::with_capacity(items.len());
338    let mut class_at: BTreeMap<EdgeIdx, (u64, usize, usize)> = BTreeMap::new();
339    let mut arena_bytes = 0u64;
340    for item in &items {
341        let mut busy: Vec<(u64, u64)> = placed
342            .iter()
343            .filter(|p| p.2 <= item.end && item.start <= p.3)
344            .map(|p| (p.0, p.1))
345            .collect();
346        busy.sort_unstable();
347        let mut offset = 0u64;
348        for (lo, hi) in busy {
349            if offset + item.bytes <= lo {
350                break;
351            }
352            offset = offset.max(hi.div_ceil(ALIGN) * ALIGN);
353        }
354        arena_bytes = arena_bytes.max(offset + item.bytes);
355        placed.push((offset, offset + item.bytes, item.start, item.end));
356        for &(r, within) in &item.classes {
357            class_at.insert(r, (offset + within, item.start, item.end));
358        }
359    }
360    let mut slots = Vec::with_capacity(edges.len());
361    for e in edges {
362        let r = root(&mut parent, e);
363        let (offset, start, end) = class_at[&r];
364        slots.push(Slot {
365            edge: e,
366            offset,
367            bytes: size[&e],
368            start,
369            end,
370            row_stride: stride_of.get(&r).copied().unwrap_or(size[&e] / rows_of[&e]),
371        });
372    }
373    Ok(BufferPlan {
374        slots,
375        arena_bytes,
376        materialized_bytes,
377    })
378}
379
380#[cfg(test)]
381#[path = "planner_tests.rs"]
382mod planner_tests;