Skip to content

Commit b7b27a5

Browse files
committed
test(v7-h40): KV cache linear growth contract pin
Closes plan item #40. Pins gen.run kv_cache observability invariants: - one growth event per generated token (eos_token_id=99999 keeps 'length' as the only finish reason so the count is deterministic), - per-layer length == growth_events, - total_bytes == growth_events * num_layers * head_dim * 4 * 2 (fp32 size × k+v), - kv_cache is None when layers=0, - doubling max_new_tokens doubles growth_events and total_bytes. 4/4 pytest.
1 parent aca73c9 commit b7b27a5

1 file changed

Lines changed: 64 additions & 0 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""V7-H40: gen.run KV cache grows linearly with token count.
2+
3+
Pins the contract that with kv_cache_layers > 0:
4+
* each generated token triggers exactly one growth event,
5+
* per-layer length equals the number of tokens generated,
6+
* total_bytes scales as growth_events * num_layers * head_dim * fp32_size
7+
(the synthetic kv_cache_state appends one (1, 1, head_dim) k+v per
8+
layer per token at fp32, so 2 * 4 bytes per element).
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from cppmega_v4.jsonrpc.gen_run_method import GenRunParams, gen_run
14+
15+
16+
def _gen(layers: int, head_dim: int, max_new_tokens: int):
17+
# eos_token_id outside the default vocab so 'length' is always the
18+
# finish reason — keeps the linear-growth contract independent of
19+
# the sampler's per-seed EOS hit rate.
20+
return gen_run(GenRunParams(
21+
prompt_tokens=[1], eos_token_id=99_999,
22+
max_new_tokens=max_new_tokens,
23+
strategy="greedy", seed=7,
24+
kv_cache_layers=layers, kv_cache_head_dim=head_dim,
25+
))
26+
27+
28+
def test_v7_h40_kv_cache_lengths_equal_growth_events_per_layer():
29+
out = _gen(layers=4, head_dim=16, max_new_tokens=32)
30+
assert out.kv_cache is not None
31+
kv = out.kv_cache
32+
assert kv["num_layers"] == 4
33+
assert kv["head_dim"] == 16
34+
# One growth event per generated token; per-layer length matches.
35+
assert kv["growth_events"] == 32
36+
assert kv["lengths_per_layer"] == [32, 32, 32, 32]
37+
38+
39+
def test_v7_h40_total_bytes_matches_growth_arithmetic():
40+
out = _gen(layers=4, head_dim=16, max_new_tokens=32)
41+
kv = out.kv_cache
42+
# KVCache.append stores fp32 k + fp32 v per layer per row,
43+
# (1, 1, head_dim) shape → head_dim elements × 4 bytes × 2 (k+v).
44+
fp32_size = 4
45+
expected = (kv["growth_events"] * kv["num_layers"]
46+
* kv["head_dim"] * fp32_size * 2)
47+
assert kv["total_bytes"] == expected, (
48+
f"total_bytes={kv['total_bytes']} expected={expected}")
49+
50+
51+
def test_v7_h40_kv_cache_disabled_when_layers_zero():
52+
out = gen_run(GenRunParams(
53+
prompt_tokens=[1], eos_token_id=0,
54+
max_new_tokens=4, kv_cache_layers=0,
55+
))
56+
assert out.kv_cache is None
57+
58+
59+
def test_v7_h40_growth_scales_linearly_in_steps():
60+
"""Doubling max_new_tokens doubles growth_events + total_bytes."""
61+
a = _gen(layers=2, head_dim=8, max_new_tokens=10)
62+
b = _gen(layers=2, head_dim=8, max_new_tokens=20)
63+
assert b.kv_cache["growth_events"] == 2 * a.kv_cache["growth_events"]
64+
assert b.kv_cache["total_bytes"] == 2 * a.kv_cache["total_bytes"]

0 commit comments

Comments
 (0)