| import os |
| |
| os.environ["NCCL_P2P_DISABLE"] = "1" |
| os.environ["NCCL_IB_DISABLE"] = "1" |
| os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1" |
| os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1" |
| |
| os.environ["TOKENIZERS_PARALLELISM"] = "false" |
|
|
| import math |
| import time |
| import argparse |
| import logging |
| from pathlib import Path |
|
|
| import torch |
| import torch.distributed as dist |
| from torch.nn.parallel import DistributedDataParallel as DDP |
| |
| from torch.amp import GradScaler, autocast |
| import numpy as np |
| from transformers import AutoTokenizer |
| from huggingface_hub import hf_hub_download, HfApi |
| from huggingface_hub.utils import EntryNotFoundError, HfHubHTTPError |
|
|
| try: |
| from kaggle_secrets import UserSecretsClient |
| HAS_KAGGLE_SECRETS = True |
| except ImportError: |
| HAS_KAGGLE_SECRETS = False |
|
|
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") |
| logger = logging.getLogger(__name__) |
|
|
| def get_hf_token(): |
| token = os.environ.get("HF_TOKEN") |
| if token: |
| token = token.strip() |
| logger.info("Loaded HF_TOKEN from environment.") |
| return token |
| if HAS_KAGGLE_SECRETS: |
| try: |
| token = UserSecretsClient().get_secret("HF_TOKEN") |
| if token: |
| token = token.strip() |
| os.environ["HF_TOKEN"] = token |
| logger.info("Loaded HF_TOKEN from kaggle_secrets.") |
| return token |
| except Exception as e: |
| logger.warning(f"Failed to get HF_TOKEN from kaggle_secrets: {e}") |
| raise ValueError("HF_TOKEN not found in environment or kaggle_secrets.") |
|
|
| def setup_ddp(): |
| if "RANK" not in os.environ or "WORLD_SIZE" not in os.environ: |
| return 0, 0, 1, torch.device("cuda" if torch.cuda.is_available() else "cpu"), False |
| dist.init_process_group("nccl") |
| local_rank = int(os.environ["LOCAL_RANK"]) |
| device = torch.device(f"cuda:{local_rank}") |
| torch.cuda.set_device(device) |
| return int(os.environ["RANK"]), local_rank, int(os.environ["WORLD_SIZE"]), device, True |
|
|
| def load_tokenizer_robust(hf_token: str, model_repo: str, subfolder: str, is_main: bool): |
| if is_main: logger.info("Loading tokenizer...") |
| local_candidates = [ |
| "/kaggle/working/ViuRec/tokenizer", |
| "/kaggle/input/viuai-500m-tokenizer", |
| "./tokenizer", |
| "./viuai-500m-tokenizer" |
| ] |
| |
| for fast in [True, False]: |
| for p_str in local_candidates: |
| p = Path(p_str) |
| if (p / "tokenizer.json").exists() or (p / "vocab.json").exists() or (p / "tokenizer.model").exists(): |
| try: |
| tok = AutoTokenizer.from_pretrained(str(p), local_files_only=True, use_fast=fast, trust_remote_code=True) |
| if is_main: logger.info(f"Loaded tokenizer from {p} fast={fast}") |
| return tok |
| except Exception as e: |
| if is_main: logger.warning(f"Local tokenizer {p} fast={fast} failed: {e}") |
| continue |
|
|
| |
| hf_repos_to_try = [model_repo, "ViuAI/viuai-500m-tokenizer", "ViuAI/ViuRec"] |
| for repo_try in hf_repos_to_try: |
| for attempt_subfolder in [subfolder, None]: |
| try: |
| if attempt_subfolder: |
| tok = AutoTokenizer.from_pretrained(repo_try, subfolder=attempt_subfolder, token=hf_token, use_fast=fast, trust_remote_code=True) |
| else: |
| tok = AutoTokenizer.from_pretrained(repo_try, token=hf_token, use_fast=fast, trust_remote_code=True) |
| if is_main: logger.info(f"Loaded tokenizer from HF {repo_try} subfolder={attempt_subfolder} fast={fast}") |
| return tok |
| except Exception as e: |
| err = str(e).lower() |
| if "sentencepiece" in err or "tiktoken" in err or "protobuf" in err: |
| if is_main: logger.warning(f"Tokenizer needs sentencepiece/tiktoken/protobuf: {e}") |
| |
| continue |
| if is_main: logger.debug(f"HF tokenizer try failed {repo_try} fast={fast} subfolder={attempt_subfolder}: {e}") |
| continue |
|
|
| if is_main: |
| logger.warning("Tokenizer failed, using dummy vocab 64000") |
| return None |
|
|
| def load_checkpoint(model, optimizer, scaler, api, token, repo_id, filename="checkpoints/latest_chk.pt", strict=False): |
| |
| |
| local_path = Path(filename) |
| local_path.parent.mkdir(parents=True, exist_ok=True) |
| try: |
| logger.info(f"Checking for existing checkpoint on HF: {filename}") |
| |
| downloaded_path = hf_hub_download(repo_id=repo_id, filename=filename, local_dir=".", token=token) |
| logger.info(f"Downloaded checkpoint to {downloaded_path}") |
| |
| ckpt = torch.load(downloaded_path, map_location="cpu", weights_only=False) |
| missing, unexpected = model.load_state_dict(ckpt["model"], strict=strict) |
| if missing or unexpected: |
| logger.warning(f"Checkpoint loaded with Missing: {len(missing)} | Unexpected: {len(unexpected)}") |
| else: |
| logger.info("Model weights loaded successfully.") |
| |
| try: |
| optimizer.load_state_dict(ckpt["optimizer"]) |
| except Exception as e: |
| logger.warning(f"Optimizer state load failed, continuing: {e}") |
| if "scaler" in ckpt and scaler is not None: |
| try: |
| scaler.load_state_dict(ckpt["scaler"]) |
| except Exception as e: |
| logger.warning(f"Scaler state load failed: {e}") |
| |
| step = ckpt.get("step", 0) |
| logger.info(f"Resuming from step {step}") |
| return step |
| except (EntryNotFoundError, HfHubHTTPError): |
| logger.info("No checkpoint found on HF. Starting from scratch.") |
| return 0 |
| except Exception as e: |
| logger.error(f"Error loading checkpoint: {e}") |
| return 0 |
|
|
| class ShardPool: |
| def __init__(self, api, repo_id, work_dir, token, vocab_size, max_pool=3): |
| self.api = api |
| self.repo_id = repo_id |
| self.work_dir = Path(work_dir) |
| self.work_dir.mkdir(parents=True, exist_ok=True) |
| self.token = token |
| self.max_pool = max_pool |
| self.pool = [] |
| self.available_files = [] |
| self.vocab_size = vocab_size |
| self._refresh_file_list() |
| |
| def _refresh_file_list(self): |
| try: |
| files = self.api.list_repo_files(repo_id=self.repo_id, repo_type="dataset", token=self.token) |
| except Exception as e: |
| logger.warning(f"Failed to list dataset repo {self.repo_id}, trying as model repo. Error: {e}") |
| files = self.api.list_repo_files(repo_id=self.repo_id, repo_type="model", token=self.token) |
| self.available_files = [f for f in files if f.startswith("shards/") and f.endswith(".bin") and not f.endswith("_val.bin")] |
| np.random.shuffle(self.available_files) |
| logger.info(f"Found {len(self.available_files)} training shards") |
| |
| def fill(self, ctx_len): |
| max_attempts = self.max_pool * 5 |
| attempts = 0 |
| while len(self.pool) < self.max_pool: |
| if attempts >= max_attempts: |
| raise RuntimeError("Failed to fill ShardPool: too many invalid shards.") |
| if not self.available_files: |
| self._refresh_file_list() |
| if not self.available_files: |
| raise RuntimeError("No shard files available") |
| f = self.available_files.pop() |
| local_p = hf_hub_download(repo_id=self.repo_id, repo_type="dataset", filename=f, local_dir=self.work_dir, token=self.token) |
| |
| dtype = np.uint16 if self.vocab_size <= 65535 else np.uint32 |
| mmap_arr = np.memmap(local_p, dtype=dtype, mode="r") |
| |
| if len(mmap_arr) < ctx_len + 1: |
| logger.warning(f"Shard {f} too small ({len(mmap_arr)}). Discarding.") |
| del mmap_arr |
| Path(local_p).unlink(missing_ok=True) |
| attempts += 1 |
| continue |
|
|
| |
| if len(mmap_arr) > 1000: |
| sample_max = int(mmap_arr[:1000].max()) |
| if sample_max >= self.vocab_size: |
| logger.warning(f"Shard {f} has token {sample_max} >= vocab {self.vocab_size}. Discarding.") |
| del mmap_arr |
| Path(local_p).unlink(missing_ok=True) |
| attempts += 1 |
| continue |
| |
| self.pool.append((local_p, mmap_arr)) |
| logger.info(f"Loaded shard {f} ({len(mmap_arr)} tokens)") |
| |
| def sample_batch(self, batch_size, ctx_len, rng): |
| xs, ys = [], [] |
| while len(xs) < batch_size: |
| idx = rng.integers(0, len(self.pool)) |
| _, tokens = self.pool[idx] |
| max_start = len(tokens) - ctx_len - 1 |
| if max_start <= 0: |
| continue |
| i = rng.integers(0, max_start) |
| xs.append(np.array(tokens[i:i+ctx_len], dtype=np.int64)) |
| ys.append(np.array(tokens[i+1:i+1+ctx_len], dtype=np.int64)) |
| return np.stack(xs), np.stack(ys) |
| |
| def rotate_one(self): |
| if not self.pool: return |
| old_path, old_mmap = self.pool.pop(0) |
| del old_mmap |
| try: |
| Path(old_path).unlink(missing_ok=True) |
| logger.info(f"Rotated out shard {Path(old_path).name}") |
| except Exception as e: |
| logger.warning(f"Failed to delete {old_path}: {e}") |
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="ViuResonance Training - T4 Optimized") |
| parser.add_argument("--max-steps", type=int, default=10000) |
| parser.add_argument("--micro-batch", type=int, default=1, help="T4 16GB ke liye 1 rakho") |
| parser.add_argument("--grad-accum", type=int, default=16) |
| parser.add_argument("--eval-every", type=int, default=500) |
| parser.add_argument("--save-every", type=int, default=500) |
| parser.add_argument("--rotate-every", type=int, default=500) |
| parser.add_argument("--lr", type=float, default=3e-4) |
| parser.add_argument("--weight-decay", type=float, default=0.1) |
| parser.add_argument("--ctx-len", type=int, default=1024, help="T4 pe 1024 safe, 2048 OOM de sakta hai") |
| parser.add_argument("--vocab-size", type=int, default=None) |
| args = parser.parse_args() |
|
|
| |
| torch.backends.cuda.matmul.allow_tf32 = True |
| torch.backends.cudnn.allow_tf32 = True |
| torch.backends.cudnn.benchmark = True |
|
|
| token = get_hf_token() |
| rank, local_rank, world_size, device, is_ddp = setup_ddp() |
| is_main = (rank == 0) |
| |
| |
| seed = 42 + rank |
| torch.manual_seed(seed) |
| torch.cuda.manual_seed_all(seed) |
| np.random.seed(seed) |
| rng = np.random.default_rng(seed) |
|
|
| |
| tokenizer = load_tokenizer_robust(token, "ViuAI/ViuRec", "tokenizer", is_main) |
| vocab_size = args.vocab_size or (len(tokenizer) if tokenizer is not None else 64000) |
| if is_main: |
| logger.info(f"Using vocab_size={vocab_size}, ctx_len={args.ctx_len}, device={device}, is_ddp={is_ddp}") |
|
|
| |
| try: |
| from model_fixed_T4 import ViuResonance100M, ModelConfig |
| except ModuleNotFoundError: |
| try: |
| from model import ViuResonance100M, ModelConfig |
| except ModuleNotFoundError: |
| |
| import importlib.util, sys |
| for cand in ["./model_fixed_T4.py", "./model.py", "/kaggle/working/code/model_fixed_T4.py", "/kaggle/working/code/model.py"]: |
| p = Path(cand) |
| if p.exists(): |
| spec = importlib.util.spec_from_file_location("model", str(p)) |
| mod = importlib.util.module_from_spec(spec) |
| sys.modules["model"] = mod |
| spec.loader.exec_module(mod) |
| ViuResonance100M = mod.ViuResonance100M |
| ModelConfig = mod.ModelConfig |
| break |
| else: |
| raise ModuleNotFoundError("model.py / model_fixed_T4.py nahi mila - dono files same folder me rakho") |
|
|
| config = ModelConfig(vocab_size=vocab_size, chunk_size=256) |
| unwrapped_model = ViuResonance100M(config).to(device) |
| |
| |
| try: |
| unwrapped_model = torch.compile(unwrapped_model, mode="default") |
| if is_main: logger.info("Compiled model successfully (before DDP).") |
| compiled = True |
| except Exception as e: |
| if is_main: logger.warning(f"Failed to compile model: {e}. Falling back to eager.") |
| compiled = False |
|
|
| if is_ddp: |
| model = DDP(unwrapped_model, device_ids=[local_rank], output_device=local_rank, broadcast_buffers=False) |
| else: |
| model = unwrapped_model |
|
|
| |
| try: |
| optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay, betas=(0.9, 0.95), fused=True) |
| if is_main: logger.info("Using fused AdamW") |
| except Exception as e: |
| if is_main: logger.warning(f"fused=True failed ({e}). Using non-fused AdamW for T4.") |
| optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay, betas=(0.9, 0.95)) |
|
|
| |
| use_bf16 = torch.cuda.is_available() and torch.cuda.is_bf16_supported() |
| amp_dtype = torch.bfloat16 if use_bf16 else torch.float16 |
| scaler = GradScaler('cuda', enabled=not use_bf16) |
| if is_main: |
| logger.info(f"T4 Detected: bf16_supported={use_bf16}, using amp_dtype={amp_dtype}, scaler_enabled={scaler.is_enabled()}") |
|
|
| api = HfApi() |
| start_step = load_checkpoint(unwrapped_model, optimizer, scaler, api, token, "ViuAI/ViuRec", strict=False) |
|
|
| work_dir = Path("/kaggle/working" if Path("/kaggle/working").exists() else ".") |
| shard_pool = ShardPool(api, "ViuAI/viuai-500m-data", work_dir / "shards", token, vocab_size) |
| shard_pool.fill(args.ctx_len) |
|
|
| |
| val_tokens = None |
| if is_main: |
| logger.info("Loading validation data...") |
| try: |
| val_files = [f for f in api.list_repo_files(repo_id="ViuAI/viuai-500m-data", repo_type="dataset", token=token) if f.startswith("shards/") and f.endswith("_val.bin")] |
| except Exception: |
| val_files = [f for f in api.list_repo_files(repo_id="ViuAI/viuai-500m-data", repo_type="model", token=token) if f.startswith("shards/") and f.endswith("_val.bin")] |
| try: |
| if val_files: |
| val_p = hf_hub_download(repo_id="ViuAI/viuai-500m-data", repo_type="dataset", filename=val_files[0], local_dir=work_dir, token=token) |
| dtype = np.uint16 if vocab_size <= 65535 else np.uint32 |
| val_arr = np.memmap(val_p, dtype=dtype, mode="r") |
| max_toks = 2_000_000 |
| val_tokens = np.array(val_arr[:max_toks]) |
| del val_arr |
| |
| |
| except Exception as e: |
| logger.warning(f"Failed to load val data: {e}") |
|
|
| @torch.no_grad() |
| def evaluate(): |
| if val_tokens is None or len(val_tokens) < args.ctx_len + 1: return 0.0 |
| unwrapped_model.eval() |
| val_loss = 0.0 |
| val_iters = 10 |
| for _ in range(val_iters): |
| i = rng.integers(0, len(val_tokens) - args.ctx_len - 1) |
| x = torch.from_numpy(val_tokens[i:i+args.ctx_len].astype(np.int64)).unsqueeze(0).to(device, non_blocking=True) |
| y = torch.from_numpy(val_tokens[i+1:i+1+args.ctx_len].astype(np.int64)).unsqueeze(0).to(device, non_blocking=True) |
| with autocast('cuda', dtype=amp_dtype): |
| _, loss = unwrapped_model(x, y, use_checkpoint=False) |
| val_loss += loss.item() |
| unwrapped_model.train() |
| return val_loss / val_iters |
|
|
| |
| model.train() |
| if is_main: |
| logger.info(f"Starting training for {args.max_steps} steps from step {start_step}...") |
| |
| for step in range(start_step, args.max_steps): |
| |
| warmup = 1000 |
| progress = min(1.0, step / max(1, warmup)) |
| if step < warmup: |
| lr = args.lr * progress |
| else: |
| lr = args.lr * (0.5 * (1.0 + math.cos(math.pi * (step - warmup) / max(1, args.max_steps - warmup)))) |
| for param_group in optimizer.param_groups: |
| param_group['lr'] = lr |
|
|
| optimizer.zero_grad(set_to_none=True) |
| accum_loss = 0.0 |
| |
| t0 = time.time() |
| for micro_i in range(args.grad_accum): |
| x_np, y_np = shard_pool.sample_batch(args.micro_batch, args.ctx_len, rng) |
| x = torch.from_numpy(x_np).to(device, non_blocking=True) |
| y = torch.from_numpy(y_np).to(device, non_blocking=True) |
| |
| with autocast('cuda', dtype=amp_dtype): |
| _, loss = model(x, y) |
| loss = loss / args.grad_accum |
| |
| scaler.scale(loss).backward() |
| accum_loss += loss.item() |
| del x, y, loss |
|
|
| |
| scaler.unscale_(optimizer) |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) |
| |
| |
| has_nan = not math.isfinite(accum_loss) |
| if not has_nan: |
| for p in model.parameters(): |
| if p.grad is not None and not torch.isfinite(p.grad).all(): |
| has_nan = True |
| break |
| |
| if has_nan: |
| if is_main: logger.warning(f"NaN/Inf detected at step {step}! Skipping.") |
| optimizer.zero_grad(set_to_none=True) |
| scaler.update() |
| continue |
| |
| scaler.step(optimizer) |
| scaler.update() |
|
|
| dt = time.time() - t0 |
| |
| if is_main and step % 10 == 0: |
| tok_s = (args.micro_batch * args.grad_accum * args.ctx_len * world_size) / dt if dt > 0 else 0 |
| mem_alloc = torch.cuda.memory_allocated(device) / 1e9 if torch.cuda.is_available() else 0 |
| mem_res = torch.cuda.memory_reserved(device) / 1e9 if torch.cuda.is_available() else 0 |
| logger.info(f"step {step:5d}/{args.max_steps} | loss {accum_loss:.4f} | lr {lr:.2e} | {tok_s:.0f} tok/s | mem {mem_alloc:.2f}GB/{mem_res:.2f}GB") |
|
|
| if step > start_step and step % args.rotate_every == 0: |
| shard_pool.rotate_one() |
| shard_pool.fill(args.ctx_len) |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
|
|
| if step > start_step and step % args.eval_every == 0 and is_main: |
| val_loss = evaluate() |
| logger.info(f"--- Eval step {step}: val_loss {val_loss:.4f} ---") |
|
|
| if step > start_step and step % args.save_every == 0: |
| if is_ddp: dist.barrier() |
| if is_main: |
| logger.info(f"Saving checkpoint at step {step}...") |
| ckpt_path = Path("checkpoints/latest_chk.pt") |
| ckpt_path.parent.mkdir(parents=True, exist_ok=True) |
| chk = { |
| "model": unwrapped_model.state_dict(), |
| "optimizer": optimizer.state_dict(), |
| "scaler": scaler.state_dict(), |
| "step": step, |
| } |
| torch.save(chk, ckpt_path) |
| try: |
| api.upload_file( |
| path_or_fileobj=str(ckpt_path), |
| path_in_repo="checkpoints/latest_chk.pt", |
| repo_id="ViuAI/ViuRec", |
| token=token |
| ) |
| logger.info("Upload successful.") |
| except Exception as e: |
| logger.error(f"Upload failed: {e}") |
| if is_ddp: dist.barrier() |
|
|
| if is_ddp: |
| dist.destroy_process_group() |
|
|
| if __name__ == "__main__": |
| main() |
|
|