#[cfg(target_os = "android")]
fn main() {
println!(
"hybrid_gpu_report is a desktop-only example: it measures the wgpu \
backend, which is target-gated off Android."
);
}
#[cfg(not(target_os = "android"))]
fn main() {
desktop::run();
}
#[cfg(not(target_os = "android"))]
mod desktop {
#[cfg(all(feature = "gpu", not(target_os = "android")))]
use petir::mathf::RealMath;
#[cfg(all(feature = "gpu", not(target_os = "android")))]
use std::sync::Arc;
#[cfg(all(feature = "gpu", not(target_os = "android")))]
use std::time::{Duration, Instant};
#[cfg(all(feature = "gpu", not(target_os = "android")))]
use outram_foam_basic_lib::compute::ComputeBackend;
#[cfg(all(feature = "gpu", not(target_os = "android")))]
use outram_foam_basic_lib::ldu_matrix::ldu_matrix::LduMatrix;
#[cfg(all(feature = "gpu", not(target_os = "android")))]
use outram_foam_basic_lib::ldu_matrix::parallel::{self as par, HybridLdu, LduTopology};
#[cfg(all(feature = "gpu", not(target_os = "android")))]
const SIZES: &[usize] = &[1 << 12, 1 << 14, 1 << 16, 1 << 18, 1 << 20, 1 << 22];
#[cfg(all(feature = "gpu", not(target_os = "android")))]
const REPEATS: u32 = 5;
pub fn run() {
println!("# Hybrid backend — parity and crossover report\n");
report_environment();
#[cfg(all(feature = "gpu", not(target_os = "android")))]
{
use outram_foam_basic_lib::compute::gpu;
match gpu::context() {
Some(ctx) => {
println!("GPU adapter : {}", ctx.adapter_label());
println!("max lanes : {}", ctx.max_lanes());
println!("max storage bufs : {}\n", ctx.max_storage_buffers());
parity_section();
crossover_section();
}
None => println!("\nNo GPU adapter — GPU columns omitted (a valid outcome).\n"),
}
}
#[cfg(not(all(feature = "gpu", not(target_os = "android"))))]
{
println!(
"\nBuilt without the `gpu` feature — rebuild with \
`--features gpu,parallel` for the GPU columns.\n"
);
}
}
fn report_environment() {
println!("host threads : {}", num_threads());
println!(
"features : gpu={} parallel={}",
cfg!(feature = "gpu"),
cfg!(feature = "parallel")
);
println!("f64 oracle : ComputeBackend::Serial");
}
fn num_threads() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
#[cfg(all(feature = "gpu", not(target_os = "android")))]
fn laplacian(n: usize) -> (LduMatrix, LduTopology) {
let owner: Vec<usize> = (0..n - 1).collect();
let neighbour: Vec<usize> = (1..n).collect();
let mut m = LduMatrix::new(n, owner, neighbour);
m.diag.iter_mut().for_each(|d| *d = 2.0);
m.lower.iter_mut().for_each(|v| *v = -1.0);
m.upper.iter_mut().for_each(|v| *v = -1.0);
let t = LduTopology::from_matrix(&m);
(m, t)
}
#[cfg(all(feature = "gpu", not(target_os = "android")))]
fn operand(n: usize) -> Vec<f64> {
(0..n).map(|i| ((i % 17) as f64) * 0.25 - 2.0).collect()
}
#[cfg(all(feature = "gpu", not(target_os = "android")))]
fn parity_operand(n: usize) -> Vec<f64> {
(0..n)
.map(|i| {
let t = (i as f64) * std::f64::consts::PI / 1000.0;
t.r_sin() * std::f64::consts::E + (i as f64) / 3.0
})
.collect()
}
#[cfg(all(feature = "gpu", not(target_os = "android")))]
fn parity_laplacian(n: usize) -> (LduMatrix, LduTopology) {
let owner: Vec<usize> = (0..n - 1).collect();
let neighbour: Vec<usize> = (1..n).collect();
let mut m = LduMatrix::new(n, owner, neighbour);
for (i, d) in m.diag.iter_mut().enumerate() {
*d = 2.0 + (i as f64) / 7.0_f64.sqrt();
}
for (i, v) in m.lower.iter_mut().enumerate() {
*v = -1.0 - (i as f64) / 3.0_f64.sqrt();
}
for (i, v) in m.upper.iter_mut().enumerate() {
*v = -1.0 - (i as f64) / 11.0_f64.sqrt();
}
let t = LduTopology::from_matrix(&m);
(m, t)
}
#[cfg(all(feature = "gpu", not(target_os = "android")))]
fn deviation(got: &[f64], want: &[f64]) -> (f64, f64) {
let mut max = 0.0_f64;
let mut sq = 0.0_f64;
for (g, w) in got.iter().zip(want) {
let rel = (g - w).abs() / w.abs().max(1.0);
max = max.max(rel);
sq += rel * rel;
}
(max, (sq / got.len().max(1) as f64).sqrt())
}
#[cfg(all(feature = "gpu", not(target_os = "android")))]
fn time<F: FnMut()>(mut f: F) -> Duration {
f(); let t0 = Instant::now();
for _ in 0..REPEATS {
f();
}
t0.elapsed() / REPEATS
}
#[cfg(all(feature = "gpu", not(target_os = "android")))]
fn parity_section() {
use outram_foam_basic_lib::ldu_matrix::parallel::gpu as kgpu;
println!("## Parity vs the serial f64 oracle\n");
println!("Deviations are relative, computed over the whole output array.");
println!("GPU kernels compute in f32 (WGSL has no f64), so ~1.2e-7 is the floor.\n");
println!("| Kernel | n | max rel | RMS rel |");
println!("|---|---:|---:|---:|");
let n = 1 << 20;
let (m, t) = parity_laplacian(n);
let x = parity_operand(n);
let hybrid = HybridLdu::new(Arc::new(m.clone()));
let want = hybrid.spmv(&x, ComputeBackend::Serial);
let mut got = vec![0.0; n];
if kgpu::spmv_into(&m, &t, &x, &mut got).is_some() {
let (mx, rms) = deviation(&got, &want);
println!("| `spmv` (1-D Laplacian) | {n} | {mx:.2e} | {rms:.2e} |");
}
let y0 = parity_operand(n);
let mut want_y = y0.clone();
par::axpy(2.5, &x, &mut want_y, ComputeBackend::Serial);
let mut got_y = y0.clone();
if kgpu::axpy(2.5, &x, &mut got_y).is_some() {
let (mx, rms) = deviation(&got_y, &want_y);
println!("| `axpy` | {n} | {mx:.2e} | {rms:.2e} |");
}
let mut want_s = x.clone();
par::scale(-1.5, &mut want_s, ComputeBackend::Serial);
let mut got_s = x.clone();
if kgpu::scale(-1.5, &mut got_s).is_some() {
let (mx, rms) = deviation(&got_s, &want_s);
println!("| `scale` | {n} | {mx:.2e} | {rms:.2e} |");
}
let b: Vec<f64> = parity_operand(n)
.iter()
.map(|v| v * std::f64::consts::LN_2)
.collect();
let want_d = par::dot(&x, &b, ComputeBackend::Serial);
if let Some(got_d) = kgpu::dot(&x, &b) {
let rel = (got_d - want_d).abs() / want_d.abs().max(1.0);
println!("| `dot` | {n} | {rel:.2e} | — |");
}
let want_l1 = par::norm_l1(&x, ComputeBackend::Serial);
if let Some(got_l1) = kgpu::norm_l1(&x) {
let rel = (got_l1 - want_l1).abs() / want_l1.abs().max(1.0);
println!("| `norm_l1` | {n} | {rel:.2e} | — |");
}
println!();
}
#[cfg(all(feature = "gpu", not(target_os = "android")))]
fn crossover_section() {
use outram_foam_basic_lib::ldu_matrix::parallel::gpu as kgpu;
println!("## Crossover — wall clock per call, including host-device transfer\n");
println!("| Kernel | n | serial | cpu-multi | gpu | gpu vs cpu-multi |");
println!("|---|---:|---:|---:|---:|---:|");
let mut spmv_cross: Option<usize> = None;
let mut axpy_cross: Option<usize> = None;
let mut dot_cross: Option<usize> = None;
for &n in SIZES {
let (m, t) = laplacian(n);
let x = operand(n);
let hybrid = HybridLdu::new(Arc::new(m.clone()));
let ts = time(|| {
std::hint::black_box(hybrid.spmv(&x, ComputeBackend::Serial));
});
let tc = time(|| {
std::hint::black_box(hybrid.spmv(&x, ComputeBackend::CpuMulti));
});
let mut buf = vec![0.0; n];
let tg = time(|| {
std::hint::black_box(kgpu::spmv_into(&m, &t, &x, &mut buf));
});
let ratio = tc.as_secs_f64() / tg.as_secs_f64();
if ratio > 1.0 && spmv_cross.is_none() {
spmv_cross = Some(n);
}
println!(
"| `spmv` | {n} | {} | {} | {} | {ratio:.2}x |",
us(ts),
us(tc),
us(tg)
);
let y0 = operand(n);
let mut y = y0.clone();
let ts = time(|| {
let mut yy = y0.clone();
par::axpy(2.5, &x, &mut yy, ComputeBackend::Serial);
std::hint::black_box(&yy);
});
let tc = time(|| {
let mut yy = y0.clone();
par::axpy(2.5, &x, &mut yy, ComputeBackend::CpuMulti);
std::hint::black_box(&yy);
});
let tg = time(|| {
std::hint::black_box(kgpu::axpy(2.5, &x, &mut y));
});
let ratio = tc.as_secs_f64() / tg.as_secs_f64();
if ratio > 1.0 && axpy_cross.is_none() {
axpy_cross = Some(n);
}
println!(
"| `axpy` | {n} | {} | {} | {} | {ratio:.2}x |",
us(ts),
us(tc),
us(tg)
);
let ts = time(|| {
std::hint::black_box(par::dot(&x, &x, ComputeBackend::Serial));
});
let tc = time(|| {
std::hint::black_box(par::dot(&x, &x, ComputeBackend::CpuMulti));
});
let tg = time(|| {
std::hint::black_box(kgpu::dot(&x, &x));
});
let ratio = tc.as_secs_f64() / tg.as_secs_f64();
if ratio > 1.0 && dot_cross.is_none() {
dot_cross = Some(n);
}
println!(
"| `dot` | {n} | {} | {} | {} | {ratio:.2}x |",
us(ts),
us(tc),
us(tg)
);
}
println!("\n## Crossover summary (first size where GPU beats CpuMulti)\n");
for (name, cross) in [
("spmv", spmv_cross),
("axpy", axpy_cross),
("dot", dot_cross),
] {
match cross {
Some(n) => println!("- `{name}`: n = {n}"),
None => {
println!("- `{name}`: never within the swept range — keep it off auto-select")
}
}
}
println!();
}
#[cfg(all(feature = "gpu", not(target_os = "android")))]
fn us(d: Duration) -> String {
format!("{:.1} us", d.as_secs_f64() * 1e6)
}
}