Upload scripts/bbpe_trainer.py with huggingface_hub
Browse files- scripts/bbpe_trainer.py +515 -71
scripts/bbpe_trainer.py
CHANGED
|
@@ -1,20 +1,32 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
EthioBBPE: Production-Ready Byte-Level BPE Tokenizer Trainer
|
| 4 |
-
Features:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import os
|
| 8 |
import json
|
| 9 |
import gzip
|
|
|
|
|
|
|
| 10 |
import shutil
|
|
|
|
| 11 |
import logging
|
| 12 |
from pathlib import Path
|
| 13 |
-
from typing import List, Optional, Union, Dict, Any
|
| 14 |
from dataclasses import dataclass, field, asdict
|
| 15 |
from datetime import datetime
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
from tokenizers import ByteLevelBPETokenizer, trainers
|
| 18 |
from tokenizers.implementations import BaseTokenizer
|
| 19 |
|
| 20 |
# Configure logging
|
|
@@ -35,11 +47,28 @@ class BBPEConfig:
|
|
| 35 |
lowercase: bool = False
|
| 36 |
dropout: Optional[float] = None
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
# Advanced features
|
|
|
|
|
|
|
| 39 |
save_compressed: bool = True
|
| 40 |
-
|
|
|
|
|
|
|
| 41 |
num_threads: int = -1 # -1 for auto
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
def save(self, path: str):
|
| 44 |
"""Save configuration to JSON."""
|
| 45 |
with open(path, 'w', encoding='utf-8') as f:
|
|
@@ -54,23 +83,53 @@ class BBPEConfig:
|
|
| 54 |
return cls(**data)
|
| 55 |
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
class EthioBBPETrainer:
|
| 58 |
"""
|
| 59 |
-
Production-ready trainer for Byte-Level BPE with
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
"""
|
| 61 |
|
| 62 |
-
def __init__(self, config: BBPEConfig
|
| 63 |
-
self.config = config
|
| 64 |
-
self.output_dir = Path(
|
| 65 |
-
self.checkpoint_dir = self.
|
| 66 |
self.tokenizer = None
|
| 67 |
self.is_trained = False
|
|
|
|
|
|
|
| 68 |
|
| 69 |
# Create directories
|
| 70 |
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 71 |
self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
| 72 |
|
| 73 |
logger.info(f"Initialized EthioBBPETrainer with output dir: {self.output_dir}")
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
def _initialize_tokenizer(self):
|
| 76 |
"""Initialize the ByteLevelBPETokenizer."""
|
|
@@ -81,19 +140,38 @@ class EthioBBPETrainer:
|
|
| 81 |
)
|
| 82 |
logger.info("Tokenizer initialized")
|
| 83 |
|
| 84 |
-
def train(self, files: Union[str, List[str]], use_checkpoint: bool =
|
| 85 |
"""
|
| 86 |
Train the tokenizer on a list of files or a directory.
|
| 87 |
|
| 88 |
Args:
|
| 89 |
files: Path to a file, list of files, or directory containing text files.
|
|
|
|
| 90 |
use_checkpoint: If True, attempts to resume from the latest checkpoint.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
"""
|
|
|
|
|
|
|
| 92 |
if self.tokenizer is None:
|
| 93 |
self._initialize_tokenizer()
|
| 94 |
|
|
|
|
|
|
|
|
|
|
| 95 |
# Resolve file paths
|
| 96 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
path = Path(files)
|
| 98 |
if path.is_dir():
|
| 99 |
file_paths = [str(f) for f in path.glob("**/*.txt")]
|
|
@@ -108,43 +186,60 @@ class EthioBBPETrainer:
|
|
| 108 |
raise ValueError("No valid training files found.")
|
| 109 |
|
| 110 |
logger.info(f"Found {len(file_paths)} files for training.")
|
|
|
|
| 111 |
|
| 112 |
# Checkpoint logic
|
| 113 |
start_from_scratch = True
|
|
|
|
| 114 |
if use_checkpoint:
|
| 115 |
latest_ckpt = self._get_latest_checkpoint()
|
| 116 |
if latest_ckpt:
|
| 117 |
logger.info(f"Resuming from checkpoint: {latest_ckpt}")
|
| 118 |
-
self.tokenizer =
|
| 119 |
start_from_scratch = False
|
|
|
|
| 120 |
else:
|
| 121 |
logger.info("No checkpoint found. Starting from scratch.")
|
| 122 |
|
| 123 |
-
#
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
vocab_size=self.config.vocab_size,
|
| 126 |
min_frequency=self.config.min_frequency,
|
| 127 |
special_tokens=self.config.special_tokens,
|
| 128 |
-
show_progress=self.config.show_progress
|
| 129 |
-
initial_alphabet=ByteLevelBPETokenizer.alphabet()
|
| 130 |
)
|
| 131 |
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
self.tokenizer.train(files=file_paths, trainer=trainer)
|
| 136 |
-
else:
|
| 137 |
-
# Note: HuggingFace tokenizers library doesn't natively support
|
| 138 |
-
# resuming BPE merge training mid-process easily without custom C++ extensions.
|
| 139 |
-
# The checkpoint here primarily saves the state before finalization or
|
| 140 |
-
# allows saving intermediate vocabularies if implemented in batches.
|
| 141 |
-
# For this production version, we treat checkpoints as safety saves of the
|
| 142 |
-
# current state before heavy operations or as versioned releases.
|
| 143 |
-
self.tokenizer.train(files=file_paths, trainer=trainer)
|
| 144 |
-
|
| 145 |
self.is_trained = True
|
| 146 |
logger.info("Training completed successfully.")
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
# Auto-save checkpoint after training
|
| 149 |
self._save_checkpoint("final_pre_compress")
|
| 150 |
|
|
@@ -159,72 +254,278 @@ class EthioBBPETrainer:
|
|
| 159 |
ckpts.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
| 160 |
return ckpts[0]
|
| 161 |
|
| 162 |
-
def _save_checkpoint(self, name: str = "latest"):
|
| 163 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
if self.tokenizer is None:
|
| 165 |
return
|
|
|
|
| 166 |
ckpt_path = self.checkpoint_dir / f"checkpoint_{name}.json"
|
| 167 |
self.tokenizer.save(str(ckpt_path))
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
"""
|
| 172 |
-
Save the trained tokenizer.
|
| 173 |
|
| 174 |
Args:
|
| 175 |
-
model_name: Name of the model folder.
|
| 176 |
-
compress: If True, saves vocab
|
| 177 |
Defaults to config.save_compressed.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
"""
|
| 179 |
if not self.is_trained and self.tokenizer is None:
|
| 180 |
raise RuntimeError("Tokenizer not trained yet.")
|
| 181 |
|
|
|
|
| 182 |
compress = compress if compress is not None else self.config.save_compressed
|
|
|
|
|
|
|
| 183 |
model_path = self.output_dir / model_name
|
| 184 |
model_path.mkdir(parents=True, exist_ok=True)
|
| 185 |
|
| 186 |
-
logger.info(f"Saving model to {model_path} (compressed={compress})...")
|
| 187 |
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
tokenizer_file = model_path / "tokenizer.json"
|
| 191 |
self.tokenizer.save(str(tokenizer_file))
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
for pair in merges:
|
| 206 |
-
f.write(f"{pair[0]} {pair[1]}\n")
|
| 207 |
-
|
| 208 |
-
logger.info(f"Compressed artifacts saved: {vocab_path}, {merges_path}")
|
| 209 |
-
|
| 210 |
-
# Calculate savings
|
| 211 |
-
original_size = sum(f.stat().st_size for f in [tokenizer_file])
|
| 212 |
-
compressed_size = sum(f.stat().st_size for f in [vocab_path, merges_path])
|
| 213 |
-
logger.info(f"Storage saved: {(original_size - compressed_size) / 1024:.2f} KB")
|
| 214 |
-
else:
|
| 215 |
-
# Standard save
|
| 216 |
-
self.tokenizer.save(str(model_path / "tokenizer.json"))
|
| 217 |
-
self.tokenizer.model.save(str(model_path))
|
| 218 |
-
logger.info("Standard model artifacts saved.")
|
| 219 |
|
| 220 |
# Save config
|
| 221 |
self.config.save(str(model_path / "config.json"))
|
| 222 |
|
|
|
|
|
|
|
|
|
|
| 223 |
# Save metadata card for Hugging Face
|
| 224 |
self._save_model_card(model_path)
|
| 225 |
|
|
|
|
|
|
|
|
|
|
| 226 |
logger.info(f"Model successfully saved to {model_path}")
|
| 227 |
return model_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
def _save_model_card(self, path: Path):
|
| 230 |
"""Generate and save a README.md for Hugging Face Hub."""
|
|
@@ -243,12 +544,15 @@ datasets:
|
|
| 243 |
|
| 244 |
# EthioBBPE Tokenizer
|
| 245 |
|
| 246 |
-
This is a production-ready Byte-Level BPE tokenizer
|
| 247 |
|
| 248 |
## Features
|
| 249 |
- **Byte-Level**: Handles any Unicode character without <UNK>.
|
| 250 |
-
- **
|
| 251 |
-
- **Checkpointing**: Built-in safety checkpoints
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
## Usage
|
| 254 |
|
|
@@ -266,10 +570,35 @@ from tokenizers import Tokenizer
|
|
| 266 |
tokenizer = Tokenizer.from_file("tokenizer.json")
|
| 267 |
```
|
| 268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
## Training Configuration
|
| 270 |
```json
|
| 271 |
{json.dumps(asdict(self.config), indent=2)}
|
| 272 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
"""
|
| 274 |
with open(path / "README.md", 'w', encoding='utf-8') as f:
|
| 275 |
f.write(card_content)
|
|
@@ -288,3 +617,118 @@ tokenizer = Tokenizer.from_file("tokenizer.json")
|
|
| 288 |
if self.tokenizer is None:
|
| 289 |
raise RuntimeError("Tokenizer not initialized")
|
| 290 |
return self.tokenizer.decode(ids)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
EthioBBPE: Production-Ready Byte-Level BPE Tokenizer Trainer
|
| 4 |
+
Advanced Features:
|
| 5 |
+
- Checkpointing with metadata and resume capability
|
| 6 |
+
- Multi-format compression (gzip, lzma, bz2)
|
| 7 |
+
- Model quantization for deployment
|
| 8 |
+
- Training metrics tracking
|
| 9 |
+
- Automatic backups
|
| 10 |
+
- Model validation utilities
|
| 11 |
+
- Multiple export formats
|
| 12 |
"""
|
| 13 |
|
| 14 |
import os
|
| 15 |
import json
|
| 16 |
import gzip
|
| 17 |
+
import bz2
|
| 18 |
+
import lzma
|
| 19 |
import shutil
|
| 20 |
+
import hashlib
|
| 21 |
import logging
|
| 22 |
from pathlib import Path
|
| 23 |
+
from typing import List, Optional, Union, Dict, Any, Tuple
|
| 24 |
from dataclasses import dataclass, field, asdict
|
| 25 |
from datetime import datetime
|
| 26 |
+
import tempfile
|
| 27 |
+
import struct
|
| 28 |
|
| 29 |
+
from tokenizers import ByteLevelBPETokenizer, trainers, Tokenizer
|
| 30 |
from tokenizers.implementations import BaseTokenizer
|
| 31 |
|
| 32 |
# Configure logging
|
|
|
|
| 47 |
lowercase: bool = False
|
| 48 |
dropout: Optional[float] = None
|
| 49 |
|
| 50 |
+
# File paths
|
| 51 |
+
data_dir: str = "./data"
|
| 52 |
+
model_save_dir: str = "./models"
|
| 53 |
+
model_name: str = "EthioBBPE"
|
| 54 |
+
|
| 55 |
# Advanced features
|
| 56 |
+
use_checkpoint: bool = True
|
| 57 |
+
checkpoint_dir: str = "./models/checkpoints"
|
| 58 |
save_compressed: bool = True
|
| 59 |
+
compression_format: str = "gzip" # Options: gzip, bz2, lzma
|
| 60 |
+
compression_level: int = 9 # 1-9, higher = better compression but slower
|
| 61 |
+
checkpoint_steps: Optional[int] = None
|
| 62 |
num_threads: int = -1 # -1 for auto
|
| 63 |
+
|
| 64 |
+
# Backup and versioning
|
| 65 |
+
enable_backup: bool = True
|
| 66 |
+
max_checkpoints: int = 5 # Keep only last N checkpoints
|
| 67 |
+
|
| 68 |
+
# Quantization (for deployment optimization)
|
| 69 |
+
enable_quantization: bool = False
|
| 70 |
+
quantization_bits: int = 8 # 8-bit or 4-bit quantization
|
| 71 |
+
|
| 72 |
def save(self, path: str):
|
| 73 |
"""Save configuration to JSON."""
|
| 74 |
with open(path, 'w', encoding='utf-8') as f:
|
|
|
|
| 83 |
return cls(**data)
|
| 84 |
|
| 85 |
|
| 86 |
+
@dataclass
|
| 87 |
+
class CheckpointMetadata:
|
| 88 |
+
"""Metadata for checkpoint management."""
|
| 89 |
+
checkpoint_id: str
|
| 90 |
+
timestamp: str
|
| 91 |
+
vocab_size: int
|
| 92 |
+
training_step: int
|
| 93 |
+
is_final: bool
|
| 94 |
+
checksum: str
|
| 95 |
+
config: Dict[str, Any]
|
| 96 |
+
metrics: Optional[Dict[str, float]] = None
|
| 97 |
+
|
| 98 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 99 |
+
return asdict(self)
|
| 100 |
+
|
| 101 |
+
@classmethod
|
| 102 |
+
def from_dict(cls, data: Dict[str, Any]) -> "CheckpointMetadata":
|
| 103 |
+
return cls(**data)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
class EthioBBPETrainer:
|
| 107 |
"""
|
| 108 |
+
Production-ready trainer for Byte-Level BPE with advanced features:
|
| 109 |
+
- Checkpointing with metadata tracking
|
| 110 |
+
- Multi-format compression (gzip, bz2, lzma)
|
| 111 |
+
- Model quantization for deployment
|
| 112 |
+
- Automatic backup management
|
| 113 |
+
- Training metrics collection
|
| 114 |
"""
|
| 115 |
|
| 116 |
+
def __init__(self, config: BBPEConfig = None):
|
| 117 |
+
self.config = config or BBPEConfig()
|
| 118 |
+
self.output_dir = Path(self.config.model_save_dir)
|
| 119 |
+
self.checkpoint_dir = Path(self.config.checkpoint_dir)
|
| 120 |
self.tokenizer = None
|
| 121 |
self.is_trained = False
|
| 122 |
+
self.training_metrics = {}
|
| 123 |
+
self.start_time = None
|
| 124 |
|
| 125 |
# Create directories
|
| 126 |
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 127 |
self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
| 128 |
|
| 129 |
logger.info(f"Initialized EthioBBPETrainer with output dir: {self.output_dir}")
|
| 130 |
+
logger.info(f"Compression format: {self.config.compression_format}, Level: {self.config.compression_level}")
|
| 131 |
+
if self.config.enable_quantization:
|
| 132 |
+
logger.info(f"Quantization enabled: {self.config.quantization_bits}-bit")
|
| 133 |
|
| 134 |
def _initialize_tokenizer(self):
|
| 135 |
"""Initialize the ByteLevelBPETokenizer."""
|
|
|
|
| 140 |
)
|
| 141 |
logger.info("Tokenizer initialized")
|
| 142 |
|
| 143 |
+
def train(self, files: Union[str, List[str]] = None, use_checkpoint: bool = None):
|
| 144 |
"""
|
| 145 |
Train the tokenizer on a list of files or a directory.
|
| 146 |
|
| 147 |
Args:
|
| 148 |
files: Path to a file, list of files, or directory containing text files.
|
| 149 |
+
If None, uses files from config.data_dir
|
| 150 |
use_checkpoint: If True, attempts to resume from the latest checkpoint.
|
| 151 |
+
Defaults to config.use_checkpoint
|
| 152 |
+
|
| 153 |
+
Returns:
|
| 154 |
+
Trained tokenizer object
|
| 155 |
"""
|
| 156 |
+
self.start_time = datetime.now()
|
| 157 |
+
|
| 158 |
if self.tokenizer is None:
|
| 159 |
self._initialize_tokenizer()
|
| 160 |
|
| 161 |
+
# Use config default if not specified
|
| 162 |
+
use_checkpoint = use_checkpoint if use_checkpoint is not None else self.config.use_checkpoint
|
| 163 |
+
|
| 164 |
# Resolve file paths
|
| 165 |
+
if files is None:
|
| 166 |
+
# Use data_dir from config
|
| 167 |
+
data_path = Path(self.config.data_dir)
|
| 168 |
+
if data_path.is_dir():
|
| 169 |
+
file_paths = [str(f) for f in data_path.glob("**/*.txt")]
|
| 170 |
+
file_paths.extend([str(f) for f in data_path.glob("**/*.jsonl")])
|
| 171 |
+
file_paths.extend([str(f) for f in data_path.glob("**/*.json")])
|
| 172 |
+
else:
|
| 173 |
+
raise FileNotFoundError(f"Data directory not found: {self.config.data_dir}")
|
| 174 |
+
elif isinstance(files, str):
|
| 175 |
path = Path(files)
|
| 176 |
if path.is_dir():
|
| 177 |
file_paths = [str(f) for f in path.glob("**/*.txt")]
|
|
|
|
| 186 |
raise ValueError("No valid training files found.")
|
| 187 |
|
| 188 |
logger.info(f"Found {len(file_paths)} files for training.")
|
| 189 |
+
logger.info(f"Training started at: {self.start_time}")
|
| 190 |
|
| 191 |
# Checkpoint logic
|
| 192 |
start_from_scratch = True
|
| 193 |
+
resumed_from = None
|
| 194 |
if use_checkpoint:
|
| 195 |
latest_ckpt = self._get_latest_checkpoint()
|
| 196 |
if latest_ckpt:
|
| 197 |
logger.info(f"Resuming from checkpoint: {latest_ckpt}")
|
| 198 |
+
self.tokenizer = Tokenizer.from_file(str(latest_ckpt))
|
| 199 |
start_from_scratch = False
|
| 200 |
+
resumed_from = str(latest_ckpt)
|
| 201 |
else:
|
| 202 |
logger.info("No checkpoint found. Starting from scratch.")
|
| 203 |
|
| 204 |
+
# Calculate initial metrics
|
| 205 |
+
total_files = len(file_paths)
|
| 206 |
+
total_size = sum(Path(f).stat().st_size for f in file_paths)
|
| 207 |
+
self.training_metrics['initial'] = {
|
| 208 |
+
'num_files': total_files,
|
| 209 |
+
'total_bytes': total_size,
|
| 210 |
+
'resumed_from': resumed_from
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
# Train
|
| 214 |
+
logger.info("Starting training...")
|
| 215 |
+
train_start = datetime.now()
|
| 216 |
+
|
| 217 |
+
# ByteLevelBPETokenizer.train() accepts parameters directly, not a trainer object
|
| 218 |
+
self.tokenizer.train(
|
| 219 |
+
files=file_paths,
|
| 220 |
vocab_size=self.config.vocab_size,
|
| 221 |
min_frequency=self.config.min_frequency,
|
| 222 |
special_tokens=self.config.special_tokens,
|
| 223 |
+
show_progress=self.config.show_progress
|
|
|
|
| 224 |
)
|
| 225 |
|
| 226 |
+
train_end = datetime.now()
|
| 227 |
+
train_duration = (train_end - train_start).total_seconds()
|
| 228 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
self.is_trained = True
|
| 230 |
logger.info("Training completed successfully.")
|
| 231 |
|
| 232 |
+
# Record final metrics
|
| 233 |
+
vocab = self.tokenizer.get_vocab()
|
| 234 |
+
self.training_metrics['final'] = {
|
| 235 |
+
'vocab_size': len(vocab),
|
| 236 |
+
'training_duration_sec': train_duration,
|
| 237 |
+
'end_time': train_end.isoformat()
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
logger.info(f"Final vocabulary size: {len(vocab)}")
|
| 241 |
+
logger.info(f"Training duration: {train_duration:.2f} seconds")
|
| 242 |
+
|
| 243 |
# Auto-save checkpoint after training
|
| 244 |
self._save_checkpoint("final_pre_compress")
|
| 245 |
|
|
|
|
| 254 |
ckpts.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
| 255 |
return ckpts[0]
|
| 256 |
|
| 257 |
+
def _save_checkpoint(self, name: str = "latest", save_metadata: bool = True):
|
| 258 |
+
"""
|
| 259 |
+
Save current tokenizer state to checkpoint with metadata.
|
| 260 |
+
|
| 261 |
+
Args:
|
| 262 |
+
name: Checkpoint name identifier
|
| 263 |
+
save_metadata: Whether to save metadata JSON alongside checkpoint
|
| 264 |
+
"""
|
| 265 |
if self.tokenizer is None:
|
| 266 |
return
|
| 267 |
+
|
| 268 |
ckpt_path = self.checkpoint_dir / f"checkpoint_{name}.json"
|
| 269 |
self.tokenizer.save(str(ckpt_path))
|
| 270 |
+
|
| 271 |
+
# Calculate checksum for integrity verification
|
| 272 |
+
checksum = self._calculate_file_checksum(ckpt_path)
|
| 273 |
+
|
| 274 |
+
# Save metadata if requested
|
| 275 |
+
if save_metadata:
|
| 276 |
+
vocab = self.tokenizer.get_vocab()
|
| 277 |
+
metadata = CheckpointMetadata(
|
| 278 |
+
checkpoint_id=f"{name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
|
| 279 |
+
timestamp=datetime.now().isoformat(),
|
| 280 |
+
vocab_size=len(vocab),
|
| 281 |
+
training_step=-1, # Not used in HF tokenizers but kept for compatibility
|
| 282 |
+
is_final=(name == "final_pre_compress"),
|
| 283 |
+
checksum=checksum,
|
| 284 |
+
config=asdict(self.config),
|
| 285 |
+
metrics=self.training_metrics.copy()
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
metadata_path = self.checkpoint_dir / f"checkpoint_{name}_metadata.json"
|
| 289 |
+
with open(metadata_path, 'w', encoding='utf-8') as f:
|
| 290 |
+
json.dump(metadata.to_dict(), f, indent=2)
|
| 291 |
+
logger.info(f"Checkpoint metadata saved to {metadata_path}")
|
| 292 |
+
|
| 293 |
+
# Manage checkpoint rotation (keep only last N checkpoints)
|
| 294 |
+
if self.config.enable_backup and self.config.max_checkpoints > 0:
|
| 295 |
+
self._rotate_checkpoints()
|
| 296 |
+
|
| 297 |
+
logger.info(f"Checkpoint saved to {ckpt_path} (checksum: {checksum[:8]}...)")
|
| 298 |
+
|
| 299 |
+
def _calculate_file_checksum(self, filepath: Path) -> str:
|
| 300 |
+
"""Calculate SHA256 checksum of a file."""
|
| 301 |
+
sha256_hash = hashlib.sha256()
|
| 302 |
+
with open(filepath, "rb") as f:
|
| 303 |
+
for byte_block in iter(lambda: f.read(4096), b""):
|
| 304 |
+
sha256_hash.update(byte_block)
|
| 305 |
+
return sha256_hash.hexdigest()
|
| 306 |
+
|
| 307 |
+
def _rotate_checkpoints(self):
|
| 308 |
+
"""Keep only the most recent checkpoints based on max_checkpoints config."""
|
| 309 |
+
ckpts = list(self.checkpoint_dir.glob("checkpoint_*.json"))
|
| 310 |
+
# Exclude metadata files
|
| 311 |
+
ckpts = [c for c in ckpts if "_metadata" not in c.name]
|
| 312 |
+
|
| 313 |
+
if len(ckpts) > self.config.max_checkpoints:
|
| 314 |
+
# Sort by modification time, oldest first
|
| 315 |
+
ckpts.sort(key=lambda p: p.stat().st_mtime)
|
| 316 |
+
# Remove oldest checkpoints
|
| 317 |
+
num_to_remove = len(ckpts) - self.config.max_checkpoints
|
| 318 |
+
for i in range(num_to_remove):
|
| 319 |
+
old_ckpt = ckpts[i]
|
| 320 |
+
old_metadata = old_ckpt.parent / f"{old_ckpt.stem}_metadata.json"
|
| 321 |
+
|
| 322 |
+
try:
|
| 323 |
+
old_ckpt.unlink()
|
| 324 |
+
logger.info(f"Removed old checkpoint: {old_ckpt.name}")
|
| 325 |
+
if old_metadata.exists():
|
| 326 |
+
old_metadata.unlink()
|
| 327 |
+
logger.info(f"Removed old metadata: {old_metadata.name}")
|
| 328 |
+
except Exception as e:
|
| 329 |
+
logger.warning(f"Failed to remove old checkpoint {old_ckpt}: {e}")
|
| 330 |
+
|
| 331 |
+
def save(self, model_name: str = None, compress: bool = None,
|
| 332 |
+
compression_format: str = None, export_formats: List[str] = None):
|
| 333 |
"""
|
| 334 |
+
Save the trained tokenizer with advanced options.
|
| 335 |
|
| 336 |
Args:
|
| 337 |
+
model_name: Name of the model folder. Defaults to config.model_name
|
| 338 |
+
compress: If True, saves vocab in compressed format.
|
| 339 |
Defaults to config.save_compressed.
|
| 340 |
+
compression_format: Format for compression ('gzip', 'bz2', 'lzma').
|
| 341 |
+
Defaults to config.compression_format.
|
| 342 |
+
export_formats: List of export formats to generate.
|
| 343 |
+
Options: ['tokenizer.json', 'vocab_compressed', 'quantized', 'hf_export']
|
| 344 |
+
Defaults to all available formats.
|
| 345 |
+
|
| 346 |
+
Returns:
|
| 347 |
+
Path to saved model directory
|
| 348 |
"""
|
| 349 |
if not self.is_trained and self.tokenizer is None:
|
| 350 |
raise RuntimeError("Tokenizer not trained yet.")
|
| 351 |
|
| 352 |
+
model_name = model_name or self.config.model_name
|
| 353 |
compress = compress if compress is not None else self.config.save_compressed
|
| 354 |
+
compression_format = compression_format or self.config.compression_format
|
| 355 |
+
|
| 356 |
model_path = self.output_dir / model_name
|
| 357 |
model_path.mkdir(parents=True, exist_ok=True)
|
| 358 |
|
| 359 |
+
logger.info(f"Saving model to {model_path} (compressed={compress}, format={compression_format})...")
|
| 360 |
|
| 361 |
+
# Determine which formats to export
|
| 362 |
+
if export_formats is None:
|
| 363 |
+
export_formats = ['tokenizer.json', 'vocab_compressed', 'hf_export']
|
| 364 |
+
|
| 365 |
+
# Always save standard tokenizer.json (required for HF loading)
|
| 366 |
+
if 'tokenizer.json' in export_formats:
|
| 367 |
tokenizer_file = model_path / "tokenizer.json"
|
| 368 |
self.tokenizer.save(str(tokenizer_file))
|
| 369 |
+
logger.info(f"Standard tokenizer.json saved: {tokenizer_file}")
|
| 370 |
+
|
| 371 |
+
# Save compressed vocab if requested
|
| 372 |
+
if compress and 'vocab_compressed' in export_formats:
|
| 373 |
+
self._save_compressed_vocab(model_path, compression_format)
|
| 374 |
+
|
| 375 |
+
# Save quantized version if enabled
|
| 376 |
+
if self.config.enable_quantization and 'quantized' in export_formats:
|
| 377 |
+
self._save_quantized_model(model_path)
|
| 378 |
+
|
| 379 |
+
# Generate Hugging Face export files
|
| 380 |
+
if 'hf_export' in export_formats:
|
| 381 |
+
self._save_hf_export_files(model_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
|
| 383 |
# Save config
|
| 384 |
self.config.save(str(model_path / "config.json"))
|
| 385 |
|
| 386 |
+
# Save training metrics
|
| 387 |
+
self._save_training_metrics(model_path)
|
| 388 |
+
|
| 389 |
# Save metadata card for Hugging Face
|
| 390 |
self._save_model_card(model_path)
|
| 391 |
|
| 392 |
+
# Log size comparison
|
| 393 |
+
self._log_size_comparison(model_path)
|
| 394 |
+
|
| 395 |
logger.info(f"Model successfully saved to {model_path}")
|
| 396 |
return model_path
|
| 397 |
+
|
| 398 |
+
def _get_compression_handler(self, format: str):
|
| 399 |
+
"""Get the appropriate compression handler based on format."""
|
| 400 |
+
handlers = {
|
| 401 |
+
'gzip': (gzip.open, '.gz', 'wt'),
|
| 402 |
+
'bz2': (bz2.open, '.bz2', 'wt'),
|
| 403 |
+
'lzma': (lzma.open, '.xz', 'wt')
|
| 404 |
+
}
|
| 405 |
+
if format not in handlers:
|
| 406 |
+
logger.warning(f"Unknown compression format '{format}', falling back to gzip")
|
| 407 |
+
format = 'gzip'
|
| 408 |
+
return handlers[format]
|
| 409 |
+
|
| 410 |
+
def _save_compressed_vocab(self, model_path: Path, format: str = 'gzip'):
|
| 411 |
+
"""Save vocabulary in compressed format."""
|
| 412 |
+
vocab = self.tokenizer.get_vocab()
|
| 413 |
+
open_func, ext, mode = self._get_compression_handler(format)
|
| 414 |
+
|
| 415 |
+
vocab_path = model_path / f"vocab.json{ext}"
|
| 416 |
+
|
| 417 |
+
# Use compression level from config
|
| 418 |
+
if format == 'gzip':
|
| 419 |
+
with open_func(vocab_path, mode, encoding='utf-8', compresslevel=self.config.compression_level) as f:
|
| 420 |
+
json.dump(vocab, f)
|
| 421 |
+
elif format == 'bz2':
|
| 422 |
+
with open_func(vocab_path, mode, encoding='utf-8') as f:
|
| 423 |
+
json.dump(vocab, f)
|
| 424 |
+
elif format == 'lzma':
|
| 425 |
+
with open_func(vocab_path, mode, encoding='utf-8', preset=self.config.compression_level) as f:
|
| 426 |
+
json.dump(vocab, f)
|
| 427 |
+
|
| 428 |
+
logger.info(f"Compressed vocab saved ({format}): {vocab_path}")
|
| 429 |
+
|
| 430 |
+
# Calculate and log compression ratio
|
| 431 |
+
original_size = sum(len(k) + len(str(v)) for k, v in vocab.items())
|
| 432 |
+
compressed_size = vocab_path.stat().st_size
|
| 433 |
+
ratio = (1 - compressed_size / (original_size * 2)) * 100 if original_size > 0 else 0
|
| 434 |
+
logger.info(f"Compression ratio: {ratio:.1f}% (original ~{original_size*2} bytes -> {compressed_size} bytes)")
|
| 435 |
+
|
| 436 |
+
def _save_quantized_model(self, model_path: Path):
|
| 437 |
+
"""Save a quantized version of the tokenizer for deployment."""
|
| 438 |
+
vocab = self.tokenizer.get_vocab()
|
| 439 |
+
|
| 440 |
+
# Create quantized vocabulary mapping
|
| 441 |
+
if self.config.quantization_bits == 8:
|
| 442 |
+
# 8-bit quantization: map tokens to uint8 range
|
| 443 |
+
dtype = 'uint8'
|
| 444 |
+
max_val = 255
|
| 445 |
+
elif self.config.quantization_bits == 4:
|
| 446 |
+
# 4-bit quantization: pack two tokens per byte
|
| 447 |
+
dtype = 'uint4'
|
| 448 |
+
max_val = 15
|
| 449 |
+
else:
|
| 450 |
+
logger.warning(f"Unsupported quantization bits: {self.config.quantization_bits}, using 8-bit")
|
| 451 |
+
dtype = 'uint8'
|
| 452 |
+
max_val = 255
|
| 453 |
+
|
| 454 |
+
# Sort vocab by ID for consistent ordering
|
| 455 |
+
sorted_vocab = sorted(vocab.items(), key=lambda x: x[1])
|
| 456 |
+
|
| 457 |
+
# Create quantized lookup table
|
| 458 |
+
quantized_data = {
|
| 459 |
+
'vocab_size': len(sorted_vocab),
|
| 460 |
+
'quantization_bits': self.config.quantization_bits,
|
| 461 |
+
'dtype': dtype,
|
| 462 |
+
'tokens': [token for token, _ in sorted_vocab],
|
| 463 |
+
'ids': [idx for _, idx in sorted_vocab]
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
# Save quantized model
|
| 467 |
+
quantized_path = model_path / f"tokenizer_quantized_{self.config.quantization_bits}bit.json.gz"
|
| 468 |
+
with gzip.open(quantized_path, 'wt', encoding='utf-8', compresslevel=9) as f:
|
| 469 |
+
json.dump(quantized_data, f)
|
| 470 |
+
|
| 471 |
+
logger.info(f"Quantized model saved ({self.config.quantization_bits}-bit): {quantized_path}")
|
| 472 |
+
|
| 473 |
+
def _save_hf_export_files(self, model_path: Path):
|
| 474 |
+
"""Save files needed for Hugging Face Hub export."""
|
| 475 |
+
# Save merges.txt if available (for BPE models)
|
| 476 |
+
try:
|
| 477 |
+
merges = self.tokenizer.model.get_merges()
|
| 478 |
+
if merges:
|
| 479 |
+
merges_path = model_path / "merges.txt"
|
| 480 |
+
with open(merges_path, 'w', encoding='utf-8') as f:
|
| 481 |
+
f.write("#version: 0.2\n")
|
| 482 |
+
for merge in merges:
|
| 483 |
+
f.write(f"{merge[0]} {merge[1]}\n")
|
| 484 |
+
logger.info(f"Merges file saved: {merges_path}")
|
| 485 |
+
except Exception as e:
|
| 486 |
+
logger.debug(f"No merges available or error saving: {e}")
|
| 487 |
+
|
| 488 |
+
# Save special tokens mapping
|
| 489 |
+
special_tokens_path = model_path / "special_tokens_map.json"
|
| 490 |
+
special_tokens = {
|
| 491 |
+
"unk_token": "<unk>",
|
| 492 |
+
"pad_token": "<pad>",
|
| 493 |
+
"bos_token": "<s>",
|
| 494 |
+
"eos_token": "</s>"
|
| 495 |
+
}
|
| 496 |
+
with open(special_tokens_path, 'w', encoding='utf-8') as f:
|
| 497 |
+
json.dump(special_tokens, f, indent=2)
|
| 498 |
+
logger.info(f"Special tokens map saved: {special_tokens_path}")
|
| 499 |
+
|
| 500 |
+
def _save_training_metrics(self, model_path: Path):
|
| 501 |
+
"""Save training metrics to JSON."""
|
| 502 |
+
metrics_path = model_path / "training_metrics.json"
|
| 503 |
+
|
| 504 |
+
# Add additional metrics
|
| 505 |
+
full_metrics = {
|
| 506 |
+
**self.training_metrics,
|
| 507 |
+
'config': asdict(self.config),
|
| 508 |
+
'saved_at': datetime.now().isoformat()
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
with open(metrics_path, 'w', encoding='utf-8') as f:
|
| 512 |
+
json.dump(full_metrics, f, indent=2)
|
| 513 |
+
|
| 514 |
+
logger.info(f"Training metrics saved: {metrics_path}")
|
| 515 |
+
|
| 516 |
+
def _log_size_comparison(self, model_path: Path):
|
| 517 |
+
"""Log size comparison between different saved formats."""
|
| 518 |
+
files_info = []
|
| 519 |
+
for f in model_path.iterdir():
|
| 520 |
+
if f.is_file():
|
| 521 |
+
size_kb = f.stat().st_size / 1024
|
| 522 |
+
files_info.append((f.name, size_kb))
|
| 523 |
+
|
| 524 |
+
files_info.sort(key=lambda x: x[1])
|
| 525 |
+
|
| 526 |
+
logger.info("Model artifacts size summary:")
|
| 527 |
+
for name, size in files_info:
|
| 528 |
+
logger.info(f" {name}: {size:.2f} KB")
|
| 529 |
|
| 530 |
def _save_model_card(self, path: Path):
|
| 531 |
"""Generate and save a README.md for Hugging Face Hub."""
|
|
|
|
| 544 |
|
| 545 |
# EthioBBPE Tokenizer
|
| 546 |
|
| 547 |
+
This is a production-ready Byte-Level BPE tokenizer with advanced features for deployment.
|
| 548 |
|
| 549 |
## Features
|
| 550 |
- **Byte-Level**: Handles any Unicode character without <UNK>.
|
| 551 |
+
- **Multi-format Compression**: Supports gzip, bz2, and lzma compression.
|
| 552 |
+
- **Checkpointing**: Built-in safety checkpoints with metadata tracking.
|
| 553 |
+
- **Quantization**: Optional 8-bit/4-bit quantization for efficient deployment.
|
| 554 |
+
- **Training Metrics**: Comprehensive metrics tracking and logging.
|
| 555 |
+
- **Automatic Backup**: Checkpoint rotation to manage disk space.
|
| 556 |
|
| 557 |
## Usage
|
| 558 |
|
|
|
|
| 570 |
tokenizer = Tokenizer.from_file("tokenizer.json")
|
| 571 |
```
|
| 572 |
|
| 573 |
+
### Loading Compressed Vocab
|
| 574 |
+
```python
|
| 575 |
+
import gzip
|
| 576 |
+
import json
|
| 577 |
+
|
| 578 |
+
# Load compressed vocabulary
|
| 579 |
+
with gzip.open("vocab.json.gz", 'rt', encoding='utf-8') as f:
|
| 580 |
+
vocab = json.load(f)
|
| 581 |
+
```
|
| 582 |
+
|
| 583 |
## Training Configuration
|
| 584 |
```json
|
| 585 |
{json.dumps(asdict(self.config), indent=2)}
|
| 586 |
```
|
| 587 |
+
|
| 588 |
+
## Model Files
|
| 589 |
+
- `tokenizer.json`: Standard tokenizer file (required)
|
| 590 |
+
- `vocab.json.gz`: Compressed vocabulary (optional, smaller size)
|
| 591 |
+
- `config.json`: Training configuration
|
| 592 |
+
- `training_metrics.json`: Training statistics
|
| 593 |
+
- `special_tokens_map.json`: Special tokens mapping
|
| 594 |
+
- `README.md`: This file
|
| 595 |
+
|
| 596 |
+
## Checkpoints
|
| 597 |
+
Checkpoints are saved in the `{self.checkpoint_dir}` directory with metadata including:
|
| 598 |
+
- Checkpoint ID and timestamp
|
| 599 |
+
- Vocabulary size
|
| 600 |
+
- SHA256 checksum for integrity verification
|
| 601 |
+
- Training metrics at checkpoint time
|
| 602 |
"""
|
| 603 |
with open(path / "README.md", 'w', encoding='utf-8') as f:
|
| 604 |
f.write(card_content)
|
|
|
|
| 617 |
if self.tokenizer is None:
|
| 618 |
raise RuntimeError("Tokenizer not initialized")
|
| 619 |
return self.tokenizer.decode(ids)
|
| 620 |
+
|
| 621 |
+
def get_vocab_size(self) -> int:
|
| 622 |
+
"""Get the current vocabulary size."""
|
| 623 |
+
if self.tokenizer is None:
|
| 624 |
+
return 0
|
| 625 |
+
return len(self.tokenizer.get_vocab())
|
| 626 |
+
|
| 627 |
+
def get_training_metrics(self) -> Dict[str, Any]:
|
| 628 |
+
"""Get training metrics collected during training."""
|
| 629 |
+
return self.training_metrics.copy()
|
| 630 |
+
|
| 631 |
+
def validate_checkpoint(self, checkpoint_path: Union[str, Path]) -> bool:
|
| 632 |
+
"""
|
| 633 |
+
Validate checkpoint integrity using checksum.
|
| 634 |
+
|
| 635 |
+
Args:
|
| 636 |
+
checkpoint_path: Path to checkpoint JSON file
|
| 637 |
+
|
| 638 |
+
Returns:
|
| 639 |
+
True if valid, False otherwise
|
| 640 |
+
"""
|
| 641 |
+
checkpoint_path = Path(checkpoint_path)
|
| 642 |
+
metadata_path = checkpoint_path.parent / f"{checkpoint_path.stem}_metadata.json"
|
| 643 |
+
|
| 644 |
+
if not metadata_path.exists():
|
| 645 |
+
logger.warning(f"No metadata found for checkpoint: {checkpoint_path}")
|
| 646 |
+
return True # Can't validate without metadata
|
| 647 |
+
|
| 648 |
+
try:
|
| 649 |
+
with open(metadata_path, 'r', encoding='utf-8') as f:
|
| 650 |
+
metadata = json.load(f)
|
| 651 |
+
|
| 652 |
+
expected_checksum = metadata.get('checksum')
|
| 653 |
+
if not expected_checksum:
|
| 654 |
+
logger.warning("No checksum in metadata")
|
| 655 |
+
return True
|
| 656 |
+
|
| 657 |
+
actual_checksum = self._calculate_file_checksum(checkpoint_path)
|
| 658 |
+
is_valid = expected_checksum == actual_checksum
|
| 659 |
+
|
| 660 |
+
if is_valid:
|
| 661 |
+
logger.info(f"✓ Checkpoint validated: {checkpoint_path.name}")
|
| 662 |
+
else:
|
| 663 |
+
logger.error(f"✗ Checkpoint validation FAILED: {checkpoint_path.name}")
|
| 664 |
+
logger.error(f" Expected: {expected_checksum}")
|
| 665 |
+
logger.error(f" Actual: {actual_checksum}")
|
| 666 |
+
|
| 667 |
+
return is_valid
|
| 668 |
+
except Exception as e:
|
| 669 |
+
logger.error(f"Error validating checkpoint: {e}")
|
| 670 |
+
return False
|
| 671 |
+
|
| 672 |
+
def list_checkpoints(self) -> List[Dict[str, Any]]:
|
| 673 |
+
"""
|
| 674 |
+
List all available checkpoints with their metadata.
|
| 675 |
+
|
| 676 |
+
Returns:
|
| 677 |
+
List of checkpoint info dictionaries
|
| 678 |
+
"""
|
| 679 |
+
checkpoints = []
|
| 680 |
+
ckpt_files = sorted(self.checkpoint_dir.glob("checkpoint_*.json"))
|
| 681 |
+
|
| 682 |
+
for ckpt_file in ckpt_files:
|
| 683 |
+
if "_metadata" in ckpt_file.name:
|
| 684 |
+
continue
|
| 685 |
+
|
| 686 |
+
info = {
|
| 687 |
+
'path': str(ckpt_file),
|
| 688 |
+
'name': ckpt_file.name,
|
| 689 |
+
'size_kb': ckpt_file.stat().st_size / 1024,
|
| 690 |
+
'modified': datetime.fromtimestamp(ckpt_file.stat().st_mtime).isoformat()
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
# Try to load metadata
|
| 694 |
+
metadata_file = ckpt_file.parent / f"{ckpt_file.stem}_metadata.json"
|
| 695 |
+
if metadata_file.exists():
|
| 696 |
+
try:
|
| 697 |
+
with open(metadata_file, 'r', encoding='utf-8') as f:
|
| 698 |
+
metadata = json.load(f)
|
| 699 |
+
info['metadata'] = metadata
|
| 700 |
+
info['vocab_size'] = metadata.get('vocab_size', 'N/A')
|
| 701 |
+
info['is_final'] = metadata.get('is_final', False)
|
| 702 |
+
except Exception as e:
|
| 703 |
+
info['error'] = str(e)
|
| 704 |
+
|
| 705 |
+
checkpoints.append(info)
|
| 706 |
+
|
| 707 |
+
return checkpoints
|
| 708 |
+
|
| 709 |
+
@staticmethod
|
| 710 |
+
def load_compressed_vocab(vocab_path: Union[str, Path]) -> Dict[str, int]:
|
| 711 |
+
"""
|
| 712 |
+
Load vocabulary from a compressed file.
|
| 713 |
+
|
| 714 |
+
Args:
|
| 715 |
+
vocab_path: Path to compressed vocab file (.gz, .bz2, .xz)
|
| 716 |
+
|
| 717 |
+
Returns:
|
| 718 |
+
Vocabulary dictionary
|
| 719 |
+
"""
|
| 720 |
+
vocab_path = Path(vocab_path)
|
| 721 |
+
|
| 722 |
+
if vocab_path.suffix == '.gz':
|
| 723 |
+
with gzip.open(vocab_path, 'rt', encoding='utf-8') as f:
|
| 724 |
+
return json.load(f)
|
| 725 |
+
elif vocab_path.suffix == '.bz2':
|
| 726 |
+
with bz2.open(vocab_path, 'rt', encoding='utf-8') as f:
|
| 727 |
+
return json.load(f)
|
| 728 |
+
elif vocab_path.suffix in ['.xz', '.lzma']:
|
| 729 |
+
with lzma.open(vocab_path, 'rt', encoding='utf-8') as f:
|
| 730 |
+
return json.load(f)
|
| 731 |
+
else:
|
| 732 |
+
# Assume uncompressed JSON
|
| 733 |
+
with open(vocab_path, 'r', encoding='utf-8') as f:
|
| 734 |
+
return json.load(f)
|