Skip to content

Commit 200041d

Browse files
physicsrobclaude
andcommitted
Emit annotate() label paths in the debug sidecar
OnnxDebugSession had no way to recover the annotate()/@Annotated label hierarchy a node carried at compile time — annotations lived only on the in-process graph. Key them by canonical node id (matching placements and the residual states) so they round-trip onto a fresh rebuild via the new OnnxDebugSession.annotation(node) accessor. Test Suite Status: Unknown Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c4f1856 commit 200041d

3 files changed

Lines changed: 75 additions & 4 deletions

File tree

tests/debug/test_onnx_debug_session.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,43 @@ def test_sidecar_written_with_expected_schema(token_artifact):
125125
assert sidecar["assert_coverage"]["n_asserts"] > 0
126126

127127

128+
def _build_annotated_graph():
129+
"""Small graph whose nodes carry nested ``annotate()`` label paths."""
130+
from torchwright.graph.node import annotate
131+
132+
a = create_input("a", 1)
133+
b = create_input("b", 1)
134+
with annotate("scene"):
135+
prod = signed_multiply(a, b, max_abs1=10, max_abs2=10)
136+
with annotate("scene/sum"):
137+
out = add(prod, multiply_const(a, 2.0))
138+
return out, create_pos_encoding(), prod, out
139+
140+
141+
def test_sidecar_carries_annotations(tmp_path):
142+
"""The ``annotate()`` label path round-trips through the debug sidecar
143+
and onto OnnxDebugSession.annotation() against a fresh rebuild."""
144+
out, pos, prod, top = _build_annotated_graph()
145+
onnx_path = str(tmp_path / "annotated.onnx")
146+
compile_headless_to_onnx(
147+
out, pos, onnx_path, d=D, d_head=D_HEAD, max_seq_len=16, verbose=False
148+
)
149+
150+
with open(debug_meta_path_for(onnx_path)) as f:
151+
sidecar = json.load(f)
152+
# The annotated nodes' paths are present; "scene/sum" is deduped, not
153+
# "scene/scene/sum".
154+
labels = set(sidecar["annotations"].values())
155+
assert "scene" in labels
156+
assert "scene/sum" in labels
157+
158+
# Round-trip onto a fresh rebuild (new node ids) via the session.
159+
out2, pos2, prod2, top2 = _build_annotated_graph()
160+
sess = OnnxDebugSession(onnx_path, out2, pos2)
161+
assert sess.annotation(prod2) == "scene"
162+
assert sess.annotation(top2) == "scene/sum"
163+
164+
128165
# ---------------------------------------------------------------------------
129166
# Parity with CompiledHeadless
130167
# ---------------------------------------------------------------------------

torchwright/compiler/export.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -372,10 +372,12 @@ def _write_debug_sidecar(
372372
The sidecar carries the residual assignment keyed by CANONICAL node
373373
id (see :mod:`torchwright.compiler.graph_identity`) per capture
374374
state, a structural fingerprint of the compiled graph for rebuild
375-
validation, and the Assert/DebugWatch coverage present at compile
376-
time (so the loader can warn when a rebuilt graph carries fewer
377-
checks than the compiled one did — the fingerprint is deliberately
378-
wrapper-transparent and cannot see that).
375+
validation, the ``annotate()`` label path for every reachable node
376+
that carries one (also keyed by canonical id), and the
377+
Assert/DebugWatch coverage present at compile time (so the loader can
378+
warn when a rebuilt graph carries fewer checks than the compiled one
379+
did — the fingerprint is deliberately wrapper-transparent and cannot
380+
see that).
379381
380382
State keys correspond one-to-one with the per-layer residual tensor
381383
names in the emitted ONNX graph: ``"input"`` ↔ ``res_0``,
@@ -392,6 +394,7 @@ def _write_debug_sidecar(
392394
canonical_ids,
393395
debug_fingerprint,
394396
encode_cols,
397+
nodes_by_canonical_id,
395398
unwrap_debug,
396399
)
397400

@@ -437,6 +440,16 @@ def _write_debug_sidecar(
437440
if unwrap_debug(a.inputs[0]).node_id in canon
438441
}
439442
)
443+
# Annotation paths (the "/"-separated label hierarchy set by
444+
# ``annotate()`` / ``@annotated``) keyed by canonical node id, for
445+
# every reachable node that carries one. Wrappers are stepped
446+
# through by ``_canonical_walk``, so this reads the annotation on the
447+
# wrapped node — the same node the residual states key by.
448+
annotations = {
449+
str(cid): node.annotation
450+
for cid, node in nodes_by_canonical_id(out).items()
451+
if node.annotation is not None
452+
}
440453
matrices, placements, n_heads, d_hidden_per_layer = _build_matrix_occupancy(
441454
compiled, canon, d, d_head
442455
)
@@ -451,6 +464,7 @@ def _write_debug_sidecar(
451464
"n_layers": len(compiled.layers),
452465
"matrices": matrices,
453466
"placements": placements,
467+
"annotations": annotations,
454468
"input_specs": [list(spec) for spec in input_specs],
455469
"cache_stride": int(cache_stride),
456470
"cache_window": int(cache_window) if cache_window is not None else None,

torchwright/debug/onnx_debug.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,16 @@ def __init__(
220220
ra.assign(st, node, decode_cols(runs))
221221
self._ra = ra
222222
self._key_to_state = key_to_state
223+
224+
# --- Annotation paths from the sidecar, keyed by canonical id and
225+
# mapped onto the rebuilt graph's live node objects so callers can
226+
# look one up by node (mirroring node.annotation on the in-process
227+
# backend). Reachable-but-unannotated nodes are simply absent.
228+
self._annotation_by_node_id: Dict[int, str] = {}
229+
for cid_str, label in (sidecar.get("annotations") or {}).items():
230+
node = by_canon.get(int(cid_str))
231+
if node is not None:
232+
self._annotation_by_node_id[node.node_id] = label
223233
# Post-MLP triples — the ordered states every check/probe scans,
224234
# mirroring the in-process backend.
225235
self._ordered: List[tuple] = [
@@ -525,6 +535,16 @@ def debug_value(self, node: Node) -> Optional[torch.Tensor]:
525535
res_tensor, _ = tensor_pair
526536
return extract_compiled_value(node, ds.ra, state, res_tensor)
527537

538+
def annotation(self, node: Node) -> Optional[str]:
539+
"""The ``annotate()`` path recorded for ``node`` at compile time.
540+
541+
Looks the rebuilt ``node`` up against the annotations the sidecar
542+
captured (keyed by canonical id). Returns ``None`` for nodes that
543+
carried no annotation when compiled, or that aren't reachable from
544+
the output. Assert/DebugWatch wrappers are unwrapped first.
545+
"""
546+
return self._annotation_by_node_id.get(unwrap_debug(node).node_id)
547+
528548
# ---- DebugRuntime protocol surface (probe entry points) ----------------
529549

530550
def debug_layout(self) -> tuple:

0 commit comments

Comments
 (0)