embedded3 PERF-3 P1-2: the predictor's device tables derived from the projection's (shared buffers move over; 282 -> 48 ms), the operator key lazy after a refresh (240 -> 0 ms), parallel active / owner maps — slab flag CSV byte-identical; RTX_E3_TABLES_REBUILD=1 = the full-rebuild reference
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 / Clippy Check (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 5s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
CI / Build CPU-Only (Explicit) (push) Failing after 6s
CI / Format Check (push) Failing after 13s
CI / Build (ubuntu-latest) (push) Failing after 1m24s
Documentation / Build API Documentation (push) Failing after 1m26s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-19 18:35:04 -05:00
co-authored by Claude Fable 5.1
parent f6f1fe048b
commit d9a2752cb9
2 changed files with 73 additions and 7 deletions
@@ -55,7 +55,9 @@ fn kernels() -> &'static CgKernels {
/// The prepared operator on the device and the CG's work vectors. /// The prepared operator on the device and the CG's work vectors.
pub struct DeviceCg { pub struct DeviceCg {
key: OperatorKey, /// `None` after a `refresh` (PERF-3 P1-2): the key copies and hashes the whole operator, and
/// the refresh path never consults it — a refreshed solver simply never matches the cache.
key: Option<OperatorKey>,
n: usize, n: usize,
nx: i32, nx: i32,
n_cells: usize, n_cells: usize,
@@ -139,7 +141,7 @@ impl DeviceCg {
link_ptr.push(link_idx.len() as u32); link_ptr.push(link_idx.len() as u32);
} }
Self { Self {
key: OperatorKey::of(problem, params), key: Some(OperatorKey::of(problem, params)),
n, n,
nx: problem.nx as i32, nx: problem.nx as i32,
n_cells, n_cells,
@@ -178,7 +180,9 @@ impl DeviceCg {
/// Whether this prepared operator is the one for `problem` + `params`. /// Whether this prepared operator is the one for `problem` + `params`.
pub fn matches(&self, problem: &Problem, params: &MultigridParameters) -> bool { pub fn matches(&self, problem: &Problem, params: &MultigridParameters) -> bool {
self.key.matches(problem, params) self.key
.as_ref()
.is_some_and(|k| k.matches(problem, params))
} }
/// Refresh the FINE operator (cells, coefficients, links, components) /// Refresh the FINE operator (cells, coefficients, links, components)
@@ -251,7 +255,7 @@ impl DeviceCg {
self.singular = singular_count > 0; self.singular = singular_count > 0;
self.active_host = fine.active.clone(); self.active_host = fine.active.clone();
let l_upload = lap.elapsed(); let l_upload = lap.elapsed();
self.key = OperatorKey::of(problem, params); self.key = None;
let l_key = lap.elapsed(); let l_key = lap.elapsed();
// The V-cycle's finest level follows the operator (its coarser // The V-cycle's finest level follows the operator (its coarser
// levels stay): the fine level alone, no hierarchy build. // levels stay): the fine level alone, no hierarchy build.
@@ -184,7 +184,9 @@ impl DeviceCut {
mask.wall_flux_table(body, t).0 mask.wall_flux_table(body, t).0
}; };
let nc = g.cells(); let nc = g.cells();
use rayon::prelude::*;
let active: Vec<i32> = (0..nc) let active: Vec<i32> = (0..nc)
.into_par_iter()
.map(|i| { .map(|i| {
i32::from(if projection { i32::from(if projection {
mask.cell_active(i) mask.cell_active(i)
@@ -202,6 +204,7 @@ impl DeviceCut {
let ap_v: &[f64] = step.map_or(&cut.a_v, |a| &a.1); let ap_v: &[f64] = step.map_or(&cut.a_v, |a| &a.1);
let ap_w: &[f64] = step.map_or(&cut.a_w, |a| &a.2); let ap_w: &[f64] = step.map_or(&cut.a_w, |a| &a.2);
let owner: Vec<u32> = (0..nc) let owner: Vec<u32> = (0..nc)
.into_par_iter()
.map(|i| mask.master(i).unwrap_or(i) as u32) .map(|i| mask.master(i).unwrap_or(i) as u32)
.collect(); .collect();
// The slaves of every cell as a CSR by counting (no per-cell lists). // The slaves of every cell as a CSR by counting (no per-cell lists).
@@ -246,6 +249,51 @@ impl DeviceCut {
}) })
} }
/// The PREDICTOR tables from the PROJECTION tables of the same mask and
/// time (PERF-3 P1-2): the wall distances, the centroid shifts, the
/// surface velocities, the wall flux and the merged-cell maps are the
/// same arrays — their device buffers move over; only the open flags, the
/// activity flags and the apertures (end-of-step instead of the step's)
/// are recomputed and uploaded. Identical to a fresh `build_with` in
/// every table.
pub(super) fn predictor_from(prev: Self, solver: &Solver, g: Grid) -> Option<Self> {
use rayon::prelude::*;
let mask = solver.mask()?;
let cut = mask.cut()?;
let rt = runtime();
let up_f = |v: &[f64]| -> CudaSlice<f64> {
rt.stream
.memcpy_stod(if v.is_empty() { &[0.0f64][..] } else { v })
.expect("upload")
};
let up_i = |v: &[i32]| -> CudaSlice<i32> { rt.stream.memcpy_stod(v).expect("upload") };
let (nx, ny, nz) = (g.nx, g.ny, g.nz);
let counts = [(nx + 1) * ny * nz, nx * (ny + 1) * nz, nx * ny * (nz + 1)];
let open = |c: usize| -> Vec<i32> {
(0..counts[c])
.into_par_iter()
.map(|idx| {
let kind = match c {
0 => mask.u_kind(idx),
1 => mask.v_kind(idx),
_ => mask.w_kind(idx),
};
i32::from(kind == FaceKind::Fluid)
})
.collect()
};
let active: Vec<i32> = (0..g.cells())
.into_par_iter()
.map(|i| i32::from(mask.is_fluid_cell(i)))
.collect();
Some(Self {
a: [up_f(&cut.a_u), up_f(&cut.a_v), up_f(&cut.a_w)],
open: [up_i(&open(0)), up_i(&open(1)), up_i(&open(2))],
active: up_i(&active),
..prev
})
}
fn ptrs(&self) -> E3CutPtrs { fn ptrs(&self) -> E3CutPtrs {
let rt = runtime(); let rt = runtime();
let s = &rt.stream; let s = &rt.stream;
@@ -526,9 +574,23 @@ impl DeviceStep {
// The prescribed faces take the surface velocity (the host's // The prescribed faces take the surface velocity (the host's
// end-of-step impose), and the next predictor's tables. // end-of-step impose), and the next predictor's tables.
if self.solver.is_moving() { if self.solver.is_moving() {
let shared = self.cut.take().map(|c| c.ub_host); let lap_tables = Instant::now();
self.cut = self.cut = match self.cut.take() {
DeviceCut::build_with(&self.solver, g, Phase::Predictor, t_new, shared.as_ref()); Some(prev) if std::env::var("RTX_E3_TABLES_REBUILD").is_err() => {
DeviceCut::predictor_from(prev, &self.solver, g)
}
// `RTX_E3_TABLES_REBUILD=1`: the full rebuild (the identity reference).
prev => {
let shared = prev.map(|c| c.ub_host);
DeviceCut::build_with(&self.solver, g, Phase::Predictor, t_new, shared.as_ref())
}
};
if std::env::var("RTX_E3_MOVING_PROFILE").is_ok() {
eprintln!(
" moving laps: predictor tables {:.0} ms",
lap_tables.elapsed().as_secs_f64() * 1e3
);
}
let cptrs = self.cut.as_ref().expect("cut").ptrs(); let cptrs = self.cut.as_ref().expect("cut").ptrs();
for c in 0..3i32 { for c in 0..3i32 {
unsafe { unsafe {