237 lines
8.8 KiB
Rust
237 lines
8.8 KiB
Rust
//! Kernel benchmarks: a 1M-vertex heightfield (fbm noise + normals) and a
|
|
//! 100k-instance transform (position, quaternion, scale -> mat4 + colour),
|
|
//! on the vector code (4 or 8 lanes: MAKEPAD_KERNEL_LANES), the scalar code and the reference interpreter, on
|
|
//! one thread and on N workers of a persistent pool. Every multi-thread and
|
|
//! vector result is checked bit-identical to the one-thread scalar run.
|
|
//!
|
|
//! The heightfield runs two ways: one pass that evaluates the height three
|
|
//! times per vertex (for finite-difference normals), and the two-pass idiom
|
|
//! (heights first, then normals from the neighbouring heights), which is
|
|
//! how terrain kernels are written.
|
|
//!
|
|
//! cargo run --release -p makepad-script-compute --example bench_kernels
|
|
//! (BENCH_INTERP=1 also times the interpreter, which takes seconds.)
|
|
|
|
use makepad_script_compute::kernel::{compile_with, KernelRef};
|
|
use makepad_script_compute_sched::sched::{Executor, InlineExecutor, Job, ThreadExecutor};
|
|
use makepad_script_compute::Backend;
|
|
use std::sync::Arc;
|
|
use std::time::Instant;
|
|
|
|
const W: usize = 1024;
|
|
|
|
const ONE_PASS: &str = r#"
|
|
let W = 1024
|
|
let base = input(f32)
|
|
let pos = output(vec3)
|
|
let nrm = output(vec3)
|
|
let amp = param(6.0)
|
|
fn height(x, z) { base[int(z) * W + int(x)] + fbm2(vec2(x, z) * 0.01, 4, 2.0, 0.5) * amp }
|
|
fn vertex(i) {
|
|
let x = float(i % W)
|
|
let z = float(i / W)
|
|
let h = height(x, z)
|
|
pos[i] = vec3(x, h, z)
|
|
let dx = height(x + 1.0, z) - h
|
|
let dz = height(x, z + 1.0) - h
|
|
nrm[i] = normalize(vec3(-dx, 1.0, -dz))
|
|
}
|
|
"#;
|
|
|
|
/// Pass 1: heights (and positions).
|
|
const HEIGHTS: &str = r#"
|
|
let W = 1024
|
|
let base = input(f32)
|
|
let pos = output(vec3)
|
|
let hgt = output(f32)
|
|
let amp = param(6.0)
|
|
fn vertex(i) {
|
|
let x = float(i % W)
|
|
let z = float(i / W)
|
|
let h = base[i] + fbm2(vec2(x, z) * 0.01, 4, 2.0, 0.5) * amp
|
|
pos[i] = vec3(x, h, z)
|
|
hgt[i] = h
|
|
}
|
|
"#;
|
|
|
|
/// Pass 2: normals from the neighbours' heights (forward differences;
|
|
/// the last row and column reuse their own height).
|
|
const NORMALS: &str = r#"
|
|
let W = 1024
|
|
let hgt = input(f32)
|
|
let nrm = output(vec3)
|
|
fn vertex(i) {
|
|
let x = i % W
|
|
let z = i / W
|
|
let h = hgt[i]
|
|
let dx = hgt[if x < W - 1 { i + 1 } else { i }] - h
|
|
let dz = hgt[if z < count / W - 1 { i + W } else { i }] - h
|
|
nrm[i] = normalize(vec3(-dx, 1.0, -dz))
|
|
}
|
|
"#;
|
|
|
|
const INSTANCES: &str = r#"
|
|
let src = input(f32)
|
|
let xf = output(mat4, 24, 0, inst)
|
|
let col = output(vec4, 24, 16, inst)
|
|
let t = param(0.0)
|
|
fn instance(i) {
|
|
let p = vec3(src[i * 8], src[i * 8 + 1], src[i * 8 + 2])
|
|
let q = normalize(vec4(src[i * 8 + 3], src[i * 8 + 4], src[i * 8 + 5], src[i * 8 + 6]))
|
|
let s = src[i * 8 + 7]
|
|
let spin = quat_axis_angle(vec3(0.0, 1.0, 0.0), t + hash01(i, 1) * TAU)
|
|
xf[i] = mat4_trs(p + vec3(0.0, sin(t + float(i) * 0.01) * 0.2, 0.0), quat_mul(spin, q), vec3(s, s, s))
|
|
col[i] = vec4(hash01(i, 2), hash01(i, 3), hash01(i, 4), 1.0)
|
|
}
|
|
"#;
|
|
|
|
fn bits(v: &[f32]) -> Vec<u32> {
|
|
v.iter().map(|x| x.to_bits()).collect()
|
|
}
|
|
|
|
/// Best of `reps` wall times (ms) of `f`.
|
|
fn best(reps: usize, mut f: impl FnMut()) -> f64 {
|
|
(0..reps)
|
|
.map(|_| {
|
|
let t0 = Instant::now();
|
|
f();
|
|
t0.elapsed().as_secs_f64() * 1e3
|
|
})
|
|
.fold(f64::MAX, f64::min)
|
|
}
|
|
|
|
struct Run {
|
|
label: &'static str,
|
|
simd: bool,
|
|
threads: usize,
|
|
}
|
|
|
|
fn main() {
|
|
let threads = 8;
|
|
let exec = ThreadExecutor::new(threads - 1);
|
|
let n = W * W;
|
|
let base: Arc<[f32]> = (0..n).map(|i| ((i % 97) as f32) * 0.01).collect::<Vec<_>>().into();
|
|
let interp = std::env::var("BENCH_INTERP").is_ok();
|
|
// the vector code's lanes: this host's rule, or MAKEPAD_KERNEL_LANES=4|8
|
|
let vl: &'static str = if makepad_script_compute::native_vec::host_lanes() == 8 { "x8" } else { "x4" };
|
|
let runs = [Run { label: "scalar", simd: false, threads: 1 }, Run { label: vl, simd: true, threads: 1 }, Run { label: vl, simd: true, threads }];
|
|
let run = |job: &mut Job, r: &Run| {
|
|
job.set_simd(r.simd);
|
|
let e: &dyn Executor = if r.threads > 1 { &exec } else { &InlineExecutor };
|
|
job.run(e, r.threads).unwrap();
|
|
};
|
|
let native = |src: &str| compile_with(src, &[], Backend::Native).unwrap_or_else(|e| panic!("{:?}", e));
|
|
|
|
// -- one pass: three height evaluations per vertex --------------------------
|
|
let k = native(ONE_PASS);
|
|
println!("heightfield, 1M vertices (vector code: {})", k.simd());
|
|
let mut job = Job::new(k.clone(), n);
|
|
job.input("base", base.clone()).unwrap();
|
|
job.output("pos", vec![0.0; n * 3]).unwrap();
|
|
job.output("nrm", vec![0.0; n * 3]).unwrap();
|
|
let mut reference = None;
|
|
for r in &runs {
|
|
let ms = best(3, || run(&mut job, r));
|
|
let out = (bits(job.out("pos").unwrap()), bits(job.out("nrm").unwrap()));
|
|
let same = match &reference {
|
|
None => {
|
|
reference = Some(out);
|
|
"reference"
|
|
}
|
|
Some(want) => {
|
|
assert!(*want == out, "{} x{} differs from the one-thread scalar run", r.label, r.threads);
|
|
"bit-identical"
|
|
}
|
|
};
|
|
println!(" one pass (3 fbm/vertex) {:6} x{} threads: {:7.2} ms ({:5.1} ns/vertex) {}", r.label, r.threads, ms, ms * 1e6 / n as f64, same);
|
|
}
|
|
if interp {
|
|
let ki = compile_with(ONE_PASS, &[], Backend::Interp).unwrap();
|
|
let mut j = Job::new(ki, n);
|
|
j.input("base", base.clone()).unwrap();
|
|
j.output("pos", vec![0.0; n * 3]).unwrap();
|
|
j.output("nrm", vec![0.0; n * 3]).unwrap();
|
|
let ms = best(1, || {
|
|
j.run(&InlineExecutor, 1).unwrap();
|
|
});
|
|
assert!(reference.as_ref().unwrap().0 == bits(j.out("pos").unwrap()));
|
|
println!(" one pass interp x1 threads: {:7.1} ms bit-identical", ms);
|
|
}
|
|
|
|
// -- two passes: heights, then normals from the heights ----------------------
|
|
let kh = native(HEIGHTS);
|
|
let kn = native(NORMALS);
|
|
let mut jh = Job::new(kh.clone(), n);
|
|
jh.input("base", base.clone()).unwrap();
|
|
jh.output("pos", vec![0.0; n * 3]).unwrap();
|
|
jh.output("hgt", vec![0.0; n]).unwrap();
|
|
let mut reference: Option<(Vec<u32>, Vec<u32>)> = None;
|
|
for r in &runs {
|
|
let mut jn: Option<Job> = None;
|
|
let mut hms = f64::MAX;
|
|
let mut nms = f64::MAX;
|
|
let ms = best(5, || {
|
|
let t0 = Instant::now();
|
|
run(&mut jh, r);
|
|
hms = hms.min(t0.elapsed().as_secs_f64() * 1e3);
|
|
// Pass 2 reads pass 1's heights (shared, read-only).
|
|
let h: Arc<[f32]> = jh.out("hgt").unwrap().into();
|
|
let j = jn.get_or_insert_with(|| {
|
|
let mut j = Job::new(kn.clone(), n);
|
|
j.output("nrm", vec![0.0; n * 3]).unwrap();
|
|
j
|
|
});
|
|
j.input("hgt", h).unwrap();
|
|
let t1 = Instant::now();
|
|
run(j, r);
|
|
nms = nms.min(t1.elapsed().as_secs_f64() * 1e3);
|
|
});
|
|
let out = (bits(jh.out("pos").unwrap()), bits(jn.as_ref().unwrap().out("nrm").unwrap()));
|
|
let same = match &reference {
|
|
None => {
|
|
reference = Some(out);
|
|
"reference"
|
|
}
|
|
Some(want) => {
|
|
assert!(*want == out, "{} x{} differs from the one-thread scalar run", r.label, r.threads);
|
|
"bit-identical"
|
|
}
|
|
};
|
|
println!(
|
|
" two passes (heights + normals) {:6} x{} threads: {:7.2} ms = {:.2} + {:.2} ({:4.1} ns/vertex) {}",
|
|
r.label,
|
|
r.threads,
|
|
ms,
|
|
hms,
|
|
nms,
|
|
ms * 1e6 / n as f64,
|
|
same
|
|
);
|
|
}
|
|
|
|
// -- instances -------------------------------------------------------------------
|
|
let m = 100_000;
|
|
let src: Arc<[f32]> = (0..m * 8).map(|i| ((i * 7919) % 1000) as f32 * 0.001 + 0.1).collect::<Vec<_>>().into();
|
|
let k: KernelRef = native(INSTANCES);
|
|
println!("instances, 100k (vector code: {})", k.simd());
|
|
let mut job = Job::new(k, m);
|
|
job.set_param("t", 1.5);
|
|
job.input("src", src).unwrap();
|
|
job.output("inst", vec![0.0; m * 24]).unwrap();
|
|
let mut reference = None;
|
|
for r in &runs {
|
|
let ms = best(5, || run(&mut job, r));
|
|
let out = bits(job.out("inst").unwrap());
|
|
let same = match &reference {
|
|
None => {
|
|
reference = Some(out);
|
|
"reference"
|
|
}
|
|
Some(want) => {
|
|
assert!(*want == out, "instances {} x{} differ", r.label, r.threads);
|
|
"bit-identical"
|
|
}
|
|
};
|
|
println!(" {:6} x{} threads: {:6.2} ms ({:5.1} ns/instance) {}", r.label, r.threads, ms, ms * 1e6 / m as f64, same);
|
|
}
|
|
}
|