Skip to content

Commit d4322f8

Browse files
committed
feat(v8-r03): memory.matrix RPC + MemoryMatrixTab sidebar grid
Closes cppmega-mlx-yz6n.3. New RPC `memory.matrix(spec, topologies?, precisions?, headroom?)` runs verify_and_estimate once in bf16 and post-scales weights/grads/optimizer/activations/edge_handoff per precision (fp32=4, bf16=fp16=2, fp8=1, mxfp4=0.5 bytes/element). KV cache stays separate since kv_cache_dtype_bytes is independent of the weight dtype. Default axes: 4 topologies (h100_8x / m3_ultra_solo / gb10_quarter / tpu_v6e_8) × 5 precisions = 20 cells, each with fits flag against the topology's total_hbm_bytes × headroom. UI: new MemoryMatrixTab in the sidebar (key "memory") renders a colour-coded grid — green if fits, red otherwise — with title-tooltip showing the per-component breakdown. Refetches on specPayload change. Tests: 10/10 new pytest (axes, precision ordering, breakdown sum, unknown-axis rejection, dispatch e2e), 3/3 new vitest (grid mount, refetch on payload change, error banner). Regression: 893 pytest + 458 vitest green.
1 parent 2917632 commit d4322f8

8 files changed

Lines changed: 1004 additions & 6 deletions

File tree

cppmega_v4/jsonrpc/dispatcher.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
CacheMetricsParams,
7676
cache_metrics,
7777
)
78+
from cppmega_v4.jsonrpc.memory_matrix_method import (
79+
MemoryMatrixParams,
80+
memory_matrix,
81+
)
7882
from cppmega_v4.jsonrpc.tokenizer_roundtrip_text_method import (
7983
TokenizerRoundtripTextParams,
8084
roundtrip_text,
@@ -129,6 +133,10 @@
129133
ScaleDownParams,
130134
lambda p, c: scale_down_method(p, cache=c),
131135
),
136+
"memory.matrix": (
137+
MemoryMatrixParams,
138+
lambda p, c: memory_matrix(p, cache=c),
139+
),
132140
"probe.run": (
133141
ProbeRunParams,
134142
lambda p, c: probe_run(p, cache=c),
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
"""V8-R03: ``memory.matrix`` RPC — compute a (topology × precision) grid
2+
of memory estimates and per-cell fit verdicts.
3+
4+
Used by the V8 MemoryMatrix sidebar tab to show the user at-a-glance
5+
which device-precision combinations fit their current canvas spec.
6+
7+
Backend strategy
8+
----------------
9+
10+
For each cell we run :func:`cppmega_v4.spec.verify_and_estimate` once
11+
in bf16, then post-scale dtype-sensitive components (weights,
12+
gradients, activations, optimizer state) by ``precision_bytes / 2``.
13+
``mxfp4`` is 0.5 bytes per element — supported because the scaling is
14+
done in float space and rounded only on the final ``total_bytes``.
15+
16+
This is the same approach the V8 spec §3 nominates: one verify per
17+
matrix, post-scale per precision, so the round-trip is fast enough to
18+
re-run on every spec edit.
19+
"""
20+
21+
from __future__ import annotations
22+
23+
import math
24+
from typing import Any
25+
26+
from pydantic import BaseModel, ConfigDict
27+
28+
from cppmega_v4.fusion.brick_graph import from_block_specs
29+
from cppmega_v4.jsonrpc.cache import LRUCache
30+
from cppmega_v4.jsonrpc.methods import (
31+
_cache_lookup, _cache_store, _graph_to_specs,
32+
)
33+
from cppmega_v4.jsonrpc.schema import VerifyParams
34+
from cppmega_v4.parallelism import topology as _topo
35+
from cppmega_v4.spec.api import verify_and_estimate
36+
37+
38+
__all__ = [
39+
"MemoryMatrixParams",
40+
"MemoryMatrixCell",
41+
"MemoryMatrixResult",
42+
"memory_matrix",
43+
"PRECISION_BYTES",
44+
"TOPOLOGY_BUILDERS",
45+
]
46+
47+
48+
# Precision -> bytes per element. mxfp4 is half a byte (4-bit mantissa
49+
# with shared e4m3 scale per block — the scale overhead is folded into
50+
# the headroom and is < 5% at block_size=16).
51+
PRECISION_BYTES: dict[str, float] = {
52+
"fp32": 4.0,
53+
"bf16": 2.0,
54+
"fp16": 2.0,
55+
"fp8": 1.0,
56+
"mxfp4": 0.5,
57+
}
58+
59+
60+
# Builder lookup for topologies the matrix supports. Wraps the
61+
# zero-arg factories from cppmega_v4.parallelism.topology.
62+
TOPOLOGY_BUILDERS: dict[str, Any] = {
63+
"h100_8x": lambda: _topo.h100_8x(),
64+
"h200_8x": lambda: _topo.h200_8x(),
65+
"a100_8x": lambda: _topo.a100_8x(),
66+
"b100_8x": lambda: _topo.b100_8x(),
67+
"gb10_quarter": lambda: _topo.gb10_quarter(),
68+
"tpu_v6e_8": lambda: _topo.tpu_v6e_8(),
69+
"tpu_v5p_4": lambda: _topo.tpu_v5p_4(),
70+
"m3_ultra_solo": lambda: _topo.m3_ultra_solo(),
71+
}
72+
73+
74+
_DEFAULT_TOPOLOGIES = (
75+
"h100_8x", "m3_ultra_solo", "gb10_quarter", "tpu_v6e_8",
76+
)
77+
_DEFAULT_PRECISIONS = ("fp32", "bf16", "fp16", "fp8", "mxfp4")
78+
79+
80+
class MemoryMatrixParams(BaseModel):
81+
"""Input — a VerifyParams payload + the topologies/precisions axes."""
82+
83+
model_config = ConfigDict(extra="forbid")
84+
85+
spec: VerifyParams
86+
topologies: list[str] | None = None
87+
precisions: list[str] | None = None
88+
headroom: float = 0.9
89+
90+
91+
class MemoryMatrixCell(BaseModel):
92+
"""One cell of the matrix — wire-form."""
93+
94+
model_config = ConfigDict(extra="forbid")
95+
96+
topology: str
97+
precision: str
98+
bytes: int
99+
device_hbm_bytes: int
100+
fits: bool
101+
headroom: float
102+
breakdown: dict[str, int]
103+
104+
105+
class MemoryMatrixResult(BaseModel):
106+
"""The full matrix — one row per (topology, precision) pair."""
107+
108+
model_config = ConfigDict(extra="forbid")
109+
110+
cells: list[MemoryMatrixCell]
111+
topologies: list[str]
112+
precisions: list[str]
113+
114+
115+
def _scale_int(value: int, factor: float) -> int:
116+
"""Round half-away-from-zero so e.g. ``2 * 0.5 = 1`` not ``0``."""
117+
return int(math.floor(value * factor + 0.5))
118+
119+
120+
def memory_matrix(
121+
params: MemoryMatrixParams, *, cache: LRUCache | None = None,
122+
) -> MemoryMatrixResult:
123+
"""Build the (topology × precision) memory matrix for ``params.spec``."""
124+
key, hit = _cache_lookup(cache, "memory.matrix", params)
125+
if hit is not None:
126+
return hit
127+
128+
topologies = list(params.topologies or _DEFAULT_TOPOLOGIES)
129+
precisions = list(params.precisions or _DEFAULT_PRECISIONS)
130+
for t in topologies:
131+
if t not in TOPOLOGY_BUILDERS:
132+
raise ValueError(
133+
f"unknown topology {t!r}; choose from "
134+
f"{sorted(TOPOLOGY_BUILDERS)}")
135+
for p in precisions:
136+
if p not in PRECISION_BYTES:
137+
raise ValueError(
138+
f"unknown precision {p!r}; choose from "
139+
f"{sorted(PRECISION_BYTES)}")
140+
if not 0.0 < params.headroom <= 1.0:
141+
raise ValueError(
142+
f"headroom must be in (0, 1], got {params.headroom}")
143+
144+
# One verify_and_estimate call gives us the full MemoryReport with
145+
# aggregate totals; we then post-scale per precision in float space.
146+
specs = _graph_to_specs(params.spec.graph)
147+
hidden = params.spec.dim_env.get("H", 64)
148+
graph = from_block_specs(specs, hidden_size=hidden, instantiate=False)
149+
base = verify_and_estimate(
150+
graph,
151+
dim_env=params.spec.dim_env,
152+
training=params.spec.training,
153+
).memory
154+
155+
base_weights_b = base.weights_bytes
156+
base_grads_b = base.grads_bytes
157+
base_optim_b = base.optimizer_bytes
158+
base_act_b = base.activations_bytes
159+
base_kv_b = base.kv_cache_bytes
160+
base_edge_b = base.edge_handoff_bytes
161+
# KV cache uses kv_cache_dtype_bytes (independent of weight dtype),
162+
# so we don't scale it. Edge handoff is bytes-of-tensors-in-transit
163+
# and IS dtype-sensitive.
164+
165+
cells: list[MemoryMatrixCell] = []
166+
for t_name in topologies:
167+
topo = TOPOLOGY_BUILDERS[t_name]()
168+
hbm = topo.total_hbm_bytes
169+
for p_name in precisions:
170+
scale = PRECISION_BYTES[p_name] / 2.0 # bf16 baseline
171+
w = _scale_int(base_weights_b, scale)
172+
g = _scale_int(base_grads_b, scale)
173+
o = _scale_int(base_optim_b, scale)
174+
a = _scale_int(base_act_b, scale)
175+
e = _scale_int(base_edge_b, scale)
176+
total = w + g + o + a + base_kv_b + e
177+
cells.append(MemoryMatrixCell(
178+
topology=t_name, precision=p_name,
179+
bytes=total, device_hbm_bytes=hbm,
180+
fits=total <= int(hbm * params.headroom),
181+
headroom=params.headroom,
182+
breakdown={
183+
"weights": w,
184+
"grads": g,
185+
"optimizer": o,
186+
"activations": a,
187+
"kv_cache": base_kv_b,
188+
"edge_handoff": e,
189+
},
190+
))
191+
192+
out = MemoryMatrixResult(
193+
cells=cells, topologies=topologies, precisions=precisions)
194+
_cache_store(cache, key, out)
195+
return out

cppmega_v4/jsonrpc/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,7 @@ class PipelineRunResult(BaseModel):
808808
"side_channels.preview",
809809
"platform.get_info",
810810
"architectures.scale_down",
811+
"memory.matrix",
811812
})
812813

813814

tests/v4/test_memory_matrix.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""V8-R03 unit tests: memory.matrix RPC.
2+
3+
Asserts:
4+
* default 4×5 matrix when topologies/precisions omitted
5+
* per-precision bytes scale linearly with precision_bytes/2 (bf16 baseline)
6+
* mxfp4 < fp8 < bf16 == fp16 < fp32 for the same topology
7+
* fits flag flips when bytes exceed device_hbm_bytes × headroom
8+
* unknown topology / precision rejected
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import pytest
14+
15+
from cppmega_v4.jsonrpc.dispatcher import dispatch
16+
from cppmega_v4.jsonrpc.memory_matrix_method import (
17+
MemoryMatrixParams, memory_matrix, PRECISION_BYTES,
18+
TOPOLOGY_BUILDERS,
19+
)
20+
from cppmega_v4.jsonrpc.schema import JsonRpcRequest, VerifyParams
21+
22+
23+
def _mini_spec() -> dict:
24+
"""Minimal canvas spec: attention + mlp, hidden=256."""
25+
return {
26+
"graph": {
27+
"nodes": [
28+
{"id": "a", "kind": "attention", "params": {}},
29+
{"id": "b", "kind": "mlp", "params": {}},
30+
],
31+
"edges": [{"src": "a", "dst": "b"}],
32+
},
33+
"dim_env": {"H": 256, "B": 1, "S": 64},
34+
"loss": {"kind": "cross_entropy", "head_outputs": ["b"]},
35+
"optim": {"kind": "adamw", "groups": [
36+
{"matcher": "all", "lr": 1e-3, "weight_decay": 0.01,
37+
"betas": [0.9, 0.95]},
38+
]},
39+
"sharding": None,
40+
"training": True,
41+
}
42+
43+
44+
def test_default_axes_yield_4x5_grid():
45+
res = memory_matrix(MemoryMatrixParams(
46+
spec=VerifyParams(**_mini_spec()),
47+
))
48+
assert len(res.cells) == 4 * 5
49+
assert set(res.topologies) == {
50+
"h100_8x", "m3_ultra_solo", "gb10_quarter", "tpu_v6e_8"}
51+
assert set(res.precisions) == {"fp32", "bf16", "fp16", "fp8", "mxfp4"}
52+
53+
54+
def test_precision_ordering_within_a_topology():
55+
res = memory_matrix(MemoryMatrixParams(
56+
spec=VerifyParams(**_mini_spec()),
57+
topologies=["h100_8x"],
58+
))
59+
by_p = {c.precision: c.bytes for c in res.cells}
60+
assert by_p["mxfp4"] < by_p["fp8"] < by_p["bf16"]
61+
assert by_p["bf16"] == by_p["fp16"]
62+
assert by_p["bf16"] < by_p["fp32"]
63+
64+
65+
def test_bf16_baseline_unchanged():
66+
"""The bf16 column must equal the raw verify_and_estimate total —
67+
no rounding loss when the scale is exactly 1.0."""
68+
res = memory_matrix(MemoryMatrixParams(
69+
spec=VerifyParams(**_mini_spec()),
70+
topologies=["h100_8x"], precisions=["bf16"],
71+
))
72+
cell = res.cells[0]
73+
breakdown = cell.breakdown
74+
# Sum of the parts is the cell total.
75+
assert (breakdown["weights"] + breakdown["grads"] + breakdown["optimizer"]
76+
+ breakdown["activations"] + breakdown["kv_cache"]
77+
+ breakdown["edge_handoff"]) == cell.bytes
78+
79+
80+
def test_fits_flag_reflects_device_hbm():
81+
"""All cells with bytes < gb10's HBM × headroom must fit."""
82+
res = memory_matrix(MemoryMatrixParams(
83+
spec=VerifyParams(**_mini_spec()),
84+
topologies=["gb10_quarter"], precisions=["bf16"],
85+
headroom=0.9,
86+
))
87+
cell = res.cells[0]
88+
assert cell.fits == (cell.bytes <= int(cell.device_hbm_bytes * 0.9))
89+
90+
91+
def test_unknown_topology_rejected():
92+
with pytest.raises(ValueError, match="unknown topology"):
93+
memory_matrix(MemoryMatrixParams(
94+
spec=VerifyParams(**_mini_spec()),
95+
topologies=["nonexistent_zzz"],
96+
))
97+
98+
99+
def test_unknown_precision_rejected():
100+
with pytest.raises(ValueError, match="unknown precision"):
101+
memory_matrix(MemoryMatrixParams(
102+
spec=VerifyParams(**_mini_spec()),
103+
precisions=["int1024"],
104+
))
105+
106+
107+
def test_invalid_headroom_rejected():
108+
with pytest.raises(ValueError, match="headroom"):
109+
memory_matrix(MemoryMatrixParams(
110+
spec=VerifyParams(**_mini_spec()),
111+
headroom=1.5,
112+
))
113+
114+
115+
def test_dispatch_end_to_end():
116+
req = JsonRpcRequest(
117+
jsonrpc="2.0", id="t-r03",
118+
method="memory.matrix",
119+
params={
120+
"spec": _mini_spec(),
121+
"topologies": ["m3_ultra_solo", "h100_8x"],
122+
"precisions": ["bf16", "mxfp4"],
123+
},
124+
)
125+
resp = dispatch(req)
126+
assert resp.error is None, resp.error
127+
r = resp.result
128+
assert len(r["cells"]) == 4
129+
by = {(c["topology"], c["precision"]): c["bytes"] for c in r["cells"]}
130+
assert by[("h100_8x", "mxfp4")] < by[("h100_8x", "bf16")]
131+
132+
133+
def test_precision_bytes_table_matches_spec():
134+
"""V8 spec §3 nominates the canonical precision -> bytes map."""
135+
assert PRECISION_BYTES["fp32"] == 4.0
136+
assert PRECISION_BYTES["bf16"] == 2.0
137+
assert PRECISION_BYTES["fp16"] == 2.0
138+
assert PRECISION_BYTES["fp8"] == 1.0
139+
assert PRECISION_BYTES["mxfp4"] == 0.5
140+
141+
142+
def test_all_default_topologies_buildable():
143+
"""Each named topology builder produces a usable DeviceTopology."""
144+
for name, factory in TOPOLOGY_BUILDERS.items():
145+
topo = factory()
146+
assert topo.total_hbm_bytes > 0, name

0 commit comments

Comments
 (0)