Andrej Karpathy commited on
Commit ·
92d63d4
1
Parent(s): eb16d01
clean up fragile code
Browse files- nanochat/gpt.py +50 -7
- scripts/infer_bench.py +8 -59
nanochat/gpt.py
CHANGED
|
@@ -328,12 +328,6 @@ class GPT(nn.Module):
|
|
| 328 |
- Chinchilla counts the embedding layer as flops (? weird, it's just a lookup => we ignore)
|
| 329 |
- Chinchilla counts exp/sum/divide in attention softmax as flops (a little sus and very tiny => we ignore)
|
| 330 |
"""
|
| 331 |
-
nparams = sum(p.numel() for p in self.parameters())
|
| 332 |
-
# Exclude non-matmul params: embeddings and per-layer scalars
|
| 333 |
-
value_embeds_numel = sum(ve.weight.numel() for ve in self.value_embeds.values())
|
| 334 |
-
nparams_exclude = (self.transformer.wte.weight.numel() + value_embeds_numel +
|
| 335 |
-
self.resid_lambdas.numel() + self.x0_lambdas.numel() +
|
| 336 |
-
self.smear_gate.weight.numel() + self.smear_lambda.numel() + self.backout_lambda.numel())
|
| 337 |
h, q, t = self.config.n_head, self.config.n_embd // self.config.n_head, self.config.sequence_len
|
| 338 |
# Sum attention FLOPs per layer, accounting for sliding window
|
| 339 |
attn_flops = 0
|
|
@@ -341,9 +335,58 @@ class GPT(nn.Module):
|
|
| 341 |
window = window_size[0] # (left, right) tuple, we use left
|
| 342 |
effective_seq = t if window < 0 else min(window, t)
|
| 343 |
attn_flops += 12 * h * q * effective_seq
|
| 344 |
-
num_flops_per_token = 6 * (
|
| 345 |
return num_flops_per_token
|
| 346 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
def num_scaling_params(self):
|
| 348 |
"""
|
| 349 |
Return detailed parameter counts for scaling law analysis.
|
|
|
|
| 328 |
- Chinchilla counts the embedding layer as flops (? weird, it's just a lookup => we ignore)
|
| 329 |
- Chinchilla counts exp/sum/divide in attention softmax as flops (a little sus and very tiny => we ignore)
|
| 330 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
h, q, t = self.config.n_head, self.config.n_embd // self.config.n_head, self.config.sequence_len
|
| 332 |
# Sum attention FLOPs per layer, accounting for sliding window
|
| 333 |
attn_flops = 0
|
|
|
|
| 335 |
window = window_size[0] # (left, right) tuple, we use left
|
| 336 |
effective_seq = t if window < 0 else min(window, t)
|
| 337 |
attn_flops += 12 * h * q * effective_seq
|
| 338 |
+
num_flops_per_token = 6 * self.num_matmul_params() + attn_flops
|
| 339 |
return num_flops_per_token
|
| 340 |
|
| 341 |
+
def num_matmul_params(self):
|
| 342 |
+
"""
|
| 343 |
+
The number of parameters that participate in matmuls with the token stream,
|
| 344 |
+
i.e. contribute 2 FLOPs/param to the forward pass. Counted structurally: every
|
| 345 |
+
matmul in this model goes through the Linear class, while non-matmul params
|
| 346 |
+
(embeddings = lookups, per-layer scalars) are nn.Embedding or raw Parameters.
|
| 347 |
+
"""
|
| 348 |
+
matmul_params = sum(m.weight.numel() for m in self.modules() if isinstance(m, Linear))
|
| 349 |
+
return matmul_params
|
| 350 |
+
|
| 351 |
+
def estimate_decode_flops(self, context_len):
|
| 352 |
+
"""
|
| 353 |
+
Forward FLOPs to decode one token at a given context length during inference:
|
| 354 |
+
2 FLOPs per matmul param, plus attention over min(context, window) per layer.
|
| 355 |
+
"""
|
| 356 |
+
h = self.config.n_head
|
| 357 |
+
q = self.config.n_embd // self.config.n_head
|
| 358 |
+
attn_flops = sum(4 * h * q * min(context_len, window) for window, _ in self.window_sizes)
|
| 359 |
+
decode_flops = 2 * self.num_matmul_params() + attn_flops
|
| 360 |
+
return decode_flops
|
| 361 |
+
|
| 362 |
+
def estimate_prefill_flops(self, num_tokens):
|
| 363 |
+
"""Forward FLOPs to prefill a prompt: causal, so token t attends to min(t, window)."""
|
| 364 |
+
h = self.config.n_head
|
| 365 |
+
q = self.config.n_embd // self.config.n_head
|
| 366 |
+
attn_flops = 0
|
| 367 |
+
for window, _ in self.window_sizes:
|
| 368 |
+
w = min(window, num_tokens)
|
| 369 |
+
attended_tokens = w * (w + 1) // 2 + (num_tokens - w) * w # ramp up to w, then flat
|
| 370 |
+
attn_flops += 4 * h * q * attended_tokens
|
| 371 |
+
prefill_flops = 2 * self.num_matmul_params() * num_tokens + attn_flops
|
| 372 |
+
return prefill_flops
|
| 373 |
+
|
| 374 |
+
def kv_bytes_per_token(self):
|
| 375 |
+
"""Bytes to *store* one token of KV cache during inference, per row (all layers)."""
|
| 376 |
+
head_dim = self.config.n_embd // self.config.n_head
|
| 377 |
+
kv_dtype_bytes = COMPUTE_DTYPE.itemsize # the KV cache is kept in the compute dtype
|
| 378 |
+
return self.config.n_layer * 2 * self.config.n_kv_head * head_dim * kv_dtype_bytes
|
| 379 |
+
|
| 380 |
+
def kv_read_bytes(self, context_len):
|
| 381 |
+
"""Bytes of KV cache *read* by one decode step at a given context length, per row.
|
| 382 |
+
Sliding window layers only attend to (and read) the last `window` tokens."""
|
| 383 |
+
head_dim = self.config.n_embd // self.config.n_head
|
| 384 |
+
kv_dtype_bytes = COMPUTE_DTYPE.itemsize
|
| 385 |
+
total = 0
|
| 386 |
+
for window, _ in self.window_sizes:
|
| 387 |
+
total += 2 * self.config.n_kv_head * head_dim * kv_dtype_bytes * min(context_len, window)
|
| 388 |
+
return total
|
| 389 |
+
|
| 390 |
def num_scaling_params(self):
|
| 391 |
"""
|
| 392 |
Return detailed parameter counts for scaling law analysis.
|
scripts/infer_bench.py
CHANGED
|
@@ -44,65 +44,14 @@ from nanochat.checkpoint_manager import load_model
|
|
| 44 |
from nanochat.engine import Engine
|
| 45 |
|
| 46 |
# -----------------------------------------------------------------------------
|
| 47 |
-
#
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
"""Bytes to *store* one token of KV cache, per row (all layers)."""
|
| 51 |
-
config = model.config
|
| 52 |
-
head_dim = config.n_embd // config.n_head
|
| 53 |
-
kv_dtype_bytes = 2 # KV cache is kept in bf16
|
| 54 |
-
return config.n_layer * 2 * config.n_kv_head * head_dim * kv_dtype_bytes
|
| 55 |
-
|
| 56 |
-
def kv_read_bytes(model, context_len):
|
| 57 |
-
"""Bytes of KV cache *read* by one decode step at a given context length, per row.
|
| 58 |
-
Sliding window layers only attend to (and read) the last `window` tokens."""
|
| 59 |
-
config = model.config
|
| 60 |
-
head_dim = config.n_embd // config.n_head
|
| 61 |
-
kv_dtype_bytes = 2
|
| 62 |
-
total = 0
|
| 63 |
-
for window, _ in model.window_sizes:
|
| 64 |
-
total += 2 * config.n_kv_head * head_dim * kv_dtype_bytes * min(context_len, window)
|
| 65 |
-
return total
|
| 66 |
|
| 67 |
def weight_bytes(model):
|
| 68 |
"""Bytes of parameters as stored (each decode step reads all of them)."""
|
| 69 |
return sum(p.numel() * p.element_size() for p in model.parameters())
|
| 70 |
|
| 71 |
-
def matmul_param_count(model):
|
| 72 |
-
"""Number of matmul (weight) parameters. Backed out of estimate_flops() so that
|
| 73 |
-
the exclusion list of non-matmul params (embeddings, scalars, ...) lives in one place."""
|
| 74 |
-
config = model.config
|
| 75 |
-
h = config.n_head
|
| 76 |
-
q = config.n_embd // config.n_head
|
| 77 |
-
t = config.sequence_len
|
| 78 |
-
train_attn_flops = sum(12 * h * q * min(window, t) for window, _ in model.window_sizes)
|
| 79 |
-
matmul_params = (model.estimate_flops() - train_attn_flops) // 6
|
| 80 |
-
return matmul_params
|
| 81 |
-
|
| 82 |
-
def decode_flops_per_token(model, context_len):
|
| 83 |
-
"""Forward FLOPs to decode one token at a given context length: 2 FLOPs per
|
| 84 |
-
matmul param, plus attention reads of min(context, window) per layer."""
|
| 85 |
-
config = model.config
|
| 86 |
-
h = config.n_head
|
| 87 |
-
q = config.n_embd // config.n_head
|
| 88 |
-
attn_flops = sum(4 * h * q * min(context_len, window) for window, _ in model.window_sizes)
|
| 89 |
-
return 2 * matmul_param_count(model) + attn_flops
|
| 90 |
-
|
| 91 |
-
def prefill_flops(model, prompt_len):
|
| 92 |
-
"""Forward FLOPs to prefill a prompt: causal, so token t attends to min(t, window)."""
|
| 93 |
-
config = model.config
|
| 94 |
-
h = config.n_head
|
| 95 |
-
q = config.n_embd // config.n_head
|
| 96 |
-
attn_flops = 0
|
| 97 |
-
for window, _ in model.window_sizes:
|
| 98 |
-
w = min(window, prompt_len)
|
| 99 |
-
attended = w * (w + 1) // 2 + (prompt_len - w) * w # ramp up to w, then flat
|
| 100 |
-
attn_flops += 4 * h * q * attended
|
| 101 |
-
return 2 * matmul_param_count(model) * prompt_len + attn_flops
|
| 102 |
-
|
| 103 |
-
# -----------------------------------------------------------------------------
|
| 104 |
-
# Measurement
|
| 105 |
-
|
| 106 |
def bench_generate(engine, prompt_tokens, batch_size, decode_tokens, temperature):
|
| 107 |
"""Run one timed generation. Returns dict of measurements."""
|
| 108 |
device = engine.model.get_device()
|
|
@@ -180,9 +129,9 @@ def main():
|
|
| 180 |
total_vram = torch.cuda.get_device_properties(device).total_memory
|
| 181 |
w_bytes = weight_bytes(model)
|
| 182 |
num_params = sum(p.numel() for p in model.parameters())
|
| 183 |
-
kv_store = kv_bytes_per_token(
|
| 184 |
context_mid = prompt_len + args.decode_tokens // 2 # representative decode context
|
| 185 |
-
kv_read = kv_read_bytes(
|
| 186 |
# tokens/sec ceiling at batch 1: every step must at least re-read weights + KV
|
| 187 |
ceiling_bs1 = peak_bw / (w_bytes + kv_read)
|
| 188 |
# how many rows of full-context KV fit next to the weights
|
|
@@ -221,7 +170,7 @@ def main():
|
|
| 221 |
"kv_read_bytes_per_step": kv_read,
|
| 222 |
"context_mid": context_mid,
|
| 223 |
"peak_flops_per_sec": peak_flops if peak_flops != float("inf") else None,
|
| 224 |
-
"decode_flops_per_token":
|
| 225 |
"ceiling_bs1_tok_per_sec": round(ceiling_bs1, 1) if ceiling_bs1 != float("inf") else None,
|
| 226 |
"max_full_context_rows": max_rows,
|
| 227 |
"prompt_tokens": prompt_len,
|
|
@@ -236,7 +185,7 @@ def main():
|
|
| 236 |
bench_generate(engine, prompt_tokens, 1, 2, args.temperature) # warmup
|
| 237 |
prefill_result = bench_generate(engine, prompt_tokens, 1, 2, args.temperature)
|
| 238 |
prefill_time = prefill_result["ttft"]
|
| 239 |
-
prefill_mfu = 100 *
|
| 240 |
prefill_tok_per_sec = prompt_len / prefill_time
|
| 241 |
print(f"Prefill (batch 1, {prompt_len} tokens): {prefill_tok_per_sec:,.0f} tok/s | MFU {prefill_mfu:.1f}%")
|
| 242 |
payload["prefill"] = {
|
|
@@ -269,7 +218,7 @@ def main():
|
|
| 269 |
bytes_per_step = w_bytes + batch_size * kv_read
|
| 270 |
mbu = 100 * (bytes_per_step / tpot) / peak_bw
|
| 271 |
# MFU: FLOPs each decode step must do, over what the GPU can do
|
| 272 |
-
flops_per_step = batch_size *
|
| 273 |
mfu = 100 * (flops_per_step / tpot) / peak_flops
|
| 274 |
vram_gib = result["peak_vram"] / 2**30
|
| 275 |
note = "" if num_steps == args.decode_tokens - 1 else f" (early stop @ {num_steps})"
|
|
|
|
| 44 |
from nanochat.engine import Engine
|
| 45 |
|
| 46 |
# -----------------------------------------------------------------------------
|
| 47 |
+
# Measurement
|
| 48 |
+
# (the architecture-side cost accounting - FLOPs, KV cache bytes - lives on the
|
| 49 |
+
# GPT model itself: estimate_decode_flops, estimate_prefill_flops, kv_bytes_per_token, kv_read_bytes)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
def weight_bytes(model):
|
| 52 |
"""Bytes of parameters as stored (each decode step reads all of them)."""
|
| 53 |
return sum(p.numel() * p.element_size() for p in model.parameters())
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
def bench_generate(engine, prompt_tokens, batch_size, decode_tokens, temperature):
|
| 56 |
"""Run one timed generation. Returns dict of measurements."""
|
| 57 |
device = engine.model.get_device()
|
|
|
|
| 129 |
total_vram = torch.cuda.get_device_properties(device).total_memory
|
| 130 |
w_bytes = weight_bytes(model)
|
| 131 |
num_params = sum(p.numel() for p in model.parameters())
|
| 132 |
+
kv_store = model.kv_bytes_per_token()
|
| 133 |
context_mid = prompt_len + args.decode_tokens // 2 # representative decode context
|
| 134 |
+
kv_read = model.kv_read_bytes(context_mid)
|
| 135 |
# tokens/sec ceiling at batch 1: every step must at least re-read weights + KV
|
| 136 |
ceiling_bs1 = peak_bw / (w_bytes + kv_read)
|
| 137 |
# how many rows of full-context KV fit next to the weights
|
|
|
|
| 170 |
"kv_read_bytes_per_step": kv_read,
|
| 171 |
"context_mid": context_mid,
|
| 172 |
"peak_flops_per_sec": peak_flops if peak_flops != float("inf") else None,
|
| 173 |
+
"decode_flops_per_token": model.estimate_decode_flops(context_mid),
|
| 174 |
"ceiling_bs1_tok_per_sec": round(ceiling_bs1, 1) if ceiling_bs1 != float("inf") else None,
|
| 175 |
"max_full_context_rows": max_rows,
|
| 176 |
"prompt_tokens": prompt_len,
|
|
|
|
| 185 |
bench_generate(engine, prompt_tokens, 1, 2, args.temperature) # warmup
|
| 186 |
prefill_result = bench_generate(engine, prompt_tokens, 1, 2, args.temperature)
|
| 187 |
prefill_time = prefill_result["ttft"]
|
| 188 |
+
prefill_mfu = 100 * model.estimate_prefill_flops(prompt_len) / prefill_time / peak_flops
|
| 189 |
prefill_tok_per_sec = prompt_len / prefill_time
|
| 190 |
print(f"Prefill (batch 1, {prompt_len} tokens): {prefill_tok_per_sec:,.0f} tok/s | MFU {prefill_mfu:.1f}%")
|
| 191 |
payload["prefill"] = {
|
|
|
|
| 218 |
bytes_per_step = w_bytes + batch_size * kv_read
|
| 219 |
mbu = 100 * (bytes_per_step / tpot) / peak_bw
|
| 220 |
# MFU: FLOPs each decode step must do, over what the GPU can do
|
| 221 |
+
flops_per_step = batch_size * model.estimate_decode_flops(context_mid)
|
| 222 |
mfu = 100 * (flops_per_step / tpot) / peak_flops
|
| 223 |
vram_gib = result["peak_vram"] / 2**30
|
| 224 |
note = "" if num_steps == args.decode_tokens - 1 else f" (early stop @ {num_steps})"
|