PERF-2: red-black symmetric Gauss–Seidel smoother behind a knob (MgSmoother::RedBlack; default lexicographic = the recorded regime, bit-identical) — red/black cell lists per level, the symmetric pair red,black,black,red, the operator cache keyed on the smoother; EmbeddedParameters::poisson_smoother, harness knobs RTX_FSI2O_MG_RB (overset) and RTX_FSI2_MG_RB (embedded, the noise probe); pin: solves the masked problem to the same stop, agrees with lexicographic to 2e-12, cached = uncached bit for bit
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 / Build CPU-Only (Explicit) (push) Failing after 5s
Documentation / Build API Documentation (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 5s
CI / Format Check (push) Failing after 16s
CI / Build (ubuntu-latest) (push) Failing after 2m18s
CI / Clippy Check (push) Failing after 2m38s
Performance Benchmarks / Run Benchmarks (push) Successful in 4m13s

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 23:48:55 -05:00
co-authored by Claude Fable 5.1
parent 26904e37d8
commit cb5771fa71
7 changed files with 208 additions and 5 deletions
@@ -268,11 +268,27 @@ pub enum MgPrecision {
F32,
}
/// The V-cycle's smoother ordering (PERF-2, `docs/perf2_campaign.md`).
/// `Lexicographic` is the recorded regime (row-major symmetric
/// GaussSeidel, a dependency chain through the division per cell);
/// `RedBlack` updates the two colours of the five-point stencil in turn —
/// each colour a map of independent cells (threads, vectors, the GPU) —
/// and is a different preconditioner, gated by the noise probe and the
/// anchor's band, never bit-identical.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MgSmoother {
#[default]
Lexicographic,
RedBlack,
}
/// Multigrid preconditioner parameters.
#[derive(Debug, Clone)]
pub struct MultigridParameters {
/// Precision of the V-cycle (see [`MgPrecision`]).
pub precision: MgPrecision,
/// Smoother ordering (see [`MgSmoother`]).
pub smoother: MgSmoother,
/// Symmetric GaussSeidel sweeps before AND after the coarse correction
/// (default 2; a value of 0 is treated as 1). One count for both on
/// purpose: unequal pre/post counts make the V-cycle non-symmetric and
@@ -290,6 +306,7 @@ impl Default for MultigridParameters {
fn default() -> Self {
Self {
precision: MgPrecision::F64,
smoother: MgSmoother::Lexicographic,
smoother_sweeps: 2,
coarsest_cells: 32,
max_iterations: 500,
@@ -431,6 +448,10 @@ struct Level<T: MgScalar> {
ap: Vec<T>,
/// Row-major indices of the active cells.
cells: Vec<usize>,
/// The active cells with `i + j` even / odd, each row-major (the
/// red-black smoother's two independent maps on the five-point stencil).
red: Vec<usize>,
black: Vec<usize>,
/// Fine index → coarse index (empty on the coarsest level).
coarse_of: Vec<usize>,
}
@@ -491,6 +512,17 @@ impl<T: MgScalar> Level<T> {
}
}
let cells: Vec<usize> = (0..n).filter(|&idx| active[idx]).collect();
let parity = |idx: usize| (idx % nx + idx / nx) % 2;
let red: Vec<usize> = cells
.iter()
.copied()
.filter(|&idx| parity(idx) == 0)
.collect();
let black: Vec<usize> = cells
.iter()
.copied()
.filter(|&idx| parity(idx) == 1)
.collect();
let cast = |v: &[f64]| v.iter().map(|&x| T::from_f64(x)).collect::<Vec<T>>();
Self {
ae: cast(&problem.ae),
@@ -501,6 +533,8 @@ impl<T: MgScalar> Level<T> {
problem,
active,
cells,
red,
black,
coarse_of: Vec::new(),
}
}
@@ -559,6 +593,33 @@ impl<T: MgScalar> Level<T> {
}
}
/// One symmetric red-black GaussSeidel sweep: red, black, black, red —
/// within a colour every cell reads only the other colour, so each
/// half-sweep is a map (symmetric as a preconditioner, like the
/// lexicographic pair).
fn symmetric_gs_rb(&self, b: &[T], x: &mut [T]) {
for &idx in &self.red {
x[idx] = (b[idx] + self.neighbour_sum(x, idx)) / self.ap[idx];
}
for &idx in &self.black {
x[idx] = (b[idx] + self.neighbour_sum(x, idx)) / self.ap[idx];
}
for &idx in &self.black {
x[idx] = (b[idx] + self.neighbour_sum(x, idx)) / self.ap[idx];
}
for &idx in &self.red {
x[idx] = (b[idx] + self.neighbour_sum(x, idx)) / self.ap[idx];
}
}
/// One symmetric sweep in the chosen ordering.
fn smooth(&self, b: &[T], x: &mut [T], smoother: MgSmoother) {
match smoother {
MgSmoother::Lexicographic => self.symmetric_gs(b, x),
MgSmoother::RedBlack => self.symmetric_gs_rb(b, x),
}
}
/// Galerkin coarsening: coarse face coefficient = sum of the fine
/// coefficients across that coarse face, coarse `extra_diag` = sum of
/// the children's. Returns the coarse problem and the parent map.
@@ -601,6 +662,7 @@ pub(crate) struct Hierarchy<T: MgScalar = f64> {
levels: Vec<Level<T>>,
work: Vec<Work<T>>,
sweeps: usize,
smoother: MgSmoother,
}
impl<T: MgScalar> Hierarchy<T> {
@@ -630,6 +692,7 @@ impl<T: MgScalar> Hierarchy<T> {
levels,
work,
sweeps: params.smoother_sweeps.max(1),
smoother: params.smoother,
}
}
@@ -672,7 +735,7 @@ impl<T: MgScalar> Hierarchy<T> {
wf.x[idx] = T::ZERO;
}
for _ in 0..self.sweeps {
fine.symmetric_gs(&wf.b, &mut wf.x);
fine.smooth(&wf.b, &mut wf.x, self.smoother);
}
fine.residual(&wf.b, &wf.x, &mut wf.r);
for &idx in &coarse.cells {
@@ -695,7 +758,7 @@ impl<T: MgScalar> Hierarchy<T> {
wb.x[idx] = T::ZERO;
}
for _ in 0..COARSEST_SWEEPS {
bottom.symmetric_gs(&wb.b, &mut wb.x);
bottom.smooth(&wb.b, &mut wb.x, self.smoother);
}
}
// Up: prolongate, smooth.
@@ -707,7 +770,7 @@ impl<T: MgScalar> Hierarchy<T> {
wf.x[idx] += T::from_f64(COARSE_CORRECTION) * wc.x[fine.coarse_of[idx]];
}
for _ in 0..self.sweeps {
fine.symmetric_gs(&wf.b, &mut wf.x);
fine.smooth(&wf.b, &mut wf.x, self.smoother);
}
}
for &idx in &levels[0].cells {
@@ -762,6 +825,7 @@ struct OperatorKey {
coefficients: Vec<u64>,
smoother_sweeps: usize,
coarsest_cells: usize,
smoother: MgSmoother,
}
impl OperatorKey {
@@ -782,6 +846,7 @@ impl OperatorKey {
coefficients,
smoother_sweeps: params.smoother_sweeps,
coarsest_cells: params.coarsest_cells,
smoother: params.smoother,
}
}
@@ -790,6 +855,7 @@ impl OperatorKey {
&& self.ny == problem.ny
&& self.smoother_sweeps == params.smoother_sweeps
&& self.coarsest_cells == params.coarsest_cells
&& self.smoother == params.smoother
&& self.active == problem.active
&& self.coefficients.iter().copied().eq(problem
.ae