| """ |
| Hebrew DINT Transformer β A from-scratch Hebrew LLM |
| Architecture based on DINT Transformer (arxiv:2501.17486) which extends |
| Differential Attention (arxiv:2410.05258) with integral global context. |
| |
| Key innovations: |
| - Differential Attention: cancels attention noise via dual softmax subtraction |
| - DINT integral term: adds global token importance awareness |
| - Pre-RMSNorm, SwiGLU FFN, RoPE (LLaMA-style macro layout) |
| - Per-head RMSNorm after differential attention for gradient stability |
| """ |
|
|
| import math |
| from typing import Optional |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
|
|
| |
| class RMSNorm(nn.Module): |
| def __init__(self, dim: int, eps: float = 1e-6): |
| super().__init__() |
| self.eps = eps |
| self.weight = nn.Parameter(torch.ones(dim)) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| norm = torch.rsqrt(x.float().pow(2).mean(-1, keepdim=True) + self.eps) |
| return (x.float() * norm).type_as(x) * self.weight |
|
|
|
|
| |
| class RotaryEmbedding(nn.Module): |
| def __init__(self, dim: int, max_seq_len: int = 4096, theta: float = 10000.0): |
| super().__init__() |
| inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim)) |
| self.register_buffer("inv_freq", inv_freq, persistent=False) |
| self.max_seq_len = max_seq_len |
|
|
| def forward(self, seq_len: int, device: torch.device, dtype: torch.dtype): |
| t = torch.arange(seq_len, device=device, dtype=torch.float32) |
| freqs = torch.outer(t, self.inv_freq.to(device)) |
| cos = freqs.cos().to(dtype) |
| sin = freqs.sin().to(dtype) |
| return cos, sin |
|
|
|
|
| def apply_rotary_emb(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor: |
| """Apply rotary embeddings. x: [B, S, H, D], cos/sin: [S, D/2]""" |
| rotary_dim = cos.shape[-1] * 2 |
| x_rot = x[..., :rotary_dim] |
| x_pass = x[..., rotary_dim:] |
|
|
| x1, x2 = x_rot[..., ::2], x_rot[..., 1::2] |
| cos = cos.unsqueeze(0).unsqueeze(2) |
| sin = sin.unsqueeze(0).unsqueeze(2) |
|
|
| rot_x1 = x1 * cos - x2 * sin |
| rot_x2 = x1 * sin + x2 * cos |
| rot_x = torch.stack([rot_x1, rot_x2], dim=-1).reshape_as(x_rot) |
| return torch.cat([rot_x, x_pass], dim=-1) |
|
|
|
|
| |
| def lambda_init_fn(depth: int) -> float: |
| """Per-layer Ξ» initialization schedule from DIFF Transformer paper.""" |
| return 0.8 - 0.6 * math.exp(-0.3 * depth) |
|
|
|
|
| class DINTAttention(nn.Module): |
| """ |
| DINT (Differential + INTegral) Attention. |
| |
| DIFF: A = softmax(Q1·K1^T) - λ·softmax(Q2·K2^T) (noise cancellation) |
| DINT: adds global integral term G = mean(A_pos, dim=0) broadcast back |
| Final: A_diff + Ξ³Β·G (where Ξ³ = Ξ» for row-normalization stability) |
| |
| Uses SDPA (Flash Attention compatible) for efficiency. |
| """ |
| def __init__( |
| self, |
| embed_dim: int, |
| depth: int, |
| num_heads: int, |
| num_kv_heads: Optional[int] = None, |
| ): |
| super().__init__() |
| self.embed_dim = embed_dim |
| self.num_heads = num_heads |
| self.num_kv_heads = num_kv_heads or num_heads |
| self.n_rep = self.num_heads // self.num_kv_heads |
|
|
| |
| self.head_dim = embed_dim // self.num_heads // 2 |
| assert self.head_dim * self.num_heads * 2 == embed_dim |
|
|
| |
| self.q_proj = nn.Linear(embed_dim, embed_dim, bias=False) |
| self.k_proj = nn.Linear(embed_dim, embed_dim // self.n_rep, bias=False) |
| self.v_proj = nn.Linear(embed_dim, embed_dim // self.n_rep, bias=False) |
| self.out_proj = nn.Linear(embed_dim, embed_dim, bias=False) |
|
|
| |
| self.lambda_init = lambda_init_fn(depth) |
| self.lambda_q1 = nn.Parameter(torch.randn(self.head_dim) * 0.1) |
| self.lambda_k1 = nn.Parameter(torch.randn(self.head_dim) * 0.1) |
| self.lambda_q2 = nn.Parameter(torch.randn(self.head_dim) * 0.1) |
| self.lambda_k2 = nn.Parameter(torch.randn(self.head_dim) * 0.1) |
|
|
| |
| self.subln = nn.LayerNorm(2 * self.head_dim, eps=1e-5) |
|
|
| def _repeat_kv(self, x: torch.Tensor) -> torch.Tensor: |
| """Repeat K/V heads for GQA.""" |
| bs, n_kv_heads, slen, head_dim = x.shape |
| if self.n_rep == 1: |
| return x |
| return ( |
| x[:, :, None, :, :] |
| .expand(bs, n_kv_heads, self.n_rep, slen, head_dim) |
| .reshape(bs, n_kv_heads * self.n_rep, slen, head_dim) |
| ) |
|
|
| def forward( |
| self, |
| x: torch.Tensor, |
| cos: torch.Tensor, |
| sin: torch.Tensor, |
| ) -> torch.Tensor: |
| bsz, seq_len, _ = x.size() |
|
|
| |
| q = self.q_proj(x) |
| k = self.k_proj(x) |
| v = self.v_proj(x) |
|
|
| |
| q = q.view(bsz, seq_len, 2 * self.num_heads, self.head_dim) |
| k = k.view(bsz, seq_len, 2 * self.num_kv_heads, self.head_dim) |
| v = v.view(bsz, seq_len, self.num_kv_heads, 2 * self.head_dim) |
|
|
| |
| q = apply_rotary_emb(q, cos, sin) |
| k = apply_rotary_emb(k, cos, sin) |
|
|
| |
| q = q.transpose(1, 2) |
| k = k.transpose(1, 2) |
| v = v.transpose(1, 2) |
|
|
| |
| k = self._repeat_kv(k) |
| v = self._repeat_kv(v) |
|
|
| |
| q_pairs = q.view(bsz, 2, self.num_heads, seq_len, self.head_dim).permute(0, 2, 1, 3, 4) |
| k_pairs = k.view(bsz, 2, self.num_heads, seq_len, self.head_dim).permute(0, 2, 1, 3, 4) |
|
|
| q_pos, q_neg = q_pairs[:, :, 0], q_pairs[:, :, 1] |
| k_pos, k_neg = k_pairs[:, :, 0], k_pairs[:, :, 1] |
|
|
| |
| lambda_1 = torch.exp(torch.sum(self.lambda_q1 * self.lambda_k1)).type_as(q_pos) |
| lambda_2 = torch.exp(torch.sum(self.lambda_q2 * self.lambda_k2)).type_as(q_pos) |
| lambda_full = lambda_1 - lambda_2 + self.lambda_init |
|
|
| |
| ctx_pos = F.scaled_dot_product_attention(q_pos, k_pos, v, is_causal=True) |
| ctx_neg = F.scaled_dot_product_attention(q_neg, k_neg, v, is_causal=True) |
|
|
| |
| attn_out = ctx_pos - lambda_full * ctx_neg |
|
|
| |
| |
| |
| global_ctx = ctx_pos.mean(dim=2, keepdim=True).expand_as(ctx_pos) |
| attn_out = attn_out + lambda_full * global_ctx |
|
|
| |
| attn_out = self.subln(attn_out) * (1.0 - self.lambda_init) |
|
|
| |
| attn_out = attn_out.transpose(1, 2).reshape(bsz, seq_len, self.embed_dim) |
| return self.out_proj(attn_out) |
|
|
|
|
| |
| class SwiGLUFFN(nn.Module): |
| """SwiGLU feed-forward network (LLaMA-style).""" |
| def __init__(self, embed_dim: int, ffn_dim: int): |
| super().__init__() |
| self.gate_proj = nn.Linear(embed_dim, ffn_dim, bias=False) |
| self.up_proj = nn.Linear(embed_dim, ffn_dim, bias=False) |
| self.down_proj = nn.Linear(ffn_dim, embed_dim, bias=False) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x)) |
|
|
|
|
| |
| class DINTTransformerBlock(nn.Module): |
| def __init__(self, embed_dim: int, num_heads: int, ffn_dim: int, depth: int): |
| super().__init__() |
| self.attn = DINTAttention(embed_dim, depth, num_heads) |
| self.ffn = SwiGLUFFN(embed_dim, ffn_dim) |
| self.attn_norm = RMSNorm(embed_dim) |
| self.ffn_norm = RMSNorm(embed_dim) |
|
|
| def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor: |
| |
| x = x + self.attn(self.attn_norm(x), cos, sin) |
| x = x + self.ffn(self.ffn_norm(x)) |
| return x |
|
|
|
|
| |
| class DINTTransformerLM(nn.Module): |
| """ |
| DINT Transformer Language Model for Hebrew. |
| |
| Architecture: Pre-RMSNorm + DINT Attention + SwiGLU FFN + RoPE |
| Based on: arxiv:2501.17486 (DINT) + arxiv:2410.05258 (DIFF Transformer) |
| """ |
| def __init__( |
| self, |
| vocab_size: int = 32000, |
| embed_dim: int = 2048, |
| num_layers: int = 24, |
| num_heads: int = 16, |
| ffn_dim: int = 5504, |
| max_seq_len: int = 2048, |
| tie_embeddings: bool = True, |
| ): |
| super().__init__() |
| self.vocab_size = vocab_size |
| self.embed_dim = embed_dim |
| self.max_seq_len = max_seq_len |
|
|
| |
| self.token_emb = nn.Embedding(vocab_size, embed_dim) |
|
|
| |
| head_dim = embed_dim // num_heads // 2 |
| self.rotary = RotaryEmbedding(head_dim, max_seq_len) |
|
|
| |
| self.layers = nn.ModuleList([ |
| DINTTransformerBlock(embed_dim, num_heads, ffn_dim, depth=i) |
| for i in range(num_layers) |
| ]) |
|
|
| |
| self.norm = RMSNorm(embed_dim) |
| self.lm_head = nn.Linear(embed_dim, vocab_size, bias=False) |
|
|
| |
| if tie_embeddings: |
| self.lm_head.weight = self.token_emb.weight |
|
|
| |
| self.apply(self._init_weights) |
|
|
| def _init_weights(self, module): |
| if isinstance(module, nn.Linear): |
| torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) |
| if module.bias is not None: |
| torch.nn.init.zeros_(module.bias) |
| elif isinstance(module, nn.Embedding): |
| torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) |
|
|
| def forward( |
| self, |
| input_ids: torch.Tensor, |
| labels: Optional[torch.Tensor] = None, |
| ) -> dict: |
| bsz, seq_len = input_ids.shape |
| assert seq_len <= self.max_seq_len, f"Sequence length {seq_len} exceeds max {self.max_seq_len}" |
|
|
| |
| x = self.token_emb(input_ids) |
|
|
| |
| cos, sin = self.rotary(seq_len, x.device, x.dtype) |
|
|
| |
| for layer in self.layers: |
| x = layer(x, cos, sin) |
|
|
| |
| x = self.norm(x) |
| logits = self.lm_head(x) |
|
|
| loss = None |
| if labels is not None: |
| shift_logits = logits[..., :-1, :].contiguous() |
| shift_labels = labels[..., 1:].contiguous() |
| loss = F.cross_entropy( |
| shift_logits.view(-1, self.vocab_size), |
| shift_labels.view(-1), |
| ignore_index=-100, |
| ) |
|
|
| return {"loss": loss, "logits": logits} |
|
|
| def count_parameters(self) -> int: |
| return sum(p.numel() for p in self.parameters() if p.requires_grad) |
|
|
|
|
| |
| def create_hebrew_dint_1_5b(vocab_size: int = 32000) -> DINTTransformerLM: |
| """~1.5B parameter DINT Transformer for Hebrew.""" |
| return DINTTransformerLM( |
| vocab_size=vocab_size, |
| embed_dim=2048, |
| num_layers=24, |
| num_heads=16, |
| ffn_dim=5504, |
| max_seq_len=2048, |
| ) |
|
|
|
|
| def create_hebrew_dint_400m(vocab_size: int = 32000) -> DINTTransformerLM: |
| """~400M parameter DINT Transformer for Hebrew (faster iteration).""" |
| return DINTTransformerLM( |
| vocab_size=vocab_size, |
| embed_dim=1024, |
| num_layers=20, |
| num_heads=8, |
| ffn_dim=2816, |
| max_seq_len=2048, |
| ) |
|
|
|
|
|
|
| |
| |
| |
| """ |
| Hebrew DINT Transformer β Pretraining Script |
| |
| Trains a DINT Transformer (Differential + Integral Attention) from scratch |
| on Hebrew text data (HeDC4 + OzLabs Wikipedia + Ben Yehuda + Military + Wiktionary). |
| |
| Architecture: arxiv:2501.17486 (DINT) + arxiv:2410.05258 (DIFF Transformer) |
| Training recipe: Informed by DictaLM 2.0 (arxiv:2407.07080) hyperparameters |
| """ |
|
|
| import os |
| import sys |
| import math |
| import json |
| import time |
| import argparse |
| from pathlib import Path |
|
|
| import torch |
| import torch.nn.functional as F |
| from torch.utils.data import DataLoader, Dataset, IterableDataset |
| from torch.cuda.amp import GradScaler |
| from torch.optim import AdamW |
| from torch.optim.lr_scheduler import CosineAnnealingLR |
|
|
| |
| |
|
|
| |
| def get_args(): |
| parser = argparse.ArgumentParser(description="Train Hebrew DINT Transformer") |
| |
| parser.add_argument("--model_size", type=str, default="400m", choices=["400m", "1.5b"]) |
| parser.add_argument("--vocab_size", type=int, default=32000) |
| parser.add_argument("--max_seq_len", type=int, default=2048) |
|
|
| |
| parser.add_argument("--batch_size", type=int, default=8) |
| parser.add_argument("--grad_accum", type=int, default=8) |
| parser.add_argument("--lr", type=float, default=3e-4) |
| parser.add_argument("--min_lr", type=float, default=3e-5) |
| parser.add_argument("--weight_decay", type=float, default=0.1) |
| parser.add_argument("--warmup_steps", type=int, default=1000) |
| parser.add_argument("--max_steps", type=int, default=50000) |
| parser.add_argument("--bf16", action="store_true", default=True) |
| parser.add_argument("--gradient_checkpointing", action="store_true", default=True) |
|
|
| |
| parser.add_argument("--tokenizer_path", type=str, default="./tokenizer") |
|
|
| |
| parser.add_argument("--output_dir", type=str, default="./hebrew-dint-transformer") |
| parser.add_argument("--hub_model_id", type=str, default="guychuk/hebrew-dint-transformer") |
| parser.add_argument("--log_every", type=int, default=10) |
| parser.add_argument("--save_every", type=int, default=2000) |
| parser.add_argument("--eval_every", type=int, default=500) |
|
|
| return parser.parse_args() |
|
|
|
|
| |
| def train_hebrew_tokenizer(vocab_size: int, save_path: str): |
| """Train a BPE tokenizer on Hebrew text data.""" |
| from tokenizers import Tokenizer, models, trainers, pre_tokenizers, decoders, processors |
| from datasets import load_dataset |
|
|
| print("Loading Hebrew datasets for tokenizer training...") |
| texts = [] |
|
|
| |
| try: |
| hedc4 = load_dataset("HeNLP/HeDC4", split="train", streaming=True) |
| for i, row in enumerate(hedc4): |
| if i >= 100000: |
| break |
| text = row.get("text", "") |
| if text and len(text.strip()) > 10: |
| texts.append(text) |
| print(f" HeDC4: {len(texts)} docs") |
| except Exception as e: |
| print(f" HeDC4 error: {e}") |
|
|
| |
| try: |
| wiki = load_dataset("OzLabs/hebrew-wiki-articles", split="train", streaming=True) |
| wiki_count = 0 |
| for row in wiki: |
| if wiki_count >= 50000: |
| break |
| text = row.get("text", "") |
| if text and len(text.strip()) > 10: |
| texts.append(text) |
| wiki_count += 1 |
| print(f" Wiki: {wiki_count} docs") |
| except Exception as e: |
| print(f" Wiki error: {e}") |
|
|
| |
| try: |
| benyehuda = load_dataset("OzLabs/hebrew-project-benyehuda", split="train", streaming=True) |
| by_count = 0 |
| for row in benyehuda: |
| text = row.get("text", "") |
| if text and len(text.strip()) > 10: |
| texts.append(text) |
| by_count += 1 |
| print(f" Ben Yehuda: {by_count} docs") |
| except Exception as e: |
| print(f" Ben Yehuda error: {e}") |
|
|
| |
| texts = [t for t in texts if t and isinstance(t, str) and len(t.strip()) > 0] |
| print(f"Total texts for tokenizer: {len(texts)}") |
|
|
| |
| tokenizer = Tokenizer(models.BPE()) |
| tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False) |
| tokenizer.decoder = decoders.ByteLevel() |
| tokenizer.post_processor = processors.ByteLevel(trim_offsets=False) |
|
|
| bpe_trainer = trainers.BpeTrainer( |
| vocab_size=vocab_size, |
| special_tokens=["<|pad|>", "<|bos|>", "<|eos|>", "<|unk|>"], |
| min_frequency=2, |
| show_progress=True, |
| ) |
|
|
| tokenizer.train_from_iterator(iter(texts), bpe_trainer, length=len(texts)) |
|
|
| |
| os.makedirs(save_path, exist_ok=True) |
| tokenizer.save(os.path.join(save_path, "tokenizer.json")) |
|
|
| |
| config = { |
| "vocab_size": tokenizer.get_vocab_size(), |
| "pad_id": tokenizer.token_to_id("<|pad|>"), |
| "bos_id": tokenizer.token_to_id("<|bos|>"), |
| "eos_id": tokenizer.token_to_id("<|eos|>"), |
| } |
| with open(os.path.join(save_path, "config.json"), "w") as f: |
| json.dump(config, f) |
|
|
| print(f"Tokenizer saved to {save_path} (vocab size: {config['vocab_size']})") |
|
|
| |
| test = "Χ©ΧΧΧ Χ’ΧΧΧ, ΧΧΧΧ ΧΧΧΧ§Χͺ ΧΧΧΧ§Χ ΧΧΧΧ¨ ΧΧ’ΧΧ¨Χ" |
| encoded = tokenizer.encode(test) |
| decoded = tokenizer.decode(encoded.ids) |
| print(f"Test encode/decode: '{test}' -> {len(encoded.ids)} tokens -> '{decoded}'") |
|
|
| return tokenizer, config |
|
|
|
|
| def load_tokenizer(path: str): |
| from tokenizers import Tokenizer |
| tokenizer = Tokenizer.from_file(os.path.join(path, "tokenizer.json")) |
| with open(os.path.join(path, "config.json")) as f: |
| config = json.load(f) |
| return tokenizer, config |
|
|
|
|
| |
| class HebrewPretrainDataset(IterableDataset): |
| """ |
| Streaming dataset that concatenates Hebrew text into chunks of max_seq_len. |
| Packs documents together with EOS separators for maximum throughput. |
| """ |
| def __init__(self, tokenizer, tok_config, max_seq_len: int = 2048, seed: int = 42): |
| self.tokenizer = tokenizer |
| self.max_seq_len = max_seq_len |
| self.eos_id = tok_config["eos_id"] |
| self.pad_id = tok_config["pad_id"] |
| self.seed = seed |
|
|
| def _text_iterator(self): |
| """Yields text from all Hebrew sources, streaming.""" |
| from datasets import load_dataset |
|
|
| |
| try: |
| ds = load_dataset("HeNLP/HeDC4", split="train", streaming=True) |
| for row in ds: |
| text = row.get("text", "") |
| if text and len(text) > 50: |
| yield text |
| except Exception as e: |
| print(f"HeDC4 stream error: {e}") |
|
|
| |
| try: |
| ds = load_dataset("OzLabs/hebrew-wiki-articles", split="train", streaming=True) |
| for row in ds: |
| text = row.get("text", "") |
| if text and len(text) > 50: |
| yield text |
| except Exception as e: |
| print(f"Wiki stream error: {e}") |
|
|
| |
| try: |
| ds = load_dataset("OzLabs/hebrew-project-benyehuda", split="train", streaming=True) |
| for row in ds: |
| text = row.get("text", "") |
| if text and len(text) > 50: |
| yield text |
| except Exception as e: |
| print(f"BenYehuda stream error: {e}") |
|
|
| |
| try: |
| ds = load_dataset("OzLabs/hebrew-military-documents", split="train", streaming=True) |
| for row in ds: |
| text = row.get("text", "") |
| if text and len(text) > 50: |
| yield text |
| except Exception as e: |
| print(f"Military stream error: {e}") |
|
|
| |
| try: |
| ds = load_dataset("OzLabs/hebrew-wiktionary-articles", split="train", streaming=True) |
| for row in ds: |
| text = row.get("text", "") |
| if text and len(text) > 20: |
| yield text |
| except Exception as e: |
| print(f"Wiktionary stream error: {e}") |
|
|
| def __iter__(self): |
| """Pack documents into fixed-length sequences.""" |
| buffer = [] |
|
|
| for text in self._text_iterator(): |
| |
| encoded = self.tokenizer.encode(text) |
| tokens = encoded.ids + [self.eos_id] |
| buffer.extend(tokens) |
|
|
| |
| while len(buffer) >= self.max_seq_len + 1: |
| chunk = buffer[:self.max_seq_len + 1] |
| buffer = buffer[self.max_seq_len:] |
| input_ids = torch.tensor(chunk[:-1], dtype=torch.long) |
| labels = torch.tensor(chunk[1:], dtype=torch.long) |
| yield {"input_ids": input_ids, "labels": labels} |
|
|
|
|
| |
| def get_lr(step: int, warmup_steps: int, max_steps: int, lr: float, min_lr: float) -> float: |
| """Cosine schedule with linear warmup.""" |
| if step < warmup_steps: |
| return lr * (step + 1) / warmup_steps |
| if step >= max_steps: |
| return min_lr |
| progress = (step - warmup_steps) / (max_steps - warmup_steps) |
| return min_lr + 0.5 * (lr - min_lr) * (1 + math.cos(math.pi * progress)) |
|
|
|
|
| |
| def train(args): |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| print(f"Device: {device}") |
| if device.type == "cuda": |
| print(f"GPU: {torch.cuda.get_device_name()}") |
| print(f"VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB") |
|
|
| |
| tokenizer_path = args.tokenizer_path |
| if not os.path.exists(os.path.join(tokenizer_path, "tokenizer.json")): |
| print("Training Hebrew tokenizer...") |
| tokenizer, tok_config = train_hebrew_tokenizer(args.vocab_size, tokenizer_path) |
| else: |
| print("Loading existing tokenizer...") |
| tokenizer, tok_config = load_tokenizer(tokenizer_path) |
|
|
| actual_vocab_size = tok_config["vocab_size"] |
| print(f"Vocab size: {actual_vocab_size}") |
|
|
| |
| print(f"Creating DINT Transformer ({args.model_size})...") |
| if args.model_size == "1.5b": |
| model = create_hebrew_dint_1_5b(vocab_size=actual_vocab_size) |
| else: |
| model = create_hebrew_dint_400m(vocab_size=actual_vocab_size) |
|
|
| n_params = model.count_parameters() |
| print(f"Model parameters: {n_params:,} ({n_params/1e9:.2f}B)") |
|
|
| model = model.to(device) |
| if args.bf16 and device.type == "cuda": |
| model = model.to(torch.bfloat16) |
|
|
| |
| print("Setting up streaming Hebrew dataset...") |
| dataset = HebrewPretrainDataset( |
| tokenizer=tokenizer, |
| tok_config=tok_config, |
| max_seq_len=args.max_seq_len, |
| ) |
| dataloader = DataLoader( |
| dataset, |
| batch_size=args.batch_size, |
| num_workers=0, |
| pin_memory=True if device.type == "cuda" else False, |
| ) |
|
|
| |
| |
| decay_params = [] |
| no_decay_params = [] |
| for name, param in model.named_parameters(): |
| if param.requires_grad: |
| if "norm" in name or "bias" in name or "lambda" in name: |
| no_decay_params.append(param) |
| else: |
| decay_params.append(param) |
|
|
| optimizer = AdamW([ |
| {"params": decay_params, "weight_decay": args.weight_decay}, |
| {"params": no_decay_params, "weight_decay": 0.0}, |
| ], lr=args.lr, betas=(0.9, 0.95), eps=1e-8) |
|
|
| print(f"Optimizer: AdamW (lr={args.lr}, betas=(0.9, 0.95), wd={args.weight_decay})") |
| print(f"Schedule: Cosine with {args.warmup_steps} warmup steps") |
| print(f"Effective batch size: {args.batch_size * args.grad_accum}") |
| print(f"Max steps: {args.max_steps}") |
|
|
| |
| try: |
| import trackio |
| trackio.init( |
| project="hebrew-dint-transformer", |
| name=f"pretrain-{args.model_size}", |
| ) |
| use_trackio = True |
| print("Trackio monitoring enabled") |
| except Exception as e: |
| print(f"Trackio not available: {e}") |
| use_trackio = False |
|
|
| |
| os.makedirs(args.output_dir, exist_ok=True) |
| model.train() |
| step = 0 |
| accum_loss = 0.0 |
| tokens_processed = 0 |
| start_time = time.time() |
| best_loss = float("inf") |
|
|
| print("\n" + "="*60) |
| print("Starting Hebrew DINT Transformer pretraining!") |
| print("="*60 + "\n") |
|
|
| data_iter = iter(dataloader) |
|
|
| while step < args.max_steps: |
| optimizer.zero_grad() |
|
|
| |
| for micro_step in range(args.grad_accum): |
| try: |
| batch = next(data_iter) |
| except StopIteration: |
| |
| data_iter = iter(dataloader) |
| batch = next(data_iter) |
|
|
| input_ids = batch["input_ids"].to(device) |
| labels = batch["labels"].to(device) |
|
|
| if args.bf16 and device.type == "cuda": |
| with torch.amp.autocast("cuda", dtype=torch.bfloat16): |
| output = model(input_ids, labels=labels) |
| loss = output["loss"] / args.grad_accum |
| else: |
| output = model(input_ids, labels=labels) |
| loss = output["loss"] / args.grad_accum |
|
|
| loss.backward() |
| accum_loss += loss.item() |
| tokens_processed += input_ids.numel() |
|
|
| |
| grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) |
|
|
| |
| lr = get_lr(step, args.warmup_steps, args.max_steps, args.lr, args.min_lr) |
| for param_group in optimizer.param_groups: |
| param_group["lr"] = lr |
|
|
| optimizer.step() |
| step += 1 |
|
|
| |
| if step % args.log_every == 0: |
| elapsed = time.time() - start_time |
| tokens_per_sec = tokens_processed / elapsed |
| current_loss = accum_loss / args.log_every |
|
|
| print( |
| f"step={step:6d} | " |
| f"loss={current_loss:.4f} | " |
| f"lr={lr:.2e} | " |
| f"grad_norm={grad_norm:.2f} | " |
| f"tok/s={tokens_per_sec:.0f} | " |
| f"tokens={tokens_processed:,}" |
| ) |
|
|
| if use_trackio: |
| trackio.log({ |
| "train/loss": current_loss, |
| "train/lr": lr, |
| "train/grad_norm": grad_norm.item() if torch.is_tensor(grad_norm) else grad_norm, |
| "train/tokens_per_sec": tokens_per_sec, |
| "train/tokens_total": tokens_processed, |
| "train/step": step, |
| }) |
|
|
| if current_loss < best_loss: |
| best_loss = current_loss |
|
|
| accum_loss = 0.0 |
|
|
| |
| if step % args.save_every == 0: |
| ckpt_path = os.path.join(args.output_dir, f"checkpoint-{step}") |
| os.makedirs(ckpt_path, exist_ok=True) |
| torch.save({ |
| "model_state_dict": model.state_dict(), |
| "optimizer_state_dict": optimizer.state_dict(), |
| "step": step, |
| "best_loss": best_loss, |
| "tokens_processed": tokens_processed, |
| "args": vars(args), |
| }, os.path.join(ckpt_path, "checkpoint.pt")) |
| print(f" β Checkpoint saved to {ckpt_path}") |
|
|
| |
| print("\nTraining complete! Saving final model...") |
| final_path = os.path.join(args.output_dir, "final") |
| os.makedirs(final_path, exist_ok=True) |
| torch.save(model.state_dict(), os.path.join(final_path, "model.pt")) |
|
|
| |
| model_config = { |
| "architecture": "DINTTransformer", |
| "model_size": args.model_size, |
| "vocab_size": actual_vocab_size, |
| "embed_dim": model.embed_dim, |
| "num_layers": len(model.layers), |
| "max_seq_len": model.max_seq_len, |
| "total_params": n_params, |
| "total_tokens_trained": tokens_processed, |
| "best_loss": best_loss, |
| } |
| with open(os.path.join(final_path, "config.json"), "w") as f: |
| json.dump(model_config, f, indent=2) |
|
|
| |
| import shutil |
| for fname in ["tokenizer.json", "config.json"]: |
| src = os.path.join(tokenizer_path, fname) |
| if os.path.exists(src): |
| shutil.copy2(src, os.path.join(final_path, fname.replace("config", "tok_config") if fname == "config.json" else fname)) |
|
|
| |
| print(f"Pushing to HuggingFace Hub: {args.hub_model_id}") |
| try: |
| from huggingface_hub import HfApi, upload_folder |
| api = HfApi() |
| api.create_repo(args.hub_model_id, exist_ok=True, private=False) |
|
|
| |
| upload_folder( |
| folder_path=final_path, |
| repo_id=args.hub_model_id, |
| commit_message=f"Hebrew DINT Transformer ({args.model_size}) β step {step}, loss {best_loss:.4f}", |
| ) |
|
|
| |
| api.upload_file( |
| path_or_fileobj="model.py", |
| path_in_repo="model.py", |
| repo_id=args.hub_model_id, |
| ) |
| api.upload_file( |
| path_or_fileobj="train.py", |
| path_in_repo="train.py", |
| repo_id=args.hub_model_id, |
| ) |
|
|
| print(f"β
Model pushed to https://huggingface.co/{args.hub_model_id}") |
| except Exception as e: |
| print(f"Hub push error: {e}") |
| print("Model saved locally at:", final_path) |
|
|
| print(f"\n{'='*60}") |
| print(f"Hebrew DINT Transformer Pretraining Complete!") |
| print(f" Model size: {args.model_size} ({n_params/1e9:.2f}B params)") |
| print(f" Steps: {step}") |
| print(f" Tokens: {tokens_processed:,}") |
| print(f" Best loss: {best_loss:.4f}") |
| print(f"{'='*60}") |
|
|
|
|
| if __name__ == "__main__": |
| args = get_args() |
| train(args) |
|
|