"""Stage 1 continual pre-training (PRIVATE to Training_Module). CPU tiers (2 and 3): Layer-by-Layer ELEVATOR unfreezing - only 2 transformer layers are trainable at any moment (promoted to fp32), all the others stay frozen. The window rides from layer 0 up to layer N-1. In tier 3 the window layers are pinned in the ring loader and persisted to disk when the elevator leaves them. GPU tier (1) : rsLoRA on the 4-bit NF4 base model. Data: ./cleaned_data/pretrain_clean.jsonl -> ~75% repo text chunks + ~25% rational/math/identity chunks """ from __future__ import annotations import contextlib import gc import json import math import random import time from pathlib import Path from typing import Callable, Dict, Iterator, List, Optional import torch LORA_TARGETS = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"] # ------------------------------------------------------------------- helpers def model_device(model) -> torch.device: try: return next(model.parameters()).device except StopIteration: return torch.device("cpu") def make_amp(plan: Dict): """Return (autocast_factory, grad_scaler_or_None) for the plan.""" dtype = plan["dtype"] if plan["tier"] == 1 and torch.cuda.is_available(): dt = getattr(torch, dtype) scaler = None if dt == torch.float16: try: scaler = torch.amp.GradScaler("cuda") except Exception: scaler = torch.cuda.amp.GradScaler() return (lambda: torch.autocast("cuda", dtype=dt)), scaler if dtype == "bfloat16": return (lambda: torch.autocast("cpu", dtype=torch.bfloat16)), None return (lambda: contextlib.nullcontext()), None def find_layers(model): base = model.get_base_model() if hasattr(model, "get_base_model") else model return base.model.layers def train_loop(model, params: List[torch.nn.Parameter], batches: Iterator[Dict], loss_fn: Callable, steps: int, lr: float, accum: int, amp, scaler, tag: str, log_every: int = 1) -> float: """Generic AdamW loop with warmup + cosine decay, grad clipping and ETA logging.""" opt = torch.optim.AdamW(params, lr=lr, weight_decay=0.0, betas=(0.9, 0.95)) warm = max(1, int(0.1 * steps)) def lr_at(step: int) -> float: if step < warm: return (step + 1) / warm progress = (step - warm) / max(1, steps - warm) return 0.1 + 0.9 * 0.5 * (1 + math.cos(math.pi * progress)) sched = torch.optim.lr_scheduler.LambdaLR(opt, lr_at) model.train() started = time.time() history: List[float] = [] for step in range(steps): total = 0.0 for _ in range(accum): batch = next(batches) with amp(): loss = loss_fn(model, batch) / accum if scaler is not None: scaler.scale(loss).backward() else: loss.backward() total += float(loss.detach()) if scaler is not None: scaler.unscale_(opt) torch.nn.utils.clip_grad_norm_(params, 1.0) if scaler is not None: scaler.step(opt) scaler.update() else: opt.step() sched.step() opt.zero_grad(set_to_none=True) history.append(total) if (step + 1) % log_every == 0 or step == steps - 1: elapsed = time.time() - started eta = elapsed / (step + 1) * (steps - step - 1) print(f" [{tag}] step {step + 1:>4}/{steps} loss {total:.4f} lr {opt.param_groups[0]['lr']:.2e} " f"elapsed {elapsed:5.0f}s eta {eta:5.0f}s") del opt, sched gc.collect() tail = history[len(history) // 2:] or history return sum(tail) / max(len(tail), 1) # -------------------------------------------------------------------- data def build_chunks(tokenizer, path: Path, seq_len: int, max_chunks: int, conv_ratio: float, seed: int = 1337, max_chunks_per_doc: int = 8) -> List[List[int]]: """Tokenise pretrain_clean.jsonl into fixed windows; chat.jsonl chunks make up ~conv_ratio.""" eos = tokenizer.convert_tokens_to_ids("<|endoftext|>") if eos is None or eos == tokenizer.unk_token_id: eos = tokenizer.eos_token_id n_chat_target = int(round(max_chunks * conv_ratio)) n_repo_target = max_chunks - n_chat_target repo_chunks: List[List[int]] = [] chat_chunks: List[List[int]] = [] buffers = {"repo": [], "chat": []} with open(path, "r", encoding="utf-8") as handle: for line in handle: rec = json.loads(line) kind = "chat" if rec.get("source") == "chat.jsonl" else "repo" bucket = chat_chunks if kind == "chat" else repo_chunks target = n_chat_target if kind == "chat" else n_repo_target if len(bucket) >= max(target, 1) * (3 if kind == "chat" else 1): if len(repo_chunks) >= n_repo_target and len(chat_chunks) >= n_chat_target: break continue ids = tokenizer(rec["text"], add_special_tokens=False, truncation=False)["input_ids"] ids = ids[: seq_len * max_chunks_per_doc] + [eos] buf = buffers[kind] buf.extend(ids) while len(buf) >= seq_len: bucket.append(buf[:seq_len]) del buf[:seq_len] rng = random.Random(seed) rng.shuffle(repo_chunks) rng.shuffle(chat_chunks) repo_part = repo_chunks[:n_repo_target] if not chat_chunks: chat_part: List[List[int]] = [] elif not repo_part: chat_part = [chat_chunks[i % len(chat_chunks)] for i in range(max_chunks)] else: chat_part = [chat_chunks[i % len(chat_chunks)] for i in range(n_chat_target)] mixed = repo_part + chat_part rng.shuffle(mixed) share = 100.0 * len(chat_part) / max(len(mixed), 1) print(f" [pretrain-data] {len(mixed)} chunks of {seq_len} tokens " f"(repo={len(repo_part)}, chat={len(chat_part)} -> {share:.0f}% conversation share)") return mixed def batch_stream(chunks: List[List[int]], batch_size: int, device, seed: int = 7) -> Iterator[Dict]: rng = random.Random(seed) order: List[int] = [] while True: if len(order) < batch_size: fresh = list(range(len(chunks))) rng.shuffle(fresh) order.extend(fresh) take, order = order[:batch_size], order[batch_size:] ids = torch.tensor([chunks[i] for i in take], dtype=torch.long, device=device) yield {"input_ids": ids, "attention_mask": torch.ones_like(ids), "labels": ids.clone()} def lm_loss(model, batch): out = model(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"], labels=batch["labels"]) return out.loss # -------------------------------------------------------------------- LoRA def attach_lora(model, plan: Dict, rank: int, rslora: bool, resized: bool, adapter_dir: Optional[str] = None, save_resized_modules: bool = True): from peft import LoraConfig, PeftModel, get_peft_model, prepare_model_for_kbit_training if plan.get("quant") == "nf4": model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=True, gradient_checkpointing_kwargs={"use_reentrant": False}) if adapter_dir: print(f" [lora] continuing adapter from {adapter_dir}") return PeftModel.from_pretrained(model, adapter_dir, is_trainable=True) cfg = LoraConfig(r=rank, lora_alpha=rank if rslora else 2 * rank, lora_dropout=0.05, target_modules=LORA_TARGETS, bias="none", task_type="CAUSAL_LM", use_rslora=rslora, modules_to_save=["embed_tokens", "lm_head"] if (resized and save_resized_modules) else None) model = get_peft_model(model, cfg) trainable = sum(p.numel() for p in model.parameters() if p.requires_grad) print(f" [lora] {'rsLoRA' if rslora else 'LoRA'} r={rank}: {trainable / 1e6:.2f}M trainable params") return model def save_stage1_checkpoint(model, tokenizer, out_dir: Path, cfg: Dict, kind: str) -> Path: """Write Stage 1 to a sibling temp directory and publish it without deleting an existing checkpoint.""" out_dir = Path(out_dir) tmp_dir = out_dir.with_name(out_dir.name + ".tmp") if out_dir.exists(): raise FileExistsError(f"refusing to overwrite protected Stage-1 checkpoint: {out_dir}") if tmp_dir.exists(): import shutil shutil.rmtree(tmp_dir, ignore_errors=True) tmp_dir.mkdir(parents=True, exist_ok=True) ring = getattr(model, "_rss_ring", None) if kind == "adapter": model.save_pretrained(str(tmp_dir / "adapter"), safe_serialization=True) elif ring is not None: ring.export(tmp_dir) else: model.to(torch.bfloat16) model.save_pretrained(str(tmp_dir), safe_serialization=True) tokenizer.save_pretrained(str(tmp_dir)) (tmp_dir / "rss_meta.json").write_text(json.dumps({ "stage": 1, "kind": kind, "base_model": cfg["base_model_repo"], "adapter_dir": "adapter" if kind == "adapter" else None, "created": time.time()}, indent=1), encoding="utf-8") out_dir.parent.mkdir(parents=True, exist_ok=True) import os os.replace(tmp_dir, out_dir) print(f" [stage1] checkpoint atomically saved -> {out_dir} ({kind})", flush=True) return out_dir # --------------------------------------------------------------- the stage def run_pretrain(model, tokenizer, plan: Dict, cfg: Dict, surgery: Dict, pretrain_path: Path, checkpoint_dir: Path) -> Dict: tune = cfg["tune"] gpu = plan["tier"] == 1 seq_len = tune.get("pretrain_seq_len") or (512 if gpu else 192) max_chunks = tune.get("pretrain_max_chunks") or (2000 if gpu else 96) batch_size = tune.get("pretrain_batch_size") or (4 if gpu else 2) lr = tune.get("pretrain_lr") or (2e-4 if gpu else 2e-5) chunks = build_chunks(tokenizer, pretrain_path, seq_len, max_chunks, cfg.get("conv_ratio", 0.25)) if not chunks: raise RuntimeError("no pre-training chunks could be built from pretrain_clean.jsonl") amp, scaler = make_amp(plan) model.config.use_cache = False device = model_device(model) ring = getattr(model, "_rss_ring", None) batches = batch_stream(chunks, batch_size, device) if gpu: model = attach_lora(model, plan, int(tune.get("lora_rank", 16)), True, surgery.get("resized", False)) steps = int(tune.get("pretrain_steps") or min(300, max(20, len(chunks) // batch_size))) params = [p for p in model.parameters() if p.requires_grad] print(f"[Pretrain] Stage 1 (rsLoRA, 4-bit) for {steps} steps") loss = train_loop(model, params, batches, lm_loss, steps, lr, 1, amp, scaler, "cpt-lora", log_every=5) save_stage1_checkpoint(model, tokenizer, checkpoint_dir, cfg, "adapter") return {"kind": "adapter", "loss": loss, "checkpoint": str(checkpoint_dir)} # ---------------- CPU elevator ---------------- layers = find_layers(model) total_layers = len(layers) window = 2 steps_per_window = int(tune.get("pretrain_steps_per_window") or 4) base_dtype = next(model.parameters()).dtype for p in model.parameters(): p.requires_grad_(False) if ring is not None: model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False}) print(f"[Pretrain] Stage 1 elevator: {total_layers} layers, window={window}, {steps_per_window} steps/window, " f"seq={seq_len} batch={batch_size} lr={lr}") window_losses = [] for start in range(0, total_layers, window): idxs = list(range(start, min(total_layers, start + window))) if ring is not None: ring.pin(idxs) params: List[torch.nn.Parameter] = [] for i in idxs: if base_dtype != torch.float32: layers[i].to(torch.float32) for p in layers[i].parameters(): p.requires_grad_(True) params.append(p) print(f" [elevator] floor {idxs[0]}-{idxs[-1]} unfrozen " f"({sum(p.numel() for p in params) / 1e6:.1f}M params)" + (f" | {ring.describe()}" if ring else "")) loss = train_loop(model, params, batches, lm_loss, steps_per_window, lr, 1, amp, scaler, f"elevator {idxs[0]}-{idxs[-1]}", log_every=max(1, steps_per_window)) window_losses.append(loss) for i in idxs: for p in layers[i].parameters(): p.requires_grad_(False) if base_dtype != torch.float32: layers[i].to(base_dtype) if ring is not None: ring.unpin(idxs, save=True) del params gc.collect() if ring is not None: model.gradient_checkpointing_disable() model.eval() save_stage1_checkpoint(model, tokenizer, checkpoint_dir, cfg, "full") return {"kind": "full", "loss": sum(window_losses) / max(len(window_losses), 1), "checkpoint": str(checkpoint_dir)}