1use 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#[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 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 pub fn part(self) -> &'static str {
80 match self {
81 RduChip::Sn40l => "SN40L",
82 RduChip::Sn50 => "SN50",
83 }
84 }
85
86 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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
108pub struct Fact<T> {
109 pub value: T,
110 pub provenance: Provenance,
111 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#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
157pub struct ScaleUp {
158 pub chips_per_rack: usize,
159 pub intra_node_links: usize,
161 pub inter_node_links: usize,
163 pub link_gbps: u32,
165 pub tb_s_per_chip: f64,
167 pub scale_out: &'static str,
169}
170
171#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
174pub struct OtherFigure {
175 pub fact: &'static str,
177 pub stated: &'static str,
179 pub cite: &'static str,
181}
182
183#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
186pub struct RduUnitModel {
187 pub pcu_stages: Fact<usize>,
189 pub pcu_lanes: Fact<usize>,
191 pub pcu_envelope: Fact<(usize, usize)>,
193 pub datapath_bits: Fact<u32>,
195 pub acc_tiles: Fact<usize>,
197 pub mesh_columns: Fact<usize>,
199 pub fusion_window: Fact<usize>,
201 pub counter_nest: Fact<usize>,
203 pub pmu: Fact<PmuSpec>,
205 pub credit: Option<Fact<CreditLatency>>,
208}
209
210impl RduUnitModel {
211 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 pub fn metapipeline_unit(&self) -> UnitModel {
233 UnitModel::plasticine_pcu(self.pcu_stages.value)
234 }
235}
236
237#[derive(Clone, Debug, PartialEq, Serialize)]
239pub struct RduChipFacts {
240 pub chip: RduChip,
241 pub part: &'static str,
242 pub peak_bf16_tflops: Fact<u32>,
244 pub peak_fp8_tflops: Option<Fact<u32>>,
246 pub silicon_peak_bf16_pflops: Fact<f64>,
249 pub sram_mib: Fact<usize>,
252 pub hbm_gib: Fact<usize>,
253 pub ddr_gib_max: Fact<usize>,
254 pub pmu_kib: Option<Fact<usize>>,
256 pub hbm_bandwidth_gbps: Fact<f64>,
258 pub ddr_bandwidth_gbps: Fact<f64>,
260 pub p2p_tb_s: Fact<f64>,
262 pub node_nm: Fact<u32>,
263 pub dies: Fact<u32>,
264 pub chips_per_node: Fact<usize>,
266 pub scale_up: Option<Fact<ScaleUp>>,
268 pub pcu_count: Fact<usize>,
269 pub pmu_count: Fact<usize>,
270 pub witnessed: &'static [Precision],
272 pub witness_pending: &'static [Precision],
274 pub index_datatype: Fact<Precision>,
278 pub unit_model: RduUnitModel,
279 pub cites: &'static [&'static str],
282 pub others: &'static [OtherFigure],
284}
285
286pub 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
293pub 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 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 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 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 pub fn hbm_gbps(&self) -> Fact<f64> {
449 self.hbm_bandwidth_gbps
450 }
451
452 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 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 pub fn local_mem_bytes_per_pcu(&self) -> usize {
499 self.sram_bytes() / self.pcu_count.value.max(1)
500 }
501
502 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 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 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 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 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 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#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
680pub struct RduIiRow {
681 pub stage: usize,
683 pub name: String,
684 pub op: &'static str,
685 pub reduce: bool,
687 pub carried: bool,
689 pub lanes: usize,
690 pub timing: Stage,
691}
692
693#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
695pub struct RduCreditRow {
696 pub edge: usize,
698 pub from: usize,
700 pub to: usize,
701 pub hops: usize,
703 pub forward_latency: usize,
706 pub backward_latency: usize,
708 pub n_buffer_depth: usize,
711 pub credit_depth: usize,
713 pub depth: usize,
715}
716
717#[derive(Clone, Debug, PartialEq, Serialize)]
720pub struct RduIiReport {
721 pub chip: RduChip,
722 pub program: String,
723 pub unit_model: RduUnitModel,
724 pub banking: BankingMode,
726 pub credit_declared: bool,
728 pub stages: Vec<RduIiRow>,
729 pub pipeline_ii: usize,
731 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
757fn 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
804pub 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
983pub fn witnessed_precisions() -> BTreeSet<Precision> {
985 WITNESSED.iter().copied().collect()
986}