JavRedstone's picture
download
raw
24.6 kB
"""
This training script can be run both on a single gpu in debug mode,
and also in a larger training run with distributed data parallel (ddp).
To run on a single GPU, example:
$ python train.py --batch_size=32 --compile=False
To run with DDP on 4 gpus on 1 node, example:
$ torchrun --standalone --nproc_per_node=4 train.py
To run with DDP on 4 gpus across 2 nodes, example:
- Run on the first (master) node with example IP 123.456.123.456:
$ torchrun --nproc_per_node=8 --nnodes=2 --node_rank=0 --master_addr=123.456.123.456 --master_port=1234 train.py
- Run on the worker node:
$ torchrun --nproc_per_node=8 --nnodes=2 --node_rank=1 --master_addr=123.456.123.456 --master_port=1234 train.py
(If your cluster does not have Infiniband interconnect prepend NCCL_IB_DISABLE=1)
"""
import os
import time
import math
import pickle
from datetime import timedelta
from contextlib import nullcontext
from itertools import cycle
from functools import partial
import numpy as np
import torch
from torch.utils.data import DataLoader
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.distributed import init_process_group, destroy_process_group
from miditok.pytorch_data import DataCollator
import torch._dynamo
# Some optional paths use dynamic caches/custom Triton kernels; fall back to eager if torch.compile fails.
torch._dynamo.config.suppress_errors = True
from model import GPTConfig, GPT
from data.indirect_idx.preprocess import IndirectIdxDataset, collate_fn
from data.indirect_idx.tokenizer import CharacterTokenizer
from data.jsb.load import JSBDataset
from data.maestro.load import get_maestro_dataset
from data.hrg.load import HRGDataset
# -----------------------------------------------------------------------------
# default config values designed to train a gpt2 (124M) on OpenWebText
# I/O
out_dir = 'out'
eval_interval = 2000
log_interval = 1
eval_iters = 200
eval_only = False # if True, script exits right after the first eval
always_save_checkpoint = True # if True, always save a checkpoint after each eval
init_from = 'scratch' # 'scratch' or 'resume' or 'gpt2*'
ckpt_fname = 'ckpt.pt' # checkpoint filename
# wandb logging
wandb_log = False # disabled by default
wandb_project = 'owt'
wandb_run_name = 'gpt2' # 'run' + str(time.time())
wandb_run_id = '' # used to resume a wandb run
# data
base_dir = ''
dataset = 'openwebtext'
max_shift = 15 # for indirect_idx dataset, max shift value
min_length = 20 # for indirect_idx dataset, minimum sequence length
max_length = 40 # for indirect_idx dataset, maximum sequence length
augment = False # whether to augment the MAESTRO dataset
gradient_accumulation_steps = 5 * 8 # used to simulate larger batch sizes
batch_size = 12 # if gradient_accumulation_steps > 1, this is the micro-batch size
block_size = 1024
n_workers = 8 # number of workers for data loading
persistent = True # whether to use persistent workers for data loading
# model
n_layer = 12
n_head = 12
n_embd = 768
dropout = 0.0 # for pretraining 0 is good, for finetuning try 0.1+
norm_type = 'rmsnorm' # 'layernorm' or 'rmsnorm'
pos_type = 'rope' # 'learnable', 'sinusoidal', 'rope', 'pope', 'alibi'
alibi_n_heads = 6 # number of heads using ALiBi in [1, n_head]
use_theta_bias = True # whether to use theta_bias in PoPE
base_freq = 10000 # base frequency for rotary positional encoding
rotate_fraction = 1.0 # fraction of the embedding dimension to rotate in RoPE/PoPE
thetab_init = 'two_pi' # 'two_pi', 'zero' or 'hybrid', for PoPE init of theta_bias
bias = False # do we use bias inside LayerNorm and Linear layers?
# adamw optimizer
learning_rate = 6e-4 # max learning rate
max_iters = 600000 # total number of training iterations
weight_decay = 1e-1
beta1 = 0.9
beta2 = 0.95
grad_clip = 1.0 # clip gradients at this value, or disable if == 0.0
# learning rate decay settings
decay_lr = True # whether to decay the learning rate
warmup_iters = 2000 # how many steps to warm up for
lr_decay_iters = 600000 # should be ~= max_iters per Chinchilla
min_lr = 6e-5 # minimum learning rate, should be ~= learning_rate/10 per Chinchilla
# DDP settings
backend = 'nccl' # 'nccl', 'gloo', etc.
# system
device = 'cuda' # examples: 'cpu', 'cuda', 'cuda:0', 'cuda:1' etc., or try 'mps' on macbooks
dtype = 'bfloat16' if torch.cuda.is_available() and torch.cuda.get_device_properties(0).major > 8 else 'float16' # 'float32', 'bfloat16', or 'float16', the latter will auto implement a GradScaler
compile = True # use PyTorch 2.0 to compile the model to be faster
complex_flash = False # use custom complex flash attention
seed = 1337 # random seed for reproducibility
# -----------------------------------------------------------------------------
config_keys = [k for k,v in globals().items() if not k.startswith('_') and isinstance(v, (int, float, bool, str))]
exec(open('configurator.py').read()) # overrides from command line or config file
config = {k: globals()[k] for k in config_keys} # will be useful for logging
# -----------------------------------------------------------------------------
torch.multiprocessing.set_sharing_strategy('file_system')
# various inits, derived attributes, I/O setup
ddp = int(os.environ.get('RANK', -1)) != -1 # is this a ddp run?
if ddp:
init_process_group(backend=backend, timeout=timedelta(minutes=20))
ddp_rank = int(os.environ['RANK'])
ddp_local_rank = int(os.environ['LOCAL_RANK'])
ddp_world_size = int(os.environ['WORLD_SIZE'])
device = f'cuda:{ddp_local_rank}'
torch.cuda.set_device(device)
master_process = ddp_rank == 0 # this process will do logging, checkpointing etc.
seed_offset = ddp_rank # each process gets a different seed
# world_size number of processes will be training simultaneously, so we can scale
# down the desired gradient accumulation iterations per process proportionally
assert gradient_accumulation_steps % ddp_world_size == 0
gradient_accumulation_steps //= ddp_world_size
else:
# if not ddp, we are running on a single gpu, and one process
master_process = True
seed_offset = 0
ddp_world_size = 1
tokens_per_iter = gradient_accumulation_steps * ddp_world_size * batch_size * block_size
print(f"tokens per iteration will be: {tokens_per_iter:,}")
if master_process:
os.makedirs(out_dir, exist_ok=True)
np.random.seed(seed + seed_offset)
torch.manual_seed(seed + seed_offset)
torch.backends.cuda.matmul.allow_tf32 = True # allow tf32 on matmul
torch.backends.cudnn.allow_tf32 = True # allow tf32 on cudnn
device_type = 'cuda' if 'cuda' in device else 'cpu' # for later use in torch.autocast
# note: float16 data type will automatically use a GradScaler
ptdtype = {'float32': torch.float32, 'bfloat16': torch.bfloat16, 'float16': torch.float16}[dtype]
ctx = nullcontext() if device_type == 'cpu' else torch.amp.autocast(device_type=device_type, dtype=ptdtype)
# Improve reproducibility in dataloader
g = torch.Generator()
g.manual_seed(seed)
# poor man's data loader
data_dir = os.path.join(base_dir, 'data', dataset)
pad_token_id, vocab_size, col_fn, ds_itr, eval_loaders = None, None, None, {}, {}
# for indirect_idx/jsb/maestro/hrg datasets use regular Dataloader
if dataset == 'indirect_idx':
tokenizer = CharacterTokenizer()
with open(f"data/indirect_idx/ds_minl{min_length}_maxl{max_length}_shift_{max_shift}.txt", "r") as f:
data = f.readlines()
train_data = data[0:1000000]
val_data = data[1000000:1010000]
test_data = data[1010000:1020000]
train_ds = IndirectIdxDataset(train_data, tokenizer)
val_ds = IndirectIdxDataset(val_data, tokenizer)
test_ds = IndirectIdxDataset(test_data, tokenizer)
vocab_size = tokenizer.vocab_size
pad_token_id = tokenizer.pad_idx
col_fn = partial(collate_fn, pad_idx=pad_token_id)
elif dataset == 'jsb':
train_ds = JSBDataset(data_dir, 'train', block_size)
val_ds = JSBDataset(data_dir, 'valid', block_size)
test_ds = JSBDataset(data_dir, 'test', block_size)
vocab_size = train_ds.vocab_size
pad_token_id = train_ds.pad_token_id
elif dataset == 'maestro':
train_ds, val_ds, test_ds, tokenizer = get_maestro_dataset(data_dir, block_size, augment)
vocab_size = tokenizer.vocab_size
pad_token_id = tokenizer.pad_token_id
col_fn = DataCollator(pad_token_id)
elif dataset == 'hrg':
train_ds = HRGDataset('train', block_size)
val_ds = HRGDataset('validation', block_size)
test_ds = HRGDataset('test', block_size)
vocab_size, pad_token_id = train_ds.hrg_vocab_size, train_ds.hrg_pad_token_id
if dataset in ['indirect_idx', 'jsb', 'maestro', 'hrg']:
train_data = DataLoader(train_ds, batch_size, shuffle=True, drop_last=True, num_workers=n_workers, collate_fn=col_fn, persistent_workers=persistent)
val_data = DataLoader(val_ds, batch_size, shuffle=True, drop_last=True, num_workers=n_workers, collate_fn=col_fn, persistent_workers=persistent)
ds_itr['train'] = cycle(train_data)
ds_itr['val'] = cycle(val_data)
if dataset in ['indirect_idx', 'jsb', 'maestro', 'hrg']:
test_data = DataLoader(test_ds, batch_size, shuffle=True, drop_last=True, num_workers=n_workers, collate_fn=col_fn, persistent_workers=persistent)
ds_itr['test'] = cycle(test_data)
if dataset == 'indirect_idx':
# For eval, iterate full split exactly once (no shuffle, keep tail batch).
eval_loaders['val'] = DataLoader(val_ds, batch_size, shuffle=False, drop_last=False, num_workers=n_workers, collate_fn=col_fn, persistent_workers=persistent)
eval_loaders['test'] = DataLoader(test_ds, batch_size, shuffle=False, drop_last=False, num_workers=n_workers, collate_fn=col_fn, persistent_workers=persistent)
def get_batch(split):
target_mask = None # for indirect_idx dataset
if dataset == 'jsb':
x, y = next(ds_itr[split])
elif dataset == 'indirect_idx':
x, y, target_mask = next(ds_itr[split])
elif dataset in ['maestro', 'hrg']:
full_seq = next(ds_itr[split])['input_ids']
x = full_seq[:, :-1]
y = full_seq[:, 1:]
# for all other LM datasets we sample random position and shift L length sequence by one
else:
# We recreate np.memmap every batch to avoid a memory leak, as per
# https://stackoverflow.com/questions/45132940/numpy-memmap-memory-usage-want-to-iterate-once/61472122#61472122
if dataset == 'wikitext103' and split == 'val':
data = np.memmap(os.path.join(data_dir, 'validation.bin'), dtype=np.uint16, mode='r')
else:
data = np.memmap(os.path.join(data_dir, split + '.bin'), dtype=np.uint16, mode='r')
ix = torch.randint(len(data) - block_size, (batch_size,))
x = torch.stack([torch.from_numpy((data[i:i+block_size]).astype(np.int64)) for i in ix])
y = torch.stack([torch.from_numpy((data[i+1:i+1+block_size]).astype(np.int64)) for i in ix])
if device_type == 'cuda':
# pin arrays x,y, which allows us to move them to GPU asynchronously (non_blocking=True)
x, y = x.pin_memory().to(device, non_blocking=True), y.pin_memory().to(device, non_blocking=True)
if target_mask is not None:
target_mask = target_mask.pin_memory().to(device, non_blocking=True)
else:
x, y = x.to(device), y.to(device)
if target_mask is not None:
target_mask = target_mask.to(device)
return x, y, target_mask
# init these up here, can override if init_from='resume' (i.e. from a checkpoint)
iter_num = 0
best_val_loss = 1e9
# attempt to derive vocab_size from the dataset
meta_path = os.path.join(data_dir, 'meta.pkl')
meta_vocab_size = vocab_size
print(f"found vocab_size = {meta_vocab_size} from preprocessing")
if os.path.exists(meta_path):
with open(meta_path, 'rb') as f:
meta = pickle.load(f)
meta_vocab_size = meta['vocab_size']
print(f"Overriding vocab_size. Using vocab_size = {meta_vocab_size} (inside {meta_path})")
# model init
model_args = dict(n_layer=n_layer, n_head=n_head, n_embd=n_embd, norm_type=norm_type, pos_type=pos_type,
alibi_n_heads=alibi_n_heads,
base_freq=base_freq, rotate_fraction=rotate_fraction, use_theta_bias=use_theta_bias,
thetab_init=thetab_init, block_size=block_size, bias=bias, dataset=dataset,
vocab_size=None, dropout=dropout, complex_flash=complex_flash) # start with model_args from command line
if init_from == 'scratch':
# init a new model from scratch
print("Initializing a new model from scratch")
# determine the vocab size we'll use for from-scratch training
if meta_vocab_size is None:
print("defaulting to vocab_size of GPT-2 to 50304 (50257 rounded up for efficiency)")
model_args['vocab_size'] = meta_vocab_size if meta_vocab_size is not None else 50304
gptconf = GPTConfig(**model_args)
model = GPT(gptconf)
elif init_from == 'resume':
print(f"Resuming training from {out_dir}")
# resume training from a checkpoint.
ckpt_path = os.path.join(out_dir, ckpt_fname)
checkpoint = torch.load(ckpt_path, map_location=device)
checkpoint_model_args = checkpoint['model_args']
# force these config attributes to be equal otherwise we can't even resume training
# the rest of the attributes (e.g. dropout) can stay as desired from command line
for k in ['n_layer', 'n_head', 'n_embd', 'block_size', 'bias', 'vocab_size']:
model_args[k] = checkpoint_model_args[k]
# create the model
gptconf = GPTConfig(**model_args)
model = GPT(gptconf)
state_dict = checkpoint['model']
# fix the keys of the state dictionary :(
# honestly no idea how checkpoints sometimes get this prefix, have to debug more
unwanted_prefix = '_orig_mod.'
for k,v in list(state_dict.items()):
if k.startswith(unwanted_prefix):
state_dict[k[len(unwanted_prefix):]] = state_dict.pop(k)
model.load_state_dict(state_dict)
if 'ft' in wandb_run_name and pos_type=='pope':
# Reset all theta_bias parameters to zero before fine-tuning
for name, param in model.named_parameters():
if 'delta_c' in name:
param.data.zero_()
if 'ft' not in wandb_run_name:
iter_num = checkpoint['iter_num']
best_val_loss = checkpoint['best_val_loss']
elif init_from.startswith('gpt2'):
print(f"Initializing from OpenAI GPT-2 weights: {init_from}")
# initialize from OpenAI GPT-2 weights
override_args = dict(dropout=dropout)
model = GPT.from_pretrained(init_from, override_args)
# read off the created config params, so we can store them into checkpoint correctly
for k in ['n_layer', 'n_head', 'n_embd', 'block_size', 'bias', 'vocab_size']:
model_args[k] = getattr(model.config, k)
# crop down the model block size if desired, using model surgery
if block_size < model.config.block_size:
model.crop_block_size(block_size)
model_args['block_size'] = block_size # so that the checkpoint will have the right value
model.to(device)
# initialize a GradScaler. If enabled=False scaler is a no-op
scaler = torch.amp.GradScaler('cuda', enabled=(dtype == 'float16'))
# optimizer
optimizer = model.configure_optimizers(weight_decay, learning_rate, (beta1, beta2), pos_type, device_type)
if init_from == 'resume' and 'ft' not in wandb_run_name:
optimizer.load_state_dict(checkpoint['optimizer'])
checkpoint = None # free up memory
# compile the model
if compile:
print("compiling the model... (takes a ~minute)")
# unoptimized_model = model
model = torch.compile(model) # requires PyTorch 2.0
# wrap model into DDP container
if ddp:
model = DDP(model, device_ids=[ddp_local_rank])
# helps estimate an arbitrarily accurate loss over either split using many batches
@torch.no_grad()
def estimate_loss():
out = {}
model.eval()
def compute_batch_acc(logits, targets, target_mask, split):
pred = torch.argmax(logits, dim=-1).view(-1)
selected_targets = targets.view(-1)
# If targets and mask are shape-compatible, mirror model.forward masking here.
if target_mask is not None and target_mask.numel() == selected_targets.numel():
selected_targets = selected_targets[target_mask.view(-1)]
if pred.numel() != selected_targets.numel():
raise RuntimeError(
f"Prediction/target size mismatch in eval for split='{split}': "
f"pred={tuple(pred.shape)}, targets={tuple(selected_targets.shape)}, "
f"Y={tuple(targets.shape)}, target_mask={tuple(target_mask.shape) if target_mask is not None else None}"
)
if selected_targets.numel() == 0:
return torch.tensor(0.0)
return (pred == selected_targets).float().mean()
if dataset in ['indirect_idx', 'maestro', 'jsb', 'hrg']:
splits = ['train', 'val', 'test']
else:
splits = ['train', 'val']
for split in splits:
# For selected datasets, run one full epoch over eval splits.
if split in eval_loaders:
split_losses = []
split_acc = []
for X, Y, target_mask in eval_loaders[split]:
if device_type == 'cuda':
X = X.pin_memory().to(device, non_blocking=True)
Y = Y.pin_memory().to(device, non_blocking=True)
if target_mask is not None:
target_mask = target_mask.pin_memory().to(device, non_blocking=True)
else:
X = X.to(device)
Y = Y.to(device)
if target_mask is not None:
target_mask = target_mask.to(device)
with ctx:
logits, loss = model(X, Y, pad_token_id=pad_token_id, target_mask=target_mask)
split_losses.append(loss.item())
split_acc.append(compute_batch_acc(logits, Y, target_mask, split).item())
out[split] = torch.tensor(split_losses).mean() if split_losses else torch.tensor(float('nan'))
out[split + '_acc'] = torch.tensor(split_acc).mean() if split_acc else torch.tensor(float('nan'))
else:
losses = torch.zeros(eval_iters)
acc = torch.zeros(eval_iters)
for k in range(eval_iters):
X, Y, target_mask = get_batch(split)
with ctx:
logits, loss = model(X, Y, pad_token_id=pad_token_id, target_mask=target_mask)
losses[k] = loss.item()
if dataset == 'indirect_idx':
acc[k] = compute_batch_acc(logits, Y, target_mask, split)
out[split] = losses.mean()
if dataset == 'indirect_idx':
out[split + '_acc'] = acc.mean()
model.train()
return out
# learning rate decay scheduler (cosine with warmup)
def get_lr(it):
# 1) linear warmup for warmup_iters steps
if it < warmup_iters:
return learning_rate * (it + 1) / (warmup_iters + 1)
# 2) if it > lr_decay_iters, return min learning rate
if it > lr_decay_iters:
return min_lr
# 3) in between, use cosine decay down to min learning rate
decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters)
assert 0 <= decay_ratio <= 1
coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio)) # coeff ranges 0..1
return min_lr + coeff * (learning_rate - min_lr)
# logging
if wandb_log and master_process:
import wandb
wandb.init(project=wandb_project, config=config)
# training loop
X, Y, target_mask = get_batch('train') # fetch the very first batch
t0 = time.time()
local_iter_num = 0 # number of iterations in the lifetime of this process
raw_model = model.module if ddp else model # unwrap DDP container if needed
running_mfu = -1.0
while True:
# determine and set the learning rate for this iteration
lr = get_lr(iter_num) if decay_lr else learning_rate
for param_group in optimizer.param_groups:
param_group['lr'] = lr
# evaluate the loss on train/val/test splits and write checkpoints
if iter_num % eval_interval == 0 and master_process:
losses = estimate_loss()
print(f"step {iter_num}: train loss {losses['train']:.4f}, val loss {losses['val']:.4f}")
if dataset == 'indirect_idx':
print(f"step {iter_num}: train acc {losses['train_acc']:.4f}, val acc {losses['val_acc']:.4f}, test acc {losses['test_acc']:.4f}")
if wandb_log:
wandb.log({
"iter": iter_num,
"train/loss": losses['train'],
"val/loss": losses['val'],
"lr": lr,
"mfu": running_mfu*100, # convert to percentage
})
if 'test' in losses.keys():
wandb.log({
"test/loss": losses['test'],
"test/ppl": np.exp(losses['test']),
})
# for indirect_idx dataset, log task accuracy instead of perplexity
if dataset == 'indirect_idx':
wandb.log({
"train/accuracy": losses['train_acc'],
"val/accuracy": losses['val_acc'],
"test/accuracy": losses['test_acc']
})
else:
wandb.log({
"train/ppl": np.exp(losses['train']),
"val/ppl": np.exp(losses['val']),
})
if losses['val'] < best_val_loss or always_save_checkpoint:
best_val_loss = losses['val']
if iter_num > 0:
checkpoint = {
'model': raw_model.state_dict(),
'optimizer': optimizer.state_dict(),
'model_args': model_args,
'iter_num': iter_num,
'best_val_loss': best_val_loss,
'config': config,
}
print(f"saving checkpoint to {out_dir}")
ckpt_name = wandb_run_name + '-' + pos_type + '-ckpt.pt'
torch.save(checkpoint, os.path.join(out_dir, ckpt_name))
if iter_num == 0 and eval_only:
break
# forward backward update, with optional gradient accumulation to simulate larger batch size
# and using the GradScaler if data type is float16
for micro_step in range(gradient_accumulation_steps):
if ddp:
# in DDP training we only need to sync gradients at the last micro step.
# the official way to do this is with model.no_sync() context manager, but
# I really dislike that this bloats the code and forces us to repeat code
# looking at the source of that context manager, it just toggles this variable
model.require_backward_grad_sync = (micro_step == gradient_accumulation_steps - 1)
with ctx:
logits, loss = model(X, Y, pad_token_id=pad_token_id, target_mask=target_mask)
loss = loss / gradient_accumulation_steps # scale the loss to account for gradient accumulation
# immediately async prefetch next batch while model is doing the forward pass on the GPU
X, Y, target_mask = get_batch('train')
# backward pass, with gradient scaling if training in fp16
scaler.scale(loss).backward()
# clip the gradient
if grad_clip != 0.0:
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip)
# step the optimizer and scaler if training in fp16
scaler.step(optimizer)
scaler.update()
# flush the gradients as soon as we can, no need for this memory anymore
optimizer.zero_grad(set_to_none=True)
# timing and logging
t1 = time.time()
dt = t1 - t0
t0 = t1
if iter_num % log_interval == 0 and master_process:
# get loss as float. note: this is a CPU-GPU sync point
# scale up to undo the division above, approximating the true total loss (exact would have been a sum)
lossf = loss.item() * gradient_accumulation_steps
if local_iter_num >= 5: # let the training loop settle a bit
mfu = raw_model.estimate_mfu(batch_size * gradient_accumulation_steps, dt)
running_mfu = mfu if running_mfu == -1.0 else 0.9*running_mfu + 0.1*mfu
print(f"iter {iter_num}: loss {lossf:.4f}, time {dt*1000:.2f}ms, mfu {running_mfu*100:.2f}%")
iter_num += 1
local_iter_num += 1
# termination conditions
if iter_num > max_iters:
break
if ddp:
destroy_process_group()

Xet Storage Details

Size:
24.6 kB
·
Xet hash:
7d19219dfd14a4dad2636a01a494ae6c89913dab3d0f215f2a5b9f4da2526e27

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.