| import os
|
| import sys
|
| import argparse
|
| import time
|
| import math
|
| import torch
|
| from torch.utils.data import DataLoader
|
|
|
|
|
| sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| from model import RecursiveCausalLM, ModelConfig
|
| from dataset import MemmappedDataset
|
|
|
| def get_lr_scheduler(step, total_steps, warmup_steps, peak_lr, min_lr):
|
| """Calculates cosine learning rate decay with a linear warmup phase."""
|
| if step < warmup_steps:
|
|
|
| return peak_lr * (step + 1) / warmup_steps
|
| if step >= total_steps or total_steps <= warmup_steps:
|
| return min_lr
|
|
|
| decay_ratio = (step - warmup_steps) / (total_steps - warmup_steps)
|
| coefficient = 0.5 * (1.0 + math.cos(math.pi * decay_ratio))
|
| return min_lr + coefficient * (peak_lr - min_lr)
|
|
|
| @torch.no_grad()
|
| def run_live_sample(model, tokenizer, device, prompt="Question: A train of length 150 meters passes a pole in 15 seconds. What is the speed of", max_new_tokens=50):
|
| """Pauses training to run a quick evaluation prompt on the active model state."""
|
| was_training = model.training
|
| model.eval()
|
| input_ids = tokenizer.encode(prompt, return_tensors="pt").to(device)
|
| generated = list(input_ids[0].cpu().numpy())
|
|
|
| use_amp = (device.type == "cuda")
|
| for _ in range(max_new_tokens):
|
| curr_input = torch.tensor([generated[-model.config.max_seq_len:]], dtype=torch.long, device=device)
|
| with torch.amp.autocast(device_type="cuda", enabled=use_amp, dtype=torch.float16):
|
| logits, _ = model(curr_input)
|
| next_token_logits = logits[0, -1, :] / 0.7
|
|
|
|
|
| v, _ = torch.topk(next_token_logits, min(50, next_token_logits.size(-1)))
|
| next_token_logits[next_token_logits < v[-1]] = -float('inf')
|
|
|
| probs = torch.softmax(next_token_logits, dim=-1)
|
| next_token = torch.multinomial(probs, num_samples=1).item()
|
| generated.append(next_token)
|
|
|
| if next_token == tokenizer.eos_token_id:
|
| break
|
|
|
| decoded = tokenizer.decode(generated)
|
| print(f"\n[LIVE GENERATION CALLBACK]:\n \"{decoded}\"\n")
|
| if was_training:
|
| model.train()
|
|
|
| def save_checkpoint(model, optimizer, step, val_loss, config, base_path, max_to_keep=3):
|
| """Saves an atomic checkpoint and maintains a rotating list of the last N checkpoints on disk."""
|
| import glob
|
| import re
|
| import shutil
|
|
|
| dir_name = os.path.dirname(base_path)
|
| if dir_name:
|
| os.makedirs(dir_name, exist_ok=True)
|
|
|
| base_name = os.path.basename(base_path)
|
| name_no_ext, ext = os.path.splitext(base_name)
|
|
|
|
|
| name_no_ext = re.sub(r"_step_\d+", "", name_no_ext)
|
|
|
|
|
| step_path = os.path.join(dir_name, f"{name_no_ext}_step_{step}{ext}")
|
|
|
|
|
| tmp_path = step_path + ".tmp"
|
| torch.save({
|
| "step": step,
|
| "model_state_dict": model.state_dict(),
|
| "optimizer_state_dict": optimizer.state_dict(),
|
| "val_loss": val_loss,
|
| "config": config
|
| }, tmp_path)
|
| os.replace(tmp_path, step_path)
|
|
|
|
|
| try:
|
| shutil.copy(step_path, base_path)
|
| except Exception as e:
|
| print(f"⚠️ Warning: Could not copy step checkpoint to base path: {e}")
|
|
|
| print(f"-> Saved checkpoint to '{step_path}' and copied to '{base_path}'")
|
|
|
|
|
| pattern = os.path.join(dir_name, f"{name_no_ext}_step_*{ext}")
|
| ckpt_files = glob.glob(pattern)
|
|
|
| ckpts_with_steps = []
|
| for f in ckpt_files:
|
| match = re.search(r"_step_(\d+)" + re.escape(ext) + r"$", f)
|
| if match:
|
| ckpts_with_steps.append((int(match.group(1)), f))
|
|
|
| ckpts_with_steps.sort(key=lambda x: x[0])
|
|
|
| while len(ckpts_with_steps) > max_to_keep:
|
| oldest_step, oldest_file = ckpts_with_steps.pop(0)
|
| try:
|
| os.remove(oldest_file)
|
| print(f"-> Pruned oldest checkpoint file: '{oldest_file}' (keeping last {max_to_keep})")
|
| except Exception as e:
|
| print(f"⚠️ Warning: Could not remove old checkpoint '{oldest_file}': {e}")
|
|
|
| def main():
|
| parser = argparse.ArgumentParser(description="Micro-Pretraining a Recursive Universal Causal Transformer (UCT)")
|
| parser.add_argument("--config", type=str, default="mini", choices=["mini", "target"],
|
| help="Choose model config: 'mini' (13.5M unique params) or 'target' (124.13M unique params, unrolled to 176.29M virtual)")
|
| parser.add_argument("--epochs", type=float, default=1.0, help="Number of pre-training epochs (default: 1.0)")
|
| parser.add_argument("--steps", type=int, default=None, help="Explicit number of training steps (overrides --epochs calculation)")
|
| parser.add_argument("--batch_size", type=int, default=1, help="Physical batch size loaded per GPU forward pass")
|
| parser.add_argument("--accumulate", type=int, default=8, help="Gradient accumulation steps (simulates larger batch size)")
|
| parser.add_argument("--eval_interval", type=int, default=None, help="Evaluate validation loss every N steps (default: auto-detected, ~10% of total steps)")
|
| parser.add_argument("--warmup_steps", type=int, default=None, help="Linear LR warmup steps (default: auto-detected, ~5% of total steps)")
|
| parser.add_argument("--lr", type=float, default=None, help="Custom learning rate (defaults to config preset)")
|
| parser.add_argument("--dry_run", action="store_true", help="Load dataset, print step-budgeting and scheduling statistics, and exit immediately without instantiating model or allocating VRAM.")
|
| parser.add_argument("--checkpoint_path", type=str, default=None, help="Custom checkpoint output file path (e.g. micro_llm_200m/uct_pretrain_new.pt)")
|
| parser.add_argument("--keep_checkpoints", type=int, default=3, help="Max number of rotating checkpoints to keep (default: 3)")
|
| parser.add_argument("--profile", action="store_true", help="Run PyTorch Profiler on the first 5 steps and output trace file.")
|
| args = parser.parse_args()
|
|
|
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| print("====================================================")
|
| print(f"--- LAUNCHING RECURSIVE TRANSFORMER PRE-TRAINING ---")
|
| print("====================================================")
|
| print(f"Target Device: {device}")
|
| if torch.cuda.is_available():
|
| print(f"GPU Name: {torch.cuda.get_device_name(0)}")
|
|
|
|
|
| if args.config == "mini":
|
| config = ModelConfig(
|
| d_model=256,
|
| n_iterations=4,
|
| n_heads=4,
|
| n_kv_heads=2,
|
| d_ff=512,
|
| max_seq_len=256
|
| )
|
| peak_lr = args.lr if args.lr else 1e-3
|
| min_lr = peak_lr * 0.1
|
| else:
|
| config = ModelConfig(
|
| d_model=768,
|
| n_iterations=16,
|
| n_heads=12,
|
| n_kv_heads=4,
|
| d_ff=2048,
|
| max_seq_len=512
|
| )
|
| peak_lr = args.lr if args.lr else 4e-4
|
| min_lr = peak_lr * 0.1
|
|
|
| print(f"\nModel Configuration Type: {args.config.upper()}")
|
| print(f"-> Vocabulary Size: {config.vocab_size:,}")
|
| print(f"-> Hidden State Dim: {config.d_model}")
|
| print(f"-> Attention Heads: {config.n_heads} (Query), {config.n_kv_heads} (KV for GQA)")
|
| print(f"-> Recurrence Loops (Virtual Depth): {config.n_iterations}")
|
| print(f"-> Context Size window: {config.max_seq_len} tokens")
|
|
|
|
|
| print("\nInitializing memory-mapped data loaders...")
|
| train_bin = os.path.join("micro_llm_200m", "data_train_trinity.bin")
|
| val_bin = os.path.join("micro_llm_200m", "data_val_trinity.bin")
|
|
|
| train_ds = MemmappedDataset(train_bin, seq_len=config.max_seq_len)
|
| val_ds = MemmappedDataset(val_bin, seq_len=config.max_seq_len)
|
|
|
|
|
| train_loader = DataLoader(train_ds, batch_size=args.batch_size, shuffle=True, drop_last=True)
|
| val_loader = DataLoader(val_ds, batch_size=min(args.batch_size, 4), shuffle=False)
|
|
|
| print(f"-> Train samples: {len(train_ds):,} slices")
|
| print(f"-> Val samples: {len(val_ds):,} slices")
|
|
|
|
|
| checkpoint_path = args.checkpoint_path if args.checkpoint_path is not None else f"micro_llm_200m/uct_{args.config}_new.pt"
|
| start_step = 0
|
| if os.path.exists(checkpoint_path):
|
| try:
|
| ckpt_meta = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
|
| if "step" in ckpt_meta:
|
| start_step = ckpt_meta["step"]
|
| except Exception as e:
|
| pass
|
|
|
|
|
| if args.accumulate <= 0:
|
| args.accumulate = 1
|
|
|
|
|
| if args.steps is not None:
|
| total_steps = args.steps
|
| print(f"-> Explicit steps provided: {total_steps:,} (overriding epoch step budget calculation)")
|
| else:
|
| steps_per_epoch = len(train_ds) // (args.batch_size * args.accumulate)
|
| total_steps = int(args.epochs * steps_per_epoch)
|
| print(f"-> 1-Epoch Step-Budgeting Law Applied:")
|
| print(f" * Total training samples (slices): {len(train_ds):,}")
|
| print(f" * Context Length: {config.max_seq_len} tokens")
|
| print(f" * Effective batch size: {args.batch_size * args.accumulate} slices ({args.batch_size * args.accumulate * config.max_seq_len:,} tokens per step)")
|
| print(f" * Steps per Epoch: {steps_per_epoch:,}")
|
| print(f" * Training Budget: {args.epochs} epoch(s)")
|
| print(f" * Calculated Total steps: {total_steps:,}")
|
|
|
|
|
| if args.warmup_steps is not None:
|
| warmup_steps = args.warmup_steps
|
| print(f"-> Warmup steps: {warmup_steps:,} (explicitly specified)")
|
| else:
|
|
|
| warmup_steps = max(10, int(0.05 * total_steps))
|
| print(f"-> Warmup steps: {warmup_steps:,} (automatically calculated as 5% of total steps)")
|
|
|
| if args.eval_interval is not None:
|
| eval_interval = args.eval_interval
|
| print(f"-> Eval interval steps: {eval_interval:,} (explicitly specified)")
|
| else:
|
|
|
| eval_interval = min(300, max(20, int(0.10 * total_steps)))
|
| print(f"-> Eval interval steps: {eval_interval:,} (automatically calculated as 10% of total steps)")
|
|
|
| if args.dry_run:
|
| print("\n[DRY RUN SUCCESSFUL] Dataset detected and steps dynamically computed. Exiting gracefully without instantiating model.")
|
| sys.exit(0)
|
|
|
|
|
| print("\nInstantiating network weights...")
|
| model = RecursiveCausalLM(config).to(device)
|
| unique_params = model.get_num_params(unique_only=True)
|
| effective_params = model.get_num_params(unique_only=False)
|
| print(f"-> Unique Parameters Saved (VRAM footprint): {unique_params / 1e6:.2f}M")
|
| print(f"-> Unrolled Virtual Parameters (Capacity): {effective_params / 1e6:.2f}M")
|
|
|
|
|
| print("\nSetting up optimizer...")
|
| try:
|
| import bitsandbytes as bnb
|
| print("-> Community bitsandbytes detected! Using 8-bit AdamW to save VRAM.")
|
|
|
| decay_params = []
|
| nodecay_params = []
|
| gate_params = []
|
| for name, param in model.named_parameters():
|
| if not param.requires_grad:
|
| continue
|
| if "depth_gate" in name:
|
| gate_params.append(param)
|
| elif param.ndim >= 2:
|
| decay_params.append(param)
|
| else:
|
| nodecay_params.append(param)
|
|
|
| optim_groups = [
|
| {"params": decay_params, "weight_decay": 0.05},
|
| {"params": gate_params, "weight_decay": 0.10},
|
| {"params": nodecay_params, "weight_decay": 0.0}
|
| ]
|
| optimizer = bnb.optim.Adam8bit(optim_groups, lr=peak_lr)
|
| except Exception as e:
|
| print(f"-> bitsandbytes not fully linked: {e}")
|
| print("-> Falling back to native PyTorch high-performance Fused AdamW optimizer.")
|
| decay_params = []
|
| nodecay_params = []
|
| gate_params = []
|
| for name, param in model.named_parameters():
|
| if not param.requires_grad:
|
| continue
|
| if "depth_gate" in name:
|
| gate_params.append(param)
|
| elif param.ndim >= 2:
|
| decay_params.append(param)
|
| else:
|
| nodecay_params.append(param)
|
|
|
| optim_groups = [
|
| {"params": decay_params, "weight_decay": 0.05},
|
| {"params": gate_params, "weight_decay": 0.10},
|
| {"params": nodecay_params, "weight_decay": 0.0}
|
| ]
|
|
|
| use_fused = (device.type == "cuda")
|
| optimizer = torch.optim.AdamW(optim_groups, lr=peak_lr, fused=use_fused)
|
|
|
|
|
| use_amp = (device.type == "cuda")
|
| scaler = torch.amp.GradScaler("cuda", enabled=use_amp)
|
|
|
|
|
| from transformers import AutoTokenizer
|
| tokenizer_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "tokenizer")
|
| tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
|
|
|
| best_val_loss = float("inf")
|
| if args.checkpoint_path is not None:
|
| checkpoint_path = args.checkpoint_path
|
| else:
|
| checkpoint_path = f"micro_llm_200m/uct_{args.config}_new.pt"
|
|
|
|
|
| start_step = 0
|
| if os.path.exists(checkpoint_path):
|
| print(f"Prior checkpoint detected at '{checkpoint_path}'. Loading weights...")
|
| checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
|
|
|
|
|
| checkpoint_vocab_size = checkpoint["model_state_dict"].get("embeddings.weight", None)
|
| if checkpoint_vocab_size is not None:
|
| old_size = checkpoint_vocab_size.shape[0]
|
| new_size = model.config.vocab_size
|
|
|
| if old_size < new_size:
|
| print(f"-> Upgrading checkpoint vocab size from {old_size} to {new_size} for 128-bit CUDA alignment.")
|
|
|
| old_weight = checkpoint["model_state_dict"]["embeddings.weight"]
|
| mean_weight = old_weight.mean(dim=0, keepdim=True)
|
|
|
|
|
| padding_weight = mean_weight.repeat(new_size - old_size, 1) + torch.randn(new_size - old_size, model.config.d_model, device=device) * 0.02
|
| checkpoint["model_state_dict"]["embeddings.weight"] = torch.cat([old_weight, padding_weight], dim=0)
|
|
|
|
|
| if "lm_head_bias" in checkpoint["model_state_dict"] and model.lm_head_bias is not None:
|
| old_bias = checkpoint["model_state_dict"]["lm_head_bias"]
|
| padding_bias = torch.zeros(new_size - old_size, device=device, dtype=old_bias.dtype)
|
| checkpoint["model_state_dict"]["lm_head_bias"] = torch.cat([old_bias, padding_bias], dim=0)
|
|
|
|
|
|
|
| all_params = []
|
| for group in optimizer.param_groups:
|
| for p in group["params"]:
|
| if any(p is x for x in all_params):
|
| pass
|
| else:
|
| all_tokens = all_params.append(p)
|
|
|
|
|
| param_to_idx = {p: i for i, p in enumerate(all_params)}
|
|
|
|
|
| embed_param = model.embeddings.weight
|
| if embed_param in param_to_idx:
|
| embed_idx = param_to_idx[embed_param]
|
| if "optimizer_state_dict" in checkpoint and "state" in checkpoint["optimizer_state_dict"]:
|
| for key in [embed_idx, str(embed_idx)]:
|
| if key in checkpoint["optimizer_state_dict"]["state"]:
|
| param_state = checkpoint["optimizer_state_dict"]["state"][key]
|
| for state_key in ["exp_avg", "exp_avg_sq"]:
|
| if state_key in param_state:
|
| old_buf = param_state[state_key]
|
| padding_shape = (new_size - old_size,) + old_buf.shape[1:]
|
| padding_buf = torch.zeros(padding_shape, dtype=old_buf.dtype, device=old_buf.device)
|
| param_state[state_key] = torch.cat([old_buf, padding_buf], dim=0)
|
| print(f"-> Successfully upgraded optimizer momentum buffers for embeddings.weight.")
|
|
|
|
|
| bias_param = model.lm_head_bias
|
| if bias_param is not None and bias_param in param_to_idx:
|
| bias_idx = param_to_idx[bias_param]
|
| if "optimizer_state_dict" in checkpoint and "state" in checkpoint["optimizer_state_dict"]:
|
| for key in [bias_idx, str(bias_idx)]:
|
| if key in checkpoint["optimizer_state_dict"]["state"]:
|
| param_state = checkpoint["optimizer_state_dict"]["state"][key]
|
| for state_key in ["exp_avg", "exp_avg_sq"]:
|
| if state_key in param_state:
|
| old_buf = param_state[state_key]
|
| padding_buf = torch.zeros(new_size - old_size, dtype=old_buf.dtype, device=old_buf.device)
|
| param_state[state_key] = torch.cat([old_buf, padding_buf], dim=0)
|
| print(f"-> Successfully upgraded optimizer momentum buffers for lm_head_bias.")
|
|
|
| model.load_state_dict(checkpoint["model_state_dict"], strict=False)
|
| optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
|
| if "step" in checkpoint:
|
| start_step = checkpoint["step"]
|
| print(f"Resuming training smoothly from step {start_step:,}!")
|
|
|
| print(f"\nTraining execution window initialized for {total_steps:,} steps.")
|
| print("----------------------------------------------------------------")
|
|
|
| train_iter = iter(train_loader)
|
| model.train()
|
|
|
| t0 = time.time()
|
| accumulated_loss = 0.0
|
|
|
|
|
| prof = None
|
| if args.profile:
|
| print("-> Profiling enabled. Starting PyTorch Profiler for the first 5 steps...")
|
| prof = torch.profiler.profile(
|
| schedule=torch.profiler.schedule(wait=1, warmup=1, active=3, repeat=1),
|
| on_trace_ready=torch.profiler.tensorboard_trace_handler('micro_llm_200m/profiler_logs'),
|
| record_shapes=True,
|
| profile_memory=True,
|
| with_stack=True,
|
| acc_events=True
|
| )
|
| prof.start()
|
|
|
| for step in range(start_step, total_steps):
|
|
|
| lr = get_lr_scheduler(step, total_steps, warmup_steps, peak_lr, min_lr)
|
| for param_group in optimizer.param_groups:
|
| param_group["lr"] = lr
|
|
|
| optimizer.zero_grad(set_to_none=True)
|
|
|
| step_loss = 0.0
|
| for micro_step in range(args.accumulate):
|
| try:
|
| x, y = next(train_iter)
|
| except StopIteration:
|
| train_iter = iter(train_loader)
|
| x, y = next(train_iter)
|
|
|
| x, y = x.to(device, non_blocking=True), y.to(device, non_blocking=True)
|
|
|
| with torch.amp.autocast(device_type="cuda", enabled=use_amp, dtype=torch.float16):
|
| logits, loss = model(x, y)
|
|
|
| loss = loss / args.accumulate
|
|
|
| step_loss += loss.item()
|
| scaler.scale(loss).backward()
|
|
|
|
|
| scaler.unscale_(optimizer)
|
|
|
| torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=0.5)
|
|
|
| scaler.step(optimizer)
|
| scaler.update()
|
|
|
| accumulated_loss += step_loss
|
|
|
|
|
| if (step + 1) % 20 == 0 or step == start_step:
|
| torch.cuda.synchronize() if device.type == "cuda" else None
|
| t1 = time.time()
|
| is_interval = (step > start_step and (step + 1) % 20 == 0)
|
| step_time = (t1 - t0) / (20 if is_interval else 1)
|
|
|
| step_time_safe = max(step_time, 1e-5)
|
|
|
| tokens_per_sec = (config.max_seq_len * args.batch_size * args.accumulate) / step_time_safe
|
|
|
| vram_gb = 0.0
|
| if device.type == "cuda":
|
| vram_gb = torch.cuda.memory_allocated(device) / 1024 / 1024 / 1024
|
|
|
| print(f"Step {step+1:5d}/{total_steps:5d} | Loss: {accumulated_loss / (20 if is_interval else 1):.4f} | LR: {lr:.2e} | Speed: {tokens_per_sec:.0f} tok/s ({step_time*1000:.0f} ms) | VRAM: {vram_gb:.2f} GB")
|
| accumulated_loss = 0.0
|
| t0 = time.time()
|
|
|
|
|
| if (step + 1) % eval_interval == 0:
|
|
|
| if 'logits' in locals(): del logits
|
| if 'loss' in locals(): del loss
|
|
|
| print("\nEvaluating validation set performance...")
|
|
|
|
|
| import gc
|
| gc.collect()
|
| if device.type == "cuda":
|
| torch.cuda.empty_cache()
|
|
|
| model.eval()
|
| val_loss = 0.0
|
| val_count = min(len(val_loader), 50)
|
|
|
| val_iter = iter(val_loader)
|
| with torch.no_grad():
|
| for v_step in range(val_count):
|
| x_v, y_v = next(val_iter)
|
| x_v, y_v = x_v.to(device), y_v.to(device)
|
| with torch.amp.autocast(device_type="cuda", enabled=use_amp, dtype=torch.float16):
|
| logits_v, loss = model(x_v, y_v)
|
| val_loss += loss.item()
|
|
|
|
|
| del x_v, y_v, loss, logits_v
|
| if device.type == "cuda" and (v_step + 1) % 10 == 0:
|
| torch.cuda.empty_cache()
|
|
|
| val_loss /= val_count
|
|
|
|
|
| perplexity = math.exp(val_loss) if val_loss < 50 else float("inf")
|
| print(f"-> Validation Loss: {val_loss:.4f} | Perplexity (PPL): {perplexity:.2f} (Best: {best_val_loss:.4f})")
|
|
|
|
|
| log_path = os.path.join("micro_llm_200m", "metrics_log.csv")
|
| is_new = not os.path.exists(log_path)
|
| try:
|
| with open(log_path, "a", encoding="utf-8") as f:
|
| if is_new:
|
| f.write("step,val_loss,perplexity\n")
|
| f.write(f"{step+1},{val_loss:.6f},{perplexity:.6f}\n")
|
| except Exception as e:
|
| print(f"⚠️ Warning: Could not write metrics to CSV: {e}")
|
|
|
|
|
| print("Triggering Live Generation Callback...")
|
| run_live_sample(model, tokenizer, device)
|
|
|
|
|
| if val_loss < best_val_loss:
|
| best_val_loss = val_loss
|
| print(f"-> New best loss!")
|
| save_checkpoint(
|
| model=model,
|
| optimizer=optimizer,
|
| step=step + 1,
|
| val_loss=val_loss,
|
| config=config,
|
| base_path=checkpoint_path,
|
| max_to_keep=args.keep_checkpoints
|
| )
|
|
|
|
|
| if 'x_v' in locals(): del x_v
|
| if 'y_v' in locals(): del y_v
|
| if 'logits_v' in locals(): del logits_v
|
| if 'val_iter' in locals(): del val_iter
|
| if 'loss' in locals(): del loss
|
| import gc
|
| gc.collect()
|
| if device.type == "cuda":
|
| torch.cuda.empty_cache()
|
|
|
| print("----------------------------------------------------------------")
|
| model.train()
|
| t0 = time.time()
|
|
|
| if prof is not None:
|
| prof.step()
|
|
|
| if prof is not None:
|
| prof.stop()
|
| print("\nPre-training run completed successfully! Best model weights saved.")
|
| print(f"Weights ready for interactive autoregressive generation at '{checkpoint_path}'.")
|
|
|
| if __name__ == "__main__":
|
| main()
|
|
|