Cion-lab commited on
Commit
9119e1e
·
verified ·
1 Parent(s): 52c7981

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
Files changed (1) hide show
  1. 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", "2", "--log-dir", TLOG]
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 = 1500, 1200, 2400
73
  else:
74
  STEPS, PUSH_EVERY, STOP1, VAL = 180, 60, 120, 2000000
75
- T_LEG1, T_LEG2, T_FRESH = 4200, 3000, 2400
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
- 'for f in $(find "%s" -name "*.log" 2>/dev/null | head -8); do echo "==== $f"; '
153
- 'tail -60 "$f"; done' % TLOG], "DIAG_" + tag, 180)
 
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"), "tok_per_s": rj2.get("tok_per_s"),
 
 
 
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
- "peak_memory_below_13_6_gb": (not GATE_PEAK) or ((rj1.get("peak_gpu_gb") or 99) <= 13.6
233
- and (rj2.get("peak_gpu_gb") or 99) <= 13.6),
 
 
 
 
 
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
  }