From a963009855798d4b1197fe4763e238e08921bd0a Mon Sep 17 00:00:00 2001 From: ilgeco Date: Thu, 6 Aug 2026 14:32:11 +0200 Subject: [PATCH] Rust wait and sync --- .../src/lib/binary_to_instruction/mod.rs | 4 + .../src/lib/json_to_instruction/json_isa.rs | 11 +- .../pim/pim-simulator/src/lib/pimcore.rs | 40 +++- .../pim/pim-simulator/src/lib/send_recv.rs | 4 +- .../pim/pim-simulator/tests/sync.rs | 65 +++++++ .../raptor_graph_explorer.egg-info/PKG-INFO | 10 +- .../SOURCES.txt | 1 + validation/.gitignore | 2 + .../googlenet/googlenet-12.onnx | Bin 28021836 -> 28021885 bytes .../networks/pimcomp_models/results.csv | 6 +- .../pimcomp_models/validation_results.csv | 6 + validation/tools/analyze_yolo11n_attention.py | 182 ++++++++++++++++++ 12 files changed, 311 insertions(+), 20 deletions(-) create mode 100644 validation/networks/pimcomp_models/validation_results.csv create mode 100644 validation/tools/analyze_yolo11n_attention.py diff --git a/backend-simulators/pim/pim-simulator/src/lib/binary_to_instruction/mod.rs b/backend-simulators/pim/pim-simulator/src/lib/binary_to_instruction/mod.rs index ce14896..61e0edd 100644 --- a/backend-simulators/pim/pim-simulator/src/lib/binary_to_instruction/mod.rs +++ b/backend-simulators/pim/pim-simulator/src/lib/binary_to_instruction/mod.rs @@ -326,9 +326,13 @@ fn append_record( inst_builder.make_inst(recv, inst_data_builder.build()); } 31 => { + inst_data_builder.set_offset_select_value(generic1, generic2); inst_builder.make_inst(wait, inst_data_builder.build()); } 32 => { + inst_data_builder + .set_imm_core(r2_or_imm + 1) + .set_offset_select_value(generic1, 0); inst_builder.make_inst(sync, inst_data_builder.build()); } _ => bail!("unsupported PIM binary opcode {opcode}"), diff --git a/backend-simulators/pim/pim-simulator/src/lib/json_to_instruction/json_isa.rs b/backend-simulators/pim/pim-simulator/src/lib/json_to_instruction/json_isa.rs index a1f94f5..e2391c5 100644 --- a/backend-simulators/pim/pim-simulator/src/lib/json_to_instruction/json_isa.rs +++ b/backend-simulators/pim/pim-simulator/src/lib/json_to_instruction/json_isa.rs @@ -601,7 +601,11 @@ fn json_to_wait( inst_data_builder: &mut InstructionDataBuilder, json: &Value, ) -> Result<()> { - todo!("Not present in the compiler"); + inst_data_builder.set_offset_select_value( + json_i64!(json, "event_register") as i32, + json_i64!(json, "wait_value") as i32, + ); + inst_builder.make_inst(wait, inst_data_builder.build()); Ok(()) } @@ -610,7 +614,10 @@ fn json_to_sync( inst_data_builder: &mut InstructionDataBuilder, json: &Value, ) -> Result<()> { - todo!("Not present in the compiler"); + inst_data_builder + .set_imm_core(json_i64!(json, "core") as i32 + 1) + .set_offset_select_value(json_i64!(json, "event_register") as i32, 0); + inst_builder.make_inst(sync, inst_data_builder.build()); Ok(()) } diff --git a/backend-simulators/pim/pim-simulator/src/lib/pimcore.rs b/backend-simulators/pim/pim-simulator/src/lib/pimcore.rs index 487b65e..744c6f2 100644 --- a/backend-simulators/pim/pim-simulator/src/lib/pimcore.rs +++ b/backend-simulators/pim/pim-simulator/src/lib/pimcore.rs @@ -93,6 +93,8 @@ struct DeadlockInfo { states: String, } +type SyncEvents = Vec<[i32; 32]>; + fn print_status(core_instructions: &[CoreInstructions]) { let mut tot_instructions = 0; let mut progress = 0; @@ -135,6 +137,7 @@ impl<'a> Executable<'a> { } = self; let mut cpu_progressed = 0; let max_core = cpu.num_core(); + let mut sync_events: SyncEvents = vec![[0; 32]; max_core]; let mut cpu_index = 0; let mut now = SystemTime::now(); @@ -169,7 +172,9 @@ impl<'a> Executable<'a> { now = SystemTime::now(); } } - handle_wait_sync(cpu, cores_instructions, core_result); + if handle_wait_sync(cores_instructions, &mut sync_events, core_result) { + cpu_progressed = 0; + } match handle_send_recv(cpu, cores_instructions, send_recv, core_result) { (true, other_cpu_index) => { cpu_progressed = 0; @@ -349,12 +354,31 @@ fn detect_deadlock(cores_instructions: &[CoreInstructions]) -> Option( - cpu: &'b mut CPU<'a>, - core_instructions: &'c mut [CoreInstructions], +fn handle_wait_sync( + core_instructions: &mut [CoreInstructions], + events: &mut SyncEvents, core_result: InstructionStatus, -) where - 'a: 'b, - 'a: 'c, -{ +) -> bool { + match core_result { + InstructionStatus::Sync(data) => { + let (source, target) = data.get_core_immcore(); + let register = data.offset_select() as usize; + events[target as usize][register] += 1; + core_instructions[source as usize].program_counter += 1; + true + } + InstructionStatus::Waiting(data) => { + let core = data.core_indx() as usize; + let register = data.offset_select() as usize; + let value = data.offset_value(); + if events[core][register] >= value { + events[core][register] -= value; + core_instructions[core].program_counter += 1; + true + } else { + false + } + } + _ => false, + } } diff --git a/backend-simulators/pim/pim-simulator/src/lib/send_recv.rs b/backend-simulators/pim/pim-simulator/src/lib/send_recv.rs index 258ac2a..2fdad1c 100644 --- a/backend-simulators/pim/pim-simulator/src/lib/send_recv.rs +++ b/backend-simulators/pim/pim-simulator/src/lib/send_recv.rs @@ -134,7 +134,7 @@ where send_recv.sending[sender] = None; send_recv.receiving[receiver] = None; } - (transfered, receiver) + (transfered, if transfered { receiver } else { 0 }) } InstructionStatus::Reciving(instruction_data) => { let (core_idx, imm_core) = instruction_data.get_core_immcore(); @@ -163,7 +163,7 @@ where send_recv.sending[sender] = None; send_recv.receiving[receiver] = None; } - (transfered, sender) + (transfered, if transfered { sender } else { 0 }) } _ => (false, 0), } diff --git a/backend-simulators/pim/pim-simulator/tests/sync.rs b/backend-simulators/pim/pim-simulator/tests/sync.rs index 66094c6..e638e70 100644 --- a/backend-simulators/pim/pim-simulator/tests/sync.rs +++ b/backend-simulators/pim/pim-simulator/tests/sync.rs @@ -295,3 +295,68 @@ fn multiple_send_recv_test() { "send_recv failed to store" ); } + +#[test] +fn sync_wait_tokens_test() { + let cpu = common::empty_cpu(2); + let mut cores = CoreInstructionsBuilder::new(2); + let mut instructions = InstructionsBuilder::new(); + let mut data = InstructionDataBuilder::new(); + + data.set_core_indx(1).fix_core_indx(); + for _ in 0..2 { + instructions.make_inst( + sync, + data.set_imm_core(2).set_offset_select_value(0, 0).build(), + ); + } + cores.set_core(1, instructions.build()); + + data.set_core_indx(2).fix_core_indx(); + for _ in 0..2 { + instructions.make_inst(wait, data.set_offset_select_value(0, 1).build()); + } + cores.set_core(2, instructions.build()); + + Executable::new(cpu, cores.build()).execute().unwrap(); +} + +#[test] +fn blocked_transfers_do_not_starve_sync_producer() { + let cpu = common::empty_cpu(4); + let mut cores = CoreInstructionsBuilder::new(4); + + let mut instructions = InstructionsBuilder::new(); + let mut data = InstructionDataBuilder::new(); + data.set_core_indx(1).fix_core_indx(); + instructions.make_inst(sldi, data.set_rdimm(1, 0).build()); + instructions.make_inst(recv, data.set_rd(1).set_imm_core(2).set_imm_len(1).build()); + instructions.make_inst(send, data.set_r1(1).set_imm_core(3).set_imm_len(1).build()); + cores.set_core(1, instructions.build()); + + let mut instructions = InstructionsBuilder::new(); + let mut data = InstructionDataBuilder::new(); + data.set_core_indx(2).fix_core_indx(); + instructions.make_inst(sldi, data.set_rdimm(1, 0).build()); + instructions.make_inst(wait, data.set_offset_select_value(0, 1).build()); + instructions.make_inst(send, data.set_r1(1).set_imm_core(1).set_imm_len(1).build()); + cores.set_core(2, instructions.build()); + + let mut instructions = InstructionsBuilder::new(); + let mut data = InstructionDataBuilder::new(); + data.set_core_indx(3).fix_core_indx(); + instructions.make_inst(sldi, data.set_rdimm(1, 0).build()); + instructions.make_inst(recv, data.set_rd(1).set_imm_core(1).set_imm_len(1).build()); + cores.set_core(3, instructions.build()); + + let mut instructions = InstructionsBuilder::new(); + let mut data = InstructionDataBuilder::new(); + data.set_core_indx(4).fix_core_indx(); + instructions.make_inst( + sync, + data.set_imm_core(2).set_offset_select_value(0, 0).build(), + ); + cores.set_core(4, instructions.build()); + + Executable::new(cpu, cores.build()).execute().unwrap(); +} diff --git a/tools/raptor_graph_explorer/raptor_graph_explorer.egg-info/PKG-INFO b/tools/raptor_graph_explorer/raptor_graph_explorer.egg-info/PKG-INFO index 56c6495..327fff3 100644 --- a/tools/raptor_graph_explorer/raptor_graph_explorer.egg-info/PKG-INFO +++ b/tools/raptor_graph_explorer/raptor_graph_explorer.egg-info/PKG-INFO @@ -169,11 +169,11 @@ Motifs are not inferred from rendered geometry. For each operation graph the too ## Viewer and API -Except for spatial4, the viewer initially fetches an aggregate operation graph unless the browser has a saved view choice. Sigma renders the graph with WebGL. Controls cover report/view/metric selection, text and tensor search, mapping filters, self edges, relayout, fitting, and motif selection. Raw nodes and edges have their own selection details and never call aggregate-only detail or mapping endpoints. Mapping panels remain available for operation aggregate edges. +The viewer initially fetches an aggregate operation graph, so a previously selected raw view cannot make a new report exceed the display safety cap before the first render. Browser responses disable caching so HTML and JavaScript from different tool revisions cannot be mixed. Sigma renders the graph with WebGL. Controls cover report/view selection, text and tensor search, mapping filters, self edges, relayout, fitting, and motif selection. Edges use a fixed width. Raw nodes and edges have their own selection details and never call aggregate-only detail or mapping endpoints. Nodes without an SSA name fall back to their source identity. Mapping panels remain available for operation aggregate edges. Operation expansion is a deterministic projection of the source graph. Expand selected, Expand all operations, Collapse selected operation, and Collapse all rebuild the complete display from the current expanded-operation set. An expanded operation's raw nodes replace its aggregate node, including isolated raw nodes. An aggregate edge is retained only when both endpoint operations are collapsed; otherwise its raw edges replace it with endpoints calculated from the complete expansion set. Expansion order therefore cannot leave stale endpoints or simultaneous aggregate/raw representations. -Operation ranks are calculated from the complete operation graph, including isolated operations. Collapsed nodes use stable operation anchors. Expanded nodes are sorted by lane and node ID; lane numbers form perpendicular rows, equal lanes align across adjacent operations, and lane-less nodes use distinct deterministic scalar rows. This same model is used by Rerun layout, so unrelated operations do not jump during expansion or collapse and disconnected nodes never pile up at `(0, 0)`. +Operation ranks are calculated from the complete operation graph, including isolated operations, and every view flows top-to-bottom. A collapsed operation reserves one displayed row. Expanded nodes use compact rows for distinct lanes in numeric order, followed by deterministic lane-less rows, so sparse lane numbers create no empty geometric space. Raw nodes sharing a lane receive centered deterministic horizontal offsets. Operation and raw views use stable server coordinates, and `Reset layout` restores them; core and node-kind views use ELK, and `Rerun layout` runs it again. Operation labels use the first SSA name; Sigma's collision grid thins normal labels, and a label is skipped when its measured screen rectangle intersects another visible node. Selected or hovered nodes keep the original white bubble with black text. Selecting a motif frames its member nodes on the first click. Aggregate-node sizes remain bounded. Browser dependencies are pinned in one place, `static/index.html`: @@ -206,8 +206,10 @@ Raw pages default to 100 and cannot exceed 500. Subgraph depth cannot exceed fiv ## Performance behavior - CSV readers stream rows and insert them in bounded batches. -- Temporary SQLite staging and bulk joins resolve endpoints; ingestion performs no per-edge node query. -- Secondary indexes are created after raw insertion. +- Temporary SQLite staging tables are function-scoped and dropped immediately; bulk joins resolve endpoints without per-edge node queries. +- CSVs without additional columns use a constant empty JSON representation. +- Ingestion closes its write connection before derived indexes, aggregation, motifs, and diagnostics reopen the database. +- Secondary indexes are created after raw insertion and focus on serving and aggregation queries. - Raw edges are never retained as a Python object graph or loaded into NetworkX. - Only aggregate operation nodes/edges enter NetworkX. - Mapping statistics are grouped in SQL; exact stencil comparison streams one operation pair. diff --git a/tools/raptor_graph_explorer/raptor_graph_explorer.egg-info/SOURCES.txt b/tools/raptor_graph_explorer/raptor_graph_explorer.egg-info/SOURCES.txt index 6a41281..a614400 100644 --- a/tools/raptor_graph_explorer/raptor_graph_explorer.egg-info/SOURCES.txt +++ b/tools/raptor_graph_explorer/raptor_graph_explorer.egg-info/SOURCES.txt @@ -1,6 +1,7 @@ README.md pyproject.toml raptor_graph_explorer/__init__.py +raptor_graph_explorer/__main__.py raptor_graph_explorer/aggregate.py raptor_graph_explorer/api.py raptor_graph_explorer/cli.py diff --git a/validation/.gitignore b/validation/.gitignore index 0d62ec7..f24a90f 100644 --- a/validation/.gitignore +++ b/validation/.gitignore @@ -20,3 +20,5 @@ networks/**/*.csv !networks/full_net/validation_results.csv !networks/pimcomp_models/validation_results.csv !networks/pimcomp_models/results.csv +!networks/pimcomp_models/validation_results.csv +!operations/validation_results.csv diff --git a/validation/networks/pimcomp_models/googlenet/googlenet-12.onnx b/validation/networks/pimcomp_models/googlenet/googlenet-12.onnx index 1865acc0a19390cd1f562b32f4f87751b873fdae..97a4db3480c969c153e3549f588b2e1860852633 100644 GIT binary patch delta 1526 zcmWm6XMhj_07r30D0EvOGb?v*`e!4vylSXQFf9FQc)^NWhs;@QY2NS znpBq>Qd4S4lG;*7>PkJSFAb!jG?JZV7ila_q^UHM=F&oTmEB}_X(_E_57|>%%U;q( z_LjD?kF=A0rM+~Jjss~ji?$-&Z1x=Rn~DZQk(^pQj4P&rHvmm}mz zIZBR}zS2+n%Q14S43L2`NCwMsa=Z+Y6XZlWNluobGE9o)6e*EYWw?xxkupj~%NRLL z#>zMuFB7Cx%4DLPE@w!&oGFuJvP_Ywa+XY!>2kK5BQs>C%#ztMN6wYGqRf-?F1cIok$YvU+$Zl#k?N z`9waI&*XFYLcWx*=-*mg{T;nqH+{Ql_-j;Q7x)Rji?#5;{QkOs1tRgUeu2U(J>&aq20jwaDGnnm+y z5xd52v3saD9 zjdig;E{`kX%D5`7jty~5Y>Z8@xnL_>3OX%sI<{R=-QtO5W#c=w>oT=Vae4dV2_@y@ cird$2P*kt5%DM-aZ7HZzsaJ)f9coqk4=vWv8UO$Q delta 1489 zcmWm6XTS&q00!YcgzU4Eor;9)k+MP=84+ z=jYwuJFhRjFDPEDe9383r_O9NX4LrcaP(ni`!J2^_)%h7U-bdZkHNjggxIaaz#H|Z|N$??)ddP*

g%2FAvCr@{l|%kI19)m^>~| z$dmGv?31VE8F^Noljr3Hc~M@Hm*o|CRbG?VzzLKxy8~IkglkepR`B8q7pXC?%ReqD-5onOq7js@&BWIREUaEDJn;ms2T@GwKyoMM~$c%wW4;^iMnxc z)Qdyn&^RpWM}spaXPC>lqTI5L_>vuGYIqGhy-*3l-~M!PsF+Q-pxOmv8j(J4Ad zmpC@MMz`o5$Hno{BYH-!=p83SpXeL?qJNwi17cvD6oX=L42hFtXbg+tF(O9BDKRQW z$Cwx!<6?YFh>0;NPL0ViB~FW}F)dDy=`kbDh%@7?I6KaXnQ?BM7w5+Xaba8(7ssra z9dlxC%nM_FEQp1%C@zV`u_TtpvRED~;?lS*E{~OQMO+!HVs)&EwQ*Ifi}kS~HpZsd j99!b**c#hnd+dmvaZT(h*vsyMmMe>PE>dE@a;5$Qj6=l; diff --git a/validation/networks/pimcomp_models/results.csv b/validation/networks/pimcomp_models/results.csv index 49345f5..69c4816 100644 --- a/validation/networks/pimcomp_models/results.csv +++ b/validation/networks/pimcomp_models/results.csv @@ -1,5 +1,3 @@ model,raptor_latency_ms,pimcomp_latency_ms,raptor_energy_pj,pimcomp_energy_pj,faster_compiler,speedup -vgg8,1.465778,7.985074,477298145.040001,1597904071.120000,raptor,5.45 -resnet18,28.099952,58.853733,8781611766.119984,13983168748.119974,raptor,2.09 -resnet34,45.781486,91.607980,14962940227.679951,22722922369.680016,raptor,2.00 -googlenet,13.371204,62.923463,6117835798.919991,14547526780.240000,raptor,4.71 +vgg8,1.521060,7.985074,486309111.040001,1597904071.120000,raptor,5.25 +resnet18,33.552733,58.853613,9702508727.119982,13983148468.119974,raptor,1.75 diff --git a/validation/networks/pimcomp_models/validation_results.csv b/validation/networks/pimcomp_models/validation_results.csv new file mode 100644 index 0000000..4549cd5 --- /dev/null +++ b/validation/networks/pimcomp_models/validation_results.csv @@ -0,0 +1,6 @@ +Operation,Result,Compile,Host mem,Cores mem,Cores,Xbars,Latency,Power,Energy +vgg8-mnist-reconstructed,PASS,1.009 s,1.37 MiB,3.14 MiB,141,761,1.465778 ms,325.627854 mW,477298145.040001 pJ +resnet18-v1-7,PASS,11.548 s,9.89 MiB,40.24 MiB,168,7676,28.099952 ms,312.513408 mW,8781611766.119984 pJ +resnet34-v1-7,PASS,28.495 s,9.90 MiB,48.89 MiB,168,15292,45.781486 ms,326.833870 mW,14962940227.679951 pJ +googlenet-12-latency,PASS,6.573 s,10.74 MiB,22.41 MiB,168,7176,13.371204 ms,457.538139 mW,6117835798.919991 pJ +yolo11n-latency,FAIL,58.572 s,82.55 MiB,185.68 MiB,168,6484,885.264931 ms,189.218985 mW,167508931321.001465 pJ diff --git a/validation/tools/analyze_yolo11n_attention.py b/validation/tools/analyze_yolo11n_attention.py new file mode 100644 index 0000000..8453e4f --- /dev/null +++ b/validation/tools/analyze_yolo11n_attention.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Save exact attention-tap arrays and quantify the MatMul error sources.""" + +import argparse +import hashlib +import json +import sys +from pathlib import Path + +import numpy as np +import onnx +from onnx import numpy_helper + +REPO_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(REPO_ROOT / "validation")) + +from raptor_validation.onnx_utils import onnx_io # noqa: E402 +from raptor_validation.validate_one import ( # noqa: E402 + parse_pim_simulator_outputs, + sanitize_output_name, +) + + +TAP_NAMES = { + "v": "/model.10/m/m.0/attn/Split_output_2", + "raw": "/model.10/m/m.0/attn/MatMul_output_0", + "scaled": "/model.10/m/m.0/attn/Mul_output_0", + "rhs": "/model.10/m/m.0/attn/Transpose_1_output_0", + "c": "/model.10/m/m.0/attn/MatMul_1_output_0", +} +ABSOLUTE_TOLERANCE = 1e-3 +RELATIVE_TOLERANCE = 1e-5 + + +def sha256(path): + digest = hashlib.sha256() + with Path(path).open("rb") as stream: + for block in iter(lambda: stream.read(1 << 20), b""): + digest.update(block) + return digest.hexdigest() + + +def metric(actual, expected): + difference = np.abs(actual.astype(np.float64) - expected.astype(np.float64)) + allowed = ABSOLUTE_TOLERANCE + RELATIVE_TOLERANCE * np.abs(expected.astype(np.float64)) + return { + "max_abs": float(np.max(difference)), + "mean_abs": float(np.mean(difference)), + "rms": float(np.sqrt(np.mean(np.square(difference)))), + "elements_over_validator_limit": int(np.count_nonzero(difference > allowed)), + } + + +def f32_matmul(lhs, rhs): + return np.matmul(lhs.astype(np.float32), rhs.astype(np.float32)).astype(np.float32) + + +def f64_matmul(lhs, rhs): + return np.matmul(lhs.astype(np.float64), rhs.astype(np.float64)).astype(np.float64) + + +def load_constant(model, output_name): + for initializer in model.graph.initializer: + if initializer.name == output_name: + return float(numpy_helper.to_array(initializer).reshape(-1)[0]) + for node in model.graph.node: + if output_name not in node.output: + continue + for attribute in node.attribute: + if attribute.name == "value" and attribute.HasField("t"): + return float(numpy_helper.to_array(attribute.t).reshape(-1)[0]) + raise ValueError(f"could not find ONNX Constant producing {output_name}") + + +def load_arrays(workspace, model_path): + model = onnx.load(model_path) + descriptors = onnx_io(model_path) + output_descriptors = {name: (index, dtype, shape) for index, name, dtype, shape in descriptors[1]} + missing = sorted(set(TAP_NAMES.values()) - set(output_descriptors)) + if missing: + raise ValueError("tap model is missing outputs: " + ", ".join(missing)) + + sim_arrays = parse_pim_simulator_outputs( + workspace / "simulation" / "out.bin", descriptors[1] + ) + reference = {} + simulated = {} + input_files = {} + for key, name in TAP_NAMES.items(): + index, _dtype, shape = output_descriptors[name] + csv_path = workspace / "outputs" / f"output{index}_{sanitize_output_name(name)}.csv" + reference[key] = np.loadtxt(csv_path, delimiter=",", dtype=np.float32).reshape(shape) + simulated[key] = np.asarray(sim_arrays[index], dtype=np.float32).reshape(shape) + input_files[key] = csv_path + return reference, simulated, input_files + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--workspace", type=Path, required=True, + help="validator workspace containing inputs, outputs, and simulation") + parser.add_argument("--model", type=Path, required=True, help="five-output ONNX tap model") + parser.add_argument("--output-dir", type=Path, required=True, + help="directory for arrays.npz, metadata.json, and decomposition.json") + args = parser.parse_args() + args.output_dir.mkdir(parents=True, exist_ok=True) + + model = onnx.load(args.model) + reference, simulated, source_files = load_arrays(args.workspace, args.model) + scale = np.float32(load_constant(model, "/model.10/m/m.0/attn/Constant_1_output_0")) + ref_v, ref_raw, ref_scaled, ref_rhs, ref_c = (reference[key] for key in ("v", "raw", "scaled", "rhs", "c")) + sim_v, sim_raw, sim_scaled, sim_rhs, sim_c = (simulated[key] for key in ("v", "raw", "scaled", "rhs", "c")) + + ref_score_transpose = np.swapaxes(ref_scaled, -1, -2) + sim_score_transpose = np.swapaxes(sim_scaled, -1, -2) + ref_ss_f32 = f32_matmul(ref_v, ref_rhs) + sim_ss_f32 = f32_matmul(sim_v, sim_rhs) + ref_ss_f64 = f64_matmul(ref_v, ref_rhs) + sim_ss_f64 = f64_matmul(sim_v, sim_rhs) + ref_split_f32 = (f32_matmul(ref_v, np.swapaxes(ref_raw, -1, -2)) * scale).astype(np.float32) + sim_split_f32 = (f32_matmul(sim_v, np.swapaxes(sim_raw, -1, -2)) * scale).astype(np.float32) + ref_split_f64 = f64_matmul(ref_v, np.swapaxes(ref_raw, -1, -2)) * np.float64(scale) + sim_split_f64 = f64_matmul(sim_v, np.swapaxes(sim_raw, -1, -2)) * np.float64(scale) + + arrays = { + **{f"ref_{key}": value for key, value in reference.items()}, + **{f"sim_{key}": value for key, value in simulated.items()}, + "ref_ss_f32": ref_ss_f32, + "sim_ss_f32": sim_ss_f32, + "ref_ss_f64": ref_ss_f64, + "sim_ss_f64": sim_ss_f64, + "ref_split_f32": ref_split_f32, + "sim_split_f32": sim_split_f32, + "ref_split_f64": ref_split_f64, + "sim_split_f64": sim_split_f64, + } + arrays_path = args.output_dir / "arrays.npz" + np.savez_compressed(arrays_path, **arrays) + + metrics = { + "validator_policy": { + "absolute_tolerance": ABSOLUTE_TOLERANCE, + "relative_tolerance": RELATIVE_TOLERANCE, + }, + "scale": float(scale), + "shape": list(ref_c.shape), + "tap_differences": {key: metric(simulated[key], reference[key]) for key in TAP_NAMES}, + "rhs_transpose_consistency": metric(ref_rhs, ref_score_transpose), + "sim_rhs_transpose_consistency": metric(sim_rhs, sim_score_transpose), + "c_sim_vs_ss_f32": metric(sim_c, ref_ss_f32), + "c_ref_vs_ss_f32": metric(ref_c, ref_ss_f32), + "c_sim_vs_simulated_inputs_ss_f32": metric(sim_c, sim_ss_f32), + "v_drift_only": metric(f32_matmul(sim_v, ref_rhs), ref_ss_f32), + "rhs_drift_only": metric(f32_matmul(ref_v, sim_rhs), ref_ss_f32), + "joint_input_drift": metric(sim_ss_f32, ref_ss_f32), + "scale_reassociation_reference": metric(ref_split_f32, ref_ss_f32), + "scale_reassociation_simulated": metric(sim_split_f32, sim_ss_f32), + "reference_accumulation_f32_vs_f64": metric(ref_ss_f32, ref_ss_f64), + "simulated_accumulation_f32_vs_f64": metric(sim_ss_f32, sim_ss_f64), + "split_accumulation_reference_f32_vs_f64": metric(ref_split_f32, ref_split_f64), + "split_accumulation_simulated_f32_vs_f64": metric(sim_split_f32, sim_split_f64), + } + decomposition_path = args.output_dir / "decomposition.json" + decomposition_path.write_text(json.dumps(metrics, indent=2) + "\n", encoding="utf-8") + + metadata = { + "model": str(args.model), + "model_sha256": sha256(args.model), + "workspace": str(args.workspace), + "arrays_sha256": sha256(arrays_path), + "source_sha256": {key: sha256(path) for key, path in source_files.items()}, + "simulator_output_sha256": sha256(args.workspace / "simulation" / "out.bin"), + "input_sha256": sha256(args.workspace / "inputs" / "in0.csv"), + "outputs": TAP_NAMES, + "arrays": {key: {"dtype": str(value.dtype), "shape": list(value.shape)} for key, value in arrays.items()}, + } + (args.output_dir / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8") + print(json.dumps(metrics, indent=2)) + + +if __name__ == "__main__": + main()