1use std::panic::{catch_unwind, AssertUnwindSafe};
22
23use serde_json::{json, Value};
24
25use super::choreography::{synthesize, Choreography};
26use super::fabric_grid::route_loop_body;
27use super::fabric_rtl::{
28 eval_loop_fabric, loop_fabric_from_source, FabricOp, LoopCell, LoopFabric, Src,
29};
30use super::fusion::{share_operand, OperandStream, Sharing};
31use super::metapipeline::{eval_loop_fabric_at_ii, loop_stage, Metapipeline, Stage, UnitModel};
32use super::plasticine::PlasticineTarget;
33use super::plasticine_place::{
34 max_par, max_routable_par_within, place_unrolled, words_per_pmu, LinkBudget, Placement,
35};
36use super::pmu::{address_datapath_stages, memory_ii, strided_banking_for, AffineAccess, PmuSpec};
37use super::pnr_certificate::{certify_external_placement, PlacementCertificate};
38use super::spatial_emit::{
39 emit_spatial_counted_loop, emit_spatial_counted_loop_test, emit_spatial_gda_test, emit_spatial_gemm_test,
40 emit_spatial_kmeans_test, emit_spatial_outer_test, emit_spatial_zip_map_test, emit_spatial_zip_sum_test, GdaColumn,
41 GdaPar, KmeansPar, MapColumn, MapPar, OuterPar, ZipColumn, ZipPar,
42};
43use crate::tensor_recognize::{Axis, ZipOp, ZipSum, ZipTerm};
44use crate::compile::vm_outcome;
45
46pub const PROVENANCE_MODELED: &str = "modeled";
48pub const REPORT_PATH: &str = "benchmarks/silicon/plasticine_report.json";
50pub const CERTIFICATE_WIDTH: u32 = 4;
52pub const TOOLCHAIN_LEDGER_PATH: &str = "benchmarks/silicon/plasticine_toolchain.json";
54pub const CALIBRATION_PATH: &str = "benchmarks/silicon/plasticine_calibration.json";
56pub const PROVENANCE_SIMULATED: &str = "simulated_tungsten";
58
59#[derive(Clone, Debug, PartialEq)]
61pub enum WorkloadKind {
62 CountedLoop {
65 source: String,
66 fn_name: String,
67 params: Vec<i64>,
68 },
69 Gemm {
72 m: usize,
73 n: usize,
74 k: usize,
75 tile: Option<usize>,
76 par: Option<usize>,
77 },
78}
79
80#[derive(Clone, Debug, PartialEq)]
81pub struct Workload {
82 pub name: String,
83 pub kind: WorkloadKind,
84}
85
86#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
87pub struct StageRow {
88 pub name: String,
89 pub host: String,
91 pub resource_ii: usize,
92 pub recurrence_ii: usize,
93 pub memory_ii: usize,
94 pub ii: usize,
95 pub latency: usize,
96 pub cells: usize,
97 pub pcus: usize,
98 pub critical: bool,
99}
100
101#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
102pub struct BufferRow {
103 pub writer: usize,
104 pub reader: usize,
105 pub depth: usize,
106 pub words: usize,
107 pub pmus: usize,
108}
109
110#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
112pub struct OperandRow {
113 pub name: String,
114 pub sharing: String,
116 pub consumers: usize,
117 pub bytes_per_copy: usize,
118 pub copies: usize,
120 pub bytes_moved: usize,
121 pub dram_cycles_saved: usize,
122 pub fabric_cycles_added: usize,
123 pub why: String,
124}
125
126#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
128pub struct ChannelRow {
129 pub writer: usize,
130 pub reader: usize,
131 pub hops: usize,
132 pub forward_latency: usize,
133 pub backward_latency: usize,
134 pub n_buffer_depth: usize,
136 pub credit_depth: usize,
138 pub depth: usize,
140 pub binding: String,
142}
143
144#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
146pub struct ControlRow {
147 pub nets: usize,
148 pub hops: usize,
149 pub max_scalar_occupancy: usize,
150 pub scalar_link_budget: usize,
151 pub routed_statically: bool,
152}
153
154#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
155pub struct VmAnchor {
156 pub params: Vec<i64>,
157 pub vm: i64,
158 pub fabric: i64,
159 pub pipelined_at_ii: i64,
160 pub hazards_at_ii: usize,
161 pub hazards_below_ii: Option<usize>,
163}
164
165#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
166pub struct CertificateRow {
167 pub steps: usize,
168 pub vars: usize,
169 pub proves: String,
170 pub rechecked: bool,
171}
172
173#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
174pub struct GemmRow {
175 pub m: usize,
176 pub n: usize,
177 pub k: usize,
178 pub tile_m: usize,
179 pub tile_n: usize,
180 pub col_tiles: usize,
181 pub row_tiles: usize,
182 pub k_chunks: usize,
183 pub iters_per_tile: usize,
184 pub lanes: usize,
185 #[serde(default)]
188 pub k_tile: usize,
189 #[serde(default)]
191 pub kk: usize,
192 #[serde(default)]
194 pub chunk_load_cycles: usize,
195 #[serde(default)]
196 pub chunk_compute_cycles: usize,
197 #[serde(default)]
199 pub chunks_in_sequence: usize,
200}
201
202#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
204pub struct WorkloadRow {
205 pub name: String,
206 pub kind: String,
208 pub target: String,
209 pub provenance: String,
210 pub par: usize,
212 pub par_capacity: usize,
215 pub router: String,
218 pub stages: Vec<StageRow>,
219 pub pipeline_ii: usize,
220 pub critical_stage: String,
221 pub buffers: Vec<BufferRow>,
222 pub pcu_used: usize,
223 pub pcu_total: usize,
224 pub pcu_utilization_pct: f64,
225 pub pmu_used: usize,
226 pub pmu_total: usize,
227 pub pmu_utilization_pct: f64,
228 pub fu_utilization_pct: f64,
229 pub ag_stages: usize,
231 pub total_hops: usize,
232 pub max_hops: usize,
233 pub max_link_occupancy: usize,
234 pub vector_links: usize,
235 pub routed_statically: bool,
236 pub iterations: usize,
238 pub fill_cycles: usize,
239 pub compute_cycles: usize,
240 #[serde(default)]
243 pub operand_stall_cycles: usize,
244 pub dram_bytes: usize,
245 pub dram_cycles: usize,
246 pub modeled_cycles: usize,
247 pub bound: String,
249 pub throughput_iters_per_cycle: f64,
250 pub gflops: Option<f64>,
251 pub pct_of_peak_fp32: Option<f64>,
252 #[serde(default)]
254 pub operands: Option<Vec<OperandRow>>,
255 #[serde(default)]
257 pub channels: Vec<ChannelRow>,
258 #[serde(default)]
260 pub control: Option<ControlRow>,
261 pub certificate: Option<CertificateRow>,
262 pub certificate_omitted: Option<String>,
263 pub vm_anchor: Option<VmAnchor>,
264 pub gemm: Option<GemmRow>,
265}
266
267#[derive(Debug)]
270pub struct PlasticineArtifact {
271 pub fn_name: String,
272 pub target: &'static str,
273 pub loop_fabric: LoopFabric,
274 pub metapipeline: Metapipeline,
275 pub placement: Placement,
276 pub row: WorkloadRow,
277 pub certificate: Option<PlacementCertificate>,
278 pub spatial: String,
280 pub spatial_test: String,
283}
284
285impl PlasticineArtifact {
286 pub fn placement_json(&self) -> String {
288 format!(
289 "{}\n",
290 serde_json::to_string_pretty(&self.placement).expect("a placement serializes")
291 )
292 }
293 pub fn row_json(&self) -> String {
295 format!(
296 "{}\n",
297 serde_json::to_string_pretty(&self.row).expect("a row serializes")
298 )
299 }
300}
301
302fn r3(x: f64) -> f64 {
303 (x * 1000.0).round() / 1000.0
304}
305
306fn pct(x: f64) -> f64 {
307 r3(x * 100.0)
308}
309
310fn ceil_div(a: usize, b: usize) -> usize {
311 (a + b.max(1) - 1) / b.max(1)
312}
313
314fn quietly<T>(f: impl FnOnce() -> T) -> Result<T, String> {
318 let prev = std::panic::take_hook();
319 std::panic::set_hook(Box::new(|_| {}));
320 let r = catch_unwind(AssertUnwindSafe(f));
321 std::panic::set_hook(prev);
322 r.map_err(|p| {
323 p.downcast_ref::<String>()
324 .cloned()
325 .or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string()))
326 .unwrap_or_else(|| "the lowering panicked".to_string())
327 })
328}
329
330fn without_main(source: &str) -> String {
333 let mut out = String::new();
334 let mut skipping = false;
335 for line in source.lines() {
336 if line.trim() == "## Main" {
337 skipping = true;
338 continue;
339 }
340 if skipping && line.starts_with("## ") {
341 skipping = false;
342 }
343 if !skipping {
344 out.push_str(line);
345 out.push('\n');
346 }
347 }
348 out
349}
350
351fn vm_value(source: &str, fn_name: &str, params: &[i64]) -> Result<i64, String> {
352 let args = params
353 .iter()
354 .map(|p| {
355 if *p < 0 {
356 format!("(0 - {})", -p)
357 } else {
358 p.to_string()
359 }
360 })
361 .collect::<Vec<_>>()
362 .join(", ");
363 let prog = format!(
364 "{}## Main\n Show {fn_name}({args}).\n",
365 without_main(source)
366 );
367 let out = vm_outcome(&prog);
368 if let Some(e) = out.error {
369 return Err(format!("the VM errored running `{fn_name}({args})`: {e:?}"));
370 }
371 out.output
372 .split_whitespace()
373 .next()
374 .and_then(|s| s.parse::<i64>().ok())
375 .ok_or_else(|| {
376 format!(
377 "the VM printed no integer for `{fn_name}({args})`: {:?}",
378 out.output
379 )
380 })
381}
382
383fn certify_body(
387 lf: &LoopFabric,
388) -> (
389 Option<PlacementCertificate>,
390 Option<CertificateRow>,
391 Option<String>,
392) {
393 let mut ops = Vec::with_capacity(lf.body.len());
394 for cell in &lf.body {
395 match cell {
396 LoopCell::Arith { op, a, b } => ops.push(FabricOp { op: *op, a: *a, b: *b }),
397 LoopCell::Select { .. } => return (None, None, Some("the body has a predicated select — outside the bounded ALU fragment the certificate covers".into())),
398 LoopCell::Func { unit, .. } => return (None, None, Some(format!("the body has a `{unit:?}` functional unit — outside the bounded ALU fragment the certificate covers"))),
399 _ => return (None, None, Some("the body has memory tiles — outside the bounded ALU fragment the certificate covers".into())),
400 }
401 }
402 let Some(Src::Val(result)) = lf.next.first().copied() else {
403 return (None, None, Some("next-state 0 is not a body value (the register holds or copies an input) — nothing to certify".into()));
404 };
405 let n_body_in = lf.n_state + 1 + lf.invariants.len();
406 let grid = route_loop_body(&lf.body, n_body_in, &lf.next);
407 match certify_external_placement(&grid, &ops, result, CERTIFICATE_WIDTH) {
408 Some(cert) => {
409 let rechecked = cert.recheck();
410 let row = CertificateRow {
411 steps: cert.len(),
412 vars: cert.num_vars(),
413 proves: format!(
414 "the routed body grid's output 0 ≡ the next-state-0 datapath ({} ALU cells) for every {CERTIFICATE_WIDTH}-bit input: the negation is SAT-refuted (PR/RUP), re-checkable without the placer",
415 ops.len()
416 ),
417 rechecked,
418 };
419 (Some(cert), Some(row), None)
420 }
421 None => (None, None, Some("the placement-equivalence obligation escaped the propositional fragment (fail-closed)".into())),
422 }
423}
424
425fn row_from_placement(
426 name: &str,
427 kind: &str,
428 t: &PlasticineTarget,
429 mp: &Metapipeline,
430 ag_stages: &[usize],
431 par: usize,
432 par_capacity: usize,
433 placement: &Placement,
434 iterations: usize,
435 dram_bytes: usize,
436 operand_stall_cycles: usize,
437) -> WorkloadRow {
438 let ii = mp.ii();
439 let fill: usize = mp.stages.iter().map(|s| s.latency).sum();
440 let compute_cycles = mp.cycles(iterations);
441 let dram_cycles = if dram_bytes == 0 {
442 0
443 } else {
444 (dram_bytes as f64 * t.clock_ghz / t.dram.gbps).ceil() as usize
445 };
446 let (modeled_cycles, bound) = if dram_cycles > compute_cycles + operand_stall_cycles {
447 (dram_cycles, "dram")
448 } else {
449 (compute_cycles + operand_stall_cycles, "compute")
450 };
451 let stages: Vec<StageRow> = mp
452 .stages
453 .iter()
454 .enumerate()
455 .map(|(i, s)| StageRow {
456 name: s.name.clone(),
457 host: if ag_stages.contains(&i) { "ag" } else { "pcu" }.to_string(),
458 resource_ii: s.resource_ii,
459 recurrence_ii: s.recurrence_ii,
460 memory_ii: s.memory_ii,
461 ii: s.ii(),
462 latency: s.latency,
463 cells: s.cells,
464 pcus: placement.alloc.stages[i].pcus,
465 critical: s.ii() == ii,
466 })
467 .collect();
468 let critical_stage = mp
469 .stages
470 .iter()
471 .find(|s| s.ii() == ii)
472 .map(|s| s.name.clone())
473 .unwrap_or_default();
474 let buffers: Vec<BufferRow> = placement
475 .alloc
476 .buffers
477 .iter()
478 .take(mp.edges.len())
479 .map(|b| BufferRow {
480 writer: b.edge.0,
481 reader: b.edge.1,
482 depth: b.depth,
483 words: b.words,
484 pmus: b.pmus,
485 })
486 .collect();
487 let fu = if placement.pcu_used() == 0 {
488 0.0
489 } else {
490 placement.fu_utilization(t.pcu_stages)
491 };
492 let hops: Vec<usize> = mp
494 .edges
495 .iter()
496 .map(|e| {
499 placement
500 .routes
501 .iter()
502 .find(|r| r.edge == *e)
503 .map(|r| r.hops)
504 .unwrap_or(0)
505 })
506 .collect();
507 let choreo: Choreography = synthesize(mp, &hops, t, placement.budget);
508 let channels: Vec<ChannelRow> = choreo
509 .channels
510 .iter()
511 .map(|ch| ChannelRow {
512 writer: ch.edge.0,
513 reader: ch.edge.1,
514 hops: ch.hops,
515 forward_latency: ch.forward_latency,
516 backward_latency: ch.backward_latency,
517 n_buffer_depth: ch.n_buffer_depth,
518 credit_depth: ch.credit_depth,
519 depth: ch.depth,
520 binding: if ch.credit_depth >= ch.n_buffer_depth {
521 "credits"
522 } else {
523 "n_buffer"
524 }
525 .to_string(),
526 })
527 .collect();
528 let control = ControlRow {
529 nets: choreo.nets.len(),
530 hops: choreo.control_hops,
531 max_scalar_occupancy: choreo.max_scalar_occupancy,
532 scalar_link_budget: choreo.scalar_link_budget,
533 routed_statically: choreo.control_routed_statically,
534 };
535 WorkloadRow {
536 name: name.to_string(),
537 kind: kind.to_string(),
538 target: t.name.to_string(),
539 provenance: PROVENANCE_MODELED.to_string(),
540 par,
541 par_capacity,
542 router: placement.router.to_string(),
543 stages,
544 pipeline_ii: ii,
545 critical_stage,
546 buffers,
547 pcu_used: placement.pcu_used(),
548 pcu_total: placement.pcu_total,
549 pcu_utilization_pct: pct(placement.pcu_utilization()),
550 pmu_used: placement.pmu_used(),
551 pmu_total: placement.pmu_total,
552 pmu_utilization_pct: pct(placement.pmu_utilization()),
553 fu_utilization_pct: pct(fu),
554 ag_stages: ag_stages.len() * par,
555 total_hops: placement.total_hops(),
556 max_hops: placement.max_hops(),
557 max_link_occupancy: placement.max_link_occupancy,
558 vector_links: placement.budget.vector_links,
559 routed_statically: placement.routed_statically,
560 iterations,
561 fill_cycles: fill,
562 compute_cycles,
563 operand_stall_cycles,
564 dram_bytes,
565 dram_cycles,
566 modeled_cycles,
567 bound: bound.to_string(),
568 throughput_iters_per_cycle: r3(mp.throughput()),
569 gflops: None,
570 pct_of_peak_fp32: None,
571 operands: None,
572 channels,
573 control: Some(control),
574 certificate: None,
575 certificate_omitted: None,
576 vm_anchor: None,
577 gemm: None,
578 }
579}
580
581pub fn counted_loop_fabric(source: &str, fn_name: &str) -> Result<LoopFabric, String> {
586 let lf = quietly(|| loop_fabric_from_source(source, fn_name))
587 .map_err(|why| format!("`{fn_name}` is not a counted range loop the metapipeline model takes (Spatial `Foreach` over `lo..=hi`): {why}"))?;
588 if lf.seq_input.is_some() || lf.seq_heap_ptr.is_some() {
589 return Err("sequence loops (`Repeat for x in xs`): the DRAM → PMU tile-load stage is modeled for GEMM only so far".into());
590 }
591 if !lf.epilogue.is_empty() || !lf.prologue_heap.is_empty() {
592 return Err(
593 "a post-loop epilogue / prologue heap island is outside the single-stage loop model"
594 .into(),
595 );
596 }
597 Ok(lf)
598}
599
600pub fn lower_counted_loop(
604 source: &str,
605 fn_name: &str,
606 params: &[i64],
607 t: &PlasticineTarget,
608) -> Result<PlasticineArtifact, String> {
609 let lf = counted_loop_fabric(source, fn_name)?;
610 if lf.n_in != params.len() {
611 return Err(format!(
612 "`{fn_name}` takes {} parameters; {} sample values were given",
613 lf.n_in,
614 params.len()
615 ));
616 }
617 let model = UnitModel::plasticine_pcu(t.pcu_stages);
619 let stage = loop_stage(&lf, &model, fn_name);
620 let mp = Metapipeline {
621 stages: vec![stage],
622 edges: vec![],
623 };
624 let placement = place_unrolled(&mp, &[], &[], 1, t, LinkBudget::spade_default())
625 .map_err(|e| format!("{e:?}"))?;
626 let empty: [i64; 0] = [];
627 let lo = lf.lo.eval(params, &empty);
628 let hi = lf.hi.eval(params, &empty);
629 let iterations = if hi < lo { 0 } else { (hi - lo + 1) as usize };
630 let ii = mp.ii();
631 let fabric = eval_loop_fabric(&lf, params);
632 let at_ii = eval_loop_fabric_at_ii(&lf, params, ii, &model);
633 let below = if ii > 1 {
634 Some(eval_loop_fabric_at_ii(&lf, params, ii - 1, &model).hazards)
635 } else {
636 None
637 };
638 let vm = vm_value(source, fn_name, params)?;
639 let (certificate, cert_row, omitted) = certify_body(&lf);
640 let spatial = emit_spatial_counted_loop(&lf, fn_name)?;
641 let spatial_test =
642 emit_spatial_counted_loop_test(&lf, &format!("Logos_{fn_name}"), params, vm)?;
643 let mut row = row_from_placement(
644 fn_name,
645 "counted_loop",
646 t,
647 &mp,
648 &[],
649 1,
650 1,
651 &placement,
652 iterations,
653 0,
654 0,
655 );
656 row.vm_anchor = Some(VmAnchor {
657 params: params.to_vec(),
658 vm,
659 fabric,
660 pipelined_at_ii: at_ii.value,
661 hazards_at_ii: at_ii.hazards,
662 hazards_below_ii: below,
663 });
664 row.certificate = cert_row;
665 row.certificate_omitted = omitted;
666 Ok(PlasticineArtifact {
667 fn_name: fn_name.to_string(),
668 target: t.name,
669 loop_fabric: lf,
670 metapipeline: mp,
671 placement,
672 row,
673 certificate,
674 spatial,
675 spatial_test,
676 })
677}
678
679fn pmu_spec(t: &PlasticineTarget) -> PmuSpec {
680 PmuSpec {
681 banks: t.pmu_banks,
682 words_per_bank: words_per_pmu(t) / t.pmu_banks.max(1),
683 n_buffers: 2,
684 stages: t.pmu_stages,
685 }
686}
687
688pub fn gemm_tile_for(k: usize, t: &PlasticineTarget) -> usize {
691 let per_buffer = words_per_pmu(t) / 2;
692 let max_tile = per_buffer / (2 * k.max(1));
693 let by8 = max_tile / 8 * 8;
694 by8.max(1).min(max_tile.max(1))
695}
696
697pub fn gemm_metapipeline(
702 k: usize,
703 tile: usize,
704 t: &PlasticineTarget,
705) -> (Metapipeline, Vec<usize>, Vec<usize>) {
706 let lanes = t.pcu_lanes.max(1);
707 let tree = (usize::BITS - (lanes - 1).leading_zeros()) as usize + 1;
710 let spec = pmu_spec(t);
711 let a_read = AffineAccess {
712 base: 0,
713 stride: 1,
714 lanes,
715 };
716 let b_read = AffineAccess {
717 base: 0,
718 stride: tile,
719 lanes,
720 };
721 let mem_ii = memory_ii(&[a_read], strided_banking_for(1), &spec).max(memory_ii(
722 &[b_read],
723 strided_banking_for(tile),
724 &spec,
725 ));
726 let agu = address_datapath_stages(2, true);
727 let load = Stage {
728 name: "load: A row-panel + B column-panel, DRAM → PMU (perimeter AGs)".into(),
729 resource_ii: 1,
730 recurrence_ii: 0,
731 latency: agu,
732 cells: 2,
733 memory_ii: 1,
734 };
735 let mac = Stage {
736 name: format!(
737 "mac-reduce: {lanes}-lane multiply → {tree}-stage reduction tree → accumulator"
738 ),
739 resource_ii: 1,
740 recurrence_ii: 1,
741 latency: 1 + tree,
742 cells: 1 + tree,
743 memory_ii: mem_ii,
744 };
745 let store = Stage {
746 name: "store: C tile, PMU → DRAM (perimeter AG)".into(),
747 resource_ii: 1,
748 recurrence_ii: 0,
749 latency: agu,
750 cells: 1,
751 memory_ii: 1,
752 };
753 let mp = Metapipeline {
754 stages: vec![load, mac, store],
755 edges: vec![(0, 1), (1, 2)],
756 };
757 (mp, vec![tile * k + k * tile, tile * tile], vec![0, 2])
758}
759
760pub fn gemm_row(
766 name: &str,
767 m: usize,
768 n: usize,
769 k: usize,
770 tile: Option<usize>,
771 par: Option<usize>,
772 t: &PlasticineTarget,
773) -> Result<WorkloadRow, String> {
774 gemm_row_unrolled(name, m, n, k, tile, par, 1, 1, None, t)
775}
776
777#[allow(clippy::too_many_arguments)]
783pub fn gemm_row_unrolled(
784 name: &str,
785 m: usize,
786 n: usize,
787 k: usize,
788 tile: Option<usize>,
789 par: Option<usize>,
790 intra: usize,
791 kk: usize,
792 k_tile: Option<usize>,
793 t: &PlasticineTarget,
794) -> Result<WorkloadRow, String> {
795 let (intra, kk) = (intra.max(1), kk.max(1));
796 let k_tile = k_tile.unwrap_or(k).max(1).min(k.max(1));
797 let tile = tile
799 .unwrap_or_else(|| gemm_tile_for(k, t))
800 .max(1)
801 .min(m.max(1))
802 .min(n.max(1));
803 let (mp, words, ags) = gemm_metapipeline(k, tile, t);
804 let budget = LinkBudget::spade_default();
805 let capacity = max_par(&mp, &words, &ags, t).map_err(|e| format!("{e:?}"))?;
806 let lanes = t.pcu_lanes;
807 let col_tiles = ceil_div(n, tile);
808 let row_tiles = ceil_div(m, tile);
809 let par = match par {
812 Some(p) => p.max(1),
813 None => {
814 max_routable_par_within(&mp, &words, &ags, t, budget, col_tiles)
815 .map_err(|e| format!("{e:?}"))?
816 .0
817 }
818 };
819 let placement =
820 place_unrolled(&mp, &words, &ags, par, t, budget).map_err(|e| format!("{e:?}"))?;
821 let k_chunks = ceil_div(k, lanes);
822 let chunks_in_sequence_per_tile = ceil_div(ceil_div(k, k_tile), kk);
831 let chunk_load_cycles = ceil_div(k_tile * tile, lanes);
832 let chunk_compute_cycles = ceil_div(tile * tile * ceil_div(k_tile, lanes), intra).max(1);
833 let iters_per_tile = chunks_in_sequence_per_tile * chunk_compute_cycles;
834 let tiles_in_sequence = ceil_div(col_tiles, par) * row_tiles;
835 let iterations = tiles_in_sequence * iters_per_tile;
836 let chunks_in_sequence = tiles_in_sequence * chunks_in_sequence_per_tile;
837 let operand_stall_cycles =
838 chunk_load_cycles + chunks_in_sequence * chunk_load_cycles.saturating_sub(chunk_compute_cycles);
839 let hops = placement.routes.first().map(|r| r.hops).unwrap_or(1).max(1);
843 let b_bytes = 4 * k * n;
846 let a_bytes = 4 * m * k;
849 let a_stream = OperandStream {
850 name: "A".to_string(),
851 bytes: a_bytes,
852 consumers: par,
853 broadcast_hops: hops,
854 };
855 let a_dec = share_operand(&a_stream, t);
856 let a_copies = if a_dec.sharing == Sharing::Broadcast {
857 ceil_div(col_tiles, par)
858 } else {
859 col_tiles
860 };
861 let operands = vec![
862 OperandRow {
863 name: "B".to_string(),
864 sharing: "partitioned".to_string(),
865 consumers: par,
866 bytes_per_copy: b_bytes,
867 copies: 1,
868 bytes_moved: b_bytes,
869 dram_cycles_saved: 0,
870 fabric_cycles_added: 0,
871 why: "each instance owns disjoint column panels: every byte is fetched once, nothing to share".to_string(),
872 },
873 OperandRow {
874 name: "A".to_string(),
875 sharing: a_dec.sharing.label().to_string(),
876 consumers: par,
877 bytes_per_copy: a_bytes,
878 copies: a_copies,
879 bytes_moved: a_copies * a_bytes,
880 dram_cycles_saved: a_dec.dram_cycles_saved,
881 fabric_cycles_added: a_dec.fabric_cycles_added,
882 why: a_dec.why,
883 },
884 ];
885 let dram_bytes = b_bytes + a_copies * a_bytes + 4 * m * n;
886 let mut row = row_from_placement(
887 name,
888 "gemm",
889 t,
890 &mp,
891 &ags,
892 par,
893 capacity,
894 &placement,
895 iterations,
896 dram_bytes,
897 operand_stall_cycles,
898 );
899 row.operands = Some(operands);
900 let flops = 2.0 * m as f64 * n as f64 * k as f64;
901 let gflops = r3(flops * t.clock_ghz / row.modeled_cycles.max(1) as f64);
902 row.gflops = Some(gflops);
903 row.pct_of_peak_fp32 = Some(r3(gflops / (t.peak_fp32_tflops * 1000.0) * 100.0));
904 row.gemm = Some(GemmRow {
905 m,
906 n,
907 k,
908 tile_m: tile,
909 tile_n: tile,
910 col_tiles,
911 row_tiles,
912 k_chunks,
913 iters_per_tile,
914 lanes,
915 k_tile,
916 kk,
917 chunk_load_cycles,
918 chunk_compute_cycles,
919 chunks_in_sequence,
920 });
921 Ok(row)
922}
923
924pub fn workload_row(w: &Workload, t: &PlasticineTarget) -> Result<WorkloadRow, String> {
926 match &w.kind {
927 WorkloadKind::CountedLoop {
928 source,
929 fn_name,
930 params,
931 } => {
932 let mut row = lower_counted_loop(source, fn_name, params, t)?.row;
933 row.name = w.name.clone();
934 Ok(row)
935 }
936 WorkloadKind::Gemm { m, n, k, tile, par } => gemm_row(&w.name, *m, *n, *k, *tile, *par, t),
937 }
938}
939
940const SELECT_SRC: &str = "## To f (n: Int) -> Int:\n Let mutable s be 1.\n Repeat for i from 1 to n:\n If s is greater than 10:\n Set s to s - i.\n Else:\n Set s to s + i * 2.\n Return s.\n";
941
942const ARGMAX_SRC: &str = "## To argmax128 (x: Int) -> Int:\n Let mutable bv be 0.\n Let mutable bi be 1.\n Repeat for j from 1 to 128:\n Let v be (((x * j * 7) + (j * 13) + 5) % 101).\n If v is greater than bv:\n Set bv to v.\n Set bi to j.\n Return bi.\n";
946
947pub fn canonical_workloads() -> Vec<Workload> {
950 fn loop_prog(body: &str) -> String {
951 format!("## To f (n: Int) -> Int:\n Let mutable s be 1.\n Repeat for i from 1 to n:\n Set s to {body}.\n Return s.\n")
952 }
953 let counted = |name: &str, source: String, n: i64| Workload {
956 name: name.to_string(),
957 kind: WorkloadKind::CountedLoop {
958 source,
959 fn_name: "f".to_string(),
960 params: vec![n],
961 },
962 };
963 vec![
964 counted("loop-sum", loop_prog("s + i"), 1000),
965 counted("loop-affine", loop_prog("s * 3 + i"), 30),
966 counted("loop-affine-deep", loop_prog("(s * 3 + i) * 2"), 20),
967 counted("loop-mul-chain", loop_prog("((s * 2) * 3) * 5 + i"), 12),
968 counted("loop-no-carry", loop_prog("i * 7 + 1"), 1000),
969 counted("loop-select", SELECT_SRC.to_string(), 1000),
970 Workload {
971 name: "moe-argmax-round-128".to_string(),
972 kind: WorkloadKind::CountedLoop {
973 source: ARGMAX_SRC.to_string(),
974 fn_name: "argmax128".to_string(),
975 params: vec![3],
976 },
977 },
978 Workload {
979 name: "dot-product-1024".to_string(),
980 kind: WorkloadKind::Gemm {
981 m: 1,
982 n: 1,
983 k: 1024,
984 tile: Some(1),
985 par: Some(1),
986 },
987 },
988 Workload {
989 name: "gemm-640x640x640".to_string(),
990 kind: WorkloadKind::Gemm {
991 m: 640,
992 n: 640,
993 k: 640,
994 tile: None,
995 par: None,
996 },
997 },
998 Workload {
999 name: "gemm-2048x1024x512".to_string(),
1000 kind: WorkloadKind::Gemm {
1001 m: 2048,
1002 n: 1024,
1003 k: 512,
1004 tile: None,
1005 par: None,
1006 },
1007 },
1008 ]
1009}
1010
1011pub fn canonical_targets() -> Vec<PlasticineTarget> {
1013 vec![PlasticineTarget::isca17(), PlasticineTarget::sara_20x20()]
1014}
1015
1016pub fn not_modeled() -> Vec<(String, String)> {
1018 let s = |a: &str, b: &str| (a.to_string(), b.to_string());
1019 vec![
1020 s(
1021 "data-dependent `while` loops (Collatz, the data_dependent_control corpus)",
1022 "a while loop's trip count is a runtime predicate; the metapipeline model takes counted loops (Spatial `Foreach`). These programs are proven ≡VM on the fabric's decision / loop mechanisms; a stream-controller (Spatial `Stream`/FSM) II model is the P5 follow-on",
1023 ),
1024 s(
1025 "route128 top-8-of-128 MoE routing (whole function)",
1026 "three nested while loops over a heap-resident Seq — proven ≡VM on the fabric call-stack mechanism; the nested counter-chain controller model (Spatial `Foreach` nests with PMU-resident state) is P5. The argmax round it repeats eight times is modeled above",
1027 ),
1028 s(
1029 "tiny transformer block / Qwen3-30B MoE routing at real scale",
1030 "nested loops with heap-resident tensors — same nested-controller gap as route128",
1031 ),
1032 s(
1033 "scalar sequence loops (`Repeat for x in xs`)",
1034 "the DRAM → PMU tile-load stage is modeled for GEMM only; a parameter sequence's load stage comes with the nested-controller model",
1035 ),
1036 ]
1037}
1038
1039fn target_summary(t: &PlasticineTarget) -> Value {
1040 json!({
1041 "name": t.name,
1042 "mesh": format!("{}x{} checkerboard", t.pcu_rows, t.pcu_cols),
1043 "pcus": t.pcus(),
1044 "pmus": t.pmus(),
1045 "pcu_lanes": t.pcu_lanes,
1046 "pcu_stages": t.pcu_stages,
1047 "pcu_regs_per_stage": t.pcu_regs_per_stage,
1048 "pmu_banks": t.pmu_banks,
1049 "pmu_kib": t.pmu_kib,
1050 "pmu_stages": t.pmu_stages,
1051 "scratchpad_kib": t.scratchpad_kib(),
1052 "dram": { "channels": t.dram.channels, "kind": t.dram.kind, "gbps": t.dram.gbps },
1053 "clock_ghz": t.clock_ghz,
1054 "peak_fp32_tflops": r3(t.peak_fp32_tflops),
1055 "area_mm2": t.area_mm2,
1056 "power_w": t.power_w,
1057 "cite": t.cite,
1058 })
1059}
1060
1061pub fn plasticine_report(workloads: &[Workload], targets: &[PlasticineTarget]) -> Value {
1064 let mut rows = Vec::with_capacity(workloads.len() * targets.len());
1065 for t in targets {
1066 for w in workloads {
1067 let row = workload_row(w, t)
1068 .unwrap_or_else(|e| panic!("canonical workload `{}` on {}: {e}", w.name, t.name));
1069 rows.push(serde_json::to_value(row).expect("a row serializes"));
1070 }
1071 }
1072 let not_modeled: Vec<Value> = not_modeled()
1073 .into_iter()
1074 .map(|(name, reason)| json!({ "name": name, "reason": reason }))
1075 .collect();
1076 json!({
1077 "_generated_by": "the private place-and-route engine's report emitter, emit_plasticine_report (codegen_sva::plasticine_report) — never hand-edited; plasticine_report_lock regenerates it byte-equal",
1078 "provenance": PROVENANCE_MODELED,
1079 "honesty": "Every number is the paper-parameterized model's (ISCA'17 / SARA descriptors, Spatial's II definition, the PMU banking model, the checkerboard placer). Nothing here ran on a Plasticine simulator or on silicon; Tungsten rows land beside these under provenance `simulated_tungsten`.",
1080 "model": {
1081 "ii": "Spatial (Koeplinger et al., PLDI 2018, §4): stage II = max(resource II, recurrence II, memory II); pipeline II = the slowest stage; N-buffer depth = reader − writer + 1",
1082 "pcu": "ISCA'17 §3.1: a stage of depth D chains ceil(D / 6) PCUs, each 16 lanes wide; the reduction tree is log2(lanes)+1 stages",
1083 "pmu": "ISCA'17 §3.2: 16 banks × 16 KB; strided banking at the access stride is conflict-free for 16 lanes; N-buffers partition the address space",
1084 "control": "every inter-stage channel carries a data-valid net forward and a space-available net backward (codegen_sva::choreography); its credit depth is the bandwidth-delay product ceil((forward + backward latency) / II) and the shipped depth is the deeper of that and Spatial's writer-to-reader N-buffer rule; control nets are budgeted on the SCALAR network against the PCU's control ports, separately from the vector network the data crosses. `plasticine_choreography_lock` proves the depth tight: at it the pipeline runs at exactly 1/II, and one credit less on any channel loses throughput",
1085 "placement": "row-balanced checkerboard placement; static routing by the private place-and-route engine over the mesh's routing-resource graph (one wire per channel of every DIRECTED link at Spade's vlink 2, unit vector ports, perimeter DRAM ports): the negotiated-congestion rung, the exact SAT rung on a stall, and the independent legality checker as the verdict; address-generator stages attach at the left/right perimeter switches (a port counts one hop)",
1086 "dram": "cycles = bytes × clock_GHz / (GB/s); a GEMM tiling moves B column-panels once, A once per column panel, C once",
1087 "cycles": "fill (Σ stage latency) + (iterations − 1) × II per unrolled instance; modeled cycles = max(compute, DRAM)",
1088 },
1089 "targets": targets.iter().map(target_summary).collect::<Vec<_>>(),
1090 "rows": rows,
1091 "not_modeled": not_modeled,
1092 "published_reference": {
1093 "note": "Spatial PLDI'18 Table 4 (Plasticine column) — the paper's numbers, not ours: the credibility target the Tungsten lane reproduces before our rows are compared to it",
1094 "spatial_pldi18_table4_plasticine": {
1095 "GEMM": { "pcu_pct": 96.8, "pmu_pct": 64.1, "ag_pct": 11.7, "time_ms": 15.98 },
1096 "BlackScholes": { "pcu_pct": 73.4, "pmu_pct": 10.9, "ag_pct": 20.6 },
1097 "GDA": { "pcu_pct": 95.3, "pmu_pct": 73.4, "ag_pct": 38.2 },
1098 "KMeans": { "pcu_pct": 89.1, "pmu_pct": 57.8, "ag_pct": 17.6 },
1099 "TQ6": { "pcu_pct": 29.7, "pmu_pct": 37.5, "ag_pct": 70.6 },
1100 },
1101 },
1102 })
1103}
1104
1105pub fn target_for_mesh(rows: usize, cols: usize) -> Option<PlasticineTarget> {
1109 if (rows, cols) == (20, 20) {
1110 return Some(PlasticineTarget::tungsten_20x20_ddr4());
1111 }
1112 canonical_targets()
1113 .into_iter()
1114 .find(|t| t.pcu_rows == rows && t.pcu_cols == cols)
1115}
1116
1117pub fn calibration_row(t: &PlasticineTarget, row: &Value) -> Result<Value, String> {
1124 if row["kind"].as_str() == Some("counted_loop") {
1125 return calibration_row_loop(t, row);
1126 }
1127 if let Some(why) = unmodeled(row) {
1128 return Err(format!("{}: {why}", row["key"]));
1129 }
1130 if let Some(why) = routed_failure(row) {
1131 return Err(format!("{}: {why}", row["key"]));
1132 }
1133 let dot = row["kind"].as_str() == Some("dot");
1134 let par = |p: &str| {
1135 let key = if dot && p == "loop_kk" { "op" } else { p };
1136 row["params"][key].as_u64().unwrap_or(1)
1137 };
1138 let shape = &row["shape"];
1139 let (m, n, k) = (
1140 shape["m"].as_u64().ok_or("shape.m")? as usize,
1141 shape["n"].as_u64().ok_or("shape.n")? as usize,
1142 shape["k"].as_u64().ok_or("shape.k")? as usize,
1143 );
1144 let ts = row["params"]["ts"].as_u64().ok_or("params.ts")? as usize;
1145 let ip = row["params"]["ip"].as_u64().ok_or("params.ip")? as usize;
1146 let mesh_par: u64 = ["loop_ii", "loop_jj"].iter().map(|p| par(p)).product();
1149 let intra: u64 = ["loop_i", "loop_j"].iter().map(|p| par(p)).product();
1150 let kk = par("loop_kk");
1151 let mut app_t = t.clone();
1159 if ip >= 1 {
1160 app_t.pcu_lanes = ip.min(t.pcu_lanes);
1161 }
1162 let modeled = gemm_row_unrolled(
1163 row["app"].as_str().unwrap_or("app"),
1164 m,
1165 n,
1166 k,
1167 Some(ts),
1168 Some(mesh_par.max(1) as usize),
1169 intra.max(1) as usize,
1170 kk.max(1) as usize,
1171 Some(ts),
1172 &app_t,
1173 )?;
1174 let simulated = row["hybrid_network"]["cycles"]
1175 .as_u64()
1176 .ok_or("hybrid_network.cycles")? as usize;
1177 let ideal = row["ideal_network"]["cycles"]
1178 .as_u64()
1179 .ok_or("ideal_network.cycles")? as usize;
1180 let ratio = modeled.modeled_cycles as f64 / simulated as f64;
1181 Ok(json!({
1182 "key": row["key"],
1183 "app": row["app"],
1184 "kind": if dot { "dot" } else { "gemm" },
1185 "target": t.name,
1186 "shape": { "m": m, "n": n, "k": k, "tile": ts, "lanes": ip },
1187 "modeled": {
1188 "cycles": modeled.modeled_cycles,
1189 "bound": modeled.bound,
1190 "pipeline_ii": modeled.pipeline_ii,
1191 "lanes": modeled.gemm.as_ref().map(|g| g.lanes),
1192 "operand_stall_cycles": modeled.operand_stall_cycles,
1193 "compute_pcus": modeled.pcu_used,
1194 "pmus": modeled.pmu_used,
1195 "ag_stages": modeled.ag_stages,
1196 "provenance": PROVENANCE_MODELED,
1197 },
1198 "simulated": {
1199 "cycles_hybrid_network": simulated,
1200 "cycles_ideal_network": ideal,
1201 "pir_pcus": row["pir"]["PCU"]["used"],
1202 "pir_pmus": row["pir"]["PMU"]["used"],
1203 "pir_dags": row["pir"]["DAG"]["used"],
1204 "pir_mcs": row["pir"]["MC"]["used"],
1205 "plastiroute_best_score": row["plastiroute"]["best_score"],
1206 "pass": row["hybrid_network"]["pass"],
1207 "provenance": PROVENANCE_SIMULATED,
1208 },
1209 "modeled_over_simulated": r3(ratio),
1210 "difference_pct": r3((ratio - 1.0).abs() * 100.0),
1211 "lanes_agree": modeled.gemm.as_ref().map(|g| g.lanes == ip).unwrap_or(false),
1212 }))
1213}
1214
1215fn calibration_row_loop(t: &PlasticineTarget, row: &Value) -> Result<Value, String> {
1219 let source = row["logos_source"].as_str().ok_or("logos_source")?;
1220 let fn_name = row["fn_name"].as_str().ok_or("fn_name")?;
1221 let params: Vec<i64> = row["params"]
1222 .as_array()
1223 .ok_or("params")?
1224 .iter()
1225 .map(|v| v.as_i64().ok_or("params: i64"))
1226 .collect::<Result<_, _>>()?;
1227 let art = lower_counted_loop(source, fn_name, ¶ms, t)?;
1228 let modeled = &art.row;
1229 let simulated = row["hybrid_network"]["cycles"]
1230 .as_u64()
1231 .ok_or("hybrid_network.cycles")? as usize;
1232 let ideal = row["ideal_network"]["cycles"]
1233 .as_u64()
1234 .ok_or("ideal_network.cycles")? as usize;
1235 let ratio = modeled.modeled_cycles as f64 / simulated as f64;
1236 let anchor = modeled.vm_anchor.as_ref().ok_or("vm anchor")?;
1237 Ok(json!({
1238 "key": row["key"],
1239 "app": row["app"],
1240 "kind": "counted_loop",
1241 "target": t.name,
1242 "loop": { "fn": fn_name, "params": params, "iterations": modeled.iterations, "vm_answer": anchor.vm },
1243 "modeled": {
1244 "cycles": modeled.modeled_cycles,
1245 "pipeline_ii": modeled.pipeline_ii,
1246 "fill_cycles": modeled.fill_cycles,
1247 "compute_pcus": modeled.pcu_used,
1248 "provenance": PROVENANCE_MODELED,
1249 },
1250 "simulated": {
1251 "cycles_hybrid_network": simulated,
1252 "cycles_ideal_network": ideal,
1253 "pir_pcus": row["pir"]["PCU"]["used"],
1254 "pir_pmus": row["pir"]["PMU"]["used"],
1255 "pass": row["hybrid_network"]["pass"],
1256 "provenance": PROVENANCE_SIMULATED,
1257 },
1258 "modeled_over_simulated": r3(ratio),
1259 "difference_pct": r3((ratio - 1.0).abs() * 100.0),
1260 "simulated_cycles_per_iteration": r3(simulated as f64 / modeled.iterations.max(1) as f64),
1261 }))
1262}
1263
1264pub const UNMODELED_KINDS: &[(&str, &str)] = &[
1267 (
1268 "outer",
1269 "the model has no outer-product row: its pipeline computes a ts1 x ts2 product tile and stores it whole, a \
1270 store-bound pipeline the GEMM metapipeline (a MemReduce over K) does not describe",
1271 ),
1272 (
1273 "tpchq6",
1274 "the model has no guarded-sum row: TPC-H Q6 streams four columns and selects per element, which the two-stream \
1275 1 x 1 x N GEMM a dot product is modeled as does not describe",
1276 ),
1277 (
1278 "gda",
1279 "the model has no scatter-accumulation row: GDA centres each row and adds its outer product to a C x C tile \
1280 under two nested MemReduces, a pipeline the GEMM metapipeline does not describe",
1281 ),
1282 (
1283 "blackscholes",
1284 "the model has no elementwise-map row: Black-Scholes streams six columns through FIFOs into a chain of \
1285 several hundred scalar operations per element, a dataflow graph the GEMM metapipeline does not describe",
1286 ),
1287 (
1288 "kmeans",
1289 "the model has no clustering row: k-means runs epochs of a distance reduce per point and centroid, a min and an \
1290 index reduce, and a one-hot MemReduce into the centroids, a pipeline the GEMM metapipeline does not describe",
1291 ),
1292 (
1293 "spatial_app",
1294 "one of Spatial's own apps whose twin the compiler does not print yet: the model describes the shapes the \
1295 compiler lowers (GEMMs, dot products, counted loops), not an arbitrary Spatial program",
1296 ),
1297];
1298
1299pub fn routed_failure(row: &Value) -> Option<String> {
1304 row["hybrid_network"]["why"].as_str().map(|why| format!("its routed-network simulation {why}"))
1305}
1306
1307pub fn unmodeled(row: &Value) -> Option<&'static str> {
1308 let kind = row["kind"].as_str()?;
1309 UNMODELED_KINDS
1310 .iter()
1311 .find(|(k, _)| *k == kind)
1312 .map(|(_, why)| *why)
1313}
1314
1315pub fn calibration(toolchain_ledger: &Value) -> Result<Value, String> {
1319 let mut rows = Vec::new();
1320 let mut uncalibrated = Vec::new();
1321 for row in toolchain_ledger["rows"]
1322 .as_array()
1323 .ok_or("toolchain ledger has no rows array")?
1324 {
1325 if let Some(why) = unmodeled(row) {
1326 uncalibrated.push(json!({ "key": row["key"], "app": row["app"], "kind": row["kind"], "why": why }));
1327 continue;
1328 }
1329 if let Some(why) = routed_failure(row) {
1330 uncalibrated.push(json!({ "key": row["key"], "app": row["app"], "kind": row["kind"], "why": why }));
1331 continue;
1332 }
1333 let mesh = &row["mesh"];
1334 let (r, c) = (
1335 mesh["rows"].as_u64().ok_or("mesh.rows")? as usize,
1336 mesh["cols"].as_u64().ok_or("mesh.cols")? as usize,
1337 );
1338 let t = target_for_mesh(r, c).ok_or_else(|| {
1339 format!(
1340 "toolchain row {} ran on a {r}x{c} mesh we have no descriptor for",
1341 row["key"]
1342 )
1343 })?;
1344 rows.push(calibration_row(&t, row)?);
1345 }
1346 Ok(json!({
1347 "_generated_by": "the private place-and-route engine's report emitter, emit_plasticine_report (codegen_sva::plasticine_report::calibration) — joins benchmarks/silicon/plasticine_toolchain.json with the model; never hand-edited",
1348 "what": "For each app the public Plasticine toolchain simulated (Spatial → PIR → plastiroute → Tungsten), our MODELED row for the same shape, tile and unroll on the same mesh — the calibration an RDU engineer asks for before trusting a model. `simulated` numbers are the toolchain's (a cycle-accurate simulation of the paper's architecture, not silicon); `modeled` numbers are ours.",
1349 "tolerance_pct": CALIBRATION_TOLERANCE_PCT,
1350 "rows": rows,
1351 "uncalibrated": uncalibrated,
1352 }))
1353}
1354
1355pub const CALIBRATION_TOLERANCE_PCT: f64 = 15.0;
1357
1358pub fn render_report(v: &Value) -> String {
1360 format!(
1361 "{}\n",
1362 serde_json::to_string_pretty(v).expect("the report serializes")
1363 )
1364}
1365
1366pub fn logos_gemm_program(m: usize, n: usize, k: usize) -> String {
1369 format!(
1370 "## Main\nLet rows be {m}.\nLet cols be {n}.\nLet mid be {k}.\nLet mutable a be a new Seq of Int.\nLet mutable b be a new Seq of Int.\nLet mutable c be a new Seq of Int.\nLet mutable p be 0.\nWhile p is less than rows * mid:\n Push 1 to a.\n Set p to p + 1.\nSet p to 0.\nWhile p is less than mid * cols:\n Push 1 to b.\n Set p to p + 1.\nSet p to 0.\nWhile p is less than rows * cols:\n Push 0 to c.\n Set p to p + 1.\nLet mutable i be 0.\nWhile i is less than rows:\n Let mutable k be 0.\n While k is less than mid:\n Let mutable j be 0.\n While j is less than cols:\n Let idx be i * cols + j + 1.\n Set item idx of c to item idx of c + item (i * mid + k + 1) of a * item (k * cols + j + 1) of b.\n Set j to j + 1.\n Set k to k + 1.\n Set i to i + 1.\nShow item 1 of c.\n"
1371 )
1372}
1373
1374pub fn lower_gemm_source(
1378 source: &str,
1379 ts: usize,
1380 its: usize,
1381 ip: usize,
1382) -> Result<(usize, usize, usize, String, String), String> {
1383 let (m, n, k) = crate::ui_bridge::with_parsed_program(source, |parsed, interner| {
1384 let (stmts, _types, _policies) = parsed.map_err(|e| format!("parse: {e:?}"))?;
1385 let spec = crate::tensor_recognize::recognize_matmul(stmts, interner)
1386 .map_err(|e| format!("not a matmul the tensor recognizer reads: {e:?}"))?;
1387 Ok::<_, String>((spec.m, spec.n, spec.k))
1388 })?;
1389 let class = format!("Logos_gemm_{m}x{n}x{k}");
1390 let spatial = emit_spatial_gemm_test(m, n, k, ts, its, ip, &class)?;
1391 Ok((m, n, k, class, spatial))
1392}
1393
1394pub fn logos_dot_program(n: usize) -> String {
1396 format!(
1397 "## Main\nLet n be {n}.\nLet mutable a be a new Seq of Int.\nLet mutable b be a new Seq of Int.\nLet mutable p be 0.\nWhile p is less than n:\n Push p % 7 to a.\n Push p % 5 to b.\n Set p to p + 1.\nLet mutable s be 0.\nLet mutable i be 0.\nWhile i is less than n:\n Set s to s + item (i + 1) of a * item (i + 1) of b.\n Set i to i + 1.\nShow s.\n"
1398 )
1399}
1400
1401pub fn logos_outer_program(rows: usize, cols: usize) -> String {
1404 format!(
1405 "## Main\nLet rows be {rows}.\nLet cols be {cols}.\nLet mutable a be a new Seq of Int.\nLet mutable b be a new Seq of Int.\nLet mutable c be a new Seq of Int.\nLet mutable p be 0.\nWhile p is less than rows:\n Push p % 7 to a.\n Set p to p + 1.\nSet p to 0.\nWhile p is less than cols:\n Push p % 5 to b.\n Set p to p + 1.\nSet p to 0.\nWhile p is less than rows * cols:\n Push 0 to c.\n Set p to p + 1.\nLet mutable i be 0.\nWhile i is less than rows:\n Let mutable j be 0.\n While j is less than cols:\n Let idx be i * cols + j + 1.\n Set item idx of c to item (i + 1) of a * item (j + 1) of b.\n Set j to j + 1.\n Set i to i + 1.\nLet mutable sum be 0.\nSet p to 0.\nWhile p is less than rows * cols:\n Set sum to sum + item (p + 1) of c * (p + 1).\n Set p to p + 1.\nShow sum.\n"
1406 )
1407}
1408
1409fn emit_zip_twin(
1413 stmts: &[logicaffeine_language::ast::stmt::Stmt],
1414 interner: &crate::intern::Interner,
1415 zip: &ZipSum,
1416 family: &str,
1417 p: ZipPar,
1418) -> Result<(usize, String, String), String> {
1419 let columns: Vec<ZipColumn> = zip
1420 .operands
1421 .iter()
1422 .map(|o| ZipColumn {
1423 name: interner.resolve(*o).to_string(),
1424 fill: crate::tensor_recognize::recognize_fill(stmts, *o),
1425 })
1426 .collect();
1427 let class = format!("Logos_{family}_{}_ts{}_ip{}_op{}", zip.n, p.ts, p.ip, p.op);
1428 let spatial = emit_spatial_zip_sum_test(zip.n, &columns, &zip.term, p, &class)?;
1429 Ok((zip.n, class, spatial))
1430}
1431
1432pub fn lower_dot_source(source: &str, p: ZipPar) -> Result<(usize, String, String), String> {
1436 crate::ui_bridge::with_parsed_program(source, |parsed, interner| {
1437 let (stmts, _types, _policies) = parsed.map_err(|e| format!("parse: {e:?}"))?;
1438 let dot = crate::tensor_recognize::recognize_dot(stmts, interner)
1439 .map_err(|e| format!("not a dot product the tensor recognizer reads: {e:?}"))?;
1440 let operands = if dot.a == dot.b { vec![dot.a] } else { vec![dot.a, dot.b] };
1441 let read = |s| ZipTerm::Read(operands.iter().position(|o| *o == s).expect("an operand"));
1442 let zip = ZipSum {
1443 n: dot.n,
1444 term: ZipTerm::Op(ZipOp::Mul, Box::new(read(dot.a)), Box::new(read(dot.b))),
1445 operands,
1446 acc: dot.acc,
1447 };
1448 emit_zip_twin(stmts, interner, &zip, "dot", p)
1449 })
1450}
1451
1452pub fn tpchq6_query() -> ZipTerm {
1457 let (date, disct, quant, price) = (|| ZipTerm::Read(0), || ZipTerm::Read(1), || ZipTerm::Read(2), || ZipTerm::Read(3));
1458 let op = |o, l, r| ZipTerm::Op(o, Box::new(l), Box::new(r));
1459 let valid = [
1460 op(ZipOp::Gt, date(), ZipTerm::Const(0)),
1461 op(ZipOp::Lt, date(), ZipTerm::Const(9999)),
1462 op(ZipOp::Ge, disct(), ZipTerm::Const(0)),
1463 op(ZipOp::Le, disct(), ZipTerm::Const(9999)),
1464 op(ZipOp::Lt, quant(), ZipTerm::Const(24)),
1465 ]
1466 .into_iter()
1467 .reduce(|a, b| op(ZipOp::And, a, b))
1468 .expect("five conditions");
1469 ZipTerm::Select(Box::new(valid), Box::new(op(ZipOp::Mul, price(), disct())), Box::new(ZipTerm::Const(0)))
1470}
1471
1472pub fn logos_tpchq6_program(n: usize) -> String {
1475 format!(
1476 "## Main\nLet n be {n}.\nLet mutable dates be a new Seq of Int.\nLet mutable quants be a new Seq of Int.\nLet mutable discts be a new Seq of Int.\nLet mutable prices be a new Seq of Int.\nLet mutable p be 0.\nWhile p is less than n:\n Push p % 256 to dates.\n Push p % 256 to quants.\n Push p % 256 to discts.\n Push p % 256 to prices.\n Set p to p + 1.\nLet mutable revenue be 0.\nLet mutable i be 0.\nWhile i is less than n:\n Let date be item (i + 1) of dates.\n Let disct be item (i + 1) of discts.\n Let quant be item (i + 1) of quants.\n If date is greater than 0 and date is less than 9999 and disct is at least 0 and disct is at most 9999 and quant is less than 24:\n Set revenue to revenue + item (i + 1) of prices * disct.\n Set i to i + 1.\nShow revenue.\n"
1477 )
1478}
1479
1480pub fn lower_tpchq6_source(source: &str, p: ZipPar) -> Result<(usize, String, String), String> {
1483 crate::ui_bridge::with_parsed_program(source, |parsed, interner| {
1484 let (stmts, _types, _policies) = parsed.map_err(|e| format!("parse: {e:?}"))?;
1485 let query = tpchq6_query();
1486 let zip = crate::tensor_recognize::recognize_zip_sum_where(stmts, interner, &|z| z.term == query)
1487 .map_err(|e| format!("not TPC-H Q6 as the tensor recognizer reads it: {e:?}"))?;
1488 emit_zip_twin(stmts, interner, &zip, "tpchq6", p)
1489 })
1490}
1491
1492pub fn gda_query() -> (Vec<Axis>, ZipTerm) {
1496 let op = |o, l, r| ZipTerm::Op(o, Box::new(l), Box::new(r));
1497 let label = op(ZipOp::Eq, ZipTerm::Read(0), ZipTerm::Const(1));
1498 let mean = ZipTerm::Select(Box::new(label), Box::new(ZipTerm::Read(2)), Box::new(ZipTerm::Read(3)));
1499 (vec![Axis::Row, Axis::RowCol, Axis::Col, Axis::Col], op(ZipOp::Sub, ZipTerm::Read(1), mean))
1500}
1501
1502pub fn logos_gda_program(rows: usize, cols: usize) -> String {
1507 format!(
1508 "## Main\nLet rows be {rows}.\nLet cols be {cols}.\nLet mutable x be a new Seq of Int.\nLet mutable y be a new Seq of Int.\nLet mutable mu0 be a new Seq of Int.\nLet mutable mu1 be a new Seq of Int.\nLet mutable sigma be a new Seq of Int.\nLet mutable sub be a new Seq of Int.\nLet mutable p be 0.\nWhile p is less than rows * cols:\n Push p % 256 to x.\n Set p to p + 1.\nSet p to 0.\nWhile p is less than rows:\n Push p % 256 to y.\n Set p to p + 1.\nSet p to 0.\nWhile p is less than cols:\n Push p % 2 to mu0.\n Push p % 2 to mu1.\n Push 0 to sub.\n Set p to p + 1.\nSet p to 0.\nWhile p is less than cols * cols:\n Push 0 to sigma.\n Set p to p + 1.\nLet mutable r be 0.\nWhile r is less than rows:\n Let label be item (r + 1) of y.\n Let mutable c be 0.\n While c is less than cols:\n If label is 1:\n Set item (c + 1) of sub to item (r * cols + c + 1) of x - item (c + 1) of mu1.\n Otherwise:\n Set item (c + 1) of sub to item (r * cols + c + 1) of x - item (c + 1) of mu0.\n Set c to c + 1.\n Let mutable i be 0.\n While i is less than cols:\n Let mutable j be 0.\n While j is less than cols:\n Let idx be i * cols + j + 1.\n Set item idx of sigma to item idx of sigma + item (i + 1) of sub * item (j + 1) of sub.\n Set j to j + 1.\n Set i to i + 1.\n Set r to r + 1.\nLet mutable sum be 0.\nSet p to 0.\nWhile p is less than cols * cols:\n Set sum to sum + item (p + 1) of sigma * (p + 1).\n Set p to p + 1.\nShow sum.\n"
1509 )
1510}
1511
1512pub fn lower_gda_source(source: &str, p: GdaPar) -> Result<(usize, usize, String, String), String> {
1516 crate::ui_bridge::with_parsed_program(source, |parsed, interner| {
1517 let (stmts, _types, _policies) = parsed.map_err(|e| format!("parse: {e:?}"))?;
1518 let spec = crate::tensor_recognize::recognize_scatter(stmts, interner)
1519 .map_err(|e| format!("not a scatter accumulation the tensor recognizer reads: {e:?}"))?;
1520 let (axes, query) = gda_query();
1521 if spec.term != query || spec.operands.iter().map(|(_, a)| *a).collect::<Vec<_>>() != axes {
1522 return Err("a scatter accumulation, but not GDA's covariance step".to_string());
1523 }
1524 let columns: Vec<GdaColumn> = spec
1525 .operands
1526 .iter()
1527 .map(|(o, axis)| GdaColumn {
1528 name: interner.resolve(*o).to_string(),
1529 axis: *axis,
1530 fill: crate::tensor_recognize::recognize_fill(stmts, *o),
1531 })
1532 .collect();
1533 let class = format!(
1534 "Logos_gda_{}x{}_ts{}_op{}_mp{}x{}_ip{}",
1535 spec.rows, spec.cols, p.ts, p.op, p.mp1, p.mp2, p.ip
1536 );
1537 let spatial = emit_spatial_gda_test(spec.rows, spec.cols, &columns, &spec.term, p, &class)?;
1538 Ok((spec.rows, spec.cols, class, spatial))
1539 })
1540}
1541
1542pub fn blackscholes_query() -> ZipTerm {
1547 let r = ZipTerm::real;
1548 let op = |o, l: ZipTerm, r: ZipTerm| ZipTerm::Op(o, Box::new(l), Box::new(r));
1549 let (add, sub) = (|a, b| op(ZipOp::Add, a, b), |a, b| op(ZipOp::Sub, a, b));
1550 let (mul, div) = (|a, b| op(ZipOp::Mul, a, b), |a, b| op(ZipOp::Div, a, b));
1551 let lt = |a, b| op(ZipOp::Lt, a, b);
1552 let sq = |a| op(ZipOp::Pow, a, ZipTerm::Const(2));
1553 let neg = |a| sub(ZipTerm::Const(0), a);
1554 let sel = |c, a, b| ZipTerm::Select(Box::new(c), Box::new(a), Box::new(b));
1555 let exp_taylor = |x: ZipTerm| {
1556 let poly = add(
1557 add(
1558 add(add(add(r(1.0), x.clone()), div(mul(x.clone(), x.clone()), r(2.0))), div(mul(mul(x.clone(), x.clone()), x.clone()), r(6.0))),
1559 div(mul(mul(mul(x.clone(), x.clone()), x.clone()), x.clone()), r(24.0)),
1560 ),
1561 div(mul(mul(mul(mul(x.clone(), x.clone()), x.clone()), x.clone()), x.clone()), r(120.0)),
1562 );
1563 sel(lt(x.clone(), r(-3.5)), r(0.0), sel(lt(x.clone(), r(-1.2)), add(mul(x, r(0.1)), r(0.35)), poly))
1564 };
1565 let log_taylor = |x: ZipTerm| {
1566 let xx = sub(x, r(1.0));
1567 sub(
1568 add(sub(xx.clone(), div(mul(xx.clone(), xx.clone()), r(2.0))), div(mul(mul(xx.clone(), xx.clone()), xx.clone()), r(3.0))),
1569 div(mul(mul(mul(xx.clone(), xx.clone()), xx.clone()), xx), r(4.0)),
1570 )
1571 };
1572 let sqrt_approx = |x: ZipTerm| {
1573 let m1 = || sub(x.clone(), r(1.0));
1574 let taylor = add(sub(add(r(1.0), div(m1(), r(2.0))), div(mul(m1(), m1()), r(8.0))), div(mul(mul(m1(), m1()), m1()), r(16.0)));
1575 let line = |a: f64, b: f64| add(mul(x.clone(), r(a)), r(b));
1576 sel(
1577 lt(x.clone(), r(2.0)),
1578 taylor,
1579 sel(
1580 lt(x.clone(), r(10.0)),
1581 line(0.22, 1.0),
1582 sel(
1583 lt(x.clone(), r(100.0)),
1584 line(0.08, 2.5),
1585 sel(lt(x.clone(), r(1000.0)), line(0.028, 8.0), sel(lt(x.clone(), r(10000.0)), line(0.008, 20.0), line(0.0002, 300.0))),
1586 ),
1587 ),
1588 )
1589 };
1590 let cndf = |x: ZipTerm| {
1591 let ax = ZipTerm::Abs(Box::new(x.clone()));
1592 let n_prime = mul(exp_taylor(mul(sq(ax.clone()), r(-0.05))), r(0.39894228040143270286));
1593 let k = div(r(1.0), add(mul(ax, r(0.2316419)), r(1.0)));
1594 let k2 = sq(k.clone());
1595 let k3 = mul(k2.clone(), k.clone());
1596 let k4 = mul(k3.clone(), k.clone());
1597 let k5 = mul(k4.clone(), k.clone());
1598 let l1 = add(
1599 add(add(add(mul(k2, r(-0.356563782)), mul(k3, r(1.781477937))), mul(k4, r(-1.821255978))), mul(k5, r(1.330274429))),
1600 mul(k, r(0.31938153)),
1601 );
1602 let l0 = mul(l1, n_prime);
1603 sel(lt(x, r(0.0)), l0.clone(), add(neg(l0), r(1.0)))
1604 };
1605 let (price, strike, rate, vol, time, otype) =
1606 (ZipTerm::Read(0), ZipTerm::Read(1), ZipTerm::Read(2), ZipTerm::Read(3), ZipTerm::Read(4), ZipTerm::Read(5));
1607 let log_term = log_taylor(div(price.clone(), strike.clone()));
1608 let power_term = mul(sq(vol.clone()), r(0.5));
1609 let num = add(mul(add(rate.clone(), power_term), time.clone()), log_term);
1610 let den = mul(vol, sqrt_approx(time.clone()));
1611 let d = div(num, sq(den.clone()));
1612 let (nd1, nd2) = (cndf(d.clone()), cndf(sub(d, den)));
1613 let future = mul(strike, exp_taylor(mul(neg(rate), time)));
1614 let (neg_nd1, neg_nd2) = (add(neg(nd1.clone()), r(1.0)), add(neg(nd2.clone()), r(1.0)));
1615 let price1 = sub(mul(price.clone(), nd1), mul(future.clone(), nd2));
1616 let price2 = sub(mul(future, neg_nd2), mul(price, neg_nd1));
1617 sel(op(ZipOp::Eq, otype, ZipTerm::Const(0)), price2, price1)
1618}
1619
1620pub fn logos_blackscholes_program(n: usize) -> String {
1626 format!(
1627 "## To expTaylor (x: Real) -> Real:\n If x is less than -3.5:\n Return 0.0.\n If x is less than -1.2:\n Return x * 0.1 + 0.35.\n Return 1.0 + x + x * x / 2.0 + x * x * x / 6.0 + x * x * x * x / 24.0 + x * x * x * x * x / 120.0.\n\n\
1628## To logTaylor (x: Real) -> Real:\n Let xx be x - 1.0.\n Return xx - xx * xx / 2.0 + xx * xx * xx / 3.0 - xx * xx * xx * xx / 4.0.\n\n\
1629## To sqrtApprox (x: Real) -> Real:\n If x is less than 2.0:\n Return 1.0 + (x - 1.0) / 2.0 - (x - 1.0) * (x - 1.0) / 8.0 + (x - 1.0) * (x - 1.0) * (x - 1.0) / 16.0.\n If x is less than 10.0:\n Return x * 0.22 + 1.0.\n If x is less than 100.0:\n Return x * 0.08 + 2.5.\n If x is less than 1000.0:\n Return x * 0.028 + 8.0.\n If x is less than 10000.0:\n Return x * 0.008 + 20.0.\n Return x * 0.0002 + 300.0.\n\n\
1630## To cndf (x: Real) -> Real:\n Let ax be abs(x).\n Let xNPrimeofX be expTaylor(ax ** 2 * -0.05) * 0.3989422804014327.\n Let xK2 be 1.0 / (ax * 0.2316419 + 1.0).\n Let xK2_2 be xK2 ** 2.\n Let xK2_3 be xK2_2 * xK2.\n Let xK2_4 be xK2_3 * xK2.\n Let xK2_5 be xK2_4 * xK2.\n Let xLocal_10 be xK2 * 0.31938153.\n Let xLocal_20 be xK2_2 * -0.356563782.\n Let xLocal_30 be xK2_3 * 1.781477937.\n Let xLocal_31 be xK2_4 * -1.821255978.\n Let xLocal_32 be xK2_5 * 1.330274429.\n Let xLocal_21 be xLocal_20 + xLocal_30.\n Let xLocal_22 be xLocal_21 + xLocal_31.\n Let xLocal_23 be xLocal_22 + xLocal_32.\n Let xLocal_1 be xLocal_23 + xLocal_10.\n Let xLocal0 be xLocal_1 * xNPrimeofX.\n Let xLocal be 0 - xLocal0 + 1.0.\n If x is less than 0.0:\n Return xLocal0.\n Return xLocal.\n\n\
1631## To blackScholes (sptprice: Real, strike: Real, rate: Real, volatility: Real, time: Real, otype: Int) -> Real:\n Let xLogTerm be logTaylor(sptprice / strike).\n Let xPowerTerm be volatility ** 2 * 0.5.\n Let xNum be (rate + xPowerTerm) * time + xLogTerm.\n Let xDen be volatility * sqrtApprox(time).\n Let xDiv be xNum / (xDen ** 2).\n Let nofXd1 be cndf(xDiv).\n Let nofXd2 be cndf(xDiv - xDen).\n Let futureValueX be strike * expTaylor((0 - rate) * time).\n Let negNofXd1 be 0 - nofXd1 + 1.0.\n Let negNofXd2 be 0 - nofXd2 + 1.0.\n Let optionPrice1 be sptprice * nofXd1 - futureValueX * nofXd2.\n Let optionPrice2 be futureValueX * negNofXd2 - sptprice * negNofXd1.\n If otype is 0:\n Return optionPrice2.\n Return optionPrice1.\n\n\
1632## Main\nLet n be {n}.\nLet mutable types be a new Seq of Int.\nLet mutable prices be a new Seq of Real.\nLet mutable strikes be a new Seq of Real.\nLet mutable rates be a new Seq of Real.\nLet mutable vols be a new Seq of Real.\nLet mutable times be a new Seq of Real.\nLet mutable out be a new Seq of Real.\nLet mutable p be 0.\nWhile p is less than n:\n Push p % 2 to types.\n Push (p % 97 + 50) / 100.0 to prices.\n Push (p % 89 + 50) / 100.0 to strikes.\n Push (p % 13 + 1) / 100.0 to rates.\n Push (p % 41 + 10) / 100.0 to vols.\n Push (p % 23 + 2) / 4.0 to times.\n Push 0.0 to out.\n Set p to p + 1.\nLet mutable i be 0.\nWhile i is less than n:\n Set item (i + 1) of out to blackScholes(item (i + 1) of prices, item (i + 1) of strikes, item (i + 1) of rates, item (i + 1) of vols, item (i + 1) of times, item (i + 1) of types).\n Set i to i + 1.\nLet mutable sum be 0.0.\nSet p to 0.\nWhile p is less than n:\n Set sum to sum + item (p + 1) of out * (p + 1).\n Set p to p + 1.\nShow sum.\n"
1633 )
1634}
1635
1636pub fn lower_blackscholes_source(source: &str, p: MapPar, pir_args: Option<&str>) -> Result<(usize, String, String), String> {
1642 crate::ui_bridge::with_parsed_program(source, |parsed, interner| {
1643 let (stmts, _types, _policies) = parsed.map_err(|e| format!("parse: {e:?}"))?;
1644 let query = blackscholes_query();
1645 let map = crate::tensor_recognize::recognize_zip_map_where(stmts, interner, &|m| m.term == query)
1646 .map_err(|e| format!("not Black-Scholes as the tensor recognizer reads it: {e:?}"))?;
1647 let columns: Vec<MapColumn> = map
1648 .operands
1649 .iter()
1650 .zip(&map.types)
1651 .map(|(o, ty)| MapColumn {
1652 name: interner.resolve(*o).to_string(),
1653 ty: *ty,
1654 fill: crate::tensor_recognize::recognize_fill(stmts, *o),
1655 })
1656 .collect();
1657 let declared = declaration_order(stmts, &map.operands)?;
1658 let class = format!("Logos_bs_{}_ts{}_op{}_ip{}", map.n, p.ts, p.op, p.ip);
1659 let out = interner.resolve(map.out).to_string();
1660 let spatial = emit_spatial_zip_map_test(map.n, &columns, &declared, &map.term, &out, p, pir_args, &class)?;
1661 Ok((map.n, class, spatial))
1662 })
1663}
1664
1665fn declaration_order(stmts: &[logicaffeine_language::ast::stmt::Stmt], arrays: &[crate::intern::Symbol]) -> Result<Vec<usize>, String> {
1668 let at = |a: &crate::intern::Symbol| {
1669 stmts
1670 .iter()
1671 .position(|s| matches!(s, logicaffeine_language::ast::stmt::Stmt::Let { var, .. } if var == a))
1672 .ok_or_else(|| "a column the program reads is not declared at its top level".to_string())
1673 };
1674 let mut order: Vec<(usize, usize)> = arrays.iter().enumerate().map(|(k, a)| Ok((at(a)?, k))).collect::<Result<_, String>>()?;
1675 order.sort_unstable();
1676 Ok(order.into_iter().map(|(_, k)| k).collect())
1677}
1678
1679pub fn kmeans_distance_query() -> ZipTerm {
1682 let sub = ZipTerm::Op(ZipOp::Sub, Box::new(ZipTerm::Read(0)), Box::new(ZipTerm::Read(1)));
1683 ZipTerm::Op(ZipOp::Pow, Box::new(sub), Box::new(ZipTerm::Const(2)))
1684}
1685
1686pub fn logos_kmeans_program(n: usize, k: usize, f: usize, iters: usize) -> String {
1693 format!(
1694 "## Main\nLet n be {n}.\nLet k be {k}.\nLet f be {f}.\nLet iters be {iters}.\nLet mutable pts be a new Seq of Int.\nLet mutable cts be a new Seq of Int.\nLet mutable sums be a new Seq of Int.\nLet mutable counts be a new Seq of Int.\nLet mutable q be 0.\nWhile q is less than n * f:\n Push q % 97 to pts.\n Set q to q + 1.\nSet q to 0.\nWhile q is less than k * f:\n Push item (q + 1) of pts to cts.\n Push 0 to sums.\n Set q to q + 1.\nSet q to 0.\nWhile q is less than k:\n Push 0 to counts.\n Set q to q + 1.\nLet mutable epoch be 0.\nWhile epoch is less than iters:\n Set q to 0.\n While q is less than k * f:\n Set item (q + 1) of sums to 0.\n Set q to q + 1.\n Set q to 0.\n While q is less than k:\n Set item (q + 1) of counts to 0.\n Set q to q + 1.\n Let mutable p be 0.\n While p is less than n:\n Let mutable best be 0.\n Let mutable bestDist be 0.\n Let mutable c be 0.\n While c is less than k:\n Let mutable dist be 0.\n Let mutable d be 0.\n While d is less than f:\n Set dist to dist + (item (p * f + d + 1) of pts - item (c * f + d + 1) of cts) ** 2.\n Set d to d + 1.\n If c is 0 or dist is at most bestDist:\n Set best to c.\n Set bestDist to dist.\n Set c to c + 1.\n Set q to 0.\n While q is less than f:\n Set item (best * f + q + 1) of sums to item (best * f + q + 1) of sums + item (p * f + q + 1) of pts.\n Set q to q + 1.\n Set item (best + 1) of counts to item (best + 1) of counts + 1.\n Set p to p + 1.\n Let mutable j be 0.\n While j is less than k:\n Let cnt be item (j + 1) of counts.\n Set q to 0.\n While q is less than f:\n If cnt is 0:\n Set item (j * f + q + 1) of cts to 0.\n Otherwise:\n Set item (j * f + q + 1) of cts to item (j * f + q + 1) of sums / cnt.\n Set q to q + 1.\n Set j to j + 1.\n Set epoch to epoch + 1.\nLet mutable sum be 0.\nSet q to 0.\nWhile q is less than k * f:\n Set sum to sum + item (q + 1) of cts * (q + 1).\n Set q to q + 1.\nShow sum.\n"
1695 )
1696}
1697
1698pub fn lower_kmeans_source(source: &str, p: KmeansPar) -> Result<(usize, usize, usize, usize, String, String), String> {
1703 crate::ui_bridge::with_parsed_program(source, |parsed, interner| {
1704 let (stmts, _types, _policies) = parsed.map_err(|e| format!("parse: {e:?}"))?;
1705 let spec = crate::tensor_recognize::recognize_kmeans(stmts, interner)
1706 .map_err(|e| format!("not k-means as the tensor recognizer reads it: {e:?}"))?;
1707 if spec.distance != kmeans_distance_query() {
1708 return Err("k-means, but not by the distance Spatial's Kmeans app sums".to_string());
1709 }
1710 let points = crate::tensor_recognize::recognize_fill(stmts, spec.points);
1711 let class = format!(
1712 "Logos_kmeans_{}x{}_k{}_i{}_ts{}_ip{}_op{}_mp{}x{}x{}",
1713 spec.n, spec.f + 1, spec.k, spec.iters, p.ts, p.ip, p.op, p.mp1, p.mp2, p.mp3
1714 );
1715 let spatial = emit_spatial_kmeans_test(&spec, &points, p, &class)?;
1716 Ok((spec.n, spec.k, spec.f + 1, spec.iters, class, spatial))
1717 })
1718}
1719
1720pub fn lower_outer_source(source: &str, p: OuterPar) -> Result<(usize, usize, String, String), String> {
1725 let (m, n) = crate::ui_bridge::with_parsed_program(source, |parsed, interner| {
1726 let (stmts, _types, _policies) = parsed.map_err(|e| format!("parse: {e:?}"))?;
1727 crate::tensor_recognize::recognize_outer(stmts, interner)
1728 .map(|spec| (spec.m, spec.n))
1729 .map_err(|e| format!("not an outer product the tensor recognizer reads: {e:?}"))
1730 })?;
1731 let class = format!(
1732 "Logos_outer_{m}x{n}_ts{}x{}_ip{}x{}x{}_op{}x{}",
1733 p.ts1, p.ts2, p.ip, p.ip1, p.ip2, p.op1, p.op2
1734 );
1735 let spatial = emit_spatial_outer_test(m, n, p, &class)?;
1736 Ok((m, n, class, spatial))
1737}