Cion-lab commited on
Commit
182a8d9
·
verified ·
1 Parent(s): 3880d41

train: --benchmark N runs the same step with no Trainer, to attribute the P2 throughput gap

Browse files
Files changed (1) hide show
  1. train/train_ounce100m.py +72 -0
train/train_ounce100m.py CHANGED
@@ -160,6 +160,68 @@ class HubPush(TrainerCallback):
160
  return control
161
 
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  def main():
164
  ap = argparse.ArgumentParser()
165
  ap.add_argument("--root", default="/kaggle/working/mixroot")
@@ -187,6 +249,8 @@ def main():
187
  ap.add_argument("--push-every-steps", type=int, default=0, help="0 = only at 10% boundaries")
188
  ap.add_argument("--prune", action="store_true", help="delete local checkpoint after hub verify")
189
  ap.add_argument("--dry-run", action="store_true", help="build everything, train 0 steps")
 
 
190
  args = ap.parse_args()
191
 
192
  rank = int(os.environ.get("RANK", "0"))
@@ -284,6 +348,14 @@ def main():
284
  if not (90_000_000 <= pc["sum_numel"] <= 110_000_000) and not args.layers and not args.hidden:
285
  raise SystemExit(f"parameter count {pc['sum_numel']:,} is outside §2's 90-110M budget")
286
 
 
 
 
 
 
 
 
 
287
  # 10 checkpoints per run is §2's cadence; --push-every-steps overrides for the preflight.
288
  ckpt_every = args.push_every_steps or max(1, steps_planned // 10)
289
 
 
160
  return control
161
 
162
 
163
+ def run_benchmark(args, model, dataset, steps, world, seqs_per_step):
164
+ """The same data path, optimizer and fp16 recipe as the real run, with no Trainer in the loop.
165
+
166
+ Why this exists: P2 measured 5,105 tok/s for 22L/seq-1024 where probe p0c measured 11,062 tok/s for a
167
+ near-identical shape with a hand-rolled loop. The gap is either (a) the attention kernel, (b) per-step
168
+ framework overhead, or (c) a measurement artefact, and which one it is decides whether the main run
169
+ needs 27 h or 68 h. Guessing wrong in either direction is expensive, so measure both paths.
170
+ """
171
+ import torch.distributed as dist
172
+ from torch.nn.parallel import DistributedDataParallel as DDP
173
+ from torch.utils.data import DataLoader
174
+
175
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
176
+ device = torch.device("cuda", local_rank)
177
+ torch.cuda.set_device(device)
178
+ if world > 1 and not dist.is_initialized():
179
+ dist.init_process_group("nccl")
180
+ model = model.to(device)
181
+ net = DDP(model, device_ids=[local_rank]) if world > 1 else model
182
+
183
+ opt = torch.optim.AdamW(net.parameters(), lr=args.lr, betas=(0.9, 0.95),
184
+ weight_decay=0.1, eps=1e-8)
185
+ try:
186
+ scaler = torch.amp.GradScaler("cuda", enabled=True) # torch >= 2.4
187
+ except TypeError:
188
+ scaler = torch.cuda.amp.GradScaler(enabled=True) # older signature
189
+ # Same reader, same shuffle, no accumulation: the comparison against the Trainer cells is then
190
+ # per-step cost with the framework removed, and nothing else differs.
191
+ dl = DataLoader(dataset, batch_size=args.micro_batch, shuffle=False, num_workers=2,
192
+ pin_memory=False, drop_last=True)
193
+ t0 = time.time()
194
+ toks = 0
195
+ for i, batch in enumerate(dl):
196
+ if i >= steps:
197
+ break
198
+ batch = {k: v.to(device, non_blocking=True) for k, v in batch.items()}
199
+ opt.zero_grad(set_to_none=True)
200
+ with torch.autocast("cuda", dtype=torch.float16):
201
+ out = net(**batch)
202
+ scaler.scale(out.loss).backward()
203
+ scaler.unscale_(opt)
204
+ torch.nn.utils.clip_grad_norm_(net.parameters(), 1.0)
205
+ scaler.step(opt)
206
+ scaler.update()
207
+ toks += batch["input_ids"].numel()
208
+ if i == 0:
209
+ torch.cuda.synchronize()
210
+ t0 = time.time() # exclude CUDA context + first-trace cost from the rate
211
+ toks = 0
212
+ sec = time.time() - t0
213
+ res = {"benchmark_steps": i + 1 if steps else 0, "tok_per_s": round(toks / max(1e-6, sec)),
214
+ "seconds": round(sec, 1), "tokens": toks,
215
+ "peak_gpu_gb": round(torch.cuda.max_memory_allocated() / 1e9, 2),
216
+ "world": world, "seq_len": args.seq_len, "micro_batch": args.micro_batch,
217
+ "attn": args.attn, "params": sum(p.numel() for p in model.parameters())}
218
+ print("BENCH_JSON " + json.dumps(res, sort_keys=True), flush=True)
219
+ if world > 1:
220
+ dist.barrier()
221
+ dist.destroy_process_group()
222
+ return res
223
+
224
+
225
  def main():
226
  ap = argparse.ArgumentParser()
227
  ap.add_argument("--root", default="/kaggle/working/mixroot")
 
249
  ap.add_argument("--push-every-steps", type=int, default=0, help="0 = only at 10% boundaries")
250
  ap.add_argument("--prune", action="store_true", help="delete local checkpoint after hub verify")
251
  ap.add_argument("--dry-run", action="store_true", help="build everything, train 0 steps")
252
+ ap.add_argument("--benchmark", type=int, default=0,
253
+ help="N steps with no Trainer in the loop (isolates framework per-step cost)")
254
  args = ap.parse_args()
255
 
256
  rank = int(os.environ.get("RANK", "0"))
 
348
  if not (90_000_000 <= pc["sum_numel"] <= 110_000_000) and not args.layers and not args.hidden:
349
  raise SystemExit(f"parameter count {pc['sum_numel']:,} is outside §2's 90-110M budget")
350
 
351
+ if args.benchmark:
352
+ # Both ranks iterate the same samples here (no DistributedSampler): this path measures step cost,
353
+ # not a training run, and it must never be used to produce a checkpoint.
354
+ say(f"benchmark mode: {args.benchmark} steps, no Trainer, micro={args.micro_batch} "
355
+ f"seq={args.seq_len} attn={args.attn} world={world}")
356
+ run_benchmark(args, model, train_ds, args.benchmark, world, seqs_per_step)
357
+ return
358
+
359
  # 10 checkpoints per run is §2's cadence; --push-every-steps overrides for the preflight.
360
  ckpt_every = args.push_every_steps or max(1, steps_planned // 10)
361