Skip to content

Commit 9ec4e4d

Browse files
committed
test(v7-h41): sustained gen.run does not leak Metal allocator peak
Closes plan item #41. 5 × max_new_tokens=1024 + kv_cache_layers=2 through gen.run, assert peak after run 5 < 1.2× peak after run 1. Warm-up + reset_peak before measurement so import-time allocator init doesn't bias the first reading. Skips cleanly on non-Metal backends or when peak reports 0. 1/1 pytest.
1 parent b7b27a5 commit 9ec4e4d

1 file changed

Lines changed: 73 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""V7-H41: long-form sustained generation does not leak memory.
2+
3+
Pins that running gen.run repeatedly with max_new_tokens=1024 doesn't
4+
balloon Metal allocator peak across invocations. Each gen.run builds
5+
+ tears down its own KVCache, samplers, and event-bus subscribers;
6+
if any of those leak references the peak memory after 5 runs would
7+
be N× larger than after the first.
8+
9+
Acceptance: peak_memory after run 5 < 1.2× peak after run 1.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import pytest
15+
16+
import mlx.core as mx
17+
18+
from cppmega_v4.jsonrpc.gen_run_method import GenRunParams, gen_run
19+
20+
21+
def _has_metal_peak() -> bool:
22+
return hasattr(mx, "get_peak_memory") or hasattr(mx, "metal")
23+
24+
25+
def _peak_bytes() -> int:
26+
"""Read Metal allocator peak in a backward-compatible way."""
27+
if hasattr(mx, "get_peak_memory"):
28+
return int(mx.get_peak_memory())
29+
if hasattr(mx, "metal") and hasattr(mx.metal, "get_peak_memory"):
30+
return int(mx.metal.get_peak_memory())
31+
return 0
32+
33+
34+
def _reset_peak() -> None:
35+
if hasattr(mx, "reset_peak_memory"):
36+
mx.reset_peak_memory()
37+
elif hasattr(mx, "metal") and hasattr(mx.metal, "reset_peak_memory"):
38+
mx.metal.reset_peak_memory()
39+
40+
41+
@pytest.fixture(autouse=True)
42+
def _skip_without_metal():
43+
if not _has_metal_peak():
44+
pytest.skip("no Metal allocator peak — cannot bound memory")
45+
46+
47+
def _one_long_gen() -> None:
48+
gen_run(GenRunParams(
49+
prompt_tokens=[1], eos_token_id=99_999,
50+
max_new_tokens=1024, strategy="greedy", seed=11,
51+
kv_cache_layers=2, kv_cache_head_dim=16,
52+
))
53+
54+
55+
def test_v7_h41_repeated_long_gen_memory_bounded():
56+
"""5 × max_new_tokens=1024: peak after 5 < 1.2× peak after 1."""
57+
# Warm up so the first measurement isn't dominated by import-time
58+
# allocator initialisation.
59+
_one_long_gen()
60+
_reset_peak()
61+
_one_long_gen()
62+
peak_after_1 = _peak_bytes()
63+
for _ in range(4):
64+
_one_long_gen()
65+
peak_after_5 = _peak_bytes()
66+
if peak_after_1 == 0:
67+
# Some backends report 0 even with metal present — skip cleanly.
68+
pytest.skip("Metal peak reported 0 — cannot bound delta")
69+
ratio = peak_after_5 / peak_after_1
70+
assert ratio < 1.2, (
71+
f"sustained gen leak: peak_after_1={peak_after_1} bytes, "
72+
f"peak_after_5={peak_after_5} bytes, ratio={ratio:.3f}× "
73+
f"(limit 1.2×)")

0 commit comments

Comments
 (0)