Andrej Karpathy commited on
Commit
92d63d4
·
1 Parent(s): eb16d01

clean up fragile code

Browse files
Files changed (2) hide show
  1. nanochat/gpt.py +50 -7
  2. 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 * (nparams - nparams_exclude) + attn_flops
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
- # Static accounting: what the architecture implies about inference cost
48
-
49
- def kv_bytes_per_token(model):
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(model)
184
  context_mid = prompt_len + args.decode_tokens // 2 # representative decode context
185
- kv_read = kv_read_bytes(model, context_mid)
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": decode_flops_per_token(model, context_mid),
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 * prefill_flops(model, prompt_len) / prefill_time / peak_flops
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 * decode_flops_per_token(model, context_mid)
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})"