include/pops/runtime/program/cache_manager.hpp Source FileΒΆ

adc_cpp: include/pops/runtime/program/cache_manager.hpp Source File
adc_cpp 0.3.0
Model-free C++23 core for coupled hyperbolic-elliptic systems on adaptive (AMR) meshes, with MPI and GPU (Kokkos) backends
cache_manager.hpp
Go to the documentation of this file.
1#pragma once
2
3// Per-node value cache for the unified Program scheduler (Spec 3 section 17-18, ADC-458). A Program
4// node with a non-always schedule (e.g. every(N).hold) recomputes only when DUE and reuses the
5// cached field in between; an accumulate_dt policy also tracks the summed dt of the skipped steps so
6// the held result is applied with eff_dt = sum(dt_skipped), not N * dt_current. This header owns the
7// typed cache (a MultiFab + its bookkeeping) and the due/store/retrieve/accumulate logic; the
8// codegen wraps a scheduled node in `if (due) { recompute; store } else { retrieve }`. The System
9// owns one CacheManager per installed Program (System::program_cache()) so the checkpoint can reach
10// it; the ProgramContext forwards its cache_* seam ops to that single System-owned manager.
11//
12// CHECKPOINT (Spec 3 section 30, ADC-458): the held cache state (the cached MultiFab, last_update_step,
13// accumulated_dt, valid) round-trips through the EXISTING checkpoint -- exactly the way the System
14// history rings do (gather_global / write_state, MPI-safe, bit-identical). The System exposes the
15// serialize/restore accessors over THIS class (node_ids / name_of / valid / value_of / restore_slot);
16// the sim.checkpoint / sim.restart facade gathers and scatters the slots alongside the block state.
17// A restart against a DIFFERENT compiled Program is rejected by the program-hash guard, and a held
18// scheduled node whose cached value the checkpoint never recorded fails loud at restart.
19//
20// A slot caches EITHER the System aux (a held field solve restores phi/grad/E) OR a named scratch
21// MultiFab (a held rhs / source / linear_combine restores its own scratch buffer). The store/retrieve
22// API is the same in both cases (a deep MultiFab copy keyed by the IR node id); the codegen picks the
23// aux or the scratch as the value to cache per node. A slot carries an optional human-readable NAME
24// (the scheduled operator, e.g. "fields_from_state") so the restart can name a missing node; the
25// codegen's nameless store defaults it to "node_<id>", and the named store is the documented seam a
26// later codegen export uses to carry the operator name verbatim.
27
28#include <cstddef>
29#include <map>
30#include <string>
31#include <utility>
32#include <vector>
33
34#include <pops/core/foundation/types.hpp> // Real
35#include <pops/mesh/storage/multifab.hpp> // MultiFab
36
38
39// One cached node value plus the bookkeeping the schedule needs.
40struct CacheSlot {
41 MultiFab value; // the cached field/state (a deep copy of the last recompute)
42 int last_update_step = -1; // macro step at the last recompute (-1 = never)
43 Real accumulated_dt = Real(0); // for accumulate_dt: summed dt of the steps skipped since
44 bool valid = false; // false until the first store (a cold-start node is always due)
45 std::string name; // scheduled node name ("fields_from_state"); "" = "node_<id>"
46};
47
49 public:
50 // Is node `node_id` due to recompute at `macro_step`? A node never stored (cold start) is always
51 // due; otherwise it is due every `every_n` macro-steps (every_n <= 1 means every step).
52 bool is_due(int node_id, int macro_step, int every_n) const {
53 auto it = slots_.find(node_id);
54 if (it == slots_.end() || !it->second.valid) {
55 return true;
56 }
57 if (every_n <= 1) {
58 return true;
59 }
60 return (macro_step % every_n) == 0;
61 }
62
63 // Store `value` as node `node_id`'s cached result computed at `macro_step`; resets accumulated_dt
64 // (the held value is now fresh).
65 void store(int node_id, const MultiFab& value, int macro_step) {
66 CacheSlot& s = slots_[node_id];
67 s.value = value; // deep copy: the cache outlives the live scratch
68 s.last_update_step = macro_step;
69 s.accumulated_dt = Real(0);
70 s.valid = true;
71 }
72
73 // Same as store(), but also tags the slot with a human-readable NAME (the scheduled operator, e.g.
74 // "fields_from_state") so the checkpoint can name a missing node verbatim at restart. The nameless
75 // store above leaves the name empty (the checkpoint then falls back to "node_<id>"); a later codegen
76 // export of the operator name routes through here without changing the cached-value semantics.
77 void store(int node_id, const MultiFab& value, int macro_step, const std::string& name) {
78 store(node_id, value, macro_step);
79 slots_[node_id].name = name;
80 }
81
82 // The cached value of `node_id` (must be valid: guard with is_due()==false first).
83 const MultiFab& retrieve(int node_id) const { return slots_.at(node_id).value; }
84
85 // Add a skipped step's dt to node `node_id`'s accumulator (accumulate_dt policy). Creates the slot
86 // if the node was never stored (a cold accumulate_dt node accumulates from its first skipped step,
87 // before any recompute), so this never throws on an absent slot -- the sum is the actual skipped dt.
88 void accumulate_dt(int node_id, Real dt) { slots_[node_id].accumulated_dt += dt; }
89
90 // The accumulated skipped dt of `node_id` (0 if the node has no slot yet).
91 Real accumulated_dt(int node_id) const {
92 auto it = slots_.find(node_id);
93 return it == slots_.end() ? Real(0) : it->second.accumulated_dt;
94 }
95
96 // The effective dt a due accumulate_dt step should apply: the actual dt of THIS step plus the dt
97 // summed over the steps skipped since the last recompute. CRITICAL with a variable step_cfl: this
98 // is the real sum of the skipped dt, never N * dt_current. Resets the accumulator (a fresh window
99 // starts after this recompute); call once at the start of a due accumulate_dt step.
100 Real effective_dt(int node_id, Real dt_now) {
101 CacheSlot& s = slots_[node_id];
102 const Real eff = dt_now + s.accumulated_dt;
103 s.accumulated_dt = Real(0);
104 return eff;
105 }
106
107 bool has(int node_id) const {
108 auto it = slots_.find(node_id);
109 return it != slots_.end() && it->second.valid;
110 }
111
112 int last_update_step(int node_id) const {
113 auto it = slots_.find(node_id);
114 return it == slots_.end() ? -1 : it->second.last_update_step;
115 }
116
117 std::size_t size() const { return slots_.size(); }
118
119 void clear() { slots_.clear(); }
120
121 // --- Checkpoint serialize / restore (Spec 3 section 30, ADC-458) ---
122 // The System reads these to gather each slot into the checkpoint and writes restore_slot to rebuild
123 // the cache on restart, mirroring the history ring serialization. Only VALID slots are serialized
124 // (a never-stored cold node carries no value); the codegen-keyed integer id is the stable key.
125
126 // Node ids of every VALID slot, ascending (std::map order). A slot is valid once it has been stored;
127 // a cold accumulate_dt node that only has an accumulator (no value yet) is skipped -- its eff_dt
128 // window restarts from the first post-restart skipped step (the held value it would read is absent).
129 std::vector<int> node_ids() const {
130 std::vector<int> ids;
131 for (const auto& [id, s] : slots_) {
132 if (s.valid) {
133 ids.push_back(id);
134 }
135 }
136 return ids;
137 }
138
139 // The human-readable name of node `node_id` ("fields_from_state"), or "node_<id>" when the slot was
140 // stored without one (today's nameless codegen). Used to name a missing node at restart.
141 std::string name_of(int node_id) const {
142 auto it = slots_.find(node_id);
143 if (it == slots_.end() || it->second.name.empty()) {
144 return "node_" + std::to_string(node_id);
145 }
146 return it->second.name;
147 }
148
149 bool valid(int node_id) const {
150 auto it = slots_.find(node_id);
151 return it != slots_.end() && it->second.valid;
152 }
153
154 Real accumulated_dt_of(int node_id) const { return accumulated_dt(node_id); }
155
156 // The component count of node `node_id`'s cached value (for the checkpoint's per-slot ncomp key).
157 int ncomp_of(int node_id) const { return slots_.at(node_id).value.ncomp(); }
158
159 // The ghost-cell width of node `node_id`'s cached value (for the checkpoint's per-slot ngrow key).
160 // The aux is 1-ghost, but a held SCRATCH slot is allocated at the block-state width (2 ghosts), and a
161 // 2-ghost-stencil consumer reads the 2nd ghost layer -- so restore MUST rebuild with the SAME ngrow,
162 // not a hard-coded 1 (else a held-scratch restart under-reads its outer ghosts).
163 int ngrow_of(int node_id) const { return slots_.at(node_id).value.n_grow(); }
164
165 // The cached MultiFab of node `node_id` (the checkpoint gathers it via the System's gather_global,
166 // exactly like a history slot). @throws if the slot is absent.
167 const MultiFab& value_of(int node_id) const { return slots_.at(node_id).value; }
168
169 // RESTORE (restart) node `node_id` from a checkpoint: take ownership of the restored `value` (the
170 // System scattered the global buffer into it), tag the bookkeeping, and mark it valid -- the inverse
171 // of node_ids/value_of. `name` may be empty (defaults to "node_<id>" on read). Replaces any existing
172 // slot for that id (a re-installed Program re-keys by the same ids).
174 const std::string& name) {
175 CacheSlot& s = slots_[node_id];
176 s.value = std::move(value);
179 s.valid = true;
180 s.name = name;
181 }
182
183 private:
184 std::map<int, CacheSlot> slots_; // node id (IR Value.id) -> its cache slot
185};
186
187} // namespace pops::runtime::program
Field distributed over a level: decomposition (BoxArray) + distribution (DistributionMapping) + ncomp...
Definition multifab.hpp:33
Definition cache_manager.hpp:48
const MultiFab & retrieve(int node_id) const
Definition cache_manager.hpp:83
Real accumulated_dt(int node_id) const
Definition cache_manager.hpp:91
void clear()
Definition cache_manager.hpp:119
std::size_t size() const
Definition cache_manager.hpp:117
Real effective_dt(int node_id, Real dt_now)
Definition cache_manager.hpp:100
const MultiFab & value_of(int node_id) const
Definition cache_manager.hpp:167
void store(int node_id, const MultiFab &value, int macro_step)
Definition cache_manager.hpp:65
int ncomp_of(int node_id) const
Definition cache_manager.hpp:157
std::vector< int > node_ids() const
Definition cache_manager.hpp:129
int ngrow_of(int node_id) const
Definition cache_manager.hpp:163
int last_update_step(int node_id) const
Definition cache_manager.hpp:112
bool is_due(int node_id, int macro_step, int every_n) const
Definition cache_manager.hpp:52
bool valid(int node_id) const
Definition cache_manager.hpp:149
std::string name_of(int node_id) const
Definition cache_manager.hpp:141
void store(int node_id, const MultiFab &value, int macro_step, const std::string &name)
Definition cache_manager.hpp:77
void restore_slot(int node_id, MultiFab value, int last_update_step, Real accumulated_dt, const std::string &name)
Definition cache_manager.hpp:173
void accumulate_dt(int node_id, Real dt)
Definition cache_manager.hpp:88
Real accumulated_dt_of(int node_id) const
Definition cache_manager.hpp:154
bool has(int node_id) const
Definition cache_manager.hpp:107
MultiFab: a field DISTRIBUTED over a level (equivalent of AMReX's MultiFab).
Definition cache_manager.hpp:37
double Real
Definition types.hpp:30
Definition cache_manager.hpp:40
bool valid
Definition cache_manager.hpp:44
int last_update_step
Definition cache_manager.hpp:42
std::string name
Definition cache_manager.hpp:45
Real accumulated_dt
Definition cache_manager.hpp:43
MultiFab value
Definition cache_manager.hpp:41
Base scalar types and the POPS_HD macro (host+device portability).