PhysiQuanty's picture
Duplicate from SpiceeChat/Train-From-Scratch-Via-Flash3-Kernels
a295d80
Raw
History Blame Contribute Delete
58.1 kB
#!/usr/bin/env python3
import argparse
import json
import math
import os
import random
import time
from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple
import numpy as np
import pyarrow.parquet as pq
import torch
import torch.nn as nn
import torch.nn.functional as F
from tokenizers import Tokenizer
from torch.utils.data import DataLoader, IterableDataset, get_worker_info
from tqdm import tqdm
PAD_ID = 0
DEFAULT_BOS_MARKER = "<|BOS|>"
DEFAULT_EOS_MARKER = "<|EOS|>"
DEFAULT_BOS_TOKEN = "[BOS]"
DEFAULT_EOS_TOKEN = "[EOS]"
BOUNDARY_MODE = "marker_aware_generic_special_markers_v9"
_FLASH2_KERNEL = None
_FLASH3_KERNEL = None
def get_flash2_kernel():
global _FLASH2_KERNEL
if _FLASH2_KERNEL is None:
from kernels import get_kernel
_FLASH2_KERNEL = get_kernel(
"kernels-community/flash-attn2",
version=1,
)
return _FLASH2_KERNEL
def get_flash3_kernel():
global _FLASH3_KERNEL
if _FLASH3_KERNEL is None:
from kernels import get_kernel
_FLASH3_KERNEL = get_kernel(
"kernels-community/flash-attn3",
version=1,
)
return _FLASH3_KERNEL
def format_tokens(n: int) -> str:
if n >= 1_000_000_000:
return f"{n / 1_000_000_000:.2f}B"
if n >= 1_000_000:
return f"{n / 1_000_000:.2f}M"
if n >= 1_000:
return f"{n / 1_000:.2f}K"
return str(n)
def resolve_tokenizer_path(path: str) -> str:
p = Path(path)
if p.is_dir():
candidate = p / "tokenizer.json"
if candidate.exists():
return str(candidate)
return str(p)
def stable_row_score(row_index: int, seed: int) -> float:
x = (row_index + 1) & 0xFFFFFFFFFFFFFFFF
x ^= (seed + 0x9E3779B97F4A7C15) & 0xFFFFFFFFFFFFFFFF
x = (x * 0xBF58476D1CE4E5B9) & 0xFFFFFFFFFFFFFFFF
x ^= x >> 30
x = (x * 0x94D049BB133111EB) & 0xFFFFFFFFFFFFFFFF
x ^= x >> 31
return (x & 0xFFFFFFFF) / 0x100000000
def normalize_activity_value(value: Any) -> Optional[str]:
if value is None:
return None
if isinstance(value, str):
text = value.strip()
return text if text else None
if isinstance(value, (list, tuple)):
parts = []
for item in value:
if item is None:
continue
s = str(item).strip()
if s:
parts.append(s)
text = " ; ".join(parts).strip()
return text if text else None
if isinstance(value, dict):
text = json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
).strip()
return text if text else None
text = str(value).strip()
return text if text else None
def canonical_special_token(value: str) -> str:
value = str(value).strip()
if not value:
raise ValueError("Special token vide.")
if value.startswith("[") and value.endswith("]"):
inner = value[1:-1].strip()
if not inner:
raise ValueError(f"Token spécial invalide: {value}")
return "[" + inner.upper() + "]"
return "[" + value.upper() + "]"
def parse_special_marker_spec(spec: str) -> Tuple[str, str]:
spec = str(spec).strip()
if "=" not in spec:
raise ValueError(
f"Format --special-marker invalide: {spec}. Format attendu: '<|BOC|>=[BOC]'"
)
marker, token = spec.split("=", 1)
marker = marker.strip()
token = token.strip()
if not marker:
raise ValueError(f"Marker vide dans: {spec}")
if not token:
raise ValueError(f"Token vide dans: {spec}")
token = canonical_special_token(token)
return marker, token
def build_marker_token_map(custom_specs: List[str]) -> Dict[str, str]:
marker_token_map: Dict[str, str] = {
DEFAULT_BOS_MARKER: DEFAULT_BOS_TOKEN,
DEFAULT_EOS_MARKER: DEFAULT_EOS_TOKEN,
}
for spec in custom_specs:
marker, token = parse_special_marker_spec(spec)
marker_token_map[marker] = token
return marker_token_map
class RNETokenCache:
def __init__(
self,
src: str,
tokenizer_path: str,
cache_dir: str,
activity_column: str = "activites",
row_batch_size: int = 100_000,
val_ratio: float = 0.01,
seed: int = 42,
lowercase: bool = False,
append_special_tokens: bool = True,
rebuild_cache: bool = False,
shuffle_before_tokenize: bool = True,
shuffle_buffer_size: int = 500_000,
special_marker_specs: Optional[List[str]] = None,
):
if not 0.0 < val_ratio < 0.5:
raise ValueError("--val-ratio must be > 0 and < 0.5")
if shuffle_buffer_size <= 0:
raise ValueError("--shuffle-buffer-size must be > 0")
self.src = str(src)
self.tokenizer_path = resolve_tokenizer_path(tokenizer_path)
self.cache_dir = Path(cache_dir)
self.activity_column = activity_column
self.row_batch_size = int(row_batch_size)
self.val_ratio = float(val_ratio)
self.seed = int(seed)
self.lowercase = bool(lowercase)
self.append_special_tokens = bool(append_special_tokens)
self.rebuild_cache = bool(rebuild_cache)
self.shuffle_before_tokenize = bool(shuffle_before_tokenize)
self.shuffle_buffer_size = int(shuffle_buffer_size)
self.special_marker_specs = list(special_marker_specs or [])
self.cache_dir.mkdir(parents=True, exist_ok=True)
self.train_bin = self.cache_dir / "train_tokens.uint32.bin"
self.val_bin = self.cache_dir / "val_tokens.uint32.bin"
self.meta_path = self.cache_dir / "meta.json"
self.tokenizer = Tokenizer.from_file(self.tokenizer_path)
self.vocab_size = self.tokenizer.get_vocab_size()
self.marker_token_map = build_marker_token_map(self.special_marker_specs)
self.marker_id_map = self._build_marker_id_map()
self.bos_id = self._find_bos_id() if self.append_special_tokens else None
self.eos_id = self._find_eos_id() if self.append_special_tokens else None
self.sep_id = self._find_sep_id() if self.append_special_tokens else None
if self.append_special_tokens:
if self.bos_id is None:
raise RuntimeError(
"BOS token introuvable. Le tokenizer doit contenir [BOS], <bos>, <BOS>, <s>, [CLS] ou équivalent."
)
if self.eos_id is None:
raise RuntimeError(
"EOS token introuvable. Le tokenizer doit contenir [EOS], <eos>, <EOS>, </s>, [SEP] ou équivalent."
)
self.shuffle_rng_train = random.Random(self.seed + 123_456_789)
self.shuffle_rng_val = random.Random(self.seed + 987_654_321)
def _find_token_id(self, candidates: List[str]) -> Optional[int]:
for token in candidates:
token_id = self.tokenizer.token_to_id(token)
if token_id is not None:
return int(token_id)
return None
def _find_bos_id(self) -> Optional[int]:
explicit_token = self.marker_token_map.get(DEFAULT_BOS_MARKER, DEFAULT_BOS_TOKEN)
return self._find_token_id(
[
explicit_token,
"[BOS]",
"<bos>",
"<BOS>",
"<s>",
"[CLS]",
DEFAULT_BOS_MARKER,
]
)
def _find_eos_id(self) -> Optional[int]:
explicit_token = self.marker_token_map.get(DEFAULT_EOS_MARKER, DEFAULT_EOS_TOKEN)
return self._find_token_id(
[
explicit_token,
"[EOS]",
"<eos>",
"<EOS>",
"</s>",
"[SEP]",
"<sep>",
"<SEP>",
DEFAULT_EOS_MARKER,
]
)
def _find_sep_id(self) -> Optional[int]:
return self._find_token_id(
[
"[SEP]",
"</s>",
"<eos>",
"<EOS>",
"[EOS]",
"<sep>",
"<SEP>",
DEFAULT_EOS_MARKER,
]
)
def _build_marker_id_map(self) -> Dict[str, int]:
marker_id_map: Dict[str, int] = {}
for marker, token in self.marker_token_map.items():
token_id = self.tokenizer.token_to_id(token)
if token_id is None:
raise RuntimeError(
f"Token spécial introuvable dans le tokenizer: marker {repr(marker)} -> token {repr(token)}. "
f"Ajoute-le au tokenizer avec --add-special-token."
)
marker_id_map[marker] = int(token_id)
return marker_id_map
def _cache_is_valid(self) -> bool:
if self.rebuild_cache:
return False
if not self.train_bin.exists():
return False
if not self.val_bin.exists():
return False
if not self.meta_path.exists():
return False
try:
meta = json.loads(self.meta_path.read_text(encoding="utf-8"))
except Exception:
return False
expected = {
"src": os.path.abspath(self.src),
"tokenizer_path": os.path.abspath(self.tokenizer_path),
"activity_column": self.activity_column,
"val_ratio": self.val_ratio,
"seed": self.seed,
"lowercase": self.lowercase,
"append_special_tokens": self.append_special_tokens,
"bos_id": self.bos_id,
"eos_id": self.eos_id,
"sep_id": self.sep_id,
"vocab_size": self.vocab_size,
"shuffle_before_tokenize": self.shuffle_before_tokenize,
"shuffle_buffer_size": self.shuffle_buffer_size,
"boundary_mode": BOUNDARY_MODE,
"default_bos_marker": DEFAULT_BOS_MARKER,
"default_eos_marker": DEFAULT_EOS_MARKER,
"marker_token_map": self.marker_token_map,
"marker_id_map": self.marker_id_map,
}
for key, value in expected.items():
if meta.get(key) != value:
return False
return True
def _write_meta(
self,
train_tokens: int,
val_tokens: int,
rows_seen: int,
rows_used: int,
rows_with_mapped_markers: int,
rows_with_explicit_boundaries: int,
rows_with_legacy_boundaries: int,
):
payload = {
"src": os.path.abspath(self.src),
"tokenizer_path": os.path.abspath(self.tokenizer_path),
"activity_column": self.activity_column,
"val_ratio": self.val_ratio,
"seed": self.seed,
"lowercase": self.lowercase,
"append_special_tokens": self.append_special_tokens,
"bos_id": self.bos_id,
"eos_id": self.eos_id,
"sep_id": self.sep_id,
"vocab_size": self.vocab_size,
"shuffle_before_tokenize": self.shuffle_before_tokenize,
"shuffle_buffer_size": self.shuffle_buffer_size,
"boundary_mode": BOUNDARY_MODE,
"default_bos_marker": DEFAULT_BOS_MARKER,
"default_eos_marker": DEFAULT_EOS_MARKER,
"marker_token_map": self.marker_token_map,
"marker_id_map": self.marker_id_map,
"train_tokens": int(train_tokens),
"val_tokens": int(val_tokens),
"rows_seen": int(rows_seen),
"rows_used": int(rows_used),
"rows_with_mapped_markers": int(rows_with_mapped_markers),
"rows_with_explicit_boundaries": int(rows_with_explicit_boundaries),
"rows_with_legacy_boundaries": int(rows_with_legacy_boundaries),
"dtype": "uint32",
}
self.meta_path.write_text(
json.dumps(payload, ensure_ascii=False, indent=2),
encoding="utf-8",
)
def _shuffle_buffer_with_progress(
self,
buffer: List[str],
rng: random.Random,
desc: str,
):
n = len(buffer)
if n <= 1:
return
pbar = tqdm(
total=n - 1,
desc=desc,
dynamic_ncols=True,
unit="swap",
)
for i in range(n - 1, 0, -1):
j = rng.randint(0, i)
buffer[i], buffer[j] = buffer[j], buffer[i]
pbar.update(1)
pbar.close()
def _has_explicit_bos_and_eos_markers(self, text: str) -> bool:
return DEFAULT_BOS_MARKER in text and DEFAULT_EOS_MARKER in text
def _has_any_mapped_marker(self, text: str) -> bool:
for marker in self.marker_id_map.keys():
if marker in text:
return True
return False
def _encode_plain_chunk(self, text: str) -> List[int]:
if not text:
return []
if self.lowercase:
text = text.lower()
ids = self.tokenizer.encode(
text,
add_special_tokens=False,
).ids
return [int(x) for x in ids]
def _find_next_marker(self, text: str, start: int) -> Tuple[int, Optional[str], Optional[int]]:
best_pos = -1
best_marker = None
best_id = None
for marker, marker_id in self.marker_id_map.items():
pos = text.find(marker, start)
if pos == -1:
continue
if best_pos == -1 or pos < best_pos:
best_pos = pos
best_marker = marker
best_id = marker_id
return best_pos, best_marker, best_id
def _encode_text_replacing_markers(self, text: str) -> Tuple[List[int], bool]:
ids: List[int] = []
i = 0
n = len(text)
used_marker = False
while i < n:
marker_pos, marker, marker_id = self._find_next_marker(text, i)
if marker_pos == -1 or marker is None or marker_id is None:
chunk = text[i:]
ids.extend(self._encode_plain_chunk(chunk))
break
chunk = text[i:marker_pos]
ids.extend(self._encode_plain_chunk(chunk))
ids.append(int(marker_id))
used_marker = True
i = marker_pos + len(marker)
return ids, used_marker
def _encode_text_with_boundaries(self, text: str) -> Tuple[List[int], bool, bool]:
has_explicit_boundaries = self._has_explicit_bos_and_eos_markers(text)
has_any_marker = self._has_any_mapped_marker(text)
if not self.append_special_tokens:
ids, used_marker = self._encode_text_replacing_markers(text)
return ids, used_marker, has_explicit_boundaries
if has_any_marker:
ids, used_marker = self._encode_text_replacing_markers(text)
if has_explicit_boundaries:
return ids, used_marker, True
ids = [int(self.bos_id)] + ids + [int(self.eos_id)]
return ids, used_marker, False
if self.lowercase:
text = text.lower()
ids = self.tokenizer.encode(
text,
add_special_tokens=False,
).ids
ids = [int(x) for x in ids]
ids = [int(self.bos_id)] + ids + [int(self.eos_id)]
return ids, False, False
def _tokenize_to_file(
self,
texts: List[str],
file_obj,
desc: str,
) -> Tuple[int, int, int, int]:
written_tokens = 0
used_texts = 0
mapped_marker_rows = 0
explicit_boundary_rows = 0
legacy_boundary_rows = 0
pbar = tqdm(
total=len(texts),
desc=desc,
dynamic_ncols=True,
unit="texts",
)
for text in texts:
ids, used_mapped_marker, used_explicit_boundaries = self._encode_text_with_boundaries(text)
if len(ids) >= 2:
arr = np.asarray(ids, dtype=np.uint32)
arr.tofile(file_obj)
written_tokens += int(arr.size)
used_texts += 1
if used_mapped_marker:
mapped_marker_rows += 1
if used_explicit_boundaries:
explicit_boundary_rows += 1
else:
legacy_boundary_rows += 1
pbar.update(1)
if used_texts > 0 and used_texts % 10_000 == 0:
pbar.set_postfix(
used=f"{used_texts:,}",
tokens=format_tokens(written_tokens),
markers=f"{mapped_marker_rows:,}",
explicit=f"{explicit_boundary_rows:,}",
legacy=f"{legacy_boundary_rows:,}",
)
pbar.close()
return written_tokens, mapped_marker_rows, explicit_boundary_rows, legacy_boundary_rows
def _flush_text_buffer(
self,
buffer: List[str],
file_obj,
rng: random.Random,
name: str,
) -> Tuple[int, int, int, int]:
if not buffer:
return 0, 0, 0, 0
print()
print(f"[FLUSH] {name}")
print(f"[FLUSH] texts in buffer: {len(buffer):,}")
if self.shuffle_before_tokenize:
self._shuffle_buffer_with_progress(
buffer=buffer,
rng=rng,
desc=f"Shuffling {name}",
)
written_tokens, mapped_marker_rows, explicit_boundary_rows, legacy_boundary_rows = self._tokenize_to_file(
texts=buffer,
file_obj=file_obj,
desc=f"Tokenizing {name}",
)
print(f"[FLUSH] {name} tokens written: {written_tokens:,}")
print(f"[FLUSH] {name} mapped marker rows: {mapped_marker_rows:,}")
print(f"[FLUSH] {name} explicit boundary rows: {explicit_boundary_rows:,}")
print(f"[FLUSH] {name} legacy boundary rows: {legacy_boundary_rows:,}")
print()
buffer.clear()
return written_tokens, mapped_marker_rows, explicit_boundary_rows, legacy_boundary_rows
def build_if_needed(self):
if self._cache_is_valid():
print("[INFO] Token cache found.")
meta = json.loads(self.meta_path.read_text(encoding="utf-8"))
print(f"[INFO] Train tokens: {meta['train_tokens']:,}")
print(f"[INFO] Val tokens: {meta['val_tokens']:,}")
print(f"[INFO] Rows seen: {meta.get('rows_seen', 0):,}")
print(f"[INFO] Rows used: {meta.get('rows_used', 0):,}")
print(f"[INFO] Mapped marker rows: {meta.get('rows_with_mapped_markers', 0):,}")
print(f"[INFO] Explicit boundary rows: {meta.get('rows_with_explicit_boundaries', 0):,}")
print(f"[INFO] Legacy boundary rows: {meta.get('rows_with_legacy_boundaries', 0):,}")
print(f"[INFO] Vocab size: {meta['vocab_size']:,}")
print(f"[INFO] BOS id: {meta.get('bos_id')}")
print(f"[INFO] EOS id: {meta.get('eos_id')}")
print(f"[INFO] SEP id: {meta.get('sep_id')}")
print(f"[INFO] Boundary mode: {meta.get('boundary_mode')}")
print(f"[INFO] Marker token map: {meta.get('marker_token_map')}")
print(f"[INFO] Marker id map: {meta.get('marker_id_map')}")
print(f"[INFO] Shuffle before tok: {meta.get('shuffle_before_tokenize')}")
print(f"[INFO] Shuffle buffer: {meta.get('shuffle_buffer_size'):,}")
return
print("[INFO] Building token cache from parquet.")
print(f"[INFO] Source: {self.src}")
print(f"[INFO] Column: {self.activity_column}")
print(f"[INFO] Tokenizer: {self.tokenizer_path}")
print(f"[INFO] Cache dir: {self.cache_dir}")
print(f"[INFO] Vocab size: {self.vocab_size:,}")
print(f"[INFO] Append special: {self.append_special_tokens}")
print(f"[INFO] BOS id: {self.bos_id}")
print(f"[INFO] EOS id: {self.eos_id}")
print(f"[INFO] SEP id: {self.sep_id}")
print(f"[INFO] Boundary mode: {BOUNDARY_MODE}")
print(f"[INFO] Marker token map: {self.marker_token_map}")
print(f"[INFO] Marker id map: {self.marker_id_map}")
print(f"[INFO] Explicit boundary rule: if <|BOS|> and <|EOS|> are present, no auto BOS/EOS")
print(f"[INFO] Legacy boundary rule: otherwise BOS + text + EOS")
print(f"[INFO] Shuffle before tok: {self.shuffle_before_tokenize}")
print(f"[INFO] Shuffle buffer size: {self.shuffle_buffer_size:,}")
print()
pf = pq.ParquetFile(self.src)
if self.activity_column not in pf.schema.names:
raise ValueError(
f"Column '{self.activity_column}' not found. Available columns: {pf.schema.names}"
)
total_rows = pf.metadata.num_rows
train_tmp = self.train_bin.with_suffix(".tmp")
val_tmp = self.val_bin.with_suffix(".tmp")
if train_tmp.exists():
train_tmp.unlink()
if val_tmp.exists():
val_tmp.unlink()
train_tokens = 0
val_tokens = 0
rows_seen = 0
rows_used = 0
rows_with_mapped_markers = 0
rows_with_explicit_boundaries = 0
rows_with_legacy_boundaries = 0
train_text_buffer: List[str] = []
val_text_buffer: List[str] = []
with train_tmp.open("wb") as f_train, val_tmp.open("wb") as f_val:
pbar = tqdm(
total=total_rows,
desc="Reading + shuffling + tokenizing rows",
dynamic_ncols=True,
unit="rows",
)
for batch in pf.iter_batches(
batch_size=self.row_batch_size,
columns=[self.activity_column],
):
d = batch.to_pydict()
values = d[self.activity_column]
for value in values:
row_index = rows_seen
rows_seen += 1
text = normalize_activity_value(value)
if text is None:
pbar.update(1)
continue
if not text:
pbar.update(1)
continue
if stable_row_score(row_index, self.seed) < self.val_ratio:
val_text_buffer.append(text)
else:
train_text_buffer.append(text)
rows_used += 1
if len(train_text_buffer) >= self.shuffle_buffer_size:
written, marker_rows, explicit_rows, legacy_rows = self._flush_text_buffer(
buffer=train_text_buffer,
file_obj=f_train,
rng=self.shuffle_rng_train,
name="train buffer",
)
train_tokens += written
rows_with_mapped_markers += marker_rows
rows_with_explicit_boundaries += explicit_rows
rows_with_legacy_boundaries += legacy_rows
if len(val_text_buffer) >= max(1_000, self.shuffle_buffer_size // 10):
written, marker_rows, explicit_rows, legacy_rows = self._flush_text_buffer(
buffer=val_text_buffer,
file_obj=f_val,
rng=self.shuffle_rng_val,
name="val buffer",
)
val_tokens += written
rows_with_mapped_markers += marker_rows
rows_with_explicit_boundaries += explicit_rows
rows_with_legacy_boundaries += legacy_rows
pbar.update(1)
if rows_used % 10_000 == 0:
pbar.set_postfix(
used=f"{rows_used:,}",
train_tok=format_tokens(train_tokens),
val_tok=format_tokens(val_tokens),
tr_buf=f"{len(train_text_buffer):,}",
va_buf=f"{len(val_text_buffer):,}",
markers=f"{rows_with_mapped_markers:,}",
explicit=f"{rows_with_explicit_boundaries:,}",
legacy=f"{rows_with_legacy_boundaries:,}",
)
written, marker_rows, explicit_rows, legacy_rows = self._flush_text_buffer(
buffer=train_text_buffer,
file_obj=f_train,
rng=self.shuffle_rng_train,
name="final train buffer",
)
train_tokens += written
rows_with_mapped_markers += marker_rows
rows_with_explicit_boundaries += explicit_rows
rows_with_legacy_boundaries += legacy_rows
written, marker_rows, explicit_rows, legacy_rows = self._flush_text_buffer(
buffer=val_text_buffer,
file_obj=f_val,
rng=self.shuffle_rng_val,
name="final val buffer",
)
val_tokens += written
rows_with_mapped_markers += marker_rows
rows_with_explicit_boundaries += explicit_rows
rows_with_legacy_boundaries += legacy_rows
pbar.close()
train_tmp.replace(self.train_bin)
val_tmp.replace(self.val_bin)
self._write_meta(
train_tokens=train_tokens,
val_tokens=val_tokens,
rows_seen=rows_seen,
rows_used=rows_used,
rows_with_mapped_markers=rows_with_mapped_markers,
rows_with_explicit_boundaries=rows_with_explicit_boundaries,
rows_with_legacy_boundaries=rows_with_legacy_boundaries,
)
print()
print("[INFO] Token cache built.")
print(f"[INFO] Rows seen: {rows_seen:,}")
print(f"[INFO] Rows used: {rows_used:,}")
print(f"[INFO] Mapped marker rows: {rows_with_mapped_markers:,}")
print(f"[INFO] Explicit boundary rows: {rows_with_explicit_boundaries:,}")
print(f"[INFO] Legacy boundary rows: {rows_with_legacy_boundaries:,}")
print(f"[INFO] Train tokens: {train_tokens:,}")
print(f"[INFO] Val tokens: {val_tokens:,}")
print()
class LocalUint32BlockStream(IterableDataset):
def __init__(
self,
bin_path: str,
block_size: int,
seed: int = 42,
shuffle_blocks: bool = False,
max_tokens: int = 0,
):
super().__init__()
self.bin_path = str(bin_path)
self.block_size = int(block_size)
self.seed = int(seed)
self.shuffle_blocks = bool(shuffle_blocks)
self.max_tokens = int(max_tokens)
self._epoch = 0
file_size = os.path.getsize(self.bin_path)
if file_size % 4 != 0:
raise ValueError(f"Token file size is not divisible by 4: {self.bin_path}")
self.num_tokens_total = file_size // 4
if self.max_tokens > 0:
self.num_tokens = min(self.num_tokens_total, self.max_tokens)
else:
self.num_tokens = self.num_tokens_total
if self.num_tokens <= self.block_size + 1:
raise ValueError(
f"Not enough tokens in {self.bin_path}: "
f"{self.num_tokens} <= block_size+1={self.block_size + 1}"
)
self.num_blocks = self.num_tokens // (self.block_size + 1)
if self.num_blocks <= 0:
raise ValueError("No full blocks available.")
def set_epoch(self, epoch: int):
self._epoch = int(epoch)
def __iter__(self) -> Iterator[Dict[str, torch.Tensor]]:
mm = np.memmap(
self.bin_path,
dtype=np.uint32,
mode="r",
shape=(self.num_tokens_total,),
)
wi = get_worker_info()
if wi is None:
worker_id = 0
num_workers = 1
else:
worker_id = wi.id
num_workers = wi.num_workers
block_ids = list(range(self.num_blocks))
if self.shuffle_blocks:
rng = random.Random(self.seed + 1_000_003 * self._epoch)
rng.shuffle(block_ids)
block_ids = block_ids[worker_id::num_workers]
need = self.block_size + 1
for block_id in block_ids:
start = block_id * need
end = start + need
if end > self.num_tokens:
continue
window = np.asarray(mm[start:end], dtype=np.uint32)
src = torch.from_numpy(window[:-1].astype(np.int64, copy=False))
tgt = torch.from_numpy(window[1:].astype(np.int64, copy=False))
yield {
"src": src,
"tgt": tgt,
"length": torch.tensor(self.block_size, dtype=torch.long),
}
def collate_lm_fixed(batch):
src = torch.stack([item["src"] for item in batch], dim=0)
tgt = torch.stack([item["tgt"] for item in batch], dim=0)
padding_mask = torch.zeros(
src.shape,
dtype=torch.bool,
)
return src, tgt, padding_mask
class GPTConfig:
def __init__(
self,
vocab_size: int,
ctx_len: int = 512,
n_layer: int = 4,
n_head: int = 4,
n_embd: int = 384,
dropout: float = 0.0,
attention_backend: str = "sage",
):
if attention_backend not in ("sage", "torch", "flash2", "flash3"):
raise ValueError("--attention-backend must be 'sage', 'torch', 'flash2' or 'flash3'")
if n_embd % n_head != 0:
raise ValueError("n_embd must be divisible by n_head")
head_dim = n_embd // n_head
if attention_backend == "sage" and head_dim not in (64, 96, 128):
raise ValueError(
f"SageAttention requires head_dim in [64, 96, 128], got {head_dim}. "
"Examples: 384/4=96, 384/6=64, 256/4=64, 128/2=64."
)
if attention_backend == "sage" and dropout != 0.0:
raise ValueError("SageAttention strict mode requires --dropout 0.0")
if attention_backend == "flash3" and dropout != 0.0:
raise ValueError("FlashAttention3 backend requires --dropout 0.0")
if attention_backend in ("flash2", "flash3") and head_dim % 8 != 0:
raise ValueError(
f"FlashAttention requires head_dim multiple of 8, got {head_dim}."
)
self.vocab_size = int(vocab_size)
self.ctx_len = int(ctx_len)
self.n_layer = int(n_layer)
self.n_head = int(n_head)
self.n_embd = int(n_embd)
self.dropout = float(dropout)
self.attention_backend = str(attention_backend)
class CausalSelfAttention(nn.Module):
def __init__(self, cfg: GPTConfig):
super().__init__()
self.n_head = cfg.n_head
self.head_dim = cfg.n_embd // cfg.n_head
self.attention_backend = cfg.attention_backend
self.dropout_p = float(cfg.dropout)
self.qkv = nn.Linear(cfg.n_embd, 3 * cfg.n_embd, bias=False)
self.proj = nn.Linear(cfg.n_embd, cfg.n_embd, bias=False)
self.dropout = nn.Dropout(cfg.dropout)
mask = torch.tril(torch.ones(cfg.ctx_len, cfg.ctx_len))
self.register_buffer(
"mask",
mask.view(1, 1, cfg.ctx_len, cfg.ctx_len),
persistent=False,
)
self.sageattn = None
self.flash_kernel = None
if self.attention_backend == "sage":
try:
from sageattention import sageattn
except Exception as exc:
raise RuntimeError(
"SageAttention demandé, mais impossible d'importer : "
"from sageattention import sageattn"
) from exc
self.sageattn = sageattn
if self.attention_backend == "flash2":
try:
self.flash_kernel = get_flash2_kernel()
except Exception as exc:
raise RuntimeError(
"FlashAttention2 demandé, mais impossible de charger : "
'get_kernel("kernels-community/flash-attn2", version=1)'
) from exc
if self.attention_backend == "flash3":
try:
self.flash_kernel = get_flash3_kernel()
except Exception as exc:
raise RuntimeError(
"FlashAttention3 demandé, mais impossible de charger : "
'get_kernel("kernels-community/flash-attn3", version=1)'
) from exc
def _torch_attention(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
t: int,
) -> torch.Tensor:
scores = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
scores = scores.masked_fill(
self.mask[:, :, :t, :t] == 0,
float("-inf"),
)
att = F.softmax(scores.float(), dim=-1).to(q.dtype)
att = self.dropout(att)
y = att @ v
return y
def _sage_attention(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
) -> torch.Tensor:
if self.sageattn is None:
raise RuntimeError("SageAttention demandé mais sageattn est None")
if not q.is_cuda:
raise RuntimeError("SageAttention exige CUDA")
q = q.contiguous()
k = k.contiguous()
v = v.contiguous()
y = self.sageattn(
q,
k,
v,
tensor_layout="HND",
is_causal=True,
)
return y
def _flash2_attention(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
) -> torch.Tensor:
if self.flash_kernel is None:
raise RuntimeError("FlashAttention2 demandé mais flash_kernel est None")
if not q.is_cuda:
raise RuntimeError("FlashAttention2 exige CUDA")
q = q.transpose(1, 2).contiguous()
k = k.transpose(1, 2).contiguous()
v = v.transpose(1, 2).contiguous()
dropout_p = self.dropout_p if self.training else 0.0
y = self.flash_kernel.flash_attn_func(
q,
k,
v,
dropout_p=dropout_p,
causal=True,
)
y = y.transpose(1, 2).contiguous()
return y
def _flash3_attention(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
) -> torch.Tensor:
if self.flash_kernel is None:
raise RuntimeError("FlashAttention3 demandé mais flash_kernel est None")
if not q.is_cuda:
raise RuntimeError("FlashAttention3 exige CUDA")
q = q.transpose(1, 2).contiguous()
k = k.transpose(1, 2).contiguous()
v = v.transpose(1, 2).contiguous()
y = self.flash_kernel.flash_attn_func(
q,
k,
v,
causal=True,
)
y = y.transpose(1, 2).contiguous()
return y
def forward(self, x: torch.Tensor) -> torch.Tensor:
b, t, c = x.shape
qkv = self.qkv(x)
q, k, v = qkv.chunk(3, dim=-1)
q = q.view(b, t, self.n_head, self.head_dim).transpose(1, 2).contiguous()
k = k.view(b, t, self.n_head, self.head_dim).transpose(1, 2).contiguous()
v = v.view(b, t, self.n_head, self.head_dim).transpose(1, 2).contiguous()
if self.attention_backend == "sage":
y = self._sage_attention(q, k, v)
elif self.attention_backend == "flash2":
y = self._flash2_attention(q, k, v)
elif self.attention_backend == "flash3":
y = self._flash3_attention(q, k, v)
else:
y = self._torch_attention(q, k, v, t)
y = y.transpose(1, 2).contiguous().view(b, t, c)
y = self.proj(y)
return y
class MLP(nn.Module):
def __init__(self, cfg: GPTConfig):
super().__init__()
self.fc = nn.Linear(cfg.n_embd, 4 * cfg.n_embd, bias=False)
self.proj = nn.Linear(4 * cfg.n_embd, cfg.n_embd, bias=False)
self.dropout = nn.Dropout(cfg.dropout)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.fc(x)
x = F.gelu(x)
x = self.proj(x)
x = self.dropout(x)
return x
class Block(nn.Module):
def __init__(self, cfg: GPTConfig):
super().__init__()
self.ln1 = nn.LayerNorm(cfg.n_embd)
self.attn = CausalSelfAttention(cfg)
self.ln2 = nn.LayerNorm(cfg.n_embd)
self.mlp = MLP(cfg)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = x + self.attn(self.ln1(x))
x = x + self.mlp(self.ln2(x))
return x
class TinyGPT(nn.Module):
def __init__(self, cfg: GPTConfig):
super().__init__()
self.cfg = cfg
self.tok_emb = nn.Embedding(cfg.vocab_size, cfg.n_embd)
self.pos_emb = nn.Embedding(cfg.ctx_len, cfg.n_embd)
self.drop = nn.Dropout(cfg.dropout)
self.blocks = nn.ModuleList(
[Block(cfg) for _ in range(cfg.n_layer)]
)
self.ln_f = nn.LayerNorm(cfg.n_embd)
self.head = nn.Linear(cfg.n_embd, cfg.vocab_size, bias=False)
self.head.weight = self.tok_emb.weight
self.apply(self._init_weights)
def _init_weights(self, module):
if isinstance(module, nn.Linear):
nn.init.normal_(
module.weight,
mean=0.0,
std=0.02,
)
if isinstance(module, nn.Embedding):
nn.init.normal_(
module.weight,
mean=0.0,
std=0.02,
)
def forward(
self,
idx: torch.Tensor,
return_hidden: bool = False,
):
b, t = idx.shape
if t > self.cfg.ctx_len:
raise ValueError(f"Input length {t} > ctx_len {self.cfg.ctx_len}")
pos = torch.arange(
0,
t,
dtype=torch.long,
device=idx.device,
).unsqueeze(0)
x = self.tok_emb(idx) + self.pos_emb(pos)
x = self.drop(x)
for block in self.blocks:
x = block(x)
hidden = self.ln_f(x)
logits = self.head(hidden)
if return_hidden:
return logits, hidden
return logits
def embed_mean_pool(self, idx: torch.Tensor) -> torch.Tensor:
_, hidden = self.forward(idx, return_hidden=True)
mask = idx.ne(PAD_ID).unsqueeze(-1).to(hidden.dtype)
summed = (hidden * mask).sum(dim=1)
denom = mask.sum(dim=1).clamp(min=1.0)
emb = summed / denom
emb = F.normalize(emb, p=2, dim=-1)
return emb
def param_count(model: nn.Module) -> int:
return int(sum(p.numel() for p in model.parameters()))
class RNETrainer:
def __init__(
self,
model: TinyGPT,
train_loader: DataLoader,
val_loader: DataLoader,
out_dir: str,
max_steps: int,
lr: float,
weight_decay: float,
save_every: int,
log_every: int,
val_every: int,
val_batches: int,
dtype: str,
grad_clip: float,
device: torch.device,
compile_model: bool = False,
):
self.model = model
self.train_loader = train_loader
self.val_loader = val_loader
self.out_dir = Path(out_dir)
self.max_steps = int(max_steps)
self.lr = float(lr)
self.weight_decay = float(weight_decay)
self.save_every = int(save_every)
self.log_every = int(log_every)
self.val_every = int(val_every)
self.val_batches = int(val_batches)
self.dtype = dtype
self.grad_clip = float(grad_clip)
self.device = device
if dtype == "float16":
self.amp_dtype = torch.float16
elif dtype == "bfloat16":
self.amp_dtype = torch.bfloat16
else:
self.amp_dtype = torch.float32
self.use_amp = self.device.type == "cuda" and dtype in ("float16", "bfloat16")
self.optimizer = torch.optim.AdamW(
self.model.parameters(),
lr=self.lr,
betas=(0.9, 0.95),
weight_decay=self.weight_decay,
)
self.scaler = torch.amp.GradScaler(
"cuda",
enabled=self.use_amp,
)
self.criterion = nn.CrossEntropyLoss()
if compile_model:
self.model = torch.compile(self.model)
self.tokens_seen_total = 0
self.tokens_seen_since = 0
self.steps_since = 0
self.amp_overflow_count = 0
self.rate_t0 = time.perf_counter()
def _set_lr(self, lr: float):
for group in self.optimizer.param_groups:
group["lr"] = lr
def _get_lr(self, step: int) -> float:
return self.lr
def _reset_rate_window(self):
self.rate_t0 = time.perf_counter()
self.tokens_seen_since = 0
self.steps_since = 0
self.amp_overflow_count = 0
def _rate_info(self) -> Tuple[float, float]:
now = time.perf_counter()
dt = max(now - self.rate_t0, 1e-9)
tok_s = self.tokens_seen_since / dt
step_s = self.steps_since / dt
return tok_s, step_s
def _save(self, step: int):
self.out_dir.mkdir(parents=True, exist_ok=True)
raw_model = self.model._orig_mod if hasattr(self.model, "_orig_mod") else self.model
payload = {
"step": int(step),
"model": raw_model.state_dict(),
"optimizer": self.optimizer.state_dict(),
"config": {
"vocab_size": raw_model.cfg.vocab_size,
"ctx_len": raw_model.cfg.ctx_len,
"n_layer": raw_model.cfg.n_layer,
"n_head": raw_model.cfg.n_head,
"n_embd": raw_model.cfg.n_embd,
"dropout": raw_model.cfg.dropout,
"attention_backend": raw_model.cfg.attention_backend,
"PAD_ID": PAD_ID,
"boundary_mode": BOUNDARY_MODE,
"default_bos_marker": DEFAULT_BOS_MARKER,
"default_eos_marker": DEFAULT_EOS_MARKER,
},
"tokens_seen_total": int(self.tokens_seen_total),
}
ckpt = self.out_dir / f"checkpoint_step_{step}.pt"
latest = self.out_dir / "latest.pt"
torch.save(payload, ckpt)
torch.save(payload, latest)
print(f"\n[SAVE] {ckpt}")
def evaluate(self) -> float:
self.model.eval()
total_loss = 0.0
seen = 0
with torch.no_grad():
for batch in self.val_loader:
src, tgt, padding_mask = batch
src = src.to(self.device, non_blocking=True)
tgt = tgt.to(self.device, non_blocking=True)
with torch.autocast(
device_type="cuda",
dtype=self.amp_dtype,
enabled=self.use_amp,
):
logits = self.model(src)
loss = self.criterion(
logits.reshape(-1, logits.size(-1)).float(),
tgt.reshape(-1),
)
total_loss += float(loss.item())
seen += 1
if seen >= self.val_batches:
break
self.model.train()
return total_loss / max(1, seen)
def train(self):
self.model.train()
step = 0
running_loss = 0.0
running_count = 0
last_val_loss = None
train_iter = iter(self.train_loader)
self._reset_rate_window()
pbar = tqdm(
total=self.max_steps,
desc="Training/LM-SAGE9-FLASH-KERNELS",
dynamic_ncols=True,
)
while step < self.max_steps:
try:
src, tgt, padding_mask = next(train_iter)
except StopIteration:
train_iter = iter(self.train_loader)
src, tgt, padding_mask = next(train_iter)
src = src.to(self.device, non_blocking=True)
tgt = tgt.to(self.device, non_blocking=True)
batch_tokens = int(src.numel())
lr = self._get_lr(step + 1)
self._set_lr(lr)
self.optimizer.zero_grad(set_to_none=True)
with torch.autocast(
device_type="cuda",
dtype=self.amp_dtype,
enabled=self.use_amp,
):
logits = self.model(src)
loss = self.criterion(
logits.reshape(-1, logits.size(-1)).float(),
tgt.reshape(-1),
)
if not torch.isfinite(loss):
raise RuntimeError(f"Non-finite loss detected: {loss.item()}")
self.scaler.scale(loss).backward()
self.scaler.unscale_(self.optimizer)
if self.grad_clip > 0:
nn.utils.clip_grad_norm_(
self.model.parameters(),
max_norm=self.grad_clip,
)
scale_before = float(self.scaler.get_scale())
self.scaler.step(self.optimizer)
self.scaler.update()
scale_after = float(self.scaler.get_scale())
if self.use_amp and scale_after < scale_before:
self.amp_overflow_count += 1
self.optimizer.zero_grad(set_to_none=True)
if self.amp_overflow_count <= 3:
print(
f"[amp] overflow detected: scale {scale_before:.1f} -> {scale_after:.1f}; skipping update"
)
continue
step += 1
pbar.update(1)
self.tokens_seen_total += batch_tokens
self.tokens_seen_since += batch_tokens
self.steps_since += 1
running_loss += float(loss.item())
running_count += 1
if step % self.val_every == 0:
last_val_loss = self.evaluate()
if step % self.log_every == 0:
avg_loss = running_loss / max(1, running_count)
ppl = math.exp(min(avg_loss, 20.0))
tok_s, step_s = self._rate_info()
postfix = {
"loss": f"{avg_loss:.4f}",
"ppl": f"{ppl:.2f}",
"lr": f"{lr:.2e}",
"seen": format_tokens(self.tokens_seen_total),
"tok_s": f"{tok_s:,.0f}",
"step_s": f"{step_s:.2f}",
}
if last_val_loss is not None:
postfix["val_loss"] = f"{last_val_loss:.4f}"
postfix["val_ppl"] = f"{math.exp(min(last_val_loss, 20.0)):.2f}"
if self.amp_overflow_count > 0:
postfix["amp_of"] = str(self.amp_overflow_count)
pbar.set_postfix(**postfix)
running_loss = 0.0
running_count = 0
self._reset_rate_window()
if step % self.save_every == 0:
self._save(step)
pbar.close()
self._save(step)
print()
print("[DONE] Training finished.")
print(f"[DONE] Steps: {step:,}")
print(f"[DONE] Tokens seen: {self.tokens_seen_total:,}")
print(f"[DONE] Tokens compact: {format_tokens(self.tokens_seen_total)}")
if last_val_loss is not None:
print(f"[DONE] Last val loss: {last_val_loss:.6f}")
print(f"[DONE] Last val ppl: {math.exp(min(last_val_loss, 20.0)):.6f}")
def parse_args():
parser = argparse.ArgumentParser(
description="LM trainer with shuffled pretokenization cache, generic marker->special-token mapping, BOS/EOS boundaries, SageAttention, torch attention, FlashAttention2 and FlashAttention3 via HF kernels."
)
parser.add_argument("--src", required=True)
parser.add_argument("--tokenizer", required=True)
parser.add_argument("--out-dir", default="LM_SAGE9")
parser.add_argument("--cache-dir", default="lm_token_cache_sage9_marker_special")
parser.add_argument("--activity-column", default="activites")
parser.add_argument("--row-batch-size", type=int, default=100_000)
parser.add_argument("--rebuild-cache", action="store_true")
parser.add_argument("--shuffle-before-tokenize", action="store_true")
parser.add_argument("--no-shuffle-before-tokenize", action="store_true")
parser.add_argument("--shuffle-buffer-size", type=int, default=500_000)
parser.add_argument("--ctx-len", type=int, default=512)
parser.add_argument("--batch-size", type=int, default=4)
parser.add_argument("--num-workers", type=int, default=0)
parser.add_argument("--shuffle-blocks", action="store_true")
parser.add_argument("--max-train-tokens", type=int, default=0)
parser.add_argument("--max-val-tokens", type=int, default=0)
parser.add_argument("--val-ratio", type=float, default=0.01)
parser.add_argument("--val-every", type=int, default=2000)
parser.add_argument("--val-batches", type=int, default=10)
parser.add_argument("--n-layer", type=int, default=4)
parser.add_argument("--n-head", type=int, default=4)
parser.add_argument("--n-embd", type=int, default=384)
parser.add_argument("--dropout", type=float, default=0.0)
parser.add_argument(
"--attention-backend",
default="sage",
choices=["sage", "torch", "flash2", "flash3"],
)
parser.add_argument("--lr", type=float, default=3e-4)
parser.add_argument("--weight-decay", type=float, default=0.1)
parser.add_argument("--max-steps", type=int, default=50_000)
parser.add_argument("--save-every", type=int, default=10_000)
parser.add_argument("--log-every", type=int, default=20)
parser.add_argument("--grad-clip", type=float, default=1.0)
parser.add_argument("--dtype", default="bfloat16", choices=["float32", "float16", "bfloat16"])
parser.add_argument("--device", default="cuda")
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--lowercase", action="store_true")
parser.add_argument(
"--special-marker",
action="append",
default=[],
help='Map a dataset marker to a tokenizer special token. Example: --special-marker "<|BOC|>=[BOC]". Can be repeated.',
)
parser.add_argument(
"--no-special-boundaries",
action="store_true",
help="Disable BOS/EOS insertion and marker replacement during pretokenization.",
)
parser.add_argument(
"--no-append-sep",
action="store_true",
help="Legacy alias: disables BOS/EOS insertion too.",
)
parser.add_argument("--compile", action="store_true")
return parser.parse_args()
def main():
args = parse_args()
random.seed(args.seed)
np.random.seed(args.seed)
torch.manual_seed(args.seed)
if args.device == "cuda" and not torch.cuda.is_available():
print("[WARN] CUDA unavailable, using CPU.")
args.device = "cpu"
if args.attention_backend in ("sage", "flash2", "flash3") and args.device != "cuda":
raise RuntimeError(f"--attention-backend {args.attention_backend} requires --device cuda")
if args.no_shuffle_before_tokenize:
shuffle_before_tokenize = False
else:
shuffle_before_tokenize = True
if args.shuffle_before_tokenize:
shuffle_before_tokenize = True
append_special_tokens = True
if args.no_special_boundaries:
append_special_tokens = False
if args.no_append_sep:
append_special_tokens = False
token_cache = RNETokenCache(
src=args.src,
tokenizer_path=args.tokenizer,
cache_dir=args.cache_dir,
activity_column=args.activity_column,
row_batch_size=args.row_batch_size,
val_ratio=args.val_ratio,
seed=args.seed,
lowercase=args.lowercase,
append_special_tokens=append_special_tokens,
rebuild_cache=args.rebuild_cache,
shuffle_before_tokenize=shuffle_before_tokenize,
shuffle_buffer_size=args.shuffle_buffer_size,
special_marker_specs=args.special_marker,
)
token_cache.build_if_needed()
train_ds = LocalUint32BlockStream(
bin_path=str(token_cache.train_bin),
block_size=args.ctx_len,
seed=args.seed,
shuffle_blocks=args.shuffle_blocks,
max_tokens=args.max_train_tokens,
)
val_ds = LocalUint32BlockStream(
bin_path=str(token_cache.val_bin),
block_size=args.ctx_len,
seed=args.seed + 10_000_000,
shuffle_blocks=False,
max_tokens=args.max_val_tokens,
)
train_loader = DataLoader(
train_ds,
batch_size=args.batch_size,
num_workers=args.num_workers,
collate_fn=collate_lm_fixed,
drop_last=True,
pin_memory=(args.device == "cuda"),
persistent_workers=(args.num_workers > 0),
)
val_loader = DataLoader(
val_ds,
batch_size=args.batch_size,
num_workers=max(0, args.num_workers // 2),
collate_fn=collate_lm_fixed,
drop_last=True,
pin_memory=(args.device == "cuda"),
persistent_workers=(args.num_workers > 1),
)
cfg = GPTConfig(
vocab_size=token_cache.vocab_size,
ctx_len=args.ctx_len,
n_layer=args.n_layer,
n_head=args.n_head,
n_embd=args.n_embd,
dropout=args.dropout,
attention_backend=args.attention_backend,
)
device = torch.device(args.device)
model = TinyGPT(cfg).to(device)
params = param_count(model)
target_tokens = args.max_steps * args.batch_size * args.ctx_len
train_epoch_steps = max(1, train_ds.num_blocks // max(1, args.batch_size))
approx_epochs = args.max_steps / train_epoch_steps
print("[INFO] LM SAGE9 GENERIC SPECIAL MARKERS + FLASH KERNELS")
print(f"[INFO] Source: {args.src}")
print(f"[INFO] Activity column: {args.activity_column}")
print(f"[INFO] Tokenizer: {token_cache.tokenizer_path}")
print(f"[INFO] Cache dir: {args.cache_dir}")
print(f"[INFO] Vocab size: {token_cache.vocab_size:,}")
print(f"[INFO] Append special tokens: {append_special_tokens}")
print(f"[INFO] BOS id: {token_cache.bos_id}")
print(f"[INFO] EOS id: {token_cache.eos_id}")
print(f"[INFO] SEP id: {token_cache.sep_id}")
print(f"[INFO] Boundary mode: {BOUNDARY_MODE}")
print(f"[INFO] Marker token map: {token_cache.marker_token_map}")
print(f"[INFO] Marker id map: {token_cache.marker_id_map}")
print(f"[INFO] Boundary rule: explicit <|BOS|> + <|EOS|> => no auto BOS/EOS")
print(f"[INFO] Legacy rule: otherwise BOS + text + EOS")
print(f"[INFO] Shuffle before tok: {shuffle_before_tokenize}")
print(f"[INFO] Shuffle buffer size: {args.shuffle_buffer_size:,}")
print(f"[INFO] Ctx len: {args.ctx_len}")
print(f"[INFO] Batch size: {args.batch_size}")
print(f"[INFO] Num workers: {args.num_workers}")
print(f"[INFO] Shuffle blocks: {args.shuffle_blocks}")
print(f"[INFO] Tokens / step: {args.batch_size * args.ctx_len:,}")
print(f"[INFO] Train tokens file: {train_ds.num_tokens:,}")
print(f"[INFO] Val tokens file: {val_ds.num_tokens:,}")
print(f"[INFO] Train blocks: {train_ds.num_blocks:,}")
print(f"[INFO] Steps / epoch: {train_epoch_steps:,}")
print(f"[INFO] Approx epochs: {approx_epochs:.2f}")
print(f"[INFO] Target tokens seen: {target_tokens:,}")
print(f"[INFO] Target compact: {format_tokens(target_tokens)}")
print(f"[INFO] Val ratio: {args.val_ratio}")
print(f"[INFO] Val every: {args.val_every}")
print(f"[INFO] Val batches: {args.val_batches}")
print(f"[INFO] Params: {params:,}")
print(f"[INFO] Device: {device}")
print(f"[INFO] Dtype: {args.dtype}")
print(f"[INFO] Attention backend: {args.attention_backend}")
print(f"[INFO] Head dim: {args.n_embd // args.n_head}")
print(f"[INFO] LR fixed: {args.lr}")
print(f"[INFO] Output dir: {args.out_dir}")
print()
trainer = RNETrainer(
model=model,
train_loader=train_loader,
val_loader=val_loader,
out_dir=args.out_dir,
max_steps=args.max_steps,
lr=args.lr,
weight_decay=args.weight_decay,
save_every=args.save_every,
log_every=args.log_every,
val_every=args.val_every,
val_batches=args.val_batches,
dtype=args.dtype,
grad_clip=args.grad_clip,
device=device,
compile_model=args.compile,
)
trainer.train()
if __name__ == "__main__":
main()