Download train_gpu.py from sage002/angstrom: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/sage002/angstrom/resolve/main/train_gpu.py
- Command line
-
hf download hf://sage002/angstrom/train_gpu.py
-
curl -L -o train_gpu.py https://huggingface.co/sage002/angstrom/resolve/main/train_gpu.py
15.6 kB
| """ | |
| train_gpu.py β AngstromE1-Nano GPU Training | |
| Optimized for 2x NVIDIA T4 (16GB VRAM each) | |
| Features: | |
| - Mixed precision (FP16/BF16) | |
| - Gradient checkpointing | |
| - DataParallel across 2 GPUs | |
| - Cosine LR schedule with warmup | |
| - Gradient clipping | |
| - Periodic checkpointing | |
| - Wandb logging (optional) | |
| Usage: | |
| python train_gpu.py # default config | |
| python train_gpu.py --config large # larger model | |
| python train_gpu.py --resume checkpoint.pt # resume training | |
| """ | |
| import sys; sys.path.insert(0, '.') | |
| import os | |
| import math | |
| import time | |
| import json | |
| import argparse | |
| from pathlib import Path | |
| import torch | |
| import torch.nn as nn | |
| from torch.cuda.amp import GradScaler, autocast | |
| from torch.nn.parallel import DistributedDataParallel as DDP | |
| from angstrom_nano import AngstromNanoConfig, AngstromNanoForCausalLM | |
| from angstrom_nano.tokenizer import AngstromNanoTokenizer | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Configs | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CONFIGS = { | |
| "small": { | |
| "vocab_size": 8192, "hidden_size": 256, "intermediate_size": 1024, | |
| "num_hidden_layers": 8, "num_attention_heads": 8, "num_key_value_heads": 2, | |
| "head_dim": 32, "num_local_experts": 4, "num_experts_per_tok": 2, | |
| "max_position_embeddings": 2048, "sliding_window": 512, | |
| "scoring_func": "sigmoid", "use_qk_norm": True, "use_routing_bias": True, | |
| "tie_word_embeddings": True, | |
| }, | |
| "medium": { | |
| "vocab_size": 16384, "hidden_size": 512, "intermediate_size": 2048, | |
| "num_hidden_layers": 12, "num_attention_heads": 16, "num_key_value_heads": 4, | |
| "head_dim": 32, "num_local_experts": 8, "num_experts_per_tok": 2, | |
| "max_position_embeddings": 4096, "sliding_window": 1024, | |
| "scoring_func": "sigmoid", "use_qk_norm": True, "use_routing_bias": True, | |
| "tie_word_embeddings": True, | |
| }, | |
| "large": { | |
| "vocab_size": 32000, "hidden_size": 1024, "intermediate_size": 4096, | |
| "num_hidden_layers": 24, "num_attention_heads": 16, "num_key_value_heads": 4, | |
| "head_dim": 64, "num_local_experts": 8, "num_experts_per_tok": 2, | |
| "max_position_embeddings": 4096, "sliding_window": 1024, | |
| "scoring_func": "sigmoid", "use_qk_norm": True, "use_routing_bias": True, | |
| "tie_word_embeddings": True, | |
| }, | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Dataset | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TextDataset(torch.utils.data.Dataset): | |
| """Memory-mapped token dataset for large corpora.""" | |
| def __init__(self, token_ids: torch.Tensor, seq_len: int): | |
| self.token_ids = token_ids | |
| self.seq_len = seq_len | |
| self.n_samples = len(token_ids) - seq_len - 1 | |
| def __len__(self): | |
| return self.n_samples | |
| def __getitem__(self, idx): | |
| x = self.token_ids[idx : idx + self.seq_len] | |
| y = self.token_ids[idx + 1 : idx + self.seq_len + 1] | |
| return x, y | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Training | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def setup_device(): | |
| """Setup multi-GPU or single GPU.""" | |
| if not torch.cuda.is_available(): | |
| print("WARNING: No GPU found, using CPU (will be slow)") | |
| return torch.device("cpu"), 1 | |
| n_gpus = torch.cuda.device_count() | |
| device = torch.device("cuda:0") | |
| print(f"Using {n_gpus} GPU(s):") | |
| for i in range(n_gpus): | |
| props = torch.cuda.get_device_properties(i) | |
| print(f" GPU {i}: {props.name} ({props.total_mem / 1e9:.1f} GB)") | |
| return device, n_gpus | |
| def get_lr(step, warmup_steps, max_steps, base_lr, min_lr): | |
| """Cosine learning rate schedule with warmup.""" | |
| if step < warmup_steps: | |
| return base_lr * step / max(1, warmup_steps) | |
| progress = (step - warmup_steps) / max(1, max_steps - warmup_steps) | |
| return min_lr + 0.5 * (base_lr - min_lr) * (1.0 + math.cos(math.pi * progress)) | |
| def count_params(model): | |
| """Count trainable parameters.""" | |
| return sum(p.numel() for p in model.parameters() if p.requires_grad) | |
| def save_checkpoint(model, optimizer, scaler, step, loss, config_dict, out_dir): | |
| """Save training checkpoint.""" | |
| out_dir = Path(out_dir) | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| # Save model weights | |
| sd = model.module.state_dict() if hasattr(model, 'module') else model.state_dict() | |
| if "lm_head.weight" not in sd: | |
| sd["lm_head.weight"] = sd["model.embed_tokens.weight"] | |
| from safetensors.torch import save_file | |
| weights_path = out_dir / f"checkpoint-{step}.safetensors" | |
| save_file({k: v.contiguous().cpu() for k, v in sd.items()}, str(weights_path)) | |
| # Save training state | |
| state = { | |
| "step": step, | |
| "loss": loss, | |
| "config": config_dict, | |
| "optimizer": optimizer.state_dict(), | |
| "scaler": scaler.state_dict() if scaler else None, | |
| } | |
| torch.save(state, str(out_dir / f"checkpoint-{step}.pt")) | |
| # Save config | |
| (out_dir / "config.json").write_text(json.dumps(config_dict, indent=2)) | |
| print(f" Saved checkpoint at step {step}") | |
| def train(args): | |
| """Main training loop.""" | |
| torch.manual_seed(42) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed_all(42) | |
| # ββ Setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| device, n_gpus = setup_device() | |
| use_amp = device.type == "cuda" and torch.cuda.is_available() | |
| # ββ Tokenizer ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| tok_path = Path("checkpoints/tokenizer.json") | |
| if tok_path.exists(): | |
| tok = AngstromNanoTokenizer.from_bpe_file(str(tok_path)) | |
| print(f"Loaded tokenizer: {len(tok)} vocab") | |
| else: | |
| print("Training new tokenizer...") | |
| tok = AngstromNanoTokenizer.train_bpe( | |
| [str(args.data_path)], vocab_size=args.vocab_size, | |
| out_path=str(tok_path), | |
| ) | |
| print(f"Trained tokenizer: {len(tok)} vocab") | |
| # ββ Load and tokenize data βββββββββββββββββββββββββββββββββββββ | |
| print(f"\nLoading data from {args.data_path}...") | |
| text = Path(args.data_path).read_text(encoding="utf-8") | |
| print(f" Raw: {len(text):,} chars ({len(text)/1e6:.1f} MB)") | |
| ids = torch.tensor(tok.encode(text, add_bos=True, add_eos=True), dtype=torch.long) | |
| print(f" Tokens: {len(ids):,} ({len(ids)/1e6:.1f}M)") | |
| dataset = TextDataset(ids, args.seq_len) | |
| print(f" Samples: {len(dataset):,}") | |
| dataloader = torch.utils.data.DataLoader( | |
| dataset, batch_size=args.batch_size, shuffle=True, | |
| num_workers=2, pin_memory=True, drop_last=True, | |
| ) | |
| # ββ Model ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| config_dict = CONFIGS[args.config] | |
| config_dict["vocab_size"] = len(tok) | |
| cfg = AngstromNanoConfig(**config_dict) | |
| model = AngstromNanoForCausalLM(cfg) | |
| n_params = count_params(model) | |
| print(f"\nModel: {n_params:,} params ({n_params * 4 / 1e6:.1f} MB FP32)") | |
| model = model.to(device) | |
| # Multi-GPU | |
| if n_gpus > 1: | |
| model = nn.DataParallel(model, device_ids=list(range(n_gpus))) | |
| print(f" Wrapped in DataParallel across {n_gpus} GPUs") | |
| # ββ Optimizer ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| optimizer = torch.optim.AdamW( | |
| model.parameters(), lr=args.lr, | |
| weight_decay=0.1, betas=(0.9, 0.95), | |
| ) | |
| scaler = GradScaler(enabled=use_amp) | |
| # ββ Resume βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| start_step = 0 | |
| if args.resume and Path(args.resume).exists(): | |
| print(f"\nResuming from {args.resume}...") | |
| ckpt = torch.load(args.resume, map_location=device) | |
| if hasattr(model, 'module'): | |
| model.module.load_state_dict(ckpt["model"]) | |
| else: | |
| model.load_state_dict(ckpt["model"]) | |
| optimizer.load_state_dict(ckpt["optimizer"]) | |
| start_step = ckpt["step"] | |
| print(f" Resumed at step {start_step}") | |
| # ββ Training βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| max_steps = args.steps | |
| warmup_steps = args.warmup_steps | |
| grad_clip = args.grad_clip | |
| log_every = args.log_every | |
| save_every = args.save_every | |
| print(f"\nTraining for {max_steps} steps, seq_len={args.seq_len}, batch={args.batch_size}") | |
| print(f" LR: {args.lr} β {args.min_lr}, warmup: {warmup_steps} steps") | |
| print(f" Gradient clipping: {grad_clip}") | |
| print(f" Mixed precision: {use_amp}") | |
| print(f" Checkpoint every: {save_every} steps") | |
| print() | |
| model.train() | |
| t0 = time.time() | |
| running_loss = 0.0 | |
| running_steps = 0 | |
| for step in range(start_step + 1, max_steps + 1): | |
| # Get batch | |
| try: | |
| x, y = next(dataloader_iter) | |
| except (StopIteration, NameError): | |
| dataloader_iter = iter(dataloader) | |
| x, y = next(dataloader_iter) | |
| x = x.to(device, non_blocking=True) | |
| y = y.to(device, non_blocking=True) | |
| # Forward pass with AMP | |
| with autocast(enabled=use_amp, dtype=torch.float16): | |
| out = model(x, labels=y, output_router_logits=True) | |
| loss = out["loss"] | |
| aux_loss = out["aux_loss"] | |
| # Backward pass | |
| optimizer.zero_grad() | |
| scaler.scale(loss).backward() | |
| # Gradient clipping | |
| if grad_clip > 0: | |
| scaler.unscale_(optimizer) | |
| nn.utils.clip_grad_norm_(model.parameters(), grad_clip) | |
| scaler.step(optimizer) | |
| scaler.update() | |
| # Update LR | |
| lr = get_lr(step, warmup_steps, max_steps, args.lr, args.min_lr) | |
| for param_group in optimizer.param_groups: | |
| param_group["lr"] = lr | |
| # Logging | |
| running_loss += loss.item() | |
| running_steps += 1 | |
| if step % log_every == 0 or step == 1: | |
| avg_loss = running_loss / running_steps | |
| ppl = math.exp(min(avg_loss, 20)) # cap to avoid overflow | |
| elapsed = time.time() - t0 | |
| tokens_per_sec = (args.batch_size * args.seq_len * running_steps) / elapsed | |
| gpu_mem = torch.cuda.memory_allocated(0) / 1e9 if device.type == "cuda" else 0 | |
| print(f" step {step:>6d}/{max_steps} loss={avg_loss:.4f} ppl={ppl:.2f} " | |
| f"aux={aux_loss.item():.6f} lr={lr:.1e} " | |
| f"tok/s={tokens_per_sec:.0f} gpu={gpu_mem:.1f}GB " | |
| f"{elapsed:.0f}s") | |
| running_loss = 0.0 | |
| running_steps = 0 | |
| # Save checkpoint | |
| if step % save_every == 0: | |
| save_checkpoint(model, optimizer, scaler, step, avg_loss, | |
| config_dict, args.output_dir) | |
| # ββ Final save βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("\nTraining complete!") | |
| save_checkpoint(model, optimizer, scaler, max_steps, avg_loss, | |
| config_dict, args.output_dir) | |
| # Save final model as the main model file | |
| final_path = Path(args.output_dir) / "model_final.safetensors" | |
| sd = model.module.state_dict() if hasattr(model, 'module') else model.state_dict() | |
| if "lm_head.weight" not in sd: | |
| sd["lm_head.weight"] = sd["model.embed_tokens.weight"] | |
| from safetensors.torch import save_file | |
| save_file({k: v.contiguous().cpu() for k, v in sd.items()}, str(final_path)) | |
| print(f"Saved final model: {final_path}") | |
| total_time = time.time() - t0 | |
| print(f"Total time: {total_time/3600:.1f} hours") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CLI | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="AngstromE1-Nano GPU Training") | |
| parser.add_argument("--config", default="medium", choices=["small", "medium", "large"], | |
| help="Model config (default: medium)") | |
| parser.add_argument("--data-path", default="data/corpus.txt", | |
| help="Path to training corpus") | |
| parser.add_argument("--output-dir", default="checkpoints", | |
| help="Output directory for checkpoints") | |
| parser.add_argument("--vocab-size", type=int, default=16384, | |
| help="BPE vocab size (if training tokenizer)") | |
| parser.add_argument("--seq-len", type=int, default=512, | |
| help="Sequence length (default: 512)") | |
| parser.add_argument("--batch-size", type=int, default=4, | |
| help="Batch size per GPU (default: 4)") | |
| parser.add_argument("--steps", type=int, default=50000, | |
| help="Total training steps (default: 50000)") | |
| parser.add_argument("--lr", type=float, default=3e-3, | |
| help="Peak learning rate (default: 3e-3)") | |
| parser.add_argument("--min-lr", type=float, default=3e-4, | |
| help="Min learning rate (default: 3e-4)") | |
| parser.add_argument("--warmup-steps", type=int, default=500, | |
| help="Warmup steps (default: 500)") | |
| parser.add_argument("--grad-clip", type=float, default=1.0, | |
| help="Gradient clipping (default: 1.0)") | |
| parser.add_argument("--log-every", type=int, default=100, | |
| help="Log every N steps (default: 100)") | |
| parser.add_argument("--save-every", type=int, default=5000, | |
| help="Save checkpoint every N steps (default: 5000)") | |
| parser.add_argument("--resume", type=str, default=None, | |
| help="Resume from checkpoint path") | |
| args = parser.parse_args() | |
| train(args) | |