Skip to main content

logicaffeine_compile/codegen_sva/
plasticine_place.rs

1//! **Mesh placement + static routing** for a Plasticine configuration — P3 of the Plasticine lane.
2//!
3//! Given a [`Metapipeline`] (stages with their datapath depth and cell count) and the words each inter-stage
4//! buffer must hold, this allocates PCUs and PMUs, places them on the checkerboard, routes every edge with
5//! dimension-order (X then Y) static routing, and reports what an RDU engineer asks for: PCU/PMU used vs
6//! total, FU utilization, hop counts, per-link occupancy against the switch's link budget, and whether the
7//! static routing fit. Every stage's datapath is still the SAT-certified placement of
8//! [`FabricTarget::plasticine_shaped`](crate::codegen_sva::fabric_target::FabricTarget::plasticine_shaped) (P0) — this module only decides WHERE on the mesh and HOW MANY.
9//!
10//! Model (ISCA'17 §3.1–3.3, §3.6 "hierarchical binding"; Spade defaults for link counts): a scalar body of
11//! critical depth `D` and `C` cells chains `ceil(max(D, C) / pcu_stages)` PCUs (a lane owns one FU per
12//! stage: depth needs stages, cells need slots), each PCU running `pcu_lanes` iterations in parallel; a buffer written in stage `i` and read in stage `j` is `j-i+1`-deep N-buffered inside one PMU
13//! when it fits `words_per_pmu / depth`, else spans `ceil(words / (words_per_pmu / depth))` PMUs; the
14//! checkerboard alternates PCU/PMU cells; routes follow X-Y order and each DIRECTED mesh link carries at most
15//! `vector_links` vector routes; a stage that takes no PCU (a perimeter address generator) enters the mesh at
16//! the edge switch of its neighbour's row, one extra hop. MODELED; never a Plasticine simulation.
17//!
18//! [`FabricTarget::plasticine_shaped`]: crate::codegen_sva::fabric_target::FabricTarget::plasticine_shaped
19
20use super::fabric_target::{Capability, Unplaceable};
21use super::metapipeline::Metapipeline;
22use super::plasticine::PlasticineTarget;
23
24/// Switch/link budget per mesh segment (Spade `SpadeConfig` defaults: `vlink 2`, `slink 4`, `vc 4`).
25#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
26pub struct LinkBudget {
27    pub vector_links: usize,
28    pub scalar_links: usize,
29}
30
31impl LinkBudget {
32    pub fn spade_default() -> LinkBudget {
33        LinkBudget {
34            vector_links: 2,
35            scalar_links: 4,
36        }
37    }
38}
39
40/// A net endpoint on the mesh.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum Endpoint {
43    /// A unit on cell `(row, col)`.
44    Cell(usize, usize),
45    /// The DRAM side (address generators behind the perimeter switches).
46    Dram,
47}
48
49/// One vector net to route.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct MeshNet {
52    pub from: Endpoint,
53    pub to: Endpoint,
54}
55
56/// A mesh link `((r,c),(r',c'))`; a link whose ends coincide is a perimeter port of that edge cell.
57pub type Link = ((usize, usize), (usize, usize));
58
59/// The vector nets of one placement, routed. For every net, in order: the mesh links it occupies, and its
60/// occupancy keys — `(link, 0)` for a channel of a directed link, `((cell, cell), 1)` for a perimeter port
61/// carrying DRAM → mesh and `((cell, cell), 2)` for mesh → DRAM — so that the largest count over keys of a
62/// legal routing never exceeds the channel budget.
63pub struct MeshRouting {
64    pub links: Vec<Vec<Link>>,
65    pub occupancy: Vec<Vec<(Link, u8)>>,
66    /// Every net connected and no resource shared, by a check independent of the router.
67    pub legal: bool,
68    /// Which router (and rung) produced the routes, as a [`Placement`] reports it.
69    pub router: &'static str,
70}
71
72/// A router for the mesh's vector nets. [`place_unrolled`] places with row-balanced cells and routes through
73/// the installed router ([`place_routed`]), or falls back to [`place`]'s xy-static routes when none is.
74pub trait MeshRouter: Send + Sync {
75    fn route(&self, t: &PlasticineTarget, budget: LinkBudget, nets: &[MeshNet]) -> MeshRouting;
76}
77
78static MESH_ROUTER: std::sync::OnceLock<&'static dyn MeshRouter> = std::sync::OnceLock::new();
79
80/// Install this process's mesh router, once; `Err` hands back the argument when one is already installed.
81/// Certified place-and-route installs its congestion-negotiating router this way, from its own side: the
82/// compiler never depends on it, so the published tree builds without it.
83pub fn install_mesh_router(router: &'static dyn MeshRouter) -> Result<(), &'static dyn MeshRouter> {
84    MESH_ROUTER.set(router)
85}
86
87/// The installed mesh router, if any.
88pub fn mesh_router() -> Option<&'static dyn MeshRouter> {
89    MESH_ROUTER.get().copied()
90}
91
92/// PCUs one stage needs.
93#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
94pub struct StageAllocation {
95    pub stage: usize,
96    pub pcus: usize,
97    /// FU stage-slots used in the LAST PCU of the chain (the others are full).
98    pub stages_in_last: usize,
99    /// Cells the stage places (each cell occupies one FU stage across `lanes` lanes).
100    pub cells: usize,
101}
102
103/// PMUs one buffer edge needs.
104#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
105pub struct BufferAllocation {
106    pub edge: (usize, usize),
107    /// N-buffer depth (Spatial's writer→reader rule).
108    pub depth: usize,
109    pub words: usize,
110    pub pmus: usize,
111}
112
113#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
114pub struct Allocation {
115    pub stages: Vec<StageAllocation>,
116    pub buffers: Vec<BufferAllocation>,
117}
118
119impl Allocation {
120    pub fn pcus(&self) -> usize {
121        self.stages.iter().map(|s| s.pcus).sum()
122    }
123    pub fn pmus(&self) -> usize {
124        self.buffers.iter().map(|b| b.pmus).sum()
125    }
126}
127
128/// 32-bit words one PMU holds (ISCA'17: 16 banks × 16 KB = 256 KB = 65,536 words).
129pub fn words_per_pmu(t: &PlasticineTarget) -> usize {
130    t.pmu_kib * 1024 / 4
131}
132
133/// PMUs a `depth`-deep N-buffer of `words` needs on `t`: a PMU's address space is partitioned `depth` ways
134/// (Spatial's N-buffering), so one PMU holds `words_per_pmu / depth` words per buffer; an empty tile still
135/// occupies one PMU (the buffer exists).
136pub fn pmus_for(words: usize, depth: usize, t: &PlasticineTarget) -> usize {
137    let per_buffer = (words_per_pmu(t) / depth.max(1)).max(1);
138    if words == 0 {
139        1
140    } else {
141        (words + per_buffer - 1) / per_buffer
142    }
143}
144
145/// Allocate PCUs/PMUs for `mp` on `t`; `buffer_words[i]` is the tile size flowing over `mp.edges[i]`. Every
146/// stage is hosted on PCUs.
147pub fn allocate(
148    mp: &Metapipeline,
149    buffer_words: &[usize],
150    t: &PlasticineTarget,
151) -> Result<Allocation, Unplaceable> {
152    allocate_hosted(mp, buffer_words, t, &[])
153}
154
155/// [`allocate`] with the stages listed in `ag_stages` hosted on the PERIMETER ADDRESS GENERATORS (ISCA'17
156/// §3.4: the AGs at the chip's edges drive the DRAM tile loads and stores) rather than on PCUs: such a stage
157/// takes no PCU and places no FU stage; [`place`] routes it from the mesh edge.
158pub fn allocate_hosted(
159    mp: &Metapipeline,
160    buffer_words: &[usize],
161    t: &PlasticineTarget,
162    ag_stages: &[usize],
163) -> Result<Allocation, Unplaceable> {
164    assert_eq!(buffer_words.len(), mp.edges.len(), "one tile size per edge");
165    mp.fits(t)?;
166    let per_pcu = t.pcu_stages.max(1);
167    let stages = mp
168        .stages
169        .iter()
170        .enumerate()
171        .map(|(i, s)| {
172            if ag_stages.contains(&i) {
173                return StageAllocation {
174                    stage: i,
175                    pcus: 0,
176                    stages_in_last: 0,
177                    cells: 0,
178                };
179            }
180            // A lane owns one FU per PCU stage, so a scalar body needs an FU SLOT per cell as well as a stage per
181            // level of depth: the chain is sized by whichever is larger (a wide, shallow body still fills slots).
182            let slots = s.latency.max(s.cells).max(1);
183            let pcus = (slots + per_pcu - 1) / per_pcu;
184            StageAllocation {
185                stage: i,
186                pcus,
187                stages_in_last: slots - (pcus - 1) * per_pcu,
188                cells: s.cells,
189            }
190        })
191        .collect();
192    let mut buffers = Vec::new();
193    for (k, &(w, r)) in mp.edges.iter().enumerate() {
194        let depth = Metapipeline::buffer_depth(w, r);
195        let words = buffer_words[k];
196        buffers.push(BufferAllocation {
197            edge: (w, r),
198            depth,
199            words,
200            pmus: pmus_for(words, depth, t),
201        });
202    }
203    let alloc = Allocation { stages, buffers };
204    if alloc.pcus() > t.pcus() || alloc.pmus() > t.pmus() {
205        return Err(Unplaceable::TooSmall {
206            needed_rows: alloc.pcus(),
207            needed_cols: alloc.pmus(),
208            target_rows: t.pcus(),
209            target_cols: t.pmus(),
210            target: t.name,
211        });
212    }
213    Ok(alloc)
214}
215
216/// A placed unit on the checkerboard.
217#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
218pub enum Unit {
219    Pcu { stage: usize, index: usize },
220    Pmu { edge: (usize, usize), index: usize },
221}
222
223#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
224pub struct Placed {
225    pub unit: Unit,
226    pub row: usize,
227    pub col: usize,
228}
229
230/// A routed edge: PCU (writer chain tail) → PMU → PCU (reader chain head), with its hop count.
231#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
232pub struct Route {
233    pub edge: (usize, usize),
234    pub hops: usize,
235    /// The mesh links `((r,c),(r',c'))` the route occupies.
236    pub links: Vec<((usize, usize), (usize, usize))>,
237}
238
239#[derive(Clone, Debug, PartialEq, serde::Serialize)]
240pub struct Placement {
241    pub target: &'static str,
242    pub alloc: Allocation,
243    pub placed: Vec<Placed>,
244    pub routes: Vec<Route>,
245    pub pcu_total: usize,
246    pub pmu_total: usize,
247    pub budget: LinkBudget,
248    /// Most vector routes sharing one DIRECTED mesh link.
249    pub max_link_occupancy: usize,
250    /// Every link within the budget.
251    pub routed_statically: bool,
252    /// Which router produced the routes: `xy-static` ([`place`]) or the installed [`MeshRouter`]'s label
253    /// ([`place_routed`]).
254    pub router: &'static str,
255}
256
257impl Placement {
258    pub fn pcu_used(&self) -> usize {
259        self.alloc.pcus()
260    }
261    pub fn pmu_used(&self) -> usize {
262        self.alloc.pmus()
263    }
264    pub fn pcu_utilization(&self) -> f64 {
265        self.pcu_used() as f64 / self.pcu_total as f64
266    }
267    pub fn pmu_utilization(&self) -> f64 {
268        self.pmu_used() as f64 / self.pmu_total as f64
269    }
270    /// FU utilization inside the allocated PCUs: cells placed / (PCUs × stages per PCU).
271    pub fn fu_utilization(&self, pcu_stages: usize) -> f64 {
272        let cells: usize = self.alloc.stages.iter().map(|s| s.cells).sum();
273        cells as f64 / (self.pcu_used() * pcu_stages.max(1)) as f64
274    }
275    pub fn total_hops(&self) -> usize {
276        self.routes.iter().map(|r| r.hops).sum()
277    }
278    pub fn max_hops(&self) -> usize {
279        self.routes.iter().map(|r| r.hops).max().unwrap_or(0)
280    }
281}
282
283fn is_pcu_cell(row: usize, col: usize) -> bool {
284    (row + col) % 2 == 0
285}
286
287fn manhattan(a: (usize, usize), b: (usize, usize)) -> usize {
288    a.0.abs_diff(b.0) + a.1.abs_diff(b.1)
289}
290
291/// Dimension-order (X first, then Y) route from `a` to `b` as the list of unit links.
292fn xy_route(a: (usize, usize), b: (usize, usize)) -> Vec<((usize, usize), (usize, usize))> {
293    let mut links = Vec::new();
294    let (mut r, mut c) = a;
295    while c != b.1 {
296        let nc = if b.1 > c { c + 1 } else { c - 1 };
297        links.push(((r, c), (r, nc)));
298        c = nc;
299    }
300    while r != b.0 {
301        let nr = if b.0 > r { r + 1 } else { r - 1 };
302        links.push(((r, c), (nr, c)));
303        r = nr;
304    }
305    links
306}
307
308/// Place the allocation on the checkerboard in pipeline order (a row-major snake over PCU cells, each
309/// stage's chain contiguous, each edge's PMU(s) taken from the PMU cells nearest the writer's tail), then
310/// route every edge X-Y and count link occupancy against the budget.
311pub fn place(
312    alloc: &Allocation,
313    t: &PlasticineTarget,
314    budget: LinkBudget,
315) -> Result<Placement, Unplaceable> {
316    let (rows, cols) = (t.pcu_rows, t.pcu_cols);
317    let mut pcu_cells: Vec<(usize, usize)> = Vec::new();
318    let mut pmu_cells: Vec<(usize, usize)> = Vec::new();
319    for r in 0..rows {
320        let cs: Vec<usize> = if r % 2 == 0 {
321            (0..cols).collect()
322        } else {
323            (0..cols).rev().collect()
324        };
325        for c in cs {
326            if is_pcu_cell(r, c) {
327                pcu_cells.push((r, c))
328            } else {
329                pmu_cells.push((r, c))
330            }
331        }
332    }
333    if alloc.pcus() > pcu_cells.len() || alloc.pmus() > pmu_cells.len() {
334        return Err(Unplaceable::TooSmall {
335            needed_rows: alloc.pcus(),
336            needed_cols: alloc.pmus(),
337            target_rows: pcu_cells.len(),
338            target_cols: pmu_cells.len(),
339            target: t.name,
340        });
341    }
342    let mut placed = Vec::new();
343    let mut next_pcu = 0usize;
344    let mut stage_head: Vec<(usize, usize)> = Vec::new();
345    let mut stage_tail: Vec<(usize, usize)> = Vec::new();
346    // Per stage: the extra hop a perimeter-hosted (0-PCU, address-generator) stage pays to enter the mesh.
347    let mut perimeter_hops: Vec<usize> = Vec::new();
348    for s in &alloc.stages {
349        if s.pcus == 0 {
350            // An address-generator stage sits off-mesh at the perimeter and enters at the edge switch of its
351            // mesh neighbour's row (the next PCU to be placed, or the last one placed), on the nearer side.
352            let neighbour = pcu_cells[next_pcu.min(pcu_cells.len() - 1)];
353            let col = if neighbour.1 * 2 < cols { 0 } else { cols - 1 };
354            stage_head.push((neighbour.0, col));
355            stage_tail.push((neighbour.0, col));
356            perimeter_hops.push(1);
357            continue;
358        }
359        stage_head.push(pcu_cells[next_pcu]);
360        for i in 0..s.pcus {
361            let (r, c) = pcu_cells[next_pcu];
362            placed.push(Placed {
363                unit: Unit::Pcu {
364                    stage: s.stage,
365                    index: i,
366                },
367                row: r,
368                col: c,
369            });
370            next_pcu += 1;
371        }
372        stage_tail.push(pcu_cells[next_pcu - 1]);
373        perimeter_hops.push(0);
374    }
375    let mut free_pmu: Vec<(usize, usize)> = pmu_cells.clone();
376    let mut routes = Vec::new();
377    let mut link_use: std::collections::HashMap<((usize, usize), (usize, usize)), usize> =
378        std::collections::HashMap::new();
379    for b in &alloc.buffers {
380        let (w, r) = b.edge;
381        let tail = stage_tail[w];
382        let head = stage_head[r];
383        let mut links = Vec::new();
384        let mut hops = 0usize;
385        for i in 0..b.pmus {
386            // Nearest free PMU cell to the writer's tail.
387            let (idx, _) = free_pmu
388                .iter()
389                .enumerate()
390                .min_by_key(|(_, p)| manhattan(**p, tail) + manhattan(**p, head))
391                .expect("a free PMU cell");
392            let cell = free_pmu.remove(idx);
393            placed.push(Placed {
394                unit: Unit::Pmu {
395                    edge: b.edge,
396                    index: i,
397                },
398                row: cell.0,
399                col: cell.1,
400            });
401            let l1 = xy_route(tail, cell);
402            let l2 = xy_route(cell, head);
403            hops += l1.len() + l2.len() + perimeter_hops[w] + perimeter_hops[r];
404            links.extend(l1);
405            links.extend(l2);
406        }
407        // Links are DIRECTED: a switch port carries `vector_links` channels in each direction (ISCA'17 §3.5:
408        // the static network's registered links are unidirectional per port), so a tile flowing into a PMU
409        // and the same PMU's read-out on the reverse segment never share a channel.
410        for l in &links {
411            *link_use.entry(*l).or_insert(0) += 1;
412        }
413        routes.push(Route {
414            edge: b.edge,
415            hops,
416            links,
417        });
418    }
419    let max_link_occupancy = link_use.values().copied().max().unwrap_or(0);
420    Ok(Placement {
421        target: t.name,
422        alloc: alloc.clone(),
423        placed,
424        routes,
425        pcu_total: pcu_cells.len(),
426        pmu_total: pmu_cells.len(),
427        budget,
428        max_link_occupancy,
429        routed_statically: max_link_occupancy <= budget.vector_links,
430        router: "xy-static",
431    })
432}
433
434/// Allocate + place in one call.
435pub fn place_metapipeline(
436    mp: &Metapipeline,
437    buffer_words: &[usize],
438    t: &PlasticineTarget,
439    budget: LinkBudget,
440) -> Result<Placement, Unplaceable> {
441    let alloc = allocate(mp, buffer_words, t)?;
442    place(&alloc, t, budget)
443}
444
445/// Replicate one instance's allocation `par` times — Spatial's outer-loop unrolling by a par factor
446/// (ISCA'17 §4: `par` independent copies of the whole metapipeline run concurrent outer iterations). Instance
447/// `k`'s stage `s` is reported as stage `k * mp.stages.len() + s`; its buffers are shifted the same way.
448fn replicate(one: &Allocation, n_stages: usize, par: usize) -> Allocation {
449    let mut stages = Vec::with_capacity(one.stages.len() * par);
450    let mut buffers = Vec::with_capacity(one.buffers.len() * par);
451    for k in 0..par {
452        let off = k * n_stages;
453        stages.extend(one.stages.iter().map(|s| StageAllocation {
454            stage: off + s.stage,
455            ..s.clone()
456        }));
457        buffers.extend(one.buffers.iter().map(|b| BufferAllocation {
458            edge: (off + b.edge.0, off + b.edge.1),
459            ..b.clone()
460        }));
461    }
462    Allocation { stages, buffers }
463}
464
465/// Allocate `mp` (with `ag_stages` on the perimeter address generators) once, unroll it `par` ways, and
466/// place every instance — a typed `TooSmall` when the replicated allocation exceeds the mesh.
467pub fn place_unrolled(
468    mp: &Metapipeline,
469    buffer_words: &[usize],
470    ag_stages: &[usize],
471    par: usize,
472    t: &PlasticineTarget,
473    budget: LinkBudget,
474) -> Result<Placement, Unplaceable> {
475    assert!(par >= 1, "an unroll factor is at least one");
476    let one = allocate_hosted(mp, buffer_words, t, ag_stages)?;
477    let alloc = replicate(&one, mp.stages.len(), par);
478    if alloc.pcus() > t.pcus() || alloc.pmus() > t.pmus() {
479        return Err(Unplaceable::TooSmall {
480            needed_rows: alloc.pcus(),
481            needed_cols: alloc.pmus(),
482            target_rows: t.pcus(),
483            target_cols: t.pmus(),
484            target: t.name,
485        });
486    }
487    // Without an installed router, fall back to the xy-static one. The `router` field then reports
488    // "xy-static", so the difference is visible in the Placement instead of silent: a static route can
489    // fail to find a legal assignment that a negotiated one would have found.
490    match mesh_router() {
491        Some(router) => place_routed(&alloc, t, budget, router),
492        None => place(&alloc, t, budget),
493    }
494}
495
496/// Perimeter DRAM ports one instance of `mp` needs, `(into the mesh, out of the mesh)`: every buffer edge
497/// written by a perimeter-hosted stage is one inbound stream, every edge read by one is one outbound stream
498/// (each PMU of a spanning buffer is its own stream).
499pub fn dram_streams(
500    mp: &Metapipeline,
501    buffer_words: &[usize],
502    ag_stages: &[usize],
503    t: &PlasticineTarget,
504) -> Result<(usize, usize), Unplaceable> {
505    let one = allocate_hosted(mp, buffer_words, t, ag_stages)?;
506    let mut inbound = 0usize;
507    let mut outbound = 0usize;
508    for b in &one.buffers {
509        if ag_stages.contains(&b.edge.0) {
510            inbound += b.pmus;
511        }
512        if ag_stages.contains(&b.edge.1) {
513            outbound += b.pmus;
514        }
515    }
516    Ok((inbound, outbound))
517}
518
519/// The widest unroll factor whose replicated allocation fits `t` by CAPACITY (at least 1 when one instance
520/// fits) — PCUs, PMUs, and the perimeter DRAM ports (`rows × 2 sides × vector_links` per direction, ISCA'17
521/// §3.4–3.5) — before routing is considered.
522pub fn max_par(
523    mp: &Metapipeline,
524    buffer_words: &[usize],
525    ag_stages: &[usize],
526    t: &PlasticineTarget,
527) -> Result<usize, Unplaceable> {
528    let one = allocate_hosted(mp, buffer_words, t, ag_stages)?;
529    let by_pcu = if one.pcus() == 0 {
530        usize::MAX
531    } else {
532        t.pcus() / one.pcus()
533    };
534    let by_pmu = if one.pmus() == 0 {
535        usize::MAX
536    } else {
537        t.pmus() / one.pmus()
538    };
539    let (inbound, outbound) = dram_streams(mp, buffer_words, ag_stages, t)?;
540    let ports = t.pcu_rows * 2 * LinkBudget::spade_default().vector_links;
541    let by_in = if inbound == 0 {
542        usize::MAX
543    } else {
544        ports / inbound
545    };
546    let by_out = if outbound == 0 {
547        usize::MAX
548    } else {
549        ports / outbound
550    };
551    Ok(by_pcu.min(by_pmu).min(by_in).min(by_out).max(1))
552}
553
554/// The widest unroll that fits `t` AND routes statically within `budget` — the routability-aware unroll
555/// (the static vector network's perimeter ports, not PCU/PMU capacity, bound a DRAM-fed unroll first on a
556/// large mesh). Returns `(routable, capacity)`; `routable == 1` is reported even when a single instance
557/// exceeds the budget, so a row can still say `routed_statically: false` honestly.
558pub fn max_routable_par(
559    mp: &Metapipeline,
560    buffer_words: &[usize],
561    ag_stages: &[usize],
562    t: &PlasticineTarget,
563    budget: LinkBudget,
564) -> Result<(usize, usize), Unplaceable> {
565    let capacity = max_par(mp, buffer_words, ag_stages, t)?;
566    max_routable_par_within(mp, buffer_words, ag_stages, t, budget, capacity)
567}
568
569/// [`max_routable_par`] searching below `upper` (capped at capacity) — for an unroll that the work itself
570/// bounds below capacity (fewer outer iterations than instances). The search is a BISECTION over
571/// `[1, upper]`: routability is treated as monotone in the unroll (more instances, more perimeter streams), so
572/// the result is the widest unroll the bisection finds routable — a stalled negotiation at some intermediate
573/// width would hide a wider routable one, which is the honest limit of a heuristic router without the exact
574/// rung on the full problem.
575pub fn max_routable_par_within(
576    mp: &Metapipeline,
577    buffer_words: &[usize],
578    ag_stages: &[usize],
579    t: &PlasticineTarget,
580    budget: LinkBudget,
581    upper: usize,
582) -> Result<(usize, usize), Unplaceable> {
583    let capacity = max_par(mp, buffer_words, ag_stages, t)?;
584    let hi = upper.min(capacity).max(1);
585    let routes = |par: usize| -> Result<bool, Unplaceable> {
586        Ok(place_unrolled(mp, buffer_words, ag_stages, par, t, budget)?.routed_statically)
587    };
588    if routes(hi)? {
589        return Ok((hi, capacity));
590    }
591    // Invariant: `lo` routes (or is 1), `bad` does not.
592    let (mut lo, mut bad) = (1usize, hi);
593    while bad - lo > 1 {
594        let mid = lo + (bad - lo) / 2;
595        if routes(mid)? {
596            lo = mid
597        } else {
598            bad = mid
599        }
600    }
601    Ok((lo, capacity))
602}
603
604/// The capability a placement demands of the descriptor beyond depth: at least one bank per lane for a
605/// conflict-free vector access (Plasticine: 16 banks for 16 lanes).
606pub fn lanes_need_banks(t: &PlasticineTarget) -> Result<(), Unplaceable> {
607    let cap = Capability::Banks(t.pcu_lanes);
608    if t.provides(cap) {
609        Ok(())
610    } else {
611        Err(Unplaceable::MissingCapability {
612            needed: cap,
613            target: t.name,
614        })
615    }
616}
617
618// ─────────────────────────────────────────────────────────────────────────────────────────────────────────
619// Row-balanced placement routed by the installed mesh router (the unrolled lane's placer).
620// ─────────────────────────────────────────────────────────────────────────────────────────────────────────
621
622/// PCU cells in a ROW-BALANCED, SIDE-ALTERNATING order: one cell per row before a second in any row, and
623/// within a row the outermost cells first (left-most, right-most, next-left, next-right, …) so that DRAM-fed
624/// instances spread over every row and both perimeter sides instead of filling one row's corridor.
625fn balanced_pcu_cells(t: &PlasticineTarget) -> Vec<(usize, usize)> {
626    let (rows, cols) = (t.pcu_rows, t.pcu_cols);
627    let mut per_row: Vec<Vec<(usize, usize)>> = Vec::with_capacity(rows);
628    for r in 0..rows {
629        let cells: Vec<(usize, usize)> = (0..cols)
630            .filter(|&c| is_pcu_cell(r, c))
631            .map(|c| (r, c))
632            .collect();
633        let mut order = Vec::with_capacity(cells.len());
634        let (mut lo, mut hi) = (0usize, cells.len());
635        while lo < hi {
636            order.push(cells[lo]);
637            lo += 1;
638            if lo < hi {
639                hi -= 1;
640                order.push(cells[hi]);
641            }
642        }
643        per_row.push(order);
644    }
645    let width = per_row.iter().map(|v| v.len()).max().unwrap_or(0);
646    let mut out = Vec::with_capacity(rows * width);
647    for k in 0..width {
648        for row in &per_row {
649            if let Some(&c) = row.get(k) {
650                out.push(c);
651            }
652        }
653    }
654    out
655}
656
657/// Place `alloc` on the checkerboard with row-balanced PCU cells and each edge's PMU(s) on the free PMU
658/// cells nearest its writer and reader (a perimeter-hosted end pulls the PMU toward the nearer side), then
659/// route every vector net — writer tail → PMU → reader head per buffer, DRAM ↔ PMU for perimeter-hosted
660/// stages, and the PCU→PCU links inside every chain (reported as a route on the self-edge
661/// `(stage, stage)`) — through `router`, whose independent legality verdict is `routed_statically`.
662/// `max_link_occupancy` counts the channels one directed link — or one direction of a perimeter port —
663/// carries (never above the budget when legal).
664pub fn place_routed(
665    alloc: &Allocation,
666    t: &PlasticineTarget,
667    budget: LinkBudget,
668    router: &dyn MeshRouter,
669) -> Result<Placement, Unplaceable> {
670    let (rows, cols) = (t.pcu_rows, t.pcu_cols);
671    let pcu_cells = balanced_pcu_cells(t);
672    let mut pmu_cells: Vec<(usize, usize)> = Vec::new();
673    for r in 0..rows {
674        for c in 0..cols {
675            if !is_pcu_cell(r, c) {
676                pmu_cells.push((r, c));
677            }
678        }
679    }
680    if alloc.pcus() > pcu_cells.len() || alloc.pmus() > pmu_cells.len() {
681        return Err(Unplaceable::TooSmall {
682            needed_rows: alloc.pcus(),
683            needed_cols: alloc.pmus(),
684            target_rows: pcu_cells.len(),
685            target_cols: pmu_cells.len(),
686            target: t.name,
687        });
688    }
689    let mut placed = Vec::new();
690    let mut next_pcu = 0usize;
691    // Per stage: the chain's head/tail endpoint (DRAM for a perimeter-hosted stage) and its PCU cells.
692    let mut head: Vec<Endpoint> = Vec::new();
693    let mut tail: Vec<Endpoint> = Vec::new();
694    let mut chains: Vec<Vec<(usize, usize)>> = Vec::new();
695    for s in &alloc.stages {
696        if s.pcus == 0 {
697            head.push(Endpoint::Dram);
698            tail.push(Endpoint::Dram);
699            chains.push(Vec::new());
700            continue;
701        }
702        let cells: Vec<(usize, usize)> = (0..s.pcus).map(|i| pcu_cells[next_pcu + i]).collect();
703        for (i, &(r, c)) in cells.iter().enumerate() {
704            placed.push(Placed {
705                unit: Unit::Pcu {
706                    stage: s.stage,
707                    index: i,
708                },
709                row: r,
710                col: c,
711            });
712        }
713        next_pcu += s.pcus;
714        head.push(Endpoint::Cell(cells[0].0, cells[0].1));
715        let last = cells[cells.len() - 1];
716        tail.push(Endpoint::Cell(last.0, last.1));
717        chains.push(cells);
718    }
719    let mut nets: Vec<MeshNet> = Vec::new();
720    let mut net_owner: Vec<(usize, usize)> = Vec::new();
721    for (si, chain) in chains.iter().enumerate() {
722        for w in chain.windows(2) {
723            nets.push(MeshNet {
724                from: Endpoint::Cell(w[0].0, w[0].1),
725                to: Endpoint::Cell(w[1].0, w[1].1),
726            });
727            net_owner.push((alloc.stages[si].stage, alloc.stages[si].stage));
728        }
729    }
730    let side_pull = |p: (usize, usize)| p.1.min(cols - 1 - p.1);
731    let anchor = |e: Endpoint, p: (usize, usize)| -> usize {
732        match e {
733            Endpoint::Cell(rr, cc) => manhattan(p, (rr, cc)),
734            Endpoint::Dram => side_pull(p) + 1,
735        }
736    };
737    let mut free_pmu = pmu_cells.clone();
738    for b in &alloc.buffers {
739        let (w, r) = b.edge;
740        for i in 0..b.pmus {
741            let (idx, _) = free_pmu
742                .iter()
743                .enumerate()
744                .min_by_key(|(_, p)| anchor(tail[w], **p) + anchor(head[r], **p))
745                .expect("a free PMU cell");
746            let cell = free_pmu.remove(idx);
747            placed.push(Placed {
748                unit: Unit::Pmu {
749                    edge: b.edge,
750                    index: i,
751                },
752                row: cell.0,
753                col: cell.1,
754            });
755            nets.push(MeshNet {
756                from: tail[w],
757                to: Endpoint::Cell(cell.0, cell.1),
758            });
759            net_owner.push(b.edge);
760            nets.push(MeshNet {
761                from: Endpoint::Cell(cell.0, cell.1),
762                to: head[r],
763            });
764            net_owner.push(b.edge);
765        }
766    }
767    let routing = router.route(t, budget, &nets);
768    let mut routes: Vec<Route> = Vec::new();
769    let mut link_use: std::collections::HashMap<(Link, u8), usize> =
770        std::collections::HashMap::new();
771    for (i, (links, occupancy)) in routing.links.into_iter().zip(routing.occupancy).enumerate() {
772        for k in occupancy {
773            *link_use.entry(k).or_insert(0) += 1;
774        }
775        let edge = net_owner[i];
776        match routes.iter_mut().find(|rt| rt.edge == edge) {
777            Some(rt) => {
778                rt.hops += links.len();
779                rt.links.extend(links);
780            }
781            None => routes.push(Route {
782                edge,
783                hops: links.len(),
784                links,
785            }),
786        }
787    }
788    let max_link_occupancy = link_use.values().copied().max().unwrap_or(0);
789    Ok(Placement {
790        target: t.name,
791        alloc: alloc.clone(),
792        placed,
793        routes,
794        pcu_total: pcu_cells.len(),
795        pmu_total: pmu_cells.len(),
796        budget,
797        max_link_occupancy,
798        routed_statically: routing.legal,
799        router: routing.router,
800    })
801}