Capture Outcomes — the settled terminal truth of every capture¶
Every TorchLens capture product carries ONE typed, settled answer to "how did
this capture end": the CaptureOutcome record, written by the single
settlement authority in torchlens/_capture/outcome.py. Consumers branch on
the record or on the stable refusal codes below — never on exception text.
The status vocabulary¶
| status | meaning | is_partial |
|---|---|---|
COMPLETE |
settled complete; attested by the settlement stamp | False |
STOPPED |
stopped by stop_after= or imperative tl.recording.stop(); an honest prefix |
True |
ABORTED_NONFINITE |
stopped by the on_nonfinite='raise' policy |
True |
FAILED |
the capture raised; phase + diagnostic origin attribution | True |
UNATTESTED |
structurally finished, but no settlement evidence (legacy artifacts only) | None |
UNKNOWN |
unprovable terminal state; the most restrictive row | None |
phase is FAILED-only, not FAILED-required: a FAILED outcome normally
carries a phase, but two writers legitimately emit FAILED with
phase=None — the sparse-recording recorder fallback stamp (when the runtime trace's
settled record is unavailable or non-failed) and the legacy partial_error
derivation. When present, phase is one of (FORWARD — the model forward raised;
FINALIZE — the forward completed but finalize/output-extraction/cleanup
raised; POSTPROCESS — the postprocess pipeline or the core freeze raised;
TEARDOWN — teardown failed AFTER settlement, demoting the outcome) and a
diagnostic-only origin (user_op / torchlens / interrupt / unknown)
that never steers a capability gate. phase is FAILED-only, not
FAILED-required: two writers legitimately emit FAILED with
phase=None — the recorder fallback stamp (when the scratch trace's settled
record is unavailable or non-failed) and the legacy partial_error
derivation on deserialized recordings. The coherence matrix permits the
absent phase; no gate consults it.
The raised error¶
A FAILED capture whose forward raised a non-TorchLens exception (the model's
code, a user hook, or a torch op the forward called) raises one typed
tl.errors.TraceCaptureError (a CaptureError) from every capture door,
chained from the original (exc.__cause__, traceback intact; the original
object is never mutated). Its message names the original type and text, the
committed op count and the last captured op, and states the origin.
exc.partial_trace is the PartialTrace; exc.origin is the same
FailureOrigin the settled outcome records (classified on the original
exception), and exc.fields["code"] follows it: forward_raised for
user_op and unknown, capture_internal_error for torchlens (both in
the error refusal contract's stable-codes table). TorchLens's own errors (a TorchLensError, or any exception
class defined in the torchlens package) propagate as themselves, also
carrying exc.partial_trace. The outcome's reason and error_type
describe the original exception in both cases. Interrupts (KeyboardInterrupt,
SystemExit) are never wrapped.
Where to read it¶
Trace.outcome— the sidecar (settle stamp on live products; adopted or lattice-derived record on loaded artifacts;Noneonly on a live trace that has not settled yet).Recording.outcome— stamped by the recorder settlement adapter; derives conservatively from the construction status for legacy/recovered recordings.PartialTrace.outcome— forwards the inner trace's sidecar.Bundle.outcome— the DERIVED worst-of-members fold (declared container authority, never a second stored answer, as with PartialTrace and Recording). The status is the most severe member status under the fold's total severity order (complete < unattested < stopped < aborted_nonfinite < failed < unknown— capability-table restrictiveness, ties broken by epistemic weakness), so COMPLETE is never blessed above the weakest member; a member with no settled outcome folds as UNKNOWN fail-closed,derived=Truealways, and the settlement note names the driving member. The fold is a DISCLOSURE reported beside results, never a settlement and never a per-member gate: capability gating applies at the member whose facts a read cites. Both public save doors agree by construction —bundle.save(path, ...)delegates to the container door (Bundle.save), whose member writes run the N1 gate per member; per-trace payload options refuse typed (bundle_save_option_unsupported).Trace.fork().outcome— a DERIVED record settled bystamp_forked, never the parent's attestation by identity: a fork is the sanctioned mutation surface, so it settles through the structural lattice (UNATTESTED for a complete parent, STOPPED-derived for a stopped one), keeps the parent's committed-op count, and recordsforked_from=<parent status>in the settlement note. Capability parity holds (every UNATTESTED cell is allow/allow-scoped), so nothing a fork could do before is newly refused.RunOutcome.capture_outcome— the session-side copy;RunOutcome.stateremains the first-transition log and is never revised (demotion replaces only the record).
Persistence and the load derivation¶
_capture_outcome is a portable schema field (tlspec v7), persisted as a
string-only payload and parsed at load against the closed vocabularies plus a
status-specific coherence matrix (an attestation contradicting the artifact's
structural halted / _tracing_finished evidence degrades to UNKNOWN with
one warning — fail-closed, never a load crash). Attestation is self-declared
by the artifact: a coherent reauthoring is out of scope, the same documented
threat boundary as the runnable contract's section 11.
Artifacts WITHOUT an attestation derive from structural evidence alone, by
truthiness (False / None / absent are one class):
halted and finished -> STOPPED (derived)
halted and not finished -> UNKNOWN (the halted postprocess never completed)
finished and not halted -> UNATTESTED (genuine-complete and tail-failure are indistinguishable)
otherwise -> UNKNOWN
No legacy Trace without attestation ever derives COMPLETE. Finalized
Recording bundles keep their construction-status derivation
(complete/halted at finalize time is a construction proof); recover()
products are UNKNOWN (or reconstructed STOPPED where the halt markers
survived) with recovered=True. Derived outcomes never upgrade across
re-saves: UNATTESTED re-saves stay UNATTESTED.
The capability table (gates N1–N5)¶
Enforced through the ONE chokepoint
(require_capture_capability(trace, capability)); refusals raise
tl.errors.CaptureOutcomeError with a stable fields["code"]:
| capability | COMPLETE | STOPPED | ABORTED / FAILED | UNATTESTED | UNKNOWN |
|---|---|---|---|---|---|
| validation entry | allow | allow (halted output block skipped, as always) | refuse N2 | allow — the tripwire stays armed | refuse N2 |
Trace.save analysis |
allow | allow | refuse N1 | allow (no upgrade) | refuse N1 |
trace.save(path, level="runnable") |
allow | refuse N4 (runnable vocabulary: stopped_capture_unsupported) |
refuse N1 | allow-scoped (existing preflights gate) | refuse N1 |
live replay: refresh / save_new_outs / do replay / rerun / live run() / fast=True |
allow | refuse N5 | refuse N3 | allow | refuse N3 |
loaded-sparse run() |
allow | allow — executes exactly the recorded prefix DAG | refuse N3 | allow | refuse N3 |
backward / capture_backward |
allow | allow — the autograd graph IS the captured prefix | refuse N3 | allow | refuse N3 |
draw / trace.explain() / inspection / fork / load |
allow | allow | via PartialTrace / pickle inspection, as today |
allow | allow (analysis-only, coreless staging) |
Row rationales, stated once:
- UNATTESTED permissions equal the historical de-facto behavior for
loaded finished traces — nothing that worked yesterday is newly refused —
while the CLAIMS column is empty: no settled-complete assertion, no
no-swallow guarantee,
partial=None. Validation entry deliberately stays OPEN because validation is the tripwire that catches a freeze-corrupted legacy representation. - UNKNOWN refusals replace what was previously an ungated pass-through of
provably-unfinished artifacts (
Trace.saveof failed partials) or an undefined crash on unbuilt structures. The nearest legitimate workflows (partial inspection,draw, the raised error's.partial_trace) all remain open. - N5 converted two broken behaviors into typed refusals: halted refresh
crashed with a misleading "computational graph changed" error, and halted
run_transaction(inputs=..., fast=True)SILENTLY SUCCEEDED against the full native forward (the false-blessing hole). The refusal names the re-arm follow-on: re-capture withoutstop_after=, or use loaded-sparserun(). - N4 is a crash→typed conversion: no legacy workflow successfully saved a halted runnable (the producer preflight already failed).
Recording.backwardrefuses a failed partial with the table's N3 code (RecorderStateError,fields["code"] == "N3") and additionally refuses STOPPED withfields["code"] == "recording_backward_halted"— a deliberate Recording-scoped strictness beyond the table's STOPPED-allow cell, which describes Trace-side backward (a halted Trace holds the prefix autograd graph; a halted Recording is a sparse event product whose frontier pass retained no complete output to root the backward walk, the same boundary that refuses halted-no-payloadto_trace()). It also refuses a COMPLETE Recording that cannot have captured gradients:recording_backward_not_requestedwhen the record installed no gradient hooks (nobackward_ready=True,save_grads=ordefault_grad=), andrecording_backward_inference_onlywhen the forward ran undertorch.no_grad()ortorch.inference_mode(), which impliesinference_only=True(reported asRecording.outcome.inference_only).
on_nonfinite='raise''s honest scope¶
The tripwire checks RAW op-boundary tensors only: a NaN introduced by
activation_transform never aborts (COMPLETE), a raw NaN aborts even when
the transform would have sanitized it (ABORTED_NONFINITE), empty tensors are
skipped, and uncheckable dtypes warn once per capture.
The queryable nonfinite record¶
(Every name here is experimental and may change in any release.)
on_nonfinite='raise' is a stop-and-throw; the queryable form is separate and never
changes control flow. Trace.nonfinite_ops returns the pass-qualified labels
of ops whose output held at least one NaN or Inf (each a valid trace[label]
key), and Trace.nonfinite_coverage returns the frozen evidence disclosure:
basis, checked, nonfinite, unchecked (dtypes with no runnable
finiteness kernel), unexamined, and unmapped. An empty answer is only as
strong as its coverage — the programmatic twin of the prose coverage-gap note
in first_nonfinite().
Two evidence bases:
"saved_payloads"(the default): the record derives from the memoized saved-payload scan already backingprint(trace). Zero capture-time cost; the oneisfinitepass over retained payloads is paid at first query and memoized (measured 0.02–0.27 s on resnet18/transformer/gpt2-class models, then ~2 ms memoized). On a selective-save capture this basis is honestly blind to unsaved ops (unexaminedsays how many)."capture":CaptureOptions(on_nonfinite='track')opts into a per-optorch.isfinite(out).all()at op commit, covering every committed op — saved or not. fp8 widens exactly first; empty tensors are exactly finite; uncheckable dtypes land inunchecked. CPU flags are read immediately; CUDA/MPS flags are deferred as 0-dim bool tensors and drained in ONE batch at the capture finalize seam, so the forward is never synchronized per op.
Measured cost of on_nonfinite='track' relative to a default capture
(min-of-reps, 2026-08-19): CPU resnet18-b8 +5.8%, transformer-enc
+14.5%, gpt2 +3.4%; H200 (cu128) resnet18-b8 +4.9%, transformer-enc +6.7%,
gpt2 +4.3%, with an eager per-op read variant costing +6.9%/+9.3%/+7.3% —
the deferral is why a device capture never pays a per-op synchronization.
These numbers are why the knob defaults to OFF everywhere: on default
exhaustive-save captures the free "saved_payloads" basis already covers
every op, so the per-op check would buy nothing; on selective-save captures
it is the only way to get verdicts for unsaved ops and is priced above.
The knob is session-time (FieldPolicy.DROP, like
measure_python_peak_memory): load restores the default, and a loaded trace
serves the "saved_payloads" basis over its archived payloads.
structure_only=True refuses the combination
(structure_only_option_conflict) — there are no values to check. The
capture-basis record covers committed ops only: synthetic input/output mirror
nodes count as unexamined, and events whose op was removed by
postprocessing (orphans) count as unmapped. Verdicts reflect the tensors
ops actually produced — an intervened op's verdict is of the replaced value
downstream consumers saw.
The stop-request latch¶
evaluate_halt, raise_nonfinite, and imperative tl.recording.stop() all
latch a stop request on the active trace before raising. If the forward then
returns "normally" — user code swallowed the control signal in a broad
except: — the boundary checkpoint raises
tl.errors.StopSignalSwallowedError and the capture settles FAILED, never
COMPLETE. The halt-capable preview backends (paddle's shipped stop_after=, MLX's
halt selector) hold the same contract: their StopSignal raise sites latch
the request on the trace, and stamp_backend_finalized — the one preview
settlement stamp — consumes the latch, refusing a latched-but-unhalted
capture with the same typed error and a FAILED settlement instead of the
COMPLETE arm. A swallowed nonfinite abort is FAILED, never a clean
ABORTED_NONFINITE (the abort did not actually stop the forward). Imperative
tl.recording.stop() outside any capture propagates to the caller exactly as before.
Settlement totality and productless escapes¶
Every product constructor settles through the authority: the torch
orchestrator's arms (success, halted partial-return, halted re-raise,
failure, interrupt), Recording.to_trace() (cooked_from=recording
provenance note; a halted cooked trace is STOPPED and hits N4/N5), each
recorder pass and the recorder exit, and every preview backend's capture
entry (stamp_backend_finalized as the LAST act after all tail work). A
failure BEFORE any product is reachable — configuration errors, backend
constructor failures, preview-backend tail failures between the structural
_tracing_finished write and the stamp — is a productless escape: the
exception propagates and no stamp exists, so a hypothetical pickle of the
escaped object derives UNATTESTED, never COMPLETE. Post-settlement teardown
failures demote the settled outcome to FAILED/TEARDOWN in both homes
(note discloses demoted_from=...); the exception preempts the
return, so no product escapes carrying the undemoted claim.
_tracing_finished remains a STRUCTURAL finished-ness marker, never a
settlement point; the writer inventory is pinned by test and every write
sits inside a region whose only success exit reaches an authority stamp.
Disclosed conversions and residuals¶
- Halted analysis
Trace.savewas BROKEN before this design (the halted finalizer leaked_output_attribution_input_tensors, refusing every export); it now works, per the capability table. - Postprocess-tail failures (steps 18–20, the core freeze) used to be MASKED
by an
AttributeError: _raw_graph_wscleanup double-fault; guarded cleanups let the original exception propagate, and the settlement stamp infinallysurvives any double-fault. - A refresh now re-arms
on_nonfinite='raise'; it silently disarmed before. - disclosure: the halted path skips
finalize_forward_session(and with it buffer-write reconciliation). Characterization shows byte-identical buffer topology, buffer-write event counts, and model buffer state versus the complete capture through the halt frontier, so this remains a documented skip, not a behavior change. TerminalState/RunOutcome.stateis the first-transition log; it and the outcome may legitimately disagree only in the demotion direction, disclosed bynote.- Preview cleanup asymmetry vs the torch orchestrator: MLX and Paddle run
cleanup_model_sessionin afinallypositioned BEFORE their settlement stamp (settlement is the last act, per the path-20 contract), so a cleanup/teardown failure there is a PRODUCTLESS escape — the exception propagates with no settled record at all, and a hypothetical pickle of the escaped object derives UNATTESTED. The torch orchestrator instead settles FAILED in itsfinally(and demotes on post-settlement teardown failure), so the same failure class on torch yields a settled FAILED product withexc.partial_trace. Net semantics are equivalent on the never-COMPLETE guarantee (path 7 minus the demoted record); the missing settled record on previews is the disclosed residual.
Public surface added by this design¶
Trace.outcome, Recording.outcome, PartialTrace.outcome;
tl.types.{CaptureOutcome, CaptureStatus, CapturePhase, FailureOrigin};
tl.errors.{CaptureOutcomeError, StopSignalSwallowedError, TraceCaptureError}; RunnableErrorCode.HALTED_CAPTURE_NOT_RUNNABLE;
two additive StopSignal.__init__ kwargs (boundary_kind,
boundary_label); the stored Recording._outcome field; the persisted
_capture_outcome Trace field (tlspec v7). Zero renames; nothing enters
top-level __all__.