@@ -366,18 +366,25 @@ def _write_debug_sidecar(
366366 cache_stride : int ,
367367 cache_window : Optional [int ],
368368 verbose : bool ,
369+ optimize : int = 0 ,
370+ extra : Optional [dict ] = None ,
369371) -> str :
370372 """Write ``<stem>.debug.json`` — everything OnnxDebugSession needs.
371373
372374 The sidecar carries the residual assignment keyed by CANONICAL node
373375 id (see :mod:`torchwright.compiler.graph_identity`) per capture
374376 state, a structural fingerprint of the compiled graph for rebuild
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).
377+ validation, a per-node metadata table (``nodes``) — also keyed by
378+ canonical id, one entry per reachable node, carrying op type,
379+ annotation path, output width, baked-weight parameter count/shapes,
380+ input ids, and the layer/sublayer the node is scheduled into — and
381+ the Assert/DebugWatch coverage present at compile time (so the loader
382+ can warn when a rebuilt graph carries fewer checks than the compiled
383+ one did — the fingerprint is deliberately wrapper-transparent and
384+ cannot see that). ``optimize`` records the compile-optimization
385+ level the artifact was built at; ``extra`` is the caller's free-form
386+ ``extra_metadata`` dict passed straight through (torchwright does not
387+ interpret its keys), mirroring the meta sidecar's ``extra``.
381388
382389 State keys correspond one-to-one with the per-layer residual tensor
383390 names in the emitted ONNX graph: ``"input"`` ↔ ``res_0``,
@@ -410,6 +417,13 @@ def _write_debug_sidecar(
410417
411418 seen_tables : Dict [int , str ] = {} # id(mapping dict) -> first state key
412419 state_entries : List [dict ] = []
420+ # Earliest state in which each canonical id appears. The state_list
421+ # is ordered input → L0.attn → L0.mlp → L1.attn …, and a node sits in
422+ # the residual stream from the sublayer that computes it until it is
423+ # freed, so its FIRST appearance pins where it was computed. Used as
424+ # the layer/sublayer source for nodes the placement recorder doesn't
425+ # log (literals, concatenations).
426+ first_state : Dict [str , str ] = {}
413427 for key , st in state_list :
414428 table = ra .mapping .get (st )
415429 if table is None :
@@ -431,6 +445,7 @@ def _write_debug_sidecar(
431445 # PosEncoding leaf) — cannot be keyed canonically.
432446 continue
433447 nodes .setdefault (str (cid ), encode_cols (list (cols )))
448+ first_state .setdefault (str (cid ), key )
434449 state_entries .append ({"key" : key , "nodes" : nodes })
435450
436451 assert_targets = sorted (
@@ -440,19 +455,81 @@ def _write_debug_sidecar(
440455 if unwrap_debug (a .inputs [0 ]).node_id in canon
441456 }
442457 )
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- }
453458 matrices , placements , n_heads , d_hidden_per_layer = _build_matrix_occupancy (
454459 compiled , canon , d , d_head
455460 )
461+
462+ # Per-node metadata keyed by canonical id — the same key space as
463+ # ``placements`` and the residual ``states``. One entry per node
464+ # reachable from the output.
465+ #
466+ # layer/sublayer: the placement recorder logs the layer + matrix for
467+ # every weight-bearing op (Linear/Attn/ReLU/Add), so it is the
468+ # authoritative source where present; matrix_kind "attn.*"/"mlp.*"
469+ # gives the sublayer. Nodes it doesn't log (literals, concatenations)
470+ # fall back to their first residual-state appearance, and pre-layer
471+ # input nodes (Embedding) report sublayer "embed".
472+ place_loc : Dict [str , tuple ] = {}
473+ recorder = getattr (compiled , "placements" , None )
474+ if recorder is not None :
475+ for e in recorder .entries :
476+ if e .node is None :
477+ continue
478+ cid = canon .get (unwrap_debug (e .node ).node_id )
479+ if cid is None :
480+ continue
481+ cid_s = str (cid )
482+ if cid_s in place_loc :
483+ continue
484+ sub = "attn" if e .matrix_kind .startswith ("attn" ) else "mlp"
485+ place_loc [cid_s ] = (int (e .layer ), sub )
486+
487+ def _layer_sublayer (cid_s : str , node : Node ) -> tuple :
488+ if cid_s in place_loc :
489+ return place_loc [cid_s ]
490+ key = first_state .get (cid_s )
491+ if key is not None and key != "input" :
492+ lpart , sub = key .split ("." ) # "L{k}", "attn"|"mlp"
493+ return int (lpart [1 :]), sub
494+ if isinstance (node , Embedding ):
495+ return None , "embed"
496+ return None , None
497+
498+ # Baked weight tensors probed by attribute name, summed into a
499+ # parameter count and per-tensor shape list. 0 / [] for pure ops.
500+ baked_attrs = ("table" , "output_matrix" , "matrix" , "weight" , "value" )
501+ nodes_meta : Dict [str , dict ] = {}
502+ for cid , node in nodes_by_canonical_id (out ).items ():
503+ cid_s = str (cid )
504+ weight_params = 0
505+ weight_shapes : List [list ] = []
506+ for attr in baked_attrs :
507+ t = getattr (node , attr , None )
508+ shape = getattr (t , "shape" , None )
509+ if shape is None :
510+ continue
511+ dims = [int (s ) for s in shape ]
512+ if not dims : # 0-d scalar — no parameters to attribute
513+ continue
514+ weight_params += int (t .numel ()) if hasattr (t , "numel" ) else 1
515+ weight_shapes .append ([attr , dims ])
516+ input_cids : List [str ] = []
517+ for inp in getattr (node , "inputs" , None ) or []:
518+ icid = canon .get (unwrap_debug (inp ).node_id )
519+ if icid is not None :
520+ input_cids .append (str (icid ))
521+ layer , sublayer = _layer_sublayer (cid_s , node )
522+ nodes_meta [cid_s ] = {
523+ "op" : type (node ).__name__ ,
524+ "annotation" : node .annotation ,
525+ "width" : len (node ),
526+ "weight_params" : weight_params ,
527+ "weight_shapes" : weight_shapes ,
528+ "inputs" : input_cids ,
529+ "layer" : layer ,
530+ "sublayer" : sublayer ,
531+ "name" : getattr (node , "name" , None ),
532+ }
456533 payload = {
457534 "format" : DEBUG_META_FORMAT ,
458535 "kind" : kind , # "token" | "headless"
@@ -464,7 +541,9 @@ def _write_debug_sidecar(
464541 "n_layers" : len (compiled .layers ),
465542 "matrices" : matrices ,
466543 "placements" : placements ,
467- "annotations" : annotations ,
544+ "nodes" : nodes_meta ,
545+ "optimize" : int (optimize ),
546+ "extra" : dict (extra ) if extra else {},
468547 "input_specs" : [list (spec ) for spec in input_specs ],
469548 "cache_stride" : int (cache_stride ),
470549 "cache_window" : int (cache_window ) if cache_window is not None else None ,
@@ -1434,6 +1513,8 @@ def add(op, ins, outs, **attrs):
14341513 watches = all_watches ,
14351514 cache_stride = cache_stride_resolved ,
14361515 cache_window = cache_window ,
1516+ optimize = optimize ,
1517+ extra = extra_metadata ,
14371518 verbose = verbose ,
14381519 )
14391520
@@ -1779,6 +1860,8 @@ def add(op, ins, outs, **attrs):
17791860 watches = all_watches ,
17801861 cache_stride = cache_stride_resolved ,
17811862 cache_window = cache_window ,
1863+ optimize = optimize ,
1864+ extra = extra_metadata ,
17821865 verbose = verbose ,
17831866 )
17841867
0 commit comments