Skip to content

Commit ba36fa4

Browse files
committed
feat(e7-2): dim auto-adjust feedback panel — inference_log + DimensionsTab
Stage E7-2 of E2E Coverage Matrix v2 epic (cppmega-mlx-bb0.2). Surfaces 'why' behind every auto-derived brick parameter so the user sees attn_0.num_heads = 2 because H=128/head_dim=64. cppmega_v4/spec/inference_log.py (NEW): - InferenceEntry dataclass (brick / param / value / source / reason). - build_inference_log(graph, dim_env) walks every node: * attention family: num_heads + head_dim (auto-derived from H/head_dim when not user-set, with reason 'H=H/head_dim=hd → derived') * mlp / gated_mlp: intermediate_size (auto = 4*H) + activation (auto default 'glu') * moe: num_experts + top_k (auto from dim_env defaults) * other kinds: emit one 'user' row per provided param cppmega_v4/jsonrpc/: - schema.py: new InferenceEntryPayload + VerifyResult.inference_log field (default empty list, backwards-compatible). - methods.py: verify handler now calls build_inference_log on the parsed graph + dim_env and emits the payload list. vbgui/src/components/sidebar/DimensionsTab.tsx (NEW): - 5-column table (Brick / Param / Value / Source / Reason); source badge in blue for 'auto', grey for 'user'. - Source filter dropdown (all / auto / user) + brick substring filter. - Row click → onHighlight callback fires with brick id; App wires to setSelectedBrickId so the matching BrickContextPanel opens. vbgui/src/components/Sidebar.tsx: - SidebarTab union extended with 'dimensions'. - TAB_LABELS gains the new tab. - inferenceLog + onHighlightBrick props pass through to DimensionsTab. vbgui/src/App.tsx: - inferenceLog state captured from each verify response. - Sidebar wired with inferenceLog={inferenceLog} + onHighlightBrick={setSelectedBrickId} — row clicks open the BrickContextPanel for that node. Tests: - tests/v4/test_inference_log.py (+10 pytest): * attention auto-derives num_heads from H/head_dim * user override marked source='user' * mlp intermediate_size defaults to 4*H * activation defaults to 'glu' (backward-compat) * activation override marked 'user' * moe pulls num_experts/top_k from dim_env * other brick kinds emit user rows for provided params * verify RPC response carries inference_log end-to-end * every entry has source in {user, auto} + non-empty reason - vbgui/tests/DimensionsTab.test.tsx (+6 vitest): * one row per entry * source badge text matches (auto/user) * source filter restricts visibility * brick substring filter * row click fires onHighlight with brick id * empty log shows 'No matching entries' Regression: pytest 2270 (was 2260; +10) / 2 skip / 15 xfail / 0 fail (excl. pre-existing path_d). vbgui vitest 150 (was 144; +6) / 0 fail. Closes cppmega-mlx-bb0.2.
1 parent c1bc6f7 commit ba36fa4

8 files changed

Lines changed: 646 additions & 8 deletions

File tree

cppmega_v4/jsonrpc/methods.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,24 @@ def verify(params: VerifyParams, *, cache: LRUCache | None = None) -> VerifyResu
306306
for fp in fusion_plan
307307
]
308308

309+
# E7-2: build inference log so the UI Dimensions tab can show why
310+
# each per-brick parameter has the value it does (user-set vs
311+
# auto-derived from dim_env).
312+
from cppmega_v4.spec.inference_log import build_inference_log
313+
from cppmega_v4.jsonrpc.schema import InferenceEntryPayload
314+
inference_log_raw = build_inference_log(
315+
{"nodes": [{"id": n.id, "kind": n.kind, "params": dict(n.params)}
316+
for n in params.graph.nodes]},
317+
dict(params.dim_env),
318+
)
319+
inference_log_payload = [
320+
InferenceEntryPayload(
321+
brick=e.brick, param=e.param, value=e.value,
322+
source=e.source, reason=e.reason,
323+
)
324+
for e in inference_log_raw
325+
]
326+
309327
elapsed_ms = (time.perf_counter() - t0) * 1000.0
310328
out = VerifyResult(
311329
resolved=ResolvedGraph(
@@ -317,6 +335,7 @@ def verify(params: VerifyParams, *, cache: LRUCache | None = None) -> VerifyResu
317335
memory_distributed=distributed_payload,
318336
gotchas=gotcha_payloads,
319337
fusion_plan=fusion_payloads,
338+
inference_log=inference_log_payload,
320339
elapsed_ms=elapsed_ms,
321340
)
322341
_cache_store(cache, key, out)

cppmega_v4/jsonrpc/schema.py

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pydantic import BaseModel, ConfigDict, Field
1616

1717

18-
SCHEMA_VERSION: str = "1.0.0"
18+
SCHEMA_VERSION: str = "1.1.0"
1919

2020
JsonRpcVersion = Literal["2.0"]
2121

@@ -196,6 +196,137 @@ class RewriterPayload(BaseModel):
196196
params: dict[str, Any] = Field(default_factory=dict)
197197

198198

199+
SideChannelModePayload = Literal["off", "auto", "require", "if_available"]
200+
SideChannelEmbeddingPayload = Literal[
201+
"categorical", "numeric_bucket", "span", "edge_bias", "none"
202+
]
203+
SideChannelFallbackPayload = Literal[
204+
"zeros", "unknown_id", "drop_family", "error"
205+
]
206+
InferenceEnrichmentSourcePayload = Literal[
207+
"none", "prompt_only", "parse_if_possible", "project_index", "auto"
208+
]
209+
InferenceFailPolicyPayload = Literal["drop_family", "text_only", "error"]
210+
PackingPolicyPayload = Literal["sequential", "best_fit"]
211+
212+
213+
class FamilySpecPayload(BaseModel):
214+
"""Wire form of one side-channel family policy."""
215+
216+
model_config = ConfigDict(extra="forbid")
217+
218+
mode: SideChannelModePayload = "if_available"
219+
columns: list[str] = Field(default_factory=list)
220+
embedding: SideChannelEmbeddingPayload = "categorical"
221+
dropout: float = Field(default=0.0, ge=0.0, le=1.0)
222+
residual_scale: float = Field(default=1.0, ge=0.0)
223+
fallback: SideChannelFallbackPayload = "drop_family"
224+
language_scope: list[str] = Field(default_factory=lambda: ["any"])
225+
226+
227+
class InferenceEnrichmentSpecPayload(BaseModel):
228+
"""Wire form of inference-time side-channel enrichment policy."""
229+
230+
model_config = ConfigDict(extra="forbid")
231+
232+
source: InferenceEnrichmentSourcePayload = "auto"
233+
fail_policy: InferenceFailPolicyPayload = "drop_family"
234+
timeout_ms: int = Field(default=500, ge=0)
235+
cache_enabled: bool = True
236+
237+
238+
def _default_side_channel_family_payloads() -> dict[str, FamilySpecPayload]:
239+
return {
240+
"platform": FamilySpecPayload(
241+
mode="auto",
242+
columns=["platform_ids", "source_platform_ids"],
243+
embedding="categorical",
244+
dropout=0.10,
245+
),
246+
"syntax": FamilySpecPayload(
247+
mode="if_available",
248+
columns=[
249+
"token_ast_depth",
250+
"token_sibling_index",
251+
"token_ast_node_type",
252+
],
253+
embedding="categorical",
254+
dropout=0.25,
255+
),
256+
"structure": FamilySpecPayload(
257+
mode="if_available",
258+
columns=[
259+
"token_structure_ids",
260+
"token_dep_levels",
261+
"token_chunk_starts",
262+
"token_chunk_ends",
263+
"token_chunk_kinds",
264+
"token_chunk_dep_levels",
265+
],
266+
embedding="categorical",
267+
dropout=0.25,
268+
),
269+
"semantic_graph": FamilySpecPayload(
270+
mode="if_available",
271+
columns=[
272+
"token_symbol_ids",
273+
"token_call_targets",
274+
"token_type_refs",
275+
"token_def_use",
276+
"token_call_edges",
277+
"token_type_edges",
278+
],
279+
embedding="edge_bias",
280+
dropout=0.50,
281+
),
282+
"temporal_diff": FamilySpecPayload(
283+
mode="off",
284+
columns=[
285+
"token_change_mask_pre",
286+
"token_change_mask_post",
287+
"hunk_id_per_token",
288+
"edit_op_per_token",
289+
],
290+
embedding="categorical",
291+
dropout=0.0,
292+
),
293+
}
294+
295+
296+
class SideChannelSpecPayload(BaseModel):
297+
"""Wire form of the generic side-channel conditioning policy."""
298+
299+
model_config = ConfigDict(extra="forbid")
300+
301+
mode: SideChannelModePayload = "auto"
302+
families: dict[str, FamilySpecPayload] = Field(
303+
default_factory=_default_side_channel_family_payloads
304+
)
305+
inference: InferenceEnrichmentSpecPayload = Field(
306+
default_factory=InferenceEnrichmentSpecPayload
307+
)
308+
309+
310+
class DataMaterializationSpecPayload(BaseModel):
311+
"""Wire form of packed-row parquet materialization policy."""
312+
313+
model_config = ConfigDict(extra="forbid")
314+
315+
packing_policy: PackingPolicyPayload = "best_fit"
316+
max_seq_len: int = Field(default=4096, gt=0)
317+
pad_to_max: bool = True
318+
include_provenance: bool = True
319+
required_token_fields: list[str] = Field(default_factory=lambda: [
320+
"input_ids",
321+
"target_ids",
322+
"loss_mask",
323+
"doc_ids",
324+
"pack_id",
325+
"valid_token_count",
326+
"num_docs",
327+
])
328+
329+
199330
# ---------------------------------------------------------------------------
200331
# `verify` — primary endpoint, full validation pass.
201332
# ---------------------------------------------------------------------------
@@ -213,6 +344,9 @@ class VerifyParams(BaseModel):
213344
rewriters: list[RewriterPayload] = Field(default_factory=list)
214345
sharding: ShardingSpecPayload | None = None
215346
training: bool = True
347+
side_channels: SideChannelSpecPayload = Field(
348+
default_factory=SideChannelSpecPayload
349+
)
216350
available_side_channels: list[str] = Field(default_factory=lambda: ["doc_ids", "token_ids"])
217351

218352

@@ -312,6 +446,20 @@ class FusionRegionPayload(BaseModel):
312446
estimated_savings_us: float = 0.0
313447

314448

449+
class InferenceEntryPayload(BaseModel):
450+
"""One dimension-inference entry (E7-2). Surfaces in the
451+
Dimensions sidebar tab so users see why num_heads=2 came out as it
452+
did (e.g. H=128/head_dim=64 → 2)."""
453+
454+
model_config = ConfigDict(extra="forbid")
455+
456+
brick: str
457+
param: str
458+
value: Any
459+
source: Literal["user", "auto"]
460+
reason: str
461+
462+
315463
class VerifyResult(BaseModel):
316464
"""Wire form of one ``verify`` response."""
317465

@@ -322,6 +470,7 @@ class VerifyResult(BaseModel):
322470
memory_distributed: DistributedMemoryPayload | None = None
323471
gotchas: list[GotchaPayload] = Field(default_factory=list)
324472
fusion_plan: list[FusionRegionPayload] = Field(default_factory=list)
473+
inference_log: list[InferenceEntryPayload] = Field(default_factory=list)
325474
elapsed_ms: float
326475

327476

cppmega_v4/spec/inference_log.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Dim auto-adjust feedback log (E7-2).
2+
3+
Walks a BrickGraph + dim_env and synthesises an :class:`InferenceEntry`
4+
per (brick, parameter) row describing whether the value was provided by
5+
the user (source='user') or auto-derived from dim_env (source='auto'),
6+
plus a one-line ``reason`` like ``"H=128/head_dim=64 → 2"``.
7+
8+
Renders in the Dimensions sidebar tab. Click a row → highlights the
9+
node on the canvas.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from dataclasses import dataclass
15+
from typing import Any, Literal
16+
17+
18+
@dataclass(frozen=True)
19+
class InferenceEntry:
20+
brick: str
21+
param: str
22+
value: Any
23+
source: Literal["user", "auto"]
24+
reason: str
25+
26+
27+
def _attention_entries(name: str, params: dict, dim_env: dict) -> list[InferenceEntry]:
28+
H = dim_env.get("H", 128)
29+
head_dim = params.get("head_dim", dim_env.get("head_dim", 64))
30+
out: list[InferenceEntry] = []
31+
32+
if "head_dim" in params:
33+
out.append(InferenceEntry(
34+
brick=name, param="head_dim", value=params["head_dim"],
35+
source="user", reason="provided in BrickSpec.params",
36+
))
37+
else:
38+
out.append(InferenceEntry(
39+
brick=name, param="head_dim", value=head_dim,
40+
source="auto",
41+
reason=f"default from dim_env.head_dim={dim_env.get('head_dim', 64)}",
42+
))
43+
44+
if "num_heads" in params:
45+
out.append(InferenceEntry(
46+
brick=name, param="num_heads", value=params["num_heads"],
47+
source="user", reason="provided in BrickSpec.params",
48+
))
49+
else:
50+
derived = max(1, H // max(1, head_dim))
51+
out.append(InferenceEntry(
52+
brick=name, param="num_heads", value=derived,
53+
source="auto",
54+
reason=f"H={H}/head_dim={head_dim}{derived}",
55+
))
56+
return out
57+
58+
59+
def _mlp_entries(name: str, params: dict, dim_env: dict) -> list[InferenceEntry]:
60+
H = dim_env.get("H", 128)
61+
out: list[InferenceEntry] = []
62+
if "intermediate_size" in params:
63+
out.append(InferenceEntry(
64+
brick=name, param="intermediate_size",
65+
value=params["intermediate_size"],
66+
source="user", reason="provided in BrickSpec.params",
67+
))
68+
else:
69+
out.append(InferenceEntry(
70+
brick=name, param="intermediate_size", value=4 * H,
71+
source="auto", reason=f"4 * H ({H}) = {4 * H}",
72+
))
73+
if "activation" in params:
74+
out.append(InferenceEntry(
75+
brick=name, param="activation", value=params["activation"],
76+
source="user", reason="set via BrickContextPanel",
77+
))
78+
else:
79+
out.append(InferenceEntry(
80+
brick=name, param="activation", value="glu",
81+
source="auto",
82+
reason="GLU default preserves legacy sigmoid(gate)*up math",
83+
))
84+
return out
85+
86+
87+
def _moe_entries(name: str, params: dict, dim_env: dict) -> list[InferenceEntry]:
88+
out: list[InferenceEntry] = []
89+
if "num_experts" in params:
90+
out.append(InferenceEntry(
91+
brick=name, param="num_experts", value=params["num_experts"],
92+
source="user", reason="provided in BrickSpec.params",
93+
))
94+
else:
95+
ne = dim_env.get("num_experts", 4)
96+
out.append(InferenceEntry(
97+
brick=name, param="num_experts", value=ne,
98+
source="auto", reason=f"from dim_env.num_experts={ne}",
99+
))
100+
if "top_k" in params:
101+
out.append(InferenceEntry(
102+
brick=name, param="top_k", value=params["top_k"],
103+
source="user", reason="provided in BrickSpec.params",
104+
))
105+
else:
106+
tk = dim_env.get("top_k", 2)
107+
out.append(InferenceEntry(
108+
brick=name, param="top_k", value=tk,
109+
source="auto", reason=f"from dim_env.top_k={tk}",
110+
))
111+
return out
112+
113+
114+
_ATTN_KINDS = frozenset({
115+
"attention", "gated_attention", "mla", "mla_absorb",
116+
"mistral4_mla", "dsv4_attention", "gqa_sliding", "cca_attention",
117+
"gdn", "kda", "nsa",
118+
})
119+
_MLP_KINDS = frozenset({"mlp", "gated_mlp"})
120+
_MOE_KINDS = frozenset({"moe", "bailing_moe"})
121+
122+
123+
def build_inference_log(
124+
graph: dict,
125+
dim_env: dict,
126+
) -> list[InferenceEntry]:
127+
"""Build the full inference log for a graph + dim_env pair.
128+
129+
``graph`` is the wire-form dict with ``nodes: [{id, kind, params}, …]``.
130+
"""
131+
entries: list[InferenceEntry] = []
132+
for node in graph.get("nodes", []):
133+
kind = node.get("kind")
134+
name = node.get("id") or node.get("name") or "<unnamed>"
135+
params = node.get("params", {}) or {}
136+
if kind in _ATTN_KINDS:
137+
entries.extend(_attention_entries(name, params, dim_env))
138+
elif kind in _MLP_KINDS:
139+
entries.extend(_mlp_entries(name, params, dim_env))
140+
elif kind in _MOE_KINDS:
141+
entries.extend(_moe_entries(name, params, dim_env))
142+
# Other brick kinds: emit one stub row per provided param so the
143+
# Dimensions tab still shows them.
144+
else:
145+
for k, v in params.items():
146+
entries.append(InferenceEntry(
147+
brick=name, param=k, value=v,
148+
source="user", reason="provided in BrickSpec.params",
149+
))
150+
return entries
151+
152+
153+
__all__ = ["InferenceEntry", "build_inference_log"]

0 commit comments

Comments
 (0)