facett-graphview/examples/gpu_layout_adapters.rs
//! Time the GPU force layout on EVERY hardware adapter the box has.
//!
//! ```text
//! cargo run --release -p facett-graphview --features gpu --example gpu_layout_adapters -- 1000,10000,100000
//! ```
//!
//! For each adapter that is not a CPU rasteriser (llvmpipe / SwiftShader do not count as
//! a GPU), and each node count: upload the graph, run the layout's iteration budget in
//! VRAM, read the positions back — the full converge a browser pays once per graph.
//! Prints one line per (adapter, n) and the load average, because the box is shared.
use std::time::Instant;
use facett_graphview::gpu_layout;
fn graph(n: usize) -> Vec<(usize, usize)> {
// Communities of sqrt(n) nodes: a ring with a local chord each, plus one
// long-range edge per 16 nodes — the same shape the browser bench fixture uses.
let c = (n as f64).sqrt().ceil() as usize;
let per = n.div_ceil(c).max(1);
let mut e = Vec::with_capacity(n * 2 + n / 16);
let mut r: u64 = 7;
let mut next = || {
r = r.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
(r >> 33) as usize
};
for k in 0..c {
let lo = k * per;
let hi = ((k + 1) * per).min(n);
if hi <= lo {
break;
}
let len = hi - lo;
for i in lo..hi {
e.push((i, lo + (i - lo + 1) % len));
if len > 3 {
e.push((i, lo + (i - lo + 2 + next() % (len - 2)) % len));
}
}
}
for i in (0..n).step_by(16) {
e.push((i, next() % n));
}
e
}
fn load() -> String {
std::fs::read_to_string("/proc/loadavg")
.map(|s| s.split_whitespace().take(3).collect::<Vec<_>>().join(" "))
.unwrap_or_default()
}
fn main() {
let sizes: Vec<usize> = std::env::args()
.nth(1)
.unwrap_or_else(|| "1000,10000,100000".into())
.split(',')
.map(|s| s.trim().parse().expect("sizes: comma-separated integers"))
.collect();
let iters: usize = std::env::args().nth(2).map_or(80, |s| s.parse().expect("iters"));
let instance = wgpu::Instance::default();
let adapters = pollster::block_on(instance.enumerate_adapters(wgpu::Backends::VULKAN));
for adapter in adapters {
let info = adapter.get_info();
if info.device_type == wgpu::DeviceType::Cpu {
println!("skip {} ({:?}) — a CPU rasteriser is not a GPU", info.name, info.device_type);
continue;
}
let Ok((device, queue)) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("gpu_layout_adapters"),
required_features: wgpu::Features::empty(),
required_limits: adapter.limits(),
memory_hints: wgpu::MemoryHints::Performance,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
trace: wgpu::Trace::Off,
})) else {
println!("skip {} — no device", info.name);
continue;
};
for &n in &sizes {
let edges = graph(n);
let e32: Vec<(u32, u32)> = edges.iter().map(|&(a, b)| (a as u32, b as u32)).collect();
let g = gpu_layout::GraphCsr::from_edges(n, &e32, None);
let p = gpu_layout::LayoutParams::default();
let seed = gpu_layout::LayoutState::seed(n, p.ideal_len * (n as f32).sqrt());
// Warm the pipelines once at a small size so shader compile is not timed.
let _ = gpu_layout::force_layout_gpu_on(&device, &queue, 64, &graph(64), 2);
let _ = gpu_layout::force_layout_gpu_on(&device, &queue, 20_000, &graph(20_000), 2);
for kernel in ["all-pairs", "grid"] {
if kernel == "grid" && n < gpu_layout::GRID_MIN_NODES {
continue;
}
// The all-pairs kernel at 100k on an integrated GPU runs for minutes; it
// is timed over a few steps and reported per step (`steps` says how many).
let steps = if kernel == "all-pairs" && n >= 50_000 && info.device_type != wgpu::DeviceType::DiscreteGpu { 4 } else { iters };
let t = Instant::now();
let mut gl = if kernel == "grid" {
gpu_layout::GpuLayout::new_for(&device, &g, &seed)
} else {
gpu_layout::GpuLayout::new(&device, &g, &seed)
};
assert_eq!(gl.kernel(), kernel);
gl.iterate(&device, &queue, &g, &p, steps);
let pos = gl.read_positions(&device, &queue);
let ms = t.elapsed().as_secs_f64() * 1e3;
assert_eq!(pos.len(), n);
assert!(pos.iter().all(|q| q[0].is_finite() && q[1].is_finite()), "non-finite position");
println!(
"adapter={:?} kind={:?} kernel={kernel} n={n} edges={} steps={steps} ms={ms:.1} ms_per_step={:.2} load=[{}]",
info.name,
info.device_type,
edges.len(),
ms / steps as f64,
load()
);
}
}
}
}