Spaces:
Sleeping
Sleeping
File size: 19,706 Bytes
54ad1e5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 | from __future__ import annotations
import os
from pathlib import Path
import time
os.environ['KERAS_BACKEND'] = 'jax'
os.environ['XLA_PYTHON_CLIENT_PREALLOCATE'] = 'true'
os.environ['XLA_PYTHON_CLIENT_MEM_FRACTION'] = '0.90'
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
os.environ['PYTHONWARNINGS'] = 'ignore'
os.environ['TF_DATA_EXPERIMENTAL_SLACK'] = '1'
import numpy as np
import tensorflow as tf
import jax
import keras
from tokenizer import TokenizerWrapper
from veylon_model import create_llm
try:
from config import (EPOCHS, CONTEXT, vocab_size, D_MODEL, numberoflayers,
numberofheads, d_Latent, ffn_mult, swa_window,
num_kv_heads, learning_rate, weight_decay, batch_size,
data_path)
except Exception:
EPOCHS = 20
CONTEXT = 2048
vocab_size = 32000
D_MODEL = 512
numberoflayers = 8
numberofheads = 8
d_Latent = 128
ffn_mult = 3.5
swa_window = 1024
num_kv_heads = 2
learning_rate = 1e-4
weight_decay = 0.01
batch_size = 8
data_path = './training_data'
keras.mixed_precision.set_global_policy('mixed_bfloat16')
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PIPELINE
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def precompute_windows(tokens: np.ndarray, seq_len: int, stride: int) -> np.ndarray:
"""
Slice the token array into overlapping (seq_len+1) windows using
sliding_window_view, then stride-subsample.
sliding_window_view is preferred over as_strided: it validates bounds
internally and raises a clear error instead of silently reading garbage
memory past the array end.
The [::stride] slice is a zero-copy view. ascontiguousarray forces one
real allocation β a dense (N, seq_len+1) int32 buffer β which TensorFlow
can wrap without copying again.
Memory: for 5.7M tokens, CONTEXT=2048, stride=1024 β ~45 MB. Trivial
against the 42 GB host RAM on Colab/Kaggle TPU instances.
"""
window_len = seq_len + 1
view = np.lib.stride_tricks.sliding_window_view(tokens, window_len)[::stride]
if len(view) == 0:
raise ValueError(
f"Token array ({len(tokens):,}) is too short for "
f"seq_len={seq_len}, stride={stride}."
)
return np.ascontiguousarray(view, dtype=np.int32)
def _make_tf_data_options() -> tf.data.Options:
"""
Bundle all tf.data graph-level knobs in one place.
map_parallelization: lets the runtime fuse and parallelize map ops across
the thread pool automatically β no manual num_parallel_calls needed for
simple element-wise transforms.
parallel_batch: batching itself becomes a parallel operation; each batch
slot is filled concurrently instead of serially.
experimental_slack: introduces a one-step slack between the prefetch
stage and the training loop so the pipeline stays one batch ahead
without blocking on the next step. Redundant with the env var above
but belt-and-suspenders costs nothing.
"""
opts = tf.data.Options()
opts.experimental_optimization.map_parallelization = True
opts.experimental_optimization.parallel_batch = True
opts.experimental_slack = True
return opts
def create_tf_dataset(
windows: np.ndarray,
batch_size: int,
shuffle: bool = True,
shuffle_seed: int = 42,
cache_path: str = "", # "" β in-memory cache; "/tmp/..." β disk
) -> tf.data.Dataset:
"""
High-throughput tf.data pipeline.
Ordering rationale (each decision is load-bearing):
1. from_tensor_slices(windows)
TensorFlow wraps the numpy buffer directly β no tf.constant() copy.
Cardinality is exactly N (known), enabling optimal prefetch depth and
shuffle buffer auto-sizing downstream.
2. cache() β BEFORE shuffle and map
Caches individual (seq_len+1,) sequence tensors. On epoch 2+ the
entire dataset is served from RAM, eliminating all numpy I/O.
Caching before shuffle means the cache stores the canonical data once;
shuffle order changes every epoch without invalidating the cache.
Caching after batching would freeze batch composition forever, which
breaks per-epoch shuffle semantics.
3. shuffle() β AFTER cache, BEFORE repeat
Operates on individual sequences (correct granularity).
buffer=min(N, 20_000) bounds peak memory while covering the full
dataset for corpora this size.
4. repeat() β AFTER shuffle, BEFORE map/batch
Placed here so the shuffle buffer can draw across epoch boundaries,
preventing an artificial ordering reset at each epoch edge.
5. map(split_xy)
Slices each (seq_len+1,) window into x=(seq_len,) and y=(seq_len,).
No @tf.function decorator needed β tf.data traces the lambda itself.
num_parallel_calls=AUTOTUNE parallelises across the CPU thread pool.
6. batch(drop_remainder=True)
Static shape [batch_size, seq_len] β mandatory for XLA/TPU.
No symbolic/dynamic shapes ever reach the device.
7. prefetch(AUTOTUNE)
Keeps the device-transfer queue full. Always last so it buffers
fully-formed batches, not individual sequences.
8. with_options(...)
Applies graph-level optimizations in one shot after the pipeline is
fully defined.
"""
n = len(windows)
ds = tf.data.Dataset.from_tensor_slices(windows) # (1) wrap
ds = ds.cache(cache_path) # (2) cache
if shuffle:
buf = min(n, 20_000)
ds = ds.shuffle(buffer_size=buf, seed=shuffle_seed,
reshuffle_each_iteration=True) # (3) shuffle
ds = ds.repeat() # (4) repeat
ds = ds.map( # (5) split
lambda w: (w[:-1], w[1:]),
num_parallel_calls=tf.data.AUTOTUNE,
)
ds = ds.batch(batch_size, drop_remainder=True) # (6) batch
ds = ds.prefetch(tf.data.AUTOTUNE) # (7) prefetch
ds = ds.with_options(_make_tf_data_options()) # (8) opts
return ds
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# BENCHMARK
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def benchmark_dataset(
ds: tf.data.Dataset,
batch_size: int,
seq_len: int,
n_batches: int = 200,
label: str = "dataset",
) -> dict:
"""
Measure pipeline throughput independently of TPU compute.
We do NOT call .numpy() inside the loop. That forces a deviceβhost
transfer and a Python GIL acquisition on every batch, serialising what
should be a fully-async prefetch chain. Instead we consume the iterator
and let TensorFlow materialise tensors lazily. The end-to-end wall time
is what the TPU will actually experience.
Interpret results:
pipeline_tok/s > 2Γ tpu_tok/s β pipeline is not the bottleneck
pipeline_tok/s < 1.5Γ tpu_tok/s β still starving the TPU
"""
print(f"\n{'β'*52}")
print(f"Benchmarking: {label}")
print(f"{'β'*52}")
# Warm up: populate cache + prefetch buffers before timing starts.
for _ in ds.take(5):
pass
print("Warm-up (5 batches) complete.")
start = time.perf_counter()
for _ in ds.take(n_batches):
pass
elapsed = time.perf_counter() - start
tokens_per_batch = batch_size * seq_len
total_tokens = n_batches * tokens_per_batch
batches_sec = n_batches / elapsed
tokens_sec = total_tokens / elapsed
print(f"Batches measured : {n_batches}")
print(f"Elapsed : {elapsed:.2f} s")
print(f"Throughput : {batches_sec:.1f} batches/s")
print(f"Throughput : {tokens_sec:,.0f} tokens/s")
tpu_target = 30_000
headroom = tokens_sec / tpu_target
if headroom >= 2.0:
verdict = f"β {headroom:.1f}Γ headroom over TPU β pipeline is NOT the bottleneck."
elif headroom >= 1.2:
verdict = f"β {headroom:.1f}Γ headroom β marginal; increase batch_size or reduce stride."
else:
verdict = f"β {tokens_sec:,.0f} tok/s < TPU target {tpu_target:,} β still starving the TPU."
print(verdict)
print(f"{'β'*52}\n")
return {"batches_sec": batches_sec, "tokens_sec": tokens_sec,
"headroom_vs_30k": headroom}
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# THROUGHPUT CALLBACK
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class ThroughputCallback(keras.callbacks.Callback):
"""
Logs throughput every LOG_INTERVAL steps and at epoch end.
Why coarse logging matters on TPU:
Keras callbacks run on the Python host. JAX dispatches training steps
asynchronously β Python returns before XLA finishes the kernel. A
time.perf_counter() call on every step forces a host sync, equivalent
to inserting jax.block_until_ready() every step and destroying async
pipelining. At LOG_INTERVAL=50 only ~2% of steps incur this cost.
"""
LOG_INTERVAL = 50
def on_epoch_begin(self, epoch, logs=None):
self._epoch_start = time.perf_counter()
self._interval_start = self._epoch_start
self._step_count = 0
def on_train_batch_end(self, batch, logs=None):
self._step_count += 1
if batch > 0 and batch % self.LOG_INTERVAL == 0:
now = time.perf_counter()
elapsed = now - self._interval_start
tps = (self.LOG_INTERVAL * batch_size * CONTEXT) / elapsed
loss = logs.get('loss', float('nan'))
print(f" step {batch:5d} | loss {loss:.4f} | {tps:>10,.0f} tok/s")
self._interval_start = now
def on_epoch_end(self, epoch, logs=None):
elapsed = time.perf_counter() - self._epoch_start
total_tokens = self._step_count * batch_size * CONTEXT
val_loss = logs.get('val_loss', float('nan'))
print(f"\nEpoch {epoch+1} | {elapsed:.1f}s | "
f"{total_tokens / elapsed:,.0f} tok/s (avg) | "
f"val_loss={val_loss:.4f}\n")
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# HELPERS
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def load_text_corpus(path: str) -> str:
p = Path(path)
files = [p] if p.is_file() else sorted(p.glob('*.txt'))
if not files:
raise FileNotFoundError(f'No .txt files found in {path}')
return ''.join(fp.read_text(encoding='utf-8') for fp in files)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# MAIN
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class MemoryCallback(keras.callbacks.Callback):
def on_epoch_end(self, epoch, logs=None):
stats = jax.devices()[0].memory_stats()
used = stats["bytes_in_use"] / 1024**3
peak = stats["peak_bytes_in_use"] / 1024**3
limit = stats["bytes_limit"] / 1024**3
print(
f"\nHBM: {used:.2f}/{limit:.2f} GB "
f"(Peak: {peak:.2f} GB)"
)
def main():
print('\n' + '=' * 60)
print('BACKEND & DEVICE VERIFICATION')
print('=' * 60)
print(f'Keras backend : {keras.backend.backend()}')
print(f'JAX devices : {jax.devices()}')
print('=' * 60 + '\n')
# ββ Tokenize ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
tokenizer = TokenizerWrapper('tokenizer.model')
text = load_text_corpus(data_path)
raw_tokens = np.asarray(
tokenizer.encode(text, add_bos=True, add_eos=False), dtype=np.int32
)
print(f'Total tokens: {len(raw_tokens):,}')
split_idx = int(len(raw_tokens) * 0.98)
train_tokens = raw_tokens[:split_idx]
val_tokens = raw_tokens[split_idx:]
stride = max(1, CONTEXT // 2)
# ββ Pre-compute windows ββββββββββββββββββββββββββββββββββββββββββββββββ
print("Pre-computing windows...", end=" ", flush=True)
t0 = time.perf_counter()
train_windows = precompute_windows(train_tokens, CONTEXT, stride)
val_windows = precompute_windows(val_tokens, CONTEXT, stride)
print(f"done in {(time.perf_counter() - t0) * 1000:.1f} ms")
print(f" train: {train_windows.shape} ({train_windows.nbytes / 1e6:.1f} MB)")
print(f" val: {val_windows.shape} ({val_windows.nbytes / 1e6:.1f} MB)\n")
# ββ Build datasets βββββββββββββββββββββββββββββββββββββββββββββββββββββ
train_ds = create_tf_dataset(train_windows, batch_size, shuffle=True)
val_ds = create_tf_dataset(val_windows, batch_size, shuffle=False)
# ββ Benchmark before training ββββββββββββββββββββββββββββββββββββββββββ
# Verifies the pipeline can outpace the TPU before we waste a run.
benchmark_dataset(train_ds, batch_size, CONTEXT, n_batches=200,
label="train_ds")
# ββ Build model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
os.makedirs('checkpoints', exist_ok=True)
model = create_llm(
vocab_size = tokenizer.vocab_size,
d_model = D_MODEL,
n_layers = numberoflayers,
n_heads = numberofheads,
d_latent = d_Latent,
ffn_mult = ffn_mult,
max_seq_len = CONTEXT,
use_moe = False,
num_kv_heads = num_kv_heads,
swa_window = swa_window,
)
optimizer = keras.optimizers.AdamW(
learning_rate = learning_rate,
weight_decay = weight_decay,
global_clipnorm = 1.0,
)
model.compile(
optimizer = optimizer,
loss = keras.losses.SparseCategoricalCrossentropy(from_logits=True),
jit_compile = True,
)
# ββ Warmup with exact training shape ββββββββββββββββββββββββββββββββββ
# XLA traces a kernel per unique input shape. Warming up with any other
# shape (e.g. the original (1, min(4, CONTEXT))) causes a full recompile
# on the first real training batch β wasting 30-120 s on the TPU.
print("Warming up XLA with training shape...", end=" ", flush=True)
t0 = time.perf_counter()
dummy_x = np.zeros((batch_size, CONTEXT), dtype=np.int32)
_ = model(dummy_x, training=False)
_ = model(dummy_x, training=True)
print(f"done in {time.perf_counter() - t0:.2f}s\n")
# ββ Forward pass sanity check ββββββββββββββββββββββββββββββββββββββββββ
# One explicit transfer (np.array) then work in NumPy β avoids the 4
# implicit deviceβhost round trips of the original float(ops.min(...))
# scalar-cast pattern.
print('β' * 52)
print('Forward pass validation')
print('β' * 52)
for x_batch, _ in train_ds.take(1):
logits = model(x_batch[:1], training=False)
logits_np = np.array(logits)
print(f'Input : {x_batch.shape}')
print(f'Logits : {logits.shape}')
print(f'min={logits_np.min():.4f} max={logits_np.max():.4f}')
if np.isnan(logits_np).any() or np.isinf(logits_np).any():
raise ValueError('Forward pass produced NaN/Inf β check model init.')
print('β Forward pass clean.\n')
# ββ Steps per epoch from actual window count βββββββββββββββββββββββββββ
# Original used len(tokens) // (batch*seq) which undercounts overlapping
# windows and causes epochs to terminate prematurely.
print(f"Total params: {model.count_params():,}")
steps_per_epoch = max(1, len(train_windows) // batch_size)
validation_steps = max(1, len(val_windows) // batch_size)
print(f"steps_per_epoch : {steps_per_epoch}")
print(f"validation_steps : {validation_steps}\n")
# ββ Train ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
model.fit(
train_ds,
validation_data = val_ds,
epochs = EPOCHS,
steps_per_epoch = steps_per_epoch,
validation_steps = validation_steps,
callbacks = [
keras.callbacks.TerminateOnNaN(),
keras.callbacks.ModelCheckpoint(
filepath = 'checkpoints/veylon_{epoch:02d}.weights.h5',
save_freq = 'epoch',
save_weights_only = True,
),
ThroughputCallback(),
MemoryCallback(),
],
)
model.save_weights('veylon_final.weights.h5')
print('Saved veylon_final.weights.h5')
if __name__ == '__main__':
main() |