review round 3 (probe/model-card/eval): rank-visible eval failure + cross-rank peak memory + complete run_summary; card derived-and-asserted from config; harness install --no-deps and task ids confirmed; torchrun redirects 3 + tee 3
Browse files- kernels/p4_stop_probe.py +38 -8
kernels/p4_stop_probe.py
CHANGED
|
@@ -60,19 +60,22 @@ ROOT, RUN = "/kaggle/working/mixroot", "/kaggle/working/run"
|
|
| 60 |
# first run of this probe failed at, in 8.6 seconds, on a malformed torchrun command line (E-037). `soak`
|
| 61 |
# is the 180-step memory question, and it is only worth 1.6 GPU-hours once the mechanics are known to work.
|
| 62 |
MODE = os.environ.get("P4_PROBE_MODE", "soak")
|
|
|
|
|
|
|
|
|
|
| 63 |
TLOG = "/kaggle/working/tlogs"
|
| 64 |
# torchrun takes its first positional as the SCRIPT, not a command: passing sys.executable made
|
| 65 |
# it compile the Python binary (E-037). --redirects is a bitmask into the --log-dir per rank:
|
| 66 |
# 2 = redirect only stderr, so rank 1's traceback lands in a file while stdout (the loss curve,
|
| 67 |
# which §5 requires monitoring on every wake) still streams live to this log. 3 would file both.
|
| 68 |
-
TORCHRUN = ["torchrun", "--nproc_per_node=2", "--redirects", "
|
| 69 |
TPS = 262144 # the run's real step shape, in tokens
|
| 70 |
if MODE == "smoke":
|
| 71 |
STEPS, PUSH_EVERY, STOP1, VAL = 20, 10, 10, 200000
|
| 72 |
-
T_LEG1, T_LEG2, T_FRESH =
|
| 73 |
else:
|
| 74 |
STEPS, PUSH_EVERY, STOP1, VAL = 180, 60, 120, 2000000
|
| 75 |
-
T_LEG1, T_LEG2, T_FRESH =
|
| 76 |
TOKENS = STEPS * TPS
|
| 77 |
GATE_PEAK = (MODE != "smoke") # 20 steps says nothing about allocator drift
|
| 78 |
T0 = time.time()
|
|
@@ -83,6 +86,8 @@ def run(argv, label, timeout):
|
|
| 83 |
t0 = time.time()
|
| 84 |
e = dict(os.environ); e["PYTHONPATH"] = "/kaggle/working"
|
| 85 |
e["PYTHONUNBUFFERED"] = "1" # the -u that torchrun cannot carry
|
|
|
|
|
|
|
| 86 |
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
| 87 |
env=e, bufsize=1, start_new_session=True)
|
| 88 |
killed = []
|
|
@@ -94,9 +99,19 @@ def run(argv, label, timeout):
|
|
| 94 |
except Exception:
|
| 95 |
p.kill()
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
timer = threading.Timer(timeout, _kill)
|
| 98 |
timer.daemon = True
|
| 99 |
timer.start()
|
|
|
|
|
|
|
|
|
|
| 100 |
keep, lines = [], []
|
| 101 |
try:
|
| 102 |
for line in p.stdout:
|
|
@@ -110,6 +125,7 @@ def run(argv, label, timeout):
|
|
| 110 |
del keep[:-40]
|
| 111 |
finally:
|
| 112 |
timer.cancel()
|
|
|
|
| 113 |
rc = p.wait()
|
| 114 |
if killed:
|
| 115 |
print(" TIMEOUT after %d s" % timeout, flush=True)
|
|
@@ -148,9 +164,12 @@ def diag(tag):
|
|
| 148 |
"""torchrun's ChildFailedError said `error_file: <N/A>` and printed no traceback, which is useless for
|
| 149 |
a rank-1-only failure. --redirects writes each rank's own stdout/stderr into the log dir, so print
|
| 150 |
those on a failed leg: the exception is in there, and guessing at it costs a session."""
|
|
|
|
|
|
|
| 151 |
rc, out = run(["bash", "-c",
|
| 152 |
-
'
|
| 153 |
-
'
|
|
|
|
| 154 |
return out
|
| 155 |
|
| 156 |
|
|
@@ -198,10 +217,16 @@ res = {
|
|
| 198 |
"tokens_consumed": rj1.get("tokens_consumed"), "tok_per_s": rj1.get("tok_per_s"),
|
| 199 |
"params": rj1.get("params"), "peak_gpu_gb": rj1.get("peak_gpu_gb"),
|
| 200 |
"tok_per_s": rj1.get("tok_per_s"), "final_loss": rj1.get("final_loss"),
|
|
|
|
|
|
|
|
|
|
| 201 |
"pushed": pushed, "pointer": {k: ptr.get(k) for k in ("step", "path_in_repo")}},
|
| 202 |
"leg2": {"final_step": rj2.get("final_step"), "segment_stop": rj2.get("segment_stop"),
|
| 203 |
"tokens_consumed": rj2.get("tokens_consumed"), "val_ppl": rj2.get("val_ppl"),
|
| 204 |
-
"peak_gpu_gb": rj2.get("peak_gpu_gb"), "
|
|
|
|
|
|
|
|
|
|
| 205 |
"final_loss": rj2.get("final_loss"),
|
| 206 |
"pointer_after": {k: ptr2.get(k) for k in ("step", "path_in_repo")},
|
| 207 |
"has_final": any(k.startswith("final/") for k in listed2)},
|
|
@@ -229,8 +254,13 @@ res["checks"] = {
|
|
| 229 |
# 13.6 GB of ~14.56 usable leaves the run room for a fragmentation spike and for the two CUDA contexts;
|
| 230 |
# above that the answer to the user's question is "not on this hardware", not "keep going". Reported in
|
| 231 |
# both modes, gated only in the soak: a 20-step peak is not evidence about 3,814 steps.
|
| 232 |
-
|
| 233 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
"no_nan_and_loss_moved": (rj1.get("final_loss") or 1e9) < 11.0
|
| 235 |
and (rj2.get("final_loss") or 1e9) < 11.0,
|
| 236 |
}
|
|
|
|
| 60 |
# first run of this probe failed at, in 8.6 seconds, on a malformed torchrun command line (E-037). `soak`
|
| 61 |
# is the 180-step memory question, and it is only worth 1.6 GPU-hours once the mechanics are known to work.
|
| 62 |
MODE = os.environ.get("P4_PROBE_MODE", "soak")
|
| 63 |
+
if MODE not in ("smoke", "soak"):
|
| 64 |
+
# A typo here would otherwise run the 1.6-hour soak when a 13-minute smoke was asked for.
|
| 65 |
+
raise SystemExit("P4_PROBE_MODE must be smoke or soak, got %r" % MODE)
|
| 66 |
TLOG = "/kaggle/working/tlogs"
|
| 67 |
# torchrun takes its first positional as the SCRIPT, not a command: passing sys.executable made
|
| 68 |
# it compile the Python binary (E-037). --redirects is a bitmask into the --log-dir per rank:
|
| 69 |
# 2 = redirect only stderr, so rank 1's traceback lands in a file while stdout (the loss curve,
|
| 70 |
# which §5 requires monitoring on every wake) still streams live to this log. 3 would file both.
|
| 71 |
+
TORCHRUN = ["torchrun", "--nproc_per_node=2", "--redirects", "3", "--tee", "3", "--log-dir", TLOG]
|
| 72 |
TPS = 262144 # the run's real step shape, in tokens
|
| 73 |
if MODE == "smoke":
|
| 74 |
STEPS, PUSH_EVERY, STOP1, VAL = 20, 10, 10, 200000
|
| 75 |
+
T_LEG1, T_LEG2, T_FRESH = 1200, 900, 1500
|
| 76 |
else:
|
| 77 |
STEPS, PUSH_EVERY, STOP1, VAL = 180, 60, 120, 2000000
|
| 78 |
+
T_LEG1, T_LEG2, T_FRESH = 4800, 2700, 1500
|
| 79 |
TOKENS = STEPS * TPS
|
| 80 |
GATE_PEAK = (MODE != "smoke") # 20 steps says nothing about allocator drift
|
| 81 |
T0 = time.time()
|
|
|
|
| 86 |
t0 = time.time()
|
| 87 |
e = dict(os.environ); e["PYTHONPATH"] = "/kaggle/working"
|
| 88 |
e["PYTHONUNBUFFERED"] = "1" # the -u that torchrun cannot carry
|
| 89 |
+
e["NCCL_DEBUG"] = "WARN" # a rank that dies in a collective says so here and nowhere else
|
| 90 |
+
e["TORCH_CPP_LOG_LEVEL"] = "WARNING"
|
| 91 |
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
| 92 |
env=e, bufsize=1, start_new_session=True)
|
| 93 |
killed = []
|
|
|
|
| 99 |
except Exception:
|
| 100 |
p.kill()
|
| 101 |
|
| 102 |
+
def _hard():
|
| 103 |
+
killed.append(True)
|
| 104 |
+
try:
|
| 105 |
+
os.killpg(os.getpgid(p.pid), signal.SIGKILL)
|
| 106 |
+
except Exception:
|
| 107 |
+
p.kill()
|
| 108 |
+
|
| 109 |
timer = threading.Timer(timeout, _kill)
|
| 110 |
timer.daemon = True
|
| 111 |
timer.start()
|
| 112 |
+
hard = threading.Timer(timeout + 90, _hard)
|
| 113 |
+
hard.daemon = True
|
| 114 |
+
hard.start()
|
| 115 |
keep, lines = [], []
|
| 116 |
try:
|
| 117 |
for line in p.stdout:
|
|
|
|
| 125 |
del keep[:-40]
|
| 126 |
finally:
|
| 127 |
timer.cancel()
|
| 128 |
+
hard.cancel()
|
| 129 |
rc = p.wait()
|
| 130 |
if killed:
|
| 131 |
print(" TIMEOUT after %d s" % timeout, flush=True)
|
|
|
|
| 164 |
"""torchrun's ChildFailedError said `error_file: <N/A>` and printed no traceback, which is useless for
|
| 165 |
a rank-1-only failure. --redirects writes each rank's own stdout/stderr into the log dir, so print
|
| 166 |
those on a failed leg: the exception is in there, and guessing at it costs a session."""
|
| 167 |
+
# v4's diag found no *.log at all, so list the tree as well as tailing it: if torchrun named the
|
| 168 |
+
# files something else, this says so instead of printing nothing and leaving me guessing.
|
| 169 |
rc, out = run(["bash", "-c",
|
| 170 |
+
'ls -R "%s" 2>&1 | head -40; '
|
| 171 |
+
'for f in $(find "%s" -type f 2>/dev/null | head -8); do echo "==== $f"; '
|
| 172 |
+
'tail -70 "$f"; done' % (TLOG, TLOG)], "DIAG_" + tag, 180)
|
| 173 |
return out
|
| 174 |
|
| 175 |
|
|
|
|
| 217 |
"tokens_consumed": rj1.get("tokens_consumed"), "tok_per_s": rj1.get("tok_per_s"),
|
| 218 |
"params": rj1.get("params"), "peak_gpu_gb": rj1.get("peak_gpu_gb"),
|
| 219 |
"tok_per_s": rj1.get("tok_per_s"), "final_loss": rj1.get("final_loss"),
|
| 220 |
+
"peak_alloc_gb": rj1.get("peak_gpu_gb"), "peak_alloc_gb_max_rank":
|
| 221 |
+
rj1.get("peak_gpu_gb_max_rank"), "peak_reserved_gb_max_rank":
|
| 222 |
+
rj1.get("peak_reserved_gb_max_rank"), "val_error": rj1.get("val_error"),
|
| 223 |
"pushed": pushed, "pointer": {k: ptr.get(k) for k in ("step", "path_in_repo")}},
|
| 224 |
"leg2": {"final_step": rj2.get("final_step"), "segment_stop": rj2.get("segment_stop"),
|
| 225 |
"tokens_consumed": rj2.get("tokens_consumed"), "val_ppl": rj2.get("val_ppl"),
|
| 226 |
+
"peak_gpu_gb": rj2.get("peak_gpu_gb"), "peak_gpu_gb_max_rank":
|
| 227 |
+
rj2.get("peak_gpu_gb_max_rank"), "peak_reserved_gb_max_rank":
|
| 228 |
+
rj2.get("peak_reserved_gb_max_rank"), "val_error": rj2.get("val_error"),
|
| 229 |
+
"tok_per_s": rj2.get("tok_per_s"),
|
| 230 |
"final_loss": rj2.get("final_loss"),
|
| 231 |
"pointer_after": {k: ptr2.get(k) for k in ("step", "path_in_repo")},
|
| 232 |
"has_final": any(k.startswith("final/") for k in listed2)},
|
|
|
|
| 254 |
# 13.6 GB of ~14.56 usable leaves the run room for a fragmentation spike and for the two CUDA contexts;
|
| 255 |
# above that the answer to the user's question is "not on this hardware", not "keep going". Reported in
|
| 256 |
# both modes, gated only in the soak: a 20-step peak is not evidence about 3,814 steps.
|
| 257 |
+
# rank 0's allocated peak is not the constraint: reserved bytes, the CUDA contexts and the *other*
|
| 258 |
+
# rank's peak are what an OOM is actually about, so the max across ranks is what gets gated.
|
| 259 |
+
"peak_memory_below_13_6_gb": (not GATE_PEAK) or (
|
| 260 |
+
(rj1.get("peak_reserved_gb_max_rank") or rj1.get("peak_gpu_gb_max_rank") or 99) <= 13.6
|
| 261 |
+
and (rj2.get("peak_reserved_gb_max_rank") or rj2.get("peak_gpu_gb_max_rank") or 99) <= 13.6),
|
| 262 |
+
"validation_actually_ran": (rj1.get("val_ppl") is not None and rj1.get("val_error") is None
|
| 263 |
+
and rj2.get("val_ppl") is not None and rj2.get("val_error") is None),
|
| 264 |
"no_nan_and_loss_moved": (rj1.get("final_loss") or 1e9) < 11.0
|
| 265 |
and (rj2.get("final_loss") or 1e9) < 11.0,
|
| 266 |
}
|