|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| import os
|
| os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
|
|
|
| import torch
|
| import torch.nn as nn
|
| from torch.utils.data import DataLoader, IterableDataset
|
| import torch.optim as optim
|
| from torch.optim.lr_scheduler import LambdaLR
|
| import math
|
| import argparse
|
| import glob
|
| import pickle
|
| import bitsandbytes as bnb
|
| from tqdm import tqdm
|
| from safetensors.torch import save_model, load_model
|
| from model import GPT, GPTConfig
|
| from tokenizer import train_tokenizer, load_tokenizer
|
| import numpy as np
|
| from torch.nn.attention import SDPBackend, sdpa_kernel
|
|
|
| class FastDataloader:
|
| def __init__(self, bin_path, max_seq_len):
|
| self.max_seq_len = max_seq_len
|
| self.data = np.memmap(bin_path, dtype=np.uint16, mode='r')
|
| print(f"✅ Базовый датасет загружен. Всего токенов: {len(self.data):,}")
|
|
|
| def get_batch(self, batch_size):
|
| ix = torch.randint(len(self.data) - self.max_seq_len - 1, (batch_size,))
|
| x = torch.stack([torch.from_numpy(self.data[i : i + self.max_seq_len].astype(np.int64)) for i in ix])
|
| y = torch.stack([torch.from_numpy(self.data[i + 1 : i + 1 + self.max_seq_len].astype(np.int64)) for i in ix])
|
| return x, y
|
|
|
| class ChatDataset(torch.utils.data.Dataset):
|
| def __init__(self, data_list, tokenizer, max_seq_len):
|
| self.data = data_list
|
| self.tokenizer = tokenizer
|
| self.max_seq_len = max_seq_len
|
|
|
| self.user_tok = tokenizer.encode("<|user|>").ids[0]
|
| self.assist_tok = tokenizer.encode("<|assistant|>").ids[0]
|
| self.end_tok = tokenizer.encode("<|end|>").ids[0]
|
|
|
| def __len__(self):
|
| return len(self.data)
|
|
|
| def __getitem__(self, idx):
|
| line = self.data[idx]
|
| try:
|
| user_text, bot_text = line.split(" | ")
|
| except ValueError:
|
| user_text, bot_text = "Ошибка", "Используйте разделитель |"
|
|
|
| prompt_ids = self.tokenizer.encode(user_text.strip()).ids
|
| response_ids = self.tokenizer.encode(bot_text.strip()).ids
|
|
|
| x_ids = [self.user_tok] + prompt_ids + [self.end_tok, self.assist_tok] + response_ids + [self.end_tok]
|
|
|
| ignore_len = 1 + len(prompt_ids) + 1 + 1
|
| y_ids = [-100] * ignore_len + response_ids + [self.end_tok]
|
|
|
| if len(x_ids) > self.max_seq_len:
|
| x_ids = x_ids[:self.max_seq_len]
|
| y_ids = y_ids[:self.max_seq_len]
|
| else:
|
| pad_len = self.max_seq_len - len(x_ids)
|
| x_ids = x_ids + [0] * pad_len
|
| y_ids = y_ids + [-100] * pad_len
|
|
|
| return torch.tensor(x_ids, dtype=torch.long), torch.tensor(y_ids, dtype=torch.long)
|
|
|
| @torch.no_grad()
|
| def validate(model, val_loader, batch_size, eval_iters=50):
|
| print("DEBUG: starting fast validation...")
|
| model.eval()
|
|
|
| losses = torch.zeros(eval_iters)
|
|
|
| for k in range(eval_iters):
|
| x, y = val_loader.get_batch(batch_size)
|
|
|
| with torch.amp.autocast('cuda', dtype=torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16):
|
| logits, loss = model(x, y)
|
|
|
| losses[k] = loss.item()
|
|
|
| model.train()
|
| avg_loss = losses.mean().item()
|
| print(f"DEBUG: validation finished, avg_loss = {avg_loss:.4f}")
|
| return avg_loss
|
|
|
| def line_generator(file_path, max_lines=None):
|
| with open(file_path, 'r', encoding='utf-8') as f:
|
| for i, line in enumerate(f):
|
| if max_lines and i >= max_lines:
|
| break
|
| yield line
|
|
|
| def get_cosine_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps, min_lr_ratio=0.1):
|
| def lr_lambda(current_step):
|
| if current_step < num_warmup_steps:
|
| return float(current_step) / float(max(1, num_warmup_steps))
|
| progress = float(current_step - num_warmup_steps) / float(max(1, num_training_steps - num_warmup_steps))
|
| cosine = max(0.0, 0.5 * (1.0 + math.cos(math.pi * progress)))
|
| return min_lr_ratio + (1.0 - min_lr_ratio) * cosine
|
| return LambdaLR(optimizer, lr_lambda)
|
|
|
| def train(args):
|
| print(f"DEBUG: args.val_path = {args.val_path}")
|
| print(f"DEBUG: file exists? {os.path.exists(args.val_path) if args.val_path else False}")
|
| device = torch.device(args.device if torch.cuda.is_available() else 'cpu')
|
| print(f"Using device: {device}")
|
|
|
| if os.path.exists(args.tokenizer_path):
|
| print(f"Loading tokenizer from {args.tokenizer_path}")
|
| tokenizer = load_tokenizer(args.tokenizer_path)
|
| else:
|
| print("Tokenizer not found. Training new tokenizer...")
|
| train_tokenizer(line_generator(args.data_path),
|
| vocab_size=args.vocab_size, save_path=args.tokenizer_path)
|
| tokenizer = load_tokenizer(args.tokenizer_path)
|
|
|
| if not args.use_lora:
|
| print(f" Режим БАЗЫ. Загрузка бинарника: {args.data_path}")
|
| train_loader = FastDataloader(args.data_path, args.max_seq_len)
|
| def get_train_batch():
|
| return train_loader.get_batch(args.batch_size)
|
|
|
| get_val_batch = None
|
| if args.val_path and os.path.exists(args.val_path):
|
| val_loader = FastDataloader(args.val_path, args.max_seq_len)
|
| def get_val_batch():
|
| return val_loader.get_batch(args.batch_size)
|
| else:
|
| print(f" Режим DoRA. Загрузка txt диалогов: {args.data_path}")
|
| with open(args.data_path, 'r', encoding='utf-8') as f:
|
| chat_data = [line.strip() for line in f if line.strip()]
|
|
|
| chat_dataset = ChatDataset(chat_data, tokenizer, args.max_seq_len)
|
| chat_loader = DataLoader(chat_dataset, batch_size=args.batch_size, shuffle=True)
|
| chat_iter = iter(chat_loader)
|
|
|
| def get_train_batch():
|
| nonlocal chat_iter
|
| try:
|
| x, y = next(chat_iter)
|
| except StopIteration:
|
| chat_iter = iter(chat_loader)
|
| x, y = next(chat_iter)
|
| return x, y
|
|
|
| get_val_batch = None
|
| if args.val_path and os.path.exists(args.val_path):
|
| with open(args.val_path, 'r', encoding='utf-8') as f:
|
| val_chat_data = [line.strip() for line in f if line.strip()]
|
| val_chat_dataset = ChatDataset(val_chat_data, tokenizer, args.max_seq_len)
|
| val_chat_loader = DataLoader(val_chat_dataset, batch_size=args.batch_size, shuffle=True)
|
| val_chat_iter = iter(val_chat_loader)
|
|
|
| def get_val_batch():
|
| nonlocal val_chat_iter
|
| try:
|
| x, y = next(val_chat_iter)
|
| except StopIteration:
|
| val_chat_iter = iter(val_chat_loader)
|
| x, y = next(val_chat_iter)
|
| return x, y
|
|
|
| model_config = GPTConfig(
|
| vocab_size=args.vocab_size,
|
| embed_dim=args.embed_dim,
|
| n_layers=args.n_layers,
|
| n_heads=args.n_heads,
|
| max_seq_len=args.max_seq_len,
|
| dropout=args.dropout,
|
| use_lora=args.use_lora
|
| )
|
| global_dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
|
|
|
| model = GPT(model_config).to(device)
|
|
|
| total_params = sum(p.numel() for p in model.parameters())
|
| trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| print("="*40)
|
| print(f"📊 Архитектура модели:")
|
| print(f" Всего параметров: {total_params:,}")
|
| print(f" Обучаемых параметров: {trainable_params:,}")
|
| print("="*40)
|
|
|
|
|
|
|
|
|
|
|
|
|
| if args.use_lora:
|
| print("Включен режим LoRA: заморозка базовых весов..")
|
| for param in model.parameters():
|
| param.requires_grad = False
|
|
|
| lora_params = 0
|
| total_params = 0
|
| for name, param in model.named_parameters():
|
| total_params += param.numel()
|
| if 'lora_A' in name or 'lora_B' in name or 'lora_m' in name:
|
| param.requires_grad = True
|
| lora_params += param.numel()
|
|
|
| print(f"Всего параметров: {total_params:,}")
|
| print(f"Обучаемые параметры LoRA: {lora_params:,} ({(lora_params/total_params)*100:.2f}%)")
|
| trainable_params = [p for p in model.parameters() if p.requires_grad]
|
| else:
|
| print("🚀 Режим базового обучения: тренируем все параметры с нуля.")
|
| trainable_params = model.parameters()
|
|
|
| optimizer = bnb.optim.PagedAdamW8bit(trainable_params, lr=args.lr, weight_decay=args.weight_decay, betas=(0.9, 0.95))
|
| scheduler = get_cosine_schedule_with_warmup(optimizer, args.warmup_steps, args.total_steps, min_lr_ratio=0.1)
|
|
|
| if args.resume and getattr(args, 'use_lora', False):
|
| try:
|
| resume_step = int(os.path.basename(args.resume).split('_')[-1].split('.')[0])
|
| if resume_step < args.total_steps:
|
| scheduler.last_epoch = resume_step
|
| scheduler._step_count = resume_step + 1
|
| except:
|
| pass
|
|
|
| use_scaler = not torch.cuda.is_bf16_supported()
|
| if use_scaler:
|
| scaler = torch.amp.GradScaler('cuda')
|
| else:
|
| scaler = None
|
|
|
| start_step = 0
|
| if args.resume and os.path.exists(args.resume):
|
| print(f"Loading model weights from {args.resume}")
|
|
|
| from safetensors.torch import load_file
|
| sd = load_file(args.resume)
|
|
|
| if getattr(args, 'use_lora', False):
|
| new_sd = {}
|
| for k, v in sd.items():
|
| k = k.replace('c_attn.weight', 'c_attn.linear.weight')
|
| k = k.replace('c_attn.bias', 'c_attn.linear.bias')
|
| k = k.replace('c_proj.weight', 'c_proj.linear.weight')
|
| k = k.replace('c_proj.bias', 'c_proj.linear.bias')
|
| k = k.replace('c_fc.weight', 'c_fc.linear.weight')
|
| k = k.replace('c_fc.bias', 'c_fc.linear.bias')
|
| new_sd[k] = v
|
| model.load_state_dict(new_sd, strict=False)
|
| else:
|
| model.load_state_dict(sd, strict=False)
|
|
|
| print("✅ Weights successfully loaded!")
|
|
|
| import gc
|
| del sd
|
| if 'new_sd' in locals():
|
| del new_sd
|
| gc.collect()
|
| torch.cuda.empty_cache()
|
|
|
| opt_path = args.resume.replace('.safetensors', '.pt')
|
| if os.path.exists(opt_path) and not getattr(args, 'use_lora', False):
|
| print(f"Loading optimizer state from {opt_path}")
|
| try:
|
| with open(opt_path, 'rb') as f:
|
| opt_state = pickle.load(f)
|
| optimizer.load_state_dict(opt_state['optimizer'])
|
| for param_group in optimizer.param_groups:
|
| param_group['lr'] = args.lr
|
| scheduler.load_state_dict(opt_state['scheduler'])
|
| scheduler.base_lrs = [args.lr for _ in optimizer.param_groups]
|
| start_step = opt_state['step']
|
|
|
| print(f"✅ Optimizer loaded! Starting from step {start_step}")
|
|
|
| del opt_state
|
| gc.collect()
|
| torch.cuda.empty_cache()
|
|
|
| except Exception as e:
|
| print(f"⚠️ Optimizer file corrupted ({e}). Starting optimizer from scratch.")
|
| try:
|
| start_step = int(os.path.basename(args.resume).split('_')[-1].split('.')[0])
|
| except:
|
| start_step = 0
|
| else:
|
| if getattr(args, 'use_lora', False):
|
| print("⚠️ LoRA mode: Optimizer state skipped, starting from scratch for adapters.")
|
| try:
|
| start_step = int(os.path.basename(args.resume).split('_')[-1].split('.')[0])
|
| except:
|
| start_step = 0
|
| else:
|
| print("⚠️ Optimizer state not found, starting optimizer from scratch.")
|
| try:
|
| start_step = int(os.path.basename(args.resume).split('_')[-1].split('.')[0])
|
| except:
|
| start_step = 0
|
| print(f"✅ Starting from step {start_step}")
|
|
|
| os.makedirs(args.save_dir, exist_ok=True)
|
| step = start_step
|
| best_loss = float('inf')
|
| best_val_loss = float('inf')
|
|
|
| model.train()
|
| progress_bar = tqdm(total=args.total_steps, initial=step, desc="Training")
|
|
|
|
|
| optimizer.zero_grad()
|
|
|
| micro_step = 0
|
| accum_loss = 0.0
|
|
|
| train_loader = FastDataloader(args.data_path, args.max_seq_len)
|
| val_loader = FastDataloader(args.val_path, args.max_seq_len)
|
| print("Начинаем обучение...")
|
| try:
|
| while step < args.total_steps:
|
| x, y = train_loader.get_batch(args.batch_size)
|
|
|
| from torch.nn.attention import SDPBackend, sdpa_kernel
|
| with sdpa_kernel([SDPBackend.FLASH_ATTENTION, SDPBackend.EFFICIENT_ATTENTION]):
|
| with torch.amp.autocast('cuda', dtype=global_dtype):
|
| logits, loss = model(x, y)
|
| loss = loss / args.accumulate_steps
|
|
|
| accum_loss += loss.item()
|
|
|
| if use_scaler:
|
| scaler.scale(loss).backward()
|
| else:
|
| loss.backward()
|
| micro_step += 1
|
|
|
| if micro_step % args.accumulate_steps == 0:
|
| if use_scaler:
|
| scaler.unscale_(optimizer)
|
| torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| scaler.step(optimizer)
|
| scaler.update()
|
| else:
|
| torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
|
| optimizer.step()
|
|
|
| optimizer.zero_grad(set_to_none=True)
|
| scheduler.step()
|
|
|
| step += 1
|
| progress_bar.update(1)
|
|
|
| if step % 100 == 0:
|
| total_mutated = 0
|
| for layer in model.transformer.h:
|
| if hasattr(layer, 'moe'):
|
| mutated = layer.moe.mutate_dead_experts(optimizer)
|
| total_mutated += mutated
|
|
|
| if total_mutated > 0:
|
| progress_bar.write(f" [Шаг {step}] Заменено мертвых экспертов -> {total_mutated}")
|
|
|
| allocated = torch.cuda.memory_allocated() / 1024**3
|
| reserved = torch.cuda.memory_reserved() / 1024**3
|
|
|
| progress_bar.set_postfix(
|
| loss=accum_loss,
|
| lr=optimizer.param_groups[0]['lr'],
|
| vram=f"{allocated:.1f}G/{reserved:.1f}G"
|
| )
|
| accum_loss = 0.0
|
|
|
| if step % 200 == 0:
|
| torch.cuda.empty_cache()
|
|
|
| if step % args.save_every == 0:
|
| ckpt_path = os.path.join(args.save_dir, f"gpt_step_{step}.safetensors")
|
| save_model(model, ckpt_path)
|
|
|
| opt_path = ckpt_path.replace('.safetensors', '.pt')
|
| with open(opt_path, 'wb') as f:
|
| pickle.dump({
|
| 'optimizer': optimizer.state_dict(),
|
| 'scheduler': scheduler.state_dict(),
|
| 'step': step
|
| }, f)
|
| print(f"\nSaved checkpoint to {ckpt_path} and optimizer state")
|
|
|
| if args.val_path and os.path.exists(args.val_path):
|
| print(f"Running validation at step {step}...")
|
| val_loss = validate(
|
| model,
|
| val_loader,
|
| args.batch_size,
|
| eval_iters=50
|
| )
|
| print(f"Step {step}: val loss = {val_loss:.4f}")
|
| if val_loss < best_val_loss:
|
| best_val_loss = val_loss
|
| best_path = os.path.join(args.save_dir, "gpt_best.safetensors")
|
| save_model(model, best_path)
|
| print(f"New best model saved with val loss {val_loss:.4f}")
|
|
|
| except KeyboardInterrupt:
|
| print("\n⚠️ Обучение прервано вручную (Ctrl+C)! Переходим к сохранению...")
|
|
|
| print(" Сохраняем финальную модель...")
|
| final_path = os.path.join(args.save_dir, "gpt_final.safetensors")
|
| state_dict = model.state_dict()
|
|
|
| if 'lm_head.weight' in state_dict and 'transformer.wte.weight' in state_dict:
|
| if (state_dict['lm_head.weight'].data_ptr() == state_dict['transformer.wte.weight'].data_ptr()):
|
| state_dict['lm_head.weight'] = state_dict['lm_head.weight'].clone()
|
|
|
| save_model(model, final_path)
|
| print(f"Model saved to {final_path}")
|
|
|
| opt_path = final_path.replace('.safetensors', '.pt')
|
| with open(opt_path, 'wb') as f:
|
| pickle.dump({
|
| 'optimizer': optimizer.state_dict(),
|
| 'scheduler': scheduler.state_dict(),
|
| 'step': step
|
| }, f)
|
| print(f"Optimizer state saved to {opt_path}")
|
| print("Training finished.")
|
|
|
| if __name__ == "__main__":
|
| parser = argparse.ArgumentParser()
|
| parser.add_argument('--data_path', type=str, required=True)
|
| parser.add_argument('--val_path', type=str, default=None)
|
| parser.add_argument('--tokenizer_path', type=str, default='tokenizer.json')
|
| parser.add_argument('--vocab_size', type=int, default=32000)
|
| parser.add_argument('--embed_dim', type=int, default=256)
|
| parser.add_argument('--n_layers', type=int, default=6)
|
| parser.add_argument('--n_heads', type=int, default=8)
|
| parser.add_argument('--max_seq_len', type=int, default=256)
|
| parser.add_argument('--dropout', type=float, default=0.0)
|
| parser.add_argument('--batch_size', type=int, default=8)
|
| parser.add_argument('--lr', type=float, default=3e-5)
|
| parser.add_argument('--weight_decay', type=float, default=0.1)
|
| parser.add_argument('--grad_clip', type=float, default=1.0)
|
| parser.add_argument('--warmup_steps', type=int, default=500)
|
| parser.add_argument('--total_steps', type=int, default=100000)
|
| parser.add_argument('--save_every', type=int, default=5000)
|
| parser.add_argument('--save_dir', type=str, default='checkpoints')
|
| parser.add_argument('--device', type=str, default='cuda')
|
| parser.add_argument('--resume', type=str, default=None)
|
| parser.add_argument('--accumulate_steps', type=int, default=8, help="Шагов накопления для виртуального батча")
|
| parser.add_argument('--use_lora', action='store_true', help="Включить адаптацию через LoRA")
|
| args = parser.parse_args()
|
| train(args)
|
|
|