jayasriii commited on
Commit
cfb09b7
·
verified ·
1 Parent(s): 8e5acc4

Upload folder using huggingface_hub

Browse files
390M_dynamic_proper_sqrt_lr.csv ADDED
The diff for this file is too large to render. See raw diff
 
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00001000.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fe743aafc4e44b8ed34b1baefaf66ed668a2984d78833af0dbfb4d9bc4273d85
3
+ size 4205813723
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00002000.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f32046e81803bcb0cbbd77836ee00e4dd6a81f14387f515fb33627fbbb43d80c
3
+ size 4205813723
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00003000.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4e572dc1417532d473f46f78f4178e513b47f2d95b8607ba943b8f596f8b4228
3
+ size 4205813723
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00003534.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:538b1d82b75a25ef4b59da7e5312d60ac356d48e1ef7856be7c764a096c85c13
3
+ size 4205813787
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00004000.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f335e3d50acc98aa32eefc0ff3c61392043753cd68f323982989908fd2bd2d6d
3
+ size 4205813787
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00005000.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cb6fdf3ae40dd0a87ac20b4682873c2215882b180024f67aafe81bb6e16e1d94
3
+ size 4205813787
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00006000.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:572d4e27d8f19b3dbd6d6681b1b564137c9b573e82ca26510421214f6c15424d
3
+ size 4205813787
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00006139.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c4cfab10e4b7ca011913a2ccc018893caff63004ce45667485a6862c53e2cc42
3
+ size 4205813787
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00007000.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9470bcf094d6ddfd8c613d08584a753e366a11953eb029f993ab3977157b70c0
3
+ size 4205813787
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00008000.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7ae4a5c19cec77723c5ccc6b960aad8e6ed33f167975dce5efbf355366dce2b6
3
+ size 4205813787
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00008653.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3b354792c613c9a78a430e6f07e0bbc2ca4acab34d8e31fcceb5be2e21d19581
3
+ size 4205813851
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00009000.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a29a60defa308d66d57455e7b84dfae1fd0fafd77e8d93f53cf4ec896915657a
3
+ size 4205813851
390M_dynamic_proper_sqrt_lr/blumodel_muon_dynamic_sqrt_step00009042.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:76616c00871132c9b2b74c23f58d777b4fff3099f2a3eda592d53d38d34eda40
3
+ size 4205813851
check_target_loss.py CHANGED
@@ -6,13 +6,11 @@ mid-run, e.g. train_390M_dynamicb.py). Two cases, both reported:
6
  - target not yet crossed -> fit L(T) = L0 + A*T^-alpha (Eq.14) to the
7
  post-warmup smoothed trajectory and solve for T algebraically, flagging
8
  how far past the observed range the extrapolation reaches.
9
-
10
  Everything runs on a TOKEN axis, not steps: once the batch size changes, a step
11
  no longer stands for a fixed amount of data, so L(S) kinks at every switch and
12
  a predicted step can't be compared across runs. For a fixed batch T = B*S, so
13
  L0 + A*S^-alpha and L0 + A'*T^-alpha are the same curve (same L0 and alpha,
14
  A' = A*B^alpha) -- fixed-batch results are unchanged.
15
-
16
  Batch size per step: the CSV's `global_batch` column if present (the dynamic
17
  trainer logs it), else --batch-size, else parsed from the filename (_1Mbs).
18
  Tokens consumed before each step: the CSV's `tokens` column if present, else
@@ -361,3 +359,4 @@ def main():
361
 
362
  if __name__ == "__main__":
363
  main()
 
 
6
  - target not yet crossed -> fit L(T) = L0 + A*T^-alpha (Eq.14) to the
7
  post-warmup smoothed trajectory and solve for T algebraically, flagging
8
  how far past the observed range the extrapolation reaches.
 
9
  Everything runs on a TOKEN axis, not steps: once the batch size changes, a step
10
  no longer stands for a fixed amount of data, so L(S) kinks at every switch and
11
  a predicted step can't be compared across runs. For a fixed batch T = B*S, so
12
  L0 + A*S^-alpha and L0 + A'*T^-alpha are the same curve (same L0 and alpha,
13
  A' = A*B^alpha) -- fixed-batch results are unchanged.
 
14
  Batch size per step: the CSV's `global_batch` column if present (the dynamic
15
  trainer logs it), else --batch-size, else parsed from the filename (_1Mbs).
16
  Tokens consumed before each step: the CSV's `tokens` column if present, else
 
359
 
360
  if __name__ == "__main__":
361
  main()
362
+
train_390M.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  #!/usr/bin/env python3
3
  # =============================================================================
4
  # train_muon.py — the MUON arm of train.py. Same model, same data order, same
@@ -460,11 +459,6 @@ class ModelConfig:
460
  # Run length in TOKENS. The batch changes during the run, so the step count
461
  # (max_steps, and with it the WSD phases) is derived from this in main().
462
  total_tokens: int = 14_000_000_000 # what the fixed-batch arms ran: 17,802 x 786,432
463
- # The train shards are cut into windows of data_grain * T tokens, the way
464
- # the fixed-batch arms' loader cut them with micro-batch B = data_grain
465
- # (6 in the 786K arm), so this run trains on the same tokens in the same
466
- # order whatever its batch, micro-batch or GPU count (see DataLoaderLite).
467
- data_grain: int = 6
468
 
469
  # Batch-size schedule by tokens consumed: ((start_tokens, tokens/step), ...).
470
  # From tripartite_fit.py (BATCH_SIZE_STUDY.md §4.3). For each target loss
@@ -479,9 +473,10 @@ class ModelConfig:
479
  # 6.554B -> 8.324B B_opt(2.4501) = 800K
480
  # 8.324B -> end B_opt(2.4376) = 807K (the last target fitted)
481
  # Zhou et al. (arXiv:2601.05034, §4.3.2) also switch on tokens consumed and
482
- # keep the LR fixed (LR ~ sqrt(B) gave them nothing); train_390M_dynamicb.py
483
- # is that arm. THIS arm tests the alternative: the LR follows sqrt(batch)
484
- # at every switch (see lr_ref_batch below). The fitted values are
 
485
  # not whole numbers of sequences; resolve_batch_schedule() rounds them for
486
  # the GPU count and main() prints what runs. Empty = global_batch throughout.
487
  batch_schedule: tuple = ((0, 578_286),
@@ -539,8 +534,10 @@ class ModelConfig:
539
  # target-loss checkpoints are never pruned
540
  # Separate from the AdamW arm's dir: the two checkpoint formats differ (Muon
541
  # momentum vs Adam m/v) and auto-resume picks the newest file it finds.
542
- ckpt_dir: str = "ckpt_blu_muon_dynamic"
543
- ckpt_prefix: str = "blumodel_muon_dynamic"
 
 
544
 
545
  # ---- runtime knobs (not in the C++ struct) ----
546
  fused_rms_rope: bool = True
@@ -594,6 +591,7 @@ class BatchStage:
594
 
595
  def resolve_batch_schedule(cfg, world_size):
596
  """cfg.batch_schedule -> [BatchStage], split over `world_size` GPUs.
 
597
  A step is micro_B * grad_accum sequences on every rank, so a batch must be
598
  a whole number of sequences per rank (a multiple of T * world_size) that
599
  factors as micro_B * grad_accum with micro_B <= cfg.B. A target that does
@@ -638,6 +636,7 @@ def keep_batch_schedule(saved, stages, cfg, world_size):
638
  """On resume: the batches a checkpoint's run was running, split over this
639
  many GPUs, or None if the schedule config changed since (its starts or
640
  targets differ from `stages`) or a batch no longer splits evenly.
 
641
  resolve_batch_schedule() rounds for the GPU count, so a run restarted on a
642
  different count would otherwise switch to that count's rounding mid-run.
643
  Going down by a factor (8 -> 4, 2, 1) always keeps the batches.
@@ -707,29 +706,31 @@ def print_batch_schedule(stages, runs, steps_label, ref_batch):
707
  # 3. Data — port of the C++ DataLoaderLite (Tensor-Implementations/include/
708
  # DataLoader.h), including its pinned-host / side-stream prefetch ring.
709
  #
 
 
 
 
 
 
 
 
710
  # Shards are HEADERLESS flat uint16 token files. A shard is "val" if "val"
711
  # appears anywhere in its basename, "train" otherwise — this covers both an
712
  # explicit flux_train_*/flux_val_* naming and an unmarked-train naming like
713
  # flux_20b_shard_*.bin alongside a separately named *val* shard. For a batch
714
  # at position p:
715
  # x = tokens[p : p+B*T] y = tokens[p+1 : p+B*T+1]
716
- #
717
- # The token stream. Every shard is cut into windows of grain*T tokens from its
718
- # start; each window that fits (+1 token for the shifted targets) is used, the
719
- # shard's tail after the last one is not. That is exactly the set of tokens the
720
- # fixed-batch arms' loader (train_390M_bs.py: rank r reads windows r, r+W, ...
721
- # of B*T tokens) read with micro-batch B = grain. The stream is those windows'
722
- # sequences, shard after shard, and each micro-step takes the next
723
- # world_size*B sequences of it, rank r the r-th run of B, crossing into the
724
- # next shard where one runs out. So which tokens a step reads depends only on
725
- # how many tokens came before it — not on B, grad_accum or world_size: the
726
- # batch schedule, and a resume on another GPU count, change how steps are cut
727
- # from the stream, never the stream itself. After E tokens this run is E
728
- # tokens into the stream, which is where the fixed-batch arms' ranks are
729
- # spread around (theirs drift apart by a few M tokens across shards).
730
- #
731
- # The position (shard, offset) is the same on every rank, and a checkpoint
732
- # stores it to resume from.
733
  #
734
  # The prefetch mirrors the C++ ring: NBUF pinned staging buffers and NBUF device
735
  # tensors, H2D issued on a side stream, CUDA events ordering producer against
@@ -740,18 +741,20 @@ class DataLoaderLite:
740
  NBUF = 4
741
 
742
  def __init__(self, B, T, split, data_root, device, master=True,
743
- max_tokens_per_shard=100_000_000, rank=0, world_size=1, grain=None):
744
  assert split in ("train", "val")
745
- # B is also the largest micro-batch set_micro_batch() can switch to:
746
- # the buffers below are sized for it.
747
  self.B = self.B_max = B
 
748
  self.T = T
749
  self.split = split
 
750
  self.max_tokens = max_tokens_per_shard
751
  self.device = device
752
- # DDP: every rank reads the SAME stream but a DISJOINT run of each
753
- # micro-step's sequences (offset by rank), so a training step's
754
- # world_size*grad_accum micro-batches never repeat a window.
755
  self.rank = rank
756
  self.world_size = world_size
757
 
@@ -769,14 +772,6 @@ class DataLoaderLite:
769
  if master:
770
  print(f"found {len(self.shards)} shards for split {split}")
771
 
772
- # Tokens of each shard the stream uses: its whole windows (see above).
773
- G = (grain or B) * T
774
- self.usable = [max(0, (min(os.path.getsize(p) // 2, max_tokens_per_shard) - 1) // G) * G
775
- for p in self.shards]
776
- if not any(self.usable):
777
- raise RuntimeError(f"no {split} shard holds one window of {G} tokens")
778
- self.tok, self.tok_shard = None, None
779
-
780
  BT = B * T
781
  self.pin_x = [torch.empty(BT, dtype=torch.int64).pin_memory() for _ in range(self.NBUF)]
782
  self.pin_y = [torch.empty(BT, dtype=torch.int64).pin_memory() for _ in range(self.NBUF)]
@@ -786,63 +781,73 @@ class DataLoaderLite:
786
  self.h2d_done = [torch.cuda.Event() for _ in range(self.NBUF)]
787
  self.h2d_recorded = [False] * self.NBUF
788
  self.where = [(0, 0)] * self.NBUF # (shard_index, token_offset)
789
- self.block = [(0, 0)] * self.NBUF # stream position each slot's micro-step started at
790
  self.reset()
791
 
792
- # -- the stream ----------------------------------------------------------
793
- def _tokens(self, s):
794
- if s != self.tok_shard:
795
- self.tok, self.tok_shard = np.memmap(self.shards[s], dtype=np.uint16, mode="r"), s
796
- return self.tok
797
-
798
- def _advance(self, n):
799
- """Move n sequences along the stream. -> [(shard, offset, n_seq), ...],
800
- the pieces of shards they lie in (two when they cross a shard end)."""
801
- pieces = []
802
- while n > 0:
803
- if self.off >= self.usable[self.cur]:
804
- self.cur, self.off = (self.cur + 1) % len(self.shards), 0
805
- continue
806
- k = min(n, (self.usable[self.cur] - self.off) // self.T)
807
- pieces.append((self.cur, self.off, k))
808
- self.off += k * self.T
809
- n -= k
810
- return pieces
811
-
812
- def stream_offset(self, shard, off, tokens):
813
- """How far (shard, off) is past `tokens` tokens into the stream (signed,
814
- modulo one pass over the shards)."""
815
- d = (sum(self.usable[:shard]) + min(off, self.usable[shard]) - tokens) % sum(self.usable)
816
- return d - sum(self.usable) if d > sum(self.usable) // 2 else d
 
 
 
 
 
 
 
 
 
 
 
817
 
818
  def reset(self):
819
  self.seek(0, 0)
820
 
821
  # -- position (the same on every rank) -----------------------------------
 
822
  def state(self):
823
- """-> (shard_index, offset) of the next micro-step next_batch() will
824
- return. Prefetched ones it has not returned yet do not count as consumed."""
825
- return self.block[self.buf] if self.primed else (self.cur, self.off)
826
 
827
- def seek(self, shard, off):
828
  """Continue from a state(). Drops anything already prefetched."""
829
  self.copy_stream.synchronize()
830
- self.cur, self.off = shard, -(-off // self.T) * self.T # a sequence boundary
 
 
831
  self.primed = False
832
  self.buf = 0
833
 
834
- def set_micro_batch(self, B):
835
- """Serve B sequences per rank per micro-step from the next one on."""
 
836
  if not 1 <= B <= self.B_max:
837
  raise ValueError(f"micro-batch {B} outside 1..{self.B_max}")
838
- if B != self.B:
839
  self.seek(*self.state())
840
- self.B = B
841
-
842
- def skip_tokens(self, n):
843
- """Fast-forward n tokens of the stream without touching the GPU."""
844
- self.seek(*self.state())
845
- self._advance(n // self.T)
846
 
847
  # -- prefetch ------------------------------------------------------------
848
  def _stage(self, idx):
@@ -857,17 +862,15 @@ class DataLoaderLite:
857
  if self.h2d_recorded[idx]:
858
  self.h2d_done[idx].synchronize()
859
 
860
- self.block[idx] = (self.cur, self.off)
861
- self._advance(self.rank * B) # lower ranks' runs
862
- pieces = self._advance(B) # this rank's
863
- self._advance((self.world_size - 1 - self.rank) * B) # higher ranks'
864
- self.where[idx] = pieces[0][:2]
865
- at = 0
866
- for s, off, k in pieces:
867
- buf = np.asarray(self._tokens(s)[off: off + k * self.T + 1]).astype(np.int64)
868
- self.pin_x[idx][at: at + k * self.T].copy_(torch.from_numpy(buf[:-1]))
869
- self.pin_y[idx][at: at + k * self.T].copy_(torch.from_numpy(buf[1:]))
870
- at += k * self.T
871
 
872
  with torch.cuda.stream(self.copy_stream):
873
  self.dev_x[idx][:B].view(-1).copy_(self.pin_x[idx][:BT], non_blocking=True)
@@ -898,6 +901,13 @@ class DataLoaderLite:
898
  return (self.dev_x[i][:self.B], self.dev_y[i][:self.B],
899
  self.where[i][0], self.where[i][1])
900
 
 
 
 
 
 
 
 
901
 
902
  # =============================================================================
903
  # 4. Model.
@@ -1469,15 +1479,6 @@ def build_argparser():
1469
  ap.add_argument("--total-tokens", type=float, default=None,
1470
  help="run length in tokens (default 14e9); the step count is "
1471
  "derived from it and the batch schedule")
1472
- ap.add_argument("--data-grain", type=int, default=None,
1473
- help="cut the train shards into windows of this many sequences, "
1474
- "as the fixed-batch arms' loader did with that micro-batch "
1475
- "(default 6, the 786K arm's)")
1476
- ap.add_argument("--realign-data", action="store_true",
1477
- help="on resume from a checkpoint an earlier loader wrote, move "
1478
- "the data position back to tokens-seen into the stream, "
1479
- "where the fixed-batch arms are; the tokens in between are "
1480
- "seen twice. Default: continue from the stored position.")
1481
  ap.add_argument("--max-steps", type=int, default=None,
1482
  help="caps the run for benchmarking; warmup/stable shrink "
1483
  "proportionally so the WSD decay tail stays non-empty "
@@ -1518,7 +1519,7 @@ def build_argparser():
1518
  help="per-micro-batch loss + depth probes (env: BLU_DIAG=1)")
1519
  ap.add_argument("--warmup-measure", type=int, default=3,
1520
  help="steps excluded from the steady-state throughput summary")
1521
- ap.add_argument("--csv", default="blu_muon_log.csv",
1522
  help="CSV log path; appended (not truncated) on resume. The "
1523
  "first 8 columns match the AdamW arm's CSV exactly, so "
1524
  "the two files plot against each other directly; "
@@ -1552,7 +1553,7 @@ def build_argparser():
1552
  def config_from_args(args):
1553
  cfg = ModelConfig()
1554
  for f in ("B", "T", "d_model", "n_layers", "q_heads", "kv_heads", "ffn_hidden",
1555
- "vocab_size", "global_batch", "data_grain", "val_freq", "val_loss_steps",
1556
  "ckpt_dir", "ckpt_prefix", "ckpt_freq", "ckpt_keep",
1557
  "muon_lr", "muon_min_lr", "muon_momentum", "muon_ns_steps",
1558
  "muon_adjust_lr", "target_losses", "target_loss_ema_beta",
@@ -1834,7 +1835,7 @@ def main():
1834
  # the same one.
1835
  train_loader = DataLoaderLite(cfg.B, cfg.T, "train", args.data_root, device,
1836
  master=master_process, rank=ddp_rank,
1837
- world_size=ddp_world_size, grain=cfg.data_grain)
1838
  # rank=0, world_size=1 UNCONDITIONALLY, on every rank: val_loss_steps
1839
  # windows at fixed token offsets 0, BT, 2BT, ... Sharding this by
1840
  # ddp_rank/ddp_world_size like the train loader would make the reported
@@ -1882,21 +1883,6 @@ def main():
1882
  # are independent of B and world_size, so this is exact.
1883
  tokens_seen = progress["tokens_seen"]
1884
  train_loader.seek(*progress["data_pos"])
1885
- # 0 when this loader wrote the checkpoint: E tokens in, the run
1886
- # is E tokens into the stream. An earlier loader dropped more
1887
- # of each shard's tail, which left the run ahead of that.
1888
- ahead = train_loader.stream_offset(*progress["data_pos"], tokens_seen)
1889
- if args.realign_data and ahead:
1890
- train_loader.reset()
1891
- train_loader.skip_tokens(tokens_seen)
1892
- if master_process:
1893
- print(f"[resume] --realign-data: moved the data position back "
1894
- f"{ahead:,} tokens to {tokens_seen:,} tokens into the "
1895
- f"stream; those tokens are seen a second time")
1896
- elif master_process and ahead:
1897
- print(f"[resume] the data position is {ahead:+,} tokens from "
1898
- f"{tokens_seen:,} tokens into the stream (left by an "
1899
- f"earlier loader); it stays at that offset from here")
1900
  # Keep running the batches the run started with when this GPU
1901
  # count can (always true going 8 -> 4, 2, 1), rather than this
1902
  # count's own rounding: changing GPUs then changes nothing but
@@ -1916,10 +1902,11 @@ def main():
1916
  f"follows the schedule above")
1917
  else:
1918
  # A checkpoint from a trainer without a batch schedule: assume
1919
- # steps [0, start_step) ran this schedule, and go that many
1920
- # tokens into the stream.
1921
- _, tokens_seen = walk_schedule(stages, float("inf"), start_step)
1922
- train_loader.skip_tokens(tokens_seen)
 
1923
  if master_process:
1924
  print(f"[resume] checkpoint has no data position; replayed this "
1925
  f"schedule to step {start_step} ({tokens_seen:,} tokens)")
@@ -2030,7 +2017,7 @@ def main():
2030
  # carry straight on.
2031
  new_stage = stage_at(stages, tokens_seen)
2032
  if stage is None or (new_stage.batch, new_stage.micro_B) != (stage.batch, stage.micro_B):
2033
- train_loader.set_micro_batch(new_stage.micro_B)
2034
  grad_accum = new_stage.grad_accum
2035
  tokens_per_step = new_stage.batch
2036
  lr_scale = lr_batch_scale(new_stage.batch, ref_batch)
@@ -2330,8 +2317,6 @@ if __name__ == "__main__":
2330
 
2331
 
2332
 
2333
-
2334
-
2335
 
2336
 
2337
 
 
 
1
  #!/usr/bin/env python3
2
  # =============================================================================
3
  # train_muon.py — the MUON arm of train.py. Same model, same data order, same
 
459
  # Run length in TOKENS. The batch changes during the run, so the step count
460
  # (max_steps, and with it the WSD phases) is derived from this in main().
461
  total_tokens: int = 14_000_000_000 # what the fixed-batch arms ran: 17,802 x 786,432
 
 
 
 
 
462
 
463
  # Batch-size schedule by tokens consumed: ((start_tokens, tokens/step), ...).
464
  # From tripartite_fit.py (BATCH_SIZE_STUDY.md §4.3). For each target loss
 
473
  # 6.554B -> 8.324B B_opt(2.4501) = 800K
474
  # 8.324B -> end B_opt(2.4376) = 807K (the last target fitted)
475
  # Zhou et al. (arXiv:2601.05034, §4.3.2) also switch on tokens consumed and
476
+ # keep the LR fixed (LR ~ sqrt(B) gave them nothing); train_390M_dynamicb_skip.py
477
+ # is that arm. THIS arm is the same run -- same loader, same batch schedule,
478
+ # same switch points -- except that the LR follows sqrt(batch) at every
479
+ # switch (see lr_ref_batch below). The fitted values are
480
  # not whole numbers of sequences; resolve_batch_schedule() rounds them for
481
  # the GPU count and main() prints what runs. Empty = global_batch throughout.
482
  batch_schedule: tuple = ((0, 578_286),
 
534
  # target-loss checkpoints are never pruned
535
  # Separate from the AdamW arm's dir: the two checkpoint formats differ (Muon
536
  # momentum vs Adam m/v) and auto-resume picks the newest file it finds.
537
+ ckpt_dir: str = "ckpt_blu_muon_dynamic_sqrt"
538
+ ckpt_prefix: str = "blumodel_muon_dynamic_sqrt"
539
+
540
+
541
 
542
  # ---- runtime knobs (not in the C++ struct) ----
543
  fused_rms_rope: bool = True
 
591
 
592
  def resolve_batch_schedule(cfg, world_size):
593
  """cfg.batch_schedule -> [BatchStage], split over `world_size` GPUs.
594
+
595
  A step is micro_B * grad_accum sequences on every rank, so a batch must be
596
  a whole number of sequences per rank (a multiple of T * world_size) that
597
  factors as micro_B * grad_accum with micro_B <= cfg.B. A target that does
 
636
  """On resume: the batches a checkpoint's run was running, split over this
637
  many GPUs, or None if the schedule config changed since (its starts or
638
  targets differ from `stages`) or a batch no longer splits evenly.
639
+
640
  resolve_batch_schedule() rounds for the GPU count, so a run restarted on a
641
  different count would otherwise switch to that count's rounding mid-run.
642
  Going down by a factor (8 -> 4, 2, 1) always keeps the batches.
 
706
  # 3. Data — port of the C++ DataLoaderLite (Tensor-Implementations/include/
707
  # DataLoader.h), including its pinned-host / side-stream prefetch ring.
708
  #
709
+ # THIS FILE'S LOADER is the one the 390M dynamic-batch run started with, plus a
710
+ # skip for shards shorter than one step (flux_20b's shard_350001), so a resume
711
+ # of that run continues on exactly the data it would have read. It drops each
712
+ # shard's tail per step, which leaves it a little ahead of the fixed-batch
713
+ # arms' data (~+24M tokens at 7.2B). train_390M_dynamicb.py has the loader that
714
+ # reads the fixed-batch arms' token stream instead (restart from scratch, or
715
+ # --realign-data).
716
+ #
717
  # Shards are HEADERLESS flat uint16 token files. A shard is "val" if "val"
718
  # appears anywhere in its basename, "train" otherwise — this covers both an
719
  # explicit flux_train_*/flux_val_* naming and an unmarked-train naming like
720
  # flux_20b_shard_*.bin alongside a separately named *val* shard. For a batch
721
  # at position p:
722
  # x = tokens[p : p+B*T] y = tokens[p+1 : p+B*T+1]
723
+ # Each micro-step reads one contiguous block of world_size*B*T tokens starting
724
+ # at `base`, rank r taking p = base + r*B*T, and base strides by the block. An
725
+ # optimizer step is grad_accum such blocks, so it reads one contiguous range of
726
+ # its batch's tokens; a shard tail that cannot fit a whole STEP is dropped and
727
+ # the loader wraps to the next shard. All ranks move through the blocks
728
+ # together, so the position (shard, base) is one value on every rank, and
729
+ # which tokens a step reads depends only on the batch sizes, not on how they
730
+ # are split into world_size, B and grad_accum: a run resumed on another GPU
731
+ # count reads the same tokens per step. The batch schedule changes the split
732
+ # between steps without skipping or repeating a token, and a checkpoint stores
733
+ # the position to resume from directly.
 
 
 
 
 
 
734
  #
735
  # The prefetch mirrors the C++ ring: NBUF pinned staging buffers and NBUF device
736
  # tensors, H2D issued on a side stream, CUDA events ordering producer against
 
741
  NBUF = 4
742
 
743
  def __init__(self, B, T, split, data_root, device, master=True,
744
+ max_tokens_per_shard=100_000_000, rank=0, world_size=1):
745
  assert split in ("train", "val")
746
+ # B is also the largest micro-batch set_step() can switch to: the
747
+ # buffers below are sized for it. A step is grad_accum micro-steps.
748
  self.B = self.B_max = B
749
+ self.grad_accum = 1
750
  self.T = T
751
  self.split = split
752
+ self.master = master
753
  self.max_tokens = max_tokens_per_shard
754
  self.device = device
755
+ # DDP: every rank reads the SAME shard list but a DISJOINT slice of each
756
+ # block (offset by rank), so a training step's world_size*grad_accum
757
+ # micro-batches never repeat a window.
758
  self.rank = rank
759
  self.world_size = world_size
760
 
 
772
  if master:
773
  print(f"found {len(self.shards)} shards for split {split}")
774
 
 
 
 
 
 
 
 
 
775
  BT = B * T
776
  self.pin_x = [torch.empty(BT, dtype=torch.int64).pin_memory() for _ in range(self.NBUF)]
777
  self.pin_y = [torch.empty(BT, dtype=torch.int64).pin_memory() for _ in range(self.NBUF)]
 
781
  self.h2d_done = [torch.cuda.Event() for _ in range(self.NBUF)]
782
  self.h2d_recorded = [False] * self.NBUF
783
  self.where = [(0, 0)] * self.NBUF # (shard_index, token_offset)
784
+ self.block = [(0, 0)] * self.NBUF # (shard_index, base) of each slot's block
785
  self.reset()
786
 
787
+ # -- shard plumbing ------------------------------------------------------
788
+ def _load_shard(self, i):
789
+ self.cur = i
790
+ arr = np.memmap(self.shards[i], dtype=np.uint16, mode="r")
791
+ self.tok = arr
792
+ self.ntok = min(len(arr), self.max_tokens)
793
+
794
+ def _advance_shard(self):
795
+ self._load_shard((self.cur + 1) % len(self.shards))
796
+ self.base = 0
797
+
798
+ def _step_tokens(self):
799
+ return self.B * self.T * self.world_size * self.grad_accum
800
+
801
+ def _next_block(self):
802
+ """-> base of the next block, moving past it. A block that starts a
803
+ step first checks the WHOLE step fits (+1 for the last shifted
804
+ target), and leaves the shard's tail for the next shard if not — or
805
+ the whole shard, if it is smaller than a step (flux_20b's
806
+ shard_350001 holds 574,132 tokens)."""
807
+ if self.micro == 0:
808
+ for _ in range(len(self.shards) + 1):
809
+ if self.base + self._step_tokens() + 1 <= self.ntok:
810
+ break
811
+ self._advance_shard()
812
+ if self.master and self._step_tokens() + 1 > self.ntok:
813
+ print(f"[data] skipping {os.path.basename(self.shards[self.cur])}: "
814
+ f"{self.ntok:,} tokens, less than one step "
815
+ f"({self._step_tokens():,})", flush=True)
816
+ else:
817
+ raise RuntimeError(f"no {self.split} shard holds one step of "
818
+ f"{self._step_tokens():,} tokens")
819
+ base = self.base
820
+ self.base += self.B * self.T * self.world_size
821
+ self.micro = (self.micro + 1) % self.grad_accum
822
+ return base
823
 
824
  def reset(self):
825
  self.seek(0, 0)
826
 
827
  # -- position (the same on every rank) -----------------------------------
828
+ # Meant for between optimizer steps, where the next block starts a step.
829
  def state(self):
830
+ """-> (shard_index, base) of the next block next_batch() will return.
831
+ Prefetched blocks it has not returned yet do not count as consumed."""
832
+ return self.block[self.buf] if self.primed else (self.cur, self.base)
833
 
834
+ def seek(self, shard, base):
835
  """Continue from a state(). Drops anything already prefetched."""
836
  self.copy_stream.synchronize()
837
+ self._load_shard(shard)
838
+ self.base = base
839
+ self.micro = 0 # index in its step of the next block to stage
840
  self.primed = False
841
  self.buf = 0
842
 
843
+ def set_step(self, B, grad_accum=1):
844
+ """From the next step on, serve grad_accum micro-steps of B sequences
845
+ per rank per step."""
846
  if not 1 <= B <= self.B_max:
847
  raise ValueError(f"micro-batch {B} outside 1..{self.B_max}")
848
+ if (B, grad_accum) != (self.B, self.grad_accum):
849
  self.seek(*self.state())
850
+ self.B, self.grad_accum = B, grad_accum
 
 
 
 
 
851
 
852
  # -- prefetch ------------------------------------------------------------
853
  def _stage(self, idx):
 
862
  if self.h2d_recorded[idx]:
863
  self.h2d_done[idx].synchronize()
864
 
865
+ # Recorded BEFORE _next_block()'s does-the-step-fit check, so a seek()
866
+ # to it after set_step() decides that again for the new step size.
867
+ self.block[idx] = (self.cur, self.base)
868
+ base = self._next_block()
869
+ pos = base + BT * self.rank
870
+ self.where[idx] = (self.cur, pos)
871
+ buf = np.asarray(self.tok[pos: pos + BT + 1]).astype(np.int64)
872
+ self.pin_x[idx][:BT].copy_(torch.from_numpy(buf[:-1]))
873
+ self.pin_y[idx][:BT].copy_(torch.from_numpy(buf[1:]))
 
 
874
 
875
  with torch.cuda.stream(self.copy_stream):
876
  self.dev_x[idx][:B].view(-1).copy_(self.pin_x[idx][:BT], non_blocking=True)
 
901
  return (self.dev_x[i][:self.B], self.dev_y[i][:self.B],
902
  self.where[i][0], self.where[i][1])
903
 
904
+ def skip_batches(self, n):
905
+ """Fast-forward `n` micro-steps without touching the GPU (resume from a
906
+ checkpoint that has no stored position)."""
907
+ self.seek(*self.state())
908
+ for _ in range(n):
909
+ self._next_block()
910
+
911
 
912
  # =============================================================================
913
  # 4. Model.
 
1479
  ap.add_argument("--total-tokens", type=float, default=None,
1480
  help="run length in tokens (default 14e9); the step count is "
1481
  "derived from it and the batch schedule")
 
 
 
 
 
 
 
 
 
1482
  ap.add_argument("--max-steps", type=int, default=None,
1483
  help="caps the run for benchmarking; warmup/stable shrink "
1484
  "proportionally so the WSD decay tail stays non-empty "
 
1519
  help="per-micro-batch loss + depth probes (env: BLU_DIAG=1)")
1520
  ap.add_argument("--warmup-measure", type=int, default=3,
1521
  help="steps excluded from the steady-state throughput summary")
1522
+ ap.add_argument("--csv", default="blu_muon_log_dy_sq.csv",
1523
  help="CSV log path; appended (not truncated) on resume. The "
1524
  "first 8 columns match the AdamW arm's CSV exactly, so "
1525
  "the two files plot against each other directly; "
 
1553
  def config_from_args(args):
1554
  cfg = ModelConfig()
1555
  for f in ("B", "T", "d_model", "n_layers", "q_heads", "kv_heads", "ffn_hidden",
1556
+ "vocab_size", "global_batch", "val_freq", "val_loss_steps",
1557
  "ckpt_dir", "ckpt_prefix", "ckpt_freq", "ckpt_keep",
1558
  "muon_lr", "muon_min_lr", "muon_momentum", "muon_ns_steps",
1559
  "muon_adjust_lr", "target_losses", "target_loss_ema_beta",
 
1835
  # the same one.
1836
  train_loader = DataLoaderLite(cfg.B, cfg.T, "train", args.data_root, device,
1837
  master=master_process, rank=ddp_rank,
1838
+ world_size=ddp_world_size)
1839
  # rank=0, world_size=1 UNCONDITIONALLY, on every rank: val_loss_steps
1840
  # windows at fixed token offsets 0, BT, 2BT, ... Sharding this by
1841
  # ddp_rank/ddp_world_size like the train loader would make the reported
 
1883
  # are independent of B and world_size, so this is exact.
1884
  tokens_seen = progress["tokens_seen"]
1885
  train_loader.seek(*progress["data_pos"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1886
  # Keep running the batches the run started with when this GPU
1887
  # count can (always true going 8 -> 4, 2, 1), rather than this
1888
  # count's own rounding: changing GPUs then changes nothing but
 
1902
  f"follows the schedule above")
1903
  else:
1904
  # A checkpoint from a trainer without a batch schedule: assume
1905
+ # steps [0, start_step) ran this schedule and replay it.
1906
+ done, tokens_seen = walk_schedule(stages, float("inf"), start_step)
1907
+ for s, n in done:
1908
+ train_loader.set_step(s.micro_B, s.grad_accum)
1909
+ train_loader.skip_batches(n * s.grad_accum)
1910
  if master_process:
1911
  print(f"[resume] checkpoint has no data position; replayed this "
1912
  f"schedule to step {start_step} ({tokens_seen:,} tokens)")
 
2017
  # carry straight on.
2018
  new_stage = stage_at(stages, tokens_seen)
2019
  if stage is None or (new_stage.batch, new_stage.micro_B) != (stage.batch, stage.micro_B):
2020
+ train_loader.set_step(new_stage.micro_B, new_stage.grad_accum)
2021
  grad_accum = new_stage.grad_accum
2022
  tokens_per_step = new_stage.batch
2023
  lr_scale = lr_batch_scale(new_stage.batch, ref_batch)
 
2317
 
2318
 
2319
 
 
 
2320
 
2321
 
2322