Draft: fix adjoint gradient chunk-dependence without gathering the DFT monitors - #3274
Draft: fix adjoint gradient chunk-dependence without gathering the DFT monitors#3274smartalecH wants to merge 2 commits into
Conversation
material_grids_addgradient() paired the forward and adjoint DFT chunks by position in the per-component next_in_dft list, which is not a spatial correspondence: a fields chunk contributes a dft_chunk for a component only where the monitor overlaps that component's owned grid, and the yee shifts differ between components, so the per-component lists can differ in length and ordering near the monitor edges. Pair on (fields_chunk, symmetry image, lattice shift) instead, which is exactly the tuple loop_in_chunks() iterates over, and drop the abort on unequal chunk counts (a legitimate configuration, not an error). Also bounds-check neighboring-point lookups per dimension rather than on the flat index. grid_volume::index() is an inner product of the per-direction offsets with the strides, so in 3d a point one pixel off the end in y still yields an index inside [0,N) and silently reads an unrelated voxel. No communication is added. The persist dft chunks are padded one pixel past their owned region, while the restriction stencil only reaches half a pixel (unit_a*2 cancels against the yee shift), so every forward node an owned adjoint node needs lies inside the padded box -- at chunk boundaries it is the chunk's ghost layer, kept current by step_boundaries() before update_dfts(). Two counters, reported at verbosity > 2, assert that: they count any lookup that lands inside the cell but outside the local chunk, and any adjoint chunk with no matching forward chunk. Test from NanoComp#3264, unmodified. Refs NanoComp#2578, NanoComp#3264.
| meep::abort("The number of adjoint chunks (%ld) is not equal to the number of forward chunks " | ||
| "(%ld).\n", | ||
| c_adjoint_dft_chunks.size(), c_forward_dft_chunks.size()); | ||
| /* NOTE: the adjoint and forward chunk counts may legitimately differ, both |
There was a problem hiding this comment.
I'm not sure this is true. Even with different adjoint sources, the forward and adjoint fields in the design region should be the same, and thus the chunks should be the same. Can't we check that in the code that allocates the adjoint run from the forward run?
|
Thanks for putting this together. I ran the two diagnostic counters, and the Using this PR's head (
The objective changes by only The split between the counters is useful:
The stencil derivation in the PR is correct: it reaches one is = max(is - one_ivec(fc->gv.dim) * 2, fc->gv.little_corner());
ie = min(ie + one_ivec(fc->gv.dim) * 2, fc->gv.big_corner());
Adding the component shift to the clamp fixes that off-by-one node: is = max(is - one_ivec(fc->gv.dim) * 2,
fc->gv.little_corner() + fc->gv.iyee_shift(c));
ie = min(ie + one_ivec(fc->gv.dim) * 2,
fc->gv.big_corner() + fc->gv.iyee_shift(c));With those two changed bounds on top of this PR, unchanged otherwise:
I also checked the combined patch beyond this one split:
I also looked more closely at your inline note about the removed chunk-count A minimal 2d example using the same I think the useful invariant is spatial rather than numerical: if a forward My proposed minimal shape is therefore:
That keeps the DFT data distributed and adds no communication. It also means |
…pendence The minimal fix did not move the needle (rel 0.4426 vs 0.47 pre-fix), so the error is not in the terms it touched. Note that for forward_c == adjoint_c the old [cur_chunk] pairing was already correct -- same component, same volume, same fields -- so the pairing fix is a no-op for the dominant term, and with do_averaging=False the cross terms only fire where !is_material_grid(md). Add per-term counters and checksums, printed at verbosity > 0, over quantities that are sums across a partition of the monitor and so must not depend on the chunk division. To be removed once the responsible term is identified.
|
Thanks for working on this, everyone. |
Draft alternative to #3264, for #2578. Same test, no communication.
#3264 fixes a real bug, but it does so by gathering all six design-region DFT
components onto every rank (
O(volume)memory and allreduce traffic per rank),which is what @stevengj flagged. This PR attempts the same fix with local
changes only, to test whether the gather is necessary.
Claim: the cross-chunk data is already local
#3264 lists four root causes. Three are indexing bugs. The fourth — "the ±1-pixel
padding of
persistdft chunks is clamped to the owning fields chunk and cannotcross process boundaries" — I believe is not a defect, and everything else follows
from that.
The stencil reaches half a pixel, not a full pixel. With
adjoint_c = Ex,forward_c = Ey,iyee_shift(Ex) = (1,0,0),iyee_shift(Ey) = (0,1,0):unit_a * 2cancels against the yee shift. The four points areip ± x̂ ± ŷ, i.e.the four Ey nodes at the corners of the Yee cell around the Ex node — 1 ivec unit
away, half a pixel. The
persistpad asks for 2 units, twice the reach.Those nodes are always inside
fc->gv. Withio = little_corner(), chunk sizen_dpixels, andowns(p)=0 < (p−io)_d ≤ 2n_d(src/vec.cpp:445):aaip±1even ∈ [0, 2nₐ] ✅ffip±1odd ∈ [1, 2n_f+1] ✅The required range is exactly the chunk's forward-component node range. At a
chunk face the extra node is the chunk's ghost layer, which
connect_chunks()maintains (
src/boundaries.cpp:412,:480) and which is current at DFT time:update_dfts()issrc/step.cpp:126, afterstep_boundaries(E_stuff)at:121.The clamp to
fc->gvtherefore never removes a node the stencil needs.Split in y at plane P, chunk A below:
Changes
Pair chunks spatially (
matching_dft_chunk).forward_dft_chunks[ci_forward][cur_chunk]indexed the forward list with the adjoint component's list position. A fields
chunk contributes a
dft_chunkfor a component only where the monitor overlapsthat component's owned grid, so the per-component lists differ in length and
ordering near the monitor edges. Match on
(fc, sn, shift)— exactly the tupleloop_in_chunks()iterates over. This also subsumes root cause 4 (idx_adjused on the forward array), which was only valid under the chunk-alignment
assumption.
Per-dimension bounds checks (
ivec_in_box).grid_volume::index()is aninner product of per-direction offsets with strides, so in 3d a point one pixel
off the end in y still lands in
[0,N), on an unrelated voxel. Guarding the flatindex alone silently substitutes a neighboring field value. 1d/2d mostly
degenerate to the right answer, which is consistent with this surviving — every
adjoint test upstream is 2d or cylindrical.
Drop the
aborton unequal adjoint/forward chunk counts. That is alegitimate configuration, not an error.
Two diagnostic counters, reported at
verbosity > 2, that test the claimabove directly: forward lookups landing inside the cell but outside the local
chunk, and adjoint chunks with no matching forward chunk. Both should be zero
at every chunk division. Happy to drop these before merge, or keep them as a
guard.
Test
python/tests/test_adjoint_chunks.pyis taken from #3264 unmodified, so the twoPRs are directly comparable. It forces the bug in serial via
num_chunks, so itruns in every CI job rather than only the MPI ones — a good framing, since this
was never an MPI bug.
What would falsify this
If the counters in (4) come up nonzero, or the test fails, then cross-chunk data
really is needed and #3264's gather (or a surface-scale halo exchange of the DFT
boundary planes) is required. That is the question this draft is meant to settle.
Not addressed
matching_dft_chunkkeys on(fc, sn, shift), which is correct forsymmetry images, but I have not verified the rest of the path under symmetry.
eps_averagingfixes in Fix 3D MaterialGrid subpixel smoothing: kernel normalization (#3258) and gradient consistency (#3259) #3263 are independent.cc @jin-castle @stevengj @lxvm — @jin-castle, this is meant as input to #3264,
not a replacement; if the counters fire, your analysis stands and mine is wrong.
🤖 Generated with Claude Code