Clang -ffp-contract=on/fast and GCC's default (fast) all change the density hash on the L1 view; contraction off reproduces L1 (= Windows S13) at W1/W12. Seed sweep: 48 FloatingIslands seeds and 14 effective SurfaceWorld seeds (34 of 48 fell on a seed-independent cube) match across Clang/GCC and W1/W12. Standalone gains --seed/--recipe/--render-distance-chunks (viewer + headless) and a Windows replay script. Linux standalone workers get 32 MiB pthread stacks (TLS is 17.3 MiB/thread), so no external stack limit is needed. No Core or world-output change; MSVC compile unverified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
8.8 KiB
Python
165 lines
8.8 KiB
Python
#!/usr/bin/env python3
|
||
"""Run the deterministic Linux2 seed/compiler/worker matrix under the owner limits."""
|
||
import csv
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
|
||
ROOT = Path.cwd()
|
||
ROUND = ROOT / "Standalone/Research/WorldAssemblerM3/Linux2"
|
||
LOG_DIR = ROOT / "Saved/Linux"
|
||
MEASURE = ROUND / "MeasureProcess.py"
|
||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||
BINARIES = {
|
||
"clang19": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-clang19-l2-off",
|
||
"gcc14": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-gcc14-l2-off",
|
||
}
|
||
CAMERAS = {
|
||
"surface-world": "0,-35,-487.94,0,-22",
|
||
"floating-islands": "0,0,-32,0,-22",
|
||
}
|
||
HASH_RE = re.compile(
|
||
r"\[PipelineWholeViewHash\] tiles=(\d+) density=0x([0-9a-f]+) "
|
||
r"mesh=0x([0-9a-f]+) payload=0x([0-9a-f]+) topology=0x([0-9a-f]+)"
|
||
)
|
||
VOLUME_RE = re.compile(
|
||
r"\[PipelineVolume\] tiles=(\d+) lod=(\d+)\.\.(\d+) "
|
||
r"min_voxel=\((-?\d+),(-?\d+),(-?\d+)\) "
|
||
r"max_voxel=\((-?\d+),(-?\d+),(-?\d+)\)"
|
||
)
|
||
READY_RE = re.compile(r"\[StreamPipeline\].*whole_view_ready=([0-9.]+)s")
|
||
RESOURCE_RE = re.compile(r"\[ProcessResources\] exit_code=(-?\d+) wall_s=([0-9.]+) peak_rss_kib=(\d+)")
|
||
|
||
|
||
def main() -> int:
|
||
if not (1 <= len(sys.argv) <= 2):
|
||
raise SystemExit("usage: RunSeedSweep.py [budget-seconds; default 1800]")
|
||
budget_seconds = int(sys.argv[1]) if len(sys.argv) == 2 else 1800
|
||
seeds = [int(line.strip()) for line in (ROUND / "seeds.txt").read_text().splitlines() if line.strip()]
|
||
if len(seeds) != 48 or len(set(seeds)) != len(seeds):
|
||
raise SystemExit("seeds.txt must contain 48 unique signed int32 seeds")
|
||
missing = [str(path) for path in [MEASURE, DATA, *BINARIES.values()] if not path.is_file()]
|
||
if missing:
|
||
raise SystemExit("missing sweep input/build: " + ", ".join(missing))
|
||
|
||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||
output_path = ROUND / "seed_hashes.csv"
|
||
fields = [
|
||
"seed", "recipe", "compiler", "workers", "tiles", "lod_min", "lod_max",
|
||
"min_x", "min_y", "min_z", "max_x", "max_y", "max_z",
|
||
"density_hash", "mesh_hash", "payload_hash", "topology_hash",
|
||
"whole_view_ready_s", "wall_s", "peak_rss_kib", "exit_code", "status",
|
||
]
|
||
started = time.monotonic()
|
||
total = len(seeds) * len(CAMERAS) * len(BINARIES) * 2
|
||
print(f"Sweep: {len(seeds)} seeds × 2 recipes × 2 compilers × W1/W12 = {total} runs; budget={budget_seconds}s", flush=True)
|
||
|
||
with output_path.open("w", newline="") as csv_file:
|
||
writer = csv.DictWriter(csv_file, fieldnames=fields)
|
||
writer.writeheader()
|
||
for seed in seeds:
|
||
for recipe, camera in CAMERAS.items():
|
||
for compiler, binary in BINARIES.items():
|
||
for workers in (1, 12):
|
||
remaining = budget_seconds - (time.monotonic() - started)
|
||
if remaining <= 0:
|
||
print("Sweep exceeded its 30 minute budget; stopping.", file=sys.stderr, flush=True)
|
||
return 124
|
||
stem = f"linux2-seed-{seed}-{recipe}-{compiler}-w{workers}"
|
||
log_path = LOG_DIR / f"{stem}.log"
|
||
args = [
|
||
str(binary), str(DATA), f"--seed={seed}", f"--recipe={recipe}",
|
||
"--render-distance-chunks=0", f"--workers={workers}",
|
||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||
"--stream-hash", "--force-generate", "--clip-radius=1",
|
||
"--max-clip-level=0", f"--camera={camera}", "--far-horizon-lod8-step=0",
|
||
]
|
||
command = [
|
||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||
"python3", str(MEASURE), *args,
|
||
]
|
||
with log_path.open("w") as log_file:
|
||
log_file.write("[SweepCommand] " + " ".join(command) + "\n")
|
||
log_file.flush()
|
||
try:
|
||
result = subprocess.run(
|
||
command, cwd=ROOT, stdout=log_file, stderr=subprocess.STDOUT,
|
||
timeout=min(900, max(1, remaining)), check=False,
|
||
)
|
||
exit_code = result.returncode
|
||
except subprocess.TimeoutExpired:
|
||
exit_code = 124
|
||
log_file.write("\n[SweepTimeout] run reached its allotted limit\n")
|
||
|
||
output = log_path.read_text(errors="replace")
|
||
volume = VOLUME_RE.search(output)
|
||
hashes = HASH_RE.search(output)
|
||
ready = READY_RE.search(output)
|
||
resources = RESOURCE_RE.search(output)
|
||
passed = exit_code == 0 and volume and hashes and ready and resources
|
||
row = {
|
||
"seed": seed, "recipe": recipe, "compiler": compiler, "workers": workers,
|
||
"tiles": "", "lod_min": "", "lod_max": "",
|
||
"min_x": "", "min_y": "", "min_z": "", "max_x": "", "max_y": "", "max_z": "",
|
||
"density_hash": "", "mesh_hash": "", "payload_hash": "", "topology_hash": "",
|
||
"whole_view_ready_s": "", "wall_s": "", "peak_rss_kib": "",
|
||
"exit_code": exit_code, "status": "PASS" if passed else "FAIL",
|
||
}
|
||
if passed:
|
||
row.update({
|
||
"tiles": hashes.group(1),
|
||
"lod_min": volume.group(2), "lod_max": volume.group(3),
|
||
"min_x": volume.group(4), "min_y": volume.group(5), "min_z": volume.group(6),
|
||
"max_x": volume.group(7), "max_y": volume.group(8), "max_z": volume.group(9),
|
||
"density_hash": "0x" + hashes.group(2), "mesh_hash": "0x" + hashes.group(3),
|
||
"payload_hash": "0x" + hashes.group(4), "topology_hash": "0x" + hashes.group(5),
|
||
"whole_view_ready_s": ready.group(1), "wall_s": resources.group(2),
|
||
"peak_rss_kib": resources.group(3),
|
||
})
|
||
writer.writerow(row)
|
||
csv_file.flush()
|
||
completed = sum(1 for _ in output_path.open()) - 1
|
||
elapsed = time.monotonic() - started
|
||
print(f"[{completed}/{total}] seed={seed} recipe={recipe} compiler={compiler} W{workers} "
|
||
f"tiles={row['tiles']} wall={row['wall_s']}s status={row['status']} "
|
||
f"elapsed={elapsed:.1f}s", flush=True)
|
||
if not passed:
|
||
print(f"Run failed; details: {log_path}", file=sys.stderr, flush=True)
|
||
return exit_code or 1
|
||
|
||
rows = list(csv.DictReader(output_path.open(newline="")))
|
||
keys = ("density_hash", "mesh_hash", "payload_hash", "topology_hash")
|
||
checks = []
|
||
for seed in seeds:
|
||
for recipe in CAMERAS:
|
||
for workers in (1, 12):
|
||
pair = [r for r in rows if int(r["seed"]) == seed and r["recipe"] == recipe
|
||
and int(r["workers"]) == workers]
|
||
equal = len(pair) == 2 and all(pair[0][key] == pair[1][key] for key in keys)
|
||
checks.append((seed, recipe, workers, equal))
|
||
worker_checks = []
|
||
for seed in seeds:
|
||
for recipe in CAMERAS:
|
||
for compiler in BINARIES:
|
||
pair = [r for r in rows if int(r["seed"]) == seed and r["recipe"] == recipe
|
||
and r["compiler"] == compiler]
|
||
equal = len(pair) == 2 and all(pair[0][key] == pair[1][key] for key in keys)
|
||
worker_checks.append((seed, recipe, compiler, equal))
|
||
summary_path = ROUND / "seed_sweep_summary.txt"
|
||
mismatch_count = sum(not equal for _, _, _, equal in checks)
|
||
worker_mismatch_count = sum(not equal for _, _, _, equal in worker_checks)
|
||
summary_path.write_text(
|
||
f"runs={len(rows)} seeds={len(seeds)} recipes={len(CAMERAS)} compilers={len(BINARIES)} "
|
||
f"workers=1,12 cross_compiler_comparisons={len(checks)} cross_compiler_mismatches={mismatch_count} "
|
||
f"cross_worker_comparisons={len(worker_checks)} cross_worker_mismatches={worker_mismatch_count} "
|
||
f"elapsed_s={time.monotonic()-started:.3f}\n"
|
||
)
|
||
print(summary_path.read_text(), end="", flush=True)
|
||
return 0 if mismatch_count == 0 and worker_mismatch_count == 0 else 3
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|