-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtrain.py
More file actions
2537 lines (2399 loc) · 125 KB
/
Copy pathtrain.py
File metadata and controls
2537 lines (2399 loc) · 125 KB
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# (c) 2026 Ivan K (srose69, SimpleRose). Licensed under AGPL-3.0-only.
# Viper-LLM: Volumetric Language Model with Triangle Cross-Scan State Modelling.
# See TRADEMARKS.md for project naming and origin policy.
import argparse
import contextlib
import json
import os
import subprocess
import sys
import time
from dataclasses import asdict
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import numpy as np
# expandable_segments=True lets the CUDA caching allocator grow a single
# segment instead of carving fixed blocks, which eliminates the
# fragmentation-induced OOMs that hit on 8-12 GB GPUs around step 2-3
# (free memory exists but no contiguous block of the requested size).
# Set BEFORE torch is imported so the allocator picks it up.
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.autograd.graph import save_on_cpu
from datasets import Dataset, load_dataset, load_from_disk
from torch.utils.data import DataLoader
from torch.utils.data.distributed import DistributedSampler
from transformers import AutoTokenizer
from decoder import DecoderConfig, ViperTiedDecoder
from cml import CoreConfig, Pipeline
from input_proj import InputProj
from inference import step, init_carry
from container import Container
from packing import (
factorize_near_cube, triangle_sample_n, pack_messages_to_batch,
apply_fim_to_packed, apply_nat_to_packed, PackedBatch,
)
from dtypes import (
DTYPE_NAMES, parse_compute_dtype, autocast_ctx, is_low_precision,
LowPrecGradAccumulator, OffloadedOptim, OffloadedAdamW,
)
from atom.atom import ATOM
from runtime_data import (
FIM_WARMUP_FRAC,
FIM_MIN_SPAN_FRAC,
FIM_MAX_SPAN_FRAC,
)
from converter.main import (
_get_hf_id,
_to_messages,
_to_str_ids,
_load_text,
_ensure_messages,
)
from optimizer import (
count_params,
build_groups,
slab_stats,
_snapshot,
slab_updates,
group_stats,
group_updates,
)
from train_runtime import (
ckpt_chunk, render_log, mem_mb, save_ckpt, lr_at, msg_stream,
TrackedDataStream, render_dsl_log,
)
def _load_validation_dataset(ds_path: Path, dataset_type: str, text_column: str = 'auto'):
"""Load validation dataset from files/dirs with 'validation' in name.
Searches for files/directories containing 'validation' (case-insensitive).
Returns None if no validation files found.
"""
if not ds_path.is_dir():
return None
# Find files/dirs with 'validation' in name
val_items = []
for item in ds_path.iterdir():
if 'validation' in item.name.lower():
val_items.append(item)
if not val_items:
return None
# Load based on file type
parquet_files = [f for f in val_items if f.is_file() and f.suffix.lower() == '.parquet']
if parquet_files:
return load_dataset('parquet', data_files=[str(f) for f in sorted(parquet_files)], split='train')
# Check for arrow dataset directory
for item in val_items:
if item.is_dir():
try:
return load_from_disk(str(item))
except Exception:
pass
# Try text files
txt_files = [f for f in val_items if f.is_file() and f.suffix.lower() == '.txt']
if txt_files:
text = txt_files[0].read_text(encoding='utf-8')
return Dataset.from_dict({'text': [text]})
return None
def _validation_indices_path(ds_path: Path) -> Path:
return ds_path / 'validation_indices.pt'
def _validation_auto_parquet_path(ds_path: Path) -> Path:
return ds_path / 'validation_auto.parquet'
def _load_validation_indices(ds_path: Path) -> List[int]:
idx_path = _validation_indices_path(ds_path)
if not idx_path.is_file():
return []
payload = torch.load(idx_path, map_location='cpu', weights_only=False)
indices = payload.get('indices', [])
return [int(i) for i in indices]
def _exclude_validation_rows(ds, ds_path: Path):
held_out = _load_validation_indices(ds_path)
if not held_out:
return ds
n = len(ds)
drop = set(i for i in held_out if 0 <= i < n)
if not drop:
return ds
keep = [i for i in range(n) if i not in drop]
return ds.select(keep)
def _maybe_create_validation_split(
ds,
ds_path: Path,
val_fraction: float,
val_seed: int,
is_main: bool,
):
"""Auto-create held-out validation parquet plus index sidecar.
Returns `(train_ds, val_ds, created_now)`.
"""
if not ds_path.is_dir():
return ds, None, False
if _load_validation_dataset(ds_path, dataset_type='messages') is not None:
return _exclude_validation_rows(ds, ds_path), _load_validation_dataset(ds_path, dataset_type='messages'), False
if not (0.0 < float(val_fraction) < 1.0):
return ds, None, False
n = len(ds)
if n < 2:
return ds, None, False
n_val = max(1, int(round(n * float(val_fraction))))
n_val = min(n_val, n - 1)
gen = torch.Generator()
gen.manual_seed(int(val_seed))
perm = torch.randperm(n, generator=gen).tolist()
val_idx = sorted(perm[:n_val])
train_idx = sorted(perm[n_val:])
val_ds = ds.select(val_idx)
train_ds = ds.select(train_idx)
val_path = _validation_auto_parquet_path(ds_path)
idx_path = _validation_indices_path(ds_path)
val_ds.to_parquet(str(val_path))
torch.save({'indices': val_idx, 'seed': int(val_seed), 'fraction': float(val_fraction)}, idx_path)
if is_main:
print(f'[data] validation auto-created file={val_path.name} size={len(val_ds)} '
f'fraction={float(val_fraction):.6f}')
return train_ds, val_ds, True
def _dist_barrier(local_rank: int) -> None:
if not dist.is_available() or not dist.is_initialized():
return
backend = dist.get_backend()
if backend == 'nccl':
dist.barrier(device_ids=[int(local_rank)])
return
dist.barrier()
def _activation_offload_ctx(mem_device: str):
"""Autograd saved-tensor offload for activations.
Only CPU is supported here intentionally: PyTorch provides a stable,
correct path via save_on_cpu(). Cross-GPU saved-tensor offload would need
custom hooks and tends to be much more fragile/perf-sensitive.
"""
dev = (mem_device or '').strip().lower()
if dev == 'cpu':
return save_on_cpu(pin_memory=True)
return contextlib.nullcontext()
def _activation_offload_enabled(args) -> bool:
# save_on_cpu() and checkpoint(reentrant/non-reentrant) can disagree on
# saved tensor metadata in this graph; keep the safe path by disabling
# activation offload when core checkpointing is active.
return bool((args.optim_mem_device or '').strip().lower() == 'cpu' and not bool(args.checkpoint_core))
def _auto_omp_threads(nproc: int) -> int:
total = os.cpu_count() or 1
if nproc <= 0:
return 1
# Keep one thread per rank at minimum, but avoid oversubscribing CPU
# badly on multi-GPU launches. Cap to a modest value because packing
# and tokenization already use Python/DataLoader workers separately.
return max(1, min(8, total // nproc))
class ViperLanguageModel(torch.nn.Module):
def __init__(
self,
rmu_cfg: CoreConfig,
dec_cfg: DecoderConfig,
loss_chunk_size: int,
checkpoint_core: bool = True,
num_slabs: int = 3,
io_device: str = 'cuda:0',
slab_devices: Tuple[str, ...] = None,
container_k: int = 0,
container_layers: int = 1,
container_skip_slabs: bool = False,
container_skip_units: float = 0.2,
):
super().__init__()
if (rmu_cfg.grid_g, rmu_cfg.grid_h, rmu_cfg.grid_w) != (dec_cfg.grid_g, dec_cfg.grid_h, dec_cfg.grid_w):
raise ValueError("grid mismatch between RMU and decoder")
if num_slabs < 1:
raise ValueError(f"num_slabs must be >= 1, got {num_slabs}")
if slab_devices is not None and len(slab_devices) != num_slabs:
raise ValueError(
f"slab_devices length {len(slab_devices)} != num_slabs {num_slabs}"
)
self.rmu_cfg = rmu_cfg
self.dec_cfg = dec_cfg
self.loss_chunk_size = loss_chunk_size
self.checkpoint_core = checkpoint_core
self.num_slabs = num_slabs
self.io_device = torch.device(io_device)
slab_names = tuple(f"slab{i + 1}" for i in range(num_slabs))
self.core_in_proj = InputProj(
dec_cfg.hidden_dim,
rmu_cfg.channels,
grid=(rmu_cfg.grid_g, rmu_cfg.grid_h, rmu_cfg.grid_w),
)
self.core = Pipeline(
rmu_cfg, slab_names=slab_names, slab_devices=slab_devices,
)
self.core_out_proj = torch.nn.Conv3d(rmu_cfg.channels, dec_cfg.hidden_dim, kernel_size=1, bias=True)
# Sub-Xavier fan-in init for the volume->dec_hidden projection that
# feeds decoder._norm/LM head. Pytorch's Kaiming-uniform default is
# ~sqrt(2)x larger than fan-in 1.0, which (post-LeaRNorm in decode)
# over-shoots the unit-variance logit contract at init. gain=0.51
# matches the rest of the model.
_coreout_w = self.core_out_proj.weight
_coreout_fanin, _ = torch.nn.init._calculate_fan_in_and_fan_out(_coreout_w)
try:
_coreout_gain = float(os.getenv("VIPER_CORE_OUT_GAIN", "0.9"))
except Exception:
_coreout_gain = 0.51
_coreout_std = _coreout_gain / max(1.0, float(_coreout_fanin)) ** 0.5
torch.nn.init.trunc_normal_(_coreout_w, mean=0.0, std=_coreout_std,
a=-2.0 * _coreout_std, b=2.0 * _coreout_std)
torch.nn.init.zeros_(self.core_out_proj.bias)
self.decoder = ViperTiedDecoder(dec_cfg)
# Container: optional per-chunk internal router. Disabled when
# container_k==0 (absent attribute; `getattr(model, 'container',
# None)` in _chunk_forward_with_carry returns None -> identity
# path, bit-equivalent to pre-Container behavior).
if container_k > 0:
self.container = Container(
slab_names=slab_names, ray_dim=rmu_cfg.ray_dim, K=container_k,
cont_layers=container_layers,
allow_slab_skip=container_skip_slabs,
max_unit_skip_frac=container_skip_units,
)
# See `_enforce_conv3d_channels_last_3d` for why. Apply on init so
# the layout is correct before the first forward; `_apply` below
# re-applies after every subsequent `.to()`/`.cuda()` move.
self._enforce_conv3d_channels_last_3d()
def _enforce_conv3d_channels_last_3d(self) -> None:
"""Force all nn.Conv3d weight tensors into channels_last_3d strides.
cuDNN on SM>=80 emits `∂L/∂W` for 3-D convolutions in channels_last_3d
layout: strides `(C_in*D*H*W, 1, C_in*H*W, C_in*W, C_in)`. PyTorch's
DDP reducer builds its bucket view from `param.stride()` snapshotted
at wrap time; if the weight was stored as default-contiguous
`(C_in*D*H*W, D*H*W, H*W, W, 1)` the grad stride disagrees and DDP
logs:
"Grad strides do not match bucket view strides. [...] may
impair performance."
plus performs an extra device-side copy per bucket per reduction.
Aligning the PARAMETER storage with the grad layout both removes
the warning and avoids that copy. Forward also goes through
cuDNN's channels-last kernels which is the preferred fast path
on Ampere+ for 1×1×1 point-wise convs that dominate the core.
Subtlety addressed here:
* `.to(memory_format=torch.channels_last_3d)` is a no-op for
weights with a size-1 non-batch dim (depthwise kernels have
`C_in_per_group=1`): channels_last_3d and contiguous are
metadata-indistinguishable in that case. We explicitly build
an `empty_strided` target and copy the data, so `stride()`
returns exactly `(C*D*H*W, 1, C*H*W, C*W, C)`.
* `.cuda()` / `.to(device=...)` DROPS this layout on singleton-
dim tensors because the default `_apply` path clones with
default memory format. This method is invoked after every
`self._apply(...)` (see override below) so the layout holds
across moves/dtype-casts.
"""
# Enumerate every module with a 5-D `weight` tensor — this covers
# `nn.Conv3d` AND the project's `MaskedCausalConv3d` (which is a
# plain `nn.Module` that calls `F.conv3d` on its own weight, so
# isinstance(nn.Conv3d) does NOT match it). Both hit the same
# cuDNN backward path and emit channels_last_3d grad strides.
for _m in self.modules():
_w = getattr(_m, 'weight', None)
if not isinstance(_w, torch.Tensor) or _w.dim() != 5:
continue
_wd = _w.data.contiguous()
_, _C, _D, _H, _W = _wd.shape
# Depthwise / grouped kernels (C_in == 1): autograd emits grad
# in plain contiguous layout, so keep the param contiguous.
if _C == 1:
if _w.data.stride() != _wd.stride():
_m.weight.data = _wd
continue
_cl3d_strides = (_C * _D * _H * _W, 1, _C * _H * _W, _C * _W, _C)
if _wd.stride() == _cl3d_strides:
continue
_new = torch.empty_strided(
_wd.shape, _cl3d_strides, dtype=_wd.dtype, device=_wd.device,
)
_new.copy_(_wd)
_m.weight.data = _new
def _apply(self, fn, *args, **kwargs):
# nn.Module._apply underlies .to(), .cuda(), .float(), etc.
# Re-enforce the layout every time params are re-materialized so
# `param.stride()` keeps matching the grad stride cuDNN produces.
out = super()._apply(fn, *args, **kwargs)
self._enforce_conv3d_channels_last_3d()
return out
@staticmethod
def _make_write_mask(valid_mask: torch.Tensor, lengths: torch.Tensor) -> torch.Tensor:
# Write at every valid position, including BOS (pos 0) and EOS (last
# valid token). Cross-sample isolation in single-sample legacy mode is a
# non-issue; scan reset is handled by reset_mask, not write exclusion.
return valid_mask.clone()
@property
def L_native(self) -> int:
return self.rmu_cfg.grid_g * self.rmu_cfg.grid_h * self.rmu_cfg.grid_w
def _forward_chunks(self, input_ids: torch.Tensor,
valid_mask: torch.Tensor,
write_mask: torch.Tensor,
reset_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
"""Unified chunked training forward (handles N>=1 chunks).
Row length L must be a positive multiple of L_native. For each
chunk we project to core space, run `step`
(which threads RMU/vAttn/mix carries, runs Container routing + update
if attached, and honors `reset_mask` to zero scan decay at sample
boundaries), and decode to seq. Seqs are concatenated along L.
The per-chunk carry is a Python dataclass of tensors; gradients
flow through the carry chain across chunks, giving full BPTT over
the row. `checkpoint_core=1` wraps each chunk's core pass in
`torch.utils.checkpoint` to trade forward-activation VRAM for
backward-time recomputation of that chunk.
"""
B, L = input_ids.shape
Ln = self.L_native
if L % Ln != 0 or L < Ln:
raise ValueError(f'seq_len {L} must be a positive multiple of L_native={Ln}')
N = L // Ln
carry = init_carry(self)
rays: List[torch.Tensor] = []
# PCSC: accumulate per-chunk predictor MSE across the row.
# Threaded as an explicit tensor through the checkpoint boundary
# (see `ckpt_chunk`) so the backward recompute rewires it; the
# non-checkpoint path reads it from `self._predictor_step_loss`.
pred_loss_accum = torch.zeros((), device=input_ids.device, dtype=torch.float32)
dsl_exec_loss_accum = torch.zeros((), device=input_ids.device, dtype=torch.float32)
dsl_carry_loss_accum = torch.zeros((), device=input_ids.device, dtype=torch.float32)
dsl_src_loss_accum = torch.zeros((), device=input_ids.device, dtype=torch.float32)
pred_loss_chunks = 0
dsl_loss_chunks = 0
for ci in range(N):
s, e = ci * Ln, (ci + 1) * Ln
toks = input_ids[:, s:e]
vm = valid_mask[:, s:e]
wm = write_mask[:, s:e]
rm = reset_mask[:, s:e] if reset_mask is not None else None
# Single sync per chunk: if this chunk has no sample boundary,
# null out reset_mask so all reset-aware paths (RayMix.step,
# RMU.prepare, VAttn._build_gates) take their fast no-reset
# branch without any further per-slab .item() calls. In
# packed batches, only chunks that actually contain a sample
# boundary need the slower reset-aware path.
if rm is not None and not bool(rm.any()):
rm = None
volume, _ = self.decoder.encode(toks, valid_mask=vm)
core_in = self.core_in_proj(volume)
if self.training and self.checkpoint_core:
(core_out_vol, carry, chunk_pred_loss,
chunk_dsl_exec_loss, chunk_dsl_carry_loss,
chunk_dsl_src_loss) = ckpt_chunk(
self, core_in, vm, wm, carry, reset_mask=rm,
)
else:
core_out_vol, carry = step(
self, core_in, vm, wm, carry, reset_mask=rm,
)
chunk_pred_loss = self._predictor_step_loss
chunk_dsl_exec_loss = self._dsl_exec_step_loss
chunk_dsl_carry_loss = self._dsl_carry_step_loss
chunk_dsl_src_loss = self._dsl_src_step_loss
pred_loss_accum = pred_loss_accum + chunk_pred_loss.float()
dsl_exec_loss_accum = dsl_exec_loss_accum + chunk_dsl_exec_loss.float()
dsl_carry_loss_accum = dsl_carry_loss_accum + chunk_dsl_carry_loss.float()
dsl_src_loss_accum = dsl_src_loss_accum + chunk_dsl_src_loss.float()
pred_loss_chunks += 1
dsl_loss_chunks += 1
# Avoid forcing fp32 activations during normal mixed-precision
# training. Only enforce dtype/device match when autocast is off
# (e.g. direct probe callers), where Conv3d requires exact match.
_core_out_w = self.core_out_proj.weight
if core_out_vol.device != _core_out_w.device:
core_out_vol = core_out_vol.to(device=_core_out_w.device)
if (not torch.is_autocast_enabled()) and (core_out_vol.dtype != _core_out_w.dtype):
core_out_vol = core_out_vol.to(dtype=_core_out_w.dtype)
core_out = self.core_out_proj(core_out_vol)
rays.append(self.decoder.vol_to_seq(core_out))
# Mean over chunks for the row; the trainer multiplies by
# `predictor_lambda` and adds to the total loss in `forward`.
denom = float(max(1, pred_loss_chunks))
self._predictor_row_loss = pred_loss_accum / denom
dsl_denom = float(max(1, dsl_loss_chunks))
self._dsl_exec_row_loss = dsl_exec_loss_accum / dsl_denom
self._dsl_carry_row_loss = dsl_carry_loss_accum / dsl_denom
self._dsl_src_row_loss = dsl_src_loss_accum / dsl_denom
return torch.cat(rays, dim=1)
def forward(self, input_ids: torch.Tensor,
target_ids: Optional[torch.Tensor] = None,
valid_mask: Optional[torch.Tensor] = None,
write_mask: Optional[torch.Tensor] = None,
reset_mask: Optional[torch.Tensor] = None,
learnorm_lambda: float = 0.0,
predictor_lambda: float = 0.0,
vector_error_lambda: float = 0.0,
dsl_exec_lambda: float = 0.0,
dsl_carry_lambda: float = 0.0,
dsl_source_lambda: float = 0.0,
return_loss_parts: bool = False):
"""Training forward.
Two modes:
(a) Packed mode [preferred for throughput]: caller provides
`valid_mask`, `write_mask`, `reset_mask`, `target_ids`
pre-computed by `packing.pack_messages_to_batch`. All four
tensors describe the row's multi-sample layout explicitly.
Boundary labels in `target_ids` are already pad-id so
`loss_chunked`'s `ignore_index=pad_id` silences cross-sample
next-token predictions without any extra work here.
(b) Legacy mode [backward compat]: if valid_mask / write_mask
are omitted, they are derived from `lengths_from_tokens` on
target_ids, producing the single-sample behavior used
before packing. `reset_mask` stays None, so all scan paths
behave bit-identically to the pre-packing training setup.
"""
if target_ids is None:
target_ids = input_ids
B, L = input_ids.shape
if valid_mask is None:
lengths = self.decoder.lengths_from_tokens(target_ids)
idx = torch.arange(L, device=input_ids.device).unsqueeze(0)
valid_mask = idx < lengths.unsqueeze(1)
if write_mask is None:
write_mask = self._make_write_mask(valid_mask, lengths)
elif write_mask is None:
# valid provided but no write: derive a safe default from
# valid_mask minus position 0 and the last valid position.
lengths = valid_mask.sum(dim=1).long()
write_mask = self._make_write_mask(valid_mask, lengths)
seq = self._forward_chunks(input_ids, valid_mask, write_mask, reset_mask=reset_mask)
ce_loss, learnorm_loss, vector_error_loss = self.decoder.ce_blocks(
seq,
target_ids,
chunk_size=self.loss_chunk_size,
return_parts=True,
vector_error_lambda=vector_error_lambda,
)
# PCSC: small BYOL-style regression loss on the slab predictor
# outputs. Without this term the predictor would only feel the
# CE gradient through the wg modulation, which is too indirect
# for it to actually fit `actual_first`. Coefficient stays small
# (default 0.01) because the per-slab MSE is on an O(1)-scale
# ray feature and CE on the LM head is on a similar scale.
predictor_loss = self._predictor_row_loss.to(ce_loss.dtype)
dsl_exec_loss = self._dsl_exec_row_loss.to(ce_loss.dtype)
dsl_carry_loss = self._dsl_carry_row_loss.to(ce_loss.dtype)
dsl_src_loss = self._dsl_src_row_loss.to(ce_loss.dtype)
total_loss = (
ce_loss
+ float(learnorm_lambda) * learnorm_loss
+ float(predictor_lambda) * predictor_loss
+ float(vector_error_lambda) * vector_error_loss
+ float(dsl_exec_lambda) * dsl_exec_loss
+ float(dsl_carry_lambda) * dsl_carry_loss
+ float(dsl_source_lambda) * dsl_src_loss
)
if return_loss_parts:
return {
'loss': total_loss,
'ce_loss': ce_loss,
'learnorm_loss': learnorm_loss,
'predictor_loss': predictor_loss,
'vector_error_loss': vector_error_loss,
'dsl_exec_loss': dsl_exec_loss,
'dsl_carry_loss': dsl_carry_loss,
'dsl_src_loss': dsl_src_loss,
}
return total_loss
def _load_yaml_config(path: str) -> Dict[str, object]:
"""Load a YAML training config into a flat dict.
The YAML file may use either the dotted CLI form or its bare names:
e.g. both ``batch_size: 8`` and ``batch-size: 8`` are accepted (we
normalize ``-`` to ``_``). Nested mappings are flattened with a dot
separator (``data.dir`` -> ``dataset_dir`` style is the caller's job;
we leave key names verbatim apart from the dash/underscore rule).
"""
try:
import yaml # PyYAML
except ImportError as exc:
raise RuntimeError(
f"--config={path!r} requires PyYAML. Install via `pip install pyyaml`."
) from exc
with open(path, 'r', encoding='utf-8') as f:
raw = yaml.safe_load(f)
if raw is None:
return {}
if not isinstance(raw, dict):
raise ValueError(f'YAML config {path!r} must be a top-level mapping')
flat: Dict[str, object] = {}
def _walk(prefix: str, node: object) -> None:
if isinstance(node, dict):
for k, v in node.items():
key = str(k).replace('-', '_')
full = f'{prefix}.{key}' if prefix else key
if isinstance(v, dict):
_walk(full, v)
else:
flat[full if prefix else key] = v
return
flat[prefix] = node
_walk('', raw)
# Drop any nested-style keys that didn't collapse to a top-level name —
# only top-level keys map to argparse dest names. (We still surface
# them in the warning so the user can tell.)
return {k: v for k, v in flat.items() if '.' not in k}
def main():
p = argparse.ArgumentParser()
# --config must be the first parsed argument so its values seed the
# parser defaults BEFORE the rest are parsed. Anything passed on the
# CLI still overrides YAML (argparse default precedence).
p.add_argument('--config', type=str, default='',
help='Path to a YAML training config. Every CLI flag (without '
'the leading --) is a valid YAML key, so the YAML can fully '
'replace command-line arguments. CLI flags still take '
'precedence over YAML when both are present.')
p.add_argument('--tokenizer_dir', type=str, default='tools/gwen2_tokenizer')
p.add_argument('--dataset_dir', type=str, default='datasets/smol-smoltalk_train')
p.add_argument('--pt', type=str, default='',
help='Path/spec for PT corpus. Implies --dataset_type string '
'and replaces --dataset_dir. Do not pass --dataset_dir '
'explicitly together with --pt.')
p.add_argument('--dataset_split', type=str, default='train',
help='Split name when loading Hugging Face datasets.')
p.add_argument('--dataset_text_column', type=str, default='auto',
help='Text column to wrap into messages for plain-text datasets.')
p.add_argument('--val_dataset', type=str, default='',
help='Optional explicit validation dataset path/spec. When unset, '
'training will auto-discover validation* alongside --dataset_dir, '
'or create a held-out validation_auto.parquet for local dataset dirs.')
p.add_argument('--val_fraction', type=float, default=0.001,
help='Fraction of a local training dataset to hold out automatically '
'for validation when --val_dataset is unset and no validation* '
'artifact exists already. Must be in (0,1). Default 0.001.')
p.add_argument('--val_seed', type=int, default=1337,
help='Deterministic seed for the automatic held-out validation split.')
p.add_argument('--dataset_type', type=str, default='messages',
choices=['messages', 'string'],
help='messages: chat-template packing with boundaries. '
'string: plain PT token stream, no user/messages semantics.')
p.add_argument(
'--string_tokenize_otf',
action=argparse.BooleanOptionalAction,
default=True,
help='For --dataset_type string with text datasets: tokenize on-the-fly in the '
'packing loop (default: true). Use --no-string_tokenize_otf to pretokenize '
'the full dataset via map() before training.',
)
p.add_argument(
'--modes',
type=str,
default='pt',
help='Comma-separated task modes. Supported: pt,nat,blind,rev,fim. '
'Default: pt. Example: --modes pt,nat,fim',
)
p.add_argument('--ptmode', action='store_true',
help='Enable PT objective mode (alias for adding "pt" to --modes).')
p.add_argument('--blindmode', action='store_true',
help='Enable blind objective mode (alias for adding "blind" to --modes).')
p.add_argument('--revmode', action='store_true',
help='Enable reverse objective mode (alias for adding "rev" to --modes).')
p.add_argument('--fimmode', action='store_true',
help='Enable FIM objective mode (alias for adding "fim" to --modes).')
p.add_argument('--nat_weight', type=float, default=1.5,
help='Loss weight for the NAT task relative to PT (default 1.5). '
'Total gradient contribution: nat_w/(1+nat_w). '
'Raise above 1.0 to counteract the model coasting on the '
'easier PT objective when objectives are co-directional.')
p.add_argument('--natmode', action='store_true',
help='Enable NAT (blind-tail prefix-completion) objective mode '
'(alias for adding "nat" to --modes). Closes the train/inference '
'gap of one-shot generate(): each packed sample gets a random '
'suffix replaced by PAD on input while target stays the real '
'tokens, training the regime where the model emits from ray '
'state alone. Equal weight with pt; no warmup ramp.')
p.add_argument('--ckpt_dir', type=str, default='ckpt')
p.add_argument('--device', type=str, default='cuda:0')
p.add_argument('--devices', type=str, default='',
help='DDP launcher mode. Set to "cudas" to use all visible CUDA '
'GPUs in full data-parallel mode (one process per GPU).')
p.add_argument('--seq_len', type=int, default=1024,
help='Native chunk width (== L_native). Volumetric grid '
'(g, h, w) is derived as the near-cube factorization '
'of seq_len, no manual grid knobs. Rows in training '
'are N*seq_len tokens where N is sampled per-batch '
'from 1..seq_len_mul (triangle-weighted). Pick '
'seq_len as a composite number (power of 2 works '
'cleanest): it factorizes exactly.')
p.add_argument('--seq_len_mul', type=int, default=1,
help='Max chunks-per-row multiplier. row_len = N * seq_len '
'where N ~ Triangle{1..seq_len_mul} (p(N) ~ N). Mul=1 '
'means single-chunk training (carry unused). Mul>=2 '
'activates carry and Container routing. Worst-case '
'VRAM peaks at N=mul; dry-run allocates at that peak '
'on init so OOM surfaces immediately.')
p.add_argument('--batch_size', type=int, default=8)
p.add_argument('--grad_accum_steps', type=int, default=1,
help='Number of microbatches to accumulate before each optimizer step. '
'Effective global batch = batch_size * grad_accum_steps. '
'Memory stays close to one microbatch because DDP all-reduce is '
'suppressed on intermediate microsteps and grads accumulate in-place.')
p.add_argument('--decoder_dim', type=int, default=32)
p.add_argument('--factor_dim', type=int, default=0,
help='ALBERT-style factorized embedding rank; 0 disables.')
p.add_argument('--core_dim', type=int, default=960)
p.add_argument('--virtual_dim', type=float, default=0.0,
help='Virtual manifold expansion in percent over core_dim. '
'0 disables. Example: --virtual_dim 14 means 1024 -> ~1168.')
p.add_argument('--num_slabs', type=int, default=3)
p.add_argument('--tau_fast', type=str, default='auto',
help='TXSSM fast-branch half-life in tokens. "auto" => seq_len/4.')
p.add_argument('--tau_slow', type=str, default='auto',
help='TXSSM slow-branch half-life in tokens. "auto" => seq_len.')
p.add_argument('--core_devices', type=str, default='',
help='Comma-separated devices for slabs (e.g. "cuda:0,cuda:1,cuda:2"). '
'Empty = use --device for all slabs. Slabs are round-robin distributed.')
p.add_argument('--max_steps', type=int, default=1000,
help='Number of optimization steps to run. On a fresh '
'run this is the absolute total. With --resume it '
'is ADDITIONAL: total_steps = ckpt_step + max_steps '
'(the LR schedule ends at the new total).')
p.add_argument('--lr', type=float, default=1e-4)
p.add_argument('--lr_min', type=float, default=-1.0,
help='Final LR reached by the late decay tail. Default = lr * 0.1.')
p.add_argument('--dtype', type=str, default='fp32', choices=list(DTYPE_NAMES),
help='Compute dtype inside the RMU core forward (bound/siloid/scan '
'are saturating and safe in low precision). Params, optimizer '
'states and vocab log-sum-exp stay fp32. fp8_e4m3/fp8_e5m2 are '
'reserved names; see dtypes.py for the pending pack/unpack path.')
p.add_argument('--param_dtype', type=str, default='auto', choices=['auto', 'fp32', 'fp16', 'bf16'],
help='Model parameter storage dtype. auto => follow --dtype for fp16/bf16, '
'else fp32.')
p.add_argument('--dtype_acts', type=str, default='fp32', choices=['fp32', 'fp16', 'bf16'],
help='Force-compute dtype for bounded activations (siloid, bound). '
'autocast bf16 does NOT convert log1p/sqrt/cos (fp32 whitelist), so '
'setting this to bf16/fp16 disables autocast inside those activations '
'and keeps the tensors in half precision — actually halving activation '
'memory. Safe because the output envelope is bounded independent of |x|.')
p.add_argument('--grads_dtype', type=str, default='fp32', choices=['fp32', 'fp16', 'bf16'],
help='Storage dtype for accumulated grads between backward passes in the '
'multi-task mixer. fp32 = default PyTorch behavior. bf16/fp16 = after '
'each task backward, param.grad is drained into a half-prec buffer and '
'upcast to fp32 only right before optimizer.step(). Halves the grad '
'footprint during the 3-task accumulation window.')
p.add_argument('--optim_mem_device', type=str, default='',
help='Unified memory-offload target for training-state tensors. '
'When set, this is used as the default target for: '
'(1) low-precision grad accumulator buffers, '
'(2) optimizer master/state offload if --optim_device is not set, '
'(3) autograd saved-tensor offload when the target is cpu. '
'Examples: "cpu", "cuda:1". Empty = keep everything local unless '
'other explicit offload flags are set.')
p.add_argument('--grads_device', type=str, default='',
help=argparse.SUPPRESS)
p.add_argument('--optim', type=str, default='atom', choices=['adamw', 'atom'],
help='Optimizer to use: adamw (torch.optim.AdamW) or atom (Atom). Default: atom.')
p.add_argument('--optim_device', type=str, default='',
help='Run optimizer master weights + states on this '
'device instead of the compute GPU. Biggest single VRAM win: '
'~12*P bytes removed from compute GPU for a P-param model. '
'Pair with --grads_device to keep the whole grad + optim pipeline '
'off the compute GPU. Works with both adamw and atom optimizers.')
p.add_argument('--optim_dtype', type=str, default='fp32', choices=['fp32', 'fp16', 'bf16'],
help='Master parameter dtype for offloaded optimizer copies. '
'Primarily useful with --optim_device cpu to reduce RAM.')
p.add_argument('--warmup_steps', type=int, default=50,
help='Linear LR warmup 0 -> lr over this many steps; then long hold and late smooth decay to lr_min.')
p.add_argument('--gqk_lr_scale', type=float, default=0.05)
p.add_argument('--gqk_warmup_steps', type=int, default=100)
p.add_argument('--emb_proj_lr_scale', type=float, default=0.3,
help='LR multiplier for the decoder factor->hidden lift '
'(emb_proj). Sensitivity probe shows it is ~3-5x '
'more loss-sensitive than slab interior matrices '
'because it sits on the forward path AND on the '
'tied readout, so an unscaled LR there dominates '
'effective updates and starves the rest. Default '
'0.3 ≈ 1/3 of base LR; set 1.0 to disable.')
p.add_argument('--decoder_norm_lr_scale', type=float, default=1.0,
help='LR multiplier for decoder.norm (LeaRNorm). Set <1 '
'if the LeaRNorm gain/beta drift dominates loss '
'descent. Default 1.0 (no scaling).')
p.add_argument('--weight_decay', type=float, default=0.01)
p.add_argument('--learnorm_lambda', type=float, default=0.05,
help='Aux loss weight for decoder LeaRNorm quality regularizer. '
'Total loss = CE + learnorm_lambda * learnorm_loss.')
p.add_argument('--predictor_lambda', type=float, default=0.01,
help='PCSC: aux MSE weight for per-slab state-predictor '
'(pred vs actual_first, BYOL target detached). The '
'predictor also receives gradient from CE through the '
'write-gate modulation path; this aux term is what '
'forces it to actually predict instead of collapsing '
'to a constant that only minimizes the modulation '
'response. 0 disables (predictor still runs but is '
'driven only by CE via modulation, which usually '
'collapses).')
p.add_argument('--vector_error_lambda', type=float, default=0.0,
help='Weight for embedding-space vector error objective. '
'Adds dense directional correction E[target]-E_p[pred] '
'on top of CE. 0 disables.')
p.add_argument('--predictor_lr_scale', type=float, default=1.0,
help='LR multiplier for per-slab PCSC predictor groups '
'(state_predictor.* + surprise_gain).')
p.add_argument('--grad_clip', type=float, default=1.0)
p.add_argument('--save_every', type=int, default=100)
p.add_argument('--log_every', type=int, default=10)
p.add_argument('--update_stats_every', type=int, default=0,
help='Cadence (in optimizer steps) at which the per-group '
'L1-update ratio (`upd=…ppm` fields) is computed. '
'Computing the ratio requires snapshotting EVERY model '
'parameter before the step (~param-dtype × num_params '
'extra alloc + a full elementwise pass) — for a 372M '
'model that is ~700MB and ~30ms per step, dominating '
'wall time when log_every is small. 0 = follow '
'log_every (legacy behavior). >0 = independent '
'cadence; on non-snapshot logs the `upd` fields are '
'omitted from the rendered table.')
p.add_argument('--loss_chunk_size', type=int, default=1024)
p.add_argument('--checkpoint_core', type=int, default=1)
p.add_argument('--num_workers', type=int, default=2)
p.add_argument('--container_k', type=int, default=0,
help='Internal Container controller state dim (per batch row). 0 = '
'disabled (default, pre-Container behavior, bit-identical). '
'Recommended 512..2048 when enabled. Container acts as an '
'inter-chunk router: updates once per chunk, stopgrad at '
'chunk boundary (no cross-chunk BPTT), influences the NEXT '
'chunks per-slab write-amplitude and scan timescale biases. '
'Requires multi-chunk training rows, i.e. seq_len_mul > 1, '
'to see any routing signal.')
p.add_argument('--skip_slabs', action='store_true',
help='Allow Container to hard-skip middle slabs per batch/chunk. '
'First and last slab stay always active.')
p.add_argument('--skip_units', type=float, default=0.2,
help='Maximum fraction of per-slab units Container can hard-skip. '
'Range [0,1). Default 0.2.')
p.add_argument('--container_lr_scale', type=float, default=1.0,
help='LR multiplier for all Container trainable groups.')
p.add_argument('--cont_layers', type=int, default=1,
help='Container routing trunk depth. 1 = direct heads from state; >1 adds MLP trunk.')
p.add_argument('--use_dsl', action=argparse.BooleanOptionalAction, default=False,
help='Physically instantiate and execute Thought DSL. '
'When false, DSL modules are not built even if dsl_lanes is non-zero.')
p.add_argument('--dsl_lanes', type=int, default=0,
help='Thought DSL command lanes per slab. 0 disables the internal '
'typed tensor-program language.')
p.add_argument('--dsl_rank', type=int, default=64,
help='Low-rank width used by Thought DSL queries/answers.')
p.add_argument('--dsl_max_sources', type=str, default='16',
help='Maximum number of causal trace streams visible to each DSL '
'interpreter. Pass an integer or the string "all" to expose every '
'receiver published in the chunk-level registry; "all" resolves '
'to num_slabs * 8 at startup.')
p.add_argument('--dsl_max_sources_per_exec', type=int, default=0,
help='Top-K hard cap on simultaneously firing receivers per (batch, '
'lane) per DSL step. 0 disables the cap (limited only by '
'dsl_max_sources). With a large source vocabulary this is the '
'knob that bounds peak DSL VRAM.')
p.add_argument('--dsl_source_lambda', type=float, default=0.0,
help='L0-style regularizer on Hard Concrete source-open '
'probabilities. Encourages each DSL step to keep only '
'a few receivers open. 0 disables.')
p.add_argument('--dsl_steps', type=int, default=2,
help='Number of recurrent internal program steps executed per slab.')
p.add_argument('--dsl_log', action=argparse.BooleanOptionalAction, default=False,
help='Print a separate Thought DSL program/answer box under the main log.')
p.add_argument('--dsl_lr_scale', type=float, default=0.5,
help='LR multiplier for per-slab Thought DSL parameters.')
p.add_argument('--dsl_exec_lambda', type=float, default=0.0,
help='Tiny usage penalty on Thought DSL execute probabilities. '
'Encourages sparse/noop programs. 0 disables.')
p.add_argument('--dsl_carry_lambda', type=float, default=0.0,
help='Tiny usage penalty on Thought DSL answer-carry probabilities. '
'Encourages carrying only useful receiver outputs. 0 disables.')
p.add_argument('--xmem_size', type=float, default=0.0,
help='Total XMem long-term memory budget (MB, bf16) per batch '
'item for the shared bank. 0 = disabled (default). The N-dim memory tensor has '
'shape [B, mem_dim, A^N] where mem_dim defaults to '
'max(32, core_dim/8), N=6 axes (twice VAttn spatial). The axis '
'size A is solved from the budget: A = floor((MB·5e5/mem_dim)^(1/N)). '
'Tape footprint during training is approximately '
'xmem_size · batch_size · seq_len_mul MB; with defaults at '
'xmem_size=10 MB, batch=44, mul=4 that is ~1.8 GB extra '
'activation tape. Increase only when there is '
'headroom — the memory geometry scales exponentially in N, so '
'A drop below 4 around xmem_size~0.5 MB.')
p.add_argument('--resume', type=str, default='',
help='Path to a checkpoint (.pt) to resume from. Loads model+optimizer '
'state and continues from the saved step counter. When set, the '
'model and optimizer state take precedence over their current '
'initialization; architectural shapes (rmu_cfg/dec_cfg) must still '
'match the CLI-provided ones.')
p.add_argument('--dtype_audit_steps', type=int, default=0,
help='If >0, collect per-module output dtype stats for first N steps '
'and print top fp32 output producers (rank0).')
# Two-pass parsing: first pull out --config, load YAML, and push its
# values into parser defaults; then parse the full CLI so explicit
# flags override YAML. This is the standard argparse+YAML pattern
# that preserves --help and unknown-arg detection.
pre_args, _ = p.parse_known_args()
if pre_args.config:
yaml_defaults = _load_yaml_config(pre_args.config)
unknown = [k for k in yaml_defaults if k not in {a.dest for a in p._actions}]
if unknown:
raise ValueError(
f"YAML config {pre_args.config!r} contains keys not matching any "
f"CLI flag: {sorted(unknown)}"
)
p.set_defaults(**yaml_defaults)
args = p.parse_args()
# Resolve --dsl_max_sources: accept either a positive integer (as a
# string) or the literal "all". "all" means "expose every published
# receiver" and resolves to a compile-time cap that fits the actual
# per-slab publication density. Each slab publishes ~16 receivers
# (4 state streams + 4-5 bank executors + 4 vattn projections +
# optional xmem + 2 trailing cml streams visible only to later
# slabs); see `@inference.py:_process_chunk` publish() calls.
# The `command` head and `source_slot` table are sized to this cap.
_raw_msrc = str(getattr(args, 'dsl_max_sources', '16')).strip().lower()
if _raw_msrc in ('all', 'auto', '*'):
args.dsl_max_sources = max(16, int(args.num_slabs) * 16)
else:
try:
args.dsl_max_sources = int(_raw_msrc)
except ValueError as _exc:
raise ValueError(
f"--dsl_max_sources must be a positive integer or 'all', got {_raw_msrc!r}"
) from _exc
if int(args.dsl_max_sources) <= 0:
raise ValueError(f"--dsl_max_sources must be > 0, got {args.dsl_max_sources}")
# Backward-compat alias: grads_device -> optim_mem_device.
_legacy_grads_dev = str(getattr(args, 'grads_device', '') or '').strip()
_optim_mem_dev = str(getattr(args, 'optim_mem_device', '') or '').strip()
if _legacy_grads_dev and _optim_mem_dev and _legacy_grads_dev != _optim_mem_dev:
raise ValueError(
f'--grads_device={_legacy_grads_dev} conflicts with '
f'--optim_mem_device={_optim_mem_dev}; use only optim_mem_device'
)
if not _optim_mem_dev and _legacy_grads_dev:
args.optim_mem_device = _legacy_grads_dev
args.grads_device = str(getattr(args, 'optim_mem_device', '') or '')
# PT alias mode: reuse the existing string data-path.
if args.pt:
if '--dataset_dir' in sys.argv:
raise ValueError('--pt excludes explicit --dataset_dir; pass only one data source')
if '--dataset_type' in sys.argv and args.dataset_type != 'string':
raise ValueError('--pt requires --dataset_type string (or omit --dataset_type)')
args.dataset_dir = args.pt
args.dataset_type = 'string'
modes = _parse_modes(args.modes)
if args.ptmode:
modes.add('pt')
if args.blindmode:
modes.add('blind')
if args.revmode:
modes.add('rev')
if args.fimmode:
modes.add('fim')
if args.natmode:
modes.add('nat')
if not modes:
raise ValueError('no active training modes; enable at least one mode')
# PT mode now supports both string and messages dataset types.
# For messages, only assistant content is used (see _assistant_only_pt_ids in packing.py).
# UX alias: allow --device cudas as a shorthand for full DDP launch.
if args.device == 'cudas':
args.devices = 'cudas'
args.device = 'cuda:0'
if args.devices and args.devices != 'cudas':
raise ValueError(f'--devices supports only "cudas" (got {args.devices})')
if not (0.0 <= args.skip_units < 1.0):
raise ValueError(f'--skip_units must be in [0,1), got {args.skip_units}')
if args.val_dataset and args.val_fraction <= 0:
raise ValueError(f'--val_fraction must be > 0 when provided; got {args.val_fraction}')
if not args.val_dataset and not (0.0 < args.val_fraction < 1.0):
raise ValueError(f'--val_fraction must be in (0,1) for auto validation split; got {args.val_fraction}')
if (args.device.startswith('cuda') or args.devices == 'cudas') and not torch.cuda.is_available():
raise RuntimeError('CUDA unavailable')
if args.devices == 'cudas' and 'WORLD_SIZE' not in os.environ:
n = torch.cuda.device_count()
if n < 2:
raise RuntimeError('--devices cudas requested but fewer than 2 CUDA GPUs visible')
cmd = ['torchrun', '--standalone', '--nproc_per_node', str(n), __file__] + sys.argv[1:]
env = os.environ.copy()
if not env.get('OMP_NUM_THREADS'):
env['OMP_NUM_THREADS'] = str(_auto_omp_threads(n))
print('[ddp] launching:', ' '.join(cmd), flush=True)
if 'OMP_NUM_THREADS' in env and not os.environ.get('OMP_NUM_THREADS'):
print(f'[ddp] auto OMP_NUM_THREADS={env["OMP_NUM_THREADS"]}', flush=True)
raise SystemExit(subprocess.call(cmd, env=env))
world_size = int(os.environ.get('WORLD_SIZE', '1'))
distributed = world_size > 1
rank = int(os.environ.get('RANK', '0'))
local_rank = int(os.environ.get('LOCAL_RANK', '0'))
is_main = (rank == 0)
if world_size > 1 and not os.environ.get('OMP_NUM_THREADS'):
os.environ['OMP_NUM_THREADS'] = str(_auto_omp_threads(world_size))
if distributed:
dist.init_process_group(backend='nccl')
torch.cuda.set_device(local_rank)
args.device = f'cuda:{local_rank}'
if args.batch_size < world_size:
raise ValueError(
f'--batch_size={args.batch_size} is global in DDP mode and must be >= world_size={world_size}'
)
if args.batch_size % world_size != 0:
raise ValueError(
f'--batch_size={args.batch_size} must be divisible by world_size={world_size} in DDP mode'
)
global_batch_size = args.batch_size