Spaces:
Sleeping
Sleeping
| """ | |
| OpenMind Training Script. | |
| Supports: | |
| - Single GPU and multi-GPU training (PyTorch DDP/FSDP) | |
| - Mixed precision (bf16/fp16) | |
| - Gradient accumulation and clipping | |
| - Cosine learning rate schedule with warmup | |
| - Checkpointing and resume | |
| - WandB logging (optional) | |
| """ | |
| import os | |
| import sys | |
| import math | |
| import time | |
| import json | |
| import argparse | |
| from pathlib import Path | |
| from contextlib import nullcontext | |
| import yaml | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.distributed as dist | |
| from torch.nn.parallel import DistributedDataParallel as DDP | |
| from torch.utils.data import Dataset, DataLoader, DistributedSampler | |
| # Add project root to path | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) | |
| from src.models.config_openmind import OpenMindConfig | |
| from src.models.modeling_openmind import OpenMindModel | |
| from src.data.pipeline import TokenDataset | |
| # βββ Dataset Wrapper ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TrainDataset(Dataset): | |
| """PyTorch Dataset wrapper for memory-mapped token files.""" | |
| def __init__(self, data_path: str, max_seq_len: int = 2048): | |
| self.data = np.memmap(data_path, dtype=np.uint16, mode="r") | |
| self.max_seq_len = max_seq_len | |
| self.num_sequences = len(self.data) // max_seq_len | |
| print(f"TrainDataset: {self.num_sequences} sequences from {data_path}") | |
| def __len__(self): | |
| return self.num_sequences | |
| def __getitem__(self, idx): | |
| start = idx * self.max_seq_len | |
| end = start + self.max_seq_len | |
| tokens = self.data[start:end].astype(np.int64) | |
| x = torch.from_numpy(tokens) | |
| return x, x.clone() # input_ids, labels | |
| # βββ Learning Rate Scheduler ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_lr(step: int, warmup_steps: int, max_steps: int, max_lr: float, min_lr: float) -> float: | |
| """Cosine learning rate schedule with linear warmup.""" | |
| # Linear warmup | |
| if step < warmup_steps: | |
| return max_lr * (step + 1) / warmup_steps | |
| # Cosine decay | |
| if step >= max_steps: | |
| return min_lr | |
| progress = (step - warmup_steps) / (max_steps - warmup_steps) | |
| return min_lr + 0.5 * (max_lr - min_lr) * (1 + math.cos(math.pi * progress)) | |
| # βββ Checkpoint Management ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def save_checkpoint( | |
| model: nn.Module, | |
| optimizer: torch.optim.Optimizer, | |
| step: int, | |
| loss: float, | |
| config: dict, | |
| output_dir: str, | |
| keep_last_n: int = 3, | |
| ): | |
| """Save training checkpoint.""" | |
| os.makedirs(output_dir, exist_ok=True) | |
| checkpoint_path = os.path.join(output_dir, f"checkpoint-{step}") | |
| os.makedirs(checkpoint_path, exist_ok=True) | |
| # Save model state | |
| model_to_save = model.module if hasattr(model, "module") else model | |
| torch.save(model_to_save.state_dict(), os.path.join(checkpoint_path, "model.pt")) | |
| # Save optimizer state | |
| torch.save(optimizer.state_dict(), os.path.join(checkpoint_path, "optimizer.pt")) | |
| # Save training state | |
| state = { | |
| "step": step, | |
| "loss": loss, | |
| "config": config, | |
| "rng_state": torch.random.get_rng_state().tolist(), | |
| } | |
| if torch.cuda.is_available(): | |
| state["cuda_rng_state"] = torch.cuda.get_rng_state().tolist() | |
| with open(os.path.join(checkpoint_path, "training_state.json"), "w") as f: | |
| json.dump(state, f, indent=2) | |
| # Save model config | |
| model_to_save.config.save_pretrained(checkpoint_path) | |
| print(f"Checkpoint saved at step {step} -> {checkpoint_path}") | |
| # Cleanup old checkpoints | |
| if keep_last_n > 0: | |
| checkpoints = sorted( | |
| [d for d in os.listdir(output_dir) if d.startswith("checkpoint-")], | |
| key=lambda x: int(x.split("-")[1]), | |
| ) | |
| for old_ckpt in checkpoints[:-keep_last_n]: | |
| old_path = os.path.join(output_dir, old_ckpt) | |
| import shutil | |
| shutil.rmtree(old_path) | |
| print(f"Removed old checkpoint: {old_ckpt}") | |
| def load_checkpoint( | |
| checkpoint_dir: str, | |
| model: nn.Module, | |
| optimizer: torch.optim.Optimizer, | |
| device: str = "cpu", | |
| ) -> int: | |
| """Load checkpoint and return the step number.""" | |
| model_to_load = model.module if hasattr(model, "module") else model | |
| model_path = os.path.join(checkpoint_dir, "model.pt") | |
| optimizer_path = os.path.join(checkpoint_dir, "optimizer.pt") | |
| state_path = os.path.join(checkpoint_dir, "training_state.json") | |
| # Load model weights | |
| state_dict = torch.load(model_path, map_location=device) | |
| model_to_load.load_state_dict(state_dict) | |
| # Load optimizer state | |
| if os.path.exists(optimizer_path): | |
| optimizer.load_state_dict(torch.load(optimizer_path, map_location=device)) | |
| # Load training state | |
| step = 0 | |
| if os.path.exists(state_path): | |
| with open(state_path, "r") as f: | |
| state = json.load(f) | |
| step = state["step"] | |
| # Restore RNG state | |
| if "rng_state" in state: | |
| torch.random.set_rng_state(torch.ByteTensor(state["rng_state"])) | |
| if "cuda_rng_state" in state and torch.cuda.is_available(): | |
| torch.cuda.set_rng_state(torch.ByteTensor(state["cuda_rng_state"])) | |
| print(f"Resumed from checkpoint at step {step}") | |
| return step | |
| def find_latest_checkpoint(output_dir: str) -> str | None: | |
| """Find the latest checkpoint in output directory.""" | |
| if not os.path.exists(output_dir): | |
| return None | |
| checkpoints = [ | |
| d for d in os.listdir(output_dir) | |
| if d.startswith("checkpoint-") and os.path.isdir(os.path.join(output_dir, d)) | |
| ] | |
| if not checkpoints: | |
| return None | |
| latest = max(checkpoints, key=lambda x: int(x.split("-")[1])) | |
| return os.path.join(output_dir, latest) | |
| # βββ Main Training Function ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def main(config_path: str): | |
| """Main training loop.""" | |
| # ββ Load config ββββββββββββββββββββββββββββββββββββββββ | |
| with open(config_path, "r") as f: | |
| config = yaml.safe_load(f) | |
| model_cfg = config["model"] | |
| train_cfg = config["training"] | |
| data_cfg = config["data"] | |
| ckpt_cfg = config["checkpoint"] | |
| log_cfg = config["logging"] | |
| # ββ Setup distributed training βββββββββββββββββββββββββ | |
| ddp = int(os.environ.get("RANK", -1)) != -1 | |
| if ddp: | |
| dist.init_process_group(backend="nccl") | |
| rank = dist.get_rank() | |
| local_rank = int(os.environ.get("LOCAL_RANK", 0)) | |
| world_size = dist.get_world_size() | |
| device = f"cuda:{local_rank}" | |
| torch.cuda.set_device(device) | |
| is_master = rank == 0 | |
| else: | |
| rank = 0 | |
| local_rank = 0 | |
| world_size = 1 | |
| is_master = True | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| # ββ Seed everything ββββββββββββββββββββββββββββββββββββ | |
| seed = train_cfg.get("seed", 42) | |
| torch.manual_seed(seed) | |
| np.random.seed(seed) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed_all(seed) | |
| # ββ Build model ββββββββββββββββββββββββββββββββββββββββ | |
| model_config = OpenMindConfig( | |
| vocab_size=model_cfg["vocab_size"], | |
| max_seq_len=model_cfg["max_seq_len"], | |
| dim=model_cfg["dim"], | |
| n_layers=model_cfg["n_layers"], | |
| n_heads=model_cfg["n_heads"], | |
| n_kv_heads=model_cfg.get("n_kv_heads", model_cfg["n_heads"]), | |
| intermediate_dim=model_cfg.get("intermediate_dim", int(model_cfg["dim"] * 2.67)), | |
| dropout=model_cfg.get("dropout", 0.0), | |
| tie_embeddings=model_cfg.get("tie_embeddings", True), | |
| rope_theta=model_cfg.get("rope_theta", 10000.0), | |
| ) | |
| if is_master: | |
| print(f"Model config: {model_config}") | |
| model = OpenMindModel(model_config).to(device) | |
| # Compile if supported | |
| if train_cfg.get("compile", False) and hasattr(torch, "compile"): | |
| if is_master: | |
| print("Compiling model with torch.compile()...") | |
| model = torch.compile(model) | |
| # Wrap with DDP if distributed | |
| if ddp: | |
| model = DDP(model, device_ids=[local_rank]) | |
| # ββ Optimizer ββββββββββββββββββββββββββββββββββββββββββ | |
| # Separate weight decay groups (no decay for biases and norms) | |
| decay_params = [] | |
| no_decay_params = [] | |
| for name, param in model.named_parameters(): | |
| if not param.requires_grad: | |
| continue | |
| if param.ndim < 2 or "norm" in name or "bias" in name: | |
| no_decay_params.append(param) | |
| else: | |
| decay_params.append(param) | |
| optimizer = torch.optim.AdamW( | |
| [ | |
| {"params": decay_params, "weight_decay": train_cfg["weight_decay"]}, | |
| {"params": no_decay_params, "weight_decay": 0.0}, | |
| ], | |
| lr=train_cfg["lr"], | |
| betas=(train_cfg["beta1"], train_cfg["beta2"]), | |
| eps=train_cfg["eps"], | |
| ) | |
| # ββ Data loading βββββββββββββββββββββββββββββββββββββββ | |
| train_dataset = TrainDataset(data_cfg["train_path"], model_cfg["max_seq_len"]) | |
| if ddp: | |
| sampler = DistributedSampler( | |
| train_dataset, num_replicas=world_size, rank=rank, shuffle=data_cfg.get("shuffle", True) | |
| ) | |
| else: | |
| sampler = None | |
| train_loader = DataLoader( | |
| train_dataset, | |
| batch_size=train_cfg["micro_batch"], | |
| shuffle=(sampler is None and data_cfg.get("shuffle", True)), | |
| sampler=sampler, | |
| num_workers=2, | |
| pin_memory=True, | |
| drop_last=True, | |
| ) | |
| # ββ Gradient accumulation ββββββββββββββββββββββββββββββ | |
| grad_accum = train_cfg.get("gradient_accumulation_steps", "auto") | |
| if grad_accum == "auto": | |
| grad_accum = max(1, train_cfg["batch_size"] // (train_cfg["micro_batch"] * world_size)) | |
| if is_master: | |
| print(f"Gradient accumulation steps: {grad_accum}") | |
| print(f"Effective batch size: {train_cfg['micro_batch'] * world_size * grad_accum}") | |
| # ββ Mixed precision ββββββββββββββββββββββββββββββββββββ | |
| dtype_str = train_cfg.get("dtype", "float32") | |
| if dtype_str == "bfloat16" and torch.cuda.is_available() and torch.cuda.is_bf16_supported(): | |
| dtype = torch.bfloat16 | |
| elif dtype_str == "float16": | |
| dtype = torch.float16 | |
| else: | |
| dtype = torch.float32 | |
| amp_ctx = torch.autocast(device_type="cuda", dtype=dtype) if device.startswith("cuda") else nullcontext() | |
| scaler = torch.amp.GradScaler(enabled=(dtype == torch.float16)) | |
| # ββ Resume from checkpoint βββββββββββββββββββββββββββββ | |
| start_step = 0 | |
| output_dir = ckpt_cfg["output_dir"] | |
| latest_ckpt = find_latest_checkpoint(output_dir) | |
| if latest_ckpt: | |
| if is_master: | |
| print(f"Found checkpoint: {latest_ckpt}") | |
| raw_model = model.module if ddp else model | |
| start_step = load_checkpoint(latest_ckpt, raw_model, optimizer, device) | |
| # ββ WandB ββββββββββββββββββββββββββββββββββββββββββββββ | |
| if log_cfg.get("use_wandb", False) and is_master: | |
| import wandb | |
| wandb.init(project=log_cfg["project_name"], config=config) | |
| # ββ Training loop ββββββββββββββββββββββββββββββββββββββ | |
| max_steps = train_cfg["max_steps"] | |
| warmup_steps = train_cfg["warmup_steps"] | |
| max_lr = train_cfg["lr"] | |
| min_lr = train_cfg["min_lr"] | |
| grad_clip = train_cfg["grad_clip"] | |
| log_every = log_cfg.get("log_every", 10) | |
| save_every = ckpt_cfg.get("save_every", 5000) | |
| if is_master: | |
| print(f"\n{'='*60}") | |
| print(f"Starting training from step {start_step} to {max_steps}") | |
| print(f"Device: {device}, DDP: {ddp}, World size: {world_size}") | |
| print(f"{'='*60}\n") | |
| model.train() | |
| data_iter = iter(train_loader) | |
| running_loss = 0.0 | |
| tokens_processed = 0 | |
| t0 = time.time() | |
| for step in range(start_step, max_steps): | |
| # Update learning rate | |
| lr = get_lr(step, warmup_steps, max_steps, max_lr, min_lr) | |
| for param_group in optimizer.param_groups: | |
| param_group["lr"] = lr | |
| # Gradient accumulation | |
| optimizer.zero_grad(set_to_none=True) | |
| accumulated_loss = 0.0 | |
| for micro_step in range(grad_accum): | |
| # Get batch (restart iterator if exhausted) | |
| try: | |
| x, y = next(data_iter) | |
| except StopIteration: | |
| if ddp: | |
| sampler.set_epoch(step) | |
| data_iter = iter(train_loader) | |
| x, y = next(data_iter) | |
| x, y = x.to(device), y.to(device) | |
| # Forward pass | |
| with amp_ctx: | |
| outputs = model(x, labels=y) | |
| loss = outputs["loss"] / grad_accum | |
| # Backward pass | |
| scaler.scale(loss).backward() | |
| accumulated_loss += loss.item() | |
| # Gradient clipping | |
| if grad_clip > 0: | |
| scaler.unscale_(optimizer) | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip) | |
| # Optimizer step | |
| scaler.step(optimizer) | |
| scaler.update() | |
| # Tracking | |
| running_loss += accumulated_loss | |
| tokens_processed += train_cfg["micro_batch"] * model_cfg["max_seq_len"] * grad_accum * world_size | |
| # Logging | |
| if is_master and (step + 1) % log_every == 0: | |
| elapsed = time.time() - t0 | |
| avg_loss = running_loss / log_every | |
| tokens_per_sec = tokens_processed / elapsed | |
| gpu_mem = "" | |
| if torch.cuda.is_available(): | |
| mem_gb = torch.cuda.max_memory_allocated() / (1024 ** 3) | |
| gpu_mem = f" | GPU mem: {mem_gb:.1f}GB" | |
| print( | |
| f"Step {step + 1}/{max_steps} | " | |
| f"loss: {avg_loss:.4f} | " | |
| f"lr: {lr:.2e} | " | |
| f"tok/s: {tokens_per_sec:.0f}{gpu_mem}" | |
| ) | |
| if log_cfg.get("use_wandb", False): | |
| import wandb | |
| wandb.log({ | |
| "loss": avg_loss, | |
| "lr": lr, | |
| "tokens_per_sec": tokens_per_sec, | |
| "step": step + 1, | |
| }) | |
| running_loss = 0.0 | |
| tokens_processed = 0 | |
| t0 = time.time() | |
| # Save checkpoint | |
| if is_master and (step + 1) % save_every == 0: | |
| raw_model = model.module if ddp else model | |
| save_checkpoint( | |
| model, optimizer, step + 1, accumulated_loss, | |
| config, output_dir, ckpt_cfg.get("keep_last_n", 3) | |
| ) | |
| # ββ Final save βββββββββββββββββββββββββββββββββββββββββ | |
| if is_master: | |
| raw_model = model.module if ddp else model | |
| final_dir = os.path.join(output_dir, f"openmind-{model_cfg['name']}-final") | |
| raw_model.save_pretrained(final_dir) | |
| print(f"\nTraining complete! Final model saved to {final_dir}") | |
| if ddp: | |
| dist.destroy_process_group() | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="OpenMind Training") | |
| parser.add_argument("--config", type=str, required=True, help="Path to config YAML") | |
| args = parser.parse_args() | |
| main(args.config) | |