Restructure the undo-attenuation adjoint solver for readability - #2015
Open
Rohit-Kakodkar wants to merge 1 commit into
Open
Restructure the undo-attenuation adjoint solver for readability#2015Rohit-Kakodkar wants to merge 1 commit into
Rohit-Kakodkar wants to merge 1 commit into
Conversation
The algorithm is unchanged; this is about how it reads. combined_undoatt.tpp defined nine nested lambdas across ~100 lines before reaching the ~40 lines of control flow that used them, drove its recursion through a std::function<void(int,int,int,int)> whose four unnamed ints could not be read at the call site, and spelled the 13-view attenuation list four separate times. Extract two concepts into core/specfem/solver/impl/: - attenuation_snapshot.hpp holds the attenuation view list exactly once, as a constexpr tuple of pointer-to-member pairs that drives allocation, both copy directions and zero-fill. AttenuationState replaces the former restore_attenuation_memory/update_attenuation_snapshot pair -- two functions with near-identical signatures that copied in opposite directions -- with restore_into/refresh_from, where the snapshot is always the subject and the preposition gives the direction. - checkpointed_replay.hpp/.tpp carry a glossary fixing the overlapping vocabulary (checkpoint, window, segment, leaf, retained snapshot) and turn every lambda into a named, documented member of CheckpointedReplay. The recursion becomes a real recursive member function taking a StepRange and a ReplayOrigin, which makes structural an invariant that used to be implicit: a replay always resumes at exactly the step its origin describes. The slot pool is deleted rather than reorganised. Since available always equals free_slots.size() and acquisition is strictly LIFO, split_slot was always available - 1, so the recursion's own call stack does the job. Both vectors and the base_slot == -1 sentinel collapse into a scoped local, released at exactly the point slots[split_slot].reset() released it. combined_undoatt.tpp drops from 537 to 143 lines and now reads top to bottom. The stale header comment, which described a flat three-phase algorithm and cited Fortran line numbers, is replaced by the glossary. Verified bit-identical: the dim3 homogeneous_viscoelastic_kernel benchmark produces byte-for-byte identical kernels at subdivide-buffer 1 through 5, covering the flat path, the even split, the uneven short-leaf-first path and deeper recursion. Note that the benchmark default of 1 never enters the recursion at all, so values >= 2 are required for a meaningful comparison. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Refactors the attenuating adjoint time-marching path by extracting checkpoint-replay + attenuation snapshotting into reusable implementation helpers.
Changes:
- Replace in-file combined_undoatt replay/snapshot logic with
specfem::solver::impl::CheckpointedReplay. - Introduce
AttenuationStatesnapshot/reset utilities to manage attenuation memory variables consistently across dims. - Add new replay driver (
checkpointed_replay.{hpp,tpp}) to handle window/segment/leaf reconstruction and kernel accumulation.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| core/specfem/solver/time_marching/combined_undoatt.tpp | Simplifies combined_undoatt run() by delegating replay to a new helper and using shared attenuation reset. |
| core/specfem/solver/impl/checkpointed_replay.hpp | Adds the replay driver public API + supporting types (StepRange, snapshots, origins). |
| core/specfem/solver/impl/checkpointed_replay.tpp | Implements recursive replay/leaf buffering and the adjoint correlation loop. |
| core/specfem/solver/impl/attenuation_snapshot.hpp | Adds attenuation snapshot/capture/restore/refresh and a reset helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+41
to
+45
| template <typename MediumType> struct AttenuationSnapshot { | ||
| using memory_view_type = std::remove_cvref_t< | ||
| decltype(std::declval<MediumType>().memory_variable_kappa)>; | ||
| using strain_view_type = | ||
| std::remove_cvref_t<decltype(std::declval<MediumType>().epsilon_xx_att)>; |
Comment on lines
+186
to
+191
| template <typename ViewType> | ||
| ViewType clone_attenuation_view(const ViewType &view) { | ||
| ViewType clone(std::string(view.label()) + "_snapshot", view.get_mapping()); | ||
| specfem::datatype::deep_copy(clone, view); | ||
| return clone; | ||
| } |
Comment on lines
+3
to
+5
| #include "specfem/compute.tpp" | ||
| #include "specfem/solver/impl/checkpointed_replay.hpp" | ||
| #include "specfem/solver/impl/update_step.hpp" |
Comment on lines
103
to
108
| std::ostringstream strategy_message; | ||
| strategy_message << "Checkpoint replay: " | ||
| << wavefield_checkpoint_task.buffer_subdivisions() | ||
| strategy_message << "Checkpoint replay: " << schedule.buffer_subdivisions() | ||
| << " buffer subdivisions, " | ||
| << wavefield_checkpoint_task.checkpoint_slots( | ||
| checkpoint_interval) | ||
| << " in-memory checkpoints, " | ||
| << wavefield_checkpoint_task.buffer_steps() | ||
| << schedule.checkpoint_slots(checkpoint_interval) | ||
| << " retained snapshots, " << schedule.buffer_steps() | ||
| << " buffered displacement steps"; |
Comment on lines
+76
to
+78
| adjoint_attenuation_ = AttenuationState<DimensionTag>{}; | ||
| adjoint_attenuation_ = | ||
| AttenuationState<DimensionTag>::capture_from(assembly_.attenuation); |
icui
approved these changes
Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A readability-only pass over the UNDO_ATTENUATION adjoint solver. The algorithm is unchanged — this is about how it reads.
Based on
undo-att/checkpoint3, notmain, since the whole undo-attenuation feature is unmerged.Why
combined_undoatt.tpphad a few things working against it:run()defined nine nested lambdas across ~100 lines before the ~40 lines of control flow that used them.std::function<void(int,int,int,int)>— four unnamed ints, soreverse_interval(start + left_length, end, available - 1, split_slot)was unreadable.base_slot == -1was a magic sentinel meaning "reload from disk".slots+free_slots).if constexpr.restore_attenuation_memory(medium, snapshot)wrote the medium;update_attenuation_snapshot(medium, snapshot)wrote the snapshot — near-identical signatures, opposite data direction.What changed
combined_undoatt.tppgoes 537 → 143 lines and now reads straight down: validate config → build schedule → mass matrices → allocate buffer → reset adjoint attenuation → init tasks → loop windows backward → finalize.Two concepts extracted into
core/specfem/solver/impl/:attenuation_snapshot.hpp— the view list lives in exactly oneconstexprtuple of pointer-to-member pairs, which drives allocation, both copy directions, and zero-fill. A new memory variable is now a one-line registration.AttenuationStatereplaces the confusable pair withrestore_into/refresh_from: the snapshot is always the subject, the preposition gives the direction.checkpointed_replay.hpp/.tpp— carries the glossary (checkpoint / window / segment / leaf / retained snapshot) and turns every lambda into a named, documented member ofCheckpointedReplay. The recursion is a real recursive member taking aStepRangeand aReplayOrigin. Because the origin carries its own step, an invariant that used to be implicit — a replay always resumes at exactly the step its origin describes — is now structural.The slot pool is deleted, not reorganised.
availablealways equalsfree_slots.size()and acquisition is strictly LIFO, sosplit_slotwas alwaysavailable - 1— the recursion's own call stack does the job. Both vectors and the-1sentinel collapse into a scoped local, released at exactly the pointslots[split_slot].reset()released it. Peak resident snapshots is unchanged (= recursion depth).Also dropped: the
revolvenaming. It alluded to Griewank–Walther REVOLVE, but this is a simple recursive binary split driven bywavefield_checkpoint::split(), so the word implied an algorithm that isn't there.Verification
Kernels are bit-identical to the pre-refactor code, byte for byte, at
subdivide-buffer1–5 on the dim3homogeneous_viscoelastic_kernelbenchmark — 45.npyfiles. That covers the flat path (1), the even split (2), the uneven short-leaf-first path (3:buffer_steps=67,split=66) and deeper recursion (4, 5).Also: clean build of all three explicit instantiations (dim2/NGLL 5, dim2/NGLL 8, dim3/NGLL 5) with no warnings; 7/7
WavefieldCheckpointtests passing; clang-format clean.Two things worth knowing
subdivide-buffer: 1withnstep 800/time-interval 200givescheckpoint_slots(200) == 0, so every window takes the flat branch. An A/B at defaults would pass with the recursion completely broken — use ≥ 2.subdivide-buffervalues — ~1e-7 relative at 1 vs 2, down to ~1e-12 by 5. That is float32 roundoff, not a bug: a forward state reloaded from an NPY disk checkpoint differs in its last bits from one retained in memory. Pre-existing and unaffected by this PR, but it means A/B comparisons must be same-subdivision.Not included
No new tests. The recursion still has no automated coverage — verification rests on the benchmark A/B. The
CheckpointedReplayshape leaves room to later template the schedule walk on an actions policy and pin its call sequence with a recording mock, but that changes a type signature and doesn't belong in a behavior-preserving pass.🤖 Generated with Claude Code