Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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()
                );
            }
        }
    }
}