Provenance Audit — Analysis

Companion to provenance_audit.md (the measurement-only report) for torch-spyre#2574. Reference model: 1-hidden-layer MLP (128→256→128, fp16), torch 2.11. This document interprets the measured numbers; all claims trace to the audit capture (provenance_capture_raw.json), the emitted sdsc_*.json bundles, or cited source.

⚠️ Specific to one audit run — provenance_audit.md Generated: 2026-06-24 19:17. That report is regenerated on every run; if its timestamp no longer matches the one above, the counts and findings here may be stale. Re-read against the current provenance_audit.md and update this file.

0. Changes since the previous run

Two measured differences from the earlier capture — both explained by main advancing between runs (restickify is a deterministic, layout-driven decision, so this is a compiler-version effect, not randomness), and neither changes the core finding:

  • No restickify split this run. LoopLevelIR holds 5 operations; an earlier main inserted 2 restickify buffers (5 → 7), since optimized away for this model. So origins reads 5/5 here, not 7/7.

  • origin_node is fully present (✅ 5/5). The earlier run showed it nulled on both matmul buffers (◐ 5/7); here it survives on all five ops. That its coverage shifts across compiler versions is exactly why provenance should ride origins — the complete set of contributing nodes (stable 5/5) — not the single optional origin_node pointer (fx.Node | None, nullable by design).

Unchanged: the genuine drop is still OpSpec → SuperDSC JSON. The OpSpec dataclass declares no provenance field, so nothing source-related reaches the emitted JSON.

1. Field semantics

How each field is structured and how it comes to exist (copied from an upstream node, derived on demand, or generated fresh):

Field

Represents

Observed type

Populated by

Copied / derived / generated

stack_trace

the user source file:line that created the op

str (traceback; last frame = user line)

Dynamo, trace time

copied across decomposition via fx_traceback.preserve_node_meta(); not propagated to synthesized mm/add from addmm

nn_module_stack

the nn.Module path owning the op (e.g. fc1Linear)

dict {id: (qual_name, cls)}

Dynamo

copied; on module-call nodes only (the two Linears, not functional relu)

source_fn_stack

the source function/op that produced the node

list[(seq_name, fn/cls)]

Dynamo

copied; tracks the source op, survives on anchor nodes

original_aten

the ATen op this node lowered from (e.g. addmm)

OpOverload

Inductor, post-grad

generated per node; present on all 7 post-grad nodes — the most robust FX field

from_node

the pass/transform chain that produced the node

list[NodeSource(name, pass_name, action, graph_id)]

Inductor GraphTransformObserver

generated as nodes are transformed; present on the anchor nodes (3/7 here) and run-dependent (see §6)

origins

the set of FX nodes that lowered into this buffer

OrderedSet[fx.Node]

Inductor IR lowering

generated when fx nodes lower to a ComputedBuffer; propagated by Spyre passes — robust (present 5/5 at the IR stages this run)

origin_node

a single representative FX node for this buffer

fx.Node | None

Inductor IR

copied; run-dependent — present 5/5 this run, but nulled on both matmul buffers (5/7) in the earlier run

traceback

the IR node’s creation-site traceback

str | None

Inductor IR

the attribute exists on the IR node but is never populated for this model — an unused provenance slot (hasattr true, value null at every IR stage)

get_stack_traces()

source lines derived from the buffer’s origins

OrderedSet[str] (accessor)

ComputedBuffer

derived on demand from origins[*].meta['stack_trace']; empty when origins lack a stack trace — here the two bias adds (op1, op4), so 3/5

2. Metadata flow

  nn.Module source ── file:line, module tree (ground truth)
        │ Dynamo
  FX Graph (pre-grad) ─ stack_trace, nn_module_stack, source_fn_stack
        │ AOTAutograd: addmm decomposition (linear → permute + mm + add)
        │   • anchor nodes (permute/relu) keep stack_trace/nn_module_stack/source_fn_stack
        │   • synthesized mm/add keep only original_aten   ← partial loss #1
  FX Graph (post-grad)  + original_aten (all 7), from_node (3/7, run-dependent)
        │ Inductor lowering → ComputedBuffer.origins
  LoopLevelIR (pre-pass)  origins (5/5), origin_node (5/5) entering the passes
        │            ↓ pre-scheduling passes mutate in place (no restickify this run; 5 ops)
  LoopLevelIR (post-pass) origins (5/5); origin_node (5/5 this run; 5/7 earlier)
        │               get_stack_traces() derived from origins (3/5)
        │ SpyreKernel.create_op_spec
  OpSpec ────────────── the OpSpec dataclass has no provenance field, so origins
        │               (still on the buffer feeding create_op_spec) is discarded ← DROP #2
        │ codegen/superdsc.py (parse_op_spec / compile_op_spec)
  SuperDSC JSON ─────── no provenance field emitted              ← DROP #3
                        (get_kernel_metadata assembles full source mapping at
                         define_kernel, emitted only as a wrapper comment)

3. Exact drop locations

  1. addmm decomposition (AOTAutograd). aten.addmmmm + add; the synthesized mm/add nodes carry original_aten but not stack_trace/nn_module_stack/source_fn_stack. Evidence: post-grad mm_default*/add_tensor* have those three fields null (a partial loss — original_aten still resolves them to aten.addmm).

  2. OpSpec carries nothing (DROP #2 — the real drop). create_op_spec (torch_spyre/_inductor/spyre_kernel.py:516) reads the input ComputedBuffer.origins, but the OpSpec dataclass (torch_spyre/_inductor/op_spec.py:67) declares only op, is_reduction, iteration_space, args, op_info, tiled_symbols — no field to hold provenance. So origins (present 5/5 on the buffer) is dropped.

  3. Serialization emits nothing (DROP #3). parse_op_spec/compile_op_spec (torch_spyre/_inductor/codegen/superdsc.py) consume only OpSpec fields. Confirmed by direct inspection of the emitted bundles (§4): provenance in JSON: for all 5 sdsc_*.json files.

Note on origin_node: in the earlier run it was nulled on the two matmul buffers between entry and exit of CustomPreSchedulingPasses (torch_spyre/_inductor/passes.py), reading ◐ 5/7. This run it survives 5/5. Because its coverage is schedule-dependent and origins strictly supersedes it, the audit does not chase the exact nulling pass (see §5/§6).

4. What the SuperDSC JSON actually contains

Two kernels are emitted, 5 sdsc_*.json files in total (one per OpSpec):

  • sdsc_fused_addmm_linear_relu_0 — 4 buffers (op0op3), kernel metadata Original ATen: [aten.linear, aten.addmm, aten.relu], source nodes [x, x_1, x_2]; 4 sdsc_*.json files.

  • sdsc_fused_addmm_1 — 1 buffer (op4), kernel metadata Original ATen: [aten.addmm]; 1 sdsc_*.json file.

Each file is purely the device-execution spec — iteration space, labeled tensors, the compute op (opFuncName/exUnit), the schedule tree (HBM allocations + device addresses), and core/work-slice maps. No stack_trace, origins, origin_node, original_aten, from_node, or any source reference appears in any file — the factual confirmation of DROP #3. The only source-aware artifact at this stage is the wrapper comment that get_kernel_metadata(node_schedule) assembles at define_kernel (the Original ATen / source-node line above), which is emitted as a code comment, not into the SDSC JSON.

5. If we add a debug_handle at OpSpec, what can we derive it from?

At create_op_spec the input ComputedBuffer carries origins (OrderedSet[fx.Node]), present on 5/5 ops. From each origin node’s .meta:

  • original_aten — present on all origins → always derivable.

  • stack_trace / nn_module_stack / source_fn_stack — present on anchor origins (matmul, relu); absent on the bias-add origins (op1, op4), which can therefore only be sourced to their original_aten.

Recommended carrier: origins. Do not use origin_node (run-dependent — 5/5 here but 5/7 earlier) or from_node (run-dependent, §6). Equivalently, get_kernel_metadata(node_schedule) at define_kernel already aggregates this per kernel and is keyed by kernel_name — e.g. sdsc_fused_addmm_linear_relu_0Original ATen: [aten.linear, aten.addmm, aten.relu], source nodes [x, x_1, x_2]. Phase 2 (#2575): add a debug_handle field to OpSpec, populate it from origins in create_op_spec, and serialize it through codegen/superdsc.py. This mirrors upstream’s _inductor_kernel_provenance_debug_handle (an int handle) in torch/_inductor/debug.py.

6. Caveats for implementation

  • from_node is run-dependent. This audit (a normal compile) shows it on 3/7 post-grad nodes — the anchor nodes; it rises to 7/7 when debug/provenance-tracking is active. Do not assume it exists in production compiles.

  • origin_node is run-dependent. Present 5/5 this run, but nulled on the matmul buffers (5/7) in the earlier run. Coverage depends on the pass schedule, so origins (5/5 in both runs) is the stable carrier.

  • traceback is an unused slot. The attribute exists on the IR node (confirmed via hasattr in the capture) but is never populated for this model — so it reads ❌ (reachable but empty) at the IR stages, not ➖. A profiler could populate it, but nothing does today.

  • Op count is schedule-dependent. This run produced 5 LoopLevelIR ops; the earlier run produced 7 (2 restickify buffers). A regression suite (#2581) should assert provenance coverage (every op carries origins), not a fixed op count.

  • Upstream inductor_provenance_tracking_node_mappings.json is not emitted by the Spyre backend today; Phase 3b should emit a compatible artifact so tlparse tooling works (RFC 0601 §4.7).