Files
Raptor/test/PIM/test_PipelineDepth.py
T
NiccoloN 4a1fe710b2
Validate Operations / validate-operations (push) Has been cancelled
force selected pipeline stages in throughput mode
increase throughput scheduler budget
add experimental throughput auto mode
2026-11-02 12:08:33 +01:00

204 lines
12 KiB
Python

"""Strict stage identities and bounded automatic depth selection."""
import argparse
import hashlib
import json
import importlib.util
from pathlib import Path
import struct
import subprocess
import tempfile
import onnx
from onnx import TensorProto, helper
REPO = Path(__file__).resolve().parents[2]
def identity(folder):
return {str(p.relative_to(folder)): hashlib.sha256(p.read_bytes()).hexdigest()
for p in folder.rglob("*") if p.is_file()}
def validate_repeating(artifact, source, out, count):
spec = importlib.util.spec_from_file_location("publication", REPO / "test/PIM/test_HostPublication.py")
publication = importlib.util.module_from_spec(spec)
spec.loader.exec_module(publication)
from types import SimpleNamespace
options = SimpleNamespace(timeout=120, threshold=1e-3, rtol=1e-5)
samples, inputs, _, _ = publication.sync.make_inputs(source, count, 0, out)
references = publication.sync.make_references(source, samples, out, options)
_, outputs = publication.sync.compare.onnx_io(source)
streams = publication.read_events(artifact)
stalled = next((core, 0, event[0]) for core, events in streams.items()
for event in events if event[1] == 25)
for policy, seed in (("greedy", 0), ("randomized", 7), ("randomized", 11)):
publication.run_functional(artifact, inputs, references, outputs,
out / f"functional-{seed}", policy, seed, stalled if seed == 11 else None)
return streams
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--compiler", required=True)
args = parser.parse_args()
with tempfile.TemporaryDirectory(prefix="raptor-depth-") as temporary:
root = Path(temporary)
model = REPO / "validation/networks/pimcomp_models/vgg8/vgg8-mnist-reconstructed.onnx"
def compile_model(label, depth=None, source=model, extra=(), hardware=None):
out = root / label
out.mkdir()
cores, crossbars, config = hardware or (138, 128,
REPO / "validation/pimsim_configs/pimcomp/arch-b/throughput_config_1000ms.json")
command = [args.compiler, str(source), "-o", str(out / "model"),
"--maccel=PIM", "--EmitPimCodegen", f"--core-count={cores}", f"--crossbar-count={crossbars}",
"--crossbar-size=128", "--mlir-disable-threading", "--pim-detect-communication-deadlock",
*([f"--pim-target-config={config}"] if config is not None else []),
*([f"--pipeline={depth}"] if depth is not None else []), *extra]
result = subprocess.run(command, capture_output=True, text=True, timeout=120)
return out, result
latency, result = compile_model("default")
assert result.returncode == 0, result.stderr
explicit, result = compile_model("p1", 1)
assert result.returncode == 0, result.stderr
assert identity(latency / "pim") == identity(explicit / "pim")
assert not (explicit / "reports/pipeline_selection.json").exists()
for depth in (2, 4, 8):
out, result = compile_model(f"p{depth}", depth)
assert result.returncode == 0, result.stderr
report = json.loads((out / "reports/pipeline_selection.json").read_text())
assert report["requested_depth"] == report["selected_depth"] == depth
assert report["lowered_candidates"] == 1
for candidate in report["candidates"]:
assert candidate["active_stage_count"] == depth
assert {run[2] for run in candidate["task_stage_runs"]} == set(range(depth))
assert len(candidate["stage_core_budgets"]) == depth
assert all(candidate["stage_core_budgets"])
counts = {31: 0, 32: 0}
for path in (out / "pim").glob("core_*.pim"):
data = path.read_bytes()
magic, version, count = struct.unpack("<4sII", data[:12])
assert magic == b"PIMB" and version == 1 and len(data) == 12 + count * 20
for opcode in data[12::20]:
if opcode in counts:
counts[opcode] += 1
assert all(counts.values()), counts
previous = None
for repetition in range(2):
out, result = compile_model(f"auto-{repetition}", "auto")
assert result.returncode == 0, result.stderr
report = json.loads((out / "reports/pipeline_selection.json").read_text())
assert report["requested_depth"] == "auto"
assert 1 in report["explored_depths"]
assert report["queue_evaluation_budget"] == 64
assert report["costed_candidates"] <= report["queue_evaluation_budget"]
assert report["packing_attempts"] <= 4 * report["queue_evaluation_budget"]
assert report["beam_width"] == 2 and report["lowered_candidates"] == 1
assert all(c["eligible"] and c["convergence_proved"] for c in report["candidates"])
selected = report["candidates"][report["selection"]]
assert selected["active_stage_count"] == report["selected_depth"]
current = (report["selection"], report["selected_depth"],
[(c["interval_ns"], c["alternative_tie_interval_ns"]) for c in report["candidates"]],
identity(out / "pim"))
assert previous is None or current == previous
previous = current
# All dynamic-input consumers stay in stage zero. Auto p1 repeats the
# PEFT placement with the existing restart barrier, unlike latency p1.
weights = [helper.make_tensor(f"w{i}", TensorProto.FLOAT, [4, 4],
[float(i + 1) if r == c else 0. for r in range(4) for c in range(4)])
for i in range(2)]
fork = helper.make_model(helper.make_graph([
helper.make_node("MatMul", ["x", "w0"], ["y0"]),
helper.make_node("MatMul", ["x", "w1"], ["y1"])], "input-fork",
[helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 4])],
[helper.make_tensor_value_info(f"y{i}", TensorProto.FLOAT, [1, 4]) for i in range(2)],
initializer=weights), opset_imports=[helper.make_opsetid("", 18)])
fork.ir_version = 10
fork_path = root / "fork.onnx"
onnx.save(fork, fork_path)
out, result = compile_model("auto-p1", "auto", fork_path)
assert result.returncode == 0, result.stderr
report = json.loads((out / "reports/pipeline_selection.json").read_text())
assert report["selected_depth"] == 1 and report["explored_depths"] == [1]
assert report["candidates"][0]["eligible"] and report["costed_candidates"] == 1
opcodes = [opcode for path in (out / "pim").glob("core_*.pim")
for opcode in path.read_bytes()[12::20]]
assert 31 in opcodes and 32 in opcodes
validate_repeating(out / "pim", fork_path, out, 8)
_, result = compile_model("infeasible-fork", 2, fork_path)
assert result.returncode != 0 and "dependency-adjacent partition" in result.stderr
# Each weighted consumer also reads x, so every compute group stays in stage zero.
weights = [helper.make_tensor(f"w{i}", TensorProto.FLOAT, [128, 128],
[float(i + 1) if r == c else 0. for r in range(128) for c in range(128)])
for i in range(4)]
nodes = [helper.make_node("MatMul", ["x", "w0"], ["a"]),
helper.make_node("Gemm", ["a", "w1", "x"], ["b"]),
helper.make_node("Gemm", ["a", "w2", "x"], ["c"]),
helper.make_node("Gemm", ["b", "w3", "x"], ["tail0"])]
for i in range(24):
nodes.append(helper.make_node("Add", [f"tail{i}", "x"], [f"tail{i+1}"]))
tail = helper.make_model(helper.make_graph(nodes, "uneven-publications",
[helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 128])],
[helper.make_tensor_value_info(name, TensorProto.FLOAT, [1, 128])
for name in ("a", "b", "c", "tail24")], initializer=weights),
opset_imports=[helper.make_opsetid("", 18)])
tail.ir_version = 10
tail_path = root / "long-tail.onnx"
onnx.save(tail, tail_path)
profile = REPO / "validation/pimsim_configs/pimcomp/arch-b/throughput_config_1000ms.json"
config = json.loads(profile.read_text())
config["chip_config"]["core_cnt"] = 4
config["chip_config"]["core_config"]["matrix_config"]["xbar_array_count"] = 1
network = config["chip_config"]["network_config"]
network["layout"] = [1, 4]
network["net_config_file_path"] = str(profile.parent / network["net_config_file_path"])
config_path = root / "four-core.json"
config_path.write_text(json.dumps(config))
out, result = compile_model("auto-long-tail", "auto", tail_path, hardware=(4, 1, config_path))
assert result.returncode == 0, result.stdout + result.stderr
report = json.loads((out / "reports/pipeline_selection.json").read_text())
assert report["selected_depth"] == 1 and report["explored_depths"] == [1]
candidate = report["candidates"][0]
assert candidate["convergence_proved"]
queue = candidate["spatial_cost"]["memory_controller"]
assert queue["periodic_state"]["complete_scheduler_state"]
assert queue["alternative_periodic_state"]["complete_scheduler_state"]
assert "complete-state recurrence" in candidate["spatial_cost"]["interval_source"]
latency, result = compile_model("latency-long-tail", 1, tail_path, hardware=(4, 1, config_path))
assert result.returncode == 0, result.stdout + result.stderr
headers = lambda folder: [line for line in (folder / "reports/spatial3_scheduled_no_comm.txt")
.read_text().splitlines() if line.startswith("C")]
assert headers(out) == headers(latency)
for opcode in range(9, 31):
count = lambda folder: sum(path.read_bytes()[12::20].count(opcode)
for path in (folder / "pim").glob("core_*.pim"))
assert count(out) == count(latency), opcode
streams = validate_repeating(out / "pim", tail_path, out, 24)
peers = {core: {event[2] for event in events if event[1] == 29}
for core, events in streams.items()}
assert any(len(receivers) >= 2 for receivers in peers.values()), peers
assert sum(event[1] == 26 for events in streams.values() for event in events) >= 4
lengths = [len(path.read_bytes()[12:]) // 20 for path in (out / "pim").glob("core_*.pim")]
assert max(lengths) > 2 * min(lengths), lengths
graph = helper.make_graph([helper.make_node("Relu", ["x"], ["y"])], "one-group",
[helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 16])],
[helper.make_tensor_value_info("y", TensorProto.FLOAT, [1, 16])])
tiny = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 18)])
tiny.ir_version = 10
source = root / "one-group.onnx"
onnx.save(tiny, source)
_, result = compile_model("infeasible", 2, source)
assert result.returncode != 0 and "compute operation groups" in result.stderr
for value in (0, -1, "unknown"):
_, result = compile_model(f"invalid-{value}", value, source)
assert result.returncode != 0 and "positive integer|auto" in result.stderr
_, result = compile_model("cores", 139, source)
assert result.returncode != 0 and "not to exceed --core-count" in result.stderr
_, result = compile_model("intermediate-auto", "auto", source, ("--EmitSpatial",))
assert result.returncode != 0 and "requires full PIM code generation" in result.stderr
if __name__ == "__main__":
main()