4a1fe710b2
Validate Operations / validate-operations (push) Has been cancelled
increase throughput scheduler budget add experimental throughput auto mode
204 lines
12 KiB
Python
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()
|