Uh oh!
There was an error while loading. Please reload this page.
pushpull: backpropagate through the sampling grid (closes #16) - #18
Conversation
`pull` and `push` treated `grid` as a constant: `_no_grid_grad` raised NotImplementedError if it required grad, so differentiating through sample positions -- the thing you need for a learned deformation field or a registration model -- was simply unavailable. The C++ adjoints existed in the impl layer the whole time but were never exported; the stack below now exposes them (fastfields-lib and down), so `_Pull`/`_Push`'s `backward()` can return a real grid gradient and the guard is gone. Both branches are kept, because they cost different amounts: * `grid` needs grad -> one fused `pull_backward` / `push_backward` call returns *both* adjoints. * only the field needs grad -> the old, cheaper `push` / `pull` path (the plain adjoint never touches the field to compute a spatial gradient). `inp` is likewise only saved for backward when `grid.requires_grad`, so the field-only path keeps its previous memory profile. `backward` is now marked `@once_differentiable`. It calls straight into C++, which is opaque to autograd, so double-backward was never actually supported -- it would silently have produced a wrong second derivative under `create_graph`. Better to say so. `count` and `grad` stay non-differentiable here (their adjoints *are* now exported by `fastfields.dlpack`, just not wired into an autograd.Function). ## Tests `gradcheck` is the actual proof, so the removed "not supported" test is replaced by 26 of them: pull and push, wrt `grid` alone and wrt (`inp`, `grid`) jointly, in 1D/2D/3D, orders 1/2/3, five boundary conditions, and with a leading batch dimension. Plus two behavioural tests: the field gradient must be identical whichever of the two branches computes it, and out-of-FOV samples (extrapolate=0) must get an exactly-zero grid gradient -- the FOV test is a step, which no finite difference can see. Grid coordinates in these tests are kept off the spline knots. A spline of order k is only C^(k-1), so at order 1 the derivative wrt position genuinely jumps at a knot; a central difference straddling one is meaningless. That is a property of the interpolant, not a limitation of the implementation. Full suite: 104 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016AjQcY78NgbagPSbPJRr6Z
balbasty
left a comment
There was a problem hiding this comment.
We want to support as many pytorch versions as possible: check that we are not using torch functions that are too narrow (@oncedifferentiable?) and if yes, provide compatibility fallbacks.
balbasty
commented
Jul 31, 2026
Checked properly rather than guessing — no shim needed, nothing changed. Details, so you can disagree with the evidence rather than with me: What the floor actually is
|
Empty commit to force a fresh check-suite — reruns of the existing suite reuse the reusable workflow's SHA resolved at check-suite creation time, which predates fastfields/.github#3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016AjQcY78NgbagPSbPJRr6Z
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016AjQcY78NgbagPSbPJRr6Z
…ow exported) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016AjQcY78NgbagPSbPJRr6Z
Uh oh!
There was an error while loading. Please reload this page.
Closes#16.
The gap
pullandpushtreatedgridas a constant:_no_grid_gradraisedNotImplementedErrorif it required grad, so differentiating through sample positions — the thing you need for a learned deformation field or a registration model — was simply unavailable.The C++ adjoints existed in the impl layer the whole time but were never exported. The stack below now exposes them, so
_Pull/_Push'sbackward()can return a real grid gradient and the guard is gone.Implementation
Both branches are kept, because they cost different amounts:
gridneeds grad → one fusedpull_backward/push_backwardcall returns both adjoints.push/pullpath (the plain adjoint never touches the field to compute a spatial gradient).inpis likewise only saved for backward whengrid.requires_grad, so the field-only path keeps its previous memory profile.backwardis now marked@once_differentiable. It calls straight into C++, which is opaque to autograd, so double-backward was never actually supported — it would silently have produced a wrong second derivative undercreate_graph. Better to say so than to keep returning quiet garbage.countandgradstay non-differentiable here. Their adjoints are now exported byfastfields.dlpack(count_backward/grad_backward, both covered by the C++ oracle), they are just not wired into anautograd.Function— that is beyond what #16 asked for.Tests
gradcheckis the actual proof, so the removed "not supported" test is replaced by 26 of them:pullandpush, wrtgridalone and wrt (inp,grid) jointly, in 1D/2D/3D, orders 1/2/3, five boundary conditions, and with a leading batch dimension.Plus two behavioural tests that finite differences cannot cover:
extrapolate=0) must get an exactly zero grid gradient — the FOV test is a step, not a smooth factor.Grid coordinates in these tests are kept off the spline knots. A spline of order k is only C^(k−1), so at order 1 the derivative wrt position genuinely jumps at a knot and a central difference straddling one is meaningless. That is a property of the interpolant, not a limitation of the implementation.
Full suite: 104 passed.
What this turned up underneath
Exposing the adjoints put them under a finite-difference oracle for the first time, and the pre-existing math was not correct — four bugs, all inherited from jitfields:
push_backwardindexedginpwith the wrong stride array — silent wrong gradients whenever the pushed volume and the grid have different spatial strides (801/6381 oracle checks)grad_backwardreturned a zero gradient wrtinpgrad_backwardreturned a zero gradient wrtgrid(dropped mixed second derivative)grad_backward— same, all three mixed termsThe stack
Merge bottom-up; each pin needs re-bumping to its parent's merged SHA as you go.
PRs 1–6 are based on
claude/cuda-pushpull-dynamic-spline(kernels#44 / cpu-lib#48), notmain— that work is unmerged and this builds on itsFF_SPLINE_*/SplineVecmachinery. They need retargeting tomainonce it lands.Workstream: claude-jitfields-to-fastfields
🤖 Generated with Claude Code
Generated by Claude Code