| |
| """ |
| bytefalcon_fast60m.py |
| |
| One CLI for: |
| * building the byte-fallback + universal-special tokenizer, |
| * initializing a deeper ~60M-parameter hybrid local-attention model from scratch, |
| * atomically appending/deduplicating/shuffling new rewrite batches, |
| * packing rewrite.jsonl into 4096-token byte streams, |
| * training/resuming on ROCm, |
| * running quick validation and generation. |
| |
| Expected JSONL schema: |
| {"instruction": "...", "text": "...", "output": "..."} |
| """ |
|
|
| from __future__ import annotations |
| from collections import Counter |
| import argparse |
| import contextlib |
| import gc |
| import hashlib |
| import inspect |
| import json |
| import math |
| import os |
| import random |
| import shutil |
| import sqlite3 |
| import sys |
| import tempfile |
| import time |
| from dataclasses import dataclass |
| from pathlib import Path |
| from typing import Any, Iterable, Iterator, Mapping, Sequence |
|
|
|
|
| SCRIPT_VERSION = "3.1.1-fast60m-rocm-compile-safe" |
| PROJECT_DIR = Path(__file__).resolve().parent |
| DEFAULT_INVENTORY = PROJECT_DIR / "special_tokens.json" |
|
|
| CONTROL_TOKENS = [ |
| "<pad>", |
| "<bos>", |
| "<eos>", |
| "<unk>", |
| "<instruction>", |
| "<text>", |
| "<output>", |
| "<record>", |
| "<byte_start>", |
| "<byte_end>", |
| ] |
|
|
| DEFAULT_ARCHITECTURE = { |
| "target_parameters": 60_000_000, |
| "hidden_size": 512, |
| "embedding_size": 256, |
| "ffn_latent_size": 256, |
| "num_hidden_layers": 24, |
| "num_attention_heads": 8, |
| "num_key_value_heads": 2, |
| "attention_every": 4, |
| "window_size": 512, |
| "conv_kernel_size": 4, |
| "memory_size": 128, |
| "memory_heads": 4, |
| "attention_residual_group_size": 4, |
| "mtp_loss_weight": 0.20, |
| "max_position_embeddings": 4096, |
| } |
|
|
| |
| os.environ.setdefault("USE_HUB_KERNELS", "NO") |
| os.environ.setdefault("PYTORCH_ALLOC_CONF", "expandable_segments:True") |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") |
| os.environ.setdefault("USE_ROCM_CK_GEMM", "1") |
| os.environ.pop("PYTORCH_HIP_ALLOC_CONF", None) |
|
|
|
|
| |
| |
| |
|
|
| def atomic_json(path: Path, value: Any) -> None: |
| path.parent.mkdir(parents=True, exist_ok=True) |
| file_descriptor, temporary_name = tempfile.mkstemp( |
| prefix=path.name + ".", |
| suffix=".tmp", |
| dir=path.parent, |
| ) |
| try: |
| with os.fdopen( |
| file_descriptor, |
| "w", |
| encoding="utf-8", |
| ) as handle: |
| json.dump( |
| value, |
| handle, |
| ensure_ascii=False, |
| indent=2, |
| sort_keys=True, |
| ) |
| handle.write("\n") |
| os.replace(temporary_name, path) |
| finally: |
| with contextlib.suppress(FileNotFoundError): |
| os.unlink(temporary_name) |
|
|
|
|
| def sha256_file(path: Path) -> str: |
| digest = hashlib.sha256() |
| with path.open("rb") as handle: |
| for block in iter( |
| lambda: handle.read(8 * 1024 * 1024), |
| b"", |
| ): |
| digest.update(block) |
| return digest.hexdigest() |
|
|
|
|
| def sha256_text(text: str) -> str: |
| return hashlib.sha256(text.encode("utf-8")).hexdigest() |
|
|
|
|
| def now_iso() -> str: |
| import datetime as dt |
|
|
| return dt.datetime.now(dt.timezone.utc).isoformat() |
|
|
|
|
| def configure_torch_runtime(torch: Any) -> None: |
| with contextlib.suppress(Exception): |
| torch.set_float32_matmul_precision("high") |
| with contextlib.suppress(Exception): |
| torch.backends.cuda.matmul.allow_tf32 = True |
| with contextlib.suppress(Exception): |
| torch.backends.cudnn.benchmark = True |
| |
| with contextlib.suppress(Exception): |
| torch.backends.cuda.enable_flash_sdp(True) |
| with contextlib.suppress(Exception): |
| torch.backends.cuda.enable_mem_efficient_sdp(True) |
| with contextlib.suppress(Exception): |
| torch.backends.cuda.enable_math_sdp(True) |
| with contextlib.suppress(Exception): |
| torch._dynamo.config.cache_size_limit = 64 |
|
|
|
|
| def clear_memory(torch: Any | None = None) -> None: |
| gc.collect() |
| if torch is not None and torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| with contextlib.suppress(Exception): |
| torch.cuda.ipc_collect() |
|
|
|
|
| |
| |
| |
|
|
| @dataclass(frozen=True) |
| class RewriteRecord: |
| instruction: str |
| text: str |
| output: str |
|
|
| @property |
| def digest(self) -> str: |
| value = ( |
| self.instruction |
| + "\x1f" |
| + self.text |
| + "\x1f" |
| + self.output |
| ) |
| return sha256_text(value) |
|
|
| def to_dict(self) -> dict[str, str]: |
| return { |
| "instruction": self.instruction, |
| "text": self.text, |
| "output": self.output, |
| } |
|
|
|
|
| def normalize_record(value: Mapping[str, Any]) -> RewriteRecord: |
| missing = [ |
| key |
| for key in ("instruction", "text", "output") |
| if key not in value |
| ] |
| if missing: |
| raise ValueError( |
| "Missing rewrite keys: " + ", ".join(missing) |
| ) |
|
|
| return RewriteRecord( |
| instruction=str(value["instruction"] or ""), |
| text=str(value["text"] or ""), |
| output=str(value["output"] or ""), |
| ) |
|
|
|
|
| def iter_jsonl(path: Path) -> Iterator[RewriteRecord]: |
| with path.open("r", encoding="utf-8-sig") as handle: |
| for line_number, raw_line in enumerate(handle, start=1): |
| line = raw_line.strip() |
| if not line: |
| continue |
| try: |
| value = json.loads(line) |
| except json.JSONDecodeError as error: |
| raise ValueError( |
| f"{path}:{line_number}: invalid JSON: {error}" |
| ) from error |
| if not isinstance(value, dict): |
| raise ValueError( |
| f"{path}:{line_number}: expected a JSON object." |
| ) |
| try: |
| yield normalize_record(value) |
| except ValueError as error: |
| raise ValueError( |
| f"{path}:{line_number}: {error}" |
| ) from error |
|
|
|
|
| def iter_json_file(path: Path) -> Iterator[RewriteRecord]: |
| value = json.loads(path.read_text(encoding="utf-8-sig")) |
| if isinstance(value, dict) and isinstance(value.get("data"), list): |
| value = value["data"] |
| if not isinstance(value, list): |
| raise ValueError( |
| f"{path}: expected a JSON list or a {{'data': [...]}} object." |
| ) |
| for index, item in enumerate(value): |
| if not isinstance(item, dict): |
| raise ValueError( |
| f"{path}: item {index} is not an object." |
| ) |
| yield normalize_record(item) |
|
|
|
|
| def iter_records(path: Path) -> Iterator[RewriteRecord]: |
| suffix = path.suffix.lower() |
| if suffix in {".jsonl", ".ndjson"}: |
| yield from iter_jsonl(path) |
| elif suffix == ".json": |
| yield from iter_json_file(path) |
| else: |
| raise ValueError(f"Unsupported batch type: {path}") |
|
|
|
|
| def discover_batches( |
| inbox: Path, |
| *, |
| recursive: bool, |
| ) -> list[Path]: |
| patterns = ("*.jsonl", "*.ndjson", "*.json") |
| found: set[Path] = set() |
| for pattern in patterns: |
| iterator = ( |
| inbox.rglob(pattern) |
| if recursive |
| else inbox.glob(pattern) |
| ) |
| found.update( |
| path.resolve() |
| for path in iterator |
| if path.is_file() |
| ) |
| return sorted(found) |
|
|
|
|
| def deterministic_sort_key(seed: int, digest: str) -> str: |
| return sha256_text(f"{seed}:{digest}") |
|
|
|
|
| def sync_dataset(args: argparse.Namespace) -> dict[str, Any]: |
| base = args.data.resolve() |
| inbox = args.inbox.resolve() |
| archive = args.archive.resolve() if args.archive else None |
|
|
| if not base.is_file(): |
| raise FileNotFoundError(f"Base dataset does not exist: {base}") |
| if not inbox.is_dir(): |
| raise FileNotFoundError(f"Inbox directory does not exist: {inbox}") |
|
|
| batches = [ |
| path |
| for path in discover_batches( |
| inbox, |
| recursive=args.recursive, |
| ) |
| if path != base |
| ] |
|
|
| work_dir = args.work_dir.resolve() |
| work_dir.mkdir(parents=True, exist_ok=True) |
| database_path = work_dir / "dataset-sync.sqlite3" |
| database_path.unlink(missing_ok=True) |
|
|
| connection = sqlite3.connect(database_path) |
| connection.execute("PRAGMA journal_mode=WAL") |
| connection.execute("PRAGMA synchronous=NORMAL") |
| connection.execute("PRAGMA temp_store=FILE") |
| connection.execute( |
| """ |
| CREATE TABLE records ( |
| digest TEXT PRIMARY KEY, |
| sort_key TEXT NOT NULL, |
| instruction TEXT NOT NULL, |
| text_value TEXT NOT NULL, |
| output_value TEXT NOT NULL, |
| source TEXT NOT NULL |
| ) |
| """ |
| ) |
|
|
| stats = { |
| "base_rows_seen": 0, |
| "new_rows_seen": 0, |
| "unique_rows": 0, |
| "duplicates": 0, |
| "invalid_files": [], |
| "batch_files": [str(path) for path in batches], |
| } |
|
|
| def insert_record(record: RewriteRecord, source: str) -> None: |
| cursor = connection.execute( |
| """ |
| INSERT OR IGNORE INTO records |
| (digest, sort_key, instruction, text_value, output_value, source) |
| VALUES (?, ?, ?, ?, ?, ?) |
| """, |
| ( |
| record.digest, |
| deterministic_sort_key(args.seed, record.digest), |
| record.instruction, |
| record.text, |
| record.output, |
| source, |
| ), |
| ) |
| if cursor.rowcount == 0: |
| stats["duplicates"] += 1 |
|
|
| with connection: |
| for record in iter_jsonl(base): |
| stats["base_rows_seen"] += 1 |
| insert_record(record, str(base)) |
|
|
| for batch in batches: |
| try: |
| for record in iter_records(batch): |
| stats["new_rows_seen"] += 1 |
| insert_record(record, str(batch)) |
| except Exception as error: |
| stats["invalid_files"].append( |
| { |
| "path": str(batch), |
| "error": f"{type(error).__name__}: {error}", |
| } |
| ) |
| if not args.skip_invalid_files: |
| connection.close() |
| database_path.unlink(missing_ok=True) |
| raise |
|
|
| stats["unique_rows"] = int( |
| connection.execute("SELECT COUNT(*) FROM records").fetchone()[0] |
| ) |
|
|
| temporary = base.with_suffix(base.suffix + ".sync.tmp") |
| with temporary.open("w", encoding="utf-8") as handle: |
| cursor = connection.execute( |
| """ |
| SELECT instruction, text_value, output_value |
| FROM records |
| ORDER BY sort_key, digest |
| """ |
| ) |
| for instruction, text_value, output_value in cursor: |
| handle.write( |
| json.dumps( |
| { |
| "instruction": instruction, |
| "text": text_value, |
| "output": output_value, |
| }, |
| ensure_ascii=False, |
| separators=(",", ":"), |
| ) |
| ) |
| handle.write("\n") |
| handle.flush() |
| os.fsync(handle.fileno()) |
|
|
| connection.close() |
|
|
| backup = None |
| if args.backup: |
| backup = base.with_name( |
| f"{base.name}.before-sync-{int(time.time())}" |
| ) |
| shutil.copy2(base, backup) |
|
|
| os.replace(temporary, base) |
|
|
| archived = [] |
| if archive is not None: |
| archive.mkdir(parents=True, exist_ok=True) |
| for batch in batches: |
| if not batch.exists(): |
| continue |
| destination = archive / batch.name |
| if destination.exists(): |
| destination = archive / ( |
| f"{batch.stem}-{int(time.time())}{batch.suffix}" |
| ) |
| shutil.move(str(batch), str(destination)) |
| archived.append(str(destination)) |
|
|
| database_path.unlink(missing_ok=True) |
| stats.update( |
| { |
| "data": str(base), |
| "sha256": sha256_file(base), |
| "seed": args.seed, |
| "backup": str(backup) if backup else None, |
| "archived": archived, |
| "completed_at": now_iso(), |
| } |
| ) |
|
|
| audit = args.audit or base.with_suffix(".sync.json") |
| atomic_json(audit, stats) |
| print(json.dumps(stats, ensure_ascii=False, indent=2)) |
| return stats |
|
|
|
|
| |
| |
| |
|
|
| def bytes_to_unicode() -> dict[int, str]: |
| """ |
| GPT-2/ByteLevel's reversible byte-to-Unicode alphabet. |
| """ |
| byte_values = ( |
| list(range(ord("!"), ord("~") + 1)) |
| + list(range(ord("¡"), ord("¬") + 1)) |
| + list(range(ord("®"), ord("ÿ") + 1)) |
| ) |
| unicode_values = list(byte_values) |
| extra = 0 |
| for byte_value in range(256): |
| if byte_value not in byte_values: |
| byte_values.append(byte_value) |
| unicode_values.append(256 + extra) |
| extra += 1 |
| return { |
| byte_value: chr(codepoint) |
| for byte_value, codepoint in zip( |
| byte_values, |
| unicode_values, |
| strict=True, |
| ) |
| } |
|
|
|
|
| def build_tokenizer(args: argparse.Namespace) -> dict[str, Any]: |
| try: |
| from tokenizers import AddedToken, Tokenizer, decoders, models |
| from tokenizers import pre_tokenizers |
| from transformers import PreTrainedTokenizerFast |
| except ImportError as error: |
| raise RuntimeError( |
| "Tokenizer construction requires tokenizers and transformers." |
| ) from error |
|
|
| inventory_path = args.inventory.resolve() |
| inventory = json.loads( |
| inventory_path.read_text(encoding="utf-8") |
| ) |
| output_dir = args.output.resolve() |
| output_dir.mkdir(parents=True, exist_ok=True) |
|
|
| byte_alphabet = bytes_to_unicode() |
| vocab: dict[str, int] = {} |
|
|
| for token in CONTROL_TOKENS: |
| vocab[token] = len(vocab) |
|
|
| byte_ids: dict[str, int] = {} |
| for byte_value in range(256): |
| token = byte_alphabet[byte_value] |
| vocab[token] = len(vocab) |
| byte_ids[f"{byte_value:02X}"] = vocab[token] |
|
|
| backend = Tokenizer( |
| models.BPE( |
| vocab=vocab, |
| merges=[], |
| unk_token="<unk>", |
| byte_fallback=False, |
| ) |
| ) |
| backend.pre_tokenizer = pre_tokenizers.ByteLevel( |
| add_prefix_space=False, |
| use_regex=False, |
| ) |
| backend.decoder = decoders.ByteLevel() |
|
|
| added_tokens = [] |
| for entry in inventory["tokens"]: |
| surface = entry["token"] |
| if surface in CONTROL_TOKENS: |
| continue |
| added_tokens.append( |
| AddedToken( |
| surface, |
| single_word=(entry["mode"] == "word"), |
| normalized=False, |
| lstrip=False, |
| rstrip=False, |
| special=True, |
| ) |
| ) |
|
|
| backend.add_special_tokens(added_tokens) |
|
|
| universal_surfaces = [ |
| entry["token"] |
| for entry in inventory["tokens"] |
| if entry["token"] not in CONTROL_TOKENS |
| ] |
| tokenizer = PreTrainedTokenizerFast( |
| tokenizer_object=backend, |
| bos_token="<bos>", |
| eos_token="<eos>", |
| unk_token="<unk>", |
| pad_token="<pad>", |
| additional_special_tokens=[ |
| *CONTROL_TOKENS[4:], |
| *universal_surfaces, |
| ], |
| clean_up_tokenization_spaces=False, |
| model_max_length=args.context_length, |
| ) |
| tokenizer.padding_side = "right" |
| tokenizer.truncation_side = "right" |
| tokenizer.save_pretrained(output_dir) |
|
|
| samples = [ |
| "Hello, byte world.", |
| "0.003 + 15 = 15.003", |
| "encode tokens and matrices", |
| "😀 👍🏽 🇩🇴 👩💻", |
| "line one\nline two\tend", |
| "UTF-8: café, 日本語, العربية", |
| ] |
| audits = [] |
| for sample in samples: |
| ids = tokenizer.encode( |
| sample, |
| add_special_tokens=False, |
| ) |
| decoded = tokenizer.decode( |
| ids, |
| skip_special_tokens=False, |
| clean_up_tokenization_spaces=False, |
| ) |
| if decoded != sample: |
| raise RuntimeError( |
| f"Tokenizer round-trip failed: {sample!r} -> {decoded!r}" |
| ) |
| audits.append( |
| { |
| "text": sample, |
| "tokens": len(ids), |
| "ids": ids[:64], |
| } |
| ) |
|
|
| |
| |
| fallback_sample = "qxjv" |
| fallback_ids = tokenizer.encode( |
| fallback_sample, |
| add_special_tokens=False, |
| ) |
| expected_bytes = len(fallback_sample.encode("utf-8")) |
| if len(fallback_ids) != expected_bytes: |
| raise RuntimeError( |
| "Strict byte fallback audit failed for qxjv: " |
| f"{len(fallback_ids)} != {expected_bytes}" |
| ) |
|
|
| special_ids = set(tokenizer.all_special_ids) |
| universal_atomic = 0 |
| for surface in universal_surfaces: |
| ids = tokenizer.encode( |
| surface, |
| add_special_tokens=False, |
| ) |
| if len(ids) == 1 and ids[0] in special_ids: |
| universal_atomic += 1 |
|
|
| report = { |
| "version": 1, |
| "inventory": str(inventory_path), |
| "inventory_sha256": sha256_file(inventory_path), |
| "vocab_size": len(tokenizer), |
| "byte_rows": 256, |
| "control_tokens": CONTROL_TOKENS, |
| "universal_special_surfaces": len(universal_surfaces), |
| "universal_specials_atomic": universal_atomic, |
| "all_special_ids_count": len(tokenizer.all_special_ids), |
| "context_length": args.context_length, |
| "byte_id_map": byte_ids, |
| "roundtrip_audits": audits, |
| "fallback_audit": { |
| "text": fallback_sample, |
| "utf8_bytes": expected_bytes, |
| "token_count": len(fallback_ids), |
| }, |
| "warning": ( |
| "Do not decode with skip_special_tokens=True: universal lexical " |
| "and emoji atoms are intentionally registered as special." |
| ), |
| "created_at": now_iso(), |
| } |
| atomic_json(output_dir / "byte_tokenizer_report.json", report) |
|
|
| print(json.dumps(report, ensure_ascii=False, indent=2)) |
| return report |
|
|
|
|
| def load_tokenizer(path: Path): |
| from transformers import AutoTokenizer |
|
|
| tokenizer = AutoTokenizer.from_pretrained( |
| path, |
| use_fast=True, |
| ) |
| tokenizer.model_max_length = 4096 |
| return tokenizer |
|
|
|
|
| def control_token_id_map(tokenizer: Any) -> dict[str, int]: |
| result: dict[str, int] = {} |
| for token in CONTROL_TOKENS: |
| token_id = tokenizer.convert_tokens_to_ids(token) |
| if token_id is None: |
| continue |
| token_id = int(token_id) |
| if token_id < 0: |
| continue |
| result[token] = token_id |
| return result |
|
|
|
|
| def blocked_generation_token_ids(tokenizer: Any) -> list[int]: |
| """ |
| Reserved control tokens are structural, not normal text-generation targets. |
| |
| EOS remains allowed. Lexical/emoji atoms are deliberately *not* blocked, |
| even though the tokenizer registers them as special tokens. |
| """ |
| allowed = {"<eos>"} |
| mapping = control_token_id_map(tokenizer) |
| return sorted( |
| { |
| token_id |
| for token, token_id in mapping.items() |
| if token not in allowed |
| } |
| ) |
|
|
|
|
| def audit_packed_dataset(args: argparse.Namespace) -> dict[str, Any]: |
| try: |
| import numpy as np |
| except ImportError as error: |
| raise RuntimeError("Packed auditing requires NumPy.") from error |
|
|
| tokenizer = load_tokenizer(args.tokenizer.resolve()) |
| packed_dir = args.packed.resolve() |
| mapping = control_token_id_map(tokenizer) |
|
|
| report: dict[str, Any] = { |
| "packed": str(packed_dir), |
| "tokenizer": str(args.tokenizer.resolve()), |
| "vocab_size": len(tokenizer), |
| "control_ids": mapping, |
| "splits": {}, |
| } |
|
|
| for split in ("train", "validation"): |
| path = packed_dir / f"{split}.bin" |
| if not path.is_file(): |
| continue |
|
|
| values = np.memmap(path, mode="r", dtype=np.uint16) |
| counts = { |
| token: int(np.count_nonzero(values == token_id)) |
| for token, token_id in mapping.items() |
| } |
| invalid = int(np.count_nonzero(values >= len(tokenizer))) |
| report["splits"][split] = { |
| "path": str(path), |
| "tokens": int(values.size), |
| "control_token_counts": counts, |
| "invalid_token_ids": invalid, |
| } |
|
|
| dangerous = {} |
| for split, details in report["splits"].items(): |
| hits = { |
| token: count |
| for token, count in details["control_token_counts"].items() |
| if token in {"<pad>", "<bos>", "<unk>"} and count > 0 |
| } |
| if hits: |
| dangerous[split] = hits |
|
|
| report["dangerous_reserved_tokens"] = dangerous |
| report["healthy"] = not dangerous and all( |
| details["invalid_token_ids"] == 0 |
| for details in report["splits"].values() |
| ) |
| print(json.dumps(report, indent=2)) |
| return report |
|
|
|
|
| |
| |
| |
|
|
| def format_rewrite(record: RewriteRecord) -> str: |
| instruction = record.instruction.strip() |
| quoted_text = '"' + record.text + '"' |
| quoted_output = '"' + record.output + '"' |
| if instruction: |
| return ( |
| instruction |
| + "\n\n" |
| + quoted_text |
| + "\n\n" |
| + quoted_output |
| ) |
| return quoted_text + "\n\n" + quoted_output |
|
|
|
|
| def stable_validation_record( |
| record: RewriteRecord, |
| ratio: float, |
| ) -> bool: |
| threshold = int(ratio * (2**64)) |
| value = int(record.digest[:16], 16) |
| return value < threshold |
|
|
|
|
| def pack_dataset(args: argparse.Namespace) -> dict[str, Any]: |
| try: |
| import numpy as np |
| except ImportError as error: |
| raise RuntimeError("Packing requires NumPy.") from error |
|
|
| data_path = args.data.resolve() |
| tokenizer_dir = args.tokenizer.resolve() |
| output_dir = args.output.resolve() |
| output_dir.mkdir(parents=True, exist_ok=True) |
|
|
| tokenizer = load_tokenizer(tokenizer_dir) |
| if len(tokenizer) >= 65536: |
| raise RuntimeError( |
| "Tokenizer is too large for uint16 packing." |
| ) |
|
|
| fingerprint = { |
| "data_sha256": sha256_file(data_path), |
| "tokenizer_sha256": sha256_file( |
| tokenizer_dir / "tokenizer.json" |
| ), |
| "context_length": args.context_length, |
| "validation_ratio": args.validation_ratio, |
| "format": 'instruction\\n\\n"text"\\n\\n"output"<eos>', |
| "packing_version": 2, |
| } |
|
|
| manifest_path = output_dir / "packed_manifest.json" |
| if manifest_path.is_file() and not args.force: |
| existing = json.loads( |
| manifest_path.read_text(encoding="utf-8") |
| ) |
| if existing.get("fingerprint") == fingerprint: |
| print("Packed cache is current:", output_dir) |
| print(json.dumps(existing, indent=2)) |
| return existing |
|
|
| temporary_dir = Path( |
| tempfile.mkdtemp( |
| prefix=output_dir.name + ".packing.", |
| dir=output_dir.parent, |
| ) |
| ) |
| train_path = temporary_dir / "train.bin" |
| validation_path = temporary_dir / "validation.bin" |
|
|
| train_handle = train_path.open("wb") |
| validation_handle = validation_path.open("wb") |
|
|
| buffers = { |
| "train": [], |
| "validation": [], |
| } |
| token_counts = Counter() |
| record_counts = Counter() |
| control_token_counts = { |
| "train": Counter(), |
| "validation": Counter(), |
| } |
| control_ids = control_token_id_map(tokenizer) |
| forbidden_control_tokens = {"<pad>", "<bos>", "<unk>"} |
| max_buffer = 1_000_000 |
|
|
| def flush(split: str, force: bool = False) -> None: |
| buffer = buffers[split] |
| if not buffer: |
| return |
| if len(buffer) < max_buffer and not force: |
| return |
| array = np.asarray(buffer, dtype=np.uint16) |
| target = ( |
| train_handle if split == "train" else validation_handle |
| ) |
| array.tofile(target) |
| buffer.clear() |
|
|
| eos_id = int(tokenizer.eos_token_id) |
|
|
| for record in iter_jsonl(data_path): |
| split = ( |
| "validation" |
| if stable_validation_record( |
| record, |
| args.validation_ratio, |
| ) |
| else "train" |
| ) |
| text = format_rewrite(record) |
| ids = tokenizer.encode( |
| text, |
| add_special_tokens=False, |
| ) |
|
|
| id_counts = Counter(ids) |
| forbidden_hits = {} |
| for control_token, control_id in control_ids.items(): |
| occurrences = int(id_counts.get(control_id, 0)) |
| if occurrences: |
| control_token_counts[split][control_token] += occurrences |
| if control_token in forbidden_control_tokens: |
| forbidden_hits[control_token] = occurrences |
|
|
| if forbidden_hits: |
| raise RuntimeError( |
| "Reserved control token text was found in rewrite.jsonl. " |
| f"record_digest={record.digest}, hits={forbidden_hits}. " |
| "Remove or escape literal <pad>, <bos>, and <unk> strings " |
| "before packing; these tokens must never become training text." |
| ) |
|
|
| ids.append(eos_id) |
| buffers[split].extend(ids) |
| token_counts[split] += len(ids) |
| record_counts[split] += 1 |
| flush(split) |
|
|
| for split in ("train", "validation"): |
| flush(split, force=True) |
|
|
| train_handle.flush() |
| validation_handle.flush() |
| os.fsync(train_handle.fileno()) |
| os.fsync(validation_handle.fileno()) |
| train_handle.close() |
| validation_handle.close() |
|
|
| if token_counts["train"] <= args.context_length: |
| raise RuntimeError("Not enough training tokens for one block.") |
| if token_counts["validation"] <= args.context_length: |
| print( |
| "WARNING: validation split contains fewer than one full block." |
| ) |
|
|
| manifest = { |
| "fingerprint": fingerprint, |
| "data": str(data_path), |
| "tokenizer": str(tokenizer_dir), |
| "dtype": "uint16", |
| "train_records": record_counts["train"], |
| "validation_records": record_counts["validation"], |
| "train_tokens": token_counts["train"], |
| "validation_tokens": token_counts["validation"], |
| "train_blocks": max( |
| 0, |
| (token_counts["train"] - 1) // args.context_length, |
| ), |
| "validation_blocks": max( |
| 0, |
| (token_counts["validation"] - 1) |
| // args.context_length, |
| ), |
| "control_token_counts": { |
| split: dict(counts) |
| for split, counts in control_token_counts.items() |
| }, |
| "created_at": now_iso(), |
| } |
| atomic_json(temporary_dir / "packed_manifest.json", manifest) |
|
|
| for name in ("train.bin", "validation.bin", "packed_manifest.json"): |
| os.replace(temporary_dir / name, output_dir / name) |
| temporary_dir.rmdir() |
|
|
| print(json.dumps(manifest, indent=2)) |
| return manifest |
|
|
|
|
| |
| |
| |
|
|
|
|
| def import_training_stack(): |
| try: |
| import numpy as np |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from torch.utils.data import DataLoader, Dataset |
| except ImportError as error: |
| raise RuntimeError( |
| "Training requires NumPy and a ROCm-enabled PyTorch build." |
| ) from error |
|
|
| configure_torch_runtime(torch) |
| return np, torch, nn, F, DataLoader, Dataset |
|
|
|
|
| @dataclass |
| class Fast60MConfig: |
| vocab_size: int |
| padded_vocab_size: int |
| hidden_size: int = 512 |
| embedding_size: int = 256 |
| ffn_latent_size: int = 256 |
| intermediate_size: int = 1792 |
| num_hidden_layers: int = 24 |
| num_attention_heads: int = 8 |
| num_key_value_heads: int = 2 |
| attention_every: int = 4 |
| window_size: int = 512 |
| conv_kernel_size: int = 4 |
| memory_size: int = 128 |
| memory_heads: int = 4 |
| attention_residual_group_size: int = 4 |
| mtp_loss_weight: float = 0.20 |
| max_position_embeddings: int = 4096 |
| rope_theta: float = 10_000.0 |
| rms_norm_eps: float = 1e-5 |
| initializer_range: float = 0.02 |
| pad_token_id: int = 0 |
| bos_token_id: int = 1 |
| eos_token_id: int = 2 |
| model_type: str = "byte-deep-hybrid" |
| architecture: str = "FastDeepHybridLM" |
|
|
| @property |
| def head_dim(self) -> int: |
| return self.hidden_size // self.num_attention_heads |
|
|
| @property |
| def kv_width(self) -> int: |
| return self.num_key_value_heads * self.head_dim |
|
|
| @property |
| def attention_layer_count(self) -> int: |
| return sum( |
| 1 |
| for index in range(self.num_hidden_layers) |
| if (index + 1) % self.attention_every == 0 |
| ) |
|
|
| @property |
| def convolution_layer_count(self) -> int: |
| return self.num_hidden_layers - self.attention_layer_count |
|
|
| def to_dict(self) -> dict[str, Any]: |
| return dict(self.__dict__) |
|
|
| @classmethod |
| def from_dict(cls, value: Mapping[str, Any]) -> "Fast60MConfig": |
| fields = cls.__dataclass_fields__ |
| return cls(**{key: value[key] for key in fields if key in value}) |
|
|
|
|
| def round_to_multiple(value: float, multiple: int) -> int: |
| return max(multiple, int(round(value / multiple)) * multiple) |
|
|
|
|
| def _fixed_parameter_count(config: Fast60MConfig) -> int: |
| """Count every parameter except the expandable latent FFN matrices.""" |
| d_model = config.hidden_size |
| d_embed = config.embedding_size |
| d_latent = config.ffn_latent_size |
| memory = config.memory_size |
| kv_width = config.kv_width |
|
|
| total = config.padded_vocab_size * d_embed |
| total += 2 * d_model * d_embed |
| if config.mtp_loss_weight > 0: |
| total += d_model * d_embed |
| total += d_model |
|
|
| for index in range(config.num_hidden_layers): |
| has_attention = (index + 1) % config.attention_every == 0 |
| total += 1 |
| total += d_model |
| total += d_model |
| total += 2 * d_model * d_latent |
|
|
| if has_attention: |
| total += 2 * d_model * d_model |
| total += 2 * d_model * kv_width |
| total += d_model |
| total += 2 * d_model * memory + 4 * memory * memory |
| else: |
| total += 3 * d_model * d_model |
| total += d_model * config.conv_kernel_size |
|
|
| return total |
|
|
|
|
| def estimate_parameter_count(config: Fast60MConfig) -> int: |
| expandable = ( |
| config.num_hidden_layers |
| * 3 |
| * config.ffn_latent_size |
| * config.intermediate_size |
| ) |
| return _fixed_parameter_count(config) + expandable |
|
|
|
|
| def build_fast_config( |
| tokenizer: Any, |
| *, |
| target_parameters: int = 60_000_000, |
| hidden_size: int = 512, |
| embedding_size: int = 256, |
| ffn_latent_size: int = 256, |
| num_hidden_layers: int = 24, |
| num_attention_heads: int = 8, |
| num_key_value_heads: int = 2, |
| attention_every: int = 4, |
| window_size: int = 512, |
| conv_kernel_size: int = 4, |
| memory_size: int = 128, |
| memory_heads: int = 4, |
| attention_residual_group_size: int = 4, |
| mtp_loss_weight: float = 0.20, |
| context_length: int = 4096, |
| ) -> Fast60MConfig: |
| if hidden_size % num_attention_heads != 0: |
| raise ValueError("hidden_size must be divisible by num_attention_heads.") |
| if num_attention_heads % num_key_value_heads != 0: |
| raise ValueError( |
| "num_attention_heads must be divisible by num_key_value_heads." |
| ) |
| if context_length % window_size != 0: |
| raise ValueError("context_length must be divisible by window_size.") |
| if memory_size % memory_heads != 0: |
| raise ValueError("memory_size must be divisible by memory_heads.") |
| if attention_every <= 0: |
| raise ValueError("attention_every must be positive.") |
| if ffn_latent_size <= 0 or ffn_latent_size > hidden_size: |
| raise ValueError("ffn_latent_size must be in (0, hidden_size].") |
| if conv_kernel_size <= 0: |
| raise ValueError("conv_kernel_size must be positive.") |
| if attention_residual_group_size <= 0: |
| raise ValueError("attention_residual_group_size must be positive.") |
|
|
| vocab_size = len(tokenizer) |
| padded_vocab_size = int(math.ceil(vocab_size / 64) * 64) |
| provisional = Fast60MConfig( |
| vocab_size=vocab_size, |
| padded_vocab_size=padded_vocab_size, |
| hidden_size=hidden_size, |
| embedding_size=embedding_size, |
| ffn_latent_size=ffn_latent_size, |
| intermediate_size=64, |
| num_hidden_layers=num_hidden_layers, |
| num_attention_heads=num_attention_heads, |
| num_key_value_heads=num_key_value_heads, |
| attention_every=attention_every, |
| window_size=window_size, |
| conv_kernel_size=conv_kernel_size, |
| memory_size=memory_size, |
| memory_heads=memory_heads, |
| attention_residual_group_size=attention_residual_group_size, |
| mtp_loss_weight=mtp_loss_weight, |
| max_position_embeddings=context_length, |
| pad_token_id=int(tokenizer.pad_token_id), |
| bos_token_id=int(tokenizer.bos_token_id), |
| eos_token_id=int(tokenizer.eos_token_id), |
| ) |
|
|
| fixed = _fixed_parameter_count(provisional) |
| denominator = num_hidden_layers * 3 * ffn_latent_size |
| raw_intermediate = (target_parameters - fixed) / max(1, denominator) |
| intermediate_size = round_to_multiple(raw_intermediate, 64) |
| intermediate_size = max(512, min(4096, intermediate_size)) |
| provisional.intermediate_size = intermediate_size |
| return provisional |
|
|
|
|
| def create_model_classes(torch: Any, nn: Any, F: Any): |
| class RMSNorm(nn.Module): |
| def __init__(self, width: int, eps: float): |
| super().__init__() |
| self.weight = nn.Parameter(torch.ones(width)) |
| self.eps = eps |
|
|
| def forward(self, hidden_states): |
| |
| |
| |
| weight = self.weight |
| if weight.dtype != hidden_states.dtype: |
| weight = weight.to(dtype=hidden_states.dtype) |
| return F.rms_norm( |
| hidden_states, |
| (hidden_states.shape[-1],), |
| weight, |
| self.eps, |
| ) |
|
|
| def rotate_half(value): |
| even = value[..., 0::2] |
| odd = value[..., 1::2] |
| return torch.stack((-odd, even), dim=-1).flatten(-2) |
|
|
| class GroupedQueryWindowAttention(nn.Module): |
| """Windowed causal attention with cheap grouped K/V projections. |
| |
| The K/V heads are repeated only inside each local window. This keeps the |
| stable PyTorch SDPA path on ROCm while reducing projection parameters and |
| projection FLOPs relative to full multi-head QKV. |
| """ |
|
|
| def __init__(self, config: Fast60MConfig, shifted: bool): |
| super().__init__() |
| self.hidden_size = config.hidden_size |
| self.num_heads = config.num_attention_heads |
| self.num_kv_heads = config.num_key_value_heads |
| self.kv_repeat = self.num_heads // self.num_kv_heads |
| self.head_dim = config.head_dim |
| self.kv_width = config.kv_width |
| self.window_size = config.window_size |
| self.shift_size = config.window_size // 2 if shifted else 0 |
|
|
| self.q_proj = nn.Linear( |
| config.hidden_size, |
| config.hidden_size, |
| bias=False, |
| ) |
| self.k_proj = nn.Linear( |
| config.hidden_size, |
| self.kv_width, |
| bias=False, |
| ) |
| self.v_proj = nn.Linear( |
| config.hidden_size, |
| self.kv_width, |
| bias=False, |
| ) |
| self.out_proj = nn.Linear( |
| config.hidden_size, |
| config.hidden_size, |
| bias=False, |
| ) |
|
|
| def _attend_segment(self, query, key, value): |
| batch, query_heads, length, head_dim = query.shape |
| if length == 0: |
| return query |
|
|
| padding = (-length) % self.window_size |
| if padding: |
| query = F.pad(query, (0, 0, 0, padding)) |
| key = F.pad(key, (0, 0, 0, padding)) |
| value = F.pad(value, (0, 0, 0, padding)) |
|
|
| padded_length = query.shape[-2] |
| windows = padded_length // self.window_size |
|
|
| def partition(tensor, heads): |
| return ( |
| tensor.reshape( |
| batch, |
| heads, |
| windows, |
| self.window_size, |
| head_dim, |
| ) |
| .permute(0, 2, 1, 3, 4) |
| .reshape( |
| batch * windows, |
| heads, |
| self.window_size, |
| head_dim, |
| ) |
| ) |
|
|
| query_windows = partition(query, query_heads) |
| key_windows = partition(key, self.num_kv_heads) |
| value_windows = partition(value, self.num_kv_heads) |
| if self.kv_repeat > 1: |
| key_windows = key_windows.repeat_interleave( |
| self.kv_repeat, |
| dim=1, |
| ) |
| value_windows = value_windows.repeat_interleave( |
| self.kv_repeat, |
| dim=1, |
| ) |
|
|
| output = F.scaled_dot_product_attention( |
| query_windows, |
| key_windows, |
| value_windows, |
| dropout_p=0.0, |
| is_causal=True, |
| ) |
| output = ( |
| output.reshape( |
| batch, |
| windows, |
| query_heads, |
| self.window_size, |
| head_dim, |
| ) |
| .permute(0, 2, 1, 3, 4) |
| .reshape(batch, query_heads, padded_length, head_dim) |
| ) |
| return output[:, :, :length, :] |
|
|
| def forward(self, hidden_states, cos, sin): |
| batch, length, _ = hidden_states.shape |
| query = self.q_proj(hidden_states).view( |
| batch, |
| length, |
| self.num_heads, |
| self.head_dim, |
| ).transpose(1, 2) |
| key = self.k_proj(hidden_states).view( |
| batch, |
| length, |
| self.num_kv_heads, |
| self.head_dim, |
| ).transpose(1, 2) |
| value = self.v_proj(hidden_states).view( |
| batch, |
| length, |
| self.num_kv_heads, |
| self.head_dim, |
| ).transpose(1, 2) |
|
|
| query = query * cos + rotate_half(query) * sin |
| key = key * cos + rotate_half(key) * sin |
|
|
| if self.shift_size and length > self.shift_size: |
| prefix = self.shift_size |
| first = self._attend_segment( |
| query[:, :, :prefix], |
| key[:, :, :prefix], |
| value[:, :, :prefix], |
| ) |
| rest = self._attend_segment( |
| query[:, :, prefix:], |
| key[:, :, prefix:], |
| value[:, :, prefix:], |
| ) |
| output = torch.cat((first, rest), dim=-2) |
| else: |
| output = self._attend_segment(query, key, value) |
|
|
| output = output.transpose(1, 2).contiguous().view( |
| batch, |
| length, |
| self.hidden_size, |
| ) |
| return self.out_proj(output) |
|
|
| class CausalShortConvMixer(nn.Module): |
| """KDA-inspired short causal path for non-attention layers. |
| |
| This is deliberately not a literal Kimi Delta Attention port: exact KDA |
| needs custom recurrent kernels to be fast. The short depthwise convolution |
| keeps local high-frequency mixing at O(sequence) cost using stock ROCm ops. |
| """ |
|
|
| def __init__(self, config: Fast60MConfig): |
| super().__init__() |
| self.hidden_size = config.hidden_size |
| self.kernel_size = config.conv_kernel_size |
| self.in_proj = nn.Linear( |
| config.hidden_size, |
| 2 * config.hidden_size, |
| bias=False, |
| ) |
| self.depthwise_weight = nn.Parameter( |
| torch.empty(config.hidden_size, 1, self.kernel_size) |
| ) |
| self.out_proj = nn.Linear( |
| config.hidden_size, |
| config.hidden_size, |
| bias=False, |
| ) |
| nn.init.normal_( |
| self.depthwise_weight, |
| mean=0.0, |
| std=config.initializer_range, |
| ) |
|
|
| def forward(self, hidden_states): |
| length = hidden_states.shape[1] |
| value, gate = self.in_proj(hidden_states).chunk(2, dim=-1) |
| value = F.conv1d( |
| value.transpose(1, 2), |
| self.depthwise_weight, |
| padding=self.kernel_size - 1, |
| groups=self.hidden_size, |
| )[..., :length].transpose(1, 2) |
| return self.out_proj(F.silu(value) * torch.sigmoid(gate)) |
|
|
| class SummaryMemoryMixer(nn.Module): |
| """Cheap causal communication across completed local windows.""" |
|
|
| def __init__(self, config: Fast60MConfig): |
| super().__init__() |
| self.hidden_size = config.hidden_size |
| self.memory_size = config.memory_size |
| self.memory_heads = config.memory_heads |
| self.memory_head_dim = ( |
| config.memory_size // config.memory_heads |
| ) |
| self.window_size = config.window_size |
| self.down = nn.Linear( |
| config.hidden_size, |
| config.memory_size, |
| bias=False, |
| ) |
| self.qkv = nn.Linear( |
| config.memory_size, |
| 3 * config.memory_size, |
| bias=False, |
| ) |
| self.out = nn.Linear( |
| config.memory_size, |
| config.memory_size, |
| bias=False, |
| ) |
| self.up = nn.Linear( |
| config.memory_size, |
| config.hidden_size, |
| bias=False, |
| ) |
|
|
| def forward(self, hidden_states): |
| batch, length, width = hidden_states.shape |
| padding = (-length) % self.window_size |
| padded = ( |
| F.pad(hidden_states, (0, 0, 0, padding)) |
| if padding |
| else hidden_states |
| ) |
| windows = padded.view( |
| batch, |
| padded.shape[1] // self.window_size, |
| self.window_size, |
| width, |
| ) |
| summaries = windows[:, :, -1, :] |
| if padding: |
| summaries = torch.cat( |
| (summaries[:, :-1], hidden_states[:, -1:, :]), |
| dim=1, |
| ) |
|
|
| summaries = self.down(summaries) |
| query, key, value = self.qkv(summaries).chunk(3, dim=-1) |
| window_count = summaries.shape[1] |
|
|
| def split_heads(tensor): |
| return tensor.view( |
| batch, |
| window_count, |
| self.memory_heads, |
| self.memory_head_dim, |
| ).transpose(1, 2) |
|
|
| query = split_heads(query) |
| key = split_heads(key) |
| value = split_heads(value) |
| memory = F.scaled_dot_product_attention( |
| query, |
| key, |
| value, |
| dropout_p=0.0, |
| is_causal=True, |
| ) |
| memory = memory.transpose(1, 2).contiguous().view( |
| batch, |
| window_count, |
| self.memory_size, |
| ) |
| memory = self.up(self.out(memory)) |
|
|
| previous_memory = torch.cat( |
| (torch.zeros_like(memory[:, :1]), memory[:, :-1]), |
| dim=1, |
| ) |
| broadcast = ( |
| previous_memory[:, :, None, :] |
| .expand(-1, -1, self.window_size, -1) |
| .reshape(batch, padded.shape[1], width) |
| ) |
| return broadcast[:, :length] |
|
|
| class LatentSwiGLU(nn.Module): |
| """Stable-LatentMoE-inspired dense FFN bottleneck. |
| |
| All tokens use the same dense FFN, but its expensive expansion operates |
| at ffn_latent_size instead of the full residual width. This is much more |
| single-GPU friendly than sparse MoE while preserving the latent-compute idea. |
| """ |
|
|
| def __init__(self, config: Fast60MConfig): |
| super().__init__() |
| self.down_in = nn.Linear( |
| config.hidden_size, |
| config.ffn_latent_size, |
| bias=False, |
| ) |
| self.gate_up = nn.Linear( |
| config.ffn_latent_size, |
| 2 * config.intermediate_size, |
| bias=False, |
| ) |
| self.down = nn.Linear( |
| config.intermediate_size, |
| config.ffn_latent_size, |
| bias=False, |
| ) |
| self.up_out = nn.Linear( |
| config.ffn_latent_size, |
| config.hidden_size, |
| bias=False, |
| ) |
|
|
| def forward(self, hidden_states): |
| latent = self.down_in(hidden_states) |
| gate, up = self.gate_up(latent).chunk(2, dim=-1) |
| latent = self.down(F.silu(gate) * up) |
| return self.up_out(latent) |
|
|
| class FastBlock(nn.Module): |
| def __init__(self, config: Fast60MConfig, index: int): |
| super().__init__() |
| self.index = index |
| self.has_attention = ( |
| (index + 1) % config.attention_every == 0 |
| ) |
| attention_rank = index // config.attention_every |
| self.depth_residual_gate = nn.Parameter(torch.zeros(())) |
| self.mixer_norm = RMSNorm( |
| config.hidden_size, |
| config.rms_norm_eps, |
| ) |
| if self.has_attention: |
| self.mixer = GroupedQueryWindowAttention( |
| config, |
| shifted=(attention_rank % 2 == 1), |
| ) |
| self.memory_norm = RMSNorm( |
| config.hidden_size, |
| config.rms_norm_eps, |
| ) |
| self.memory_mixer = SummaryMemoryMixer(config) |
| else: |
| self.mixer = CausalShortConvMixer(config) |
| self.memory_norm = None |
| self.memory_mixer = None |
| self.ffn_norm = RMSNorm( |
| config.hidden_size, |
| config.rms_norm_eps, |
| ) |
| self.feed_forward = LatentSwiGLU(config) |
|
|
| def forward(self, hidden_states, cos, sin, depth_anchor): |
| |
| |
| mixer_source = hidden_states + torch.tanh( |
| self.depth_residual_gate |
| ) * depth_anchor |
| normalized = self.mixer_norm(mixer_source) |
| if self.has_attention: |
| hidden_states = hidden_states + self.mixer( |
| normalized, |
| cos, |
| sin, |
| ) |
| hidden_states = hidden_states + self.memory_mixer( |
| self.memory_norm(hidden_states) |
| ) |
| else: |
| hidden_states = hidden_states + self.mixer(normalized) |
| hidden_states = hidden_states + self.feed_forward( |
| self.ffn_norm(hidden_states) |
| ) |
| return hidden_states |
|
|
| @dataclass |
| class FastLMOutput: |
| loss: Any | None = None |
| logits: Any | None = None |
| main_loss: Any | None = None |
| mtp_loss: Any | None = None |
|
|
| class FastDeepHybridLM(nn.Module): |
| def __init__(self, config: Fast60MConfig): |
| super().__init__() |
| self.config = config |
| self.token_embedding = nn.Embedding( |
| config.padded_vocab_size, |
| config.embedding_size, |
| ) |
| self.embedding_projection = nn.Linear( |
| config.embedding_size, |
| config.hidden_size, |
| bias=False, |
| ) |
| self.blocks = nn.ModuleList( |
| FastBlock(config, index) |
| for index in range(config.num_hidden_layers) |
| ) |
| self.final_norm = RMSNorm( |
| config.hidden_size, |
| config.rms_norm_eps, |
| ) |
| self.output_projection = nn.Linear( |
| config.hidden_size, |
| config.embedding_size, |
| bias=False, |
| ) |
| self.mtp_projection = ( |
| nn.Linear( |
| config.hidden_size, |
| config.embedding_size, |
| bias=False, |
| ) |
| if config.mtp_loss_weight > 0 |
| else None |
| ) |
|
|
| inverse_frequency = 1.0 / ( |
| config.rope_theta |
| ** ( |
| torch.arange(0, config.head_dim, 2).float() |
| / config.head_dim |
| ) |
| ) |
| positions = torch.arange( |
| config.max_position_embeddings, |
| dtype=torch.float32, |
| ) |
| frequencies = torch.outer(positions, inverse_frequency) |
| embedding = torch.repeat_interleave(frequencies, 2, dim=-1) |
| self.register_buffer( |
| "rope_cos", |
| embedding.cos()[None, None, :, :], |
| persistent=False, |
| ) |
| self.register_buffer( |
| "rope_sin", |
| embedding.sin()[None, None, :, :], |
| persistent=False, |
| ) |
| self.apply(self._initialize_weights) |
| residual_std = config.initializer_range / math.sqrt( |
| 2 * config.num_hidden_layers |
| ) |
| for block in self.blocks: |
| if block.has_attention: |
| nn.init.normal_( |
| block.mixer.out_proj.weight, |
| mean=0.0, |
| std=residual_std, |
| ) |
| nn.init.normal_( |
| block.memory_mixer.up.weight, |
| mean=0.0, |
| std=residual_std, |
| ) |
| else: |
| nn.init.normal_( |
| block.mixer.out_proj.weight, |
| mean=0.0, |
| std=residual_std, |
| ) |
| nn.init.normal_( |
| block.feed_forward.up_out.weight, |
| mean=0.0, |
| std=residual_std, |
| ) |
|
|
| def _initialize_weights(self, module): |
| if isinstance(module, (nn.Linear, nn.Embedding)): |
| nn.init.normal_( |
| module.weight, |
| mean=0.0, |
| std=self.config.initializer_range, |
| ) |
|
|
| def get_input_embeddings(self): |
| return self.token_embedding |
|
|
| def _project_logits(self, hidden_states, projection=None): |
| active_projection = ( |
| self.output_projection if projection is None else projection |
| ) |
| vocabulary_states = active_projection(hidden_states) |
| logits = F.linear( |
| vocabulary_states, |
| self.token_embedding.weight, |
| ) |
| return logits[..., : self.config.vocab_size] |
|
|
| def forward( |
| self, |
| input_ids, |
| labels=None, |
| return_last_logits: bool = False, |
| use_mtp: bool = True, |
| ): |
| if input_ids.ndim != 2: |
| raise ValueError("input_ids must have shape [batch, sequence].") |
| sequence_length = input_ids.shape[1] |
| if sequence_length > self.config.max_position_embeddings: |
| raise ValueError( |
| f"Sequence length {sequence_length} exceeds " |
| f"{self.config.max_position_embeddings}." |
| ) |
|
|
| hidden_states = self.embedding_projection( |
| self.token_embedding(input_ids) |
| ) |
| cos = self.rope_cos[:, :, :sequence_length].to( |
| dtype=hidden_states.dtype |
| ) |
| sin = self.rope_sin[:, :, :sequence_length].to( |
| dtype=hidden_states.dtype |
| ) |
| depth_anchor = hidden_states |
| group_size = self.config.attention_residual_group_size |
| for index, block in enumerate(self.blocks): |
| if index % group_size == 0: |
| depth_anchor = hidden_states |
| hidden_states = block( |
| hidden_states, |
| cos, |
| sin, |
| depth_anchor, |
| ) |
| hidden_states = self.final_norm(hidden_states) |
|
|
| if labels is not None: |
| logits = self._project_logits(hidden_states[:, :-1]) |
| main_loss = F.cross_entropy( |
| logits.reshape(-1, self.config.vocab_size), |
| labels[:, 1:].reshape(-1), |
| ) |
| mtp_loss = None |
| loss = main_loss |
| if ( |
| use_mtp |
| and self.mtp_projection is not None |
| and sequence_length > 2 |
| ): |
| mtp_logits = self._project_logits( |
| hidden_states[:, :-2], |
| self.mtp_projection, |
| ) |
| mtp_loss = F.cross_entropy( |
| mtp_logits.reshape(-1, self.config.vocab_size), |
| labels[:, 2:].reshape(-1), |
| ) |
| loss = loss + self.config.mtp_loss_weight * mtp_loss |
| return FastLMOutput( |
| loss=loss, |
| logits=None, |
| main_loss=main_loss, |
| mtp_loss=mtp_loss, |
| ) |
|
|
| if return_last_logits: |
| hidden_states = hidden_states[:, -1:, :] |
| logits = self._project_logits(hidden_states) |
| return FastLMOutput(loss=None, logits=logits) |
|
|
| return FastDeepHybridLM, FastLMOutput |
|
|
|
|
| def count_parameters(model: Any) -> dict[str, int]: |
| total = sum(parameter.numel() for parameter in model.parameters()) |
| trainable = sum( |
| parameter.numel() |
| for parameter in model.parameters() |
| if parameter.requires_grad |
| ) |
| embedding = model.get_input_embeddings().weight.numel() |
| return { |
| "total": total, |
| "trainable": trainable, |
| "embedding": embedding, |
| "non_embedding": total - embedding, |
| } |
|
|
|
|
| def save_model_bundle( |
| model: Any, |
| tokenizer: Any, |
| output_dir: Path, |
| torch: Any, |
| ) -> None: |
| output_dir.mkdir(parents=True, exist_ok=True) |
| atomic_json(output_dir / "config.json", model.config.to_dict()) |
| torch.save(model.state_dict(), output_dir / "model.pt") |
| tokenizer.save_pretrained(output_dir) |
|
|
|
|
| def load_model_bundle(path: Path, torch: Any, nn: Any, F: Any): |
| config = Fast60MConfig.from_dict( |
| json.loads((path / "config.json").read_text(encoding="utf-8")) |
| ) |
| model_class, _ = create_model_classes(torch, nn, F) |
| model = model_class(config) |
| try: |
| state = torch.load( |
| path / "model.pt", |
| map_location="cpu", |
| weights_only=True, |
| ) |
| except TypeError: |
| state = torch.load(path / "model.pt", map_location="cpu") |
| try: |
| model.load_state_dict(state, strict=True) |
| except RuntimeError as error: |
| raise RuntimeError( |
| "Checkpoint is not architecture-compatible with fast60m-hybrid. " |
| "Start a new run or use a checkpoint created by this script." |
| ) from error |
| return model |
|
|
|
|
| def _config_from_args(tokenizer: Any, args: argparse.Namespace, context: int): |
| return build_fast_config( |
| tokenizer, |
| target_parameters=args.target_parameters, |
| hidden_size=args.hidden_size, |
| embedding_size=args.embedding_size, |
| ffn_latent_size=args.ffn_latent_size, |
| num_hidden_layers=args.layers, |
| num_attention_heads=args.heads, |
| num_key_value_heads=args.kv_heads, |
| attention_every=args.attention_every, |
| window_size=args.window_size, |
| conv_kernel_size=args.conv_kernel_size, |
| memory_size=args.memory_size, |
| memory_heads=args.memory_heads, |
| attention_residual_group_size=args.attention_residual_group_size, |
| mtp_loss_weight=args.mtp_loss_weight, |
| context_length=context, |
| ) |
|
|
|
|
| def initialize_model(args: argparse.Namespace) -> dict[str, Any]: |
| np, torch, nn, F, DataLoader, Dataset = import_training_stack() |
| del np, DataLoader, Dataset |
|
|
| tokenizer = load_tokenizer(args.tokenizer.resolve()) |
| config = _config_from_args(tokenizer, args, args.context_length) |
| model_class, _ = create_model_classes(torch, nn, F) |
| model = model_class(config) |
| parameters = count_parameters(model) |
|
|
| output_dir = args.output.resolve() |
| save_model_bundle(model, tokenizer, output_dir, torch) |
|
|
| full_attention_projection = 4 * config.hidden_size * config.hidden_size |
| gqa_projection = ( |
| 2 * config.hidden_size * config.hidden_size |
| + 2 * config.hidden_size * config.kv_width |
| ) |
| dense_ffn = 3 * config.hidden_size * config.intermediate_size |
| latent_ffn = ( |
| 2 * config.hidden_size * config.ffn_latent_size |
| + 3 * config.ffn_latent_size * config.intermediate_size |
| ) |
| report = { |
| "parameters": parameters, |
| "estimated_parameters": estimate_parameter_count(config), |
| "parameters_millions": parameters["total"] / 1_000_000, |
| "config": config.to_dict(), |
| "speed_design": { |
| "depth": config.num_hidden_layers, |
| "attention_layers": config.attention_layer_count, |
| "linear_conv_layers": config.convolution_layer_count, |
| "attention_fraction": ( |
| config.attention_layer_count / config.num_hidden_layers |
| ), |
| "attention_window": config.window_size, |
| "full_context": config.max_position_embeddings, |
| "attention_pair_fraction_vs_full": ( |
| config.window_size / config.max_position_embeddings |
| ), |
| "gqa_projection_fraction_vs_mha": ( |
| gqa_projection / full_attention_projection |
| ), |
| "latent_ffn_parameter_fraction_vs_full": ( |
| latent_ffn / dense_ffn |
| ), |
| "factorized_embedding_head": True, |
| "causal_summary_memory_on_attention_layers_only": True, |
| "attention_residuals_lite": True, |
| "multi_token_prediction": config.mtp_loss_weight > 0, |
| "external_custom_kernels_required": False, |
| }, |
| "created_at": now_iso(), |
| } |
| atomic_json(output_dir / "initialization_report.json", report) |
|
|
| lower = int(args.target_parameters * 0.90) |
| upper = int(args.target_parameters * 1.10) |
| if not (lower <= parameters["total"] <= upper): |
| raise RuntimeError( |
| f"Model is outside the requested ~{args.target_parameters / 1e6:.0f}M " |
| f"range: {parameters['total']:,}. Adjust width, depth, or target." |
| ) |
|
|
| print(json.dumps(report, indent=2)) |
| return report |
|
|
|
|
| |
| |
| |
|
|
|
|
| def find_latest_checkpoint(output_dir: Path) -> Path | None: |
| checkpoint_root = output_dir / "checkpoints" |
| if not checkpoint_root.is_dir(): |
| return None |
| candidates = sorted( |
| ( |
| path |
| for path in checkpoint_root.glob("step-*") |
| if path.is_dir() |
| ), |
| key=lambda path: int(path.name.split("-")[-1]), |
| ) |
| return candidates[-1] if candidates else None |
|
|
|
|
| def checkpoint_step(path: Path | None) -> int: |
| if path is None: |
| return 0 |
| return int(path.name.split("-")[-1]) |
|
|
|
|
| def prune_checkpoints(root: Path, keep: int) -> None: |
| candidates = sorted( |
| ( |
| path |
| for path in root.glob("step-*") |
| if path.is_dir() |
| ), |
| key=lambda path: int(path.name.split("-")[-1]), |
| ) |
| for path in candidates[:-keep]: |
| shutil.rmtree(path) |
|
|
|
|
| def load_training_state(torch: Any, path: Path) -> dict[str, Any]: |
| try: |
| return torch.load( |
| path, |
| map_location="cpu", |
| weights_only=False, |
| ) |
| except TypeError: |
| return torch.load(path, map_location="cpu") |
|
|
|
|
| def build_adamw(torch: Any, model: Any, args: argparse.Namespace): |
| common = dict( |
| params=model.parameters(), |
| lr=args.learning_rate, |
| betas=(args.beta1, args.beta2), |
| eps=args.adam_epsilon, |
| weight_decay=args.weight_decay, |
| ) |
| if args.fused_optimizer: |
| try: |
| optimizer = torch.optim.AdamW(**common, fused=True) |
| return optimizer, "fused" |
| except (TypeError, RuntimeError) as error: |
| print( |
| "Fused AdamW unavailable; falling back to foreach AdamW:", |
| error, |
| ) |
| try: |
| return torch.optim.AdamW(**common, foreach=True), "foreach" |
| except (TypeError, RuntimeError): |
| return torch.optim.AdamW(**common), "single-tensor" |
|
|
|
|
| def make_scheduler( |
| torch: Any, |
| optimizer: Any, |
| *, |
| warmup_steps: int, |
| total_steps: int, |
| minimum_ratio: float, |
| ): |
| def multiplier(step: int) -> float: |
| if step < warmup_steps: |
| return max(1e-8, float(step + 1) / max(1, warmup_steps)) |
| progress = ( |
| float(step - warmup_steps) |
| / max(1, total_steps - warmup_steps) |
| ) |
| progress = min(1.0, max(0.0, progress)) |
| cosine = 0.5 * (1.0 + math.cos(math.pi * progress)) |
| return minimum_ratio + (1.0 - minimum_ratio) * cosine |
|
|
| return torch.optim.lr_scheduler.LambdaLR(optimizer, multiplier) |
|
|
|
|
| def _atomic_replace_directory( |
| temporary: Path, |
| destination: Path, |
| ) -> None: |
| previous = destination.with_name( |
| destination.name + f".previous-{os.getpid()}" |
| ) |
| if previous.exists(): |
| shutil.rmtree(previous) |
|
|
| if destination.exists(): |
| os.replace(destination, previous) |
|
|
| try: |
| os.replace(temporary, destination) |
| except Exception: |
| if previous.exists() and not destination.exists(): |
| os.replace(previous, destination) |
| raise |
| else: |
| if previous.exists(): |
| shutil.rmtree(previous) |
|
|
|
|
| def save_named_training_checkpoint( |
| *, |
| model: Any, |
| tokenizer: Any, |
| optimizer: Any, |
| scheduler: Any, |
| torch: Any, |
| destination: Path, |
| state: dict[str, Any], |
| metadata: Mapping[str, Any] | None = None, |
| ) -> Path: |
| destination.parent.mkdir(parents=True, exist_ok=True) |
| temporary = destination.with_name( |
| destination.name + f".tmp-{os.getpid()}" |
| ) |
| if temporary.exists(): |
| shutil.rmtree(temporary) |
| temporary.mkdir(parents=True) |
|
|
| save_model_bundle(model, tokenizer, temporary, torch) |
| torch.save( |
| { |
| "optimizer": optimizer.state_dict(), |
| "scheduler": scheduler.state_dict(), |
| "state": state, |
| "torch_rng": torch.get_rng_state(), |
| "cuda_rng": ( |
| torch.cuda.get_rng_state_all() |
| if torch.cuda.is_available() |
| else None |
| ), |
| "python_rng": random.getstate(), |
| }, |
| temporary / "training_state.pt", |
| ) |
| atomic_json(temporary / "training_state.json", state) |
| if metadata is not None: |
| atomic_json( |
| temporary / "checkpoint_metadata.json", |
| dict(metadata), |
| ) |
|
|
| required = ( |
| temporary / "config.json", |
| temporary / "model.pt", |
| temporary / "training_state.pt", |
| ) |
| missing = [str(path) for path in required if not path.is_file()] |
| if missing: |
| shutil.rmtree(temporary, ignore_errors=True) |
| raise RuntimeError( |
| f"Checkpoint write was incomplete; missing={missing}" |
| ) |
|
|
| _atomic_replace_directory(temporary, destination) |
| return destination |
|
|
|
|
| def save_checkpoint( |
| *, |
| model: Any, |
| tokenizer: Any, |
| optimizer: Any, |
| scheduler: Any, |
| torch: Any, |
| output_dir: Path, |
| state: dict[str, Any], |
| keep: int, |
| ) -> Path: |
| checkpoint_root = output_dir / "checkpoints" |
| destination = checkpoint_root / f"step-{state['global_step']:08d}" |
| save_named_training_checkpoint( |
| model=model, |
| tokenizer=tokenizer, |
| optimizer=optimizer, |
| scheduler=scheduler, |
| torch=torch, |
| destination=destination, |
| state=state, |
| metadata={ |
| "kind": "regular", |
| "global_step": state["global_step"], |
| "saved_at": now_iso(), |
| }, |
| ) |
| prune_checkpoints(checkpoint_root, keep) |
| return destination |
|
|
|
|
| def restore_training_checkpoint_in_place( |
| *, |
| checkpoint: Path, |
| model: Any, |
| optimizer: Any, |
| scheduler: Any, |
| state: dict[str, Any], |
| torch: Any, |
| nn: Any, |
| F: Any, |
| ) -> None: |
| restored_model = load_model_bundle( |
| checkpoint, |
| torch, |
| nn, |
| F, |
| ) |
| model.load_state_dict( |
| restored_model.state_dict(), |
| strict=True, |
| ) |
| del restored_model |
|
|
| saved = load_training_state( |
| torch, |
| checkpoint / "training_state.pt", |
| ) |
| optimizer.load_state_dict(saved["optimizer"]) |
| scheduler.load_state_dict(saved["scheduler"]) |
| state.clear() |
| state.update(saved["state"]) |
|
|
| if saved.get("torch_rng") is not None: |
| torch.set_rng_state(saved["torch_rng"]) |
| if ( |
| torch.cuda.is_available() |
| and saved.get("cuda_rng") is not None |
| ): |
| torch.cuda.set_rng_state_all(saved["cuda_rng"]) |
| if saved.get("python_rng") is not None: |
| random.setstate(saved["python_rng"]) |
|
|
|
|
| def backoff_learning_rate( |
| optimizer: Any, |
| scheduler: Any, |
| *, |
| factor: float, |
| minimum: float, |
| ) -> list[float]: |
| updated = [] |
| for group in optimizer.param_groups: |
| new_lr = max( |
| minimum, |
| float(group["lr"]) * factor, |
| ) |
| group["lr"] = new_lr |
| group["initial_lr"] = min( |
| float(group.get("initial_lr", new_lr)), |
| new_lr, |
| ) |
| updated.append(new_lr) |
|
|
| if hasattr(scheduler, "base_lrs"): |
| scheduler.base_lrs = [ |
| max(minimum, float(value) * factor) |
| for value in scheduler.base_lrs |
| ] |
| if hasattr(scheduler, "_last_lr"): |
| scheduler._last_lr = list(updated) |
|
|
| return updated |
|
|
|
|
| def model_parameters_are_finite( |
| torch: Any, |
| model: Any, |
| ) -> bool: |
| with torch.no_grad(): |
| for parameter in model.parameters(): |
| if not bool(torch.isfinite(parameter).all().item()): |
| return False |
| return True |
|
|
|
|
| def evaluate_loss( |
| *, |
| model: Any, |
| loader: Any, |
| torch: Any, |
| device: Any, |
| dtype_name: str, |
| max_batches: int, |
| ) -> float | None: |
| """ |
| Evaluate with mixed precision first. If a batch becomes non-finite, retry |
| that batch in FP32 before declaring the checkpoint unhealthy. |
| """ |
| model.eval() |
| total = 0.0 |
| count = 0 |
| autocast_dtype = ( |
| torch.bfloat16 if dtype_name == "bf16" else torch.float16 |
| ) |
| autocast_enabled = dtype_name in {"bf16", "fp16"} |
|
|
| try: |
| with torch.no_grad(): |
| for batch_index, batch in enumerate(loader): |
| if batch_index >= max_batches: |
| break |
|
|
| input_ids = batch.to( |
| device, |
| non_blocking=True, |
| ) |
| with torch.autocast( |
| device_type="cuda", |
| dtype=autocast_dtype, |
| enabled=autocast_enabled, |
| ): |
| output = model( |
| input_ids=input_ids, |
| labels=input_ids, |
| use_mtp=False, |
| ) |
| loss = output.loss.detach() |
|
|
| if not bool(torch.isfinite(loss).item()): |
| print( |
| "Validation loss was non-finite under autocast; " |
| f"retrying batch {batch_index} in FP32." |
| ) |
| with torch.autocast( |
| device_type="cuda", |
| enabled=False, |
| ): |
| output = model( |
| input_ids=input_ids, |
| labels=input_ids, |
| use_mtp=False, |
| ) |
| loss = output.loss.detach().float() |
|
|
| if not bool(torch.isfinite(loss).item()): |
| return float("nan") |
|
|
| total += float(loss.item()) |
| count += 1 |
| finally: |
| if torch.cuda.is_available(): |
| torch.cuda.synchronize() |
| model.train() |
|
|
| if count == 0: |
| return None |
| return total / count |
|
|
|
|
|
|
| def train_model(args: argparse.Namespace) -> dict[str, Any]: |
| np, torch, nn, F, DataLoader, Dataset = import_training_stack() |
|
|
| if not torch.cuda.is_available(): |
| raise RuntimeError( |
| "ROCm PyTorch did not expose the AMD GPU through torch.cuda." |
| ) |
|
|
| device = torch.device("cuda") |
| torch.manual_seed(args.seed) |
| random.seed(args.seed) |
| torch.cuda.manual_seed_all(args.seed) |
|
|
| tokenizer = load_tokenizer(args.tokenizer.resolve()) |
| packed_dir = args.packed.resolve() |
| manifest = json.loads( |
| (packed_dir / "packed_manifest.json").read_text(encoding="utf-8") |
| ) |
| context_length = int(manifest["fingerprint"]["context_length"]) |
| if context_length != 4096: |
| raise RuntimeError( |
| f"This project expects 4096-token blocks, got {context_length}." |
| ) |
|
|
| class TokenBlocks(Dataset): |
| def __init__(self, path: Path, context: int): |
| self.tokens = np.memmap(path, mode="r", dtype=np.uint16) |
| self.context = context |
| self.blocks = max(0, (len(self.tokens) - 1) // context) |
|
|
| def __len__(self): |
| return self.blocks |
|
|
| def __getitem__(self, index): |
| start = index * self.context |
| values = np.asarray( |
| self.tokens[start : start + self.context], |
| dtype=np.int64, |
| ).copy() |
| return torch.from_numpy(values) |
|
|
| train_dataset = TokenBlocks( |
| packed_dir / "train.bin", |
| context_length, |
| ) |
| validation_dataset = TokenBlocks( |
| packed_dir / "validation.bin", |
| context_length, |
| ) |
| if len(train_dataset) == 0: |
| raise RuntimeError("Packed training dataset has zero blocks.") |
|
|
| output_dir = args.output.resolve() |
| output_dir.mkdir(parents=True, exist_ok=True) |
| checkpoint = ( |
| find_latest_checkpoint(output_dir) |
| if args.resume == "auto" |
| else ( |
| Path(args.resume).resolve() |
| if args.resume != "none" |
| else None |
| ) |
| ) |
|
|
| if checkpoint is not None: |
| print("Resuming checkpoint:", checkpoint) |
| model = load_model_bundle(checkpoint, torch, nn, F) |
| if model.config.vocab_size != len(tokenizer): |
| raise RuntimeError( |
| "Checkpoint tokenizer size does not match --tokenizer." |
| ) |
| else: |
| config = _config_from_args(tokenizer, args, context_length) |
| model_class, _ = create_model_classes(torch, nn, F) |
| model = model_class(config) |
|
|
| model.to(device) |
| model.train() |
| parameters = count_parameters(model) |
| print( |
| f"Architecture: {model.config.architecture}; " |
| f"parameters={parameters['total']:,} " |
| f"({parameters['total'] / 1e6:.3f}M); " |
| f"layers={model.config.num_hidden_layers}; " |
| f"attention_layers={model.config.attention_layer_count}; " |
| f"conv_layers={model.config.convolution_layer_count}; " |
| f"window={model.config.window_size}; " |
| f"ffn_latent={model.config.ffn_latent_size}" |
| ) |
|
|
| optimizer, optimizer_backend = build_adamw(torch, model, args) |
|
|
| updates_per_epoch = math.ceil( |
| len(train_dataset) |
| / max(1, args.batch_size * args.gradient_accumulation) |
| ) |
| run_target_steps = ( |
| args.max_steps |
| if args.max_steps > 0 |
| else max(1, args.epochs * updates_per_epoch) |
| ) |
| schedule_steps = ( |
| args.lr_decay_steps |
| if args.lr_decay_steps > 0 |
| else run_target_steps |
| ) |
| schedule_steps = max(schedule_steps, run_target_steps) |
| warmup_steps = ( |
| args.warmup_steps |
| if args.warmup_steps >= 0 |
| else int(schedule_steps * args.warmup_ratio) |
| ) |
| scheduler = make_scheduler( |
| torch, |
| optimizer, |
| warmup_steps=warmup_steps, |
| total_steps=schedule_steps, |
| minimum_ratio=args.minimum_lr_ratio, |
| ) |
|
|
| current_packed_fingerprint = manifest["fingerprint"] |
| state = { |
| "global_step": 0, |
| "epoch": 0, |
| "batch_in_epoch": 0, |
| "tokens_seen": 0, |
| "best_validation_loss": None, |
| "best_checkpoint": None, |
| "nonfinite_events": 0, |
| "last_finite_step": 0, |
| "packed_fingerprint": current_packed_fingerprint, |
| "lr_decay_steps": schedule_steps, |
| "warmup_steps": warmup_steps, |
| "started_at": now_iso(), |
| } |
|
|
| if checkpoint is not None: |
| saved = load_training_state( |
| torch, |
| checkpoint / "training_state.pt", |
| ) |
| optimizer.load_state_dict(saved["optimizer"]) |
| scheduler.load_state_dict(saved["scheduler"]) |
| saved_state = dict(saved["state"]) |
| previous_fingerprint = saved_state.get("packed_fingerprint") |
| state.update(saved_state) |
|
|
| if previous_fingerprint != current_packed_fingerprint: |
| print( |
| "Packed dataset changed; resetting epoch/batch cursor while " |
| "preserving model, optimizer, scheduler, and global step." |
| ) |
| state["epoch"] = 0 |
| state["batch_in_epoch"] = 0 |
| state["best_validation_loss"] = None |
| state["packed_fingerprint"] = current_packed_fingerprint |
|
|
| state["lr_decay_steps"] = schedule_steps |
| state["warmup_steps"] = warmup_steps |
| torch.set_rng_state(saved["torch_rng"]) |
| if saved.get("cuda_rng") is not None: |
| torch.cuda.set_rng_state_all(saved["cuda_rng"]) |
| random.setstate(saved["python_rng"]) |
|
|
| def loader_for_epoch(epoch: int): |
| generator = torch.Generator() |
| generator.manual_seed(args.seed + epoch) |
| loader_kwargs = dict( |
| dataset=train_dataset, |
| batch_size=args.batch_size, |
| shuffle=True, |
| generator=generator, |
| num_workers=args.num_workers, |
| pin_memory=args.pin_memory, |
| drop_last=True, |
| persistent_workers=( |
| args.num_workers > 0 and args.persistent_workers |
| ), |
| ) |
| if args.num_workers > 0: |
| loader_kwargs["prefetch_factor"] = args.prefetch_factor |
| return DataLoader(**loader_kwargs) |
|
|
| validation_loader = DataLoader( |
| validation_dataset, |
| batch_size=args.batch_size, |
| shuffle=False, |
| num_workers=0, |
| pin_memory=args.pin_memory, |
| drop_last=False, |
| ) |
|
|
| best_dir = output_dir / "best" |
| recovery_dir = output_dir / "recovery" |
|
|
| |
| save_named_training_checkpoint( |
| model=model, |
| tokenizer=tokenizer, |
| optimizer=optimizer, |
| scheduler=scheduler, |
| torch=torch, |
| destination=recovery_dir, |
| state=state, |
| metadata={ |
| "kind": "recovery", |
| "global_step": state["global_step"], |
| "saved_at": now_iso(), |
| }, |
| ) |
|
|
| |
| if not ( |
| (best_dir / "config.json").is_file() |
| and (best_dir / "model.pt").is_file() |
| ): |
| save_named_training_checkpoint( |
| model=model, |
| tokenizer=tokenizer, |
| optimizer=optimizer, |
| scheduler=scheduler, |
| torch=torch, |
| destination=best_dir, |
| state=state, |
| metadata={ |
| "kind": "best", |
| "provisional": True, |
| "validation_loss": state.get("best_validation_loss"), |
| "global_step": state["global_step"], |
| "saved_at": now_iso(), |
| }, |
| ) |
| state["best_checkpoint"] = str(best_dir) |
|
|
| if ( |
| args.eval_at_start |
| and len(validation_dataset) > 0 |
| ): |
| starting_validation_loss = evaluate_loss( |
| model=model, |
| loader=validation_loader, |
| torch=torch, |
| device=device, |
| dtype_name=args.dtype, |
| max_batches=args.eval_batches, |
| ) |
| print( |
| "starting validation " |
| f"step={state['global_step']:,} " |
| f"loss={starting_validation_loss}" |
| ) |
| if ( |
| starting_validation_loss is not None |
| and math.isfinite(starting_validation_loss) |
| and ( |
| state["best_validation_loss"] is None |
| or starting_validation_loss |
| < state["best_validation_loss"] |
| ) |
| ): |
| state["best_validation_loss"] = starting_validation_loss |
| state["best_checkpoint"] = str(best_dir) |
| save_named_training_checkpoint( |
| model=model, |
| tokenizer=tokenizer, |
| optimizer=optimizer, |
| scheduler=scheduler, |
| torch=torch, |
| destination=best_dir, |
| state=state, |
| metadata={ |
| "kind": "best", |
| "provisional": False, |
| "validation_loss": starting_validation_loss, |
| "global_step": state["global_step"], |
| "saved_at": now_iso(), |
| }, |
| ) |
|
|
| training_model = model |
| compile_status = "disabled" |
| effective_compile_mode = args.compile_mode |
| compile_uses_cudagraphs = False |
|
|
| if args.compile: |
| is_rocm = getattr(torch.version, "hip", None) is not None |
|
|
| |
| |
| |
| if is_rocm and effective_compile_mode == "reduce-overhead": |
| effective_compile_mode = "default" |
| print( |
| "ROCm safety: replacing compile mode 'reduce-overhead' " |
| "with 'default' to avoid CUDAGraph output reuse." |
| ) |
|
|
| compile_options = None |
| if is_rocm: |
| compile_options = {"triton.cudagraphs": False} |
|
|
| compile_kwargs = { |
| "mode": effective_compile_mode, |
| "fullgraph": args.compile_fullgraph, |
| "dynamic": False, |
| } |
| if compile_options is not None: |
| compile_kwargs["options"] = compile_options |
|
|
| try: |
| training_model = torch.compile( |
| model, |
| **compile_kwargs, |
| ) |
| compile_status = ( |
| f"enabled:{effective_compile_mode}:cudagraphs-disabled" |
| if is_rocm |
| else f"enabled:{effective_compile_mode}" |
| ) |
| print( |
| "torch.compile enabled:", |
| effective_compile_mode, |
| "(CUDAGraphs disabled on ROCm)" |
| if is_rocm |
| else "", |
| ) |
| except (TypeError, RuntimeError) as option_error: |
| |
| |
| if compile_options is not None: |
| try: |
| training_model = torch.compile( |
| model, |
| mode="default", |
| fullgraph=args.compile_fullgraph, |
| dynamic=False, |
| ) |
| effective_compile_mode = "default" |
| compile_status = ( |
| "enabled:default:option-fallback" |
| ) |
| print( |
| "torch.compile option fallback enabled in default " |
| "mode after:", |
| option_error, |
| ) |
| except Exception as error: |
| compile_status = ( |
| f"setup-failed:{type(error).__name__}" |
| ) |
| training_model = model |
| print( |
| "torch.compile setup failed; using eager mode:", |
| error, |
| ) |
| else: |
| compile_status = ( |
| f"setup-failed:{type(option_error).__name__}" |
| ) |
| training_model = model |
| print( |
| "torch.compile setup failed; using eager mode:", |
| option_error, |
| ) |
| except Exception as error: |
| compile_status = f"setup-failed:{type(error).__name__}" |
| training_model = model |
| print("torch.compile setup failed; using eager mode:", error) |
|
|
| autocast_dtype = ( |
| torch.bfloat16 if args.dtype == "bf16" else torch.float16 |
| ) |
| autocast_enabled = args.dtype in {"bf16", "fp16"} |
| scaler = None |
| if args.dtype == "fp16": |
| scaler = torch.amp.GradScaler("cuda") |
|
|
| optimizer.zero_grad(set_to_none=True) |
| accumulation = 0 |
| running_loss = torch.zeros((), device=device) |
| running_microbatches = 0 |
| nonfinite_loss_seen = torch.zeros( |
| (), |
| device=device, |
| dtype=torch.bool, |
| ) |
| last_log_time = time.perf_counter() |
| last_log_tokens = state["tokens_seen"] |
| stop = False |
|
|
| def recover_from_nonfinite( |
| reason: str, |
| batch_index: int, |
| ) -> None: |
| nonlocal training_model |
| nonlocal compile_status |
| nonlocal accumulation |
| nonlocal running_microbatches |
| nonlocal nonfinite_loss_seen |
|
|
| event_count = int(state.get("nonfinite_events", 0)) + 1 |
| print( |
| f"NON-FINITE TRAINING EVENT #{event_count}: {reason}" |
| ) |
|
|
| optimizer.zero_grad(set_to_none=True) |
| accumulation = 0 |
| running_loss.zero_() |
| running_microbatches = 0 |
| nonfinite_loss_seen.zero_() |
|
|
| if scaler is not None: |
| current_scale = float(scaler.get_scale()) |
| with contextlib.suppress(Exception): |
| scaler.update(max(1.0, current_scale * args.nan_lr_factor)) |
|
|
| if args.nan_action == "stop": |
| raise FloatingPointError( |
| f"Stopping after non-finite training state: {reason}" |
| ) |
|
|
| if args.nan_action == "rollback": |
| restore_training_checkpoint_in_place( |
| checkpoint=recovery_dir, |
| model=model, |
| optimizer=optimizer, |
| scheduler=scheduler, |
| state=state, |
| torch=torch, |
| nn=nn, |
| F=F, |
| ) |
| training_model = model |
| if compile_status.startswith("enabled"): |
| compile_status = "disabled-after-nonfinite" |
| print( |
| "Rolled back to recovery checkpoint:", |
| recovery_dir, |
| ) |
|
|
| state["nonfinite_events"] = event_count |
| state["last_nonfinite_reason"] = reason |
| state["batch_in_epoch"] = batch_index + 1 |
|
|
| new_lrs = backoff_learning_rate( |
| optimizer, |
| scheduler, |
| factor=args.nan_lr_factor, |
| minimum=args.min_learning_rate, |
| ) |
| print("Learning-rate fallback:", new_lrs) |
|
|
| save_named_training_checkpoint( |
| model=model, |
| tokenizer=tokenizer, |
| optimizer=optimizer, |
| scheduler=scheduler, |
| torch=torch, |
| destination=recovery_dir, |
| state=state, |
| metadata={ |
| "kind": "recovery", |
| "reason": reason, |
| "nonfinite_events": event_count, |
| "global_step": state["global_step"], |
| "saved_at": now_iso(), |
| }, |
| ) |
| clear_memory(torch) |
|
|
| if event_count > args.max_nan_recoveries: |
| raise FloatingPointError( |
| "Exceeded --max-nan-recoveries=" |
| f"{args.max_nan_recoveries}." |
| ) |
|
|
| while not stop: |
| epoch = int(state["epoch"]) |
| if args.max_steps <= 0 and epoch >= args.epochs: |
| break |
|
|
| loader = loader_for_epoch(epoch) |
| resume_batch = int(state["batch_in_epoch"]) |
|
|
| for batch_index, batch in enumerate(loader): |
| if batch_index < resume_batch: |
| continue |
|
|
| input_ids = batch.to(device, non_blocking=True) |
|
|
| def forward_backward(active_model): |
| |
| |
| |
| if active_model is not model: |
| marker = getattr( |
| getattr(torch, "compiler", None), |
| "cudagraph_mark_step_begin", |
| None, |
| ) |
| if marker is not None: |
| marker() |
|
|
| with torch.autocast( |
| device_type="cuda", |
| dtype=autocast_dtype, |
| enabled=autocast_enabled, |
| ): |
| output = active_model( |
| input_ids=input_ids, |
| labels=input_ids, |
| ) |
| scaled_loss = output.loss / args.gradient_accumulation |
| if scaler is None: |
| scaled_loss.backward() |
| else: |
| scaler.scale(scaled_loss).backward() |
| return output.loss.detach() |
|
|
| try: |
| detached_loss = forward_backward(training_model) |
| except Exception as error: |
| if training_model is not model: |
| print( |
| "torch.compile failed during training; discarding " |
| "the current accumulation window and continuing in " |
| "eager mode:", |
| f"{type(error).__name__}: {error}", |
| ) |
| optimizer.zero_grad(set_to_none=True) |
| accumulation = 0 |
| running_loss.zero_() |
| running_microbatches = 0 |
| nonfinite_loss_seen.zero_() |
| training_model = model |
| compile_status = ( |
| f"runtime-failed:{type(error).__name__}:eager-fallback" |
| ) |
| with contextlib.suppress(Exception): |
| torch._dynamo.reset() |
| clear_memory(torch) |
| detached_loss = forward_backward(model) |
| else: |
| raise |
|
|
| nonfinite_loss_seen.logical_or_( |
| ~torch.isfinite(detached_loss) |
| ) |
| accumulation += 1 |
| running_loss += torch.nan_to_num( |
| detached_loss, |
| nan=0.0, |
| posinf=0.0, |
| neginf=0.0, |
| ) |
| running_microbatches += 1 |
| state["tokens_seen"] += int(input_ids.numel()) |
| state["batch_in_epoch"] = batch_index + 1 |
|
|
| if accumulation < args.gradient_accumulation: |
| continue |
|
|
| if scaler is not None: |
| scaler.unscale_(optimizer) |
|
|
| grad_norm = torch.nn.utils.clip_grad_norm_( |
| model.parameters(), |
| ( |
| args.max_grad_norm |
| if args.max_grad_norm > 0 |
| else float("inf") |
| ), |
| error_if_nonfinite=False, |
| ) |
| loss_was_nonfinite = bool( |
| nonfinite_loss_seen.item() |
| ) |
| grad_norm_value = float( |
| grad_norm.detach().float().item() |
| ) |
|
|
| if ( |
| loss_was_nonfinite |
| or not math.isfinite(grad_norm_value) |
| ): |
| recover_from_nonfinite( |
| ( |
| "non-finite loss" |
| if loss_was_nonfinite |
| else f"non-finite grad norm={grad_norm_value}" |
| ), |
| batch_index, |
| ) |
| continue |
|
|
| if scaler is None: |
| optimizer.step() |
| else: |
| scaler.step(optimizer) |
| scaler.update() |
| scheduler.step() |
| optimizer.zero_grad(set_to_none=True) |
| accumulation = 0 |
| nonfinite_loss_seen.zero_() |
|
|
| prospective_step = int(state["global_step"]) + 1 |
| if ( |
| args.finite_check_every > 0 |
| and prospective_step % args.finite_check_every == 0 |
| and not model_parameters_are_finite(torch, model) |
| ): |
| recover_from_nonfinite( |
| "non-finite model parameters after optimizer.step()", |
| batch_index, |
| ) |
| continue |
|
|
| state["global_step"] = prospective_step |
| state["last_finite_step"] = prospective_step |
| step = prospective_step |
| if step % args.log_every == 0: |
| torch.cuda.synchronize() |
| now = time.perf_counter() |
| elapsed = max(1e-9, now - last_log_time) |
| delta_tokens = state["tokens_seen"] - last_log_tokens |
| tokens_per_second = delta_tokens / elapsed |
| mean_loss = float( |
| (running_loss / max(1, running_microbatches)).item() |
| ) |
| memory = torch.cuda.max_memory_allocated() / (1024**3) |
| print( |
| f"step={step:,} " |
| f"loss={mean_loss:.5f} " |
| f"lr={scheduler.get_last_lr()[0]:.3e} " |
| f"tok/s={tokens_per_second:,.0f} " |
| f"tokens={state['tokens_seen']:,} " |
| f"peak_gib={memory:.2f}" |
| ) |
| running_loss.zero_() |
| running_microbatches = 0 |
| last_log_time = now |
| last_log_tokens = state["tokens_seen"] |
| torch.cuda.reset_peak_memory_stats() |
|
|
| if ( |
| args.eval_every > 0 |
| and step % args.eval_every == 0 |
| and len(validation_dataset) > 0 |
| ): |
| validation_loss = evaluate_loss( |
| model=model, |
| loader=validation_loader, |
| torch=torch, |
| device=device, |
| dtype_name=args.dtype, |
| max_batches=args.eval_batches, |
| ) |
| print( |
| f"validation step={step:,} loss={validation_loss}" |
| ) |
| if ( |
| validation_loss is not None |
| and not math.isfinite(validation_loss) |
| ): |
| recover_from_nonfinite( |
| "validation remained non-finite after FP32 retry", |
| batch_index, |
| ) |
| continue |
|
|
| if ( |
| validation_loss is not None |
| and math.isfinite(validation_loss) |
| and ( |
| state["best_validation_loss"] is None |
| or validation_loss |
| < state["best_validation_loss"] |
| ) |
| ): |
| state["best_validation_loss"] = validation_loss |
| state["best_checkpoint"] = str(best_dir) |
| save_named_training_checkpoint( |
| model=model, |
| tokenizer=tokenizer, |
| optimizer=optimizer, |
| scheduler=scheduler, |
| torch=torch, |
| destination=best_dir, |
| state=state, |
| metadata={ |
| "kind": "best", |
| "provisional": False, |
| "validation_loss": validation_loss, |
| "global_step": step, |
| "saved_at": now_iso(), |
| }, |
| ) |
| print( |
| "New best checkpoint:", |
| best_dir, |
| f"validation_loss={validation_loss}", |
| ) |
|
|
| if args.save_every > 0 and step % args.save_every == 0: |
| destination = save_checkpoint( |
| model=model, |
| tokenizer=tokenizer, |
| optimizer=optimizer, |
| scheduler=scheduler, |
| torch=torch, |
| output_dir=output_dir, |
| state=state, |
| keep=args.keep_checkpoints, |
| ) |
| print("Saved:", destination) |
| save_named_training_checkpoint( |
| model=model, |
| tokenizer=tokenizer, |
| optimizer=optimizer, |
| scheduler=scheduler, |
| torch=torch, |
| destination=recovery_dir, |
| state=state, |
| metadata={ |
| "kind": "recovery", |
| "source_checkpoint": str(destination), |
| "global_step": step, |
| "saved_at": now_iso(), |
| }, |
| ) |
|
|
| if args.max_steps > 0 and step >= args.max_steps: |
| stop = True |
| break |
|
|
| if stop: |
| break |
| state["epoch"] = epoch + 1 |
| state["batch_in_epoch"] = 0 |
|
|
| final_checkpoint = save_checkpoint( |
| model=model, |
| tokenizer=tokenizer, |
| optimizer=optimizer, |
| scheduler=scheduler, |
| torch=torch, |
| output_dir=output_dir, |
| state=state, |
| keep=args.keep_checkpoints, |
| ) |
| save_named_training_checkpoint( |
| model=model, |
| tokenizer=tokenizer, |
| optimizer=optimizer, |
| scheduler=scheduler, |
| torch=torch, |
| destination=recovery_dir, |
| state=state, |
| metadata={ |
| "kind": "recovery", |
| "source_checkpoint": str(final_checkpoint), |
| "global_step": state["global_step"], |
| "saved_at": now_iso(), |
| }, |
| ) |
|
|
| final_dir = output_dir / "final" |
| if final_dir.exists(): |
| shutil.rmtree(final_dir) |
| save_model_bundle(model, tokenizer, final_dir, torch) |
|
|
| result = { |
| "state": state, |
| "parameters": parameters, |
| "architecture": model.config.to_dict(), |
| "optimizer_backend": optimizer_backend, |
| "compile_status": compile_status, |
| "final_checkpoint": str(final_checkpoint), |
| "final_model": str(final_dir), |
| "packed_manifest": manifest, |
| "schedule": { |
| "run_target_steps": run_target_steps, |
| "lr_decay_steps": schedule_steps, |
| "warmup_steps": warmup_steps, |
| "minimum_lr_ratio": args.minimum_lr_ratio, |
| }, |
| "completed_at": now_iso(), |
| } |
| atomic_json(output_dir / "training_result.json", result) |
| print(json.dumps(result, indent=2)) |
| return result |
|
|
|
|
| |
| |
| |
|
|
|
|
| def doctor(args: argparse.Namespace) -> dict[str, Any]: |
| report: dict[str, Any] = { |
| "python": sys.version, |
| "script_version": SCRIPT_VERSION, |
| "environment": { |
| "PYTORCH_ALLOC_CONF": os.environ.get("PYTORCH_ALLOC_CONF"), |
| "TOKENIZERS_PARALLELISM": os.environ.get( |
| "TOKENIZERS_PARALLELISM" |
| ), |
| "USE_ROCM_CK_GEMM": os.environ.get("USE_ROCM_CK_GEMM"), |
| }, |
| } |
| try: |
| np, torch, nn, F, DataLoader, Dataset = import_training_stack() |
| del np, DataLoader, Dataset |
| report["torch"] = { |
| "version": torch.__version__, |
| "hip": getattr(torch.version, "hip", None), |
| "cuda_available": torch.cuda.is_available(), |
| "device_count": torch.cuda.device_count(), |
| "device_name": ( |
| torch.cuda.get_device_name(0) |
| if torch.cuda.is_available() |
| else None |
| ), |
| "bf16_supported": ( |
| torch.cuda.is_bf16_supported() |
| if torch.cuda.is_available() |
| else False |
| ), |
| "compile_available": hasattr(torch, "compile"), |
| } |
| if torch.cuda.is_available(): |
| query = torch.randn( |
| 1, |
| 8, |
| 128, |
| 64, |
| device="cuda", |
| dtype=torch.bfloat16, |
| ) |
| with torch.no_grad(): |
| output = F.scaled_dot_product_attention( |
| query, |
| query, |
| query, |
| is_causal=True, |
| ) |
| torch.cuda.synchronize() |
| report["sdpa_probe"] = { |
| "ok": True, |
| "shape": list(output.shape), |
| } |
| del query, output |
| clear_memory(torch) |
| except Exception as error: |
| report["error"] = f"{type(error).__name__}: {error}" |
|
|
| print(json.dumps(report, indent=2)) |
| return report |
|
|
|
|
| def inspect_project(args: argparse.Namespace) -> dict[str, Any]: |
| tokenizer = load_tokenizer(args.tokenizer.resolve()) |
| report = { |
| "tokenizer_vocab_size": len(tokenizer), |
| "special_ids": len(tokenizer.all_special_ids), |
| "model_max_length": tokenizer.model_max_length, |
| "default_architecture": DEFAULT_ARCHITECTURE, |
| } |
| model_path = args.model.resolve() if args.model else None |
| if model_path and (model_path / "config.json").is_file(): |
| report["saved_model_config"] = json.loads( |
| (model_path / "config.json").read_text(encoding="utf-8") |
| ) |
| print(json.dumps(report, indent=2)) |
| return report |
|
|
|
|
| def benchmark_model(args: argparse.Namespace) -> dict[str, Any]: |
| np, torch, nn, F, DataLoader, Dataset = import_training_stack() |
| del np, DataLoader, Dataset |
| if not torch.cuda.is_available(): |
| raise RuntimeError("ROCm GPU is unavailable.") |
|
|
| device = torch.device("cuda") |
| if args.model: |
| model = load_model_bundle(args.model.resolve(), torch, nn, F) |
| else: |
| tokenizer = load_tokenizer(args.tokenizer.resolve()) |
| config = _config_from_args(tokenizer, args, args.context_length) |
| model_class, _ = create_model_classes(torch, nn, F) |
| model = model_class(config) |
|
|
| model.to(device).train() |
| active_model = model |
| compile_status = "disabled" |
| if args.compile: |
| active_model = torch.compile( |
| model, |
| mode=args.compile_mode, |
| fullgraph=args.compile_fullgraph, |
| dynamic=False, |
| ) |
| compile_status = f"enabled:{args.compile_mode}" |
|
|
| input_ids = torch.randint( |
| 0, |
| model.config.vocab_size, |
| (args.batch_size, args.context_length), |
| device=device, |
| ) |
| optimizer, optimizer_backend = build_adamw(torch, model, args) |
| autocast_dtype = ( |
| torch.bfloat16 if args.dtype == "bf16" else torch.float16 |
| ) |
| autocast_enabled = args.dtype in {"bf16", "fp16"} |
|
|
| def iteration(): |
| optimizer.zero_grad(set_to_none=True) |
| with torch.autocast( |
| device_type="cuda", |
| dtype=autocast_dtype, |
| enabled=autocast_enabled, |
| ): |
| output = active_model(input_ids=input_ids, labels=input_ids) |
| output.loss.backward() |
| optimizer.step() |
| return output.loss |
|
|
| for _ in range(args.warmup): |
| iteration() |
| torch.cuda.synchronize() |
| torch.cuda.reset_peak_memory_stats() |
| started = time.perf_counter() |
| last_loss = None |
| for _ in range(args.steps): |
| last_loss = iteration() |
| torch.cuda.synchronize() |
| elapsed = time.perf_counter() - started |
| tokens = args.steps * args.batch_size * args.context_length |
|
|
| result = { |
| "tokens_per_second": tokens / elapsed, |
| "seconds": elapsed, |
| "steps": args.steps, |
| "batch_size": args.batch_size, |
| "context_length": args.context_length, |
| "loss": ( |
| float(last_loss.detach().item()) |
| if last_loss is not None |
| else None |
| ), |
| "peak_gib": torch.cuda.max_memory_allocated() / (1024**3), |
| "parameters": count_parameters(model), |
| "config": model.config.to_dict(), |
| "compile_status": compile_status, |
| "optimizer_backend": optimizer_backend, |
| } |
| print(json.dumps(result, indent=2)) |
| return result |
|
|
|
|
| def generate_text(args: argparse.Namespace) -> str: |
| np, torch, nn, F, DataLoader, Dataset = import_training_stack() |
| del np, DataLoader, Dataset |
| if not torch.cuda.is_available(): |
| raise RuntimeError("ROCm GPU is unavailable.") |
|
|
| model_path = args.model.resolve() |
| tokenizer = load_tokenizer(model_path) |
| model = load_model_bundle(model_path, torch, nn, F).to("cuda") |
| model.eval() |
|
|
| encoded = tokenizer( |
| args.prompt, |
| add_special_tokens=False, |
| return_tensors="pt", |
| return_token_type_ids=False, |
| ) |
| input_ids = encoded.input_ids.to("cuda") |
| prompt_length = int(input_ids.shape[1]) |
|
|
| blocked_ids = ( |
| [] |
| if args.allow_control_tokens |
| else blocked_generation_token_ids(tokenizer) |
| ) |
| blocked_tensor = ( |
| torch.tensor( |
| blocked_ids, |
| device="cuda", |
| dtype=torch.long, |
| ) |
| if blocked_ids |
| else None |
| ) |
|
|
| generated: list[int] = [] |
| with torch.no_grad(): |
| for generation_step in range(args.max_new_tokens): |
| model_input = input_ids[ |
| :, -model.config.max_position_embeddings : |
| ] |
|
|
| with torch.autocast( |
| device_type="cuda", |
| dtype=torch.bfloat16, |
| enabled=True, |
| ): |
| logits = model( |
| input_ids=model_input, |
| return_last_logits=True, |
| ).logits[:, -1, :] |
|
|
| if not bool(torch.isfinite(logits).all().item()): |
| print( |
| "Non-finite generation logits under BF16; " |
| "retrying this token in FP32.", |
| file=sys.stderr, |
| ) |
| with torch.autocast( |
| device_type="cuda", |
| enabled=False, |
| ): |
| logits = model( |
| input_ids=model_input, |
| return_last_logits=True, |
| ).logits[:, -1, :].float() |
|
|
| if not bool(torch.isfinite(logits).all().item()): |
| logits = torch.nan_to_num( |
| logits, |
| nan=-float("inf"), |
| posinf=1e4, |
| neginf=-1e4, |
| ) |
|
|
| if args.show_top_tokens > 0: |
| top_values, top_indices = torch.topk( |
| logits, |
| min(args.show_top_tokens, logits.shape[-1]), |
| dim=-1, |
| ) |
| decoded = [ |
| { |
| "id": int(token_id), |
| "token": tokenizer.decode( |
| [int(token_id)], |
| skip_special_tokens=False, |
| clean_up_tokenization_spaces=False, |
| ), |
| "logit": float(value), |
| } |
| for token_id, value in zip( |
| top_indices[0].tolist(), |
| top_values[0].float().tolist(), |
| ) |
| ] |
| print( |
| f"raw top tokens at generation step {generation_step}: " |
| + json.dumps(decoded, ensure_ascii=False), |
| file=sys.stderr, |
| ) |
|
|
| |
| |
| if blocked_tensor is not None: |
| logits.index_fill_( |
| 1, |
| blocked_tensor, |
| -float("inf"), |
| ) |
|
|
| if args.repetition_penalty != 1.0: |
| used = torch.unique(model_input) |
| selected = logits[:, used] |
| logits[:, used] = torch.where( |
| selected < 0, |
| selected * args.repetition_penalty, |
| selected / args.repetition_penalty, |
| ) |
|
|
| if not bool(torch.isfinite(logits).any().item()): |
| next_token = torch.tensor( |
| [[int(tokenizer.eos_token_id)]], |
| device="cuda", |
| dtype=torch.long, |
| ) |
| elif args.temperature <= 0: |
| next_token = logits.argmax(dim=-1, keepdim=True) |
| else: |
| logits = logits / max(args.temperature, 1e-5) |
| if args.top_k > 0: |
| threshold = torch.topk( |
| logits, |
| min(args.top_k, logits.shape[-1]), |
| dim=-1, |
| ).values[:, -1:] |
| logits = logits.masked_fill( |
| logits < threshold, |
| -float("inf"), |
| ) |
| probabilities = torch.softmax(logits, dim=-1) |
| if args.top_p < 1.0: |
| sorted_probabilities, sorted_indices = torch.sort( |
| probabilities, |
| descending=True, |
| dim=-1, |
| ) |
| cumulative = sorted_probabilities.cumsum(dim=-1) |
| remove = cumulative > args.top_p |
| remove[:, 1:] = remove[:, :-1].clone() |
| remove[:, 0] = False |
| sorted_probabilities = ( |
| sorted_probabilities.masked_fill(remove, 0.0) |
| ) |
| denominator = sorted_probabilities.sum( |
| dim=-1, |
| keepdim=True, |
| ).clamp_min(1e-12) |
| sorted_probabilities /= denominator |
| sampled = torch.multinomial( |
| sorted_probabilities, |
| 1, |
| ) |
| next_token = sorted_indices.gather(-1, sampled) |
| else: |
| next_token = torch.multinomial(probabilities, 1) |
|
|
| token_id = int(next_token.item()) |
| if token_id in blocked_ids: |
| raise RuntimeError( |
| "A blocked structural control token escaped masking: " |
| f"id={token_id}, token={tokenizer.decode([token_id], skip_special_tokens=False)!r}" |
| ) |
|
|
| generated.append(token_id) |
| input_ids = torch.cat((input_ids, next_token), dim=-1) |
| if token_id == int(tokenizer.eos_token_id): |
| break |
|
|
| completion = tokenizer.decode( |
| generated, |
| skip_special_tokens=False, |
| clean_up_tokenization_spaces=False, |
| ) |
| print(completion) |
| return completion |
|
|
|
|
| |
| |
| |
|
|
|
|
| def cycle(args: argparse.Namespace) -> None: |
| sync_namespace = argparse.Namespace( |
| data=args.data, |
| inbox=args.inbox, |
| archive=args.archive, |
| work_dir=args.work_dir / "sync", |
| seed=args.seed, |
| recursive=args.recursive, |
| skip_invalid_files=False, |
| backup=args.backup, |
| audit=args.work_dir / "last_sync.json", |
| ) |
| sync_dataset(sync_namespace) |
|
|
| pack_namespace = argparse.Namespace( |
| data=args.data, |
| tokenizer=args.tokenizer, |
| output=args.packed, |
| context_length=4096, |
| validation_ratio=args.validation_ratio, |
| force=False, |
| ) |
| pack_dataset(pack_namespace) |
|
|
| latest = find_latest_checkpoint(args.output.resolve()) |
| current_step = checkpoint_step(latest) |
| target_step = current_step + args.additional_steps |
|
|
| train_namespace = argparse.Namespace( |
| tokenizer=args.tokenizer, |
| packed=args.packed, |
| output=args.output, |
| resume="auto", |
| seed=args.seed, |
| dtype="bf16", |
| batch_size=args.batch_size, |
| gradient_accumulation=args.gradient_accumulation, |
| learning_rate=args.learning_rate, |
| beta1=0.9, |
| beta2=0.95, |
| adam_epsilon=1e-8, |
| weight_decay=0.1, |
| max_grad_norm=1.0, |
| max_steps=target_step, |
| epochs=1, |
| warmup_steps=-1, |
| warmup_ratio=0.02, |
| minimum_lr_ratio=0.1, |
| lr_decay_steps=args.lr_decay_steps, |
| log_every=args.log_every, |
| eval_every=args.eval_every, |
| eval_batches=args.eval_batches, |
| save_every=args.save_every, |
| keep_checkpoints=args.keep_checkpoints, |
| num_workers=args.num_workers, |
| pin_memory=True, |
| persistent_workers=args.num_workers > 0, |
| prefetch_factor=2, |
| compile=args.compile, |
| compile_mode=args.compile_mode, |
| compile_fullgraph=args.compile_fullgraph, |
| fused_optimizer=True, |
| target_parameters=args.target_parameters, |
| hidden_size=args.hidden_size, |
| embedding_size=args.embedding_size, |
| ffn_latent_size=args.ffn_latent_size, |
| layers=args.layers, |
| heads=args.heads, |
| kv_heads=args.kv_heads, |
| attention_every=args.attention_every, |
| window_size=args.window_size, |
| conv_kernel_size=args.conv_kernel_size, |
| memory_size=args.memory_size, |
| memory_heads=args.memory_heads, |
| attention_residual_group_size=args.attention_residual_group_size, |
| mtp_loss_weight=args.mtp_loss_weight, |
| eval_at_start=True, |
| nan_action="rollback", |
| nan_lr_factor=0.5, |
| min_learning_rate=1e-7, |
| max_nan_recoveries=20, |
| finite_check_every=100, |
| ) |
| train_model(train_namespace) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def add_architecture_arguments(parser: argparse.ArgumentParser) -> None: |
| parser.add_argument( |
| "--target-parameters", |
| type=int, |
| default=60_000_000, |
| ) |
| parser.add_argument("--hidden-size", type=int, default=512) |
| parser.add_argument("--embedding-size", type=int, default=256) |
| parser.add_argument("--ffn-latent-size", type=int, default=256) |
| parser.add_argument("--layers", type=int, default=24) |
| parser.add_argument("--heads", type=int, default=8) |
| parser.add_argument("--kv-heads", type=int, default=2) |
| parser.add_argument("--attention-every", type=int, default=4) |
| parser.add_argument("--window-size", type=int, default=512) |
| parser.add_argument("--conv-kernel-size", type=int, default=4) |
| parser.add_argument("--memory-size", type=int, default=128) |
| parser.add_argument("--memory-heads", type=int, default=4) |
| parser.add_argument( |
| "--attention-residual-group-size", |
| type=int, |
| default=4, |
| ) |
| parser.add_argument("--mtp-loss-weight", type=float, default=0.20) |
|
|
|
|
| def add_compile_arguments(parser: argparse.ArgumentParser) -> None: |
| parser.add_argument( |
| "--compile", |
| action=argparse.BooleanOptionalAction, |
| default=True, |
| ) |
| parser.add_argument( |
| "--compile-mode", |
| choices=[ |
| "default", |
| "reduce-overhead", |
| "max-autotune", |
| "max-autotune-no-cudagraphs", |
| ], |
| default="default", |
| ) |
| parser.add_argument( |
| "--compile-fullgraph", |
| action=argparse.BooleanOptionalAction, |
| default=False, |
| ) |
|
|
|
|
| def build_parser() -> argparse.ArgumentParser: |
| parser = argparse.ArgumentParser( |
| description=( |
| "Train a deeper speed-first ~60M hybrid byte language model." |
| ), |
| formatter_class=argparse.ArgumentDefaultsHelpFormatter, |
| ) |
| subcommands = parser.add_subparsers(dest="command", required=True) |
|
|
| tokenizer_parser = subcommands.add_parser( |
| "tokenizer", |
| help="Build the byte + universal-special tokenizer.", |
| ) |
| tokenizer_parser.add_argument( |
| "--inventory", |
| type=Path, |
| default=DEFAULT_INVENTORY, |
| ) |
| tokenizer_parser.add_argument( |
| "--output", |
| type=Path, |
| default=Path("artifacts/byte-tokenizer"), |
| ) |
| tokenizer_parser.add_argument( |
| "--context-length", |
| type=int, |
| default=4096, |
| ) |
| tokenizer_parser.set_defaults(function=build_tokenizer) |
|
|
| sync_parser = subcommands.add_parser( |
| "sync-data", |
| help=( |
| "Append, exact-dedupe, deterministically shuffle, and archive " |
| "new batches from a directory." |
| ), |
| ) |
| sync_parser.add_argument("--data", type=Path, default=Path("rewrite.jsonl")) |
| sync_parser.add_argument("--inbox", type=Path, required=True) |
| sync_parser.add_argument("--archive", type=Path) |
| sync_parser.add_argument( |
| "--work-dir", |
| type=Path, |
| default=Path(".bytefalcon-work/sync"), |
| ) |
| sync_parser.add_argument("--seed", type=int, default=42) |
| sync_parser.add_argument("--recursive", action="store_true") |
| sync_parser.add_argument("--skip-invalid-files", action="store_true") |
| sync_parser.add_argument("--backup", action="store_true") |
| sync_parser.add_argument("--audit", type=Path) |
| sync_parser.set_defaults(function=sync_dataset) |
|
|
| pack_parser = subcommands.add_parser( |
| "pack", |
| help="Pack rewrite.jsonl into train/validation uint16 streams.", |
| ) |
| pack_parser.add_argument("--data", type=Path, default=Path("rewrite.jsonl")) |
| pack_parser.add_argument( |
| "--tokenizer", |
| type=Path, |
| default=Path("artifacts/byte-tokenizer"), |
| ) |
| pack_parser.add_argument( |
| "--output", |
| type=Path, |
| default=Path("artifacts/packed-4096"), |
| ) |
| pack_parser.add_argument("--context-length", type=int, default=4096) |
| pack_parser.add_argument("--validation-ratio", type=float, default=0.005) |
| pack_parser.add_argument("--force", action="store_true") |
| pack_parser.set_defaults(function=pack_dataset) |
|
|
| audit_parser = subcommands.add_parser( |
| "audit-packed", |
| help="Count reserved control IDs inside packed train/validation streams.", |
| ) |
| audit_parser.add_argument( |
| "--tokenizer", |
| type=Path, |
| default=Path("artifacts/byte-tokenizer"), |
| ) |
| audit_parser.add_argument( |
| "--packed", |
| type=Path, |
| default=Path("artifacts/packed-4096"), |
| ) |
| audit_parser.set_defaults(function=audit_packed_dataset) |
|
|
| init_parser = subcommands.add_parser( |
| "init", |
| help="Initialize and save the deeper ~60M speed-first model.", |
| ) |
| init_parser.add_argument( |
| "--tokenizer", |
| type=Path, |
| default=Path("artifacts/byte-tokenizer"), |
| ) |
| init_parser.add_argument( |
| "--output", |
| type=Path, |
| default=Path("runs/bytefast-60m/initial"), |
| ) |
| init_parser.add_argument("--context-length", type=int, default=4096) |
| add_architecture_arguments(init_parser) |
| init_parser.set_defaults(function=initialize_model) |
|
|
| train_parser = subcommands.add_parser( |
| "train", |
| help="Train from scratch or resume a checkpoint.", |
| ) |
| train_parser.add_argument( |
| "--tokenizer", |
| type=Path, |
| default=Path("artifacts/byte-tokenizer"), |
| ) |
| train_parser.add_argument( |
| "--packed", |
| type=Path, |
| default=Path("artifacts/packed-4096"), |
| ) |
| train_parser.add_argument( |
| "--output", |
| type=Path, |
| default=Path("runs/bytefast-60m"), |
| ) |
| train_parser.add_argument( |
| "--resume", |
| default="auto", |
| help="'auto', 'none', or a checkpoint path.", |
| ) |
| train_parser.add_argument("--seed", type=int, default=42) |
| train_parser.add_argument( |
| "--dtype", |
| choices=["bf16", "fp16", "fp32"], |
| default="bf16", |
| ) |
| train_parser.add_argument("--batch-size", type=int, default=4) |
| train_parser.add_argument( |
| "--gradient-accumulation", |
| type=int, |
| default=4, |
| ) |
| train_parser.add_argument("--learning-rate", type=float, default=2e-5) |
| train_parser.add_argument("--beta1", type=float, default=0.9) |
| train_parser.add_argument("--beta2", type=float, default=0.95) |
| train_parser.add_argument("--adam-epsilon", type=float, default=1e-8) |
| train_parser.add_argument("--weight-decay", type=float, default=0.1) |
| train_parser.add_argument("--max-grad-norm", type=float, default=1.0) |
| train_parser.add_argument("--max-steps", type=int, default=0) |
| train_parser.add_argument("--epochs", type=int, default=1) |
| train_parser.add_argument("--warmup-steps", type=int, default=-1) |
| train_parser.add_argument("--warmup-ratio", type=float, default=0.02) |
| train_parser.add_argument("--minimum-lr-ratio", type=float, default=0.1) |
| train_parser.add_argument("--lr-decay-steps", type=int, default=100000) |
| train_parser.add_argument("--log-every", type=int, default=100) |
| train_parser.add_argument("--eval-every", type=int, default=500) |
| train_parser.add_argument("--eval-batches", type=int, default=8) |
| train_parser.add_argument("--save-every", type=int, default=1000) |
| train_parser.add_argument("--keep-checkpoints", type=int, default=5) |
| train_parser.add_argument( |
| "--eval-at-start", |
| action=argparse.BooleanOptionalAction, |
| default=True, |
| help="Evaluate and materialize best/ before the first optimizer update.", |
| ) |
| train_parser.add_argument( |
| "--nan-action", |
| choices=["rollback", "skip", "stop"], |
| default="rollback", |
| ) |
| train_parser.add_argument("--nan-lr-factor", type=float, default=0.5) |
| train_parser.add_argument("--min-learning-rate", type=float, default=1e-7) |
| train_parser.add_argument("--max-nan-recoveries", type=int, default=20) |
| train_parser.add_argument( |
| "--finite-check-every", |
| type=int, |
| default=100, |
| help="Scan all model parameters for NaN/Inf every N optimizer steps.", |
| ) |
| train_parser.add_argument("--num-workers", type=int, default=2) |
| train_parser.add_argument( |
| "--pin-memory", |
| action=argparse.BooleanOptionalAction, |
| default=True, |
| ) |
| train_parser.add_argument( |
| "--persistent-workers", |
| action=argparse.BooleanOptionalAction, |
| default=True, |
| ) |
| train_parser.add_argument("--prefetch-factor", type=int, default=2) |
| train_parser.add_argument( |
| "--fused-optimizer", |
| action=argparse.BooleanOptionalAction, |
| default=True, |
| ) |
| add_compile_arguments(train_parser) |
| add_architecture_arguments(train_parser) |
| train_parser.set_defaults(function=train_model) |
|
|
| cycle_parser = subcommands.add_parser( |
| "cycle", |
| help="Sync, repack if changed, and resume fast training.", |
| ) |
| cycle_parser.add_argument("--data", type=Path, default=Path("rewrite.jsonl")) |
| cycle_parser.add_argument("--inbox", type=Path, required=True) |
| cycle_parser.add_argument("--archive", type=Path) |
| cycle_parser.add_argument( |
| "--work-dir", |
| type=Path, |
| default=Path(".bytefalcon-work"), |
| ) |
| cycle_parser.add_argument( |
| "--tokenizer", |
| type=Path, |
| default=Path("artifacts/byte-tokenizer"), |
| ) |
| cycle_parser.add_argument( |
| "--packed", |
| type=Path, |
| default=Path("artifacts/packed-4096"), |
| ) |
| cycle_parser.add_argument( |
| "--output", |
| type=Path, |
| default=Path("runs/bytefast-60m"), |
| ) |
| cycle_parser.add_argument("--additional-steps", type=int, default=500) |
| cycle_parser.add_argument("--seed", type=int, default=42) |
| cycle_parser.add_argument("--recursive", action="store_true") |
| cycle_parser.add_argument("--backup", action="store_true") |
| cycle_parser.add_argument("--validation-ratio", type=float, default=0.005) |
| cycle_parser.add_argument("--batch-size", type=int, default=4) |
| cycle_parser.add_argument("--gradient-accumulation", type=int, default=4) |
| cycle_parser.add_argument("--learning-rate", type=float, default=2e-5) |
| cycle_parser.add_argument("--lr-decay-steps", type=int, default=100000) |
| cycle_parser.add_argument("--log-every", type=int, default=100) |
| cycle_parser.add_argument("--eval-every", type=int, default=500) |
| cycle_parser.add_argument("--eval-batches", type=int, default=8) |
| cycle_parser.add_argument("--save-every", type=int, default=500) |
| cycle_parser.add_argument("--keep-checkpoints", type=int, default=5) |
| cycle_parser.add_argument("--num-workers", type=int, default=4) |
| add_compile_arguments(cycle_parser) |
| add_architecture_arguments(cycle_parser) |
| cycle_parser.set_defaults(function=cycle) |
|
|
| doctor_parser = subcommands.add_parser( |
| "doctor", |
| help="Audit ROCm, bf16, torch.compile, and SDPA.", |
| ) |
| doctor_parser.add_argument( |
| "--tokenizer", |
| type=Path, |
| default=Path("artifacts/byte-tokenizer"), |
| ) |
| doctor_parser.set_defaults(function=doctor) |
|
|
| inspect_parser = subcommands.add_parser( |
| "inspect", |
| help="Show tokenizer and architecture details.", |
| ) |
| inspect_parser.add_argument( |
| "--tokenizer", |
| type=Path, |
| default=Path("artifacts/byte-tokenizer"), |
| ) |
| inspect_parser.add_argument("--model", type=Path) |
| inspect_parser.set_defaults(function=inspect_project) |
|
|
| benchmark_parser = subcommands.add_parser( |
| "benchmark", |
| help="Measure steady-state training throughput on the GPU.", |
| ) |
| benchmark_parser.add_argument("--model", type=Path) |
| benchmark_parser.add_argument( |
| "--tokenizer", |
| type=Path, |
| default=Path("artifacts/byte-tokenizer"), |
| ) |
| benchmark_parser.add_argument("--context-length", type=int, default=4096) |
| benchmark_parser.add_argument("--batch-size", type=int, default=2) |
| benchmark_parser.add_argument("--warmup", type=int, default=3) |
| benchmark_parser.add_argument("--steps", type=int, default=100) |
| benchmark_parser.add_argument( |
| "--dtype", |
| choices=["bf16", "fp16", "fp32"], |
| default="bf16", |
| ) |
| benchmark_parser.add_argument("--learning-rate", type=float, default=2e-5) |
| benchmark_parser.add_argument("--beta1", type=float, default=0.9) |
| benchmark_parser.add_argument("--beta2", type=float, default=0.95) |
| benchmark_parser.add_argument("--adam-epsilon", type=float, default=1e-8) |
| benchmark_parser.add_argument("--weight-decay", type=float, default=0.1) |
| benchmark_parser.add_argument( |
| "--fused-optimizer", |
| action=argparse.BooleanOptionalAction, |
| default=True, |
| ) |
| add_compile_arguments(benchmark_parser) |
| add_architecture_arguments(benchmark_parser) |
| benchmark_parser.set_defaults(function=benchmark_model) |
|
|
| generate_parser = subcommands.add_parser( |
| "generate", |
| help="Generate from a trained checkpoint.", |
| ) |
| generate_parser.add_argument("--model", type=Path, required=True) |
| generate_parser.add_argument("--prompt", required=True) |
| generate_parser.add_argument("--max-new-tokens", type=int, default=128) |
| generate_parser.add_argument("--temperature", type=float, default=0.7) |
| generate_parser.add_argument("--top-p", type=float, default=0.95) |
| generate_parser.add_argument("--top-k", type=int, default=50) |
| generate_parser.add_argument("--repetition-penalty", type=float, default=1.1) |
| generate_parser.add_argument( |
| "--allow-control-tokens", |
| action="store_true", |
| help="Allow structural IDs such as <pad>; disabled by default.", |
| ) |
| generate_parser.add_argument( |
| "--show-top-tokens", |
| type=int, |
| default=0, |
| help="Print the raw top-N logits before reserved-token masking.", |
| ) |
| generate_parser.set_defaults(function=generate_text) |
|
|
| return parser |
|
|
|
|
| def validate_args(args: argparse.Namespace) -> None: |
| if hasattr(args, "context_length") and args.context_length != 4096: |
| raise ValueError("This project is fixed to context length 4096.") |
| if hasattr(args, "validation_ratio") and not ( |
| 0.0 <= args.validation_ratio < 0.5 |
| ): |
| raise ValueError("--validation-ratio must be in [0, 0.5).") |
| if hasattr(args, "batch_size") and args.batch_size <= 0: |
| raise ValueError("--batch-size must be positive.") |
| if ( |
| hasattr(args, "gradient_accumulation") |
| and args.gradient_accumulation <= 0 |
| ): |
| raise ValueError("--gradient-accumulation must be positive.") |
| if hasattr(args, "lr_decay_steps") and args.lr_decay_steps <= 0: |
| raise ValueError("--lr-decay-steps must be positive.") |
| if hasattr(args, "window_size") and 4096 % args.window_size != 0: |
| raise ValueError("--window-size must divide 4096 exactly.") |
| if ( |
| hasattr(args, "hidden_size") |
| and hasattr(args, "heads") |
| and args.hidden_size % args.heads != 0 |
| ): |
| raise ValueError("--hidden-size must be divisible by --heads.") |
| if ( |
| hasattr(args, "memory_size") |
| and hasattr(args, "memory_heads") |
| and args.memory_size % args.memory_heads != 0 |
| ): |
| raise ValueError( |
| "--memory-size must be divisible by --memory-heads." |
| ) |
| if ( |
| hasattr(args, "heads") |
| and hasattr(args, "kv_heads") |
| and args.heads % args.kv_heads != 0 |
| ): |
| raise ValueError("--heads must be divisible by --kv-heads.") |
| if hasattr(args, "attention_every") and args.attention_every <= 0: |
| raise ValueError("--attention-every must be positive.") |
| if hasattr(args, "ffn_latent_size") and ( |
| args.ffn_latent_size <= 0 |
| or args.ffn_latent_size > args.hidden_size |
| ): |
| raise ValueError( |
| "--ffn-latent-size must be positive and no larger than hidden size." |
| ) |
| if hasattr(args, "mtp_loss_weight") and args.mtp_loss_weight < 0: |
| raise ValueError("--mtp-loss-weight must be non-negative.") |
| if hasattr(args, "nan_lr_factor") and not ( |
| 0.0 < args.nan_lr_factor < 1.0 |
| ): |
| raise ValueError("--nan-lr-factor must be in (0, 1).") |
| if hasattr(args, "min_learning_rate") and args.min_learning_rate <= 0: |
| raise ValueError("--min-learning-rate must be positive.") |
| if hasattr(args, "max_nan_recoveries") and args.max_nan_recoveries < 0: |
| raise ValueError("--max-nan-recoveries must be non-negative.") |
| if hasattr(args, "finite_check_every") and args.finite_check_every < 0: |
| raise ValueError("--finite-check-every must be non-negative.") |
|
|
|
|
| def main() -> int: |
| parser = build_parser() |
| args = parser.parse_args() |
| validate_args(args) |
| args.function(args) |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| try: |
| raise SystemExit(main()) |
| except KeyboardInterrupt: |
| print("\nInterrupted.", file=sys.stderr) |
| raise SystemExit(130) |
|
|