Debugging#
LEAPP provides multiple lines of defense against an incorrectly traced graph. The goal is confidence that the traced graph is numerically correct.
When |
Use |
|---|---|
You want numeric confidence in an export |
|
Validation failed and you need the recorded ops |
|
You need to tell tracing from export from patching |
|
Confirm the export#
compile_graph(validate=True) checks each exported node against all its
cached Python input/output samples, including exported state inputs and
updates. Repeated execution can capture consecutive stateful transitions;
max_cached_io controls how many samples are retained per node. LEAPP runs
the exported model on each sample, compares outputs with
torch.allclose(..., rtol=..., atol=...), logs any deviation, and
returns a dict mapping node names to results.
leapp.compile_graph(
validate=True,
rtol=1e-3,
atol=1e-5,
strict=True,
)
strict=True raises if any node fails. strict=False still runs
validation, but leaves the result dict available. Nodes with no compiled
model, such as non_traced or dry-run cases, are skipped and treated
as successful.
A mismatch log includes the node and output names, the sample index, tolerances, shapes and dtypes, value ranges, and absolute-difference percentiles. Several samples are what make those numbers meaningful: one pair of tensors cannot characterize error, and it cannot show whether a value was captured because it is computed or because it happened to be constant on the first step.
Run more than one step#
Warning
Loop the annotated policy. A graph that matches the first observation can still have inlined a live input as a constant, and a single sample says nothing about error on any other input.
leapp.start(name="policy_graph", max_cached_io=5)
for _ in range(5):
obs = annotate.input_tensors("policy", {"obs": next_obs()})
action = policy(obs)
annotate.output_tensors(
"policy", {"action": action}, export_with="jit",
)
leapp.stop()
leapp.compile_graph(validate=True, strict=True)
Each iteration is stored as a sample, up to max_cached_io.
compile_graph(validate=True) replays the export against every one.
Use inputs that look like deployment: different state, commands, and
timing, not copies of the first frame. If a later step fails while the
first passes, the log names that sample N.
Re-entry also checks names, shapes, dtypes, and connectivity against
the first step. Those failures show up during tracing, not at
compile_graph().
Stateful transitions and sequential replay#
When consecutive calls carry state forward, cached samples preserve the original trajectory, including each state’s next value:
(observation_0, state_0) -> (action_0, state_1)
(observation_1, state_1) -> (action_1, state_2)
(observation_2, state_2) -> (action_2, state_3)
LEAPP supplies each recorded observation and state to the exported node and compares both its action and its next state with the recorded outputs. This validates the state-transition computation across the captured trajectory. State must be included in the exported interface as described in State capture and feedback; unannotated Python state is outside this check.
For deterministic computation with all relevant state represented, exact agreement on every transition also implies agreement when those transitions are chained with the same observations, initial state, and reset boundaries: each exported next state equals the next recorded input state.
Validation accepts numerical differences within rtol and atol.
Cached validation supplies the recorded reference state again at each step,
so small accepted errors do not accumulate between samples. Sequential
runtime replay feeds the exported state outputs into subsequent calls and
can reveal whether those errors grow, as well as problems in feedback wiring,
initialization, or reset handling. Increasing the cache size expands recorded
transition coverage; it does not change how validation supplies state.
Use both checks when assessing stateful deployment: capture representative consecutive transitions, then compare a sequence through LEAPP Runtime against the original pipeline from matching initial state. Include episode resets and fresh observations beyond the capture set.
Inspect what was captured#
Every leapp.start() call writes a LEAPP log file in the graph output
directory:
leapp.start("debug_policy", save_path="exports")
creates exports/debug_policy/log.txt. The file includes DEBUG
output. The console is quieter and shows warnings and errors. Pass
verbose=True to stream the same detail to the console:
leapp.start("debug_policy", save_path="exports", verbose=True)
# ... run your traced code ...
leapp.stop()
leapp.compile_graph(validate=True, verbose=True)
leapp.start(..., verbose=True) covers trace-time diagnostics.
leapp.compile_graph(..., verbose=True) covers compilation, export,
and validation.
FX graph#
For each traced node, LEAPP writes the full torch.fx.Graph to the
log after building the node’s fx.GraphModule. Search for
Compiled graph module for <node name> in log.txt:
[DEBUG]: Compiled graph module for policy:
graph():
%obs : [num_users=1] = placeholder[target=obs]
%linear : [num_users=1] = call_module[target=linear](args = (%obs,), kwargs = {})
%tanh : [num_users=1] = call_function[target=torch.tanh](args = (%linear,), kwargs = {})
return tanh
[DEBUG]: Graph module inputs: [...]
[DEBUG]: Graph module outputs: [...]
This is the graph LEAPP hands to the export backend. Use it to check
that annotated inputs became placeholders, that operations recorded as
the operators you expected, that unused inputs were trimmed, and that
the graph outputs match what you passed to annotate.output_tensors().
Graph image#
On Python 3.11+, leapp.compile_graph(visualize=True) writes a PNG
of the node graph, which is the fastest check that nodes connected the
way you intended and the dtypes are as you expect.
Isolate the failure#
These flags turn pieces of a LEAPP session off so you can tell whether a bad result comes from tracing, from export, or from LEAPP’s own patches.
Skip export#
Tracing is what produces the values that carry graph connectivity, so it cannot be skipped while still producing a graph. Export can.
leapp.start(..., dry_run=True)traces every node and writes YAML, but exports no models. Use it to inspect boundaries, I/O, and connectivity without paying export cost.leapp.start(..., non_traced=["some_node"])does the same for listed nodes only: they still appear in the graph and still connect to neighbors, but they produce no model artifact.export_with=Noneon a singleoutput_tensors()call skips export for that node only.
Warp graphs still run the annotated path a second time and still report a segment that diverges between the two runs. Only APIC capture is skipped, because nothing consumes the captured bundle when the node is not exported.
leapp.start("debug_graph", dry_run=True)
# ... run your traced code ...
leapp.stop()
leapp.compile_graph()
Disable session patches#
leapp.start(..., global_patching=False) turns off the conversions
LEAPP patches for the tracing session (for example NumPy and torch
interop). Use this only to test whether those patches are involved in
a failure.