Workload-conditioned operator profiling on patched vLLM 0.24.0 + Qwen3-30B-A3B/H20. H1b PASS (irregular patterns carry +23-45pp R64 raggedness, 8-45% token-efficiency loss vs rectangular controls); mechanism decomposition kills the padding narrative and finds the arrival-uniformization artifact (-12.9%); cross-version churn surface shows TP2/MNS64 -29.4% across vLLM 0.20->0.24 while the argmax held. Raw Layer-1 JSONL streams (507 MB) stay on disk, git-ignored; footer sidecars and metrics are tracked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1057 lines
35 KiB
Python
1057 lines
35 KiB
Python
#!/usr/bin/env python3
|
|
"""Detached/resumable dash0 controller for the Phase-3 co-location gate."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import gzip
|
|
import hashlib
|
|
import json
|
|
import math
|
|
import os
|
|
import re
|
|
import shlex
|
|
import shutil
|
|
import signal
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
import urllib.request
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
SCHEMA = 1
|
|
WORKDIR = Path("/home/admin/cpfs/wjh/opprof-phase3-dash0-20260712")
|
|
RUN_ROOT = WORKDIR / "runs/e-a-colocation"
|
|
PRIVATE = Path("/home/admin/cpfs/wjh/opprof-phase3-private/manifests")
|
|
MODEL = Path("/home/admin/cpfs/wjh/models/Qwen/Qwen3-30B-A3B")
|
|
SOURCE = Path(
|
|
"/home/admin/cpfs/wjh/opprof-phase2-dash0-20260711/vllm-v0.24.0"
|
|
)
|
|
VENV = Path("/tmp/wjh-opprof-phase2-dash0-20260711/.venv")
|
|
CLIENT = WORKDIR / "scripts/opprof_phase3_client.py"
|
|
STATE = RUN_ROOT / "controller-state.json"
|
|
CPU_MAP = {
|
|
0: "0-19",
|
|
1: "20-39",
|
|
2: "40-59",
|
|
3: "60-79",
|
|
4: "80-99",
|
|
5: "100-119",
|
|
6: "120-139",
|
|
7: "140-159",
|
|
}
|
|
BACKGROUND_8 = {
|
|
1: "P01",
|
|
2: "P02",
|
|
3: "P03",
|
|
4: "P04",
|
|
5: "P06",
|
|
6: "P07",
|
|
7: "P08",
|
|
}
|
|
BACKGROUND_4 = {1: "P01", 2: "P03", 3: "P06"}
|
|
PATTERNS = {
|
|
"P01": ["--kind", "synthetic", "--input-uniform", "128:512",
|
|
"--output-fixed", "64", "--prefix", "none", "--arrival", "steady"],
|
|
"P02": ["--kind", "synthetic", "--input-uniform", "128:512",
|
|
"--output-fixed", "512", "--prefix", "none", "--arrival", "steady"],
|
|
"P03": ["--kind", "synthetic", "--input-uniform", "4096:8192",
|
|
"--output-fixed", "64", "--prefix", "none", "--arrival", "steady"],
|
|
"P04": ["--kind", "synthetic", "--input-uniform", "4096:8192",
|
|
"--output-fixed", "512", "--prefix", "none", "--arrival", "burst:8"],
|
|
"P05": ["--kind", "synthetic", "--input-mixture",
|
|
'{"uniform:128:512":0.5,"uniform:4096:8192":0.5}',
|
|
"--output-fixed", "64", "--prefix", "none", "--arrival", "steady"],
|
|
"P06": ["--kind", "synthetic", "--input-mixture",
|
|
'{"uniform:128:512":0.5,"uniform:4096:8192":0.5}',
|
|
"--output-fixed", "512", "--prefix", "none", "--arrival", "burst:8"],
|
|
"P07": ["--kind", "synthetic", "--input-fixed", "1280",
|
|
"--output-fixed", "512", "--prefix", "none", "--arrival", "burst:8"],
|
|
"P08": ["--kind", "prefix-pool", "--num-prefixes", "8",
|
|
"--prefix-len", "1024", "--suffix-fixed", "256",
|
|
"--output-fixed", "512", "--arrival", "burst:8"],
|
|
}
|
|
PROFILE_CONFIG = {
|
|
"profiler": "torch",
|
|
"torch_profiler_with_stack": True,
|
|
"torch_profiler_record_shapes": True,
|
|
"torch_profiler_use_gzip": True,
|
|
"ignore_frontend": True,
|
|
"wait_iterations": 0,
|
|
"warmup_iterations": 2,
|
|
"active_iterations": 8,
|
|
}
|
|
|
|
|
|
def sha256_file(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as f:
|
|
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def atomic_json(path: Path, value: Any) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = path.with_name(path.name + f".tmp.{os.getpid()}")
|
|
with tmp.open("w", encoding="utf-8") as f:
|
|
json.dump(value, f, sort_keys=True, indent=2)
|
|
f.write("\n")
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
os.replace(tmp, path)
|
|
|
|
|
|
def run_text(command: list[str], check: bool = True) -> str:
|
|
result = subprocess.run(
|
|
command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
|
)
|
|
if check and result.returncode:
|
|
raise RuntimeError(
|
|
f"command failed ({result.returncode}): {shlex.join(command)}\n"
|
|
f"{result.stdout}"
|
|
)
|
|
return result.stdout
|
|
|
|
|
|
def command_log(path: Path, label: str, command: list[str], expected: str) -> None:
|
|
with path.open("a", encoding="utf-8") as f:
|
|
f.write(
|
|
f"GPU_COMMAND {label}: {shlex.join(command)} ; expected={expected}\n"
|
|
)
|
|
|
|
|
|
def load_state(resume: bool) -> dict[str, Any]:
|
|
if STATE.exists():
|
|
if not resume:
|
|
raise RuntimeError("state exists; use --resume")
|
|
return json.loads(STATE.read_text())
|
|
return {
|
|
"schema": SCHEMA,
|
|
"status": "created",
|
|
"created_at": time.time(),
|
|
"controller_pid": os.getpid(),
|
|
"stages": {},
|
|
"fingerprint": {},
|
|
}
|
|
|
|
|
|
def save_state(state: dict[str, Any]) -> None:
|
|
state["updated_at"] = time.time()
|
|
state["controller_pid"] = os.getpid()
|
|
atomic_json(STATE, state)
|
|
|
|
|
|
def make_fingerprint() -> dict[str, Any]:
|
|
return {
|
|
"client_sha256": sha256_file(CLIENT),
|
|
"controller_sha256": sha256_file(Path(__file__)),
|
|
"source_commit": run_text(
|
|
["git", "-C", str(SOURCE), "rev-parse", "HEAD"]
|
|
).strip(),
|
|
"model_path": str(MODEL),
|
|
"venv_python": str(VENV / "bin/python"),
|
|
# JSON object keys are strings. Normalize before the first write so a
|
|
# read/compare during --resume is byte-semantically stable.
|
|
"cpu_map": {str(gpu): cpus for gpu, cpus in CPU_MAP.items()},
|
|
}
|
|
|
|
|
|
def ensure_provenance() -> None:
|
|
provenance = RUN_ROOT / "provenance"
|
|
provenance.mkdir(parents=True, exist_ok=True)
|
|
for source in (CLIENT, Path(__file__)):
|
|
dest = provenance / source.name
|
|
source_sha = sha256_file(source)
|
|
if dest.exists() and sha256_file(dest) != source_sha:
|
|
# Preserve the prior immutable copy and add the reviewed runtime
|
|
# repair as a content-addressed sibling.
|
|
dest = provenance / f"{source.stem}.{source_sha[:12]}{source.suffix}"
|
|
if dest.exists() and sha256_file(dest) != source_sha:
|
|
raise RuntimeError(f"content-addressed provenance changed: {dest}")
|
|
if not dest.exists():
|
|
shutil.copy2(source, dest)
|
|
checksums = {
|
|
path.name: sha256_file(path)
|
|
for path in sorted(provenance.glob("*.py"))
|
|
}
|
|
atomic_json(provenance / "sha256.json", checksums)
|
|
|
|
|
|
def ensure_manifests() -> None:
|
|
PRIVATE.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
os.chmod(PRIVATE, 0o700)
|
|
for pattern, spec in PATTERNS.items():
|
|
out = PRIVATE / f"{pattern}.jsonl"
|
|
if out.exists():
|
|
summary = json.loads(
|
|
out.with_suffix(out.suffix + ".summary.json").read_text()
|
|
)
|
|
if summary["rows"] != 32768 or summary["sha256"] != sha256_file(out):
|
|
raise RuntimeError(f"manifest verification failed: {out}")
|
|
continue
|
|
command = [
|
|
str(VENV / "bin/python"),
|
|
str(CLIENT),
|
|
"materialize",
|
|
"--id",
|
|
pattern,
|
|
*spec,
|
|
"--num-requests",
|
|
"32768",
|
|
"--workload-seed",
|
|
"20260712",
|
|
"--out",
|
|
str(out),
|
|
]
|
|
run_text(command)
|
|
|
|
|
|
def gpu_query() -> list[dict[str, Any]]:
|
|
text = run_text(
|
|
[
|
|
"nvidia-smi",
|
|
"--query-gpu=index,uuid,memory.used,utilization.gpu",
|
|
"--format=csv,noheader,nounits",
|
|
]
|
|
)
|
|
rows = []
|
|
for line in text.strip().splitlines():
|
|
index, uuid, memory, util = [part.strip() for part in line.split(",")]
|
|
rows.append(
|
|
{
|
|
"index": int(index),
|
|
"uuid": uuid,
|
|
"memory_used_mib": int(memory),
|
|
"utilization_pct": int(util),
|
|
}
|
|
)
|
|
return rows
|
|
|
|
|
|
def compute_apps() -> list[dict[str, Any]]:
|
|
text = run_text(
|
|
[
|
|
"nvidia-smi",
|
|
"--query-compute-apps=gpu_uuid,pid,process_name,used_memory",
|
|
"--format=csv,noheader,nounits",
|
|
],
|
|
check=False,
|
|
)
|
|
rows = []
|
|
for line in text.strip().splitlines():
|
|
if not line or "No running" in line:
|
|
continue
|
|
parts = [part.strip() for part in line.split(",", 3)]
|
|
if len(parts) == 4 and parts[1].isdigit():
|
|
rows.append(
|
|
{
|
|
"gpu_uuid": parts[0],
|
|
"pid": int(parts[1]),
|
|
"process_name": parts[2],
|
|
"used_memory_mib": int(parts[3].split()[0]),
|
|
}
|
|
)
|
|
return rows
|
|
|
|
|
|
def preflight(gpus: list[int], stage_dir: Path) -> None:
|
|
samples = []
|
|
consecutive_idle = 0
|
|
deadline = time.monotonic() + 60
|
|
while time.monotonic() < deadline and consecutive_idle < 3:
|
|
snapshots = [row for row in gpu_query() if row["index"] in gpus]
|
|
apps = compute_apps()
|
|
samples.append({"gpus": snapshots, "compute_apps": apps, "time": time.time()})
|
|
idle = (
|
|
not apps
|
|
and all(row["memory_used_mib"] == 0 for row in snapshots)
|
|
and all(row["utilization_pct"] == 0 for row in snapshots)
|
|
)
|
|
consecutive_idle = consecutive_idle + 1 if idle else 0
|
|
if consecutive_idle < 3:
|
|
time.sleep(2)
|
|
atomic_json(stage_dir / "gpu-before.json", snapshots)
|
|
atomic_json(stage_dir / "gpu-before-samples.json", samples)
|
|
(stage_dir / "clocks-before.txt").write_text(
|
|
run_text(["nvidia-smi", "-q", "-d", "CLOCK"])
|
|
)
|
|
(stage_dir / "loadavg-before.txt").write_text(
|
|
" ".join(str(value) for value in os.getloadavg()) + "\n"
|
|
)
|
|
(stage_dir / "processes-before.txt").write_text(
|
|
run_text(["ps", "-eo", "user,pid,ppid,pgid,lstart,args", "--sort=pid"])
|
|
)
|
|
if consecutive_idle < 3:
|
|
raise RuntimeError(f"selected GPUs not stably idle: samples={samples}")
|
|
|
|
|
|
class Monitor:
|
|
def __init__(self, path: Path, owned_pgids: set[int]) -> None:
|
|
self.path = path
|
|
self.owned_pgids = owned_pgids
|
|
self.stop_event = threading.Event()
|
|
self.other_apps: list[dict[str, Any]] = []
|
|
self.thread = threading.Thread(target=self._run, daemon=True)
|
|
|
|
def start(self) -> None:
|
|
self.thread.start()
|
|
|
|
def stop(self) -> None:
|
|
self.stop_event.set()
|
|
self.thread.join(timeout=20)
|
|
|
|
def _run(self) -> None:
|
|
with self.path.open("a", encoding="utf-8") as f:
|
|
while not self.stop_event.is_set():
|
|
apps = compute_apps()
|
|
samples = []
|
|
for app in apps:
|
|
try:
|
|
pgid = os.getpgid(app["pid"])
|
|
except ProcessLookupError:
|
|
pgid = None
|
|
sample = {**app, "pgid": pgid}
|
|
samples.append(sample)
|
|
if pgid not in self.owned_pgids:
|
|
self.other_apps.append(sample)
|
|
clocks = run_text(
|
|
[
|
|
"nvidia-smi",
|
|
"--query-gpu=index,clocks.sm,clocks.mem,pstate,"
|
|
"utilization.gpu,memory.used",
|
|
"--format=csv,noheader,nounits",
|
|
],
|
|
check=False,
|
|
).strip().splitlines()
|
|
row = {
|
|
"wall_time": time.time(),
|
|
"loadavg": os.getloadavg(),
|
|
"gpu_clocks": clocks,
|
|
"compute_apps": samples,
|
|
}
|
|
f.write(json.dumps(row, sort_keys=True) + "\n")
|
|
f.flush()
|
|
self.stop_event.wait(15)
|
|
|
|
|
|
def server_command(gpu: int, stage_dir: Path, trace_dir: Path) -> list[str]:
|
|
config = {**PROFILE_CONFIG, "torch_profiler_dir": str(trace_dir)}
|
|
return [
|
|
"taskset",
|
|
"-c",
|
|
CPU_MAP[gpu],
|
|
str(VENV / "bin/vllm"),
|
|
"serve",
|
|
str(MODEL),
|
|
"--host",
|
|
"127.0.0.1",
|
|
"--port",
|
|
str(8000 + gpu),
|
|
"--tensor-parallel-size",
|
|
"1",
|
|
"--enable-chunked-prefill",
|
|
"--enable-prefix-caching",
|
|
"--profiler-config",
|
|
json.dumps(config, separators=(",", ":")),
|
|
]
|
|
|
|
|
|
def start_server(
|
|
gpu: int, stage_dir: Path, state_stage: dict[str, Any]
|
|
) -> tuple[subprocess.Popen[Any], Any, Path]:
|
|
gpu_dir = stage_dir / f"gpu{gpu}"
|
|
gpu_dir.mkdir(parents=True, exist_ok=True)
|
|
trace_dir = Path(f"/tmp/wjh-opprof-phase3-ea/{stage_dir.name}/gpu{gpu}")
|
|
if trace_dir.exists():
|
|
shutil.rmtree(trace_dir)
|
|
trace_dir.mkdir(parents=True)
|
|
command = server_command(gpu, stage_dir, trace_dir)
|
|
command_log(
|
|
stage_dir / "commands.log",
|
|
f"{stage_dir.name} server gpu{gpu}",
|
|
command,
|
|
"startup 60-180s; run 300-450s",
|
|
)
|
|
env = os.environ.copy()
|
|
env.update(
|
|
{
|
|
"CUDA_VISIBLE_DEVICES": str(gpu),
|
|
"VLLM_OPPROF_DIR": str(gpu_dir / "opprof"),
|
|
"HF_HUB_OFFLINE": "1",
|
|
"TRANSFORMERS_OFFLINE": "1",
|
|
"PYTHONUNBUFFERED": "1",
|
|
}
|
|
)
|
|
log_handle = (gpu_dir / "server.log").open("ab", buffering=0)
|
|
process = subprocess.Popen(
|
|
command,
|
|
cwd=SOURCE,
|
|
env=env,
|
|
stdout=log_handle,
|
|
stderr=subprocess.STDOUT,
|
|
start_new_session=True,
|
|
)
|
|
state_stage.setdefault("servers", {})[str(gpu)] = {
|
|
"pid": process.pid,
|
|
"pgid": process.pid,
|
|
"command": command,
|
|
"trace_dir": str(trace_dir),
|
|
}
|
|
return process, log_handle, trace_dir
|
|
|
|
|
|
def wait_ready(processes: dict[int, subprocess.Popen[Any]], timeout: float = 300) -> None:
|
|
deadline = time.monotonic() + timeout
|
|
pending = set(processes)
|
|
while pending and time.monotonic() < deadline:
|
|
for gpu in list(pending):
|
|
if processes[gpu].poll() is not None:
|
|
raise RuntimeError(f"server gpu{gpu} exited before ready")
|
|
try:
|
|
with urllib.request.urlopen(
|
|
f"http://127.0.0.1:{8000 + gpu}/health", timeout=1
|
|
) as response:
|
|
if response.status == 200:
|
|
pending.remove(gpu)
|
|
except Exception:
|
|
pass
|
|
time.sleep(1)
|
|
if pending:
|
|
raise TimeoutError(f"servers not ready within {timeout}s: {sorted(pending)}")
|
|
|
|
|
|
def client_command(
|
|
gpu: int,
|
|
pattern: str,
|
|
stage_dir: Path,
|
|
load_point: str,
|
|
saturation_result: Path | None,
|
|
profile: bool,
|
|
trace_dir: Path,
|
|
post_clean_seconds: int,
|
|
) -> list[str]:
|
|
result_dir = stage_dir / f"gpu{gpu}" / "client"
|
|
command = [
|
|
"taskset",
|
|
"-c",
|
|
CPU_MAP[gpu],
|
|
str(VENV / "bin/python"),
|
|
str(CLIENT),
|
|
"run",
|
|
"--manifest",
|
|
str(PRIVATE / f"{pattern}.jsonl"),
|
|
"--base-url",
|
|
f"http://127.0.0.1:{8000 + gpu}",
|
|
"--model",
|
|
str(MODEL),
|
|
"--load-point",
|
|
load_point,
|
|
"--max-concurrency",
|
|
"256",
|
|
"--ignore-eos",
|
|
"--temperature",
|
|
"0",
|
|
"--warmup-seconds",
|
|
"60",
|
|
"--clean-segment-seconds",
|
|
"80",
|
|
"--num-clean-segments",
|
|
"3",
|
|
"--recovery-seconds",
|
|
"30",
|
|
"--drain-timeout-seconds",
|
|
"120",
|
|
"--workload-seed",
|
|
"20260712",
|
|
"--result-dir",
|
|
str(result_dir),
|
|
]
|
|
if load_point == "saturation":
|
|
command += ["--request-rate", "inf"]
|
|
else:
|
|
assert saturation_result is not None
|
|
command += [
|
|
"--saturation-result",
|
|
str(saturation_result),
|
|
"--rate-fraction",
|
|
"0.60",
|
|
]
|
|
if profile:
|
|
command += [
|
|
"--profile-after-clean",
|
|
"--num-profile-windows",
|
|
"1",
|
|
"--profile-warmup-iterations",
|
|
"2",
|
|
"--profile-active-iterations",
|
|
"8",
|
|
"--profile-trace-dir",
|
|
str(trace_dir),
|
|
"--profile-timeout-seconds",
|
|
"120",
|
|
]
|
|
elif post_clean_seconds:
|
|
command += ["--post-clean-seconds", str(post_clean_seconds)]
|
|
return command
|
|
|
|
|
|
def stop_processes(processes: list[subprocess.Popen[Any]]) -> None:
|
|
live = [process for process in processes if process.poll() is None]
|
|
for process in live:
|
|
try:
|
|
os.killpg(process.pid, signal.SIGINT)
|
|
except ProcessLookupError:
|
|
pass
|
|
deadline = time.monotonic() + 60
|
|
while time.monotonic() < deadline and any(p.poll() is None for p in live):
|
|
time.sleep(1)
|
|
for sig, wait_seconds in ((signal.SIGTERM, 20), (signal.SIGKILL, 5)):
|
|
remaining = [p for p in live if p.poll() is None]
|
|
if not remaining:
|
|
break
|
|
for process in remaining:
|
|
try:
|
|
os.killpg(process.pid, sig)
|
|
except ProcessLookupError:
|
|
pass
|
|
deadline = time.monotonic() + wait_seconds
|
|
while time.monotonic() < deadline and any(
|
|
p.poll() is None for p in remaining
|
|
):
|
|
time.sleep(0.5)
|
|
|
|
|
|
def _process_group_alive(pgid: int) -> bool:
|
|
try:
|
|
os.killpg(pgid, 0)
|
|
return True
|
|
except ProcessLookupError:
|
|
return False
|
|
|
|
|
|
def stop_servers(processes: list[subprocess.Popen[Any]]) -> None:
|
|
"""Let the API parent ask EngineCore to close before group escalation."""
|
|
groups = [process.pid for process in processes]
|
|
for process in processes:
|
|
if process.poll() is None:
|
|
try:
|
|
# Group SIGINT also interrupts EngineCore and loses the OpProf
|
|
# footer. The API parent's shutdown path signals it cleanly.
|
|
os.kill(process.pid, signal.SIGINT)
|
|
except ProcessLookupError:
|
|
pass
|
|
deadline = time.monotonic() + 90
|
|
while time.monotonic() < deadline and any(
|
|
_process_group_alive(pgid) for pgid in groups
|
|
):
|
|
time.sleep(1)
|
|
for sig, wait_seconds in ((signal.SIGTERM, 20), (signal.SIGKILL, 5)):
|
|
remaining = [pgid for pgid in groups if _process_group_alive(pgid)]
|
|
if not remaining:
|
|
break
|
|
for pgid in remaining:
|
|
try:
|
|
os.killpg(pgid, sig)
|
|
except ProcessLookupError:
|
|
pass
|
|
deadline = time.monotonic() + wait_seconds
|
|
while time.monotonic() < deadline and any(
|
|
_process_group_alive(pgid) for pgid in remaining
|
|
):
|
|
time.sleep(0.5)
|
|
|
|
|
|
def verify_idle(gpus: list[int], stage_dir: Path) -> None:
|
|
samples = []
|
|
consecutive_zero = 0
|
|
deadline = time.monotonic() + 120
|
|
while time.monotonic() < deadline and consecutive_zero < 3:
|
|
rows = [row for row in gpu_query() if row["index"] in gpus]
|
|
apps = compute_apps()
|
|
samples.append({"gpus": rows, "compute_apps": apps, "time": time.time()})
|
|
zero = not apps and all(row["memory_used_mib"] == 0 for row in rows)
|
|
consecutive_zero = consecutive_zero + 1 if zero else 0
|
|
if consecutive_zero < 3:
|
|
time.sleep(5)
|
|
atomic_json(stage_dir / "gpu-after-cleanup.json", samples)
|
|
if consecutive_zero < 3:
|
|
raise RuntimeError("GPU memory did not reach three stable zero samples")
|
|
|
|
|
|
def run_stage(
|
|
state: dict[str, Any],
|
|
name: str,
|
|
backgrounds: dict[int, str],
|
|
load_point: str,
|
|
saturation_stage: str | None,
|
|
profile: bool,
|
|
) -> None:
|
|
stage_dir = RUN_ROOT / name
|
|
stage_record = state["stages"].get(name)
|
|
if (
|
|
stage_record
|
|
and stage_record.get("status") == "complete"
|
|
and (stage_dir / "stage-complete.json").exists()
|
|
):
|
|
print(f"RESUME skip complete stage {name}", flush=True)
|
|
return
|
|
if stage_dir.exists():
|
|
shutil.rmtree(stage_dir)
|
|
stage_dir.mkdir(parents=True)
|
|
gpus = [0, *sorted(backgrounds)]
|
|
state["stages"][name] = {
|
|
"status": "preflight",
|
|
"started_at": time.time(),
|
|
"gpus": gpus,
|
|
"backgrounds": backgrounds,
|
|
"load_point": load_point,
|
|
"profile": profile,
|
|
}
|
|
save_state(state)
|
|
preflight(gpus, stage_dir)
|
|
servers: dict[int, subprocess.Popen[Any]] = {}
|
|
server_logs = []
|
|
client_processes: dict[int, subprocess.Popen[Any]] = {}
|
|
client_logs = []
|
|
trace_dirs: dict[int, Path] = {}
|
|
monitor: Monitor | None = None
|
|
failure: Exception | None = None
|
|
try:
|
|
for gpu in gpus:
|
|
process, handle, trace_dir = start_server(
|
|
gpu, stage_dir, state["stages"][name]
|
|
)
|
|
servers[gpu] = process
|
|
server_logs.append(handle)
|
|
trace_dirs[gpu] = trace_dir
|
|
state["stages"][name]["status"] = "starting_servers"
|
|
save_state(state)
|
|
wait_ready(servers)
|
|
state["stages"][name]["servers_ready_at"] = time.time()
|
|
state["stages"][name]["status"] = "running_clients"
|
|
save_state(state)
|
|
monitor = Monitor(
|
|
stage_dir / "monitor.jsonl", {process.pid for process in servers.values()}
|
|
)
|
|
monitor.start()
|
|
|
|
assignments = {0: "P05", **backgrounds}
|
|
for gpu, pattern in assignments.items():
|
|
is_target = gpu == 0
|
|
sat_result = (
|
|
RUN_ROOT / saturation_stage / "gpu0/client/result.json"
|
|
if is_target and saturation_stage
|
|
else None
|
|
)
|
|
target_load = load_point if is_target else "saturation"
|
|
command = client_command(
|
|
gpu,
|
|
pattern,
|
|
stage_dir,
|
|
target_load,
|
|
sat_result,
|
|
profile=is_target and profile,
|
|
trace_dir=trace_dirs[gpu],
|
|
post_clean_seconds=60 if not is_target and profile else 0,
|
|
)
|
|
command_log(
|
|
stage_dir / "commands.log",
|
|
f"{name} client gpu{gpu} {pattern}",
|
|
command,
|
|
"60s warmup + 240s clean + optional 2+8 profile/recovery",
|
|
)
|
|
handle = (stage_dir / f"gpu{gpu}/client.log").open("ab", buffering=0)
|
|
client_logs.append(handle)
|
|
process = subprocess.Popen(
|
|
command,
|
|
cwd=WORKDIR,
|
|
stdout=handle,
|
|
stderr=subprocess.STDOUT,
|
|
start_new_session=True,
|
|
)
|
|
client_processes[gpu] = process
|
|
state["stages"][name].setdefault("clients", {})[str(gpu)] = {
|
|
"pid": process.pid,
|
|
"pgid": process.pid,
|
|
"command": command,
|
|
}
|
|
save_state(state)
|
|
|
|
deadline = time.monotonic() + 780
|
|
while time.monotonic() < deadline and any(
|
|
process.poll() is None for process in client_processes.values()
|
|
):
|
|
if any(process.poll() is not None for process in servers.values()):
|
|
raise RuntimeError("server exited while client was active")
|
|
time.sleep(2)
|
|
if any(process.poll() is None for process in client_processes.values()):
|
|
raise TimeoutError("client stage exceeded 780 seconds")
|
|
bad = {
|
|
gpu: process.returncode
|
|
for gpu, process in client_processes.items()
|
|
if process.returncode != 0
|
|
}
|
|
if bad:
|
|
raise RuntimeError(f"client failures: {bad}")
|
|
target_result = stage_dir / "gpu0/client/result.json"
|
|
if not target_result.exists():
|
|
raise RuntimeError("target result.json missing")
|
|
if profile:
|
|
trace_dest = stage_dir / "gpu0/traces"
|
|
trace_dest.mkdir()
|
|
for path in trace_dirs[0].glob("*"):
|
|
if path.is_file():
|
|
shutil.copy2(path, trace_dest / path.name)
|
|
traces = list(trace_dest.glob("*.pt.trace.json*"))
|
|
if len(traces) != 1:
|
|
raise RuntimeError(f"expected one target trace, found {len(traces)}")
|
|
state["stages"][name]["status"] = "clients_complete"
|
|
save_state(state)
|
|
except Exception as exc:
|
|
failure = exc
|
|
finally:
|
|
if monitor is not None:
|
|
monitor.stop()
|
|
stop_processes(list(client_processes.values()))
|
|
stop_servers(list(servers.values()))
|
|
for handle in client_logs + server_logs:
|
|
handle.close()
|
|
(stage_dir / "clocks-after.txt").write_text(
|
|
run_text(["nvidia-smi", "-q", "-d", "CLOCK"], check=False)
|
|
)
|
|
(stage_dir / "loadavg-after.txt").write_text(
|
|
" ".join(str(value) for value in os.getloadavg()) + "\n"
|
|
)
|
|
(stage_dir / "processes-after.txt").write_text(
|
|
run_text(
|
|
["ps", "-eo", "user,pid,ppid,pgid,lstart,args", "--sort=pid"],
|
|
check=False,
|
|
)
|
|
)
|
|
if monitor is not None:
|
|
atomic_json(stage_dir / "other-gpu-processes.json", monitor.other_apps)
|
|
try:
|
|
verify_idle(gpus, stage_dir)
|
|
except Exception as exc:
|
|
failure = failure or exc
|
|
|
|
if failure is not None:
|
|
state["stages"][name]["status"] = "failed"
|
|
state["stages"][name]["failure"] = repr(failure)
|
|
save_state(state)
|
|
raise failure
|
|
marker = {
|
|
"schema": SCHEMA,
|
|
"stage": name,
|
|
"completed_at": time.time(),
|
|
"target_result_sha256": sha256_file(
|
|
stage_dir / "gpu0/client/result.json"
|
|
),
|
|
}
|
|
atomic_json(stage_dir / "stage-complete.json", marker)
|
|
state["stages"][name].update(
|
|
{"status": "complete", "completed_at": time.time(), "servers": {},
|
|
"clients": {}}
|
|
)
|
|
save_state(state)
|
|
|
|
|
|
def classify_kernel(name: str) -> str:
|
|
value = name.lower()
|
|
if re.search(r"topkgating|moe_align|select_expert|moe.*rout", value):
|
|
return "moe_router"
|
|
if re.search(
|
|
r"nccl|all_?reduce|reduce_scatter|all_?gather|custom.*all.*reduce", value
|
|
):
|
|
return "collective"
|
|
if re.search(
|
|
r"flash|fmha|paged_attention|unified_attention|attention|"
|
|
r"reshape_and_cache", value
|
|
):
|
|
return "attention"
|
|
if re.search(r"fused_moe|moe.*gemm|nvjet", value):
|
|
return "moe_gemm"
|
|
if re.search(
|
|
r"sampling|(?<!moe_)topk|topp|argmax|multinomial|logits|penalty", value
|
|
):
|
|
return "sampler"
|
|
if re.search(r"gemm|matmul|cutlass", value):
|
|
return "dense_gemm"
|
|
if re.search(
|
|
r"rms|layer.?norm|activation|residual|elementwise|reduce|fill|silu", value
|
|
):
|
|
return "norm_elementwise"
|
|
if re.search(r"cache|swap|memcpy|memset", value):
|
|
return "kv_memory"
|
|
return "other"
|
|
|
|
|
|
def trace_analysis(stage: str) -> dict[str, Any]:
|
|
traces = list((RUN_ROOT / stage / "gpu0/traces").glob("*.pt.trace.json*"))
|
|
if len(traces) != 1:
|
|
raise RuntimeError(f"{stage}: expected one trace")
|
|
trace = traces[0]
|
|
opener = gzip.open if trace.suffix == ".gz" else open
|
|
with opener(trace, "rt", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
durations: defaultdict[str, float] = defaultdict(float)
|
|
kernel_count = 0
|
|
steps = set()
|
|
executes = 0
|
|
for event in data.get("traceEvents", []):
|
|
name = str(event.get("name", ""))
|
|
match = re.fullmatch(r"ProfilerStep#(\d+)", name)
|
|
if match:
|
|
steps.add(int(match.group(1)))
|
|
if name.startswith("execute_") and event.get("cat") == "user_annotation":
|
|
executes += 1
|
|
if event.get("cat") == "kernel" and float(event.get("dur", 0)) >= 0:
|
|
kernel_count += 1
|
|
durations[classify_kernel(name)] += float(event.get("dur", 0))
|
|
total = sum(durations.values())
|
|
shares = {
|
|
family: duration / total for family, duration in sorted(durations.items())
|
|
}
|
|
result = {
|
|
"schema": SCHEMA,
|
|
"stage": stage,
|
|
"trace_file": trace.name,
|
|
"trace_sha256": sha256_file(trace),
|
|
"trace_loadable": True,
|
|
"kernel_count": kernel_count,
|
|
"profiler_steps": sorted(steps),
|
|
"execute_annotations": executes,
|
|
"duration_us": dict(sorted(durations.items())),
|
|
"shares": shares,
|
|
"classifiable_fraction": 1.0 - shares.get("other", 0.0),
|
|
"valid": (
|
|
kernel_count > 0
|
|
and steps == set(range(2, 10))
|
|
and executes == 8
|
|
and 1.0 - shares.get("other", 0.0) >= 0.70
|
|
),
|
|
}
|
|
return result
|
|
|
|
|
|
def compare_regime(prefix: str) -> dict[str, Any]:
|
|
solo_result = json.loads(
|
|
(RUN_ROOT / "solo-moderate/gpu0/client/result.json").read_text()
|
|
)
|
|
coloc_result = json.loads(
|
|
(RUN_ROOT / f"{prefix}-moderate/gpu0/client/result.json").read_text()
|
|
)
|
|
solo_trace = trace_analysis("solo-moderate")
|
|
coloc_trace = trace_analysis(f"{prefix}-moderate")
|
|
solo_t = float(solo_result["clean"]["completed_throughput_rps"])
|
|
coloc_t = float(coloc_result["clean"]["completed_throughput_rps"])
|
|
top3 = sorted(
|
|
solo_trace["shares"], key=solo_trace["shares"].get, reverse=True
|
|
)[:3]
|
|
share_deltas = {
|
|
family: abs(
|
|
coloc_trace["shares"].get(family, 0)
|
|
- solo_trace["shares"].get(family, 0)
|
|
)
|
|
for family in top3
|
|
}
|
|
throughput_delta = abs(coloc_t / solo_t - 1)
|
|
return {
|
|
"schema": SCHEMA,
|
|
"regime": prefix,
|
|
"solo_throughput_rps": solo_t,
|
|
"colocated_throughput_rps": coloc_t,
|
|
"throughput_delta_fraction": throughput_delta,
|
|
"top3_solo_families": top3,
|
|
"solo_operator_shares": {
|
|
family: solo_trace["shares"][family] for family in top3
|
|
},
|
|
"colocated_operator_shares": {
|
|
family: coloc_trace["shares"].get(family, 0) for family in top3
|
|
},
|
|
"operator_share_delta_fraction": share_deltas,
|
|
"solo_trace": solo_trace,
|
|
"colocated_trace": coloc_trace,
|
|
"pass": (
|
|
solo_trace["valid"]
|
|
and coloc_trace["valid"]
|
|
and throughput_delta < 0.03
|
|
and all(delta < 0.03 for delta in share_deltas.values())
|
|
),
|
|
}
|
|
|
|
|
|
def numeric_sanity(values: list[float]) -> dict[str, Any]:
|
|
return {
|
|
"n": len(values),
|
|
"finite_n": sum(math.isfinite(value) for value in values),
|
|
"missing_n": sum(not math.isfinite(value) for value in values),
|
|
"min": min(values) if values else None,
|
|
"max": max(values) if values else None,
|
|
"distinct_n": len(set(values)),
|
|
}
|
|
|
|
|
|
def write_analysis(comparisons: list[dict[str, Any]]) -> dict[str, Any]:
|
|
accepted = next((c["regime"] for c in comparisons if c["pass"]), None)
|
|
throughputs = [
|
|
value
|
|
for c in comparisons
|
|
for value in (c["solo_throughput_rps"], c["colocated_throughput_rps"])
|
|
]
|
|
deltas = [
|
|
delta
|
|
for c in comparisons
|
|
for delta in c["operator_share_delta_fraction"].values()
|
|
]
|
|
result = {
|
|
"schema": SCHEMA,
|
|
"comparisons": comparisons,
|
|
"verdict": (
|
|
f"{accepted.replace('coloc-', '')}-way"
|
|
if accepted is not None
|
|
else "STOP"
|
|
),
|
|
"sanity": {
|
|
"throughput_rps": numeric_sanity(throughputs),
|
|
"throughput_delta": numeric_sanity(
|
|
[c["throughput_delta_fraction"] for c in comparisons]
|
|
),
|
|
"operator_share_delta": numeric_sanity(deltas),
|
|
"invariants": {
|
|
"throughputs_positive": all(x > 0 for x in throughputs),
|
|
"share_deltas_in_0_1": all(0 <= x <= 1 for x in deltas),
|
|
"top3_exact": all(
|
|
len(c["top3_solo_families"]) == 3 for c in comparisons
|
|
),
|
|
"traces_valid": all(
|
|
c["solo_trace"]["valid"] and c["colocated_trace"]["valid"]
|
|
for c in comparisons
|
|
),
|
|
},
|
|
},
|
|
}
|
|
atomic_json(RUN_ROOT / "colocation-analysis.json", result)
|
|
return result
|
|
|
|
|
|
def clean_recorded_processes(state: dict[str, Any]) -> None:
|
|
for stage in state.get("stages", {}).values():
|
|
if stage.get("status") == "complete":
|
|
continue
|
|
pgids = [
|
|
entry.get("pgid")
|
|
for group in ("clients", "servers")
|
|
for entry in stage.get(group, {}).values()
|
|
if entry.get("pgid")
|
|
]
|
|
for pgid in pgids:
|
|
try:
|
|
os.killpg(int(pgid), signal.SIGINT)
|
|
except ProcessLookupError:
|
|
pass
|
|
if pgids:
|
|
time.sleep(5)
|
|
|
|
|
|
def execute(resume: bool) -> None:
|
|
RUN_ROOT.mkdir(parents=True, exist_ok=True)
|
|
state = load_state(resume)
|
|
if resume:
|
|
clean_recorded_processes(state)
|
|
fingerprint = make_fingerprint()
|
|
if state["fingerprint"] and state["fingerprint"] != fingerprint:
|
|
raise RuntimeError("resume fingerprint differs from frozen execution")
|
|
state["fingerprint"] = fingerprint
|
|
state["status"] = "running"
|
|
save_state(state)
|
|
ensure_provenance()
|
|
ensure_manifests()
|
|
run_stage(state, "solo-saturation", {}, "saturation", None, False)
|
|
run_stage(
|
|
state,
|
|
"solo-moderate",
|
|
{},
|
|
"moderate",
|
|
"solo-saturation",
|
|
True,
|
|
)
|
|
run_stage(state, "coloc-8-saturation", BACKGROUND_8, "saturation", None, False)
|
|
run_stage(
|
|
state,
|
|
"coloc-8-moderate",
|
|
BACKGROUND_8,
|
|
"moderate",
|
|
"coloc-8-saturation",
|
|
True,
|
|
)
|
|
comparisons = [compare_regime("coloc-8")]
|
|
analysis = write_analysis(comparisons)
|
|
if not analysis["comparisons"][0]["pass"]:
|
|
run_stage(
|
|
state, "coloc-4-saturation", BACKGROUND_4, "saturation", None, False
|
|
)
|
|
run_stage(
|
|
state,
|
|
"coloc-4-moderate",
|
|
BACKGROUND_4,
|
|
"moderate",
|
|
"coloc-4-saturation",
|
|
True,
|
|
)
|
|
comparisons.append(compare_regime("coloc-4"))
|
|
analysis = write_analysis(comparisons)
|
|
state["status"] = "complete" if analysis["verdict"] != "STOP" else "gate_failed"
|
|
state["verdict"] = analysis["verdict"]
|
|
state["completed_at"] = time.time()
|
|
save_state(state)
|
|
print(json.dumps(analysis, sort_keys=True), flush=True)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
run = sub.add_parser("run")
|
|
run.add_argument("--resume", action="store_true")
|
|
sub.add_parser("status")
|
|
sub.add_parser("analyze")
|
|
sub.add_parser("plan")
|
|
args = parser.parse_args()
|
|
if args.command == "run":
|
|
execute(args.resume)
|
|
elif args.command == "status":
|
|
print(STATE.read_text() if STATE.exists() else '{"status":"absent"}')
|
|
elif args.command == "analyze":
|
|
comparisons = [compare_regime("coloc-8")]
|
|
if (RUN_ROOT / "coloc-4-moderate/stage-complete.json").exists():
|
|
comparisons.append(compare_regime("coloc-4"))
|
|
print(json.dumps(write_analysis(comparisons), sort_keys=True, indent=2))
|
|
else:
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"target": "P05/C00 GPU0",
|
|
"background_8": BACKGROUND_8,
|
|
"background_4": BACKGROUND_4,
|
|
"cpu_map": CPU_MAP,
|
|
"stages": [
|
|
"solo-saturation",
|
|
"solo-moderate",
|
|
"coloc-8-saturation",
|
|
"coloc-8-moderate",
|
|
"conditional coloc-4 saturation/moderate",
|
|
],
|
|
},
|
|
sort_keys=True,
|
|
indent=2,
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|