Script: prism_chip_model.py

PRISM-1

prism_chip_model.py

Download original file

This is the original source text. Reading it does not run or verify the program.

Read the typeset paper

Skip source text

1 from __future__ import annotations
2 
3 import argparse
4 import json
5 import math
6 from dataclasses import asdict, dataclass
7 from pathlib import Path
8 from typing import Iterable, Sequence
9 
10 import matplotlib
11 matplotlib.use("Agg")
12 import matplotlib.pyplot as plt
13 from matplotlib.patches import FancyArrowPatch, FancyBboxPatch, Rectangle
14 import numpy as np
15 
16 
17 @dataclass(frozen=True)
18 class PrismConfig:
19     name: str = "Komorebi PRISM-1"
20     clock_ghz: float = 1.5
21     compute_chiplets: int = 4
22     packet_streaming_multiprocessors_per_chiplet: int = 16
23     execution_groups_per_psm: int = 4
24     mac_rows: int = 16
25     mac_cols: int = 16
26     local_sram_kib_per_execution_group: int = 512
27     shared_sram_mib_per_chiplet: int = 16
28     descriptor_cache_kib_per_psm: int = 128
29     transform_engines_per_chiplet: int = 8
30     solver_engines_per_chiplet: int = 4
31     exact_arithmetic_clusters_per_chiplet: int = 1
32     hbm4_stacks: int = 4
33     hbm4_capacity_gib_per_stack: int = 36
34     hbm4_bandwidth_tbps_per_stack: float = 2.8
35     package_network_tbps: float = 16.0
36     host_bidirectional_gbps: float = 512.0
37     tdp_w: int = 500
38     ucie_gtps: int = 64
39 
40     @property
41     def psms(self) -> int:
42         return self.compute_chiplets * self.packet_streaming_multiprocessors_per_chiplet
43 
44     @property
45     def execution_groups_per_chiplet(self) -> int:
46         return self.packet_streaming_multiprocessors_per_chiplet * self.execution_groups_per_psm
47 
48     @property
49     def execution_groups(self) -> int:
50         return self.psms * self.execution_groups_per_psm
51 
52     @property
53     def tiles(self) -> int:
54         return self.execution_groups
55 
56     @property
57     def total_hbm_gib(self) -> int:
58         return self.hbm4_stacks * self.hbm4_capacity_gib_per_stack
59 
60     @property
61     def total_hbm_tbps(self) -> float:
62         return self.hbm4_stacks * self.hbm4_bandwidth_tbps_per_stack
63 
64     @property
65     def total_sram_mib(self) -> float:
66         local = self.execution_groups * self.local_sram_kib_per_execution_group / 1024
67         shared = self.compute_chiplets * self.shared_sram_mib_per_chiplet
68         return local + shared
69 
70     @property
71     def fp16_tflops(self) -> float:
72         macs = self.execution_groups * self.mac_rows * self.mac_cols
73         return macs * 2 * self.clock_ghz * 1e9 / 1e12
74 
75     @property
76     def fp32_tflops(self) -> float:
77         return self.fp16_tflops / 2
78 
79     @property
80     def fp64_tflops(self) -> float:
81         return self.fp16_tflops / 8
82 
83 
84 @dataclass(frozen=True)
85 class PacketDescriptor:
86     packet_id: int
87     parent_id: int
88     atlas_id: int
89     selector_kind: str
90     shape: tuple[int, ...]
91     base_address: int
92     bytes_per_element: int
93     precision: str
94     error_budget: float
95 
96 
97 @dataclass(frozen=True)
98 class TransferDescriptor:
99     edge_id: int
100     src_packet: int
101     dst_packet: int
102     kernel_kind: str
103     rows: int
104     cols: int
105     rank: int
106     precision: str
107     sensitivity: float
108     dependency_token: int
109 
110 
111 REPRESENTATION_FORMS = (
112     "explicit_dense",
113     "block_diagonal",
114     "block_sparse",
115     "low_rank",
116     "bdlr",
117     "kronecker",
118     "sparse_factor_chain",
119     "transform_domain",
120     "packet_graph",
121 )
122 
123 OUTPUT_CONTRACTS = (
124     "apply",
125     "compressed_operator",
126     "factor_graph",
127     "transform_plan",
128     "explicit_array",
129 )
130 
131 
132 @dataclass(frozen=True)
133 class PlanChoice:
134     name: str
135     output_form: str
136     local_cost: float
137     local_error: float = 0.0
138     children: tuple[str, ...] = ()
139 
140 
141 @dataclass(frozen=True)
142 class PlanResult:
143     node: str
144     choice: str
145     output_form: str
146     objective: float
147     local_cost: float
148     local_error: float
149     children: tuple["PlanResult", ...]
150 
151 
152 @dataclass(frozen=True)
153 class Estimate:
154     workload: str
155     flops: float
156     bytes_moved: float
157     arithmetic_intensity: float
158     compute_time_us: float
159     memory_time_us: float
160     schedule_time_us: float
161     total_time_us: float
162     energy_mj: float
163     notes: str
164 
165 
166 def packet_slices(sizes: Sequence[int]) -> tuple[slice, ...]:
167     out: list[slice] = []
168     start = 0
169     for size in sizes:
170         if size <= 0:
171             raise ValueError("packet sizes must be positive")
172         out.append(slice(start, start + int(size)))
173         start += int(size)
174     return tuple(out)
175 
176 
177 def residue_project(x: np.ndarray, order: int, phase: int) -> np.ndarray:
178     arr = np.asarray(x)
179     out = np.zeros_like(arr)
180     mask = np.arange(arr.size) % order == phase % order
181     out[mask] = arr[mask]
182     return out
183 
184 
185 def polyphase_analyze(x: np.ndarray, order: int) -> list[np.ndarray]:
186     arr = np.asarray(x)
187     return [arr[r::order].copy() for r in range(order)]
188 
189 
190 def polyphase_synthesize(channels: Sequence[np.ndarray], length: int | None = None) -> np.ndarray:
191     order = len(channels)
192     if order == 0:
193         raise ValueError("channels cannot be empty")
194     arrays = [np.asarray(c) for c in channels]
195     natural = max(order * len(c) + r for r, c in enumerate(arrays))
196     n = natural if length is None else length
197     out = np.zeros(n, dtype=np.result_type(*arrays))
198     for r, channel in enumerate(arrays):
199         positions = np.arange(r, n, order)
200         count = min(len(channel), len(positions))
201         out[positions[:count]] = channel[:count]
202     return out
203 
204 
205 def haar_analyze(x: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
206     arr = np.asarray(x, dtype=float)
207     if arr.size % 2:
208         raise ValueError("Haar analysis requires even length")
209     s = math.sqrt(2.0)
210     return (arr[0::2] + arr[1::2]) / s, (arr[0::2] - arr[1::2]) / s
211 
212 
213 def haar_synthesize(low: np.ndarray, high: np.ndarray) -> np.ndarray:
214     low = np.asarray(low, dtype=float)
215     high = np.asarray(high, dtype=float)
216     if low.shape != high.shape:
217         raise ValueError("Haar channels must match")
218     s = math.sqrt(2.0)
219     out = np.empty(low.size * 2)
220     out[0::2] = (low + high) / s
221     out[1::2] = (low - high) / s
222     return out
223 
224 
225 def block_diagonal_apply(blocks: Sequence[np.ndarray], x: np.ndarray) -> np.ndarray:
226     sizes = [b.shape[0] for b in blocks]
227     slices = packet_slices(sizes)
228     x = np.asarray(x)
229     out = np.empty_like(x, dtype=np.result_type(x, *blocks))
230     for slc, block in zip(slices, blocks, strict=True):
231         out[slc] = block @ x[slc]
232     return out
233 
234 
235 def bdlr_apply(blocks: Sequence[np.ndarray], u: np.ndarray, v: np.ndarray, x: np.ndarray) -> np.ndarray:
236     return block_diagonal_apply(blocks, x) + u @ (v.T.conj() @ x)
237 
238 
239 def materialize_bdlr(blocks: Sequence[np.ndarray], u: np.ndarray, v: np.ndarray) -> np.ndarray:
240     sizes = [b.shape[0] for b in blocks]
241     total = sum(sizes)
242     out = np.zeros((total, total), dtype=np.result_type(*blocks, u, v))
243     for slc, block in zip(packet_slices(sizes), blocks, strict=True):
244         out[slc, slc] = block
245     return out + u @ v.T.conj()
246 
247 
248 def output_contract_allows(output_form: str, contract: str) -> bool:
249     if output_form not in REPRESENTATION_FORMS:
250         raise ValueError(f"unknown output form {output_form}")
251     if contract not in OUTPUT_CONTRACTS:
252         raise ValueError(f"unknown output contract {contract}")
253     if contract == "apply":
254         return True
255     if contract == "explicit_array":
256         return output_form == "explicit_dense"
257     if contract == "transform_plan":
258         return output_form in {"transform_domain", "sparse_factor_chain", "packet_graph"}
259     if contract == "factor_graph":
260         return output_form in {
261             "block_diagonal",
262             "block_sparse",
263             "low_rank",
264             "bdlr",
265             "kronecker",
266             "sparse_factor_chain",
267             "transform_domain",
268             "packet_graph",
269         }
270     return True
271 
272 
273 def pmr_plan(
274     root: str,
275     options: dict[str, tuple[PlanChoice, ...]],
276     error_weight: float,
277     output_contract: str,
278 ) -> PlanResult:
279     """Bellman planner on a finite mixed-representation tree."""
280     if error_weight < 0:
281         raise ValueError("error weight must be nonnegative")
282     memo: dict[tuple[str, bool], PlanResult] = {}
283     active: set[str] = set()
284 
285     def solve(node: str, is_root: bool) -> PlanResult:
286         key = (node, is_root)
287         if key in memo:
288             return memo[key]
289         if node in active:
290             raise ValueError("candidate plan graph must be acyclic")
291         if node not in options or not options[node]:
292             raise ValueError(f"no candidate representations for {node}")
293         active.add(node)
294         candidates: list[PlanResult] = []
295         for choice in options[node]:
296             if choice.output_form not in REPRESENTATION_FORMS:
297                 raise ValueError(f"unknown representation {choice.output_form}")
298             if is_root and not output_contract_allows(choice.output_form, output_contract):
299                 continue
300             child_results = tuple(solve(child, False) for child in choice.children)
301             objective = (
302                 choice.local_cost
303                 + error_weight * choice.local_error * choice.local_error
304                 + sum(child.objective for child in child_results)
305             )
306             candidates.append(
307                 PlanResult(
308                     node=node,
309                     choice=choice.name,
310                     output_form=choice.output_form,
311                     objective=float(objective),
312                     local_cost=float(choice.local_cost),
313                     local_error=float(choice.local_error),
314                     children=child_results,
315                 )
316             )
317         active.remove(node)
318         if not candidates:
319             raise ValueError(f"no representation satisfies output contract {output_contract}")
320         best = min(candidates, key=lambda result: (result.objective, result.choice))
321         memo[key] = best
322         return best
323 
324     return solve(root, True)
325 
326 
327 def measured_cuda_baselines() -> dict[str, float | str]:
328     """Measured structured CUDA rows imported from the prior packet-GPU study."""
329     return {
330         "hardware": "NVIDIA GeForce RTX 3070 Ti Laptop GPU",
331         "equal_block_packet_ms": 0.046032,
332         "equal_block_speedup_vs_gpu_dense": 14.8919,
333         "equal_block_speedup_vs_cpu_dense": 235.7404,
334         "bdlr_standard_packet_ms": 0.097280,
335         "bdlr_graph_packet_ms": 0.079872,
336         "bdlr_speedup_vs_gpu_dense": 10.9355,
337         "bdlr_speedup_vs_cpu_dense": 153.6527,
338         "bdlr_graph_replay_gain": 0.097280 / 0.079872,
339         "compressed_product_ms": 0.1582,
340         "materialized_product_ms": 0.5073,
341         "compressed_output_gain": 0.5073 / 0.1582,
342         "measured_rows": 154.0,
343     }
344 
345 
346 def gain_decomposition() -> dict[str, float | str]:
347     cuda = measured_cuda_baselines()
348     return {
349         "representation_reduction_bdlr": 102.4,
350         "measured_packet_gpu_gain_vs_dense_gpu": float(cuda["bdlr_speedup_vs_gpu_dense"]),
351         "measured_cuda_graph_gain_same_representation": float(cuda["bdlr_graph_replay_gain"]),
352         "measured_compressed_output_gain": float(cuda["compressed_output_gain"]),
353         "note": "Factors are reported separately and are not multiplied unless timing scope and output contract match.",
354     }
355 
356 
357 def topological_order(nodes: Iterable[str], edges: Iterable[tuple[str, str]]) -> tuple[str, ...]:
358     nodes = tuple(sorted(set(nodes)))
359     edge_list = tuple(edges)
360     indegree = {n: 0 for n in nodes}
361     outgoing = {n: [] for n in nodes}
362     for a, b in edge_list:
363         indegree[b] += 1
364         outgoing[a].append(b)
365     ready = sorted(n for n, d in indegree.items() if d == 0)
366     order: list[str] = []
367     while ready:
368         n = ready.pop(0)
369         order.append(n)
370         for b in sorted(outgoing[n]):
371             indegree[b] -= 1
372             if indegree[b] == 0:
373                 ready.append(b)
374                 ready.sort()
375     if len(order) != len(nodes):
376         raise ValueError("graph is cyclic")
377     return tuple(order)
378 
379 
380 def substitute_task(
381     nodes: set[str],
382     edges: set[tuple[str, str]],
383     vertex: str,
384     local_nodes: set[str],
385     local_edges: set[tuple[str, str]],
386     entries: set[str],
387     exits: set[str],
388 ) -> tuple[set[str], set[tuple[str, str]], tuple[str, ...]]:
389     old_order = topological_order(nodes, edges)
390     local_order = topological_order(local_nodes, local_edges)
391     preds = {a for a, b in edges if b == vertex}
392     succs = {b for a, b in edges if a == vertex}
393     context_edges = {e for e in edges if vertex not in e}
394     new_edges = set(context_edges) | set(local_edges)
395     new_edges |= {(p, e) for p in preds for e in entries}
396     new_edges |= {(x, s) for x in exits for s in succs}
397     new_nodes = (set(nodes) - {vertex}) | set(local_nodes)
398     idx = old_order.index(vertex)
399     spliced = old_order[:idx] + local_order + old_order[idx + 1 :]
400     if tuple(n for n in spliced if n in nodes - {vertex}) != tuple(n for n in old_order if n != vertex):
401         raise RuntimeError("remote order changed")
402     if set(spliced) != new_nodes:
403         raise RuntimeError("spliced order has wrong nodes")
404     positions = {n: i for i, n in enumerate(spliced)}
405     if not all(positions[a] < positions[b] for a, b in new_edges):
406         raise RuntimeError("spliced order is not topological")
407     return new_nodes, new_edges, spliced
408 
409 
410 def peak_flops(config: PrismConfig, precision: str) -> float:
411     p = precision.lower()
412     if p in {"fp16", "bf16"}:
413         return config.fp16_tflops * 1e12
414     if p in {"fp32", "tf32"}:
415         return config.fp32_tflops * 1e12
416     if p == "fp64":
417         return config.fp64_tflops * 1e12
418     raise ValueError(f"unsupported precision {precision}")
419 
420 
421 def roofline_estimate(
422     config: PrismConfig,
423     workload: str,
424     flops: float,
425     bytes_moved: float,
426     precision: str = "fp32",
427     graph_regions: int = 1,
428     power_w: float | None = None,
429     notes: str = "",
430     use_sram: bool = False,
431 ) -> Estimate:
432     peak = peak_flops(config, precision)
433     bandwidth = (config.package_network_tbps if use_sram else config.total_hbm_tbps) * 1e12
434     efficiency_compute = 0.72 if use_sram else 0.62
435     efficiency_bw = 0.72 if use_sram else 0.74
436     compute_s = flops / (peak * efficiency_compute)
437     memory_s = bytes_moved / (bandwidth * efficiency_bw)
438     schedule_s = graph_regions * 0.32e-6
439     total_s = max(compute_s, memory_s) + schedule_s
440     watts = float(config.tdp_w if power_w is None else power_w)
441     return Estimate(
442         workload=workload,
443         flops=flops,
444         bytes_moved=bytes_moved,
445         arithmetic_intensity=flops / max(bytes_moved, 1.0),
446         compute_time_us=compute_s * 1e6,
447         memory_time_us=memory_s * 1e6,
448         schedule_time_us=schedule_s * 1e6,
449         total_time_us=total_s * 1e6,
450         energy_mj=watts * total_s * 1e3,
451         notes=notes,
452     )
453 
454 
455 def dense_apply_estimate(config: PrismConfig, n: int, q: int, itemsize: int = 4) -> Estimate:
456     flops = 2.0 * n * n * q
457     bytes_moved = itemsize * (n * n + 2 * n * q)
458     return roofline_estimate(
459         config,
460         "Dense apply baseline",
461         flops,
462         bytes_moved,
463         precision="fp32",
464         graph_regions=1,
465         notes="Dense matrix is materialized and read once",
466     )
467 
468 
469 def bdlr_estimate(config: PrismConfig, n: int, b: int, r: int, q: int, itemsize: int = 4) -> Estimate:
470     flops = 2.0 * q * n * (b + 2 * r)
471     bytes_moved = itemsize * (n * (b + 2 * r) + 3 * n * q + 2 * r * q)
472     return roofline_estimate(
473         config,
474         "Packet BDLR apply",
475         flops,
476         bytes_moved,
477         precision="fp32",
478         graph_regions=4,
479         notes="Factor-first execution with block diagonal and two low-rank contractions",
480         use_sram=True,
481     )
482 
483 
484 def block_sparse_estimate(config: PrismConfig, blocks: int, b: int, degree: int, q: int, itemsize: int = 4) -> Estimate:
485     n = blocks * b
486     edge_count = blocks * degree
487     flops = 2.0 * edge_count * b * b * q
488     bytes_moved = itemsize * (edge_count * b * b + 2 * n * q)
489     return roofline_estimate(
490         config,
491         "Regular block-sparse transfer",
492         flops,
493         bytes_moved,
494         precision="fp32",
495         graph_regions=max(1, degree),
496         notes="Edges are grouped by equal shape and scheduled as batches",
497         use_sram=True,
498     )
499 
500 
501 def multi_atlas_estimate(config: PrismConfig, pixels: int, atlases: int = 5, itemsize: int = 4) -> tuple[Estimate, Estimate]:
502     logn = math.log2(pixels)
503     transform_flops = 5.0 * pixels * logn + 8.0 * pixels
504     total_flops = atlases * transform_flops
505     baseline_passes = 3 * atlases + 2
506     prism_passes = 4
507     baseline_bytes = baseline_passes * pixels * itemsize
508     prism_bytes = prism_passes * pixels * itemsize
509     baseline = roofline_estimate(
510         config,
511         "Separate multi-atlas kernels",
512         total_flops,
513         baseline_bytes,
514         precision="fp32",
515         graph_regions=baseline_passes,
516         notes="Each atlas writes an intermediate array to HBM",
517     )
518     prism = roofline_estimate(
519         config,
520         "Fused multi-atlas pipeline",
521         total_flops,
522         prism_bytes,
523         precision="fp32",
524         graph_regions=3,
525         notes="Analysis, packet selection, feature reduction, and synthesis are fused",
526         use_sram=True,
527     )
528     return baseline, prism
529 
530 
531 def edge_recovery_estimate(config: PrismConfig, samples: int, max_jumps: int, itemsize: int = 8) -> tuple[Estimate, Estimate]:
532     fft_flops = 5.0 * samples * math.log2(samples)
533     hankel = max_jumps + 1
534     svd_flops = 20.0 * hankel**3
535     vandermonde_flops = 4.0 * max_jumps**3
536     root_flops = 250.0 * max_jumps**2
537     flops = fft_flops + svd_flops + vandermonde_flops + root_flops
538     baseline_bytes = itemsize * (10 * samples + 8 * hankel**2)
539     prism_bytes = itemsize * (3 * samples + 4 * hankel**2)
540     baseline = roofline_estimate(
541         config,
542         "General accelerator edge recovery",
543         flops,
544         baseline_bytes,
545         precision="fp64",
546         graph_regions=12,
547         power_w=420,
548         notes="FFT and small solvers run as separate kernels",
549     )
550     prism = roofline_estimate(
551         config,
552         "PRISM polyphase edge recovery",
553         flops,
554         prism_bytes,
555         precision="fp64",
556         graph_regions=4,
557         power_w=260,
558         notes="Streaming Hankel construction and resident small-solver cluster",
559         use_sram=True,
560     )
561     return baseline, prism
562 
563 
564 def bayes_estimate(config: PrismConfig, models: int, samples: int, features: int, outputs: int, itemsize: int = 4) -> tuple[Estimate, Estimate]:
565     flops_per_model = 2.0 * samples * features * features + (2.0 / 3.0) * features**3 + 2.0 * samples * features * outputs
566     flops = models * flops_per_model
567     baseline_bytes = itemsize * models * (samples * features + features * features + features * outputs)
568     prism_bytes = itemsize * (samples * features + models * (features * features + features * outputs))
569     baseline = roofline_estimate(
570         config,
571         "Independent Bayesian model kernels",
572         flops,
573         baseline_bytes,
574         precision="fp32",
575         graph_regions=models,
576         notes="Each model reloads the shared state matrix",
577     )
578     prism = roofline_estimate(
579         config,
580         "Shape-batched Bayesian posterior",
581         flops,
582         prism_bytes,
583         precision="fp32",
584         graph_regions=8,
585         notes="Shared design matrices remain resident and models are grouped by shape",
586         use_sram=True,
587     )
588     return baseline, prism
589 
590 
591 def _save(fig: plt.Figure, out_dir: Path, name: str) -> None:
592     out_dir.mkdir(parents=True, exist_ok=True)
593     fig.savefig(out_dir / f"{name}.pdf", bbox_inches="tight", pad_inches=0.08)
594     fig.savefig(out_dir / f"{name}.png", dpi=190, bbox_inches="tight", pad_inches=0.08)
595     plt.close(fig)
596 
597 
598 def _box(ax: plt.Axes, x: float, y: float, w: float, h: float, text: str, face: str = "white", fontsize: float = 10.5) -> None:
599     patch = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.015,rounding_size=0.02", facecolor=face, edgecolor="black", linewidth=1.2)
600     ax.add_patch(patch)
601     label = ax.text(x + w / 2, y + h / 2, text, ha="center", va="center", fontsize=fontsize, multialignment="center")
602     fig = ax.figure
603     for _ in range(12):
604         fig.canvas.draw()
605         renderer = fig.canvas.get_renderer()
606         bb = label.get_window_extent(renderer=renderer)
607         p0 = ax.transData.transform((x, y))
608         p1 = ax.transData.transform((x + w, y + h))
609         if bb.width <= abs(p1[0] - p0[0]) * 0.86 and bb.height <= abs(p1[1] - p0[1]) * 0.80:
610             break
611         label.set_fontsize(max(6.8, label.get_fontsize() * 0.90))
612 
613 
614 def _arrow(ax: plt.Axes, a: tuple[float, float], b: tuple[float, float], text: str | None = None, rad: float = 0.0) -> None:
615     arrow = FancyArrowPatch(a, b, arrowstyle="-|>", mutation_scale=12, linewidth=1.3, color="black", connectionstyle=f"arc3,rad={rad}")
616     ax.add_patch(arrow)
617     if text:
618         mx = (a[0] + b[0]) / 2
619         my = (a[1] + b[1]) / 2
620         ax.text(mx, my + 0.025, text, ha="center", va="bottom", fontsize=8.5, bbox={"facecolor": "white", "edgecolor": "none", "pad": 1.0})
621 
622 
623 def fig_package(config: PrismConfig, out: Path) -> None:
624     fig, ax = plt.subplots(figsize=(12.8, 7.0))
625     ax.set_xlim(0, 1)
626     ax.set_ylim(0, 1)
627     ax.axis("off")
628     ax.text(0.5, 0.96, "Komorebi PRISM-1 chiplet package", ha="center", va="top", fontsize=21, weight="bold")
629     ax.add_patch(Rectangle((0.06, 0.10), 0.88, 0.78, facecolor="#f2f2f2", edgecolor="black", linewidth=1.4))
630     positions = [(0.18, 0.58), (0.58, 0.58), (0.18, 0.22), (0.58, 0.22)]
631     for i, (x, y) in enumerate(positions):
632         _box(ax, x, y, 0.24, 0.20, f"Compute chiplet C{i}\n16 packet SMs\n64 execution groups\n8 transform engines\n4 solver engines", face="#dbeafe", fontsize=11)
633     _box(ax, 0.42, 0.40, 0.16, 0.20, "I/O and control die\nRISC-V control\nglobal scheduler\nPCIe and CXL", face="#fef3c7", fontsize=9.2)
634     hbm_pos = [(0.075, 0.69), (0.815, 0.69), (0.075, 0.20), (0.815, 0.20)]
635     for i, (x, y) in enumerate(hbm_pos):
636         _box(ax, x, y, 0.10, 0.15, f"HBM4 {i}\n36 GiB\n>2.8 TB/s", face="#dcfce7", fontsize=9.2)
637     package_links = [
638         ((0.42, 0.68), (0.435, 0.57)),
639         ((0.58, 0.68), (0.565, 0.57)),
640         ((0.42, 0.32), (0.435, 0.43)),
641         ((0.58, 0.32), (0.565, 0.43)),
642     ]
643     for start, end in package_links:
644         _arrow(ax, start, end)
645     ax.text(0.5, 0.12, f"{config.total_hbm_gib} GiB HBM4   {config.total_hbm_tbps:.1f} TB/s aggregate   UCIe {config.ucie_gtps} GT/s package links", ha="center", fontsize=12, weight="bold")
646     _save(fig, out, "01_package_architecture")
647 
648 
649 def fig_tile(config: PrismConfig, out: Path) -> None:
650     fig, ax = plt.subplots(figsize=(12.8, 7.0))
651     ax.set_xlim(0, 1)
652     ax.set_ylim(0, 1)
653     ax.axis("off")
654     ax.text(0.5, 0.96, "One execution group and shared PSM paths", ha="center", va="top", fontsize=20, weight="bold")
655     boxes = [
656         (0.05, 0.64, 0.20, 0.18, "Descriptor front end\npacket and transfer tables", "#fef3c7"),
657         (0.30, 0.64, 0.20, 0.18, "Projector DMA\nmask, gather, scatter\npolyphase permutation", "#ede9fe"),
658         (0.55, 0.64, 0.20, 0.18, "1024-bit vector lane\nselectors and reductions", "#e0f2fe"),
659         (0.78, 0.64, 0.17, 0.18, "Certificate unit\nFP64 and exact accumulators", "#fee2e2"),
660         (0.18, 0.34, 0.28, 0.22, "one 16 x 16 matrix array\nblock transfer and low rank\nFP16 BF16 FP32 FP64\ncomplex pairs", "#dbeafe"),
661         (0.54, 0.34, 0.28, 0.22, "Nonlinear and memory unit\nquadratic interactions\ndelay and Volterra kernels", "#dcfce7"),
662         (0.18, 0.10, 0.28, 0.16, f"PSM scratchpad\n{config.execution_groups_per_psm * config.local_sram_kib_per_execution_group // 1024} MiB across four banks", "#f3f4f6"),
663         (0.54, 0.10, 0.28, 0.16, "Mesh router\nmulticast and reduction", "#f3f4f6"),
664     ]
665     for x, y, w, h, text, face in boxes:
666         _box(ax, x, y, w, h, text, face=face, fontsize=10.5)
667     _arrow(ax, (0.25, 0.73), (0.30, 0.73))
668     _arrow(ax, (0.50, 0.73), (0.55, 0.73))
669     _arrow(ax, (0.75, 0.73), (0.78, 0.73))
670     _arrow(ax, (0.40, 0.64), (0.32, 0.56))
671     _arrow(ax, (0.65, 0.64), (0.68, 0.56))
672     _arrow(ax, (0.32, 0.34), (0.32, 0.26))
673     _arrow(ax, (0.68, 0.34), (0.68, 0.26))
674     _arrow(ax, (0.46, 0.18), (0.54, 0.18))
675     _save(fig, out, "02_packet_compute_tile")
676 
677 
678 def fig_calculus_pipeline(out: Path) -> None:
679     fig, ax = plt.subplots(figsize=(13.0, 5.7))
680     ax.set_xlim(0, 1)
681     ax.set_ylim(0, 1)
682     ax.axis("off")
683     ax.text(0.5, 0.92, "The calculus maps directly to a hardware pipeline", ha="center", fontsize=20, weight="bold")
684     labels = [
685         ("seed", "load object and grammar"),
686         ("address", "packet descriptor"),
687         ("project", "analysis or selector"),
688         ("transfer", "block or low-rank edge"),
689         ("refine", "local child graph"),
690         ("synthesize", "exact reconstruction"),
691         ("certify", "residual and bound"),
692     ]
693     xs = np.linspace(0.025, 0.84, len(labels))
694     for i, (x, (title, sub)) in enumerate(zip(xs, labels, strict=True)):
695         _box(ax, x, 0.43, 0.13, 0.22, f"{title}\n{sub}", face="#f8fafc", fontsize=10.2)
696         if i < len(labels) - 1:
697             _arrow(ax, (x + 0.13, 0.54), (xs[i + 1], 0.54))
698     _arrow(ax, (0.91, 0.40), (0.08, 0.40), text="certificate feeds residual-driven refinement", rad=-0.18)
699     ax.text(0.5, 0.15, "Every command keeps the packet identity, atlas, precision tag, parent relation, and error budget attached to the data.", ha="center", fontsize=11.5)
700     _save(fig, out, "03_calculus_pipeline")
701 
702 
703 def fig_descriptor(out: Path) -> None:
704     fields = [
705         ("packet_id", 12),
706         ("parent_id", 12),
707         ("atlas_id", 8),
708         ("selector", 8),
709         ("precision", 4),
710         ("shape", 24),
711         ("base_address", 64),
712         ("error_budget", 32),
713         ("flags", 12),
714     ]
715     total = sum(bits for _, bits in fields)
716     fig, ax = plt.subplots(figsize=(13.0, 4.3))
717     ax.set_xlim(0, total)
718     ax.set_ylim(0, 1)
719     ax.axis("off")
720     ax.text(total / 2, 0.92, "Packet descriptor entry", ha="center", fontsize=20, weight="bold")
721     x = 0
722     for name, bits in fields:
723         ax.add_patch(Rectangle((x, 0.35), bits, 0.30, facecolor="#e5e7eb", edgecolor="black"))
724         ax.text(x + bits / 2, 0.50, f"{name}\n{bits} bits", ha="center", va="center", fontsize=8.5)
725         x += bits
726     ax.text(total / 2, 0.14, "Selectors may be contiguous ranges, residue classes, bit masks, transform-domain bands, basis projectors, or packet-tree nodes.", ha="center", fontsize=11)
727     _save(fig, out, "04_packet_descriptor")
728 
729 
730 def fig_transfer_descriptor(out: Path) -> None:
731     fields = [
732         ("edge_id", 12),
733         ("src", 12),
734         ("dst", 12),
735         ("kernel", 8),
736         ("rows", 16),
737         ("cols", 16),
738         ("rank", 8),
739         ("precision", 4),
740         ("sensitivity", 32),
741         ("payload", 64),
742         ("token", 16),
743     ]
744     total = sum(bits for _, bits in fields)
745     fig, ax = plt.subplots(figsize=(13.0, 4.5))
746     ax.set_xlim(0, total)
747     ax.set_ylim(0, 1)
748     ax.axis("off")
749     ax.text(total / 2, 0.92, "Transfer edge descriptor", ha="center", fontsize=20, weight="bold")
750     x = 0
751     for name, bits in fields:
752         ax.add_patch(Rectangle((x, 0.35), bits, 0.30, facecolor="#dbeafe", edgecolor="black"))
753         ax.text(x + bits / 2, 0.50, f"{name}\n{bits}", ha="center", va="center", fontsize=8.2)
754         x += bits
755     ax.text(total / 2, 0.14, "The edge descriptor is the hardware form of P_beta T P_alpha. It carries the operator class and its certificate weight.", ha="center", fontsize=11)
756     _save(fig, out, "05_transfer_descriptor")
757 
758 
759 def fig_memory(config: PrismConfig, out: Path) -> None:
760     fig, ax = plt.subplots(figsize=(12.8, 6.8))
761     ax.set_xlim(0, 1)
762     ax.set_ylim(0, 1)
763     ax.axis("off")
764     ax.text(0.5, 0.95, "Factor-first packet memory hierarchy", ha="center", fontsize=20, weight="bold")
765     levels = [
766         (0.08, 0.72, 0.84, 0.14, f"HBM4 exact archive and cold factors\n{config.total_hbm_gib} GiB   {config.total_hbm_tbps:.1f} TB/s aggregate", "#dcfce7"),
767         (0.16, 0.52, 0.68, 0.14, f"Shared packet cache and graph store\n{config.compute_chiplets * config.shared_sram_mib_per_chiplet} MiB", "#dbeafe"),
768         (0.24, 0.32, 0.52, 0.14, f"Distributed PSM scratchpads\n{config.execution_groups * config.local_sram_kib_per_execution_group / 1024:.0f} MiB", "#ede9fe"),
769         (0.34, 0.12, 0.32, 0.14, "Registers and exact accumulators\nactive blocks, low-rank panels, residuals", "#fef3c7"),
770     ]
771     for x, y, w, h, text, face in levels:
772         _box(ax, x, y, w, h, text, face=face, fontsize=11)
773     for i in range(len(levels) - 1):
774         x0, y0, w0, h0, _, _ = levels[i]
775         x1, y1, w1, h1, _, _ = levels[i + 1]
776         _arrow(ax, (0.50, y0), (0.50, y1 + h1))
777     ax.text(0.5, 0.03, "No dense array is required. Explicit materialization is only one optional output contract.", ha="center", fontsize=11.5, weight="bold")
778     _save(fig, out, "06_memory_hierarchy")
779 
780 
781 def fig_bdlr(out: Path) -> None:
782     fig, ax = plt.subplots(figsize=(12.8, 5.7))
783     ax.set_xlim(0, 1)
784     ax.set_ylim(0, 1)
785     ax.axis("off")
786     ax.text(0.5, 0.92, "BDLR execution dataflow", ha="center", fontsize=20, weight="bold")
787     _box(ax, 0.05, 0.39, 0.12, 0.20, "input X", face="#f8fafc", fontsize=13)
788     _box(ax, 0.25, 0.60, 0.18, 0.18, "block engines\nD X", face="#dbeafe", fontsize=12)
789     _box(ax, 0.25, 0.20, 0.18, 0.18, "reduction engine\nV* X", face="#ede9fe", fontsize=12)
790     _box(ax, 0.52, 0.20, 0.18, 0.18, "low-rank engine\nU (V* X)", face="#dcfce7", fontsize=12)
791     _box(ax, 0.78, 0.39, 0.16, 0.20, "packet sum\nD X + U(V*X)", face="#fef3c7", fontsize=12)
792     _arrow(ax, (0.17, 0.49), (0.25, 0.69))
793     _arrow(ax, (0.17, 0.49), (0.25, 0.29))
794     _arrow(ax, (0.43, 0.29), (0.52, 0.29))
795     _arrow(ax, (0.43, 0.69), (0.78, 0.52))
796     _arrow(ax, (0.70, 0.29), (0.78, 0.46))
797     ax.text(0.5, 0.07, "The factors remain compressed through the pipeline. Materialization is a separate command.", ha="center", fontsize=11.5)
798     _save(fig, out, "07_bdlr_dataflow")
799 
800 
801 def fig_transform(out: Path) -> None:
802     fig, ax = plt.subplots(figsize=(12.8, 6.2))
803     ax.set_xlim(0, 1)
804     ax.set_ylim(0, 1)
805     ax.axis("off")
806     ax.text(0.5, 0.94, "Unified transform fabric", ha="center", fontsize=20, weight="bold")
807     _box(ax, 0.05, 0.40, 0.14, 0.20, "input packet\nstream", face="#f8fafc", fontsize=12)
808     units = [
809         (0.27, 0.70, "FFT and DCT\nbutterflies"),
810         (0.27, 0.46, "wavelet lifting\npredict and update"),
811         (0.27, 0.22, "polyphase\npermute and interleave"),
812     ]
813     for x, y, text in units:
814         _box(ax, x, y, 0.20, 0.15, text, face="#dbeafe", fontsize=11)
815         _arrow(ax, (0.19, 0.50), (x, y + 0.075))
816     _box(ax, 0.57, 0.37, 0.18, 0.26, "selector bank\nfrequency bands\npacket-tree nodes\nphase channels", face="#ede9fe", fontsize=11)
817     for x, y, _ in units:
818         _arrow(ax, (x + 0.20, y + 0.075), (0.57, 0.50))
819     _box(ax, 0.82, 0.40, 0.14, 0.20, "analysis output\nor synthesis", face="#dcfce7", fontsize=12)
820     _arrow(ax, (0.75, 0.50), (0.82, 0.50))
821     ax.text(0.5, 0.07, "One configurable butterfly, permutation, and selector fabric replaces separate fixed-function data paths.", ha="center", fontsize=11.3)
822     _save(fig, out, "08_transform_fabric")
823 
824 
825 def fig_edge_pipeline(out: Path) -> None:
826     fig, ax = plt.subplots(figsize=(13.0, 6.0))
827     ax.set_xlim(0, 1)
828     ax.set_ylim(0, 1)
829     ax.axis("off")
830     ax.text(0.5, 0.93, "Self-calibrating edge recovery pipeline", ha="center", fontsize=20, weight="bold")
831     stages = [
832         ("spectral packet", "high-frequency coefficients"),
833         ("polyphase", "decimated channels"),
834         ("Hankel", "streaming matrix build"),
835         ("small solver", "rank and annihilator"),
836         ("phase recovery", "physical roots"),
837         ("edge synthesis", "native singular packet"),
838         ("certificate", "residual and branch margin"),
839     ]
840     xs = np.linspace(0.025, 0.84, len(stages))
841     for i, (x, (a, b)) in enumerate(zip(xs, stages, strict=True)):
842         _box(ax, x, 0.40, 0.13, 0.22, f"{a}\n{b}", face="#f8fafc", fontsize=9.7)
843         if i < len(stages) - 1:
844             _arrow(ax, (x + 0.13, 0.51), (xs[i + 1], 0.51))
845     ax.text(0.5, 0.17, "The small Hankel, QR, SVD, root, and Vandermonde operations remain on one resident solver cluster.", ha="center", fontsize=11.2)
846     _save(fig, out, "09_edge_recovery_pipeline")
847 
848 
849 def fig_bayes(out: Path) -> None:
850     fig, ax = plt.subplots(figsize=(12.8, 6.1))
851     ax.set_xlim(0, 1)
852     ax.set_ylim(0, 1)
853     ax.axis("off")
854     ax.text(0.5, 0.94, "Typed Bayesian operator posterior", ha="center", fontsize=20, weight="bold")
855     _box(ax, 0.05, 0.39, 0.16, 0.22, "resident state matrix\nmacro meso micro", face="#dbeafe", fontsize=11.5)
856     _box(ax, 0.29, 0.66, 0.18, 0.16, "linear models", face="#f8fafc", fontsize=11)
857     _box(ax, 0.29, 0.43, 0.18, 0.16, "quadratic and hybrid", face="#f8fafc", fontsize=11)
858     _box(ax, 0.29, 0.20, 0.18, 0.16, "delay and memory", face="#f8fafc", fontsize=11)
859     for y in (0.74, 0.51, 0.28):
860         _arrow(ax, (0.21, 0.50), (0.29, y))
861     _box(ax, 0.56, 0.37, 0.18, 0.26, "shape-batched solver\nGram matrices\nCholesky or QR\npredictive score", face="#ede9fe", fontsize=11)
862     for y in (0.74, 0.51, 0.28):
863         _arrow(ax, (0.47, y), (0.56, 0.50))
864     _box(ax, 0.82, 0.39, 0.14, 0.22, "posterior mixture\nand disagreement", face="#dcfce7", fontsize=11)
865     _arrow(ax, (0.74, 0.50), (0.82, 0.50))
866     ax.text(0.5, 0.08, "Shared design matrices are loaded once. Candidate programs are grouped by feature shape and precision.", ha="center", fontsize=11.2)
867     _save(fig, out, "10_bayesian_pipeline")
868 
869 
870 def fig_scheduler(out: Path) -> None:
871     fig, ax = plt.subplots(figsize=(12.8, 6.3))
872     ax.set_xlim(0, 1)
873     ax.set_ylim(0, 1)
874     ax.axis("off")
875     ax.text(0.5, 0.95, "Local refinement without global recompilation", ha="center", fontsize=20, weight="bold")
876     old = [(0.08, 0.58, "A"), (0.25, 0.58, "B"), (0.42, 0.58, "packet v"), (0.59, 0.58, "C"), (0.76, 0.58, "D")]
877     for x, y, label in old:
878         _box(ax, x, y, 0.12, 0.14, label, face="#f8fafc", fontsize=12)
879     for i in range(len(old) - 1):
880         _arrow(ax, (old[i][0] + 0.12, 0.65), (old[i + 1][0], 0.65))
881     children = [(0.35, 0.23, "v0"), (0.48, 0.23, "v1"), (0.61, 0.23, "v2")]
882     for x, y, label in children:
883         _box(ax, x, y, 0.10, 0.13, label, face="#dbeafe", fontsize=11)
884     _arrow(ax, (0.20, 0.65), (0.35, 0.30))
885     _arrow(ax, (0.45, 0.295), (0.48, 0.295))
886     _arrow(ax, (0.58, 0.295), (0.61, 0.295))
887     _arrow(ax, (0.71, 0.30), (0.76, 0.65))
888     ax.text(0.14, 0.18, "A and B keep their descriptors", ha="center", fontsize=10.5)
889     ax.text(0.82, 0.18, "C and D keep their descriptors", ha="center", fontsize=10.5)
890     ax.text(0.50, 0.08, "Only the child module and the edges incident to the parent packet are regenerated.", ha="center", fontsize=11.5, weight="bold")
891     _save(fig, out, "11_refinement_local_schedule")
892 
893 
894 def fig_compiler(out: Path) -> None:
895     fig, ax = plt.subplots(figsize=(13.0, 6.2))
896     ax.set_xlim(0, 1)
897     ax.set_ylim(0, 1)
898     ax.axis("off")
899     ax.text(0.5, 0.94, "PRISM packet compiler", ha="center", fontsize=20, weight="bold")
900     stages = [
901         ("calculus IR", "atlases, packets, edges"),
902         ("canonicalize", "prove shape and parent relations"),
903         ("choose form", "block sparse, BDLR, transform"),
904         ("allocate precision", "sensitivity and error budget"),
905         ("place", "tile and memory locality"),
906         ("schedule", "dependency tokens and replay"),
907         ("emit", "descriptors and certificates"),
908     ]
909     xs = np.linspace(0.02, 0.85, len(stages))
910     for i, (x, (a, b)) in enumerate(zip(xs, stages, strict=True)):
911         _box(ax, x, 0.42, 0.125, 0.21, f"{a}\n{b}", face="#f8fafc", fontsize=9.5)
912         if i < len(stages) - 1:
913             _arrow(ax, (x + 0.125, 0.525), (xs[i + 1], 0.525))
914     ax.text(0.5, 0.18, "The compiler is allowed to change the representation only when the exact output contract and certificate are preserved.", ha="center", fontsize=11.2)
915     _save(fig, out, "12_compiler_pipeline")
916 
917 
918 def fig_workload_map(out: Path) -> None:
919     labels = ["polyphase", "Fourier", "wavelet", "BDLR", "block sparse", "Bayesian", "Prony", "Volterra", "exact modular"]
920     engines = ["projector DMA", "transform", "transform", "matrix + low rank", "matrix + router", "solver", "solver + transform", "memory unit", "exact arithmetic"]
921     fig, ax = plt.subplots(figsize=(12.8, 6.5))
922     ax.axis("off")
923     ax.text(0.5, 0.95, "Calculus operations and hardware engines", ha="center", fontsize=20, weight="bold", transform=ax.transAxes)
924     y = 0.84
925     for i, (op, engine) in enumerate(zip(labels, engines, strict=True)):
926         face = "#f8fafc" if i % 2 == 0 else "#eef2ff"
927         ax.add_patch(Rectangle((0.08, y - 0.035), 0.84, 0.065, facecolor=face, edgecolor="none", transform=ax.transAxes))
928         ax.text(0.12, y, op, ha="left", va="center", fontsize=11.5, weight="bold", transform=ax.transAxes)
929         ax.text(0.52, y, engine, ha="left", va="center", fontsize=11.5, transform=ax.transAxes)
930         y -= 0.083
931     _save(fig, out, "13_workload_engine_map")
932 
933 
934 def fig_roofline(config: PrismConfig, estimates: Sequence[Estimate], out: Path) -> None:
935     fig, ax = plt.subplots(figsize=(10.8, 6.5))
936     intensity = np.logspace(-2, 4, 400)
937     peak = config.fp32_tflops
938     bw = config.total_hbm_tbps
939     roof = np.minimum(peak, intensity * bw)
940     ax.loglog(intensity, roof, linewidth=2.2, label="PRISM FP32 roofline")
941     for est in estimates:
942         perf = est.flops / (est.total_time_us * 1e-6) / 1e12
943         ax.scatter(est.arithmetic_intensity, perf, s=55)
944         ax.annotate(est.workload, (est.arithmetic_intensity, perf), xytext=(5, 5), textcoords="offset points", fontsize=8.5)
945     ax.set_xlabel("Arithmetic intensity in operations per byte")
946     ax.set_ylabel("Modeled throughput in TFLOP/s")
947     ax.set_title("PRISM roofline and representative packet workloads", weight="bold")
948     ax.grid(alpha=0.25, which="both")
949     ax.legend()
950     _save(fig, out, "14_roofline")
951 
952 
953 def fig_performance(pairs: Sequence[tuple[Estimate, Estimate]], out: Path) -> None:
954     names = [p[1].workload.replace("PRISM ", "") for p in pairs]
955     speedups = [p[0].total_time_us / p[1].total_time_us for p in pairs]
956     fig, ax = plt.subplots(figsize=(11.5, 6.2))
957     ax.bar(np.arange(len(names)), speedups)
958     ax.set_xticks(np.arange(len(names)), names, rotation=22, ha="right")
959     ax.set_ylabel("Modeled speedup over matched generic execution")
960     ax.set_title("Architecture-model speedups from structure and kernel fusion", weight="bold")
961     ax.grid(alpha=0.22, axis="y")
962     for i, value in enumerate(speedups):
963         ax.text(i, value, f"{value:.1f}x", ha="center", va="bottom", fontsize=10)
964     fig.tight_layout()
965     _save(fig, out, "15_modeled_speedups")
966 
967 
968 def fig_memory_reduction(out: Path) -> None:
969     n, b, r = 8192, 64, 8
970     dense = n * n * 4 / 2**20
971     bdlr = n * (b + 2 * r) * 4 / 2**20
972     block = n * b * 4 / 2**20
973     labels = ["dense", "block diagonal", "BDLR"]
974     values = [dense, block, bdlr]
975     fig, ax = plt.subplots(figsize=(9.5, 5.8))
976     ax.bar(labels, values)
977     ax.set_yscale("log")
978     ax.set_ylabel("Stored representation in MiB")
979     ax.set_title("Representation memory for n=8192, b=64, r=8", weight="bold")
980     ax.grid(alpha=0.22, axis="y", which="both")
981     for i, value in enumerate(values):
982         ax.text(i, value, f"{value:.2f}", ha="center", va="bottom", fontsize=10)
983     _save(fig, out, "16_representation_memory")
984 
985 
986 def fig_power(config: PrismConfig, out: Path) -> None:
987     labels = ["execution groups", "SRAM", "packet network", "HBM PHY", "transform and solver", "I/O and control", "clock and margin"]
988     values = [175, 80, 65, 70, 40, 30, 40]
989     if sum(values) != config.tdp_w:
990         raise RuntimeError("power budget must equal TDP")
991     fig, ax = plt.subplots(figsize=(9.0, 6.2))
992     ax.pie(values, labels=labels, autopct="%1.0f%%", startangle=90)
993     ax.set_title(f"Reference power budget at {config.tdp_w} W", weight="bold")
994     _save(fig, out, "17_power_budget")
995 
996 
997 def fig_area(out: Path) -> None:
998     labels = ["distributed SRAM", "matrix and vector lanes", "network and DMA", "transform and solver", "control and DFx"]
999     values = [35, 30, 15, 12, 8]
1000     fig, ax = plt.subplots(figsize=(9.0, 6.2))
1001     ax.pie(values, labels=labels, autopct="%1.0f%%", startangle=90)
1002     ax.set_title("Target compute-chiplet area allocation", weight="bold")
1003     _save(fig, out, "18_area_allocation")
1004 
1005 
1006 def fig_precision(out: Path) -> None:
1007     sensitivity = np.array([0.10, 0.18, 0.30, 0.52, 0.80, 1.20, 2.0, 3.2])
1008     cost = np.ones_like(sensitivity)
1009     tau = 0.02
1010     eps = tau * cost / (sensitivity * cost.sum())
1011     bits = np.clip(np.ceil(-np.log2(eps)), 8, 64)
1012     fig, ax = plt.subplots(figsize=(10.2, 5.8))
1013     ax.plot(sensitivity, bits, marker="o")
1014     ax.set_xlabel("Packet sensitivity weight")
1015     ax.set_ylabel("Allocated mantissa target in bits")
1016     ax.set_title("Sensitivity-driven precision tags", weight="bold")
1017     ax.grid(alpha=0.25)
1018     _save(fig, out, "19_precision_allocation")
1019 
1020 
1021 def fig_network(out: Path) -> None:
1022     fig, ax = plt.subplots(figsize=(10.8, 7.0))
1023     ax.set_xlim(-0.5, 7.5)
1024     ax.set_ylim(-0.85, 7.5)
1025     ax.set_aspect("equal")
1026     ax.set_xticks([])
1027     ax.set_yticks([])
1028     ax.set_title("One 8 x 8 compute-chiplet packet mesh", fontsize=18, weight="bold")
1029     for i in range(8):
1030         for j in range(8):
1031             ax.add_patch(Rectangle((i - 0.28, j - 0.28), 0.56, 0.56, facecolor="#e5e7eb", edgecolor="black", linewidth=0.7))
1032             if i < 7:
1033                 ax.plot([i + 0.28, i + 0.72], [j, j], color="black", linewidth=0.7)
1034             if j < 7:
1035                 ax.plot([i, i], [j + 0.28, j + 0.72], color="black", linewidth=0.7)
1036     ax.text(3.5, -0.58, "Routers support unicast, packet multicast, and tree reduction", ha="center", va="top", fontsize=10.5)
1037     _save(fig, out, "20_packet_mesh")
1038 
1039 
1040 def fig_isa(out: Path) -> None:
1041     groups = {
1042         "packet": ["PLOAD", "PSELECT", "PSPLIT", "PCOARSEN"],
1043         "transform": ["FFT", "WAVELET", "POLYPHASE", "SYNTH"],
1044         "transfer": ["BLK_GEMM", "BDLR", "SPARSE", "WOODBURY"],
1045         "solver": ["CHOLESKY", "QR", "SVD", "ROOT"],
1046         "logic": ["NONLINEAR", "MEMORY", "BAYES", "GRAPH"],
1047         "certificate": ["RESIDUAL", "BOUND", "TRACE", "COMMIT"],
1048     }
1049     fig, ax = plt.subplots(figsize=(12.0, 6.3))
1050     ax.axis("off")
1051     ax.text(0.5, 0.95, "Packet Seed Command groups", ha="center", fontsize=20, weight="bold", transform=ax.transAxes)
1052     y = 0.82
1053     for group, ops in groups.items():
1054         ax.text(0.08, y, group, ha="left", va="center", fontsize=12, weight="bold", transform=ax.transAxes)
1055         for j, op in enumerate(ops):
1056             x = 0.28 + j * 0.17
1057             ax.add_patch(FancyBboxPatch((x, y - 0.035), 0.13, 0.07, boxstyle="round,pad=0.01", facecolor="#eef2ff", edgecolor="black", transform=ax.transAxes))
1058             ax.text(x + 0.065, y, op, ha="center", va="center", fontsize=9.5, transform=ax.transAxes)
1059         y -= 0.125
1060     _save(fig, out, "21_command_groups")
1061 
1062 
1063 def fig_prototype(out: Path) -> None:
1064     fig, ax = plt.subplots(figsize=(12.8, 5.8))
1065     ax.set_xlim(0, 1)
1066     ax.set_ylim(0, 1)
1067     ax.axis("off")
1068     ax.text(0.5, 0.93, "Implementation path", ha="center", fontsize=20, weight="bold")
1069     stages = [
1070         ("software and CUDA", "shared Packet IR\nmeasured structured kernels"),
1071         ("trace comparison", "matched CUDA and PRISM\nbackend records"),
1072         ("FPGA PSM", "one multiprocessor\nexternal memory"),
1073         ("chiplet and package", "measured PSM scaling\nthen HBM4 and UCIe"),
1074     ]
1075     xs = [0.05, 0.29, 0.53, 0.77]
1076     for i, (x, (a, b)) in enumerate(zip(xs, stages, strict=True)):
1077         _box(ax, x, 0.38, 0.18, 0.24, f"{a}\n{b}", face="#f8fafc", fontsize=11)
1078         if i < len(stages) - 1:
1079             _arrow(ax, (x + 0.18, 0.50), (xs[i + 1], 0.50))
1080     ax.text(0.5, 0.14, "Each stage preserves the same descriptor format, operator contracts, and certificate semantics.", ha="center", fontsize=11.5)
1081     _save(fig, out, "22_implementation_path")
1082 
1083 
1084 def fig_negative_controls(out: Path) -> None:
1085     labels = ["regular BDLR", "regular block sparse", "multi-atlas fusion", "small one-off FFT", "dense unstructured GEMM", "irregular no-reuse graph"]
1086     suitability = [0.95, 0.88, 0.82, 0.28, 0.22, 0.18]
1087     fig, ax = plt.subplots(figsize=(11.0, 6.0))
1088     ax.barh(np.arange(len(labels)), suitability)
1089     ax.set_yticks(np.arange(len(labels)), labels)
1090     ax.set_xlim(0, 1)
1091     ax.set_xlabel("Expected architectural fit")
1092     ax.set_title("Workloads the chip should accelerate and workloads it should reject", weight="bold")
1093     ax.grid(alpha=0.22, axis="x")
1094     _save(fig, out, "23_workload_fit")
1095 
1096 
1097 
1098 def precision_allocation(sensitivity: np.ndarray, cost: np.ndarray, tau: float) -> np.ndarray:
1099     """Allocate a continuous packet error budget under the logarithmic cost model."""
1100     s = np.asarray(sensitivity, dtype=float)
1101     c = np.asarray(cost, dtype=float)
1102     if s.shape != c.shape or s.ndim != 1:
1103         raise ValueError("sensitivity and cost must be matching one-dimensional arrays")
1104     if np.any(s <= 0) or np.any(c <= 0) or tau <= 0:
1105         raise ValueError("sensitivities, costs, and tau must be positive")
1106     return tau * c / (s * np.sum(c))
1107 
1108 
1109 def certificate_commit_allowed(
1110     dependency_ready: bool,
1111     kernel_complete: bool,
1112     residual: float,
1113     error_budget: float,
1114 ) -> bool:
1115     """Return true only when execution and its required numerical certificate both pass."""
1116     return bool(
1117         dependency_ready
1118         and kernel_complete
1119         and error_budget >= 0
1120         and residual >= 0
1121         and residual <= error_budget
1122     )
1123 
1124 
1125 def local_refinement_descriptor_bound(
1126     child_nodes: int,
1127     child_edges: int,
1128     parent_indegree: int,
1129     parent_outdegree: int,
1130     entry_count: int,
1131     exit_count: int,
1132 ) -> int:
1133     """Worst-case descriptor regeneration bound for one local refinement."""
1134     values = [child_nodes, child_edges, parent_indegree, parent_outdegree, entry_count, exit_count]
1135     if any(v < 0 for v in values):
1136         raise ValueError("descriptor counts must be nonnegative")
1137     return (
1138         child_nodes
1139         + child_edges
1140         + parent_indegree * entry_count
1141         + parent_outdegree * exit_count
1142     )
1143 
1144 
1145 def route_workload(
1146     operator_kind: str,
1147     representation_reduction: float,
1148     reuse_count: int,
1149     graph_nodes: int,
1150 ) -> str:
1151     """Simple reference routing rule used by the compiler examples and tests."""
1152     if reuse_count < 1 or graph_nodes < 1 or representation_reduction <= 0:
1153         raise ValueError("invalid workload description")
1154     kind = operator_kind.lower()
1155     if kind in {"bdlr", "block_sparse", "transform", "small_solver", "packet_graph"}:
1156         if representation_reduction >= 1.25 and (reuse_count >= 2 or graph_nodes >= 8):
1157             return "prism"
1158     if kind == "dense" and representation_reduction < 1.25:
1159         return "dense_path"
1160     if graph_nodes <= 3 and reuse_count == 1:
1161         return "host_or_general_device"
1162     return "profile_before_routing"
1163 
1164 
1165 def bdlr_sensitivity_tables(config: PrismConfig, n: int = 8192, q: int = 32) -> dict[str, object]:
1166     block_sizes = np.array([16, 32, 64, 128, 256], dtype=int)
1167     ranks = np.array([2, 4, 8, 16, 32], dtype=int)
1168     dense = dense_apply_estimate(config, n, q)
1169     speedups = np.zeros((len(ranks), len(block_sizes)))
1170     storage = np.zeros_like(speedups)
1171     for i, rank in enumerate(ranks):
1172         for j, block in enumerate(block_sizes):
1173             packet = bdlr_estimate(config, n, int(block), int(rank), q)
1174             speedups[i, j] = dense.total_time_us / packet.total_time_us
1175             storage[i, j] = n / (block + 2 * rank)
1176     return {
1177         "block_sizes": block_sizes.tolist(),
1178         "ranks": ranks.tolist(),
1179         "modeled_speedups": speedups.tolist(),
1180         "storage_reduction": storage.tolist(),
1181     }
1182 
1183 
1184 def fig_packet_lifecycle(out: Path) -> None:
1185     fig, ax = plt.subplots(figsize=(13.0, 6.2))
1186     ax.set_xlim(0, 1)
1187     ax.set_ylim(0, 1)
1188     ax.axis("off")
1189     ax.text(0.5, 0.94, "Life of one packet command", ha="center", fontsize=21, weight="bold")
1190     labels = [
1191         ("archive", "packet data and descriptor", "#e0f2fe"),
1192         ("ready", "dependencies and storage valid", "#ede9fe"),
1193         ("execute", "selected engine runs local map", "#dcfce7"),
1194         ("provisional", "result held before commit", "#fef3c7"),
1195         ("certify", "residual or bound checked", "#fee2e2"),
1196         ("commit", "result becomes visible", "#dbeafe"),
1197     ]
1198     xs = np.linspace(0.03, 0.82, len(labels))
1199     for i, (x, (title, sub, face)) in enumerate(zip(xs, labels, strict=True)):
1200         _box(ax, x, 0.45, 0.145, 0.20, f"{title}\n{sub}", face=face, fontsize=10.2)
1201         if i < len(labels) - 1:
1202             _arrow(ax, (x + 0.145, 0.55), (xs[i + 1], 0.55))
1203     _arrow(ax, (0.89, 0.43), (0.57, 0.35), text="failed certificate returns the region for replay", rad=-0.20)
1204     ax.text(
1205         0.5,
1206         0.16,
1207         "The result remains provisional until the declared certificate passes. A failed check does not silently publish data.",
1208         ha="center",
1209         fontsize=11.3,
1210     )
1211     _save(fig, out, "24_packet_lifecycle")
1212 
1213 
1214 def fig_workload_router(out: Path) -> None:
1215     fig, ax = plt.subplots(figsize=(12.5, 7.0))
1216     ax.set_xlim(0, 1)
1217     ax.set_ylim(0, 1)
1218     ax.axis("off")
1219     ax.text(0.5, 0.95, "Compiler routing decision", ha="center", fontsize=21, weight="bold")
1220     _box(ax, 0.39, 0.79, 0.22, 0.10, "Is useful packet structure proved?", face="#fef3c7", fontsize=11)
1221     _box(ax, 0.08, 0.58, 0.22, 0.12, "No\nuse the dense path or another device", face="#fee2e2", fontsize=11)
1222     _box(ax, 0.39, 0.58, 0.22, 0.12, "Yes\ndoes it reduce bytes or arithmetic?", face="#e0f2fe", fontsize=10.5)
1223     _box(ax, 0.70, 0.58, 0.22, 0.12, "Unclear\nprofile both representations", face="#f3f4f6", fontsize=10.5)
1224     _box(ax, 0.23, 0.34, 0.22, 0.12, "No\nretain the simpler route", face="#fee2e2", fontsize=11)
1225     _box(ax, 0.55, 0.34, 0.22, 0.12, "Yes\nis reuse large enough to amortize setup?", face="#dcfce7", fontsize=10.2)
1226     _box(ax, 0.41, 0.10, 0.22, 0.12, "Route to PRISM\nattach certificate plan", face="#dbeafe", fontsize=11)
1227     _arrow(ax, (0.44, 0.79), (0.25, 0.70), text="no")
1228     _arrow(ax, (0.50, 0.79), (0.50, 0.70), text="yes")
1229     _arrow(ax, (0.56, 0.79), (0.81, 0.70), text="unknown")
1230     _arrow(ax, (0.45, 0.58), (0.34, 0.46), text="no")
1231     _arrow(ax, (0.55, 0.58), (0.66, 0.46), text="yes")
1232     _arrow(ax, (0.66, 0.34), (0.52, 0.22), text="yes")
1233     ax.text(0.72, 0.23, "A small one-off graph may still be routed away", ha="center", fontsize=10.5)
1234     _save(fig, out, "25_workload_router")
1235 
1236 
1237 def fig_chiplet_floorplan(config: PrismConfig, out: Path) -> None:
1238     fig, ax = plt.subplots(figsize=(11.2, 7.4))
1239     ax.set_xlim(0, 10)
1240     ax.set_ylim(0, 10)
1241     ax.set_aspect("equal")
1242     ax.axis("off")
1243     ax.set_title("Reference compute-chiplet floorplan", fontsize=20, weight="bold", pad=10)
1244     ax.add_patch(Rectangle((0.4, 0.4), 9.2, 9.2, facecolor="#f8fafc", edgecolor="black", linewidth=1.5))
1245     for i in range(8):
1246         for j in range(8):
1247             ax.add_patch(Rectangle((1.2 + i * 0.9, 1.2 + j * 0.9), 0.68, 0.68, facecolor="#dbeafe", edgecolor="#334155", linewidth=0.45))
1248     for y in [1.25, 3.25, 5.25, 7.25]:
1249         ax.add_patch(Rectangle((8.55, y), 0.55, 1.3, facecolor="#dcfce7", edgecolor="black", linewidth=0.8))
1250         ax.text(8.825, y + 0.65, "solver", ha="center", va="center", rotation=90, fontsize=8)
1251     for x in [1.2, 3.0, 4.8, 6.6]:
1252         ax.add_patch(Rectangle((x, 8.55), 1.25, 0.55, facecolor="#ede9fe", edgecolor="black", linewidth=0.8))
1253         ax.text(x + 0.625, 8.825, "transform", ha="center", va="center", fontsize=8)
1254     ax.add_patch(Rectangle((0.55, 1.2), 0.42, 7.2, facecolor="#fef3c7", edgecolor="black", linewidth=0.8))
1255     ax.text(0.76, 4.8, "shared SRAM banks", ha="center", va="center", rotation=90, fontsize=9)
1256     ax.add_patch(Rectangle((1.2, 0.55), 6.8, 0.42, facecolor="#fee2e2", edgecolor="black", linewidth=0.8))
1257     ax.text(4.6, 0.76, "UCIe and package router edge", ha="center", va="center", fontsize=9)
1258     ax.add_patch(Rectangle((8.55, 8.55), 0.55, 0.55, facecolor="#fecaca", edgecolor="black", linewidth=0.8))
1259     ax.text(8.825, 8.825, "exact", ha="center", va="center", fontsize=7.5)
1260     _save(fig, out, "26_chiplet_floorplan")
1261 
1262 
1263 def fig_bandwidth_budget(config: PrismConfig, out: Path) -> None:
1264     labels = ["PCIe x16 host", "HBM4 aggregate", "package fabric"]
1265     values = [config.host_bidirectional_gbps / 1000.0, config.total_hbm_tbps, config.package_network_tbps]
1266     fig, ax = plt.subplots(figsize=(10.5, 5.8))
1267     bars = ax.bar(labels, values, edgecolor="black")
1268     ax.set_ylabel("Reference bidirectional or aggregate bandwidth in TB/s")
1269     ax.set_title("Bandwidth levels in the reference package", weight="bold")
1270     ax.grid(alpha=0.22, axis="y")
1271     for bar, value in zip(bars, values, strict=True):
1272         ax.text(bar.get_x() + bar.get_width() / 2, value, f"{value:.3g}", ha="center", va="bottom", fontsize=10.5)
1273     ax.text(
1274         0.5,
1275         -0.18,
1276         "The host link is not the steady-state data path. Reusable packet graphs and factors are expected to remain on the package.",
1277         ha="center",
1278         transform=ax.transAxes,
1279         fontsize=10.5,
1280     )
1281     fig.tight_layout(rect=[0, 0.08, 1, 1])
1282     _save(fig, out, "27_bandwidth_budget")
1283 
1284 
1285 def fig_command_state_machine(out: Path) -> None:
1286     fig, ax = plt.subplots(figsize=(12.5, 6.3))
1287     ax.set_xlim(0, 1)
1288     ax.set_ylim(0, 1)
1289     ax.axis("off")
1290     ax.text(0.5, 0.94, "Command and certificate state machine", ha="center", fontsize=21, weight="bold")
1291     states = {
1292         "queued": (0.08, 0.54, "#e0f2fe"),
1293         "ready": (0.26, 0.54, "#ede9fe"),
1294         "running": (0.44, 0.54, "#dcfce7"),
1295         "provisional": (0.62, 0.54, "#fef3c7"),
1296         "committed": (0.82, 0.72, "#dbeafe"),
1297         "replay": (0.82, 0.36, "#fee2e2"),
1298     }
1299     for name, (x, y, face) in states.items():
1300         _box(ax, x, y, 0.12, 0.11, name, face=face, fontsize=11)
1301     _arrow(ax, (0.20, 0.595), (0.26, 0.595), text="tokens ready")
1302     _arrow(ax, (0.38, 0.595), (0.44, 0.595), text="resources granted")
1303     _arrow(ax, (0.56, 0.595), (0.62, 0.595), text="kernel done")
1304     _arrow(ax, (0.74, 0.61), (0.82, 0.74), text="certificate passes")
1305     _arrow(ax, (0.74, 0.56), (0.82, 0.42), text="certificate fails")
1306     _arrow(ax, (0.88, 0.36), (0.50, 0.49), text="reissue local region", rad=-0.18)
1307     ax.text(0.5, 0.16, "Only committed results can satisfy downstream completion tokens.", ha="center", fontsize=11.5)
1308     _save(fig, out, "28_command_state_machine")
1309 
1310 
1311 def fig_bdlr_sensitivity(config: PrismConfig, out: Path) -> None:
1312     table = bdlr_sensitivity_tables(config)
1313     blocks = table["block_sizes"]
1314     ranks = table["ranks"]
1315     speedups = np.array(table["modeled_speedups"])
1316     fig, ax = plt.subplots(figsize=(9.8, 6.4))
1317     im = ax.imshow(speedups, origin="lower", aspect="auto", cmap="viridis")
1318     ax.set_xticks(range(len(blocks)), blocks)
1319     ax.set_yticks(range(len(ranks)), ranks)
1320     ax.set_xlabel("Packet block width b")
1321     ax.set_ylabel("Low-rank correction r")
1322     ax.set_title("Modeled BDLR speedup sensitivity for n=8192 and q=32", weight="bold")
1323     for i in range(len(ranks)):
1324         for j in range(len(blocks)):
1325             ax.text(j, i, f"{speedups[i, j]:.1f}x", ha="center", va="center", fontsize=9, color="white" if speedups[i, j] < speedups.max() * 0.55 else "black")
1326     fig.colorbar(im, ax=ax, label="Modeled speedup")
1327     fig.tight_layout()
1328     _save(fig, out, "29_bdlr_speedup_sensitivity")
1329 
1330 
1331 def fig_bdlr_storage_sensitivity(config: PrismConfig, out: Path) -> None:
1332     table = bdlr_sensitivity_tables(config)
1333     blocks = table["block_sizes"]
1334     ranks = table["ranks"]
1335     reduction = np.array(table["storage_reduction"])
1336     fig, ax = plt.subplots(figsize=(9.8, 6.4))
1337     im = ax.imshow(reduction, origin="lower", aspect="auto", cmap="magma")
1338     ax.set_xticks(range(len(blocks)), blocks)
1339     ax.set_yticks(range(len(ranks)), ranks)
1340     ax.set_xlabel("Packet block width b")
1341     ax.set_ylabel("Low-rank correction r")
1342     ax.set_title("Dense-to-factor storage reduction", weight="bold")
1343     for i in range(len(ranks)):
1344         for j in range(len(blocks)):
1345             ax.text(j, i, f"{reduction[i, j]:.1f}x", ha="center", va="center", fontsize=9, color="white" if reduction[i, j] < reduction.max() * 0.50 else "black")
1346     fig.colorbar(im, ax=ax, label="Storage reduction")
1347     fig.tight_layout()
1348     _save(fig, out, "30_bdlr_storage_sensitivity")
1349 
1350 
1351 def fig_solver_pipeline(out: Path) -> None:
1352     stages = [
1353         ("FFT packet", 0.0, 3.0, "#dbeafe"),
1354         ("polyphase", 2.3, 1.3, "#ede9fe"),
1355         ("Hankel build", 3.1, 1.0, "#dcfce7"),
1356         ("rank and SVD", 4.0, 1.5, "#fef3c7"),
1357         ("roots and phase", 5.3, 1.5, "#fee2e2"),
1358         ("Vandermonde", 6.5, 1.2, "#cffafe"),
1359         ("native synthesis", 7.4, 1.5, "#bfdbfe"),
1360         ("certificate", 8.5, 1.0, "#fecaca"),
1361     ]
1362     fig, ax = plt.subplots(figsize=(12.0, 6.0))
1363     y = np.arange(len(stages))
1364     for i, (name, start, width, color) in enumerate(stages):
1365         ax.barh(i, width, left=start, color=color, edgecolor="black")
1366         ax.text(start + width / 2, i, name, ha="center", va="center", fontsize=9.5)
1367     ax.set_yticks([])
1368     ax.set_xlabel("Illustrative normalized time")
1369     ax.set_title("Resident small-solver pipeline for unknown-edge recovery", weight="bold")
1370     ax.set_xlim(0, 10)
1371     ax.grid(alpha=0.18, axis="x")
1372     ax.invert_yaxis()
1373     fig.tight_layout()
1374     _save(fig, out, "31_solver_pipeline")
1375 
1376 
1377 def fig_pr_lasm_state_map(out: Path) -> None:
1378     fig, ax = plt.subplots(figsize=(12.8, 7.0))
1379     ax.set_xlim(0, 1)
1380     ax.set_ylim(0, 1)
1381     ax.axis("off")
1382     ax.text(0.5, 0.95, "PR-LASM data that remains resident on PRISM", ha="center", fontsize=20, weight="bold")
1383     _box(ax, 0.05, 0.58, 0.25, 0.22, "exact archive\nspatial, Fourier, wavelet, polyphase\nreconstructible signal object", face="#e0f2fe", fontsize=10.8)
1384     _box(ax, 0.38, 0.67, 0.18, 0.12, "macro state\n48 values", face="#dcfce7", fontsize=11)
1385     _box(ax, 0.38, 0.49, 0.18, 0.12, "meso state\n96 values", face="#dcfce7", fontsize=11)
1386     _box(ax, 0.38, 0.31, 0.18, 0.12, "micro state\n192 values", face="#dcfce7", fontsize=11)
1387     _box(ax, 0.66, 0.58, 0.27, 0.22, "typed program portfolio\nlinear, nonlinear, memory, Bayesian\nshared resident design matrices", face="#fef3c7", fontsize=10.8)
1388     _box(ax, 0.66, 0.23, 0.27, 0.17, "transition graph and planner\nexact replay edges kept separate\nfrom probabilistic predictions", face="#ede9fe", fontsize=10.5)
1389     _arrow(ax, (0.30, 0.69), (0.38, 0.73))
1390     _arrow(ax, (0.30, 0.69), (0.38, 0.55))
1391     _arrow(ax, (0.30, 0.69), (0.38, 0.37))
1392     _arrow(ax, (0.56, 0.73), (0.66, 0.69))
1393     _arrow(ax, (0.56, 0.55), (0.66, 0.66))
1394     _arrow(ax, (0.56, 0.37), (0.66, 0.63))
1395     _arrow(ax, (0.80, 0.58), (0.80, 0.40))
1396     _arrow(ax, (0.70, 0.23), (0.28, 0.56), text="residual-driven refinement", rad=-0.16)
1397     ax.text(0.5, 0.10, "Compact prediction state never replaces the exact archive.", ha="center", fontsize=11.5)
1398     _save(fig, out, "32_pr_lasm_state_map")
1399 
1400 
1401 def fig_verification_pyramid(out: Path) -> None:
1402     fig, ax = plt.subplots(figsize=(10.0, 7.0))
1403     ax.set_xlim(0, 1)
1404     ax.set_ylim(0, 1)
1405     ax.axis("off")
1406     ax.text(0.5, 0.95, "Verification ladder", ha="center", fontsize=21, weight="bold")
1407     levels = [
1408         (0.08, 0.14, 0.84, "mathematical operator contracts", "#dbeafe"),
1409         (0.14, 0.27, 0.72, "behavioral and performance model", "#e0f2fe"),
1410         (0.20, 0.40, 0.60, "bit-accurate kernels and compiler conformance", "#dcfce7"),
1411         (0.26, 0.53, 0.48, "RTL simulation and formal properties", "#fef3c7"),
1412         (0.32, 0.66, 0.36, "FPGA overlay and measured data movement", "#fee2e2"),
1413         (0.38, 0.79, 0.24, "single-chiplet silicon", "#fecaca"),
1414     ]
1415     for x, y, w, label, color in levels:
1416         ax.add_patch(Rectangle((x, y), w, 0.09, facecolor=color, edgecolor="black", linewidth=1.0))
1417         ax.text(x + w / 2, y + 0.045, label, ha="center", va="center", fontsize=10.5)
1418     ax.text(0.5, 0.06, "The present work covers the first two levels and defines the interfaces needed by the remaining levels.", ha="center", fontsize=10.7)
1419     _save(fig, out, "33_verification_pyramid")
1420 
1421 
1422 def fig_fpga_overlay(out: Path) -> None:
1423     fig, ax = plt.subplots(figsize=(12.6, 6.6))
1424     ax.set_xlim(0, 1)
1425     ax.set_ylim(0, 1)
1426     ax.axis("off")
1427     ax.text(0.5, 0.94, "Recommended FPGA overlay", ha="center", fontsize=21, weight="bold")
1428     _box(ax, 0.04, 0.40, 0.15, 0.20, "host and runtime\nPCIe DMA\ncommand ring", face="#fef3c7", fontsize=10.5)
1429     _box(ax, 0.25, 0.62, 0.17, 0.16, "packet directory\nand token scoreboard", face="#e0f2fe", fontsize=10.5)
1430     _box(ax, 0.25, 0.28, 0.17, 0.16, "projector DMA\nand packet SRAM", face="#ede9fe", fontsize=10.5)
1431     _box(ax, 0.49, 0.62, 0.17, 0.16, "transform engine\nFFT and lifting", face="#dcfce7", fontsize=10.5)
1432     _box(ax, 0.49, 0.28, 0.17, 0.16, "matrix and low-rank\nengine", face="#dbeafe", fontsize=10.5)
1433     _box(ax, 0.73, 0.62, 0.17, 0.16, "small solver\nQR, SVD, roots", face="#fef3c7", fontsize=10.5)
1434     _box(ax, 0.73, 0.28, 0.17, 0.16, "certificate unit\nand commit controller", face="#fee2e2", fontsize=10.5)
1435     _arrow(ax, (0.19, 0.50), (0.25, 0.70))
1436     _arrow(ax, (0.19, 0.50), (0.25, 0.36))
1437     _arrow(ax, (0.42, 0.70), (0.49, 0.70))
1438     _arrow(ax, (0.42, 0.36), (0.49, 0.36))
1439     _arrow(ax, (0.66, 0.70), (0.73, 0.70))
1440     _arrow(ax, (0.66, 0.36), (0.73, 0.36))
1441     _arrow(ax, (0.82, 0.62), (0.82, 0.44))
1442     _arrow(ax, (0.73, 0.34), (0.66, 0.34), text="replay on failure", rad=-0.18)
1443     ax.text(0.5, 0.12, "The overlay should preserve the final descriptor format so software can move to a later ASIC without changing its graph model.", ha="center", fontsize=10.8)
1444     _save(fig, out, "34_fpga_overlay")
1445 
1446 
1447 def fig_risk_matrix(out: Path) -> None:
1448     risks = [
1449         (4, 4, "HBM and package power"),
1450         (3, 4, "small-solver latency"),
1451         (3, 3, "NoC congestion"),
1452         (2, 5, "compiler misclassification"),
1453         (3, 2, "descriptor overhead"),
1454         (2, 4, "certificate false pass"),
1455         (2, 3, "exact-arithmetic timing"),
1456     ]
1457     fig, ax = plt.subplots(figsize=(9.2, 6.8))
1458     ax.set_xlim(0.5, 5.5)
1459     ax.set_ylim(0.5, 5.5)
1460     ax.set_xticks(range(1, 6))
1461     ax.set_yticks(range(1, 6))
1462     ax.set_xlabel("Likelihood before mitigation")
1463     ax.set_ylabel("Impact")
1464     ax.set_title("Development risk map", weight="bold")
1465     for x in range(1, 6):
1466         for y in range(1, 6):
1467             score = x * y
1468             color = "#dcfce7" if score <= 6 else "#fef3c7" if score <= 12 else "#fee2e2"
1469             ax.add_patch(Rectangle((x - 0.5, y - 0.5), 1, 1, facecolor=color, edgecolor="white"))
1470     for x, y, label in risks:
1471         ax.scatter([x], [y], s=70, edgecolor="black", zorder=3)
1472         ax.text(x + 0.08, y + 0.08, label, fontsize=8.5, va="bottom")
1473     ax.grid(color="white", linewidth=1.2)
1474     fig.tight_layout()
1475     _save(fig, out, "35_risk_matrix")
1476 
1477 
1478 def fig_compiler_trace(out: Path) -> None:
1479     fig, ax = plt.subplots(figsize=(12.8, 6.2))
1480     ax.set_xlim(0, 1)
1481     ax.set_ylim(0, 1)
1482     ax.axis("off")
1483     ax.text(0.5, 0.94, "Worked compiler trace for one BDLR operator", ha="center", fontsize=20, weight="bold")
1484     stages = [
1485         (0.03, "typed operator\n$A=D+UV^*$"),
1486         (0.20, "legality\nshape and output contract"),
1487         (0.37, "representation\nkeep factors"),
1488         (0.54, "placement\nco-locate blocks and factor rows"),
1489         (0.71, "precision\nallocate edge budgets"),
1490         (0.86, "schedule\nreduce and broadcast"),
1491     ]
1492     for i, (x, text) in enumerate(stages):
1493         _box(ax, x, 0.48, 0.12, 0.19, text, face="#f8fafc", fontsize=9.8)
1494         if i < len(stages) - 1:
1495             _arrow(ax, (x + 0.12, 0.575), (stages[i + 1][0], 0.575))
1496     ax.text(0.5, 0.29, "Explicit array never created", ha="center", fontsize=13, weight="bold")
1497     ax.text(0.5, 0.20, "Stored scalars fall from $n^2$ to $n(b+2r)$ while the external operator contract remains unchanged.", ha="center", fontsize=11)
1498     _save(fig, out, "36_compiler_trace")
1499 
1500 
1501 def fig_packet_streaming_multiprocessor(config: PrismConfig, out: Path) -> None:
1502     fig, ax = plt.subplots(figsize=(13.0, 7.2))
1503     ax.set_xlim(0, 1)
1504     ax.set_ylim(0, 1)
1505     ax.axis("off")
1506     ax.text(0.5, 0.96, "Packet Streaming Multiprocessor", ha="center", va="top", fontsize=21, weight="bold")
1507     _box(ax, 0.05, 0.69, 0.19, 0.16, "front end\npacket warps\ndependency tokens", face="#fef3c7", fontsize=10.5)
1508     _box(ax, 0.29, 0.69, 0.19, 0.16, "representation router\nblock, factors, transform\nsmall solver, dense leaf", face="#ede9fe", fontsize=10.0)
1509     _box(ax, 0.53, 0.69, 0.19, 0.16, "descriptor and atlas cache\nparent, precision, output contract", face="#e0f2fe", fontsize=9.8)
1510     _box(ax, 0.77, 0.69, 0.18, 0.16, "certificate and commit\nresidual, bound, replay", face="#fee2e2", fontsize=10.2)
1511     for i in range(3):
1512         starts = [0.24, 0.48, 0.72]
1513         ends = [0.29, 0.53, 0.77]
1514         _arrow(ax, (starts[i], 0.77), (ends[i], 0.77))
1515     xs = [0.06, 0.29, 0.52, 0.75]
1516     colors = ["#dbeafe", "#dcfce7", "#fef3c7", "#ede9fe"]
1517     labels = [
1518         "execution group 0\n16 x 16 matrix\nvector lanes",
1519         "execution group 1\n16 x 16 matrix\nvector lanes",
1520         "execution group 2\n16 x 16 matrix\nvector lanes",
1521         "execution group 3\n16 x 16 matrix\nvector lanes",
1522     ]
1523     for x, face, label in zip(xs, colors, labels, strict=True):
1524         _box(ax, x, 0.36, 0.19, 0.22, label, face=face, fontsize=10.4)
1525         _arrow(ax, (0.385, 0.69), (x + 0.095, 0.58))
1526     _box(ax, 0.12, 0.10, 0.34, 0.16, f"PSM packet scratchpad\n{config.execution_groups_per_psm * config.local_sram_kib_per_execution_group // 1024} MiB, four independently banked regions", face="#f3f4f6", fontsize=11)
1527     _box(ax, 0.54, 0.10, 0.34, 0.16, "asynchronous packet DMA and mesh port\ngather, scatter, multicast, reduction", face="#f3f4f6", fontsize=11)
1528     for x in xs:
1529         _arrow(ax, (x + 0.095, 0.36), (0.29, 0.26))
1530         _arrow(ax, (x + 0.095, 0.36), (0.71, 0.26))
1531     ax.text(0.5, 0.03, "A packet warp may invoke several execution forms without converting the operator into one dense array.", ha="center", fontsize=11.4, weight="bold")
1532     _save(fig, out, "37_packet_streaming_multiprocessor")
1533 
1534 
1535 def fig_mixed_representation_space(out: Path) -> None:
1536     fig, ax = plt.subplots(figsize=(13.0, 6.8))
1537     ax.set_xlim(0, 1)
1538     ax.set_ylim(0, 1)
1539     ax.axis("off")
1540     ax.text(0.5, 0.95, "One operator, several executable representations", ha="center", fontsize=21, weight="bold")
1541     _box(ax, 0.39, 0.72, 0.22, 0.14, "typed operator\noutput contract and tolerance", face="#fef3c7", fontsize=11.5)
1542     forms = [
1543         (0.04, 0.43, "block and\nblock sparse", "#dbeafe"),
1544         (0.23, 0.43, "low rank and\nBDLR factors", "#dcfce7"),
1545         (0.42, 0.43, "Kronecker and\ntensor factors", "#ede9fe"),
1546         (0.61, 0.43, "sparse chain and\nbutterfly", "#e0f2fe"),
1547         (0.80, 0.43, "transform and\npacket graph", "#fef3c7"),
1548     ]
1549     for x, y, label, face in forms:
1550         _box(ax, x, y, 0.16, 0.15, label, face=face, fontsize=10.5)
1551         _arrow(ax, (0.50, 0.72), (x + 0.08, y + 0.15))
1552     _box(ax, 0.38, 0.14, 0.24, 0.14, "explicit full array\noptional fallback or requested output", face="#fee2e2", fontsize=11.0)
1553     for x, y, _, _ in forms:
1554         _arrow(ax, (x + 0.08, y), (0.50, 0.28), rad=0.04 if x < 0.5 else -0.04)
1555     ax.text(0.5, 0.05, "The dense representation is one candidate leaf. It is not the machine state that every path must preserve.", ha="center", fontsize=11.5, weight="bold")
1556     _save(fig, out, "38_mixed_representation_space")
1557 
1558 
1559 def fig_pmr_gemm_planner(out: Path) -> None:
1560     fig, ax = plt.subplots(figsize=(13.0, 7.0))
1561     ax.set_xlim(0, 1)
1562     ax.set_ylim(0, 1)
1563     ax.axis("off")
1564     ax.text(0.5, 0.96, "PMR-GEMM mixed-representation planning", ha="center", fontsize=21, weight="bold")
1565     _box(ax, 0.39, 0.78, 0.22, 0.12, "product node\nA B", face="#fef3c7", fontsize=13)
1566     choices = [
1567         (0.04, 0.54, "generic dense leaf"),
1568         (0.21, 0.54, "low-rank\ncontraction"),
1569         (0.38, 0.54, "Kronecker\nidentity"),
1570         (0.55, 0.54, "transform-domain\nrule"),
1571         (0.72, 0.54, "sparse factor\nchain"),
1572         (0.87, 0.54, "split and\nrecurse"),
1573     ]
1574     for x, y, label in choices:
1575         w = 0.12 if x < 0.85 else 0.10
1576         _box(ax, x, y, w, 0.14, label, face="#f8fafc", fontsize=9.7)
1577         _arrow(ax, (0.50, 0.78), (x + w / 2, y + 0.14))
1578     _box(ax, 0.24, 0.25, 0.22, 0.13, "Bellman objective\nlocal cost + error + child plans", face="#dbeafe", fontsize=10.8)
1579     _box(ax, 0.54, 0.25, 0.22, 0.13, "output contract\napply, factors, transform plan, array", face="#dcfce7", fontsize=10.5)
1580     _arrow(ax, (0.92, 0.54), (0.35, 0.38))
1581     _arrow(ax, (0.50, 0.54), (0.65, 0.38))
1582     _box(ax, 0.39, 0.06, 0.22, 0.12, "least-cost certified plan", face="#ede9fe", fontsize=12)
1583     _arrow(ax, (0.35, 0.25), (0.48, 0.18))
1584     _arrow(ax, (0.65, 0.25), (0.52, 0.18))
1585     ax.text(0.5, 0.01, "The generic leaf remains available, but structured leaves are not converted back to dense form unless the output contract asks for it.", ha="center", fontsize=10.8)
1586     _save(fig, out, "39_pmr_gemm_planner")
1587 
1588 
1589 def fig_shared_packet_ir(out: Path) -> None:
1590     fig, ax = plt.subplots(figsize=(13.0, 6.6))
1591     ax.set_xlim(0, 1)
1592     ax.set_ylim(0, 1)
1593     ax.axis("off")
1594     ax.text(0.5, 0.95, "One Packet IR, two execution backends", ha="center", fontsize=21, weight="bold")
1595     _box(ax, 0.37, 0.73, 0.26, 0.13, "calculus front end\npackets, atlases, transfer graph", face="#fef3c7", fontsize=11.2)
1596     _box(ax, 0.37, 0.52, 0.26, 0.13, "Packet IR\nrepresentation, schedule, precision, certificate", face="#ede9fe", fontsize=10.8)
1597     _arrow(ax, (0.50, 0.73), (0.50, 0.65))
1598     _box(ax, 0.08, 0.20, 0.34, 0.22, "CUDA compatibility backend\nCUDA Graphs\nasynchronous copies\ngrouped GEMM, cuFFT, cuSPARSE, CUB", face="#dbeafe", fontsize=10.8)
1599     _box(ax, 0.58, 0.20, 0.34, 0.22, "PRISM backend\nPacket Streaming Multiprocessors\nresident factors and small solvers\nlocal refinement and certified commit", face="#dcfce7", fontsize=10.8)
1600     _arrow(ax, (0.43, 0.52), (0.25, 0.42))
1601     _arrow(ax, (0.57, 0.52), (0.75, 0.42))
1602     ax.text(0.5, 0.08, "The CUDA path is the software foundation and the matched baseline. PRISM accelerates the same Packet IR rather than replacing it with a different mathematics.", ha="center", fontsize=11.2, weight="bold")
1603     _save(fig, out, "40_shared_packet_ir")
1604 
1605 
1606 def fig_gain_decomposition(out: Path) -> None:
1607     data = gain_decomposition()
1608     labels = ["representation\nreduction", "measured packet CUDA\nvs dense CUDA", "CUDA Graph gain\nsame representation", "compressed output\nvs materialization"]
1609     values = [float(data["representation_reduction_bdlr"]), float(data["measured_packet_gpu_gain_vs_dense_gpu"]), float(data["measured_cuda_graph_gain_same_representation"]), float(data["measured_compressed_output_gain"])]
1610     fig, ax = plt.subplots(figsize=(12.2, 6.3))
1611     bars = ax.bar(np.arange(len(values)), values)
1612     ax.set_yscale("log")
1613     ax.set_xticks(np.arange(len(values)), labels)
1614     ax.set_ylabel("Gain or reduction factor, logarithmic scale")
1615     ax.set_title("Separate the source of each gain", weight="bold")
1616     ax.grid(alpha=0.22, axis="y", which="both")
1617     for bar, value in zip(bars, values, strict=True):
1618         ax.text(bar.get_x() + bar.get_width() / 2, value * 1.08, f"{value:.2f}x", ha="center", va="bottom", fontsize=10.5)
1619     fig.text(0.5, 0.01, "These factors answer different questions. They are not multiplied unless the timing scope, representation, and output contract match.", ha="center", fontsize=10.5)
1620     fig.tight_layout(rect=[0, 0.05, 1, 1])
1621     _save(fig, out, "41_gain_decomposition")
1622 
1623 
1624 def fig_output_contracts(out: Path) -> None:
1625     fig, ax = plt.subplots(figsize=(13.0, 6.5))
1626     ax.set_xlim(0, 1)
1627     ax.set_ylim(0, 1)
1628     ax.axis("off")
1629     ax.text(0.5, 0.95, "Output contracts determine whether materialization is needed", ha="center", fontsize=21, weight="bold")
1630     _box(ax, 0.06, 0.42, 0.18, 0.20, "operator plan\npacket graph or factors", face="#fef3c7", fontsize=11.5)
1631     contracts = [
1632         (0.31, 0.70, "apply to vectors\nreturn Y"),
1633         (0.31, 0.49, "compressed operator\nreturn factors"),
1634         (0.31, 0.28, "transform plan\nreturn chain"),
1635         (0.31, 0.07, "explicit array\nreturn every entry"),
1636     ]
1637     outputs = [
1638         (0.68, 0.70, "streamed application\nno matrix object"),
1639         (0.68, 0.49, "BDLR, Kronecker, sparse chain\nno full array"),
1640         (0.68, 0.28, "FFT, wavelet, butterfly\nno full array"),
1641         (0.68, 0.07, "materialization command\nonly for this contract"),
1642     ]
1643     for (x, y, label), (_, _, output) in zip(contracts, outputs, strict=True):
1644         _box(ax, x, y, 0.23, 0.14, label, face="#f8fafc", fontsize=10.5)
1645         _box(ax, 0.68, y, 0.25, 0.14, output, face="#dbeafe" if y > 0.1 else "#fee2e2", fontsize=10.2)
1646         _arrow(ax, (0.24, 0.52), (x, y + 0.07), rad=0.04)
1647         _arrow(ax, (x + 0.23, y + 0.07), (0.68, y + 0.07))
1648     ax.text(0.5, 0.02, "The architecture preserves the requested mathematical result, not an unnecessary intermediate representation.", ha="center", fontsize=11.3, weight="bold")
1649     _save(fig, out, "42_output_contracts")
1650 
1651 
1652 def fig_cuda_measured_baselines(out: Path) -> None:
1653     data = measured_cuda_baselines()
1654     labels = ["equal block\npacket", "BDLR standard\npacket", "BDLR graph\npacket", "compressed\nproduct", "materialized\nproduct"]
1655     values = [
1656         float(data["equal_block_packet_ms"]),
1657         float(data["bdlr_standard_packet_ms"]),
1658         float(data["bdlr_graph_packet_ms"]),
1659         float(data["compressed_product_ms"]),
1660         float(data["materialized_product_ms"]),
1661     ]
1662     fig, ax = plt.subplots(figsize=(12.4, 6.2))
1663     bars = ax.bar(np.arange(len(values)), values)
1664     ax.set_xticks(np.arange(len(values)), labels)
1665     ax.set_ylabel("Measured time in milliseconds")
1666     ax.set_title("Existing CUDA packet algorithms provide the software foundation", weight="bold")
1667     ax.grid(alpha=0.22, axis="y")
1668     for bar, value in zip(bars, values, strict=True):
1669         ax.text(bar.get_x() + bar.get_width() / 2, value + max(values) * 0.025, f"{value:.4f}", ha="center", va="bottom", fontsize=10)
1670     fig.text(0.5, 0.01, "Prior measurements on an RTX 3070 Ti Laptop GPU. Compressed and materialized product rows have different output contracts.", ha="center", fontsize=10.4)
1671     fig.tight_layout(rect=[0, 0.05, 1, 1])
1672     _save(fig, out, "43_cuda_measured_baselines")
1673 
1674 
1675 def generate_all(root: Path) -> dict[str, object]:
1676     config = PrismConfig()
1677     figures = root / "figures"
1678     results_dir = root / "results"
1679     results_dir.mkdir(parents=True, exist_ok=True)
1680 
1681     dense = dense_apply_estimate(config, 8192, 32)
1682     bdlr = bdlr_estimate(config, 8192, 64, 8, 32)
1683     sparse = block_sparse_estimate(config, 128, 64, 5, 32)
1684     atlas_base, atlas_prism = multi_atlas_estimate(config, 1024 * 1024, 5)
1685     edge_base, edge_prism = edge_recovery_estimate(config, 16384, 12)
1686     bayes_base, bayes_prism = bayes_estimate(config, 256, 4096, 96, 8)
1687 
1688     estimates = [dense, bdlr, sparse, atlas_base, atlas_prism, edge_base, edge_prism, bayes_base, bayes_prism]
1689     pairs = [(dense, bdlr), (atlas_base, atlas_prism), (edge_base, edge_prism), (bayes_base, bayes_prism)]
1690 
1691     fig_package(config, figures)
1692     fig_tile(config, figures)
1693     fig_calculus_pipeline(figures)
1694     fig_descriptor(figures)
1695     fig_transfer_descriptor(figures)
1696     fig_memory(config, figures)
1697     fig_bdlr(figures)
1698     fig_transform(figures)
1699     fig_edge_pipeline(figures)
1700     fig_bayes(figures)
1701     fig_scheduler(figures)
1702     fig_compiler(figures)
1703     fig_workload_map(figures)
1704     fig_roofline(config, [bdlr, sparse, atlas_prism, edge_prism, bayes_prism], figures)
1705     fig_performance(pairs, figures)
1706     fig_memory_reduction(figures)
1707     fig_power(config, figures)
1708     fig_area(figures)
1709     fig_precision(figures)
1710     fig_network(figures)
1711     fig_isa(figures)
1712     fig_prototype(figures)
1713     fig_negative_controls(figures)
1714     fig_packet_lifecycle(figures)
1715     fig_workload_router(figures)
1716     fig_chiplet_floorplan(config, figures)
1717     fig_bandwidth_budget(config, figures)
1718     fig_command_state_machine(figures)
1719     fig_bdlr_sensitivity(config, figures)
1720     fig_bdlr_storage_sensitivity(config, figures)
1721     fig_solver_pipeline(figures)
1722     fig_pr_lasm_state_map(figures)
1723     fig_verification_pyramid(figures)
1724     fig_fpga_overlay(figures)
1725     fig_risk_matrix(figures)
1726     fig_compiler_trace(figures)
1727     fig_packet_streaming_multiprocessor(config, figures)
1728     fig_mixed_representation_space(figures)
1729     fig_pmr_gemm_planner(figures)
1730     fig_shared_packet_ir(figures)
1731     fig_gain_decomposition(figures)
1732     fig_output_contracts(figures)
1733     fig_cuda_measured_baselines(figures)
1734 
1735     tests = run_self_tests()
1736     result = {
1737         "config": asdict(config),
1738         "derived": {
1739             "packet_streaming_multiprocessors": config.psms,
1740             "execution_groups": config.execution_groups,
1741             "tiles": config.tiles,
1742             "packet_streaming_multiprocessors_per_chiplet": config.packet_streaming_multiprocessors_per_chiplet,
1743             "execution_groups_per_psm": config.execution_groups_per_psm,
1744             "fp16_tflops": config.fp16_tflops,
1745             "fp32_tflops": config.fp32_tflops,
1746             "fp64_tflops": config.fp64_tflops,
1747             "total_hbm_gib": config.total_hbm_gib,
1748             "total_hbm_tbps": config.total_hbm_tbps,
1749             "total_sram_mib": config.total_sram_mib,
1750         },
1751         "estimates": [asdict(e) for e in estimates],
1752         "modeled_speedups": {
1753             p[1].workload: p[0].total_time_us / p[1].total_time_us for p in pairs
1754         },
1755         "self_tests": tests,
1756         "bdlr_sensitivity": bdlr_sensitivity_tables(config),
1757         "measured_cuda_baselines": measured_cuda_baselines(),
1758         "gain_decomposition": gain_decomposition(),
1759         "representation_forms": list(REPRESENTATION_FORMS),
1760         "output_contracts": list(OUTPUT_CONTRACTS),
1761         "bandwidth_budget_tbps": {
1762             "host": config.host_bidirectional_gbps / 1000.0,
1763             "hbm": config.total_hbm_tbps,
1764             "package": config.package_network_tbps,
1765         },
1766         "figure_pairs": 43,
1767     }
1768     (results_dir / "prism_chip_results.json").write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
1769     return result
1770 
1771 
1772 def run_self_tests() -> dict[str, float | int | bool]:
1773     rng = np.random.default_rng(20260730)
1774 
1775     x = rng.normal(size=64)
1776     packets = [residue_project(x, 4, r) for r in range(4)]
1777     packet_residual = float(np.max(np.abs(sum(packets, np.zeros_like(x)) - x)))
1778 
1779     channels = polyphase_analyze(x, 4)
1780     polyphase_residual = float(np.max(np.abs(polyphase_synthesize(channels, length=x.size) - x)))
1781 
1782     low, high = haar_analyze(x)
1783     haar_residual = float(np.max(np.abs(haar_synthesize(low, high) - x)))
1784 
1785     blocks = [rng.normal(size=(8, 8)) for _ in range(8)]
1786     u = rng.normal(size=(64, 4))
1787     v = rng.normal(size=(64, 4))
1788     y_structured = bdlr_apply(blocks, u, v, x)
1789     y_dense = materialize_bdlr(blocks, u, v) @ x
1790     bdlr_residual = float(np.max(np.abs(y_structured - y_dense)))
1791 
1792     nodes = {"A", "B", "v", "C", "D"}
1793     edges = {("A", "B"), ("B", "v"), ("v", "C"), ("C", "D")}
1794     local_nodes = {"v0", "v1", "v2"}
1795     local_edges = {("v0", "v1"), ("v1", "v2")}
1796     new_nodes, new_edges, order = substitute_task(nodes, edges, "v", local_nodes, local_edges, {"v0"}, {"v2"})
1797     schedule_ok = set(order) == new_nodes and all(order.index(a) < order.index(b) for a, b in new_edges)
1798 
1799     config = PrismConfig()
1800     derived_ok = (
1801         config.psms == 64
1802         and config.execution_groups == 256
1803         and config.tiles == 256
1804         and abs(config.total_hbm_tbps - 11.2) < 1e-12
1805         and abs(config.total_sram_mib - 192.0) < 1e-12
1806     )
1807 
1808     descriptor = PacketDescriptor(1, 0, 2, "residue", (64,), 0x1000, 4, "fp32", 1e-5)
1809     edge = TransferDescriptor(1, 1, 2, "bdlr", 64, 64, 4, "fp32", 0.3, 9)
1810     descriptor_ok = descriptor.packet_id == edge.src_packet and descriptor.bytes_per_element == 4
1811 
1812     sensitivities = np.array([0.2, 0.5, 1.0, 2.0])
1813     costs = np.array([1.0, 2.0, 1.0, 3.0])
1814     tau = 0.025
1815     eps = precision_allocation(sensitivities, costs, tau)
1816     precision_budget_ok = abs(float(np.dot(sensitivities, eps)) - tau) < 1e-14
1817 
1818     commit_ok = certificate_commit_allowed(True, True, 8e-6, 1e-5)
1819     commit_rejects = not certificate_commit_allowed(True, True, 2e-5, 1e-5)
1820 
1821     refinement_bound_ok = local_refinement_descriptor_bound(3, 2, 1, 1, 1, 1) == 7
1822 
1823     routing_ok = (
1824         route_workload("bdlr", 12.0, 8, 32) == "prism"
1825         and route_workload("dense", 1.0, 1, 1) == "dense_path"
1826         and route_workload("transform", 1.1, 1, 2) == "host_or_general_device"
1827     )
1828 
1829     table = bdlr_sensitivity_tables(config)
1830     sensitivity_ok = (
1831         len(table["block_sizes"]) == 5
1832         and len(table["ranks"]) == 5
1833         and np.all(np.asarray(table["modeled_speedups"]) > 1.0)
1834         and np.all(np.asarray(table["storage_reduction"]) > 1.0)
1835     )
1836 
1837     bandwidth_ok = (
1838         config.package_network_tbps > config.total_hbm_tbps
1839         and config.total_hbm_tbps > config.host_bidirectional_gbps / 1000.0
1840     )
1841 
1842     options = {
1843         "root": (
1844             PlanChoice("dense", "explicit_dense", 100.0),
1845             PlanChoice("transform", "transform_domain", 8.0),
1846             PlanChoice("split", "packet_graph", 1.0, children=("left", "right")),
1847         ),
1848         "left": (
1849             PlanChoice("left_block", "block_diagonal", 2.0),
1850             PlanChoice("left_dense", "explicit_dense", 20.0),
1851         ),
1852         "right": (
1853             PlanChoice("right_low_rank", "low_rank", 2.5, local_error=0.01),
1854             PlanChoice("right_dense", "explicit_dense", 18.0),
1855         ),
1856     }
1857     compressed_plan = pmr_plan("root", options, error_weight=10.0, output_contract="factor_graph")
1858     explicit_plan = pmr_plan("root", options, error_weight=10.0, output_contract="explicit_array")
1859     pmr_optimal_ok = compressed_plan.choice == "split" and explicit_plan.choice == "dense"
1860 
1861     contract_ok = (
1862         output_contract_allows("bdlr", "apply")
1863         and output_contract_allows("bdlr", "factor_graph")
1864         and not output_contract_allows("bdlr", "explicit_array")
1865         and output_contract_allows("explicit_dense", "explicit_array")
1866     )
1867 
1868     cuda = measured_cuda_baselines()
1869     cuda_rows_ok = (
1870         abs(float(cuda["bdlr_graph_replay_gain"]) - (0.097280 / 0.079872)) < 1e-12
1871         and float(cuda["bdlr_speedup_vs_gpu_dense"]) > 1.0
1872         and float(cuda["compressed_output_gain"]) > 1.0
1873     )
1874 
1875     decomposition = gain_decomposition()
1876     gain_decomposition_ok = (
1877         float(decomposition["representation_reduction_bdlr"]) == 102.4
1878         and float(decomposition["measured_packet_gpu_gain_vs_dense_gpu"]) > 1.0
1879     )
1880 
1881     representation_registry_ok = (
1882         len(REPRESENTATION_FORMS) == 9
1883         and len(OUTPUT_CONTRACTS) == 5
1884         and "explicit_dense" in REPRESENTATION_FORMS
1885         and "packet_graph" in REPRESENTATION_FORMS
1886     )
1887 
1888     result = {
1889         "packet_reconstruction_residual": packet_residual,
1890         "polyphase_residual": polyphase_residual,
1891         "haar_residual": haar_residual,
1892         "bdlr_residual": bdlr_residual,
1893         "local_schedule_valid": bool(schedule_ok),
1894         "derived_configuration_valid": bool(derived_ok),
1895         "descriptor_contract_valid": bool(descriptor_ok),
1896         "precision_budget_valid": bool(precision_budget_ok),
1897         "commit_protocol_valid": bool(commit_ok and commit_rejects),
1898         "local_refinement_bound_valid": bool(refinement_bound_ok),
1899         "routing_policy_valid": bool(routing_ok),
1900         "sensitivity_tables_valid": bool(sensitivity_ok),
1901         "bandwidth_hierarchy_valid": bool(bandwidth_ok),
1902         "pmr_planner_valid": bool(pmr_optimal_ok),
1903         "output_contracts_valid": bool(contract_ok),
1904         "cuda_baseline_registry_valid": bool(cuda_rows_ok),
1905         "gain_decomposition_valid": bool(gain_decomposition_ok),
1906         "representation_registry_valid": bool(representation_registry_ok),
1907         "tests_passed": int(packet_residual < 1e-12)
1908         + int(polyphase_residual < 1e-12)
1909         + int(haar_residual < 1e-12)
1910         + int(bdlr_residual < 1e-11)
1911         + int(schedule_ok)
1912         + int(derived_ok)
1913         + int(descriptor_ok)
1914         + int(precision_budget_ok)
1915         + int(commit_ok and commit_rejects)
1916         + int(refinement_bound_ok)
1917         + int(routing_ok)
1918         + int(sensitivity_ok)
1919         + int(bandwidth_ok)
1920         + int(pmr_optimal_ok)
1921         + int(contract_ok)
1922         + int(cuda_rows_ok)
1923         + int(gain_decomposition_ok)
1924         + int(representation_registry_ok),
1925         "tests_total": 18,
1926     }
1927     if result["tests_passed"] != result["tests_total"]:
1928         raise RuntimeError(f"self tests failed: {result}")
1929     return result
1930 
1931 
1932 def main() -> None:
1933     parser = argparse.ArgumentParser(description="Generate the Komorebi PRISM-1 architecture model and figures")
1934     parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent)
1935     args = parser.parse_args()
1936     result = generate_all(args.root)
1937     print(json.dumps({"status": "ok", "tests": result["self_tests"], "figure_pairs": result["figure_pairs"]}, indent=2))
1938 
1939 
1940 if __name__ == "__main__":
1941     main()

Back to source controls

Other PDFs for this work

Original file and source record

This copy matches the recorded source file. Its publication here does not change the work’s review status.

SHA-256: ca9b474adeb980e95d97947371883c3745167aea7a97d52a45e1109959afee57

Original source on GitHub

Back to PRISM-1