File size: 8,124 Bytes
bcc8d35 | 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 | #!/usr/bin/env python
"""Pretraining stage entry (section-14): gates -> wrap -> train -> checkpoint.
torchrun --nproc_per_node=4 scripts/pretrain.py configs/pretrain.toml --optimizer.lr=3e-5
The Pretraining pillar, OLMoE-shaped and honestly toy-scaled: continued
pretraining of the base model on a capped (~50M token), STREAMED slice of a
raw-text corpus, packed into the repo's shard format by
align/data/pretrain_corpus.py. The objective is the SAME packed next-token NLL
the SFT stage computes -- every pretrain row stores prompt_len 0, so
`collate_sft` labels every token (labels == input_ids, no masking) and
`make_sft_loss_adapter` runs unchanged. One loss path, two stages.
Thin by design, mirroring scripts/sft.py: everything this drives is built and
tested elsewhere; what lives here is ORDER:
build_currency -> smoke_7b -> probe_20step -> rss_preflight -> train
Differences from the SFT entry, each deliberate:
* NO golden-regression bridge. The golden anchor certifies the SFT eval
stack (MATH-500/GSM8K extraction against known scores); a base-style
pretrain checkpoint has no such anchor. configs/pretrain.toml sets
gates.require_golden_receipt: false, which run_entry_gates honors with a
loud warning rather than silence.
* NO accuracy quality gate at exit. The stage's product is a checkpoint and
its train-NLL trajectory; downstream SFT is its consumer and its judge.
IMPORT ORDER IS LOAD-BEARING. `apply_resource_guards()` sets
PYTORCH_CUDA_ALLOC_CONF, the /apps cache paths and the thread caps, and torch
reads all of them AT IMPORT. So this module imports only stdlib, the guard,
the config, and `stage_gates` (which is asserted torch-free); torch and every
align module that pulls it in are imported INSIDE main(), after the guards run.
tests/test_pretrain_entry.py enforces this by AST, exactly as the SFT test
does for scripts/sft.py.
Resume semantics: rerunning after a kill picks up the gate ledger, skips gates
that already passed, and resumes the Trainer from the latest micro-step
checkpoint. Partial checkpoints are deleted at startup.
"""
from __future__ import annotations
import logging
import os
import sys
from pathlib import Path
# The package is not installed in the venv; without this a bare `python scripts/pretrain.py` (the documented
# launch line) dies with ModuleNotFoundError unless PYTHONPATH is exported (same fix as scripts/sft.py).
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from align.config import load_config
from align.train.stage_gates import (
GateLedger,
assert_build_is_current,
assert_probe_resolved,
eval_source_hash,
parse_args,
run_entry_gates,
)
from align.utils.guard import apply_resource_guards
logger = logging.getLogger(__name__)
def build_model(cfg):
"""Imported here, not at module scope: this pulls torch in."""
from align.train.stage_smoke import build_stage_model
return build_stage_model(cfg)
def run_gates(cfg, ledger):
"""The entry sequence. Refuses to proceed on a stale or unstamped corpus
build before it touches a GPU. The golden bridge is intentionally absent
(see module docstring); run_entry_gates still calls require_golden_receipt
internally, which configs/pretrain.toml downgrades to a warning via
gates.require_golden_receipt: false."""
assert_build_is_current(cfg.data.manifest_path, cfg)
banner = run_entry_gates(cfg, ledger)
assert_probe_resolved(banner)
return banner
def train(cfg, ledger, model, banner):
"""One pretraining epoch through the section-07 Trainer.
Identical wiring to the SFT stage on purpose: same sampler construction
(the SAME sampler the entry gates measured the horizon from), same
collate, same fused loss adapter. The pretraining objective difference
lives entirely in the data -- prompt_len 0 makes collate_sft emit
labels == input_ids."""
from align.data.loader import collate_sft
from align.train.sft_loss import make_sft_loss_adapter
from align.train.stage_smoke import build_training_sampler
from align.train.trainer import Trainer
sampler, dataset = build_training_sampler(cfg)
def adapter(m, batch):
rows = [dataset[i] for i in batch]
collated = collate_sft(rows, pad_token_id=cfg.data.pad_token_id).to("cuda")
return make_sft_loss_adapter(require_fused=True)(m, (collated.input_ids, collated.labels))
trainer = Trainer(
cfg=cfg,
model=model,
sampler=sampler,
loss_adapter=adapter,
run_dir=os.path.join(cfg.paths.checkpoint_dir, "pretrain"),
stage="pretrain",
)
trainer.resume_from_latest()
summary = trainer.train()
record_train_gate(ledger, trainer, summary)
return trainer
def record_train_gate(ledger, trainer, summary) -> dict:
"""Record the 'train' gate for what `trainer.train()` ACTUALLY did (same contract as
scripts/sft.py::record_train_gate; duplicated because scripts/ is not a package).
THE STOPPED-RECORDED-AS-PASSED DEFECT (H2 review, 2026-10-13; the sibling the first
fix missed). `train()` returns normally after a COORDINATED STOP (SIGTERM or
train.max_wall_seconds) and this used to record `passed=True` unconditionally, so the
ledger certified a half-trained checkpoint as a finished stage while `main` exited
128+n / 82. A stopped run is resumable, neither finished nor failed; the ledger has no
third state, so it is `passed=False` with a `STOPPED (resumable)` detail, and the real
verdict overwrites it when the resumed run finishes.
"""
step = summary.get("optimizer_step")
if getattr(trainer, "stop_exit_code", 0):
return ledger.record(
"train",
passed=False,
detail=f"STOPPED (resumable): {trainer.stop_reason} stop at optimizer_step={step}; "
"checkpointed, not finished",
)
return ledger.record("train", passed=True, detail=str(step))
def main(argv=None) -> int:
args = parse_args(argv)
# FIRST: every env var torch reads at import, plus the heavy-job flock and
# the /apps path assertions. `heavy=True` also runs the host preflight;
# `rss_cfg` adds the full-FT RSS projection on top of the generic floor.
cfg = load_config(args.config, args.overrides)
apply_resource_guards(cpu_threads=8, heavy=True, label="pretrain-stage-14", rss_cfg=cfg)
logging.basicConfig(
level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s"
)
# BEFORE anything touches CUDA -- the duplicate-GPU defect (see
# scripts/sft.py): torchrun assigns no devices, so every rank's `.cuda()`
# lands on device 0 unless init_distributed sets one per local rank.
from align.utils.dist import init_distributed
run_dir = os.path.join(cfg.paths.checkpoint_dir, "pretrain")
# run_dir: the NCCL flight-recorder dump lands under it (never /tmp).
init_distributed(run_dir=run_dir)
from align.train.stage_gates import assert_distinct_devices
assert_distinct_devices()
ledger = GateLedger(run_dir, eval_source_hash=eval_source_hash())
banner = run_gates(cfg, ledger)
from align.registry import render_banner
print(render_banner(banner, title=f"align pretrain stage :: {cfg.run_name}"))
model = build_model(cfg)
trainer = train(cfg, ledger, model, banner)
if trainer.stop_exit_code:
# A coordinated stop (SIGTERM / train.max_wall_seconds) saved and
# returned: the stage is NOT finished, so do not fall into its
# post-train eval or announce completion. 82 = wall budget (the
# supervisor will not relaunch it), 128+n = the signal.
print(f"{trainer.stop_reason} stop: checkpointed, exiting {trainer.stop_exit_code}", file=sys.stderr)
return trainer.stop_exit_code
checkpoint = trainer.latest_checkpoint()
print(f"Pretrain stage complete. SFT base checkpoint: {checkpoint}")
return 0
if __name__ == "__main__":
from align.exit_codes import run_with_exit_codes
raise SystemExit(run_with_exit_codes(main))
|