Download source/scripts/cluster/ngram_interactive_supervisor.py from khazic/spec-b300: direct link, hf CLI and curl.
- Browser
- Download file 16.2 kB
-
https://huggingface.co/khazic/spec-b300/resolve/main/source/scripts/cluster/ngram_interactive_supervisor.py
- Command line
-
hf download hf://khazic/spec-b300/source/scripts/cluster/ngram_interactive_supervisor.py
-
curl -L -o ngram_interactive_supervisor.py https://huggingface.co/khazic/spec-b300/resolve/main/source/scripts/cluster/ngram_interactive_supervisor.py
16.2 kB
| """Run full epochs across successive four-hour salloc allocations. | |
| This process runs OUTSIDE the GPU allocation. It never submits sbatch jobs, | |
| renews only after a TIMEOUT, and stops on cancellation or training failures. | |
| """ | |
| import argparse | |
| import fcntl | |
| import json | |
| import os | |
| import re | |
| import shlex | |
| import signal | |
| import subprocess | |
| import sys | |
| import time | |
| import uuid | |
| from pathlib import Path | |
| def write_json(path, value): | |
| temporary = path.with_suffix(".json.tmp") | |
| temporary.write_text(json.dumps(value, indent=2) + "\n") | |
| os.replace(temporary, path) | |
| class Supervisor: | |
| def __init__(self, repo, root, initial_job, epochs=1, | |
| wandb_entity="Nemo-automodel", | |
| wandb_project="speculators-ngram-candidates-validation"): | |
| if epochs < 1: | |
| raise ValueError("epochs must be positive") | |
| # Preserve the shared /lustre/fs1 spelling: this CPU node's canonical | |
| # mount alias need not exist on GPU nodes or on the login host. | |
| self.repo, self.root, self.job = repo.absolute(), root.absolute(), initial_job | |
| self.root.mkdir(parents=True, exist_ok=True) | |
| self.lock = (self.root / "supervisor.lock").open("a") | |
| fcntl.flock(self.lock, fcntl.LOCK_EX | fcntl.LOCK_NB) | |
| self.ssh = [ | |
| "ssh", | |
| "-o", | |
| "BatchMode=yes", | |
| "-o", | |
| "ConnectTimeout=10", | |
| "-o", | |
| "ServerAliveInterval=30", | |
| "-o", | |
| "ServerAliveCountMax=3", | |
| "-o", | |
| "StrictHostKeyChecking=no", | |
| "-o", | |
| "UserKnownHostsFile=/tmp/codex_known_hosts", | |
| ] | |
| self.login = "huiyingl@oci-hsg-cs-001-login-02" | |
| self.child = self.holder = self.maintenance = None | |
| self.source = self.root / "source" | |
| manifest_path = self.root / "manifest.json" | |
| if manifest_path.exists(): | |
| self.manifest = json.loads(manifest_path.read_text()) | |
| if (self.manifest["epochs"], self.manifest["wandb_entity"], | |
| self.manifest["wandb_project"]) != (epochs, wandb_entity, wandb_project): | |
| raise ValueError("Existing run configuration differs; use a new run root") | |
| else: | |
| if self.source.exists(): | |
| raise RuntimeError("Unowned source snapshot already exists") | |
| revision = subprocess.check_output( | |
| ["git", "rev-parse", "HEAD"], cwd=self.repo, text=True | |
| ).strip() | |
| run_name = self.root.name | |
| checkpoint = ( | |
| self.repo.parent | |
| / "model_weights" | |
| / "ngram_candidates_qwen3_6-35b-a3b-perfectblend" | |
| / run_name | |
| / "checkpoints" | |
| ) | |
| if checkpoint.exists(): | |
| raise RuntimeError("Use a new run name; checkpoint directory exists") | |
| subprocess.run( | |
| [ | |
| "git", | |
| "clone", | |
| "--no-hardlinks", | |
| "--no-checkout", | |
| "--quiet", | |
| str(self.repo), | |
| str(self.source), | |
| ], | |
| check=True, | |
| ) | |
| subprocess.run( | |
| ["git", "checkout", "--detach", "--quiet", revision], | |
| cwd=self.source, | |
| check=True, | |
| ) | |
| self.manifest = { | |
| "run_name": run_name, | |
| "source_revision": revision, | |
| "source_snapshot": str(self.source), | |
| "initial_job": initial_job, | |
| "checkpoint_root": str(checkpoint), | |
| "epochs": epochs, | |
| "data_path": str( | |
| self.repo.parent | |
| / "datasets/qwen3.6-35b-a3b/qwen3.6-35b-a3b_train_spec_len3072_fullvocab" | |
| ), | |
| "wandb_entity": wandb_entity, | |
| "wandb_project": wandb_project, | |
| "wandb_run_id": uuid.uuid4().hex[:8], | |
| "image": str( | |
| self.repo / "images/speculators-ngram-py312-vllm0.27.1-arm64.sqsh" | |
| ), | |
| "image_sha256": "9c0b5afa769aa3637f1470341d7725f3b752f72246d978e97a17bf793b74f368", | |
| "topology": "single node; GPU0 vLLM, GPU1-3 DDP", | |
| "checkpoint_fraction": 0.01, | |
| "retained_checkpoint_generations_per_epoch": 2, | |
| } | |
| write_json(manifest_path, self.manifest) | |
| (self.root / "supervisor.pid").write_text(str(os.getpid()) + "\n") | |
| for name in ("segments", "allocations", "maintenance"): | |
| (self.root / name).mkdir(exist_ok=True) | |
| def status(self, phase, **extra): | |
| value = { | |
| "time": time.time(), | |
| "phase": phase, | |
| "job_id": self.job, | |
| "supervisor_pid": os.getpid(), | |
| **extra, | |
| } | |
| write_json(self.root / "status.json", value) | |
| print(json.dumps(value), flush=True) | |
| def remote(self, argv, timeout=30): | |
| return subprocess.run( | |
| self.ssh + [self.login, shlex.join(argv)], | |
| text=True, | |
| capture_output=True, | |
| timeout=timeout, | |
| check=True, | |
| ).stdout.strip() | |
| def job_state(self): | |
| try: | |
| result = self.remote(["squeue", "-h", "-j", self.job, "-o", "%T"]) | |
| except subprocess.CalledProcessError: | |
| # squeue exits nonzero once an old job has left the controller; | |
| # sacct remains authoritative for completed allocations. | |
| result = "" | |
| queued = result.splitlines()[0] if result else "UNKNOWN" | |
| if queued not in {"COMPLETING", "UNKNOWN"}: | |
| return queued | |
| # Node cleanup can leave squeue in COMPLETING for several minutes, | |
| # while accounting already records the actual reason for termination. | |
| result = self.remote( | |
| ["sacct", "-n", "-P", "-X", "-j", self.job, "--format=State%40"] | |
| ) | |
| accounted = ( | |
| result.splitlines()[0].split("|")[0].split()[0] | |
| if result else "UNKNOWN" | |
| ) | |
| if accounted in {"RUNNING", "COMPLETING", "UNKNOWN"}: | |
| return queued | |
| return accounted | |
| def wait_for_allocation_exit(self): | |
| running_deadline = time.monotonic() + 60 | |
| while True: | |
| self.check_stop() | |
| try: | |
| state = self.job_state() | |
| except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error: | |
| # A transient SSH/accounting outage is not evidence of TIMEOUT. | |
| self.status("waiting_for_allocation_state", error=str(error)) | |
| time.sleep(15) | |
| continue | |
| if state not in {"RUNNING", "COMPLETING", "UNKNOWN"}: | |
| return state | |
| if state == "RUNNING" and time.monotonic() >= running_deadline: | |
| # A dead trainer in a healthy allocation is a real failure. | |
| return state | |
| self.status("waiting_for_allocation_state", allocation_state=state) | |
| time.sleep(15) | |
| def recover_existing_segment(self): | |
| if not (self.root / "segments" / self.job).exists(): | |
| return | |
| state = self.wait_for_allocation_exit() | |
| if not state.startswith("TIMEOUT"): | |
| raise RuntimeError( | |
| f"Existing segment allocation state {state}; not renewing or relaunching" | |
| ) | |
| self.status("expired", recovery=True) | |
| self.allocate() | |
| def environment(self, segment): | |
| m = self.manifest | |
| return { | |
| "REPO": str(self.source), | |
| "ROOT": str(self.repo.parent), | |
| "NGRAM_RUN_ROOT": str(self.root), | |
| "RUN_NAME": m["run_name"], | |
| "LOG_DIR": str(segment), | |
| "CHECKPOINT_DIR": m["checkpoint_root"], | |
| "DATA_DIR": m["data_path"], | |
| "WANDB_ENTITY": m["wandb_entity"], | |
| "WANDB_PROJECT": m["wandb_project"], | |
| "WANDB_RUN_ID": m["wandb_run_id"], | |
| "WANDB_RESUME": "allow", | |
| "WANDB_MODE": "online", | |
| "EPOCHS": str(m["epochs"]), | |
| } | |
| def container_command(self, segment, argv, cpus): | |
| environment = self.environment(segment) | |
| outer = { | |
| **os.environ, | |
| "REPO": str(self.source), | |
| "NGRAM_STEP_CPUS": str(cpus), | |
| "NGRAM_CONTAINER_IMAGE": self.manifest["image"], | |
| "NGRAM_CONTAINER_NAME": "ngram-candidates-vllm0271", | |
| } | |
| command = [ | |
| "bash", | |
| str(self.source / "scripts/cluster/ngram_container.sh"), | |
| self.job, | |
| "/usr/bin/env", | |
| *[f"{key}={value}" for key, value in environment.items()], | |
| *argv, | |
| ] | |
| return command, outer | |
| def start_segment(self): | |
| segment = self.root / "segments" / self.job | |
| segment.mkdir(exist_ok=False) | |
| command, environment = self.container_command( | |
| segment, | |
| ["bash", str(self.source / "scripts/cluster/ngram_full_segment.sh")], | |
| 72, | |
| ) | |
| with (segment / "controller.log").open("w") as output: | |
| self.child = subprocess.Popen( | |
| command, | |
| env=environment, | |
| stdout=output, | |
| stderr=subprocess.STDOUT, | |
| start_new_session=True, | |
| ) | |
| self.status("training", segment=str(segment), launcher_pid=self.child.pid) | |
| return segment | |
| def start_maintenance(self, segment, action): | |
| if self.maintenance is not None and self.maintenance.poll() is None: | |
| return | |
| script = self.source / "scripts/cluster/ngram_wandb_maintenance.py" | |
| runtime = self.source / "scripts/cluster/ngram_runtime_env.sh" | |
| command_text = ( | |
| f"source {shlex.quote(str(runtime))}; status=0; " | |
| f"/opt/speculators_venv/bin/python {shlex.quote(str(script))} {action} || status=$?; " | |
| 'task_cache="/tmp/${USER:-speculators}/speculators-cache"; ' | |
| 'if [[ -d "$task_cache" ]]; then mkdir -p "$ROOT/.cache/compiled/ngram-candidates"; ' | |
| 'rsync -a "$task_cache/" "$ROOT/.cache/compiled/ngram-candidates/"; fi; exit "$status"' | |
| ) | |
| command, environment = self.container_command( | |
| segment, ["bash", "-lc", command_text], 4 | |
| ) | |
| log = self.root / "maintenance" / f"{int(time.time())}-{self.job}-{action}.log" | |
| with log.open("w") as output: | |
| self.maintenance = subprocess.Popen( | |
| command, | |
| env=environment, | |
| stdout=output, | |
| stderr=subprocess.STDOUT, | |
| start_new_session=True, | |
| ) | |
| self.status( | |
| "training" if action == "sync" else "uploading", maintenance=str(log) | |
| ) | |
| def allocate(self): | |
| self.status("requesting_interactive") | |
| shares = self.remote(["sshare", "-n", "-p", "-u", "huiyingl"]) | |
| accounts = [] | |
| for line in shares.splitlines(): | |
| fields = line.split("|") | |
| if len(fields) > 6 and fields[1].strip() and fields[6].strip(): | |
| accounts.append((float(fields[6]), fields[0].strip())) | |
| if not accounts: | |
| raise RuntimeError("Could not resolve an eligible fairshare account") | |
| account = max(accounts)[1] | |
| request = self.root / "allocations" / str(int(time.time())) | |
| request.mkdir() | |
| allocation = [ | |
| "salloc", | |
| f"--account={account}", | |
| "--partition=batch", | |
| "--qos=interactive", | |
| "--nodes=1", | |
| "--ntasks=1", | |
| "--gpus-per-node=4", | |
| "--cpus-per-task=72", | |
| "--mem=0", | |
| "--time=04:00:00", | |
| "--job-name=ngram-candidates-full", | |
| "bash", | |
| str(self.source / "scripts/cluster/ngram_allocation_holder.sh"), | |
| str(request), | |
| ] | |
| write_json( | |
| request / "request.json", {"command": allocation, "account": account} | |
| ) | |
| with (request / "salloc.log").open("w") as output: | |
| self.holder = subprocess.Popen( | |
| self.ssh + ["-tt", self.login, shlex.join(allocation)], | |
| stdin=subprocess.DEVNULL, | |
| stdout=output, | |
| stderr=subprocess.STDOUT, | |
| start_new_session=True, | |
| ) | |
| while not (request / "job_id").exists(): | |
| self.check_stop() | |
| if self.holder.poll() is not None: | |
| raise RuntimeError( | |
| f"Interactive request exited {self.holder.returncode}; see {request}" | |
| ) | |
| time.sleep(15) | |
| self.job = (request / "job_id").read_text().strip() | |
| if not re.fullmatch(r"\d+", self.job): | |
| raise RuntimeError("Invalid allocation ID") | |
| self.status("interactive_allocated", allocation_request=str(request)) | |
| def check_stop(self): | |
| if (self.root / "STOP").exists(): | |
| raise KeyboardInterrupt("STOP file requested; do not renew") | |
| def run(self): | |
| self.recover_existing_segment() | |
| while True: | |
| self.check_stop() | |
| segment = self.start_segment() | |
| next_sync = time.monotonic() + 3600 | |
| while self.child.poll() is None: | |
| self.check_stop() | |
| if time.monotonic() >= next_sync: | |
| self.start_maintenance(segment, "sync") | |
| next_sync = time.monotonic() + 3600 | |
| time.sleep(15) | |
| result = self.child.returncode | |
| if (self.root / "training_complete.json").exists() and result == 0: | |
| if self.maintenance is not None: | |
| self.maintenance.wait(timeout=1200) | |
| self.start_maintenance(segment, "sync") | |
| sync_result = self.maintenance.wait(timeout=1200) | |
| self.start_maintenance(segment, "upload") | |
| upload_result = self.maintenance.wait(timeout=4200) | |
| if sync_result or upload_result: | |
| raise RuntimeError( | |
| "Training complete, but W&B final sync/upload failed" | |
| ) | |
| self.status("complete", checkpoint=self.manifest["checkpoint_root"]) | |
| return | |
| state = self.wait_for_allocation_exit() | |
| if not state.startswith("TIMEOUT"): | |
| raise RuntimeError( | |
| f"Training exited {result}, allocation state {state}; not renewing a failed/cancelled job" | |
| ) | |
| if self.maintenance is not None and self.maintenance.poll() is None: | |
| self.maintenance.wait(timeout=180) | |
| self.status("expired", launcher_exit_code=result) | |
| self.allocate() | |
| def stop_children(self): | |
| for child in (self.child, self.maintenance, self.holder): | |
| if child is not None and child.poll() is None: | |
| os.killpg(child.pid, signal.SIGTERM) | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--repo", type=Path, required=True) | |
| parser.add_argument("--run-root", type=Path, required=True) | |
| parser.add_argument("--initial-job", required=True) | |
| parser.add_argument("--epochs", type=int, default=1) | |
| parser.add_argument("--wandb-entity", default="Nemo-automodel") | |
| parser.add_argument("--wandb-project", default="speculators-ngram-candidates-validation") | |
| args = parser.parse_args() | |
| controller = Supervisor(args.repo, args.run_root, args.initial_job, args.epochs, | |
| args.wandb_entity, args.wandb_project) | |
| for sig in (signal.SIGTERM, signal.SIGINT): | |
| signal.signal( | |
| sig, lambda signum, frame: (_ for _ in ()).throw(KeyboardInterrupt()) | |
| ) | |
| try: | |
| controller.run() | |
| except BaseException as error: | |
| controller.status( | |
| "stopped" if isinstance(error, KeyboardInterrupt) else "failed", | |
| error=str(error), | |
| ) | |
| controller.stop_children() | |
| raise | |
| if __name__ == "__main__": | |
| main() | |