FIX(pushpull): implement the N-dimensional backward passes - #6

Open
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd
Open

FIX(pushpull): implement the N-dimensional backward passes#6
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd

Conversation

@balbasty

Copy link
Copy Markdown
Owner

Closes#4

What was wrong

jitfields/csrc/lib/pushpull/nd.h was a stub. As reported in #4, the backward passes were copy-pasted verbatim from the 3D kernels: they still took the x, nx, sx, y, ny, sy, z, nz, sz argument convention, called utils_x/y/z::gindex, and wrote exactly three coordinate gradients (gout[0], gout[osg], gout[osg*2]) regardless of D.

The forward passes were in no better shape — they did not compile at all:

  • sd = s + 8*d where s is never declared (the array is called f),
  • db[d] used as the loop bound where the array is called l,
  • coord referenced in push/count/grad, which don't have such a parameter,
  • *out = static_cast<scalar_t>(acc) in grad, where acc is an array,
  • an outright syntax error in grad: acc[dd] += val * (g[8*d + ] * weights[D-1];,
  • #endif JF_PUSHPULL_ND (trailing tokens after #endif).

None of this was ever caught because nothing included the file: lib/pushpull.h only pulled in 1d.h, 2d.h and 3d.h.

What this PR does

nd.h is rewritten as a genuine N-dimensional implementation.

PushPullND<D, ABS> is now a class template of its own rather than a partial specialization of PushPull. Two reasons:

  • PushPull only carries three (spline order, boundary condition) pairs, so for D > 3 they have to be runtime parameters anyway — they are read from inter[] / bnd[] arrays, exactly like resize::Multiscale;
  • PushPull<D, Z, B0, Z, B0, Z, B0, ABS> would have been ambiguous with the 3D specialization PushPull<three, Z, BX, Z, BY, Z, BZ, ABS> as soon as the header was actually included.

PushPullND was already the name used to reach the fallback (it was an alias in pushpull/utils.h, unused anywhere), so call sites are unaffected.

All nine kernelspull, push, count, grad, hess, pull_backward, push_backward, count_backward, grad_backward — now enumerate the support of the separable basis over D dimensions. Spatial derivatives are obtained by swapping the weight of the differentiated dimension for its derivative:

  • coordinate gradients are accumulated over all D dimensions;
  • grad_backward builds the full D x D symmetric matrix of second derivatives (d2/dx_d dx_e) instead of the six hard-coded 3D components, and contracts it with the incoming gradient;
  • hess writes the compact symmetric layout used elsewhere in jitfields — the diagonal first, then the upper triangle in row-major order, i.e. [xx, yy, zz, xy, xz, yz] in 3D.

lib/pushpull.h now includes nd.h, so the file is compiled from here on.

Tests

jitfields/csrc/tests/test_pushpull_nd.cpp (new) checks every kernel against an independent reference: it enumerates the support explicitly and uses its own B-spline weight / first-derivative / second-derivative formulas, so the kernels are not validated against the very functions they call.

Coverage: D = 1..6, orders 0-3, boundary conditions dct1 / dct2 / dst2 / dft / replicate, mixed order+boundary across dimensions, several channels, non-cubic shapes, and the ABS=true variant. On top of that, the D == 3 results are cross-checked against the specialized 3D kernels.

jitfields/tests/test_pushpull_nd.py (new) compiles and runs it as part of the normal pytest suite; it skips itself if no C++ compiler is on the machine. It does not import torch or cppyy, so it runs anywhere.

$ pytest jitfields/tests/test_pushpull_nd.py
141/141 checks passed
PASSED

Verified with both g++ 13 and clang++. A mutation check (making node_grad always differentiate along dimension 0) turns 51 of the 141 checks red, so the suite does bite.

The existing cpp/pushpull.hpp translation unit was compiled before and after the change to confirm that including nd.h breaks nothing on the existing paths.

Two things deliberately left out of this PR

  1. The N-D path is still not reachable from python.bindings/{cpp,cuda}/pushpull.py dispatch to jf::pushpull::pullnd, pushnd, countnd, gradnd, hessnd, pullnd_backward, ... when ndim > 3, but those loop wrappers do not exist in csrc/cpp/pushpull.hpp / csrc/cuda/pushpull.cu (and the template argument lists the bindings build for them do not match the ones the 1D/2D/3D wrappers use). Wiring that up — plus the CUDA side — is a bigger, separate piece of work; this PR fixes and tests the kernels themselves, which is what Fix pushpull backward when D > 3 #4 asks for. Happy to open a follow-up issue for the plumbing.

  2. Two pre-existing bugs in 3d.h, spotted while cross-checking, left untouched so as not to bundle unrelated changes:

    • in the generic (any-order) hess, accxz is written to both the zz and the xz slot, so acczz is computed and then dropped — this is why hess is not part of the N-D vs 3D cross-check;
    • the lineargrad_backward returns a zero coordinate gradient. The diagonal second derivatives of a multilinear basis are indeed zero, but the mixed ones (d2/dx dy) are not, so the term is not identically zero. The N-D kernel computes them; the cross-check for the linear case therefore skips grad_backward's gout (flagged with a comment in the test).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1


Generated by Claude Code

`pushpull/nd.h` was a stub: the backward passes were copy-pasted from
the 3D kernels (they used the `x/y/z` argument convention, referenced
undeclared variables, and hard-coded three coordinate gradients), and
the forward passes did not compile either. Nothing included the file,
so none of it was ever built.
Rewrite it as a genuine N-dimensional implementation:
* `PushPullND<D, ABS>` is now a class template of its own instead of a
partial specialization of `PushPull`. `PushPull` only carries three
(spline order, boundary condition) pairs, so for D > 3 they must be
passed at runtime -- and a `PushPull<D, Z, B0, ...>` specialization
would have been ambiguous with the 3D one for D == 3.
* All nine kernels (pull / push / count / grad / hess and the
pull / push / count / grad backward passes) enumerate the support of
the separable basis over D dimensions, with per-dimension spline
order and boundary condition read from the `inter` / `bnd` arrays
(same convention as `resize.h`).
* The coordinate gradients are accumulated over all D dimensions, and
`grad_backward` builds the full D x D symmetric matrix of second
derivatives instead of the six 3D components.
* `hess` writes the compact symmetric layout used elsewhere in
jitfields: the diagonal first, then the upper triangle in row-major
order (`[xx, yy, zz, xy, xz, yz]` in 3D).
* `pushpull.h` now includes `nd.h`, so the file is actually compiled.
Add `csrc/tests/test_pushpull_nd.cpp`, which checks every kernel for
D = 1..6 against an independent reference that enumerates the basis
explicitly and uses its own B-spline weight/derivative formulas, and
cross-checks the D == 3 results against the specialized 3D kernels.
`tests/test_pushpull_nd.py` compiles and runs it as part of the pytest
suite (skipped if no C++ compiler is available).
Closes#4
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1
Sign up for freeto 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.

Fix pushpull backward when D > 3

2 participants

@balbasty@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

FIX(pushpull): implement the N-dimensional backward passes - #6

Open
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd
Open

FIX(pushpull): implement the N-dimensional backward passes#6
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd

Conversation

@balbasty

Copy link
Copy Markdown
Owner

Closes#4

What was wrong

jitfields/csrc/lib/pushpull/nd.h was a stub. As reported in #4, the backward passes were copy-pasted verbatim from the 3D kernels: they still took the x, nx, sx, y, ny, sy, z, nz, sz argument convention, called utils_x/y/z::gindex, and wrote exactly three coordinate gradients (gout[0], gout[osg], gout[osg*2]) regardless of D.

The forward passes were in no better shape — they did not compile at all:

  • sd = s + 8*d where s is never declared (the array is called f),
  • db[d] used as the loop bound where the array is called l,
  • coord referenced in push/count/grad, which don't have such a parameter,
  • *out = static_cast<scalar_t>(acc) in grad, where acc is an array,
  • an outright syntax error in grad: acc[dd] += val * (g[8*d + ] * weights[D-1];,
  • #endif JF_PUSHPULL_ND (trailing tokens after #endif).

None of this was ever caught because nothing included the file: lib/pushpull.h only pulled in 1d.h, 2d.h and 3d.h.

What this PR does

nd.h is rewritten as a genuine N-dimensional implementation.

PushPullND<D, ABS> is now a class template of its own rather than a partial specialization of PushPull. Two reasons:

  • PushPull only carries three (spline order, boundary condition) pairs, so for D > 3 they have to be runtime parameters anyway — they are read from inter[] / bnd[] arrays, exactly like resize::Multiscale;
  • PushPull<D, Z, B0, Z, B0, Z, B0, ABS> would have been ambiguous with the 3D specialization PushPull<three, Z, BX, Z, BY, Z, BZ, ABS> as soon as the header was actually included.

PushPullND was already the name used to reach the fallback (it was an alias in pushpull/utils.h, unused anywhere), so call sites are unaffected.

All nine kernelspull, push, count, grad, hess, pull_backward, push_backward, count_backward, grad_backward — now enumerate the support of the separable basis over D dimensions. Spatial derivatives are obtained by swapping the weight of the differentiated dimension for its derivative:

  • coordinate gradients are accumulated over all D dimensions;
  • grad_backward builds the full D x D symmetric matrix of second derivatives (d2/dx_d dx_e) instead of the six hard-coded 3D components, and contracts it with the incoming gradient;
  • hess writes the compact symmetric layout used elsewhere in jitfields — the diagonal first, then the upper triangle in row-major order, i.e. [xx, yy, zz, xy, xz, yz] in 3D.

lib/pushpull.h now includes nd.h, so the file is compiled from here on.

Tests

jitfields/csrc/tests/test_pushpull_nd.cpp (new) checks every kernel against an independent reference: it enumerates the support explicitly and uses its own B-spline weight / first-derivative / second-derivative formulas, so the kernels are not validated against the very functions they call.

Coverage: D = 1..6, orders 0-3, boundary conditions dct1 / dct2 / dst2 / dft / replicate, mixed order+boundary across dimensions, several channels, non-cubic shapes, and the ABS=true variant. On top of that, the D == 3 results are cross-checked against the specialized 3D kernels.

jitfields/tests/test_pushpull_nd.py (new) compiles and runs it as part of the normal pytest suite; it skips itself if no C++ compiler is on the machine. It does not import torch or cppyy, so it runs anywhere.

$ pytest jitfields/tests/test_pushpull_nd.py
141/141 checks passed
PASSED

Verified with both g++ 13 and clang++. A mutation check (making node_grad always differentiate along dimension 0) turns 51 of the 141 checks red, so the suite does bite.

The existing cpp/pushpull.hpp translation unit was compiled before and after the change to confirm that including nd.h breaks nothing on the existing paths.

Two things deliberately left out of this PR

  1. The N-D path is still not reachable from python.bindings/{cpp,cuda}/pushpull.py dispatch to jf::pushpull::pullnd, pushnd, countnd, gradnd, hessnd, pullnd_backward, ... when ndim > 3, but those loop wrappers do not exist in csrc/cpp/pushpull.hpp / csrc/cuda/pushpull.cu (and the template argument lists the bindings build for them do not match the ones the 1D/2D/3D wrappers use). Wiring that up — plus the CUDA side — is a bigger, separate piece of work; this PR fixes and tests the kernels themselves, which is what Fix pushpull backward when D > 3 #4 asks for. Happy to open a follow-up issue for the plumbing.

  2. Two pre-existing bugs in 3d.h, spotted while cross-checking, left untouched so as not to bundle unrelated changes:

    • in the generic (any-order) hess, accxz is written to both the zz and the xz slot, so acczz is computed and then dropped — this is why hess is not part of the N-D vs 3D cross-check;
    • the lineargrad_backward returns a zero coordinate gradient. The diagonal second derivatives of a multilinear basis are indeed zero, but the mixed ones (d2/dx dy) are not, so the term is not identically zero. The N-D kernel computes them; the cross-check for the linear case therefore skips grad_backward's gout (flagged with a comment in the test).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1


Generated by Claude Code

`pushpull/nd.h` was a stub: the backward passes were copy-pasted from
the 3D kernels (they used the `x/y/z` argument convention, referenced
undeclared variables, and hard-coded three coordinate gradients), and
the forward passes did not compile either. Nothing included the file,
so none of it was ever built.
Rewrite it as a genuine N-dimensional implementation:
* `PushPullND<D, ABS>` is now a class template of its own instead of a
partial specialization of `PushPull`. `PushPull` only carries three
(spline order, boundary condition) pairs, so for D > 3 they must be
passed at runtime -- and a `PushPull<D, Z, B0, ...>` specialization
would have been ambiguous with the 3D one for D == 3.
* All nine kernels (pull / push / count / grad / hess and the
pull / push / count / grad backward passes) enumerate the support of
the separable basis over D dimensions, with per-dimension spline
order and boundary condition read from the `inter` / `bnd` arrays
(same convention as `resize.h`).
* The coordinate gradients are accumulated over all D dimensions, and
`grad_backward` builds the full D x D symmetric matrix of second
derivatives instead of the six 3D components.
* `hess` writes the compact symmetric layout used elsewhere in
jitfields: the diagonal first, then the upper triangle in row-major
order (`[xx, yy, zz, xy, xz, yz]` in 3D).
* `pushpull.h` now includes `nd.h`, so the file is actually compiled.
Add `csrc/tests/test_pushpull_nd.cpp`, which checks every kernel for
D = 1..6 against an independent reference that enumerates the basis
explicitly and uses its own B-spline weight/derivative formulas, and
cross-checks the D == 3 results against the specialized 3D kernels.
`tests/test_pushpull_nd.py` compiles and runs it as part of the pytest
suite (skipped if no C++ compiler is available).
Closes#4
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1
Sign up for freeto 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.

Fix pushpull backward when D > 3

2 participants

@balbasty@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

FIX(pushpull): implement the N-dimensional backward passes - #6

Open
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd
Open

FIX(pushpull): implement the N-dimensional backward passes#6
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd

Conversation

@balbasty

Copy link
Copy Markdown
Owner

Closes#4

What was wrong

jitfields/csrc/lib/pushpull/nd.h was a stub. As reported in #4, the backward passes were copy-pasted verbatim from the 3D kernels: they still took the x, nx, sx, y, ny, sy, z, nz, sz argument convention, called utils_x/y/z::gindex, and wrote exactly three coordinate gradients (gout[0], gout[osg], gout[osg*2]) regardless of D.

The forward passes were in no better shape — they did not compile at all:

  • sd = s + 8*d where s is never declared (the array is called f),
  • db[d] used as the loop bound where the array is called l,
  • coord referenced in push/count/grad, which don't have such a parameter,
  • *out = static_cast<scalar_t>(acc) in grad, where acc is an array,
  • an outright syntax error in grad: acc[dd] += val * (g[8*d + ] * weights[D-1];,
  • #endif JF_PUSHPULL_ND (trailing tokens after #endif).

None of this was ever caught because nothing included the file: lib/pushpull.h only pulled in 1d.h, 2d.h and 3d.h.

What this PR does

nd.h is rewritten as a genuine N-dimensional implementation.

PushPullND<D, ABS> is now a class template of its own rather than a partial specialization of PushPull. Two reasons:

  • PushPull only carries three (spline order, boundary condition) pairs, so for D > 3 they have to be runtime parameters anyway — they are read from inter[] / bnd[] arrays, exactly like resize::Multiscale;
  • PushPull<D, Z, B0, Z, B0, Z, B0, ABS> would have been ambiguous with the 3D specialization PushPull<three, Z, BX, Z, BY, Z, BZ, ABS> as soon as the header was actually included.

PushPullND was already the name used to reach the fallback (it was an alias in pushpull/utils.h, unused anywhere), so call sites are unaffected.

All nine kernelspull, push, count, grad, hess, pull_backward, push_backward, count_backward, grad_backward — now enumerate the support of the separable basis over D dimensions. Spatial derivatives are obtained by swapping the weight of the differentiated dimension for its derivative:

  • coordinate gradients are accumulated over all D dimensions;
  • grad_backward builds the full D x D symmetric matrix of second derivatives (d2/dx_d dx_e) instead of the six hard-coded 3D components, and contracts it with the incoming gradient;
  • hess writes the compact symmetric layout used elsewhere in jitfields — the diagonal first, then the upper triangle in row-major order, i.e. [xx, yy, zz, xy, xz, yz] in 3D.

lib/pushpull.h now includes nd.h, so the file is compiled from here on.

Tests

jitfields/csrc/tests/test_pushpull_nd.cpp (new) checks every kernel against an independent reference: it enumerates the support explicitly and uses its own B-spline weight / first-derivative / second-derivative formulas, so the kernels are not validated against the very functions they call.

Coverage: D = 1..6, orders 0-3, boundary conditions dct1 / dct2 / dst2 / dft / replicate, mixed order+boundary across dimensions, several channels, non-cubic shapes, and the ABS=true variant. On top of that, the D == 3 results are cross-checked against the specialized 3D kernels.

jitfields/tests/test_pushpull_nd.py (new) compiles and runs it as part of the normal pytest suite; it skips itself if no C++ compiler is on the machine. It does not import torch or cppyy, so it runs anywhere.

$ pytest jitfields/tests/test_pushpull_nd.py
141/141 checks passed
PASSED

Verified with both g++ 13 and clang++. A mutation check (making node_grad always differentiate along dimension 0) turns 51 of the 141 checks red, so the suite does bite.

The existing cpp/pushpull.hpp translation unit was compiled before and after the change to confirm that including nd.h breaks nothing on the existing paths.

Two things deliberately left out of this PR

  1. The N-D path is still not reachable from python.bindings/{cpp,cuda}/pushpull.py dispatch to jf::pushpull::pullnd, pushnd, countnd, gradnd, hessnd, pullnd_backward, ... when ndim > 3, but those loop wrappers do not exist in csrc/cpp/pushpull.hpp / csrc/cuda/pushpull.cu (and the template argument lists the bindings build for them do not match the ones the 1D/2D/3D wrappers use). Wiring that up — plus the CUDA side — is a bigger, separate piece of work; this PR fixes and tests the kernels themselves, which is what Fix pushpull backward when D > 3 #4 asks for. Happy to open a follow-up issue for the plumbing.

  2. Two pre-existing bugs in 3d.h, spotted while cross-checking, left untouched so as not to bundle unrelated changes:

    • in the generic (any-order) hess, accxz is written to both the zz and the xz slot, so acczz is computed and then dropped — this is why hess is not part of the N-D vs 3D cross-check;
    • the lineargrad_backward returns a zero coordinate gradient. The diagonal second derivatives of a multilinear basis are indeed zero, but the mixed ones (d2/dx dy) are not, so the term is not identically zero. The N-D kernel computes them; the cross-check for the linear case therefore skips grad_backward's gout (flagged with a comment in the test).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1


Generated by Claude Code

`pushpull/nd.h` was a stub: the backward passes were copy-pasted from
the 3D kernels (they used the `x/y/z` argument convention, referenced
undeclared variables, and hard-coded three coordinate gradients), and
the forward passes did not compile either. Nothing included the file,
so none of it was ever built.
Rewrite it as a genuine N-dimensional implementation:
* `PushPullND<D, ABS>` is now a class template of its own instead of a
partial specialization of `PushPull`. `PushPull` only carries three
(spline order, boundary condition) pairs, so for D > 3 they must be
passed at runtime -- and a `PushPull<D, Z, B0, ...>` specialization
would have been ambiguous with the 3D one for D == 3.
* All nine kernels (pull / push / count / grad / hess and the
pull / push / count / grad backward passes) enumerate the support of
the separable basis over D dimensions, with per-dimension spline
order and boundary condition read from the `inter` / `bnd` arrays
(same convention as `resize.h`).
* The coordinate gradients are accumulated over all D dimensions, and
`grad_backward` builds the full D x D symmetric matrix of second
derivatives instead of the six 3D components.
* `hess` writes the compact symmetric layout used elsewhere in
jitfields: the diagonal first, then the upper triangle in row-major
order (`[xx, yy, zz, xy, xz, yz]` in 3D).
* `pushpull.h` now includes `nd.h`, so the file is actually compiled.
Add `csrc/tests/test_pushpull_nd.cpp`, which checks every kernel for
D = 1..6 against an independent reference that enumerates the basis
explicitly and uses its own B-spline weight/derivative formulas, and
cross-checks the D == 3 results against the specialized 3D kernels.
`tests/test_pushpull_nd.py` compiles and runs it as part of the pytest
suite (skipped if no C++ compiler is available).
Closes#4
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1
Sign up for freeto 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.

Fix pushpull backward when D > 3

2 participants

@balbasty@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

FIX(pushpull): implement the N-dimensional backward passes - #6

Open
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd
Open

FIX(pushpull): implement the N-dimensional backward passes#6
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd

Conversation

@balbasty

Copy link
Copy Markdown
Owner

Closes#4

What was wrong

jitfields/csrc/lib/pushpull/nd.h was a stub. As reported in #4, the backward passes were copy-pasted verbatim from the 3D kernels: they still took the x, nx, sx, y, ny, sy, z, nz, sz argument convention, called utils_x/y/z::gindex, and wrote exactly three coordinate gradients (gout[0], gout[osg], gout[osg*2]) regardless of D.

The forward passes were in no better shape — they did not compile at all:

  • sd = s + 8*d where s is never declared (the array is called f),
  • db[d] used as the loop bound where the array is called l,
  • coord referenced in push/count/grad, which don't have such a parameter,
  • *out = static_cast<scalar_t>(acc) in grad, where acc is an array,
  • an outright syntax error in grad: acc[dd] += val * (g[8*d + ] * weights[D-1];,
  • #endif JF_PUSHPULL_ND (trailing tokens after #endif).

None of this was ever caught because nothing included the file: lib/pushpull.h only pulled in 1d.h, 2d.h and 3d.h.

What this PR does

nd.h is rewritten as a genuine N-dimensional implementation.

PushPullND<D, ABS> is now a class template of its own rather than a partial specialization of PushPull. Two reasons:

  • PushPull only carries three (spline order, boundary condition) pairs, so for D > 3 they have to be runtime parameters anyway — they are read from inter[] / bnd[] arrays, exactly like resize::Multiscale;
  • PushPull<D, Z, B0, Z, B0, Z, B0, ABS> would have been ambiguous with the 3D specialization PushPull<three, Z, BX, Z, BY, Z, BZ, ABS> as soon as the header was actually included.

PushPullND was already the name used to reach the fallback (it was an alias in pushpull/utils.h, unused anywhere), so call sites are unaffected.

All nine kernelspull, push, count, grad, hess, pull_backward, push_backward, count_backward, grad_backward — now enumerate the support of the separable basis over D dimensions. Spatial derivatives are obtained by swapping the weight of the differentiated dimension for its derivative:

  • coordinate gradients are accumulated over all D dimensions;
  • grad_backward builds the full D x D symmetric matrix of second derivatives (d2/dx_d dx_e) instead of the six hard-coded 3D components, and contracts it with the incoming gradient;
  • hess writes the compact symmetric layout used elsewhere in jitfields — the diagonal first, then the upper triangle in row-major order, i.e. [xx, yy, zz, xy, xz, yz] in 3D.

lib/pushpull.h now includes nd.h, so the file is compiled from here on.

Tests

jitfields/csrc/tests/test_pushpull_nd.cpp (new) checks every kernel against an independent reference: it enumerates the support explicitly and uses its own B-spline weight / first-derivative / second-derivative formulas, so the kernels are not validated against the very functions they call.

Coverage: D = 1..6, orders 0-3, boundary conditions dct1 / dct2 / dst2 / dft / replicate, mixed order+boundary across dimensions, several channels, non-cubic shapes, and the ABS=true variant. On top of that, the D == 3 results are cross-checked against the specialized 3D kernels.

jitfields/tests/test_pushpull_nd.py (new) compiles and runs it as part of the normal pytest suite; it skips itself if no C++ compiler is on the machine. It does not import torch or cppyy, so it runs anywhere.

$ pytest jitfields/tests/test_pushpull_nd.py
141/141 checks passed
PASSED

Verified with both g++ 13 and clang++. A mutation check (making node_grad always differentiate along dimension 0) turns 51 of the 141 checks red, so the suite does bite.

The existing cpp/pushpull.hpp translation unit was compiled before and after the change to confirm that including nd.h breaks nothing on the existing paths.

Two things deliberately left out of this PR

  1. The N-D path is still not reachable from python.bindings/{cpp,cuda}/pushpull.py dispatch to jf::pushpull::pullnd, pushnd, countnd, gradnd, hessnd, pullnd_backward, ... when ndim > 3, but those loop wrappers do not exist in csrc/cpp/pushpull.hpp / csrc/cuda/pushpull.cu (and the template argument lists the bindings build for them do not match the ones the 1D/2D/3D wrappers use). Wiring that up — plus the CUDA side — is a bigger, separate piece of work; this PR fixes and tests the kernels themselves, which is what Fix pushpull backward when D > 3 #4 asks for. Happy to open a follow-up issue for the plumbing.

  2. Two pre-existing bugs in 3d.h, spotted while cross-checking, left untouched so as not to bundle unrelated changes:

    • in the generic (any-order) hess, accxz is written to both the zz and the xz slot, so acczz is computed and then dropped — this is why hess is not part of the N-D vs 3D cross-check;
    • the lineargrad_backward returns a zero coordinate gradient. The diagonal second derivatives of a multilinear basis are indeed zero, but the mixed ones (d2/dx dy) are not, so the term is not identically zero. The N-D kernel computes them; the cross-check for the linear case therefore skips grad_backward's gout (flagged with a comment in the test).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1


Generated by Claude Code

`pushpull/nd.h` was a stub: the backward passes were copy-pasted from
the 3D kernels (they used the `x/y/z` argument convention, referenced
undeclared variables, and hard-coded three coordinate gradients), and
the forward passes did not compile either. Nothing included the file,
so none of it was ever built.
Rewrite it as a genuine N-dimensional implementation:
* `PushPullND<D, ABS>` is now a class template of its own instead of a
partial specialization of `PushPull`. `PushPull` only carries three
(spline order, boundary condition) pairs, so for D > 3 they must be
passed at runtime -- and a `PushPull<D, Z, B0, ...>` specialization
would have been ambiguous with the 3D one for D == 3.
* All nine kernels (pull / push / count / grad / hess and the
pull / push / count / grad backward passes) enumerate the support of
the separable basis over D dimensions, with per-dimension spline
order and boundary condition read from the `inter` / `bnd` arrays
(same convention as `resize.h`).
* The coordinate gradients are accumulated over all D dimensions, and
`grad_backward` builds the full D x D symmetric matrix of second
derivatives instead of the six 3D components.
* `hess` writes the compact symmetric layout used elsewhere in
jitfields: the diagonal first, then the upper triangle in row-major
order (`[xx, yy, zz, xy, xz, yz]` in 3D).
* `pushpull.h` now includes `nd.h`, so the file is actually compiled.
Add `csrc/tests/test_pushpull_nd.cpp`, which checks every kernel for
D = 1..6 against an independent reference that enumerates the basis
explicitly and uses its own B-spline weight/derivative formulas, and
cross-checks the D == 3 results against the specialized 3D kernels.
`tests/test_pushpull_nd.py` compiles and runs it as part of the pytest
suite (skipped if no C++ compiler is available).
Closes#4
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1
Sign up for freeto 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.

Fix pushpull backward when D > 3

2 participants

@balbasty@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

FIX(pushpull): implement the N-dimensional backward passes - #6

Open
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd
Open

FIX(pushpull): implement the N-dimensional backward passes#6
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd

Conversation

@balbasty

Copy link
Copy Markdown
Owner

Closes#4

What was wrong

jitfields/csrc/lib/pushpull/nd.h was a stub. As reported in #4, the backward passes were copy-pasted verbatim from the 3D kernels: they still took the x, nx, sx, y, ny, sy, z, nz, sz argument convention, called utils_x/y/z::gindex, and wrote exactly three coordinate gradients (gout[0], gout[osg], gout[osg*2]) regardless of D.

The forward passes were in no better shape — they did not compile at all:

  • sd = s + 8*d where s is never declared (the array is called f),
  • db[d] used as the loop bound where the array is called l,
  • coord referenced in push/count/grad, which don't have such a parameter,
  • *out = static_cast<scalar_t>(acc) in grad, where acc is an array,
  • an outright syntax error in grad: acc[dd] += val * (g[8*d + ] * weights[D-1];,
  • #endif JF_PUSHPULL_ND (trailing tokens after #endif).

None of this was ever caught because nothing included the file: lib/pushpull.h only pulled in 1d.h, 2d.h and 3d.h.

What this PR does

nd.h is rewritten as a genuine N-dimensional implementation.

PushPullND<D, ABS> is now a class template of its own rather than a partial specialization of PushPull. Two reasons:

  • PushPull only carries three (spline order, boundary condition) pairs, so for D > 3 they have to be runtime parameters anyway — they are read from inter[] / bnd[] arrays, exactly like resize::Multiscale;
  • PushPull<D, Z, B0, Z, B0, Z, B0, ABS> would have been ambiguous with the 3D specialization PushPull<three, Z, BX, Z, BY, Z, BZ, ABS> as soon as the header was actually included.

PushPullND was already the name used to reach the fallback (it was an alias in pushpull/utils.h, unused anywhere), so call sites are unaffected.

All nine kernelspull, push, count, grad, hess, pull_backward, push_backward, count_backward, grad_backward — now enumerate the support of the separable basis over D dimensions. Spatial derivatives are obtained by swapping the weight of the differentiated dimension for its derivative:

  • coordinate gradients are accumulated over all D dimensions;
  • grad_backward builds the full D x D symmetric matrix of second derivatives (d2/dx_d dx_e) instead of the six hard-coded 3D components, and contracts it with the incoming gradient;
  • hess writes the compact symmetric layout used elsewhere in jitfields — the diagonal first, then the upper triangle in row-major order, i.e. [xx, yy, zz, xy, xz, yz] in 3D.

lib/pushpull.h now includes nd.h, so the file is compiled from here on.

Tests

jitfields/csrc/tests/test_pushpull_nd.cpp (new) checks every kernel against an independent reference: it enumerates the support explicitly and uses its own B-spline weight / first-derivative / second-derivative formulas, so the kernels are not validated against the very functions they call.

Coverage: D = 1..6, orders 0-3, boundary conditions dct1 / dct2 / dst2 / dft / replicate, mixed order+boundary across dimensions, several channels, non-cubic shapes, and the ABS=true variant. On top of that, the D == 3 results are cross-checked against the specialized 3D kernels.

jitfields/tests/test_pushpull_nd.py (new) compiles and runs it as part of the normal pytest suite; it skips itself if no C++ compiler is on the machine. It does not import torch or cppyy, so it runs anywhere.

$ pytest jitfields/tests/test_pushpull_nd.py
141/141 checks passed
PASSED

Verified with both g++ 13 and clang++. A mutation check (making node_grad always differentiate along dimension 0) turns 51 of the 141 checks red, so the suite does bite.

The existing cpp/pushpull.hpp translation unit was compiled before and after the change to confirm that including nd.h breaks nothing on the existing paths.

Two things deliberately left out of this PR

  1. The N-D path is still not reachable from python.bindings/{cpp,cuda}/pushpull.py dispatch to jf::pushpull::pullnd, pushnd, countnd, gradnd, hessnd, pullnd_backward, ... when ndim > 3, but those loop wrappers do not exist in csrc/cpp/pushpull.hpp / csrc/cuda/pushpull.cu (and the template argument lists the bindings build for them do not match the ones the 1D/2D/3D wrappers use). Wiring that up — plus the CUDA side — is a bigger, separate piece of work; this PR fixes and tests the kernels themselves, which is what Fix pushpull backward when D > 3 #4 asks for. Happy to open a follow-up issue for the plumbing.

  2. Two pre-existing bugs in 3d.h, spotted while cross-checking, left untouched so as not to bundle unrelated changes:

    • in the generic (any-order) hess, accxz is written to both the zz and the xz slot, so acczz is computed and then dropped — this is why hess is not part of the N-D vs 3D cross-check;
    • the lineargrad_backward returns a zero coordinate gradient. The diagonal second derivatives of a multilinear basis are indeed zero, but the mixed ones (d2/dx dy) are not, so the term is not identically zero. The N-D kernel computes them; the cross-check for the linear case therefore skips grad_backward's gout (flagged with a comment in the test).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1


Generated by Claude Code

`pushpull/nd.h` was a stub: the backward passes were copy-pasted from
the 3D kernels (they used the `x/y/z` argument convention, referenced
undeclared variables, and hard-coded three coordinate gradients), and
the forward passes did not compile either. Nothing included the file,
so none of it was ever built.
Rewrite it as a genuine N-dimensional implementation:
* `PushPullND<D, ABS>` is now a class template of its own instead of a
partial specialization of `PushPull`. `PushPull` only carries three
(spline order, boundary condition) pairs, so for D > 3 they must be
passed at runtime -- and a `PushPull<D, Z, B0, ...>` specialization
would have been ambiguous with the 3D one for D == 3.
* All nine kernels (pull / push / count / grad / hess and the
pull / push / count / grad backward passes) enumerate the support of
the separable basis over D dimensions, with per-dimension spline
order and boundary condition read from the `inter` / `bnd` arrays
(same convention as `resize.h`).
* The coordinate gradients are accumulated over all D dimensions, and
`grad_backward` builds the full D x D symmetric matrix of second
derivatives instead of the six 3D components.
* `hess` writes the compact symmetric layout used elsewhere in
jitfields: the diagonal first, then the upper triangle in row-major
order (`[xx, yy, zz, xy, xz, yz]` in 3D).
* `pushpull.h` now includes `nd.h`, so the file is actually compiled.
Add `csrc/tests/test_pushpull_nd.cpp`, which checks every kernel for
D = 1..6 against an independent reference that enumerates the basis
explicitly and uses its own B-spline weight/derivative formulas, and
cross-checks the D == 3 results against the specialized 3D kernels.
`tests/test_pushpull_nd.py` compiles and runs it as part of the pytest
suite (skipped if no C++ compiler is available).
Closes#4
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1
Sign up for freeto 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.

Fix pushpull backward when D > 3

2 participants

@balbasty@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

FIX(pushpull): implement the N-dimensional backward passes - #6

Open
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd
Open

FIX(pushpull): implement the N-dimensional backward passes#6
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd

Conversation

@balbasty

Copy link
Copy Markdown
Owner

Closes#4

What was wrong

jitfields/csrc/lib/pushpull/nd.h was a stub. As reported in #4, the backward passes were copy-pasted verbatim from the 3D kernels: they still took the x, nx, sx, y, ny, sy, z, nz, sz argument convention, called utils_x/y/z::gindex, and wrote exactly three coordinate gradients (gout[0], gout[osg], gout[osg*2]) regardless of D.

The forward passes were in no better shape — they did not compile at all:

  • sd = s + 8*d where s is never declared (the array is called f),
  • db[d] used as the loop bound where the array is called l,
  • coord referenced in push/count/grad, which don't have such a parameter,
  • *out = static_cast<scalar_t>(acc) in grad, where acc is an array,
  • an outright syntax error in grad: acc[dd] += val * (g[8*d + ] * weights[D-1];,
  • #endif JF_PUSHPULL_ND (trailing tokens after #endif).

None of this was ever caught because nothing included the file: lib/pushpull.h only pulled in 1d.h, 2d.h and 3d.h.

What this PR does

nd.h is rewritten as a genuine N-dimensional implementation.

PushPullND<D, ABS> is now a class template of its own rather than a partial specialization of PushPull. Two reasons:

  • PushPull only carries three (spline order, boundary condition) pairs, so for D > 3 they have to be runtime parameters anyway — they are read from inter[] / bnd[] arrays, exactly like resize::Multiscale;
  • PushPull<D, Z, B0, Z, B0, Z, B0, ABS> would have been ambiguous with the 3D specialization PushPull<three, Z, BX, Z, BY, Z, BZ, ABS> as soon as the header was actually included.

PushPullND was already the name used to reach the fallback (it was an alias in pushpull/utils.h, unused anywhere), so call sites are unaffected.

All nine kernelspull, push, count, grad, hess, pull_backward, push_backward, count_backward, grad_backward — now enumerate the support of the separable basis over D dimensions. Spatial derivatives are obtained by swapping the weight of the differentiated dimension for its derivative:

  • coordinate gradients are accumulated over all D dimensions;
  • grad_backward builds the full D x D symmetric matrix of second derivatives (d2/dx_d dx_e) instead of the six hard-coded 3D components, and contracts it with the incoming gradient;
  • hess writes the compact symmetric layout used elsewhere in jitfields — the diagonal first, then the upper triangle in row-major order, i.e. [xx, yy, zz, xy, xz, yz] in 3D.

lib/pushpull.h now includes nd.h, so the file is compiled from here on.

Tests

jitfields/csrc/tests/test_pushpull_nd.cpp (new) checks every kernel against an independent reference: it enumerates the support explicitly and uses its own B-spline weight / first-derivative / second-derivative formulas, so the kernels are not validated against the very functions they call.

Coverage: D = 1..6, orders 0-3, boundary conditions dct1 / dct2 / dst2 / dft / replicate, mixed order+boundary across dimensions, several channels, non-cubic shapes, and the ABS=true variant. On top of that, the D == 3 results are cross-checked against the specialized 3D kernels.

jitfields/tests/test_pushpull_nd.py (new) compiles and runs it as part of the normal pytest suite; it skips itself if no C++ compiler is on the machine. It does not import torch or cppyy, so it runs anywhere.

$ pytest jitfields/tests/test_pushpull_nd.py
141/141 checks passed
PASSED

Verified with both g++ 13 and clang++. A mutation check (making node_grad always differentiate along dimension 0) turns 51 of the 141 checks red, so the suite does bite.

The existing cpp/pushpull.hpp translation unit was compiled before and after the change to confirm that including nd.h breaks nothing on the existing paths.

Two things deliberately left out of this PR

  1. The N-D path is still not reachable from python.bindings/{cpp,cuda}/pushpull.py dispatch to jf::pushpull::pullnd, pushnd, countnd, gradnd, hessnd, pullnd_backward, ... when ndim > 3, but those loop wrappers do not exist in csrc/cpp/pushpull.hpp / csrc/cuda/pushpull.cu (and the template argument lists the bindings build for them do not match the ones the 1D/2D/3D wrappers use). Wiring that up — plus the CUDA side — is a bigger, separate piece of work; this PR fixes and tests the kernels themselves, which is what Fix pushpull backward when D > 3 #4 asks for. Happy to open a follow-up issue for the plumbing.

  2. Two pre-existing bugs in 3d.h, spotted while cross-checking, left untouched so as not to bundle unrelated changes:

    • in the generic (any-order) hess, accxz is written to both the zz and the xz slot, so acczz is computed and then dropped — this is why hess is not part of the N-D vs 3D cross-check;
    • the lineargrad_backward returns a zero coordinate gradient. The diagonal second derivatives of a multilinear basis are indeed zero, but the mixed ones (d2/dx dy) are not, so the term is not identically zero. The N-D kernel computes them; the cross-check for the linear case therefore skips grad_backward's gout (flagged with a comment in the test).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1


Generated by Claude Code

`pushpull/nd.h` was a stub: the backward passes were copy-pasted from
the 3D kernels (they used the `x/y/z` argument convention, referenced
undeclared variables, and hard-coded three coordinate gradients), and
the forward passes did not compile either. Nothing included the file,
so none of it was ever built.
Rewrite it as a genuine N-dimensional implementation:
* `PushPullND<D, ABS>` is now a class template of its own instead of a
partial specialization of `PushPull`. `PushPull` only carries three
(spline order, boundary condition) pairs, so for D > 3 they must be
passed at runtime -- and a `PushPull<D, Z, B0, ...>` specialization
would have been ambiguous with the 3D one for D == 3.
* All nine kernels (pull / push / count / grad / hess and the
pull / push / count / grad backward passes) enumerate the support of
the separable basis over D dimensions, with per-dimension spline
order and boundary condition read from the `inter` / `bnd` arrays
(same convention as `resize.h`).
* The coordinate gradients are accumulated over all D dimensions, and
`grad_backward` builds the full D x D symmetric matrix of second
derivatives instead of the six 3D components.
* `hess` writes the compact symmetric layout used elsewhere in
jitfields: the diagonal first, then the upper triangle in row-major
order (`[xx, yy, zz, xy, xz, yz]` in 3D).
* `pushpull.h` now includes `nd.h`, so the file is actually compiled.
Add `csrc/tests/test_pushpull_nd.cpp`, which checks every kernel for
D = 1..6 against an independent reference that enumerates the basis
explicitly and uses its own B-spline weight/derivative formulas, and
cross-checks the D == 3 results against the specialized 3D kernels.
`tests/test_pushpull_nd.py` compiles and runs it as part of the pytest
suite (skipped if no C++ compiler is available).
Closes#4
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1
Sign up for freeto 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.

Fix pushpull backward when D > 3

2 participants

@balbasty@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

FIX(pushpull): implement the N-dimensional backward passes - #6

Open
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd
Open

FIX(pushpull): implement the N-dimensional backward passes#6
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd

Conversation

@balbasty

Copy link
Copy Markdown
Owner

Closes#4

What was wrong

jitfields/csrc/lib/pushpull/nd.h was a stub. As reported in #4, the backward passes were copy-pasted verbatim from the 3D kernels: they still took the x, nx, sx, y, ny, sy, z, nz, sz argument convention, called utils_x/y/z::gindex, and wrote exactly three coordinate gradients (gout[0], gout[osg], gout[osg*2]) regardless of D.

The forward passes were in no better shape — they did not compile at all:

  • sd = s + 8*d where s is never declared (the array is called f),
  • db[d] used as the loop bound where the array is called l,
  • coord referenced in push/count/grad, which don't have such a parameter,
  • *out = static_cast<scalar_t>(acc) in grad, where acc is an array,
  • an outright syntax error in grad: acc[dd] += val * (g[8*d + ] * weights[D-1];,
  • #endif JF_PUSHPULL_ND (trailing tokens after #endif).

None of this was ever caught because nothing included the file: lib/pushpull.h only pulled in 1d.h, 2d.h and 3d.h.

What this PR does

nd.h is rewritten as a genuine N-dimensional implementation.

PushPullND<D, ABS> is now a class template of its own rather than a partial specialization of PushPull. Two reasons:

  • PushPull only carries three (spline order, boundary condition) pairs, so for D > 3 they have to be runtime parameters anyway — they are read from inter[] / bnd[] arrays, exactly like resize::Multiscale;
  • PushPull<D, Z, B0, Z, B0, Z, B0, ABS> would have been ambiguous with the 3D specialization PushPull<three, Z, BX, Z, BY, Z, BZ, ABS> as soon as the header was actually included.

PushPullND was already the name used to reach the fallback (it was an alias in pushpull/utils.h, unused anywhere), so call sites are unaffected.

All nine kernelspull, push, count, grad, hess, pull_backward, push_backward, count_backward, grad_backward — now enumerate the support of the separable basis over D dimensions. Spatial derivatives are obtained by swapping the weight of the differentiated dimension for its derivative:

  • coordinate gradients are accumulated over all D dimensions;
  • grad_backward builds the full D x D symmetric matrix of second derivatives (d2/dx_d dx_e) instead of the six hard-coded 3D components, and contracts it with the incoming gradient;
  • hess writes the compact symmetric layout used elsewhere in jitfields — the diagonal first, then the upper triangle in row-major order, i.e. [xx, yy, zz, xy, xz, yz] in 3D.

lib/pushpull.h now includes nd.h, so the file is compiled from here on.

Tests

jitfields/csrc/tests/test_pushpull_nd.cpp (new) checks every kernel against an independent reference: it enumerates the support explicitly and uses its own B-spline weight / first-derivative / second-derivative formulas, so the kernels are not validated against the very functions they call.

Coverage: D = 1..6, orders 0-3, boundary conditions dct1 / dct2 / dst2 / dft / replicate, mixed order+boundary across dimensions, several channels, non-cubic shapes, and the ABS=true variant. On top of that, the D == 3 results are cross-checked against the specialized 3D kernels.

jitfields/tests/test_pushpull_nd.py (new) compiles and runs it as part of the normal pytest suite; it skips itself if no C++ compiler is on the machine. It does not import torch or cppyy, so it runs anywhere.

$ pytest jitfields/tests/test_pushpull_nd.py
141/141 checks passed
PASSED

Verified with both g++ 13 and clang++. A mutation check (making node_grad always differentiate along dimension 0) turns 51 of the 141 checks red, so the suite does bite.

The existing cpp/pushpull.hpp translation unit was compiled before and after the change to confirm that including nd.h breaks nothing on the existing paths.

Two things deliberately left out of this PR

  1. The N-D path is still not reachable from python.bindings/{cpp,cuda}/pushpull.py dispatch to jf::pushpull::pullnd, pushnd, countnd, gradnd, hessnd, pullnd_backward, ... when ndim > 3, but those loop wrappers do not exist in csrc/cpp/pushpull.hpp / csrc/cuda/pushpull.cu (and the template argument lists the bindings build for them do not match the ones the 1D/2D/3D wrappers use). Wiring that up — plus the CUDA side — is a bigger, separate piece of work; this PR fixes and tests the kernels themselves, which is what Fix pushpull backward when D > 3 #4 asks for. Happy to open a follow-up issue for the plumbing.

  2. Two pre-existing bugs in 3d.h, spotted while cross-checking, left untouched so as not to bundle unrelated changes:

    • in the generic (any-order) hess, accxz is written to both the zz and the xz slot, so acczz is computed and then dropped — this is why hess is not part of the N-D vs 3D cross-check;
    • the lineargrad_backward returns a zero coordinate gradient. The diagonal second derivatives of a multilinear basis are indeed zero, but the mixed ones (d2/dx dy) are not, so the term is not identically zero. The N-D kernel computes them; the cross-check for the linear case therefore skips grad_backward's gout (flagged with a comment in the test).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1


Generated by Claude Code

`pushpull/nd.h` was a stub: the backward passes were copy-pasted from
the 3D kernels (they used the `x/y/z` argument convention, referenced
undeclared variables, and hard-coded three coordinate gradients), and
the forward passes did not compile either. Nothing included the file,
so none of it was ever built.
Rewrite it as a genuine N-dimensional implementation:
* `PushPullND<D, ABS>` is now a class template of its own instead of a
partial specialization of `PushPull`. `PushPull` only carries three
(spline order, boundary condition) pairs, so for D > 3 they must be
passed at runtime -- and a `PushPull<D, Z, B0, ...>` specialization
would have been ambiguous with the 3D one for D == 3.
* All nine kernels (pull / push / count / grad / hess and the
pull / push / count / grad backward passes) enumerate the support of
the separable basis over D dimensions, with per-dimension spline
order and boundary condition read from the `inter` / `bnd` arrays
(same convention as `resize.h`).
* The coordinate gradients are accumulated over all D dimensions, and
`grad_backward` builds the full D x D symmetric matrix of second
derivatives instead of the six 3D components.
* `hess` writes the compact symmetric layout used elsewhere in
jitfields: the diagonal first, then the upper triangle in row-major
order (`[xx, yy, zz, xy, xz, yz]` in 3D).
* `pushpull.h` now includes `nd.h`, so the file is actually compiled.
Add `csrc/tests/test_pushpull_nd.cpp`, which checks every kernel for
D = 1..6 against an independent reference that enumerates the basis
explicitly and uses its own B-spline weight/derivative formulas, and
cross-checks the D == 3 results against the specialized 3D kernels.
`tests/test_pushpull_nd.py` compiles and runs it as part of the pytest
suite (skipped if no C++ compiler is available).
Closes#4
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1
Sign up for freeto 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.

Fix pushpull backward when D > 3

2 participants

@balbasty@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

FIX(pushpull): implement the N-dimensional backward passes - #6

Open
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd
Open

FIX(pushpull): implement the N-dimensional backward passes#6
balbasty wants to merge 1 commit into
mainfrom
fix-pushpull-backward-nd

Conversation

@balbasty

Copy link
Copy Markdown
Owner

Closes#4

What was wrong

jitfields/csrc/lib/pushpull/nd.h was a stub. As reported in #4, the backward passes were copy-pasted verbatim from the 3D kernels: they still took the x, nx, sx, y, ny, sy, z, nz, sz argument convention, called utils_x/y/z::gindex, and wrote exactly three coordinate gradients (gout[0], gout[osg], gout[osg*2]) regardless of D.

The forward passes were in no better shape — they did not compile at all:

  • sd = s + 8*d where s is never declared (the array is called f),
  • db[d] used as the loop bound where the array is called l,
  • coord referenced in push/count/grad, which don't have such a parameter,
  • *out = static_cast<scalar_t>(acc) in grad, where acc is an array,
  • an outright syntax error in grad: acc[dd] += val * (g[8*d + ] * weights[D-1];,
  • #endif JF_PUSHPULL_ND (trailing tokens after #endif).

None of this was ever caught because nothing included the file: lib/pushpull.h only pulled in 1d.h, 2d.h and 3d.h.

What this PR does

nd.h is rewritten as a genuine N-dimensional implementation.

PushPullND<D, ABS> is now a class template of its own rather than a partial specialization of PushPull. Two reasons:

  • PushPull only carries three (spline order, boundary condition) pairs, so for D > 3 they have to be runtime parameters anyway — they are read from inter[] / bnd[] arrays, exactly like resize::Multiscale;
  • PushPull<D, Z, B0, Z, B0, Z, B0, ABS> would have been ambiguous with the 3D specialization PushPull<three, Z, BX, Z, BY, Z, BZ, ABS> as soon as the header was actually included.

PushPullND was already the name used to reach the fallback (it was an alias in pushpull/utils.h, unused anywhere), so call sites are unaffected.

All nine kernelspull, push, count, grad, hess, pull_backward, push_backward, count_backward, grad_backward — now enumerate the support of the separable basis over D dimensions. Spatial derivatives are obtained by swapping the weight of the differentiated dimension for its derivative:

  • coordinate gradients are accumulated over all D dimensions;
  • grad_backward builds the full D x D symmetric matrix of second derivatives (d2/dx_d dx_e) instead of the six hard-coded 3D components, and contracts it with the incoming gradient;
  • hess writes the compact symmetric layout used elsewhere in jitfields — the diagonal first, then the upper triangle in row-major order, i.e. [xx, yy, zz, xy, xz, yz] in 3D.

lib/pushpull.h now includes nd.h, so the file is compiled from here on.

Tests

jitfields/csrc/tests/test_pushpull_nd.cpp (new) checks every kernel against an independent reference: it enumerates the support explicitly and uses its own B-spline weight / first-derivative / second-derivative formulas, so the kernels are not validated against the very functions they call.

Coverage: D = 1..6, orders 0-3, boundary conditions dct1 / dct2 / dst2 / dft / replicate, mixed order+boundary across dimensions, several channels, non-cubic shapes, and the ABS=true variant. On top of that, the D == 3 results are cross-checked against the specialized 3D kernels.

jitfields/tests/test_pushpull_nd.py (new) compiles and runs it as part of the normal pytest suite; it skips itself if no C++ compiler is on the machine. It does not import torch or cppyy, so it runs anywhere.

$ pytest jitfields/tests/test_pushpull_nd.py
141/141 checks passed
PASSED

Verified with both g++ 13 and clang++. A mutation check (making node_grad always differentiate along dimension 0) turns 51 of the 141 checks red, so the suite does bite.

The existing cpp/pushpull.hpp translation unit was compiled before and after the change to confirm that including nd.h breaks nothing on the existing paths.

Two things deliberately left out of this PR

  1. The N-D path is still not reachable from python.bindings/{cpp,cuda}/pushpull.py dispatch to jf::pushpull::pullnd, pushnd, countnd, gradnd, hessnd, pullnd_backward, ... when ndim > 3, but those loop wrappers do not exist in csrc/cpp/pushpull.hpp / csrc/cuda/pushpull.cu (and the template argument lists the bindings build for them do not match the ones the 1D/2D/3D wrappers use). Wiring that up — plus the CUDA side — is a bigger, separate piece of work; this PR fixes and tests the kernels themselves, which is what Fix pushpull backward when D > 3 #4 asks for. Happy to open a follow-up issue for the plumbing.

  2. Two pre-existing bugs in 3d.h, spotted while cross-checking, left untouched so as not to bundle unrelated changes:

    • in the generic (any-order) hess, accxz is written to both the zz and the xz slot, so acczz is computed and then dropped — this is why hess is not part of the N-D vs 3D cross-check;
    • the lineargrad_backward returns a zero coordinate gradient. The diagonal second derivatives of a multilinear basis are indeed zero, but the mixed ones (d2/dx dy) are not, so the term is not identically zero. The N-D kernel computes them; the cross-check for the linear case therefore skips grad_backward's gout (flagged with a comment in the test).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1


Generated by Claude Code

`pushpull/nd.h` was a stub: the backward passes were copy-pasted from
the 3D kernels (they used the `x/y/z` argument convention, referenced
undeclared variables, and hard-coded three coordinate gradients), and
the forward passes did not compile either. Nothing included the file,
so none of it was ever built.
Rewrite it as a genuine N-dimensional implementation:
* `PushPullND<D, ABS>` is now a class template of its own instead of a
partial specialization of `PushPull`. `PushPull` only carries three
(spline order, boundary condition) pairs, so for D > 3 they must be
passed at runtime -- and a `PushPull<D, Z, B0, ...>` specialization
would have been ambiguous with the 3D one for D == 3.
* All nine kernels (pull / push / count / grad / hess and the
pull / push / count / grad backward passes) enumerate the support of
the separable basis over D dimensions, with per-dimension spline
order and boundary condition read from the `inter` / `bnd` arrays
(same convention as `resize.h`).
* The coordinate gradients are accumulated over all D dimensions, and
`grad_backward` builds the full D x D symmetric matrix of second
derivatives instead of the six 3D components.
* `hess` writes the compact symmetric layout used elsewhere in
jitfields: the diagonal first, then the upper triangle in row-major
order (`[xx, yy, zz, xy, xz, yz]` in 3D).
* `pushpull.h` now includes `nd.h`, so the file is actually compiled.
Add `csrc/tests/test_pushpull_nd.cpp`, which checks every kernel for
D = 1..6 against an independent reference that enumerates the basis
explicitly and uses its own B-spline weight/derivative formulas, and
cross-checks the D == 3 results against the specialized 3D kernels.
`tests/test_pushpull_nd.py` compiles and runs it as part of the pytest
suite (skipped if no C++ compiler is available).
Closes#4
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQTnUfyLAVMnY1xvRya8U1
Sign up for freeto 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.

Fix pushpull backward when D > 3

2 participants

@balbasty@claude