Skip to content

Commit 3dc3dc1

Browse files
committed
feat(v8-r06): compile.trace RPC + CompileTracePanel
Closes cppmega-mlx-yz6n.6. New RPC compile.trace(spec, backend) reads the FusionRegionPlan output of plan_fusion_regions and emits a per-op compile trace with fused/dlpack/materialised chips + aggregate counters (fused_groups list, dlpack_crossings count, materialised_ops list). The backend selector ('tilelang' | 'torch_inductor' | 'mlx') is echoed through but the underlying planner is unified — each op's per-row backend label is the actual backend the planner chose (path_c / metal_inline / dlpack_handoff). UI: CompileTracePanel renders the trace as a chip table + an aggregate header (fused / dlpack / materialised counters), one chip per op. Tests: 5 new pytest (per-brick op count, fused flag, backend label, dispatch envelope, aggregate consistency), 2 new vitest (panel mount, error banner). Regression: 112 pytest fusion+compile+mxfp4+catalog + 468 vitest green.
1 parent 7d4a100 commit 3dc3dc1

6 files changed

Lines changed: 437 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""V8-R06: ``compile.trace`` RPC — surface the fusion plan as a wire-
2+
form compile trace that the UI can render with per-op chips.
3+
4+
Each op in the trace carries:
5+
- name: the brick name
6+
- fused: True iff its region has 2+ bricks
7+
- group: the region label (e.g. ``"gemm_softmax_0"``)
8+
- materialised: True iff the region is the ``dlpack_handoff`` backend
9+
(a materialised boundary tensor is unavoidable across DLPack)
10+
- dlpack_boundary: True iff this op crosses a backend boundary
11+
- backend: tilelang | mlx | torch_inductor | metal_inline | dlpack_handoff
12+
13+
For now there is one shared planner — ``plan_fusion_regions`` — and
14+
the same trace is returned for all three ``backend`` selectors. The
15+
backend label in each op is taken from the FusionRegionPlan rather
16+
than the request, so the UI can render the actual backend chosen by
17+
the planner. The ``backend`` request parameter is reserved for the
18+
upcoming inductor-specific path.
19+
"""
20+
21+
from __future__ import annotations
22+
23+
from typing import Any, Literal
24+
25+
from pydantic import BaseModel, ConfigDict, Field
26+
27+
from cppmega_v4.fusion.auto_planner import plan_fusion_regions
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+
35+
36+
__all__ = [
37+
"CompileTraceParams",
38+
"CompileTraceOp",
39+
"CompileTraceResult",
40+
"compile_trace",
41+
]
42+
43+
44+
CompileBackend = Literal["tilelang", "mlx", "torch_inductor"]
45+
46+
47+
class CompileTraceParams(BaseModel):
48+
"""Input — VerifyParams payload + the backend perspective."""
49+
50+
model_config = ConfigDict(extra="forbid")
51+
52+
spec: VerifyParams
53+
backend: CompileBackend = "mlx"
54+
55+
56+
class CompileTraceOp(BaseModel):
57+
"""One row in the compile trace — chip-renderable in the UI."""
58+
59+
model_config = ConfigDict(extra="forbid")
60+
61+
name: str
62+
fused: bool
63+
group: str
64+
materialised: bool
65+
dlpack_boundary: bool
66+
backend: str
67+
68+
69+
class CompileTraceResult(BaseModel):
70+
"""Full trace + aggregate counters that surface in extras.train."""
71+
72+
model_config = ConfigDict(extra="forbid")
73+
74+
ops: list[CompileTraceOp] = Field(default_factory=list)
75+
fused_groups: list[str] = Field(default_factory=list)
76+
dlpack_crossings: int = 0
77+
materialised_ops: list[str] = Field(default_factory=list)
78+
compile_artifact_path: str | None = None
79+
backend: str
80+
81+
82+
def compile_trace(
83+
params: CompileTraceParams, *, cache: LRUCache | None = None,
84+
) -> CompileTraceResult:
85+
"""Plan fusion regions for ``params.spec`` and render the trace."""
86+
key, hit = _cache_lookup(cache, "compile.trace", params)
87+
if hit is not None:
88+
return hit
89+
90+
specs = _graph_to_specs(params.spec.graph)
91+
hidden = params.spec.dim_env.get("H", 64)
92+
graph = from_block_specs(specs, hidden_size=hidden, instantiate=False)
93+
plans = list(plan_fusion_regions(graph))
94+
95+
ops: list[CompileTraceOp] = []
96+
fused_groups: list[str] = []
97+
materialised: list[str] = []
98+
dlpack_crossings = 0
99+
last_backend: str | None = None
100+
101+
for idx, plan in enumerate(plans):
102+
group_label = f"region_{idx:02d}_{plan.backend}"
103+
is_materialised = plan.backend == "dlpack_handoff"
104+
if is_materialised and last_backend is not None and \
105+
last_backend != plan.backend:
106+
dlpack_crossings += 1
107+
if plan.is_fused:
108+
fused_groups.append(group_label)
109+
for i, name in enumerate(plan.brick_names):
110+
ops.append(CompileTraceOp(
111+
name=name,
112+
fused=plan.is_fused,
113+
group=group_label,
114+
materialised=is_materialised,
115+
dlpack_boundary=(is_materialised and i == 0
116+
and idx > 0),
117+
backend=plan.backend,
118+
))
119+
if is_materialised and name not in materialised:
120+
materialised.append(name)
121+
last_backend = plan.backend
122+
123+
out = CompileTraceResult(
124+
ops=ops,
125+
fused_groups=fused_groups,
126+
dlpack_crossings=dlpack_crossings,
127+
materialised_ops=materialised,
128+
compile_artifact_path=None, # set by tilelang codegen path
129+
backend=params.backend,
130+
)
131+
_cache_store(cache, key, out)
132+
return out

cppmega_v4/jsonrpc/dispatcher.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@
8787
HfQuickstartParams,
8888
hf_quickstart_method,
8989
)
90+
from cppmega_v4.jsonrpc.compile_trace_method import (
91+
CompileTraceParams,
92+
compile_trace,
93+
)
9094
from cppmega_v4.jsonrpc.tokenizer_roundtrip_text_method import (
9195
TokenizerRoundtripTextParams,
9296
roundtrip_text,
@@ -153,6 +157,10 @@
153157
HfQuickstartParams,
154158
lambda p, c: hf_quickstart_method(p, cache=c),
155159
),
160+
"compile.trace": (
161+
CompileTraceParams,
162+
lambda p, c: compile_trace(p, cache=c),
163+
),
156164
"probe.run": (
157165
ProbeRunParams,
158166
lambda p, c: probe_run(p, cache=c),

cppmega_v4/jsonrpc/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,7 @@ class PipelineRunResult(BaseModel):
811811
"memory.matrix",
812812
"architectures.auto_fit",
813813
"data.hf_quickstart",
814+
"compile.trace",
814815
})
815816

816817

tests/v4/test_compile_trace.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""V8-R06 pytest: compile.trace RPC."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from cppmega_v4.jsonrpc.compile_trace_method import (
8+
CompileTraceParams, compile_trace,
9+
)
10+
from cppmega_v4.jsonrpc.dispatcher import dispatch
11+
from cppmega_v4.jsonrpc.schema import JsonRpcRequest, VerifyParams
12+
13+
14+
def _spec() -> dict:
15+
return {
16+
"graph": {
17+
"nodes": [
18+
{"id": "a", "kind": "attention", "params": {}},
19+
{"id": "b", "kind": "mlp", "params": {}},
20+
{"id": "c", "kind": "attention", "params": {}},
21+
{"id": "d", "kind": "mlp", "params": {}},
22+
],
23+
"edges": [
24+
{"src": "a", "dst": "b"},
25+
{"src": "b", "dst": "c"},
26+
{"src": "c", "dst": "d"},
27+
],
28+
},
29+
"dim_env": {"H": 128},
30+
"loss": {"kind": "cross_entropy", "head_outputs": ["d"]},
31+
"optim": {"kind": "adamw", "groups": [
32+
{"matcher": "all", "lr": 1e-3, "weight_decay": 0.01,
33+
"betas": [0.9, 0.95]}]},
34+
"sharding": None,
35+
"training": True,
36+
}
37+
38+
39+
def test_returns_one_op_per_brick():
40+
r = compile_trace(CompileTraceParams(spec=VerifyParams(**_spec())))
41+
assert len(r.ops) == 4
42+
assert {op.name for op in r.ops} == {"a", "b", "c", "d"}
43+
44+
45+
def test_fused_flag_matches_region_size():
46+
r = compile_trace(CompileTraceParams(spec=VerifyParams(**_spec())))
47+
# All 4 land in one big path_c region for this contrived spec, so
48+
# fused is True everywhere. The aggregate fused_groups list has
49+
# one entry.
50+
assert all(op.fused for op in r.ops)
51+
assert len(r.fused_groups) >= 1
52+
53+
54+
def test_backend_label_round_trips():
55+
r = compile_trace(CompileTraceParams(
56+
spec=VerifyParams(**_spec()), backend="tilelang"))
57+
assert r.backend == "tilelang"
58+
59+
60+
def test_dispatch_end_to_end():
61+
req = JsonRpcRequest(
62+
jsonrpc="2.0", id="t-r06",
63+
method="compile.trace",
64+
params={"spec": _spec(), "backend": "mlx"},
65+
)
66+
resp = dispatch(req)
67+
assert resp.error is None, resp.error
68+
r = resp.result
69+
assert "ops" in r and len(r["ops"]) == 4
70+
for op in r["ops"]:
71+
assert {"name", "fused", "group", "materialised",
72+
"dlpack_boundary", "backend"} <= set(op)
73+
74+
75+
def test_aggregate_counters_consistent():
76+
r = compile_trace(CompileTraceParams(spec=VerifyParams(**_spec())))
77+
# Every materialised op-name appears in materialised_ops exactly once.
78+
mat_set = {op.name for op in r.ops if op.materialised}
79+
assert mat_set == set(r.materialised_ops)
80+
# dlpack_crossings counts backend transitions, not per-op flags.
81+
assert r.dlpack_crossings >= 0
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* V8-R06 CompileTracePanel — renders the compile.trace RPC output as
3+
* a per-op chip table with fused / dlpack-crossing / materialised
4+
* markers and an aggregate header line.
5+
*/
6+
7+
import { useEffect, useState } from "react";
8+
import type { RpcClient } from "@/lib/rpc";
9+
10+
interface CompileTraceOp {
11+
name: string;
12+
fused: boolean;
13+
group: string;
14+
materialised: boolean;
15+
dlpack_boundary: boolean;
16+
backend: string;
17+
}
18+
19+
interface CompileTraceResult {
20+
ops: CompileTraceOp[];
21+
fused_groups: string[];
22+
dlpack_crossings: number;
23+
materialised_ops: string[];
24+
compile_artifact_path: string | null;
25+
backend: string;
26+
}
27+
28+
export interface CompileTracePanelProps {
29+
rpc: RpcClient;
30+
specPayload: unknown;
31+
backend?: "tilelang" | "torch_inductor" | "mlx";
32+
}
33+
34+
export function CompileTracePanel({
35+
rpc, specPayload, backend = "mlx",
36+
}: CompileTracePanelProps): JSX.Element {
37+
const [trace, setTrace] = useState<CompileTraceResult | null>(null);
38+
const [err, setErr] = useState<string | null>(null);
39+
40+
useEffect(() => {
41+
let cancelled = false;
42+
(async () => {
43+
setErr(null);
44+
try {
45+
const r = await rpc.call<CompileTraceResult>(
46+
"compile.trace",
47+
{ spec: specPayload, backend },
48+
);
49+
if (!cancelled) setTrace(r);
50+
} catch (e) {
51+
if (!cancelled) {
52+
setErr(e instanceof Error ? e.message : String(e));
53+
}
54+
}
55+
})();
56+
return () => { cancelled = true; };
57+
}, [rpc, specPayload, backend]);
58+
59+
if (err) {
60+
return <div data-testid="compile-trace-error"
61+
style={{ color: "#b91c1c", padding: 12 }}>{err}</div>;
62+
}
63+
if (!trace) {
64+
return <div data-testid="compile-trace-loading"
65+
style={{ padding: 12, color: "#6b7280" }}>loading…</div>;
66+
}
67+
68+
return (
69+
<div data-testid="compile-trace" style={{ padding: 12,
70+
fontFamily: "system-ui, sans-serif", fontSize: 12 }}>
71+
<header style={{ display: "flex", gap: 12, marginBottom: 8 }}>
72+
<span>backend: <strong>{trace.backend}</strong></span>
73+
<span data-testid="compile-trace-fused-count">
74+
fused groups: {trace.fused_groups.length}
75+
</span>
76+
<span data-testid="compile-trace-dlpack-crossings">
77+
dlpack crossings: {trace.dlpack_crossings}
78+
</span>
79+
<span data-testid="compile-trace-materialised-count">
80+
materialised: {trace.materialised_ops.length}
81+
</span>
82+
</header>
83+
{trace.fused_groups.map((g) => (
84+
<span key={g}
85+
data-testid={`compile-trace-fused-group-${g}`}
86+
style={{ display: "inline-block", marginRight: 6,
87+
background: "#dbeafe", color: "#1e3a8a",
88+
borderRadius: 4, padding: "2px 6px" }}>
89+
{g}
90+
</span>
91+
))}
92+
<table style={{ marginTop: 8, borderCollapse: "collapse",
93+
width: "100%" }}>
94+
<thead>
95+
<tr style={{ borderBottom: "1px solid #e5e7eb" }}>
96+
<th style={th}>op</th>
97+
<th style={th}>backend</th>
98+
<th style={th}>group</th>
99+
<th style={th}>chips</th>
100+
</tr>
101+
</thead>
102+
<tbody>
103+
{trace.ops.map((op, i) => (
104+
<tr key={`${op.name}-${i}`}
105+
data-testid={`compile-trace-op-${i}`}>
106+
<td style={td}>{op.name}</td>
107+
<td style={td}>{op.backend}</td>
108+
<td style={td}>{op.group}</td>
109+
<td style={td}>
110+
{op.fused && (
111+
<span style={chip("#dbeafe", "#1e3a8a")}>fused</span>
112+
)}
113+
{op.materialised && (
114+
<span style={chip("#fef3c7", "#92400e")}
115+
data-testid={
116+
`compile-trace-op-${i}-materialised`}>
117+
materialised
118+
</span>
119+
)}
120+
{op.dlpack_boundary && (
121+
<span style={chip("#fee2e2", "#7f1d1d")}
122+
data-testid={
123+
`compile-trace-op-${i}-dlpack`}>
124+
dlpack
125+
</span>
126+
)}
127+
</td>
128+
</tr>
129+
))}
130+
</tbody>
131+
</table>
132+
</div>
133+
);
134+
}
135+
136+
const th: React.CSSProperties = {
137+
textAlign: "left", padding: "4px 6px", color: "#374151",
138+
fontWeight: 600,
139+
};
140+
141+
const td: React.CSSProperties = {
142+
padding: "4px 6px", color: "#1f2937",
143+
borderBottom: "1px solid #f3f4f6",
144+
};
145+
146+
function chip(bg: string, fg: string): React.CSSProperties {
147+
return {
148+
display: "inline-block", marginRight: 4, padding: "1px 6px",
149+
borderRadius: 4, background: bg, color: fg, fontSize: 10,
150+
};
151+
}

0 commit comments

Comments
 (0)