PERF-2 P0: intra-step profiler — StepTimers on the overset solver (RTX_PROFILE; overlap build / predictors / patch BiCGSTAB / background Poisson with its setup-vs-iterate split / round exchange / apply / end exchange), PoissonSolution carries setup_ns and iterate_ns, the harness times advance / restore / snapshot / load sampling / force / FEA predictor and prints the wall split of the coupled phase; no clock is read when profiling is off; the reclassified/step progress denominator fixed (was ×4)
CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (ubuntu-latest) (push) Blocked by required conditions
CI / WASM Build + Size Check (push) Blocked by required conditions
CI / Distributed Training Tests (push) Blocked by required conditions
CI / CI Success (push) Blocked by required conditions
CI / Format Check (push) Failing after 6s
CI / Build (ubuntu-latest) (push) Failing after 5s
CI / Clippy Check (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
Performance Benchmarks / Run Benchmarks (push) Failing after 27s
CI / Build CPU-Only (Explicit) (push) Failing after 1m24s
Documentation / Build API Documentation (push) Failing after 1m34s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YJPeT6WA2e7YvAnS875AHL
This commit is contained in:
Omar Sobh
2026-09-15 22:25:52 -05:00
co-authored by Claude Fable 5.1
parent 2d2fed7181
commit 172a26dee4
11 changed files with 223 additions and 40 deletions
@@ -155,6 +155,35 @@ pub struct OversetSolverState {
}
/// The composite solver.
/// PERF-2 P0 (`docs/perf2_campaign.md`): where one composite step's wall
/// time goes, in nanoseconds, summed over the steps since construction.
/// Present only when `RTX_PROFILE` is set at construction; the phase
/// boundaries are the ones of [`OversetPisoSolver::advance`].
#[derive(Debug, Clone, Default)]
pub struct StepTimers {
/// The overlap rebuild + reclassification + fringe re-stamp (phase 1).
pub overlap_build_ns: u64,
/// The patch predictor (phase 1b: operators, predict, pressure matrix).
pub predictor_patch_ns: u64,
/// The background predictor (phase 2).
pub predictor_bg_ns: u64,
/// Patch pressure solves (BiCGSTAB), summed over rounds.
pub patch_solve_ns: u64,
/// Background pressure solves (rhs assembly + multigrid-PCG), summed.
pub bg_solve_ns: u64,
/// The rest of a round: gathers, scatters, mean removal, Anderson.
pub round_exchange_ns: u64,
/// The correctors' applications on both meshes.
pub apply_ns: u64,
/// The end-of-step exchange, defects and clocks (phase 4).
pub end_exchange_ns: u64,
/// The whole of `advance`.
pub advance_ns: u64,
/// Steps and Schwarz rounds counted.
pub steps: u64,
pub rounds: u64,
}
pub struct OversetPisoSolver {
background: EmbeddedPisoSolver,
patch: CurvilinearPisoSolver,
@@ -162,6 +191,8 @@ pub struct OversetPisoSolver {
params: OversetParameters,
grid: (usize, usize, f64, f64),
pending: Option<PatchMesh>,
/// PERF-2 P0 timers (`RTX_PROFILE`), or `None` — no clock is read then.
timers: Option<Box<StepTimers>>,
/// The first corrector's converged acceptor `p'` of the previous step:
/// the temporal warm start (the correction is correlated step to step).
acceptor_warm: Vec<f64>,
@@ -293,10 +324,32 @@ impl OversetPisoSolver {
params,
grid,
pending: None,
timers: std::env::var_os("RTX_PROFILE").map(|_| Box::default()),
acceptor_warm: Vec::new(),
})
}
/// The step timers, when profiling (`RTX_PROFILE`).
pub fn timers(&self) -> Option<&StepTimers> {
self.timers.as_deref()
}
/// Charge the time since `start` to `slot` and return a fresh start;
/// `None` in, `None` out when not profiling (no clock is read).
fn lap(
&mut self,
start: Option<std::time::Instant>,
slot: fn(&mut StepTimers) -> &mut u64,
) -> Option<std::time::Instant> {
match (start, self.timers.as_deref_mut()) {
(Some(t), Some(s)) => {
*slot(s) += t.elapsed().as_nanos() as u64;
Some(std::time::Instant::now())
}
_ => None,
}
}
/// The background solver.
pub fn background(&self) -> &EmbeddedPisoSolver {
&self.background
@@ -512,6 +565,8 @@ impl OversetPisoSolver {
/// Advance both meshes one step of `dt`.
pub async fn advance(&mut self, field: &mut OversetField, dt: f64) -> CfdResult<OversetResult> {
let (nx, ny, dx, dy) = self.grid;
let t_advance = self.timers.as_ref().map(|_| std::time::Instant::now());
let mut t_lap = t_advance;
// 1. If the patch moves, the overlap follows the NEXT mesh before any
// predictor runs: the background is reclassified (fresh cells
@@ -663,14 +718,17 @@ impl OversetPisoSolver {
self.overlap.stamp_fringe_cells(&mut field.background.p, &p);
}
t_lap = self.lap(t_lap, |s| &mut s.overlap_build_ns);
// 1b. Patch predictor (swaps in the pending mesh).
let patch_start = self.patch.begin_step(&mut field.patch, dt)?;
t_lap = self.lap(t_lap, |s| &mut s.predictor_patch_ns);
// 2. Background predictor.
let bg_start = self.background.begin_step(&mut field.background, dt)?;
if let Some(old) = &old_class {
self.trace_reclassification_source(field, old, dt);
}
t_lap = self.lap(t_lap, |s| &mut s.predictor_bg_ns);
// 3. Correctors: alternating Schwarz on the acceptor p' vector `a`
// (patch solve with Dirichlet a → fringe p' → background solve →
@@ -721,6 +779,7 @@ impl OversetPisoSolver {
}
None => patch_pc.iter_mut().for_each(|v| *v = 0.0),
}
t_lap = self.lap(t_lap, |s| &mut s.patch_solve_ns);
// Background with the fringe p' from the patch.
let fringe_vals = self.overlap.fringe_cell_values(&patch_pc);
self.background
@@ -730,6 +789,7 @@ impl OversetPisoSolver {
dt,
corrector == 0 || round > 0,
)?;
t_lap = self.lap(t_lap, |s| &mut s.bg_solve_ns);
// Pin the composite level: the coupled p' problem is pure
// Neumann (walls everywhere), so its constant mode is
// undamped by the rounds and the warm start hands each
@@ -744,6 +804,10 @@ impl OversetPisoSolver {
step_scale = step_scale.max(g_max);
let scale = step_scale.max(1e-300);
let change = r.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
t_lap = self.lap(t_lap, |s| &mut s.round_exchange_ns);
if let Some(s) = self.timers.as_deref_mut() {
s.rounds += 1;
}
if trace_rounds {
println!(
" corrector {corrector} round {round}: |G(a) a| {change:.3e} / step scale {scale:.3e} = {:.3e} (max|G(a)| {g_max:.3e})",
@@ -785,6 +849,7 @@ impl OversetPisoSolver {
} else {
next
};
t_lap = self.lap(t_lap, |s| &mut s.round_exchange_ns);
}
schwarz_converged &= done;
rounds.push(used);
@@ -800,6 +865,7 @@ impl OversetPisoSolver {
if corrector + 1 < self.params.corrector_steps.max(1) {
field.background.copy_to_starred();
}
t_lap = self.lap(t_lap, |s| &mut s.apply_ns);
}
let patch_max_div = self.patch.max_divergence_pub(&field.patch.flux);
@@ -822,6 +888,11 @@ impl OversetPisoSolver {
patch_iterations,
patch_converged,
);
let _ = self.lap(t_lap, |s| &mut s.end_exchange_ns);
if let (Some(t0), Some(s)) = (t_advance, self.timers.as_deref_mut()) {
s.advance_ns += t0.elapsed().as_nanos() as u64;
s.steps += 1;
}
if (self.patch.time() - self.background.time()).abs()
> 1e-12 * self.patch.time().abs().max(1.0)
{