Files
Raptor/validation/tests/test_experiment_scheduler.py
Niccolò Nicolosi 66db4b66da
Validate Operations / validate-operations (push) Has been cancelled
add paper artifact
2026-11-03 14:37:25 +01:00

188 lines
8.6 KiB
Python

from __future__ import annotations
import argparse
import json
from pathlib import Path
import sys
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/pim/pimcomp/compare"))
from raptor_validation import experiment_scheduler as scheduler
import run_pimcomp_models as suite
import compare_raptor_pimcomp_model as comparator
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/pim/ablation"))
import run_ablation as ablation
def task(root, key, code, dependencies=()):
command = [sys.executable, "-c", code]
return scheduler.ExperimentTask(key, lambda: command, dependencies, root / f"{key}.log")
def test_dependency_starts_before_unrelated_job_finishes(tmp_path):
# The slow job cannot finish until the fast job's dependent has run.
marker = tmp_path / "released"
slow = ("import pathlib,time; deadline=time.monotonic()+5; "
f"p=pathlib.Path({str(marker)!r}); "
"exec('while not p.exists() and time.monotonic()<deadline: time.sleep(.01)'); "
"assert p.exists(), 'dependent was blocked by an unrelated job'")
tasks = [task(tmp_path, "fast", "pass"), task(tmp_path, "slow", slow),
task(tmp_path, "dependent", f"from pathlib import Path; Path({str(marker)!r}).touch()", ("fast",))]
results = scheduler.run_tasks(tasks, 2, 1, tmp_path)
assert all(result.returncode == 0 for result in results.values())
def test_failure_skips_dependents_but_runs_other_jobs(tmp_path):
marker = tmp_path / "must-not-run"
tasks = [task(tmp_path, "failed", "raise SystemExit(3)"),
task(tmp_path, "dependent", f"from pathlib import Path; Path({str(marker)!r}).touch()", ("failed",)),
task(tmp_path, "independent", "pass")]
results = scheduler.run_tasks(tasks, 2, 1, tmp_path)
assert results["failed"].returncode == 3
assert results["dependent"].returncode != 0
assert results["independent"].returncode == 0
assert not marker.exists()
def test_thread_environment_and_execution_record(tmp_path):
names = ["OMP_NUM_THREADS", "OMP_THREAD_LIMIT", "RAYON_NUM_THREADS",
"CMAKE_BUILD_PARALLEL_LEVEL", "CARGO_BUILD_JOBS"]
job = task(tmp_path, "environment", f"import os,json; print(json.dumps({{k:os.environ[k] for k in {names!r}}}))")
scheduler.run_tasks([job], 1, 3, tmp_path)
assert set(json.loads(job.log_path.read_text()).values()) == {"3"}
record = json.loads(job.log_path.with_suffix(".execution.json").read_text())
assert record["threads_per_job"] == 3
assert scheduler.thread_environment(3)["OMP_WAIT_POLICY"] == "PASSIVE"
def test_concurrent_process_limit(tmp_path):
jobs = []
for index in range(6):
output = tmp_path / f"interval-{index}.json"
code = ("import json,time; from pathlib import Path; start=time.monotonic_ns(); "
f"time.sleep(.05); Path({str(output)!r}).write_text(json.dumps([start,time.monotonic_ns()]))")
jobs.append(task(tmp_path, str(index), code))
results = scheduler.run_tasks(jobs, 2, 1, tmp_path)
assert all(result.returncode == 0 for result in results.values())
events = []
for path in tmp_path.glob("interval-*.json"):
start, finish = json.loads(path.read_text())
events.extend(((start, 1), (finish, -1)))
active = 0
for _, delta in sorted(events):
active += delta
assert active <= 2
assert len(events) == 12 and active == 0
def test_interrupt_terminates_workers(tmp_path, monkeypatch):
processes = []
popen = scheduler.subprocess.Popen
def start(*args, **kwargs):
process = popen(*args, **kwargs)
processes.append(process)
return process
monkeypatch.setattr(scheduler.subprocess, "Popen", start)
sleep = scheduler.time.sleep
interrupted = False
def interrupt_once(seconds):
nonlocal interrupted
if not interrupted:
interrupted = True
raise KeyboardInterrupt()
sleep(seconds)
monkeypatch.setattr(scheduler.time, "sleep", interrupt_once)
with pytest.raises(KeyboardInterrupt):
scheduler.run_tasks([task(tmp_path, "waiting", "import time; time.sleep(30)")], 1, 1, tmp_path)
assert processes and all(process.poll() is not None for process in processes)
def test_invalid_dependency_graph_is_rejected(tmp_path):
with pytest.raises(ValueError, match="unknown dependency"):
scheduler.run_tasks([task(tmp_path, "a", "pass", ("missing",))], 1, 1, tmp_path)
with pytest.raises(ValueError, match="cycle"):
scheduler.run_tasks([task(tmp_path, "a", "pass", ("b",)),
task(tmp_path, "b", "pass", ("a",))], 1, 1, tmp_path)
def test_defaults_follow_processor_count(monkeypatch):
monkeypatch.setattr(suite, "available_processors", lambda: 7)
args = suite.parse_args([])
assert args.jobs == args.threads_per_job == 7
args = suite.parse_args(["--jobs", "2", "--threads-per-job", "3"])
assert (args.jobs, args.threads_per_job) == (2, 3)
with pytest.raises(SystemExit):
suite.parse_args(["--threads-per-job", "0"])
monkeypatch.setattr(ablation, "available_processors", lambda: 7)
args, forwarded = ablation.parse_args([])
assert args.jobs == args.threads_per_job == 7
assert forwarded[forwarded.index("--threads-per-job") + 1] == "7"
def test_combined_plan_reuses_references_and_raptor_baselines(tmp_path, monkeypatch):
monkeypatch.setattr(suite, "validate_pimcomp_source", lambda: None)
args = suite.parse_args(["--out-dir", str(tmp_path / "comparison"),
"--ablation-out-dir", str(tmp_path / "ablation"), "--common-dir", str(tmp_path / "references"),
"--jobs", "2", "--threads-per-job", "2", "--dry-run"])
comparison = suite.make_plan(args)
ablations, _, _ = suite.combined_ablation_plans(args)
tasks = suite.build_tasks([comparison, *ablations])
assert len([job for job in tasks if job.key.startswith("reference:")]) == 5
assert len(tasks) == 5 + 40 + 24 # 16 baseline cases reuse comparison reports.
assert sum(len(plan.aliases) for plan in ablations) == 16
assert len({job.key for job in tasks}) == len(tasks)
for job in tasks:
if job.key.startswith("reference:"):
command = job.command()
assert command[command.index("--jobs") + 1] == "2"
continue
command = job.command()
assert command[command.index("--threads-per-job") + 1] == "2"
if "--reuse-pimcomp-report" in command:
assert len(job.dependencies) == 2
else:
assert len(job.dependencies) == 1
def test_raptor_receives_thread_limit(tmp_path, monkeypatch):
received = []
monkeypatch.setattr(comparator, "compile_with_raptor",
lambda *args, **kwargs: received.extend(kwargs["raptor_extra_args"]) or {})
args = argparse.Namespace(threads_per_job=3, pimcomp_config=tmp_path / "config.json",
raptor_extra_arg=[], raptor_path=tmp_path / "compiler", verbose_raptor_compile=False, timeout_seconds=0)
comparator.compile_raptor_target(tmp_path / "model.onnx", tmp_path / "output",
{"core_count": 1, "crossbar_count": 1, "crossbar_size": 128}, args, [])
assert "-j=3" in received
def test_stale_report_is_not_published_after_missing_execution(tmp_path):
args = suite.parse_args(["--out-dir", str(tmp_path), "--models", "vgg8",
"--archs", "arch-a", "--mode", "latency", "--raptor-only"])
plan = suite.make_plan(args)
report = suite.report_for(plan, plan.specs[0])
report.parent.mkdir(parents=True)
report.write_text(json.dumps({"validation_policy": suite.VALIDATION_POLICY,
"raptor_validation": {"status": "done", "passed": True},
"raptor_performance": {"latency_ms": 1}}))
tasks = suite.build_tasks([plan])
assert suite.finish_plan(plan, {task.key: suite.TaskResult(0) for task in tasks}) == 1
import csv
with (tmp_path / "results_comparison.csv").open() as stream:
row = next(csv.DictReader(stream))
assert row["raptor_latency_ms"] == row["raptor_functional_validation"] == "NA"
def test_reference_workers_do_not_create_nested_thread_teams(tmp_path, monkeypatch):
environments = []
monkeypatch.setattr(comparator, "save_inputs_to_files", lambda *a: ([], None))
monkeypatch.setattr(comparator, "run_logged", lambda *a, **k: environments.append(k["env"]))
comparator.generate_reference_outputs(tmp_path / "runner", tmp_path, tmp_path / "model.onnx", [], [],
argparse.Namespace(timeout_seconds=0), tmp_path / "output")
assert environments[0]["OMP_NUM_THREADS"] == environments[0]["OPENBLAS_NUM_THREADS"] == "1"