Skip to content

fix(autodiff): name the offending node when lowering fails closed - #135

Open
AlekseiChirkovVention wants to merge 2 commits into
feat/mean-expansion-provenancefrom
fix/fail-closed-lowering-diagnostics
Open

fix(autodiff): name the offending node when lowering fails closed#135
AlekseiChirkovVention wants to merge 2 commits into
feat/mean-expansion-provenancefrom
fix/fail-closed-lowering-diagnostics

Conversation

@AlekseiChirkovVention

Copy link
Copy Markdown
Collaborator

Prove the two properties the expansion exists to deliver, and name the offending node when
lowering fails closed.

Goal

A backend that does not understand a construct an expanded artifact introduces must be
rejected before any handler runs, and an expanded artifact must compute what the
unexpanded one computes. Neither property is worth much asserted loosely, so this branch
establishes both under conditions that would expose a vacuous proof.

The production change

_require_every_operation_supported re-raises its pre-flight failure with the offending node
id appended. The registry can only name the operator type — that is all a lookup is given —
and in a graph with several nodes of that type the message left the reader to guess. An
expanded artifact makes that worse, because it introduces generated nodes the author never
wrote by hand.

The registry's own message is untouched, so every other caller observes exactly what it did
before; one test pins that message byte-for-byte. No signature, argument, return type, or
error category changed.

Why the fail-closed proofs are not vacuous

The evidence that no handler ran is an empty invocation recorder. That is only evidence if
the recorder could have recorded, so each case additionally asserts the registry does hold
handlers for the operator types that remain — a registry wrapping nothing would leave an empty
record no matter what the code did. The recorder captures at handler call time, not at lookup
time, which is what makes an empty record mean "nothing executed" rather than "nothing was
looked up".

Why the equivalence comparison means something

The expanded artifact is lowered by a registry with no reduction, broadcast, or division
handler; the unexpanded one by a control registry that keeps its native reduction and reaches
numpy directly. The two sides share no code path, so a defect in the expansion cannot be
mirrored by the control and cancel out.

The control case is what makes the comparison worth making: the unexpanded artifact fails
against the limited registry with unsupported_operator. Without it, "the expanded artifact
lowers successfully" would be unremarkable rather than the point.

dtype and shape are compared exactly, rank included; only values are compared within
tolerance — 1e-6 for f32, 1e-12 for f64 — because substituting a reciprocal multiply
for a divide is not required to agree bit for bit. A scalar result and a one-by-one result are
asserted never interchangeable, each tier checked against the other's expectation inside a
raises block, so the distinction is executed rather than described.

Test evidence

Level: unit and integration. Tests first: the failing set landed in 7353495;
c5899cd made it pass. The test file is byte-identical between the two.

44 cases: 24 equivalence cases spanning both tiers, both differentiable dtypes, and square and
non-square shapes, for the forward result and the gradient with respect to the traced
parameter; the fail-closed and control cases; and the dependency-analysis compatibility checks.

Only three cases were failing at the start — the node-naming half — which is correct for a
branch whose sole production change is one error message. Review reproduced that by reverting
the pre-flight and re-running: exactly 3 failed, 41 passed, each for the right reason.

Pre-existing lowering tests pass unmodified. Focused autodiff suite: 885 → 888. Broad
non-integration suite: 1139 passed, 9 failed, 1 skipped — the 9 pre-existing and unrelated,
unchanged in count across the whole series.

Note for backend authors

The rank-reducing tier requires a trivial reshape handler in both the forward artifact and
the derivative program: the leading seed reshape survives expansion untouched. The
rank-preserving tier requires none. This is the easiest thing to get wrong when adopting an
expanded artifact.

Stack position

Stack position 6 of 8 — based on feat/mean-expansion-provenance.

Cannot merge into main before feat/generated-constant-operator,
chore/generic-lowering-reference-consumer, feat/matmul-based-mean-expansion,
feat/broadcast-scale-gradient-expansion, and feat/mean-expansion-provenance.

Pin the two properties the expansion exists to deliver.

A backend missing a handler an expanded artifact requires is rejected before
any handler runs. The error alone does not establish that, so each case also
asserts the invocation recorder is empty, and first asserts the registry does
hold other handlers -- an empty recorder proves nothing if nothing could have
been recorded.

Equivalence is asserted against a control registry that keeps its native
reduction, over both tiers, both differentiable dtypes, and square and
non-square shapes: dtype and shape exactly equal, values within the documented
tolerance. The control that the unexpanded artifact fails against the limited
registry is what makes those comparisons meaningful rather than incidental.

A scalar and a one-by-one value are asserted never interchangeable, each tier
checked against the other's expectation inside a raises block, so the
distinction is executed rather than described.

One assertion pins the registry lookup message byte-for-byte: only the
pre-flight check may name the offending node, and every other caller must keep
seeing what it sees today.
A backend missing a handler was told which operator type it could not lower,
but not which node carried it. In a graph with several nodes of that type the
message left the reader to guess, and an expanded artifact introduces
generated nodes the author never wrote by hand.

The registry can only name the type, since that is all a lookup is given. The
pre-flight check is the one place the node is in scope, so it re-raises the
same failure with the node id appended. The category is carried through
unchanged and the registry's own message is untouched, so every other caller
observes exactly what it did before.
@AlekseiChirkovVention
AlekseiChirkovVention marked this pull request as ready for review August 28, 2026 06:19
@AlekseiChirkovVention AlekseiChirkovVention self-assigned this Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants