Spaces:
Sleeping
Sleeping
File size: 24,314 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 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 | 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 jax.sharding as sharding
from jax.sharding import PartitionSpec as P
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
# Multiply by effective_batch if data parallelism is active
tps = (self.LOG_INTERVAL * effective_batch * 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 * effective_batch * 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)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SHARDING (Data Parallelism for v5e-8)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def setup_data_parallelism():
"""
Configure JAX for pure data parallelism across all TPU devices.
For v5e-8 (8 chips):
- Each chip runs the full model
- Different batch shards go to each chip
- Gradients are AllReduced across chips
- Effective batch = batch_per_chip Γ num_chips
This is the simplest and most efficient sharding strategy for models
that fit on a single chip. For 8M parameters, the model easily fits
on one v5e chip (16 GB HBM each).
Returns mesh_shape (tuple) and sharding spec dict for inputs/weights.
"""
devices = jax.devices()
n_devices = len(devices)
if n_devices == 1:
print(f"Single device detected ({devices[0]}). Data parallelism disabled.")
return None, None
print(f"\n{'β'*52}")
print(f"Data Parallelism Setup (v5e-{n_devices})")
print(f"{'β'*52}")
print(f"Devices: {n_devices}")
print(f"Device type: {devices[0].platform}")
# Create 1D mesh for data parallelism: each axis is one device
mesh = sharding.Mesh(
devices=np.array(devices).reshape((n_devices,)),
axis_names=("batch",)
)
# Sharding specs for inputs and weights
# Input shape: (batch, seq_len)
# Shard batch dimension across devices, keep seq_len replicated
input_spec = P("batch", None)
# Weights: replicate across all devices (each chip has full model)
weight_spec = P(None)
# Activation gradients during backward pass
# Same as input: shard batch, replicate seq
activation_spec = P("batch", None)
print(f"Mesh shape: {mesh.shape}")
print(f"Input sharding: {input_spec} (batch sharded, seq replicated)")
print(f"Weight sharding: {weight_spec} (fully replicated)")
print(f"Expected effective batch: {n_devices} Γ batch_size")
print(f"{'β'*52}\n")
return mesh, {
"input": input_spec,
"weight": weight_spec,
"activation": activation_spec,
}
def apply_sharding_to_model(model, mesh, sharding_specs):
"""
Apply JAX sharding annotations to a Keras model compiled with JAX backend.
WARNING: This is JAX-level sharding and only works if:
1. Model is built with JAX-native ops (Keras layers with JAX backend)
2. jit_compile=True is set in model.compile()
3. No TensorFlow-only ops are used in the model
For Keras models, we set the sharding via jax.Array.with_sharding_constraint()
in a custom train step. However, Keras 3 makes this tricky because it wraps
training in its own jit.
Simpler approach: set jax.config to globally use this mesh, and let XLA
infer sharding from the mesh context.
"""
if mesh is None:
return
# Tell JAX to use this mesh for all operations
with mesh:
print("Sharding configuration loaded.")
print(f" All matmul ops will shard batch dim across {mesh.shape[0]} devices")
print(f" Gradient AllReduce will use {mesh.shape[0]}-way ICI collective\n")
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 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')
# ββ Setup data parallelism βββββββββββββββββββββββββββββββββββββββββββββ
mesh, sharding_specs = setup_data_parallelism()
if mesh is not None:
apply_sharding_to_model(None, mesh, sharding_specs)
# ββ 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 βββββββββββββββββββββββββββββββββββββββββββββββββββββ
# If data parallelism is active, the effective batch becomes:
# batch_per_device Γ num_devices
# But tf.data still sees batch_per_device β JAX handles the replication.
num_devices = len(jax.devices())
actual_batch_size = batch_size # Per-device batch
effective_batch = actual_batch_size * num_devices if mesh is not None else actual_batch_size
if mesh is not None:
print(f"Effective batch size: {actual_batch_size} Γ {num_devices} devices = {effective_batch}")
train_ds = create_tf_dataset(train_windows, actual_batch_size, shuffle=True)
val_ds = create_tf_dataset(val_windows, actual_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() |