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))