[1/2] Fix adjoint gradient chunk-dependence without gathering the DFT monitors - #3274
[1/2] Fix adjoint gradient chunk-dependence without gathering the DFT monitors#3274smartalecH wants to merge 3 commits into
Conversation
|
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 |
|
Thanks for working on this, everyone. |
Fixes NanoComp#2578. The adjoint gradient of a MaterialGrid design region depended on how the cell was split into chunks, while the objective did not. Two load-bearing changes, both local; no communication is added or needed. 1. src/dft.cpp -- dft_chunk's `persist` pad clamped to fc->gv.little_corner()/big_corner(), which are on the centered grid. Component c runs from little_corner()+iyee_shift(c) to big_corner()+iyee_shift(c) (cf. LOOP_OVER_VOL), so in a yee-shifted direction the clamp truncated the topmost node -- exactly the ghost node the adjoint restriction stencil reads, and one that step_boundaries() already keeps current. The padding was never unable to reach the halo; it simply did not record it. Measured at 2 chunks under MPI with smoothing: 244 stencil lookups fell outside the recorded box, and all 244 were inside the owning fields chunk. The same clamp also left `is` off the component lattice, which desynchronized the LOOP_OVER_IVECS counter from grid_volume::index() (index() subtracts iyee_shift, vec.cpp:476; idx0 does not), so the adjoint DFT was read from the wrong element -- Ex with a 3-way split, ip=(-13,2,-4), is=(-14,0,-6): loop index 64 vs correct index 8. 1365 of 2496 points were misindexed at 3 chunks, 0 at 1 chunk. 2. src/meepgeom.cpp -- material_grids_addgradient() paired forward and adjoint chunks by position in the per-component next_in_dft list. A fields chunk contributes a dft_chunk for a component only where the monitor overlaps that component's owned grid, so the lists can differ in length and order. Pair on (fc, sn, shift), the tuple loop_in_chunks() iterates over, and drop the abort on unequal counts, which is a legitimate configuration. This only shows up at higher process counts: with (1) alone, a 1.7um design region still regressed by 3.3e-02 at -np 8. Also included, each a latent defect rather than something the tested configurations require: index dft[] by position rather than by the loop counter in material_grids_addgradient() and dft_chunk::norm2() (the latter feeds dft_norm() -> stop_when_dft_decayed(), so the solver's own termination criterion was affected); and bounds-check neighbor lookups per dimension rather than on the flat index, since in 3d a point one pixel off in y still yields an index inside [0,N) and silently aliases to an unrelated voxel. Validation. Gradient at num_chunks=N vs 3N over ten configurations (baseline 3d, smoothing off, offset design region, incommensurate resolution 13, coarse resolution 8, thin side padding, wide 1.7um design region, three frequencies, 11x11 grid, 2d), each at 1, 2 and 8 processes: all 30 combinations agree to <= 8e-12, most to ~1e-13 or better. On master nine of the ten fail in serial by 20-62%. 2d passes on master, which is why this went unnoticed -- every adjoint test upstream is 2d or cylindrical. Correctness as well as consistency: test_subpixel_3d (3d adjoint-vs-finite- difference) 5/5, test_adjoint_solver 13/13, plus test_adjoint_cyl, test_adjoint_utils, test_dft_fields, test_dft_energy. The regression test is taken unmodified from NanoComp#3264 (jin-castle). It forces the bug in serial via num_chunks, so it runs in every CI job rather than only the MPI ones. Refs NanoComp#2578, NanoComp#3264.
785cb55 to
897dbe0
Compare
|
@stevengj I think we're ready to review (@jin-castle feel free to take a look too) |
|
The current head ( One point from my earlier comment may still be worth resolving: the remaining silent-zero paths. I agree that unconditional forward/adjoint chunk-count equality is not a valid invariant. The minimal 2d case with forward For a nonempty forward-component list, however, I would expect an adjoint chunk using that component either to find the corresponding Similarly, These checks would keep the current fully distributed data path: they would require no gathering or global materialization of the DFT arrays, no additional communication, and no material increase in DFT storage. Apart from this invariant/diagnostic question, the patch matches my validation. |
test_gradient_matches_fd_with_smoothing holds fd/adjoint to 1% and the adjoint gradient currently depends on the MPI chunk division -- the pre-existing defect NanoComp#3274 fixes. This branch moved the smoothing operating point, and the chunk error that used to hide inside the tolerance now reads 1.3% at np=2 and 7.3% at np=3 (it is np-dependent, the signature of NanoComp#3274, and passes at every rank count with NanoComp#3274 merged in -- verified on a build of this branch merged with its head). Keep the serial assertion at 1%; bound the parallel one at 0.1, still far below the 70-100% disagreement the test guards against, and tighten it back when NanoComp#3274 is in.
|
This looks reasonable… |
LOOP_OVER_IVECS already yields the correct dft[] index when the loop bounds and the subvolume origin lie on the component's Yee lattice. The desynchronization this worked around came from the clamp fixed above, so recomputing the index by position was redundant and the [0, N) guard was dead code.
| /* Clamp to this component's own grid, not to the centered grid. Component c | ||
| runs from little_corner()+iyee_shift(c) to big_corner()+iyee_shift(c) (cf. | ||
| LOOP_OVER_VOL), so clamping to the bare corners truncates the topmost node | ||
| of a yee-shifted direction -- precisely the ghost node that the adjoint | ||
| restriction stencil reaches for, and which step_boundaries() already keeps | ||
| current. It also left `is` off the component lattice, which desynchronized | ||
| the LOOP_OVER_IVECS counter from grid_volume::index(). */ |
There was a problem hiding this comment.
| /* Clamp to this component's own grid, not to the centered grid. Component c | |
| runs from little_corner()+iyee_shift(c) to big_corner()+iyee_shift(c) (cf. | |
| LOOP_OVER_VOL), so clamping to the bare corners truncates the topmost node | |
| of a yee-shifted direction -- precisely the ghost node that the adjoint | |
| restriction stencil reaches for, and which step_boundaries() already keeps | |
| current. It also left `is` off the component lattice, which desynchronized | |
| the LOOP_OVER_IVECS counter from grid_volume::index(). */ | |
| /* Clamp to this component's persist-padded grid, which is one cell bigger than the | |
| usual "owned" grid given by big_owned_corner(). */ |
(written by me)
This is a lighter alternative to #3264, for #2578 (but I use the same test, thanks!)
@jin-castle picked up on a few bugs we had in the existing adjoint-/forward-fields recombination routine. In particular:
Note that this doesn't require serializing the DFT fields (e.g. broadcasting to all the processes) and the tests show notable gradient accuracy. In particular, no dependence on the number of chunks.
(cc @stevengj , @oskooi )