"""Dedicated V4-C1 full-parameter conversion + LM replay training.""" import copy import hashlib import json import math from pathlib import Path import random import signal import time import torch from vimeml.posttraining.data import ConversionDataset, ConversionCodec, TOKENIZER_SHA from vimeml.posttraining.infer import read_snapshot, fp32_evaluation, evaluate_snapshot from vimeml.posttraining.runtime import (STAGE, TASK_CONTRACT, initialize_v4, update, initial_state, apply_epoch_result, restore_checkpoint) from vimeml.posttraining.streams import ConversionWindows, CommittedStream, loader_for from vimeml.training.data import SentenceWindowDataset, file_sha, write_json from vimeml.training.data_v2 import PrefixCropWindowDataset from vimeml.training.model_v4 import TinyGPTV4, GPTV4Config from vimeml.training.train import (optimizer_for, learning_rate, atomic_checkpoint, rng_state, restore_rng, resolve_precision) ROOT = Path(__file__).resolve().parents[3] MODEL_FORMAT = "vimeml_tiny_gpt_v4" def read_json(path): return json.loads(Path(path).read_text(encoding="utf-8")) def code_signature(root=ROOT): paths = sorted((root / "src/vimeml/posttraining").glob("*.py")) paths += [root / f"src/vimeml/training/{name}.py" for name in ( "train", "data", "data_v2", "model_v2", "model_v4", "runtime_v2", "model_factory", "model_factory_v4")] paths += [root / "src/vimeml/tokenizer/store.py"] paths += [root / "scripts/posttraining/train.py"] hashes = {p.relative_to(root).as_posix(): file_sha(p) for p in paths} return hashlib.sha256(json.dumps(hashes, sort_keys=True).encode()).hexdigest(), hashes def build_plan(config, root=ROOT, verify_files=False): """CPU-only identity/config preflight; no model, optimizer, output or W&B.""" root = Path(root) if config.get("architecture") != "tiny_gpt_v4" or config.get("stage") != STAGE: raise ValueError("Use the dedicated V4-C1 configuration") model = GPTV4Config(**config["model"]) settings = dict(config["training"]) if settings.get("lm_retention_policy", "enforce") not in ("enforce", "monitor"): raise ValueError("Unknown LM retention policy") positive_ints = ("epochs", "conversion_batch", "lm_batch", "conversion_microbatch", "lm_microbatch", "bucket_multiplier", "cpu_threads", "log_every", "eval_every", "checkpoint_every", "validation_batch", "validation_subset", "early_stopping_patience", "lm_degradation_patience") if any(type(settings.get(k)) is not int or settings[k] < 1 for k in positive_ints): raise ValueError("Task sizes, intervals, epochs and patience must be positive integers") if type(settings["num_workers"]) is not int or settings["num_workers"] < 0: raise ValueError("Invalid worker count") if type(settings.get("validation_workers", 0)) is not int or settings.get("validation_workers", 0) < 0: raise ValueError("Invalid validation worker count") if type(settings["seed"]) is not int or not 0 <= settings["seed"] < 2**63: raise ValueError("Seed must be a nonnegative int64") scalars = ("conversion_weight", "lm_weight", "learning_rate", "min_learning_rate", "warmup_ratio", "weight_decay", "grad_clip", "beta1", "beta2", "early_stopping_relative_delta", "baseline_lm_bpc", "lm_relative_degradation", "prefix_crop_probability") if any(not math.isfinite(settings[k]) for k in scalars): raise ValueError("Training scalars must be finite") if (not 0 < settings["conversion_weight"] < 1 or not 0 < settings["lm_weight"] < 1 or abs(settings["conversion_weight"] + settings["lm_weight"] - 1) > 1e-9 or not 0 < settings["min_learning_rate"] <= settings["learning_rate"] or not 0 <= settings["warmup_ratio"] < 1 or settings["weight_decay"] < 0 or settings["grad_clip"] <= 0 or not 0 <= settings["beta1"] < 1 or not 0 <= settings["beta2"] < 1 or not 0 <= settings["early_stopping_relative_delta"] < 1 or settings["baseline_lm_bpc"] <= 0 or not 0 <= settings["lm_relative_degradation"] < 1 or not 0 <= settings["prefix_crop_probability"] <= 1): raise ValueError("Invalid loss, schedule, optimizer or stopping settings") if settings["precision"] not in ("bf16", "fp32") or settings["device"] not in ("cuda", "cpu"): raise ValueError("C1 supports BF16 CUDA or explicit FP32; no implicit precision fallback") if settings["device"] == "cpu" and settings["precision"] != "fp32": raise ValueError("CPU requires FP32") conversion_dir = root / config["conversion_dir"] token_dir, index_dir = root / config["token_dir"], root / config["index_dir"] tokenizer = root / config["tokenizer_dir"] / "tokenizer.model" files = {"conversion": conversion_dir / "manifest.json", "acceptance": conversion_dir / "acceptance.json", "tokens": token_dir / "manifest.json", "windows": index_dir / "manifest.json", "tokenizer": tokenizer} signatures = {name: file_sha(path) for name, path in files.items()} if signatures != config["identities"] or signatures["tokenizer"] != TOKENIZER_SHA: raise ValueError("Frozen data/tokenizer manifest identity mismatch") conversion, acceptance = read_json(files["conversion"]), read_json(files["acceptance"]) tokens, windows = read_json(files["tokens"]), read_json(files["windows"]) if (conversion.get("status") != "complete" or conversion.get("format") != "vimeml_conversion_v1" or acceptance.get("status") != "pass" or acceptance["manifest_sha256"] != signatures["conversion"] or tokens.get("status") != "complete" or windows.get("status") != "complete" or tokens["tokenizer_model_sha256"] != TOKENIZER_SHA or tokens["vocab_size"] != 8192 or windows["context_length"] != 128 or windows["token_manifest_sha256"] != signatures["tokens"] or conversion["contract"]["tokenizer_sha256"] != TOKENIZER_SHA): raise ValueError("Incomplete/incompatible conversion or LM data") # Verify exactly the binary files consumed by the GPU trainer. Audit JSONL, # raw TXT and dictionary binaries do not need uploading or rescanning. required = [] for split in ("train", "validation"): count = conversion["splits"][split]["examples"] stored = conversion["splits"][split]["stored_tokens"] for suffix, size in (("tokens.bin", stored * 2), ("offsets.bin", (count + 1) * 8), ("out-start.bin", count), ("reading-dropout.bin", count)): name = f"{split}.{suffix}" path = conversion_dir / name if path.stat().st_size != size: raise ValueError(f"Conversion file size mismatch: {name}") required.append((path, conversion["output_sha256"][name])) for suffix in ("tokens.bin", "offsets.bin", "sources.bin"): name = f"{split}.{suffix}" path = token_dir / name if path.stat().st_size != tokens["splits"][split]["file_bytes"][suffix]: raise ValueError(f"LM file size mismatch: {name}") required.append((path, tokens["output_sha256"][name])) path = index_dir / f"{split}.windows.npz" if path.stat().st_size != windows["splits"][split]["index_bytes"]: raise ValueError("Window index size mismatch") required.append((path, windows["splits"][split]["index_sha256"])) if verify_files: for path, expected in required: if file_sha(path) != expected: raise ValueError(f"Training binary changed: {path.name}") rows = read_snapshot("standard-development", root) from vimeml.posttraining.infer import TRACKS signatures["standard-development"] = TRACKS["standard-development"][1] signatures["code"], hashes = code_signature(root) count = conversion["splits"]["train"]["examples"] settings["updates_per_epoch"] = math.ceil(count / settings["conversion_batch"]) settings["max_steps"] = settings["epochs"] * settings["updates_per_epoch"] settings["warmup_steps"] = max(1, math.ceil(settings["max_steps"] * settings["warmup_ratio"])) if settings["warmup_ratio"] else 0 for name in ("conversion", "lm"): if settings[f"{name}_microbatch"] > settings[f"{name}_batch"]: raise ValueError("Microbatch exceeds its logical task batch") source = root / config["initialization"]["checkpoint"] if not source.is_file(): raise ValueError("Frozen V4-B initialization checkpoint is missing") if verify_files and file_sha(source) != config["initialization"]["sha256"]: raise ValueError("Source checkpoint identity mismatch") if not isinstance(config.get("runtime", {}).get("compile_backbone"), bool): raise ValueError("Explicit compile_backbone boolean required") if config["runtime"]["compile_backbone"] and settings["device"] != "cuda": raise ValueError("Compilation setting is for the CUDA training environment") return {"stage": STAGE, "parameters": 15_070_848, "settings": settings, "signatures": signatures, "code_sha256": hashes, "task_contract": TASK_CONTRACT, "conversion_examples": count, "validation_examples": conversion["splits"]["validation"]["examples"], "lm_validation_targets": tokens["splits"]["validation"]["prediction_pairs"], "lm_validation_characters": tokens["splits"]["validation"]["characters"], "standard_cases": len(rows), "required_files": [str(p.relative_to(root)) for p, _ in required], "verification": "full consumed binary SHA256" if verify_files else "pinned manifests plus binary sizes; no model/CUDA/training"} def validate(model, loader, device, expected_tokens=None, characters=None): total, nll, inputs, examples = 0, 0.0, 0, 0 # Caller owns fp32_evaluation so one epoch shares that context with IME. for batch in loader: labels = batch["labels"].to(device, non_blocking=True) result = model(batch["input_ids"].to(device, non_blocking=True), labels) count = int((batch["labels"] != -100).sum()) if int(result["token_count"]) != count: raise ValueError("Model/dataset supervised token counts disagree") total += count nll += float(result["loss_sum"]) inputs += int(batch["lengths"].sum()) examples += len(batch["lengths"]) if not total or not math.isfinite(nll) or expected_tokens is not None and total != expected_tokens: raise ValueError("Empty/non-finite/incomplete full validation") report = {"loss": nll / total, "nll_sum": nll, "supervised_tokens": total, "input_tokens": inputs, "examples": examples, "precision": "fp32"} if characters is not None: report.update(characters=characters, bpc=nll / (characters * math.log(2))) return report def checkpoint_payload(model, optimizer, state, config, plan, precision, initialization): return {"format": MODEL_FORMAT, "architecture": "tiny_gpt_v4", "model": model.state_dict(), "model_config": model.configuration(), "optimizer": optimizer.state_dict(), "config": config, "signatures": plan["signatures"], "precision": precision, "step": state["step"], "epoch": state["epoch"], "training_state": copy.deepcopy(state), "tokenizer_identity": {"tokenizer_model_sha256": TOKENIZER_SHA, "vocab_size": 8192, "special_ids": {"pad": 0, "unk": 1, "bos": 2, "eos": 3}}, "posttraining": {"stage": STAGE, "task_contract": TASK_CONTRACT}, "initialization": initialization, "rng": rng_state(), "scheduler": {"step": state["step"], "max_steps": plan["settings"]["max_steps"], "warmup_steps": plan["settings"]["warmup_steps"], "name": "cosine"}} def progress(state, plan, status="running", latest=None): done = state["epoch"] + state["conversion_cursor"] / plan["settings"]["updates_per_epoch"] seconds = state["training_seconds"] rate = state["step"] / seconds if seconds else None return {"status": status, "step": state["step"], "max_steps": plan["settings"]["max_steps"], "completed_conversion_epochs": done, "conversion_cursor": state["conversion_cursor"], "lm_cursor": state["lm_cursor"], "totals": state["totals"], "plateau_count": state["plateau_count"], "forgetting_count": state["forgetting_count"], "stop_reason": state["stop_reason"], "training_seconds": seconds, "training_only_eta_seconds": (plan["settings"]["max_steps"] - state["step"]) / rate if rate else None, "latest_update": latest, "full_validation": state["epoch_results"][-1] if state["epoch_results"] else None} def terminal_report(state, plan, output, status): report = progress(state, plan, status) report.update(best_conversion_loss=state["best_conversion"], best_lm_bpc=state["best_lm"], best_rerank_key=state["best_rerank_key"], checkpoints={name: str(output / f"{name}.pt") for name in ("best", "best-rerank", "best-conversion", "best-lm", "last") if (output / f"{name}.pt").exists()}) return report def validate_resume_state(state, plan): settings = plan["settings"] e, c, step = state["epoch"], state["conversion_cursor"], state["step"] if (any(type(x) is not int or x < 0 for x in (e, c, step)) or e > settings["epochs"] or c > settings["updates_per_epoch"] or step != e * settings["updates_per_epoch"] + c or step > settings["max_steps"] or state["totals"]["conversion"]["examples"] != e * plan["conversion_examples"] + min(c * settings["conversion_batch"], plan["conversion_examples"]) or state["totals"]["lm"]["examples"] != step * settings["lm_batch"]): raise ValueError("Checkpoint counters are not a committed two-stream update") results = state["epoch_results"] expected_epochs = list(range(1, e + 1)) if c == settings["updates_per_epoch"] and len(results) == e + 1: expected_epochs.append(e + 1) if [x["epoch"] for x in results] != expected_epochs: raise ValueError("Checkpoint has inconsistent pending epoch validation") def run(config, resume=False, root=ROOT): from torch.utils.tensorboard import SummaryWriter root = Path(root) output, log_dir = root / config["output_dir"], root / config["log_dir"] last = output / "last.pt" if not resume and (output.exists() and any(output.iterdir()) or log_dir.exists() and any(log_dir.iterdir())): raise ValueError("Fresh C1 run requires empty output and TensorBoard directories") if resume and not last.is_file(): raise ValueError("No C1 last.pt to resume") print("Verifying frozen C1/LM/source identities...", flush=True) plan = build_plan(config, root, verify_files=True) settings = plan["settings"] torch.set_num_threads(settings["cpu_threads"]) random.seed(settings["seed"]) torch.manual_seed(settings["seed"]) device = torch.device(settings["device"]) if device.type == "cuda" and not torch.cuda.is_available(): raise ValueError("CUDA configuration requires a GPU instance") precision = resolve_precision(device, settings["precision"]) torch.set_float32_matmul_precision("high") model = TinyGPTV4(GPTV4Config(**config["model"])) identity = {"tokenizer_model_sha256": TOKENIZER_SHA, "vocab_size": 8192, "special_ids": {"pad": 0, "unk": 1, "bos": 2, "eos": 3}} # Load source weights before moving model / creating the fresh optimizer. initialization = None if not resume: initialization = initialize_v4(model, root / config["initialization"]["checkpoint"], config["initialization"]["sha256"], config["initialization"]["step"], identity, plan["signatures"]["tokens"], plan["signatures"]["windows"]) model.to(device) optimizer = optimizer_for(model, settings, device) state, restored = initial_state(), None if resume: saved = torch.load(last, map_location="cpu", weights_only=True) state, restored, initialization = restore_checkpoint(saved, model, optimizer, config, plan["signatures"], precision) validate_resume_state(state, plan) if (output / "failure.json").exists(): old_failure = read_json(output / "failure.json") write_json(output / "failure.json", {**old_failure, "active": False, "recovered_by_resume_from_step": state["step"]}) if state["stop_reason"] or state["epoch"] >= settings["epochs"]: print("C1 run is already complete/early-stopped; no updates started", flush=True) report = terminal_report(state, plan, output, "early_stopped" if state["stop_reason"] else "complete") write_json(output / "summary.json", report) write_json(output / "progress.json", report) return report del saved output.mkdir(parents=True, exist_ok=True) write_json(output / "config.json", config) write_json(output / "plan.json", plan) environment = {"torch": str(torch.__version__), "cuda": torch.version.cuda, "precision": precision, "device": str(device), "initialization": initialization, "validation_precision": "primitive_FP32"} if config["runtime"]["compile_backbone"]: from vimeml.training.runtime_v2 import compile_backbone environment["compilation"] = compile_backbone(model) environment["compilation"]["checkpoint_inference"] = "Plain TinyGPTV4" if device.type == "cuda": environment["gpu"] = torch.cuda.get_device_name(device) write_json(output / "environment.json", environment) conv_train = ConversionWindows(ConversionDataset(root / config["conversion_dir"], "train")) conv_val = ConversionWindows(ConversionDataset(root / config["conversion_dir"], "validation")) lm_train = PrefixCropWindowDataset(root / config["token_dir"], root / config["index_dir"], "train", seed=settings["seed"] + 17, probability=settings["prefix_crop_probability"], min_remaining_tokens=8) lm_val = SentenceWindowDataset(root / config["token_dir"], root / config["index_dir"], "validation") kwargs = {"workers": settings["num_workers"], "multiplier": settings["bucket_multiplier"], "pin_memory": device.type == "cuda"} conv_loader = loader_for(conv_train, settings["conversion_batch"], seed=settings["seed"], **kwargs) lm_loader = loader_for(lm_train, settings["lm_batch"], seed=settings["seed"] + 17, **kwargs) val_kwargs = {**kwargs, "workers": settings.get("validation_workers", 0)} conv_validation = loader_for(conv_val, settings["validation_batch"], seed=settings["seed"], shuffle=False, **val_kwargs) lm_validation = loader_for(lm_val, settings["validation_batch"], seed=settings["seed"], shuffle=False, **val_kwargs) subset_indices = sorted(random.Random(settings["seed"] + 1000019).sample(range(len(conv_val)), min(len(conv_val), settings["validation_subset"]))) subset_loader = loader_for(conv_val, settings["validation_batch"], seed=settings["seed"], indices=subset_indices, shuffle=False, **val_kwargs) lm_stream = CommittedStream(lm_loader, state["lm_cursor"]) conv_stream = CommittedStream(conv_loader, {"epoch": state["epoch"], "batch": state["conversion_cursor"], "offset": 0}) codec = ConversionCodec(root / config["tokenizer_dir"] / "tokenizer.model") standard = read_snapshot("standard-development", root) writer = SummaryWriter(str(log_dir), purge_step=state["step"] + 1 if resume else None, flush_secs=5) requested = False old_handlers = {} def request_stop(signum, frame): nonlocal requested if requested: raise KeyboardInterrupt("Second interrupt; preserve last committed checkpoint") requested = True print("Stopping after current committed update/validation, then saving C1 cursors", flush=True) for sig in (signal.SIGINT, signal.SIGTERM): old_handlers[sig] = signal.getsignal(sig) signal.signal(sig, request_stop) def save(name="last"): atomic_checkpoint(output / f"{name}.pt", checkpoint_payload(model, optimizer, state, config, plan, precision, initialization)) def evaluate_conversion(loader, expected=None): with fp32_evaluation(model): return validate(model, loader, device, expected) metrics_file = (output / "metrics.jsonl").open("a", encoding="utf-8", buffering=1) if restored is not None: restore_rng(restored) write_json(output / "progress.json", progress(state, plan, "initializing" if not resume else "running")) terminal_status = None try: if not resume: initial = evaluate_conversion(conv_validation, conv_val.data.manifest["splits"]["validation"]["supervised_tokens_full_reading"]) state["conversion_reference"] = state["best_conversion"] = initial["loss"] write_json(output / "initial-validation.json", {"conversion": initial, "lm_bpc": settings["baseline_lm_bpc"], "lm_bpc_origin": "reused full FP32 result of SHA256-pinned V4-B source; no second pretraining run"}) save() save("best-conversion") model.train() if device.type == "cuda": torch.cuda.reset_peak_memory_stats(device) while True: if state["conversion_cursor"] == settings["updates_per_epoch"]: number = state["epoch"] + 1 if not any(r["epoch"] == number for r in state["epoch_results"]): # A crash in any validation restarts this pending epoch from # the checkpoint before counters/selection were changed. save() write_json(output / "progress.json", progress(state, plan, "validating")) epoch_output = output / "epoch-evaluation" / f"epoch-{number}" epoch_output.mkdir(parents=True, exist_ok=True) print(f"[epoch {number}] full conversion, full LM FP32 and frozen Standard", flush=True) before_eval = rng_state() with fp32_evaluation(model): cv = validate(model, conv_validation, device, conv_val.data.manifest["splits"]["validation"]["supervised_tokens_full_reading"]) lv = validate(model, lm_validation, device, plan["lm_validation_targets"], plan["lm_validation_characters"]) ime = evaluate_snapshot(model, codec, standard, epoch_output / "standard-development") restore_rng(before_eval) roles, qualified = apply_epoch_result(state, cv["loss"], lv["bpc"], ime["correct"], settings) report = {"epoch": number, "step": state["step"], "conversion": cv, "lm": lv, "ime": ime, "lm_retention_qualified": qualified, "stop_reason": state["stop_reason"], "plateau_count": state["plateau_count"], "forgetting_count": state["forgetting_count"]} state["epoch_results"].append(report) write_json(epoch_output / "summary.json", report) write_json(output / "full-validation.json", report) for tag, value in (("conversion/full_validation_loss", cv["loss"]), ("lm/full_validation_bpc", lv["bpc"]), ("ime/standard_correct", ime["correct"]), ("ime/fallbacks", ime["fallbacks"]), ("stopping/plateau_count", state["plateau_count"]), ("stopping/forgetting_count", state["forgetting_count"])): writer.add_scalar(tag, value, state["step"]) for role in roles: save(role) save(f"epoch-{number}") save() print(f"[epoch {number}] conversion={cv['loss']:.6f} LM BPC={lv['bpc']:.6f} IME={ime['correct']}/{ime['total']}", flush=True) state["epoch"] += 1 state["conversion_cursor"] = 0 save() conv_stream = CommittedStream(conv_loader, {"epoch": state["epoch"], "batch": 0, "offset": 0}) if state["stop_reason"] or state["step"] >= settings["max_steps"] or requested: break began = time.perf_counter() conv_batch = conv_stream.next_batch() replay = lm_stream.next_group(settings["lm_batch"]) rate = learning_rate(state["step"] + 1, settings) for group in optimizer.param_groups: group["lr"] = rate result = update(model, optimizer, [conv_batch], replay, settings, device, precision) if device.type == "cuda": torch.cuda.synchronize(device) # Commit BOTH task cursors only after successful AdamW completion. state["step"] += 1 state["conversion_cursor"] += 1 state["lm_cursor"] = lm_stream.state_dict() seconds = time.perf_counter() - began state["training_seconds"] += seconds for task in ("conversion", "lm"): for field in state["totals"][task]: state["totals"][task][field] += result[field][task] if state["step"] % settings["log_every"] == 0: latest = {"step": state["step"], "learning_rate": rate, "seconds": seconds, **result} metrics_file.write(json.dumps(latest) + "\n") for task in ("conversion", "lm"): writer.add_scalar(f"{task}/train_loss", result["losses"][task], state["step"]) writer.add_scalar(f"{task}/supervised_tokens", state["totals"][task]["supervised_tokens"], state["step"]) writer.add_scalar(f"{task}/input_tokens", state["totals"][task]["input_tokens"], state["step"]) writer.add_scalar(f"{task}/supervised_tokens_per_second", result["supervised_tokens"][task] / seconds, state["step"]) writer.add_scalar("optimizer/learning_rate", rate, state["step"]) writer.add_scalar("optimizer/grad_norm", result["grad_norm"], state["step"]) write_json(output / "progress.json", progress(state, plan, latest=latest)) print(f"step={state['step']}/{settings['max_steps']} conv={result['losses']['conversion']:.4f} lm={result['losses']['lm']:.4f} seconds={seconds:.3f}", flush=True) if state["step"] % settings["checkpoint_every"] == 0: save() if state["step"] % settings["eval_every"] == 0 and state["conversion_cursor"] != settings["updates_per_epoch"]: subset = evaluate_conversion(subset_loader) writer.add_scalar("conversion/fixed_subset_loss", subset["loss"], state["step"]) write_json(output / "subset-validation.json", {"step": state["step"], **subset}) print(f"[fixed subset] step={state['step']} conversion={subset['loss']:.6f}", flush=True) terminal_status = "early_stopped" if state["stop_reason"] else ( "complete" if state["step"] >= settings["max_steps"] else "interrupted") save() summary = terminal_report(state, plan, output, terminal_status) if device.type == "cuda": summary["peak_allocated_MiB"] = torch.cuda.max_memory_allocated(device) / 2**20 summary["peak_reserved_MiB"] = torch.cuda.max_memory_reserved(device) / 2**20 write_json(output / "summary.json", summary) write_json(output / "progress.json", summary) return summary except BaseException as error: # Never overwrite last.pt with an update/AdamW call that may be partial. write_json(output / "failure.json", {"status": "failed", "active": True, "error_type": type(error).__name__, "error": str(error), "last_committed_in_memory_step": state["step"], "safe_resume_checkpoint": str(last), "checkpoint_not_overwritten": True}) raise finally: metrics_file.close() writer.close() for sig, handler in old_handlers.items(): signal.signal(sig, handler) for data in (conv_train.data, conv_val.data, lm_train, lm_val): data.close()