Skip to main content

logicaffeine_compile/spatial/
rdu_chip.rs

1//! `RduChipFacts` — ONE source of facts for the SambaNova RDU family, from which BOTH chip descriptors
2//! (the spatial mesh [`SpatialTarget`] and the scalar-fabric envelope [`FabricTarget`]) are derived.
3//!
4//! Every number is a [`Fact`] with a provenance: `Published` reads a vendor document named in the chip's
5//! `cites`; `Modeled` fills a gap the public record leaves and says what it was inherited from. Nothing
6//! here is a measurement of the chip and nothing is a time — the descriptors, the neutral artifact and
7//! the fit report all label what they carry, and the locks reflect over the serialized facts so a number
8//! without a citation cannot land.
9//!
10//! The **unit model** ([`RduUnitModel`]) is what the public record does not say about a PCU, a PMU and the
11//! mesh — inherited from the open RDU, Plasticine (Prabhakar et al., ISCA 2017): a six-stage SIMD pipeline
12//! of sixteen lanes, a 16-bank × 4096-word scratchpad with a four-stage address datapath, double-buffered
13//! as the SN50 public description says. The credit-loop latencies are `None` — undeclared, NOT zero —
14//! until the SDK names them; then [`RduChipFacts::with_credit`] drops the number in as data and every
15//! derived figure ([`rdu_ii_report`], the descriptors, the envelope) follows without code.
16//!
17//! [`rdu_ii_report`] is the II view of a placed [`SpatialProgram`] on this unit model, built on the same
18//! models the Plasticine lane uses — [`metapipeline`](crate::codegen_sva::metapipeline) for stage II and
19//! N-buffer depths, [`pmu`](crate::codegen_sva::pmu) for bank conflicts, and
20//! [`choreography::credit_depth`](crate::codegen_sva::choreography::credit_depth) for the bandwidth-delay
21//! solve. MODELED: the model's cycles, never a simulation of the chip.
22
23use std::collections::BTreeSet;
24
25use serde::{Deserialize, Serialize};
26
27use super::cost::CycleModel;
28use super::ir::{ComputeOp, IterKind, SiteRole, SpatialProgram, StageKind, TensorBody};
29use super::roofs::{
30    ComputeRoof, Engine, MemLevel, MemRoof, Rate, Roofs, Scope, StreamMix, Transfer,
31};
32use super::target::{
33    ComputeCap, CreditLatency, DataflowPolicy, Fidelity, HandshakeKind, Interconnect, MeshShape,
34    PackerAccumulate, SiteSpec, SpatialTarget, TileShape, WriteOrdering,
35};
36use crate::codegen_sva::choreography::credit_depth;
37use crate::codegen_sva::fabric_target::{
38    Dataflow, FabricTarget, MemHierarchy, Precision, SiliconSpec, ALL_ALU_OPS, ALL_FUNC_UNITS,
39};
40use crate::codegen_sva::metapipeline::{Metapipeline, Stage, UnitModel};
41use crate::codegen_sva::pmu::{memory_ii, strided_banking_for, AffineAccess, BankingMode, PmuSpec};
42use crate::codegen_sva::simd::LaneTy;
43
44/// The RDU generations the facts describe.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
46#[serde(rename_all = "lowercase")]
47pub enum RduChip {
48    Sn40l,
49    Sn50,
50}
51
52impl RduChip {
53    pub const ALL: [RduChip; 2] = [RduChip::Sn40l, RduChip::Sn50];
54
55    pub fn facts(self) -> RduChipFacts {
56        match self {
57            RduChip::Sn40l => RduChipFacts::sn40l(),
58            RduChip::Sn50 => RduChipFacts::sn50(),
59        }
60    }
61
62    /// The dial spelling (`--chip sn50`).
63    pub fn as_str(self) -> &'static str {
64        match self {
65            RduChip::Sn40l => "sn40l",
66            RduChip::Sn50 => "sn50",
67        }
68    }
69
70    pub fn parse(s: &str) -> Option<RduChip> {
71        match s.trim().to_ascii_lowercase().as_str() {
72            "sn40l" => Some(RduChip::Sn40l),
73            "sn50" => Some(RduChip::Sn50),
74            _ => None,
75        }
76    }
77
78    /// The vendor's part name.
79    pub fn part(self) -> &'static str {
80        match self {
81            RduChip::Sn40l => "SN40L",
82            RduChip::Sn50 => "SN50",
83        }
84    }
85
86    /// The name both descriptors carry.
87    pub fn target_name(self) -> &'static str {
88        match self {
89            RduChip::Sn40l => "sn40l-rdu",
90            RduChip::Sn50 => "sn50-rdu",
91        }
92    }
93}
94
95/// Where a number comes from, strongest first: read from a public source, computed from read numbers,
96/// measured by a run, or a model's. The order is the trust order, so a figure computed from several
97/// facts is as strong as the weakest of them: their `max`.
98#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
99pub enum Provenance {
100    Published,
101    Derived,
102    Measured,
103    Modeled,
104}
105
106/// A number with its provenance and its source.
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
108pub struct Fact<T> {
109    pub value: T,
110    pub provenance: Provenance,
111    /// For a published fact: the public source (one of the chip's `cites`) and the figure as it appears
112    /// there. For a derived fact: the published inputs and the arithmetic. For a measured fact: the run's
113    /// ledger and its method. For a modeled fact: what gap it fills and what it inherits.
114    pub cite: &'static str,
115}
116
117impl<T> Fact<T> {
118    pub const fn published(value: T, cite: &'static str) -> Fact<T> {
119        Fact {
120            value,
121            provenance: Provenance::Published,
122            cite,
123        }
124    }
125    pub const fn derived(value: T, cite: &'static str) -> Fact<T> {
126        Fact {
127            value,
128            provenance: Provenance::Derived,
129            cite,
130        }
131    }
132    pub const fn measured(value: T, cite: &'static str) -> Fact<T> {
133        Fact {
134            value,
135            provenance: Provenance::Measured,
136            cite,
137        }
138    }
139    pub const fn modeled(value: T, cite: &'static str) -> Fact<T> {
140        Fact {
141            value,
142            provenance: Provenance::Modeled,
143            cite,
144        }
145    }
146    pub fn is_modeled(&self) -> bool {
147        self.provenance == Provenance::Modeled
148    }
149    pub fn is_published(&self) -> bool {
150        self.provenance == Provenance::Published
151    }
152}
153
154/// How the chips connect beyond the node count: the published rack topology and the scale-up fabric per
155/// RDU. Present only where the vendor tabulates the links (the SN50); the SN40L's per-link fabric is not.
156#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
157pub struct ScaleUp {
158    pub chips_per_rack: usize,
159    /// Fully connected intra-node links per RDU.
160    pub intra_node_links: usize,
161    /// Inter-node links per RDU.
162    pub inter_node_links: usize,
163    /// Per-link rate in Gbit/s.
164    pub link_gbps: u32,
165    /// Aggregate scale-up bandwidth per RDU in TB/s.
166    pub tb_s_per_chip: f64,
167    /// The scale-out fabric, as the vendor names it.
168    pub scale_out: &'static str,
169}
170
171/// A figure a source states for a fact other than the one the fact carries: superseded by a later source, or one of two
172/// current figures the sources disagree on. Kept beside the facts, so no published figure is lost.
173#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
174pub struct OtherFigure {
175    /// The fact it is a figure of, by its field name.
176    pub fact: &'static str,
177    /// The figure as the source states it.
178    pub stated: &'static str,
179    /// The source's bibliography key, where in it, and why the fact carries another figure.
180    pub cite: &'static str,
181}
182
183/// What the public record does not say about a PCU, a PMU and the mesh — every field modeled and cited,
184/// until an SDK number replaces it as `Published` data.
185#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
186pub struct RduUnitModel {
187    /// FU pipeline stages per PCU — a result leaves the unit no earlier than this many cycles after entry.
188    pub pcu_stages: Fact<usize>,
189    /// SIMD lanes per PCU — the width of a vector access and of the cross-lane reduction tree.
190    pub pcu_lanes: Fact<usize>,
191    /// `(rows, cols)` placement envelope of ONE datapath: what `place_with_target` may place per PCU.
192    pub pcu_envelope: Fact<(usize, usize)>,
193    /// Datapath width in bits (the SIMD/reconfig metadata and the SAT proof width of the envelope).
194    pub datapath_bits: Fact<u32>,
195    /// Accumulator capacity per PCU in native tiles.
196    pub acc_tiles: Fact<usize>,
197    /// Columns of the modeled mesh: the aspect the PCU count is laid out into.
198    pub mesh_columns: Fact<usize>,
199    /// Ops fused into one spatial pipeline stage.
200    pub fusion_window: Fact<usize>,
201    /// Nested-counter depth the PCU's own control drives.
202    pub counter_nest: Fact<usize>,
203    /// The PMU: banks × words, N-buffering, address-datapath stages.
204    pub pmu: Fact<PmuSpec>,
205    /// Credit-loop latencies. `None` is undeclared, NOT zero: the descriptor then gets the vendor's
206    /// double-buffering discipline instead of an invented rate.
207    pub credit: Option<Fact<CreditLatency>>,
208}
209
210impl RduUnitModel {
211    /// The unit model inherited from the open RDU.
212    pub fn plasticine_inherited() -> RduUnitModel {
213        RduUnitModel {
214            pcu_stages: Fact::modeled(6, "not public; inherited from Plasticine [ISCA17] §3.1 — the six-stage PCU pipeline"),
215            pcu_lanes: Fact::modeled(16, "not public; inherited from Plasticine [ISCA17] Table 3 — sixteen SIMD lanes"),
216            pcu_envelope: Fact::modeled((64, 64), "not public; the per-PCU systolic dimension is not tabulated — a 64×64 placement envelope keeps every bounded-family body placeable"),
217            datapath_bits: Fact::modeled(64, "not public; the descriptor's proof width — the PCU's own FU width is not tabulated"),
218            acc_tiles: Fact::modeled(8, "not public; the per-PCU accumulator depth is not tabulated — eight native tiles, the Tensix-class DST budget the passes were calibrated on"),
219            mesh_columns: Fact::modeled(40, "not public; the mesh aspect is not tabulated — 40 columns lay the 1040 PCUs out as 40×26"),
220            fusion_window: Fact::modeled(64, "not public; the RDU fuses a long dataflow pipeline (operator fusion, [HC26]) — 64 ops, at least a whole fused block"),
221            counter_nest: Fact::modeled(1, "not public; the counter chain's depth is not tabulated ([ISCA17]) — one counter is certain"),
222            pmu: Fact::modeled(
223                PmuSpec { banks: 16, words_per_bank: 4096, n_buffers: 2, stages: 4 },
224                "not public; inherited from Plasticine [ISCA17] Table 3 — 16 banks × 4096 words, 4 address-datapath stages; double-buffered memory units per the SN50's description [HC26]",
225            ),
226            credit: None,
227        }
228    }
229
230    /// The metapipeline unit this model drives: the ISCA'17 PCU as the public toolchain simulates it, at
231    /// this depth (result latency = pipeline depth, one-cycle carried-register turnaround).
232    pub fn metapipeline_unit(&self) -> UnitModel {
233        UnitModel::plasticine_pcu(self.pcu_stages.value)
234    }
235}
236
237/// The facts of one RDU generation.
238#[derive(Clone, Debug, PartialEq, Serialize)]
239pub struct RduChipFacts {
240    pub chip: RduChip,
241    pub part: &'static str,
242    /// Peak dense BF16 throughput per RDU, TFLOPS.
243    pub peak_bf16_tflops: Fact<u32>,
244    /// Peak dense FP8 throughput per RDU, TFLOPS — `None` where the vendor publishes no FP8 figure.
245    pub peak_fp8_tflops: Option<Fact<u32>>,
246    /// The bf16 roofline the [`SiliconSpec`] publishes, in PFLOPS: per RDU on the SN50; on the SN40L the
247    /// 16-socket node figure the descriptor has carried since its lock (16 × 640 TFLOPS).
248    pub silicon_peak_bf16_pflops: Fact<f64>,
249    /// The capacities, each in the binary unit the vendor counts in ([`BINARY`]): SRAM in MiB, HBM and the largest
250    /// DDR in GiB.
251    pub sram_mib: Fact<usize>,
252    pub hbm_gib: Fact<usize>,
253    pub ddr_gib_max: Fact<usize>,
254    /// One PMU's scratchpad, KiB — `None` where the vendor publishes no count of them.
255    pub pmu_kib: Option<Fact<usize>>,
256    /// Peak HBM bandwidth per RDU, GB/s.
257    pub hbm_bandwidth_gbps: Fact<f64>,
258    /// Peak DDR bandwidth per RDU, GB/s.
259    pub ddr_bandwidth_gbps: Fact<f64>,
260    /// Aggregate chip-to-chip bandwidth per RDU, TB/s.
261    pub p2p_tb_s: Fact<f64>,
262    pub node_nm: Fact<u32>,
263    pub dies: Fact<u32>,
264    /// RDUs per node — the tensor-parallel width a fit report shards across.
265    pub chips_per_node: Fact<usize>,
266    /// The scale-up fabric, where the vendor tabulates it.
267    pub scale_up: Option<Fact<ScaleUp>>,
268    pub pcu_count: Fact<usize>,
269    pub pmu_count: Fact<usize>,
270    /// Precisions our fabric places AND witnesses — the only ones the descriptors advertise.
271    pub witnessed: &'static [Precision],
272    /// Chip-supported precisions no witness has shipped for — never advertised, named in the envelope.
273    pub witness_pending: &'static [Precision],
274    /// The integer type the PCU computes indices and addresses in: the widest integer compute datatype the
275    /// part has. Every index a lowering carries onto the chip — a gathered row, an arg-max, a block start —
276    /// rides it, and the integer accumulator of the matrix path is this wide; nothing wider exists on the part.
277    pub index_datatype: Fact<Precision>,
278    pub unit_model: RduUnitModel,
279    /// The bibliography keys (`benchmarks/samba_papers.tsv`) of the sources the facts cite: a published fact's citation
280    /// opens with its source's key and the figure as that source states it.
281    pub cites: &'static [&'static str],
282    /// The figures sources state that the facts do not carry.
283    pub others: &'static [OtherFigure],
284}
285
286/// How every capacity is read: as the vendor counts it, in powers of two. SambaNova states 1040 PMUs of 512 KB and 520 MB
287/// of SRAM \[DIAYN26\], which agree only when a MB is 1024 KB; the SN40L paper writes the same figures as MiB, GiB and TiB
288/// in its prose and as MB, GB and TB in its table \[SN40L24\]; an HBM2E stack holds 16 GiB.
289pub const BINARY: &str = "read in powers of two, as the vendor counts (1040 PMUs × 512 KB = 520 MB [DIAYN26])";
290
291const WITNESSED: &[Precision] = &[Precision::Int8, Precision::Bf16];
292
293/// The HBM stacks an RDU's 64 GiB is laid out as for a model that counts channels: four 16 GiB HBM2E stacks.
294pub const HBM_STACKS: usize = 4;
295
296const MODELED_COUNTS_PUBLISHED: &[&str] = &["sites", "acc_tiles", "tile"];
297const MODELED_PCU_COUNT: &[&str] = &[
298    "sites",
299    "acc_tiles",
300    "tile",
301    "local_mem_bytes",
302    "silicon.compute_tiles",
303];
304const MODELED_PMU_COUNT: &[&str] = &["sites", "acc_tiles", "tile", "silicon.mem_tiles"];
305const MODELED_BOTH_COUNTS: &[&str] = &[
306    "sites",
307    "acc_tiles",
308    "tile",
309    "local_mem_bytes",
310    "silicon.compute_tiles",
311    "silicon.mem_tiles",
312];
313
314impl RduChipFacts {
315    pub fn of(chip: RduChip) -> RduChipFacts {
316        chip.facts()
317    }
318
319    /// The SN40L: 1040 PCUs and 1040 PMUs of 512 KB at TSMC 5 nm, 638 TFLOPS bf16 a socket, 16 sockets a node, a three-tier
320    /// memory of 520 MiB on-chip SRAM, 64 GiB of HBM at 1.6 TB/s and up to 1.5 TiB of DDR at over 100 GB/s. The PCU's
321    /// compute datatypes are BF16 / FP32 / INT32 / INT8: INT8 + BF16 are witnessed on the matrix path, FP32 is
322    /// chip-supported and witness-pending, and INT32 is the index datatype.
323    pub fn sn40l() -> RduChipFacts {
324        RduChipFacts {
325            chip: RduChip::Sn40l,
326            part: RduChip::Sn40l.part(),
327            peak_bf16_tflops: Fact::published(638, "[HC24] 638 TFLOPS (bf16) a socket; [DIAYN26] one socket delivers 638 TFLOPs of peak BF16"),
328            peak_fp8_tflops: None,
329            silicon_peak_bf16_pflops: Fact::derived(10.208, "the 16-socket node: [SN50WEB] 16 chips a node × [HC24] 638 TFLOPS = 10,208 TFLOPS = 10.208 PFLOPS"),
330            sram_mib: Fact::published(520, "[SN40L24] 520 MiB of on-chip PMU SRAM; read in powers of two, as the vendor counts (1040 PMUs × 512 KB = 520 MB [DIAYN26])"),
331            hbm_gib: Fact::published(64, "[SN40L24] 64 GiB of co-packaged HBM; read in powers of two, as the vendor counts (1040 PMUs × 512 KB = 520 MB [DIAYN26])"),
332            ddr_gib_max: Fact::published(1536, "[SN40L24] up to 1.5 TiB of DDR DRAM; read in powers of two, as the vendor counts (1040 PMUs × 512 KB = 520 MB [DIAYN26])"),
333            pmu_kib: Some(Fact::published(512, "[DIAYN26] each PMU provides 512 KB; read in powers of two, as the vendor counts")),
334            hbm_bandwidth_gbps: Fact::published(1600.0, "[DIAYN26] 1.6 TB/s of peak HBM bandwidth a socket; [HC24] 1600 GB/s"),
335            ddr_bandwidth_gbps: Fact::published(100.0, "[DIAYN26] over 100 GB/s of peak DDR bandwidth a socket"),
336            p2p_tb_s: Fact::derived(0.5, "the SN50 carries four times the network bandwidth of the previous generation [SN50PR], 2 TB/s an RDU [HC26]: 2 TB/s ÷ 4 = 0.5 TB/s"),
337            node_nm: Fact::published(5, "[HC24] 5nm TSMC"),
338            dies: Fact::published(2, "[DIAYN26] packaged as a two-die socket"),
339            chips_per_node: Fact::published(16, "[SN50WEB] chips per node: 16 on the SN40"),
340            scale_up: None,
341            pcu_count: Fact::published(1040, "[HC24] 1040 PCUs; [DIAYN26] 1040 Pattern Compute Units a socket"),
342            pmu_count: Fact::published(1040, "[HC24] 1040 PMUs; [DIAYN26] 1040 Pattern Memory Units a socket"),
343            witnessed: WITNESSED,
344            witness_pending: &[Precision::Fp32],
345            index_datatype: Fact::published(
346                Precision::Int32,
347                "[SN40L24] the PCU supports BF16, FP32, INT32 and INT8 compute data types — INT32 is the widest integer (arXiv:2405.07518)",
348            ),
349            unit_model: RduUnitModel::plasticine_inherited(),
350            cites: &["DIAYN26", "HC24", "HC26", "ISCA17", "SN40L24", "SN50PR", "SN50WEB"],
351            others: &[
352                OtherFigure { fact: "peak_bf16_tflops", stated: "640 TFLOPS", cite: "[SN50WEB] the SN40 column of the current specification table; the Hot Chips talk and the MLSys paper state 638" },
353                OtherFigure { fact: "hbm_bandwidth_gbps", stated: "1.8 TB/s", cite: "[SN40L24] its table; superseded by 1.6 TB/s [DIAYN26]" },
354                OtherFigure { fact: "hbm_bandwidth_gbps", stated: "about 2 TB/s", cite: "[SN40L24] its prose; superseded by 1.6 TB/s [DIAYN26]" },
355                OtherFigure { fact: "ddr_bandwidth_gbps", stated: "over 200 GB/s", cite: "[SN40L24] its prose and table; superseded by over 100 GB/s [DIAYN26]" },
356                OtherFigure { fact: "ddr_gib_max", stated: "Up to 512 GB", cite: "[SN50WEB] DDR5 in the SN40 column; both papers state 1.5 TB [SN40L24], [DIAYN26]" },
357            ],
358        }
359    }
360
361    /// The SN50: TSMC 5 nm, two max-reticle logic dies, 1600 TFLOPS BF16 / 3200 TFLOPS FP8, 432 MB on-chip SRAM, 64 GB
362    /// HBM2E, DDR5 up to 512 GB (the datasheet: up to 2 TB), 8 chips per node and 16 per SambaRack, ten 800G ports a chip
363    /// (seven make the node's full mesh, two reach the inter-node fabric) carrying 2 TB/s, 400G RoCEv2 scale-out. The
364    /// PCU/PMU counts are NOT public ("a sea of PCUs and PMUs") — inherited from the SN40L and labeled modeled, and so
365    /// are the HBM and DDR bandwidths. FP8 and FP32 are chip-supported and witness-pending.
366    pub fn sn50() -> RduChipFacts {
367        RduChipFacts {
368            chip: RduChip::Sn50,
369            part: RduChip::Sn50.part(),
370            peak_bf16_tflops: Fact::published(1600, "[SN50WEB] 1600 TFLOPS at BF16"),
371            peak_fp8_tflops: Some(Fact::published(3200, "[SN50WEB] 3200 TFLOPS at FP8")),
372            silicon_peak_bf16_pflops: Fact::published(1.6, "[SN50WEB] 1600 TFLOPS at BF16 per RDU = 1.6 PFLOPS"),
373            sram_mib: Fact::published(432, "[SN50WEB] 432 MB of on-chip SRAM; read in powers of two, as the vendor counts (1040 PMUs × 512 KB = 520 MB [DIAYN26])"),
374            hbm_gib: Fact::published(64, "[SN50WEB] 64 GB of HBM2E; read in powers of two, as the vendor counts (1040 PMUs × 512 KB = 520 MB [DIAYN26])"),
375            ddr_gib_max: Fact::published(512, "[SN50WEB] up to 512 GB of DDR5; read in powers of two, as the vendor counts (1040 PMUs × 512 KB = 520 MB [DIAYN26])"),
376            pmu_kib: None,
377            hbm_bandwidth_gbps: Fact::modeled(1600.0, "not public; inherited from the SN40L, whose 64 GB of HBM2E moves 1.6 TB/s [DIAYN26] — the specification table (sambanova.ai/products/rdu-ai-chips) lists the same HBM2E on both [SN50WEB]"),
378            ddr_bandwidth_gbps: Fact::modeled(100.0, "not public; inherited from the SN40L's over 100 GB/s [DIAYN26] — DDR5 on both [SN50WEB]"),
379            p2p_tb_s: Fact::published(2.0, "[HC26] ten 800G Ethernet ports with an aggregate 2 TB/s per RDU"),
380            node_nm: Fact::published(5, "[SN50WEB] 5 nm TSMC"),
381            dies: Fact::published(2, "[STH26] two max reticle dies for the logic"),
382            chips_per_node: Fact::published(8, "[SN50WEB] chips per node: 8"),
383            scale_up: Some(Fact::published(
384                ScaleUp { chips_per_rack: 16, intra_node_links: 7, inter_node_links: 2, link_gbps: 800, tb_s_per_chip: 2.0, scale_out: "400G RoCEv2" },
385                "[HC26] seven integrated 800G Ethernet links make the eight-socket node's full mesh, two more 800G ports reach the inter-node fabric, ten 800G ports carry an aggregate 2 TB/s per RDU, a 400G RoCEv2 NIC scales out; [STH26] 16 RDUs in a rack, two nodes",
386            )),
387            pcu_count: Fact::modeled(1040, "not public; inherited from SN40L (1040 PCUs [HC24]) — the SN50 describes a sea of PCUs and PMUs without a count"),
388            pmu_count: Fact::modeled(1040, "not public; inherited from SN40L (1040 PMUs [HC24]) — the SN50 describes a sea of PCUs and PMUs without a count"),
389            witnessed: WITNESSED,
390            witness_pending: &[Precision::Fp8, Precision::Fp32],
391            index_datatype: Fact::modeled(
392                Precision::Int32,
393                "not public; inherited from the SN40L ([SN40L24]: BF16, FP32, INT32 and INT8 compute data types) — the SN50's table names only BF16 and FP8",
394            ),
395            unit_model: RduUnitModel::plasticine_inherited(),
396            cites: &["DIAYN26", "HC24", "HC26", "ISCA17", "SN40L24", "SN50DS", "SN50WEB", "STH26"],
397            others: &[OtherFigure {
398                fact: "ddr_gib_max",
399                stated: "Up to 2 TB",
400                cite: "[SN50DS] the datasheet's specification table; the product page states up to 512 GB [SN50WEB]",
401            }],
402        }
403    }
404
405    // ── Numbers dropping in as data ──────────────────────────────────────────────────────────────────
406
407    pub fn with_pcu_stages(mut self, stages: Fact<usize>) -> RduChipFacts {
408        self.unit_model.pcu_stages = stages;
409        self
410    }
411
412    pub fn with_pmu_banks(mut self, banks: Fact<usize>) -> RduChipFacts {
413        let pmu = self.unit_model.pmu;
414        self.unit_model.pmu = Fact {
415            value: PmuSpec {
416                banks: banks.value,
417                ..pmu.value
418            },
419            provenance: banks.provenance,
420            cite: banks.cite,
421        };
422        self
423    }
424
425    pub fn with_credit(mut self, credit: Fact<CreditLatency>) -> RduChipFacts {
426        self.unit_model.credit = Some(credit);
427        self
428    }
429
430    pub fn with_pcu_count(mut self, count: Fact<usize>) -> RduChipFacts {
431        self.pcu_count = count;
432        self
433    }
434
435    pub fn with_pmu_count(mut self, count: Fact<usize>) -> RduChipFacts {
436        self.pmu_count = count;
437        self
438    }
439
440    pub fn with_hbm_gib(mut self, hbm: Fact<usize>) -> RduChipFacts {
441        self.hbm_gib = hbm;
442        self
443    }
444
445    // ── Derived figures ──────────────────────────────────────────────────────────────────────────────
446
447    /// The co-packaged HBM's bandwidth, GB/s.
448    pub fn hbm_gbps(&self) -> Fact<f64> {
449        self.hbm_bandwidth_gbps
450    }
451
452    /// The clock the unit model counts cycles in, GHz. Not published for either part: Plasticine's.
453    pub fn clock_ghz(&self) -> Fact<f64> {
454        Fact::modeled(1.0, "not public; the unit model's cycle basis — Plasticine's 1 GHz (Prabhakar et al., ISCA 2017 §5) — the RDU's clock is not published")
455    }
456
457    /// The roofs: the modeled HBM bandwidth over the clock, and the published matrix peaks per RDU. The
458    /// vendor publishes no INT8 or on-chip rate, so those resources carry no roof.
459    pub fn roofs(&self) -> Roofs {
460        let tflops = |f: Fact<u32>| Fact {
461            value: Rate::PerSecond(f.value as f64 * 1e12),
462            provenance: f.provenance,
463            cite: f.cite,
464        };
465        let hbm = self.hbm_gbps();
466        let compute = std::iter::once((Precision::Bf16, tflops(self.peak_bf16_tflops)))
467            .chain(self.peak_fp8_tflops.map(|f| (Precision::Fp8, tflops(f))))
468            .map(|(precision, ops)| ComputeRoof {
469                engine: Engine::Matrix,
470                precision,
471                scope: Scope::Chip,
472                ops,
473            })
474            .collect();
475        Roofs {
476            clock_ghz: self.clock_ghz(),
477            mem: vec![MemRoof {
478                level: MemLevel::Dram,
479                mix: StreamMix::Any,
480                scope: Scope::Chip,
481                unit: Transfer::Bytes,
482                rate: Fact {
483                    value: Rate::PerSecond(hbm.value * 1e9),
484                    provenance: hbm.provenance,
485                    cite: hbm.cite,
486                },
487            }],
488            compute,
489            sources: self.cites,
490        }
491    }
492
493    pub fn sram_bytes(&self) -> usize {
494        self.sram_mib.value * 1024 * 1024
495    }
496
497    /// The on-chip SRAM spread over the PCUs — one site's local-memory budget.
498    pub fn local_mem_bytes_per_pcu(&self) -> usize {
499        self.sram_bytes() / self.pcu_count.value.max(1)
500    }
501
502    /// The modeled mesh: the PCU count laid out into `mesh_columns` columns; a count the aspect cannot
503    /// tile is laid out as one row.
504    pub fn mesh(&self) -> MeshShape {
505        let count = self.pcu_count.value.max(1);
506        let cols = self.unit_model.mesh_columns.value.max(1);
507        if count % cols == 0 {
508            MeshShape {
509                x: cols,
510                y: count / cols,
511            }
512        } else {
513            MeshShape { x: count, y: 1 }
514        }
515    }
516
517    /// The `modeled` set of the derived [`SpatialTarget`] — a function of which counts are modeled: a
518    /// modeled PCU count makes the mesh, the per-site memory and the silicon's compute-tile figure modeled;
519    /// a modeled PMU count makes the mem-tile figure modeled. The mesh aspect, the accumulator depth and the
520    /// tile geometry are modeled on every RDU.
521    pub fn target_modeled(&self) -> &'static [&'static str] {
522        match (self.pcu_count.is_modeled(), self.pmu_count.is_modeled()) {
523            (false, false) => MODELED_COUNTS_PUBLISHED,
524            (true, false) => MODELED_PCU_COUNT,
525            (false, true) => MODELED_PMU_COUNT,
526            (true, true) => MODELED_BOTH_COUNTS,
527        }
528    }
529
530    /// Every modeled fact, by dotted path, sorted — read by reflection over the serialized facts so the
531    /// ledger cannot drift from the data.
532    pub fn modeled_fields(&self) -> Vec<String> {
533        fn walk(v: &serde_json::Value, path: &mut Vec<String>, out: &mut Vec<String>) {
534            if let serde_json::Value::Object(m) = v {
535                if m.contains_key("value") && m.contains_key("provenance") && m.contains_key("cite")
536                {
537                    if m["provenance"] == "Modeled" {
538                        out.push(path.join("."));
539                    }
540                    return;
541                }
542                for (k, c) in m {
543                    path.push(k.clone());
544                    walk(c, path, out);
545                    path.pop();
546                }
547            }
548        }
549        let json = serde_json::to_value(self).expect("the chip facts serialize");
550        let mut out = Vec::new();
551        walk(&json, &mut Vec::new(), &mut out);
552        out.sort();
553        out
554    }
555
556    /// The scalar-fabric envelope of one PCU datapath: what `place_with_target` places onto, gated by the
557    /// witnessed precisions and the three memory tiers; the silicon block carries the published tile counts
558    /// (or the modeled ones, so labeled on the mesh descriptor).
559    pub fn fabric_target(&self) -> FabricTarget {
560        let um = &self.unit_model;
561        let (rows, cols) = um.pcu_envelope.value;
562        FabricTarget {
563            name: self.chip.target_name(),
564            rows,
565            cols,
566            width: um.datapath_bits.value,
567            alu_ops: ALL_ALU_OPS.iter().copied().collect(),
568            func_units: ALL_FUNC_UNITS.iter().copied().collect(),
569            mem_depth: um.pmu.value.capacity_words(),
570            agu: true,
571            simd: Some(LaneTy::I32x2),
572            dataflows: [
573                Dataflow::Reduction,
574                Dataflow::WeightStationary,
575                Dataflow::OutputStationary,
576            ]
577            .into_iter()
578            .collect(),
579            precisions: self.witnessed.iter().copied().collect(),
580            mem: MemHierarchy {
581                sram_kib: self.sram_mib.value * 1024,
582                hbm_gib: self.hbm_gib.value,
583                ddr_gib: self.ddr_gib_max.value,
584            },
585            fusion_window: um.fusion_window.value,
586            silicon: Some(SiliconSpec {
587                compute_tiles: self.pcu_count.value,
588                mem_tiles: self.pmu_count.value,
589                node_nm: self.node_nm.value,
590                peak_bf16_pflops: self.silicon_peak_bf16_pflops.value,
591            }),
592            metapipeline_depth: um.pcu_stages.value,
593            banks: um.pmu.value.banks,
594            counter_nest: um.counter_nest.value,
595        }
596    }
597
598    /// The spatial mesh: one site per PCU, the SRAM spread over them, a crossbar switch fabric, token /
599    /// valid-ready / barrier handshakes, and the embedded scalar datapath.
600    pub fn spatial_target(&self) -> SpatialTarget {
601        let scalar = self.fabric_target();
602        let um = &self.unit_model;
603        let index = self.index_datatype.value;
604        let index_bits =
605            super::target::precision_int_bits(index).expect("the index datatype is an integer");
606        // The tile datapath computes in the witnessed precisions AND the index datatype — the integer an
607        // address is computed in. The matrix dial stays exactly the witnessed set.
608        let elementwise: BTreeSet<Precision> =
609            scalar.precisions.iter().copied().chain([index]).collect();
610        SpatialTarget {
611            name: self.chip.target_name(),
612            sites: self.mesh(),
613            site: SiteSpec {
614                local_mem_bytes: self.local_mem_bytes_per_pcu(),
615                local_mem_opt_in_bytes: None,
616                acc_tiles: um.acc_tiles.value,
617                fp32_acc_halves_acc_tiles: false,
618                movement_engines: 1,
619                nocs: 1,
620                credit: um.credit.map(|c| c.value),
621                local_mem_read_latency: None,
622                mem_banks: 1,
623                vector_lanes: 1,
624                int_compute: Some(index),
625                multiplier: None,
626            },
627            tile: TileShape::TENSIX,
628            interconnect: [Interconnect::Crossbar, Interconnect::GlobalMemory]
629                .into_iter()
630                .collect(),
631            handshakes: [
632                HandshakeKind::Token,
633                HandshakeKind::ValidReady,
634                HandshakeKind::Barrier,
635            ]
636            .into_iter()
637            .collect(),
638            compute: vec![
639                ComputeCap::MatmulTile {
640                    precisions: scalar.precisions.clone(),
641                    fidelity: Fidelity::Exact,
642                    fp32_acc: true,
643                    dataflows: [
644                        DataflowPolicy::WeightStationary,
645                        DataflowPolicy::OutputStationary,
646                    ]
647                    .into_iter()
648                    .collect(),
649                    block_api: None,
650                    wide_spill_reload: true,
651                    acc_in_registers: false,
652                    packer_accumulate: PackerAccumulate::No,
653                    acc_int_bits: index_bits,
654                    max_array: None,
655                },
656                ComputeCap::Elementwise {
657                    precisions: elementwise,
658                },
659                ComputeCap::RowReduce {
660                    precisions: scalar.precisions.clone(),
661                },
662                ComputeCap::Scalar(scalar.clone()),
663            ],
664            dram: scalar.mem,
665            precisions: scalar.precisions.clone(),
666            noc_rule: None,
667            write_ordering: WriteOrdering::Ordered,
668            cycle: CycleModel::uncalibrated(),
669            modeled: self.target_modeled(),
670            roofs: self.roofs(),
671            part: None,
672        }
673    }
674}
675
676// ── The II report ────────────────────────────────────────────────────────────────────────────────
677
678/// One compute stage's timing on the unit model.
679#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
680pub struct RduIiRow {
681    /// The IR stage id.
682    pub stage: usize,
683    pub name: String,
684    pub op: &'static str,
685    /// The stage folds an axis (pays the cross-lane reduction tree).
686    pub reduce: bool,
687    /// The stage reads a buffer it also writes (a loop-carried dependency through the PMU).
688    pub carried: bool,
689    pub lanes: usize,
690    pub timing: Stage,
691}
692
693/// One IR edge's credit loop on the unit model.
694#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
695pub struct RduCreditRow {
696    /// The IR edge id.
697    pub edge: usize,
698    /// Writer and (farthest) reader stage ids.
699    pub from: usize,
700    pub to: usize,
701    /// Mesh hops between them, from the placement.
702    pub hops: usize,
703    /// Producing the item (the writer's latency when it is a compute stage; the declared fetch latency
704    /// when it is a load and the latencies are declared) plus carrying it.
705    pub forward_latency: usize,
706    /// Carrying the freed credit back: the hops, plus the declared page service and credit return.
707    pub backward_latency: usize,
708    /// Spatial's writer-to-reader N-buffer rule for a compute→compute edge; the vendor's double-buffering
709    /// discipline otherwise.
710    pub n_buffer_depth: usize,
711    /// The bandwidth-delay solve.
712    pub credit_depth: usize,
713    /// The deeper of the two.
714    pub depth: usize,
715}
716
717/// The II view of a placed program on one RDU's unit model — the numbers an RDU engineer asks for first,
718/// every one MODELED.
719#[derive(Clone, Debug, PartialEq, Serialize)]
720pub struct RduIiReport {
721    pub chip: RduChip,
722    pub program: String,
723    pub unit_model: RduUnitModel,
724    /// The PMU banking the vector accesses were costed under.
725    pub banking: BankingMode,
726    /// Whether the credit loops were solved with declared latencies or with the hops alone.
727    pub credit_declared: bool,
728    pub stages: Vec<RduIiRow>,
729    /// The slowest stage's II.
730    pub pipeline_ii: usize,
731    /// `(writer, reader, depth)` over compute→compute edges, by row index.
732    pub buffer_depths: Vec<(usize, usize, usize)>,
733    pub credit_depths: Vec<RduCreditRow>,
734    pub provenance: &'static str,
735}
736
737fn ceil_log2(n: usize) -> usize {
738    (usize::BITS - n.max(1).saturating_sub(1).leading_zeros()) as usize
739}
740
741fn body_nodes(b: &TensorBody) -> usize {
742    match b {
743        TensorBody::Operand(_) | TensorBody::Const(_) => 1,
744        TensorBody::Add(x, y)
745        | TensorBody::Sub(x, y)
746        | TensorBody::Mul(x, y)
747        | TensorBody::Min(x, y)
748        | TensorBody::Max(x, y)
749        | TensorBody::FloorDiv(x, y) => 1 + body_nodes(x) + body_nodes(y),
750        TensorBody::Unary { arg, .. } | TensorBody::Shr { arg, .. } => 1 + body_nodes(arg),
751        TensorBody::Select {
752            then, otherwise, ..
753        } => 1 + body_nodes(then) + body_nodes(otherwise),
754    }
755}
756
757/// The vector accesses one compute stage makes per iteration: lane `l` of a map that varies along the
758/// innermost axis at stride `s` reads `base + s·l`; an operand constant along that axis is one scalar read.
759/// A backward walk (a negative stride) lands in the same banks as the forward one, so the magnitude is what
760/// the banking sees. Tile-streaming ops read their operands at unit stride.
761fn vector_accesses(kind: &StageKind, n_ins: usize, lanes: usize) -> Vec<AffineAccess> {
762    match kind {
763        StageKind::Compute(ComputeOp::Generic {
764            iters, ins, out, ..
765        }) => {
766            let inner = iters.last().map(|i| i.var);
767            ins.iter()
768                .chain(std::iter::once(out))
769                .map(|m| {
770                    let base = usize::try_from(m.base).unwrap_or(0);
771                    let stride = inner
772                        .and_then(|v| m.terms.iter().find(|(var, _)| *var == v))
773                        .map(|(_, s)| {
774                            usize::try_from(*s)
775                                .unwrap_or_else(|_| usize::try_from(-*s).unwrap_or(0))
776                        })
777                        .unwrap_or(0);
778                    if stride == 0 {
779                        AffineAccess {
780                            base,
781                            stride: 0,
782                            lanes: 1,
783                        }
784                    } else {
785                        AffineAccess {
786                            base,
787                            stride,
788                            lanes,
789                        }
790                    }
791                })
792                .collect()
793        }
794        _ => (0..n_ins.max(1))
795            .map(|_| AffineAccess {
796                base: 0,
797                stride: 1,
798                lanes,
799            })
800            .collect(),
801    }
802}
803
804/// The II view of `p` on `facts`' unit model. One row per compute-role stage: latency = the PCU pipeline
805/// depth (+ the reduction tree, `⌈log₂ lanes⌉`, when the stage folds an axis); resource II from the unit
806/// model's occupancies; recurrence II = pipeline depth + register turnaround when the stage reads a buffer
807/// it also writes (a spilled partial, an in-place update); memory II from the stage's vector accesses under
808/// the PMU's strided banking. Then the pipeline's II, Spatial's N-buffer depths over compute→compute edges,
809/// and the credit-loop depth of every IR edge.
810pub fn rdu_ii_report(p: &SpatialProgram, facts: &RduChipFacts) -> RduIiReport {
811    let um = &facts.unit_model;
812    let unit = um.metapipeline_unit();
813    let lanes = um.pcu_lanes.value.max(1);
814    let pmu = um.pmu.value;
815    let banking = strided_banking_for(1);
816    let credit = um.credit.map(|c| c.value);
817
818    let compute: Vec<&super::ir::Stage> = p
819        .stages
820        .iter()
821        .filter(|s| s.role == SiteRole::Compute)
822        .collect();
823    let row_of = |id: super::ir::StageId| compute.iter().position(|s| s.id == id);
824    let mut rows = Vec::with_capacity(compute.len());
825    for s in &compute {
826        let (op, reduce, carried_by_op, cells, resource_ii) = match &s.kind {
827            StageKind::Compute(ComputeOp::MatmulTiles { interm, .. }) => (
828                "matmul_tiles",
829                false,
830                interm.is_some(),
831                2,
832                unit.default_func.1,
833            ),
834            StageKind::Compute(ComputeOp::Requant { .. }) => {
835                ("requant", false, false, 2, unit.default_func.1)
836            }
837            StageKind::Compute(ComputeOp::Bias { .. }) => {
838                ("bias", false, false, 1, unit.default_func.1)
839            }
840            StageKind::Compute(ComputeOp::Relu) => ("relu", false, false, 1, unit.default_func.1),
841            StageKind::Compute(ComputeOp::Generic { iters, body, .. }) => (
842                "generic",
843                iters
844                    .iter()
845                    .any(|i| matches!(i.kind, IterKind::Reduce { .. })),
846                false,
847                body_nodes(body),
848                unit.default_func.1,
849            ),
850            StageKind::Compute(ComputeOp::Scalar(b)) | StageKind::Scalar(b) => (
851                "scalar",
852                false,
853                false,
854                b.cells.len(),
855                b.cells.iter().map(|c| unit.occupancy(c)).max().unwrap_or(1),
856            ),
857            StageKind::AllReduce { combine, .. } => match combine.as_ref() {
858                ComputeOp::Generic { iters, body, .. } => (
859                    "allreduce",
860                    iters
861                        .iter()
862                        .any(|i| matches!(i.kind, IterKind::Reduce { .. })),
863                    false,
864                    body_nodes(body),
865                    unit.default_func.1,
866                ),
867                _ => ("allreduce", true, false, 1, unit.default_func.1),
868            },
869            StageKind::Load { .. } | StageKind::Store { .. } | StageKind::Recv { .. } => {
870                unreachable!("a movement stage is not a compute row")
871            }
872        };
873        let carried = carried_by_op || s.ins.iter().any(|b| s.outs.contains(b));
874        let tree = if reduce {
875            ceil_log2(lanes) * unit.tile_latency
876        } else {
877            0
878        };
879        let latency = unit.pipeline_depth.max(1) + tree;
880        let recurrence_ii = if carried {
881            unit.pipeline_depth + unit.register_turnaround
882        } else {
883            0
884        };
885        let accesses = vector_accesses(&s.kind, s.ins.len(), lanes);
886        rows.push(RduIiRow {
887            stage: s.id.idx(),
888            name: s.name.clone(),
889            op,
890            reduce,
891            carried,
892            lanes,
893            timing: Stage {
894                name: s.name.clone(),
895                resource_ii,
896                recurrence_ii,
897                latency,
898                cells,
899                memory_ii: memory_ii(&accesses, banking, &pmu),
900            },
901        });
902    }
903
904    let mut edges: Vec<(usize, usize)> = Vec::new();
905    for e in &p.edges {
906        if let Some(w) = row_of(e.from) {
907            for t in &e.to {
908                if let Some(r) = row_of(*t) {
909                    if w < r {
910                        edges.push((w, r));
911                    }
912                }
913            }
914        }
915    }
916    edges.sort_unstable();
917    edges.dedup();
918    let mp = Metapipeline {
919        stages: rows.iter().map(|r| r.timing.clone()).collect(),
920        edges,
921    };
922    let pipeline_ii = mp.ii();
923    let buffer_depths = mp.buffer_depths();
924
925    let site_of = |id: super::ir::StageId| p.placement.as_ref().map(|pl| pl.site_of[id.idx()]);
926    let hops_between = |a: super::ir::StageId, b: super::ir::StageId| match (site_of(a), site_of(b))
927    {
928        (Some(x), Some(y)) => {
929            (x.x as isize - y.x as isize).unsigned_abs()
930                + (x.y as isize - y.y as isize).unsigned_abs()
931        }
932        _ => 0,
933    };
934    let mut credit_depths = Vec::with_capacity(p.edges.len());
935    for e in &p.edges {
936        let (to, hops) =
937            e.to.iter()
938                .map(|t| (*t, hops_between(e.from, *t)))
939                .max_by_key(|(t, h)| (*h, t.idx()))
940                .unwrap_or((e.from, 0));
941        let writer_row = row_of(e.from);
942        let produce = match writer_row {
943            Some(w) => rows[w].timing.latency,
944            None => credit.map(|c| c.fetch_tile as usize).unwrap_or(0),
945        };
946        let forward_latency = produce + hops;
947        let backward_latency = hops
948            + credit
949                .map(|c| (c.service_page + c.credit_return) as usize)
950                .unwrap_or(0);
951        let n_buffer_depth = match (writer_row, row_of(to)) {
952            (Some(w), Some(r)) if w < r => Metapipeline::buffer_depth(w, r),
953            _ => pmu.n_buffers.max(1),
954        };
955        let credit_depth = credit_depth(forward_latency, backward_latency, pipeline_ii);
956        credit_depths.push(RduCreditRow {
957            edge: e.id.idx(),
958            from: e.from.idx(),
959            to: to.idx(),
960            hops,
961            forward_latency,
962            backward_latency,
963            n_buffer_depth,
964            credit_depth,
965            depth: credit_depth.max(n_buffer_depth),
966        });
967    }
968
969    RduIiReport {
970        chip: facts.chip,
971        program: p.name.clone(),
972        unit_model: *um,
973        banking,
974        credit_declared: credit.is_some(),
975        stages: rows,
976        pipeline_ii,
977        buffer_depths,
978        credit_depths,
979        provenance: "modeled",
980    }
981}
982
983/// The precisions the descriptors advertise, as a set.
984pub fn witnessed_precisions() -> BTreeSet<Precision> {
985    WITNESSED.iter().copied().collect()
986}