Skip to content

Restructure the undo-attenuation adjoint solver for readability - #2015

Open
Rohit-Kakodkar wants to merge 1 commit into
undo-att/checkpoint3from
undo-att/replay-readability
Open

Restructure the undo-attenuation adjoint solver for readability#2015
Rohit-Kakodkar wants to merge 1 commit into
undo-att/checkpoint3from
undo-att/replay-readability

Conversation

@Rohit-Kakodkar

Copy link
Copy Markdown
Collaborator

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, not main, since the whole undo-attenuation feature is unmerged.

Why

combined_undoatt.tpp had a few things working against it:

  • run() defined nine nested lambdas across ~100 lines before the ~40 lines of control flow that used them.
  • The recursion went through std::function<void(int,int,int,int)> — four unnamed ints, so reverse_interval(start + left_length, end, available - 1, split_slot) was unreadable.
  • base_slot == -1 was a magic sentinel meaning "reload from disk".
  • Manual slot lifecycle across two parallel vectors (slots + free_slots).
  • The 13-view attenuation list was written out four times, each with its own dim2/dim3 if constexpr.
  • restore_attenuation_memory(medium, snapshot) wrote the medium; update_attenuation_snapshot(medium, snapshot) wrote the snapshot — near-identical signatures, opposite data direction.
  • Overlapping vocabulary (subset, window, leaf, interval, slot, buffer, subdivision) never defined in one place, and a header comment describing a flat three-phase algorithm that the code stopped being some time ago.

What changed

combined_undoatt.tpp goes 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 one constexpr tuple of pointer-to-member pairs, which drives allocation, both copy directions, and zero-fill. A new memory variable is now a one-line registration. AttenuationState replaces the confusable pair with restore_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 of CheckpointedReplay. The recursion is a real recursive member taking a StepRange and a ReplayOrigin. 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. available always equals free_slots.size() and acquisition is strictly LIFO, so split_slot was always available - 1 — the recursion's own call stack does the job. Both vectors and the -1 sentinel collapse into a scoped local, released at exactly the point slots[split_slot].reset() released it. Peak resident snapshots is unchanged (= recursion depth).

Also dropped: the revolve naming. It alluded to Griewank–Walther REVOLVE, but this is a simple recursive binary split driven by wavefield_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-buffer 1–5 on the dim3 homogeneous_viscoelastic_kernel benchmark — 45 .npy files. 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 WavefieldCheckpoint tests passing; clang-format clean.

Two things worth knowing

  1. The benchmark default never exercises the recursion. subdivide-buffer: 1 with nstep 800 / time-interval 200 gives checkpoint_slots(200) == 0, so every window takes the flat branch. An A/B at defaults would pass with the recursion completely broken — use ≥ 2.
  2. Kernels are not bit-identical across different subdivide-buffer values — ~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 CheckpointedReplay shape 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

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AttenuationState snapshot/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);
Sign up for free to 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.

3 participants