File size: 9,312 Bytes
ee4da68 33df54a ee4da68 33df54a ee4da68 33df54a ee4da68 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | # SecureCoder v2 pipeline supervisor (stdlib only).
#
# Waits for the v2 training job to leave the active set, then chains the merge
# job and the GGUF quantise job so the pipeline finishes unattended.
#
# HF_TOKEN - fine-grained token with job.write + repo.write
# TRAIN_JOB - id of the training job to wait on (required unless SKIP_MERGE=1)
# QUANT - GGUF quantisation to produce (default Q4_K_M)
#
# Changes vs the original supervisor:
# * BUG FIX: the SKIP_MERGE=1 path no longer dies with UnboundLocalError when
# logging the final summary. Every step's outcome is seeded in RESULT up
# front, so the summary can never reference an unbound name.
# * TRAIN_JOB is validated before polling instead of hitting a malformed URL.
# * api()/repo_ok() retry transient network errors, 5xx and 429 with backoff.
# * stage_of() treats a not-yet-visible job as NOT_FOUND instead of raising,
# which matters right after launch().
# * Failures are reported as clean log lines + exit codes, not tracebacks.
import json
import os
import time
import urllib.error
import urllib.request
HUB = "https://huggingface.co"
NS = "Taimwe"
TOKEN = os.environ.get("HF_TOKEN", "")
MERGE_REPO = os.environ.get("MERGE_REPO", "Taimwe/securecoder-30b-pro-v2-merged")
TRAIN_JOB = os.environ.get("TRAIN_JOB", "")
ADAPTER = os.environ.get("ADAPTER_REPO", "Taimwe/securecoder-30b-pro-v2")
MERGED = MERGE_REPO
GGUF = os.environ.get("GGUF_REPO", "Taimwe/securecoder-30b-pro-v2-GGUF")
IMAGE = "ghcr.io/astral-sh/uv:python3.12-bookworm"
ACTIVE = {"CREATED", "SCHEDULING", "RUNNING", "QUEUED", "PENDING"}
OK = {"COMPLETED", "SUCCEEDED"}
SKIP_MERGE = os.environ.get("SKIP_MERGE", "0") == "1"
QUANT = os.environ.get("QUANT", "Q4_K_M")
RETRY_ATTEMPTS = int(os.environ.get("RETRY_ATTEMPTS", "4"))
# Seeded before any work starts - this is what makes the summary crash-proof.
RESULT = {"train": "NOT_RUN", "merge": "NOT_RUN", "gguf": "NOT_RUN",
"adapter": ADAPTER, "merged": MERGED, "gguf_repo": GGUF, "quant": QUANT}
# Explicit dependencies for the child jobs. `uv run --no-project` was the cause of a
# silent 3-second merge failure: --no-project suppresses PEP 723 inline metadata, so torch
# and friends were never installed. Keep in sync with the `# dependencies = [...]` header
# of merge_securecoder.py / quantise_securecoder.py.
DEPS = ["--with", "torch", "--with", "transformers", "--with", "peft",
"--with", "huggingface_hub", "--with", "accelerate",
"--with", "safetensors", "--with", "unsloth"]
def log(msg):
print("[supervisor %s] %s" % (time.strftime("%H:%M:%S"), msg), flush=True)
def _backoff(attempt):
return min(60, 5 * 2 ** (attempt - 1))
def api(path, payload=None):
"""Call the HF API, retrying transient failures with exponential backoff."""
data = json.dumps(payload).encode() if payload is not None else None
last = "unknown"
for attempt in range(1, RETRY_ATTEMPTS + 1):
headers = {"Authorization": "Bearer " + TOKEN}
if data is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(
HUB + path, data=data, headers=headers,
method="POST" if data is not None else "GET")
try:
with urllib.request.urlopen(req, timeout=120) as resp:
return json.loads(resp.read().decode() or "null")
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", "replace")[:300]
last = "HTTP %s %s %s" % (exc.code, exc.reason, body)
if exc.code < 500 and exc.code != 429:
raise RuntimeError("%s -> %s" % (path, last))
except (urllib.error.URLError, TimeoutError, OSError) as exc:
last = "%s: %s" % (type(exc).__name__, exc)
if attempt < RETRY_ATTEMPTS:
wait = _backoff(attempt)
log("retry %d/%d in %ds (%s)" % (attempt, RETRY_ATTEMPTS, wait, last))
time.sleep(wait)
raise RuntimeError("%s failed after %d attempts -> %s" % (path, RETRY_ATTEMPTS, last))
def stage_of(job_id):
"""Current stage of a job. A job that is not visible yet is NOT_FOUND."""
try:
return api("/api/jobs/%s/%s" % (NS, job_id))["status"]["stage"]
except RuntimeError as exc:
if "HTTP 404" in str(exc):
return "NOT_FOUND"
raise
def repo_ok(repo):
"""True if the model repo exists. 404 -> False; transient errors -> retried."""
for attempt in range(1, RETRY_ATTEMPTS + 1):
try:
req = urllib.request.Request(
HUB + "/api/models/" + repo,
headers={"Authorization": "Bearer " + TOKEN})
with urllib.request.urlopen(req, timeout=60) as resp:
return resp.status == 200
except urllib.error.HTTPError as exc:
if exc.code == 404:
return False
if (exc.code >= 500 or exc.code == 429) and attempt < RETRY_ATTEMPTS:
time.sleep(_backoff(attempt))
continue
raise RuntimeError("repo_ok(%s) -> HTTP %s" % (repo, exc.code))
except (urllib.error.URLError, TimeoutError, OSError):
if attempt == RETRY_ATTEMPTS:
raise
time.sleep(_backoff(attempt))
return False
def wait_for(job_id, label, budget_s):
"""Poll until the job leaves the active set, logging only on change."""
deadline = time.time() + budget_s
seen = None
while True:
stage = stage_of(job_id)
if stage != seen:
log("%s %s stage=%s" % (label, job_id, stage))
seen = stage
if stage not in ACTIVE:
return stage
if time.time() > deadline:
log("%s %s exceeded its %ds budget" % (label, job_id, budget_s))
return "TIMEOUT"
time.sleep(60)
def launch(command, flavor, timeout_s, name):
# The secret must carry the real token value: an empty string is injected
# literally and every push/upload then fails with 401.
job = api("/api/jobs/" + NS, {
"flavor": flavor,
"dockerImage": IMAGE,
"command": command,
"arguments": [],
"environment": {},
"secrets": {"HF_TOKEN": TOKEN},
"timeoutSeconds": timeout_s,
"labels": {"name": name},
})
log("launched %s %s (%s)" % (name, job["id"], flavor))
return job["id"]
def main():
if not TOKEN.strip():
log("HF_TOKEN is empty - set it to a token with job.write + repo.write")
return 2
if SKIP_MERGE:
log("merge already done upstream; going straight to GGUF")
if not repo_ok(MERGED):
log("merged repo %s missing; stopping" % MERGED)
RESULT["merge"] = "MISSING_REPO"
log("SUMMARY " + json.dumps(RESULT))
return 1
log("merged repo %s is live" % MERGED)
RESULT["merge"] = "SKIPPED"
else:
if not TRAIN_JOB.strip():
log("TRAIN_JOB is required unless SKIP_MERGE=1")
return 2
log("watching training job %s" % TRAIN_JOB)
final = wait_for(TRAIN_JOB, "train", 6 * 3600)
RESULT["train"] = final
log("training finished stage=%s" % final)
if final not in OK:
log("training did not complete cleanly; not chaining")
log("SUMMARY " + json.dumps(RESULT))
return 1
if not repo_ok(ADAPTER):
log("adapter %s missing after training; stopping" % ADAPTER)
log("SUMMARY " + json.dumps(RESULT))
return 1
log("adapter %s is live" % ADAPTER)
merge_job = launch(
["uv", "run"] + DEPS + [
HUB + "/" + NS + "/securecoder-scripts/resolve/main/merge_securecoder.py",
"--adapter", ADAPTER, "--output-repo", MERGED],
"a100-large", 3 * 3600, "securecoder-v2-merge")
merge_final = wait_for(merge_job, "merge", 4 * 3600)
RESULT["merge"] = merge_final
log("merge finished stage=%s" % merge_final)
if merge_final not in OK or not repo_ok(MERGED):
log("merge failed or repo missing; skipping GGUF")
log("SUMMARY " + json.dumps(RESULT))
return 1
log("merged repo %s is live" % MERGED)
gguf_job = launch(
["uv", "run"] + DEPS + [
HUB + "/" + NS + "/securecoder-scripts/resolve/main/quantise_securecoder.py",
"--merged-repo", MERGED, "--gguf-repo", GGUF, "--quant", QUANT],
"cpu-performance", 6 * 3600, "securecoder-v2-gguf")
gguf_final = wait_for(gguf_job, "gguf", 7 * 3600)
RESULT["gguf"] = gguf_final
if repo_ok(GGUF):
log("PIPELINE COMPLETE gguf=%s merged=%s quant=%s" % (GGUF, MERGED, QUANT))
else:
log("PIPELINE ENDED but %s not found" % GGUF)
log("SUMMARY " + json.dumps(RESULT))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except RuntimeError as exc:
log("FATAL %s" % exc)
log("SUMMARY " + json.dumps(RESULT))
raise SystemExit(1) |