The resident state vector was timed against the CPU as whole jobs of one circuit family (ten layers of arbitrary-angle rotations and brickwork entangling pairs, 1000 shots): five runs of each, alternating, at fusion 3, 2 and 1, from 18 qubits to as many as half the host's memory held, on two hosts on 2026-10-30. The rows, each host's load, and what discovering its members took are committed (engine::fabric_measured), and examples/fabric_timing.rs takes them again. A row is faster when every run on the member beat every run on the CPU. auto places a state on a member from the smallest measured size from which every row up to the largest is faster, up to that largest, at the fusion width measured, for a plan of at least the measured launches; a process that has not discovered its members yet does so only where the saving also covers that discovery. Hosts are named by fabric::host (the processor's model, architecture and threads, hashed), members by id, so another processor, device or driver matches nothing and stays on the CPU. What the rows say: the laptop GPU is never faster from 22 qubits up at any fusion width (26 qubits at fusion 3: 3.74 s on the CPU, 4.44 s by CUDA, 4.29 s by OpenCL), so auto keeps every state on that CPU. The integrated GPU beat its CPU in every run at fusion 3 and 2 from 18 to 28 qubits (22 qubits: 205 ms against 108 ms by HIP and 102 ms by OpenCL; 28: 9.81 s against 8.47 s and 8.14 s) and lost from 22 qubits at fusion 1, so auto places fused jobs there, from 18 qubits once the members are discovered and at 28 in a fresh process (discovery takes 1.2 s). A member auto chose that fails during the run hands the job back to the CPU. A job auto placed carries the rule, the host, the day, the load and the measured row it rests on in its receipt (fabric.auto, with fabric_spread), and the plan's reasons say so; wai-quantum fabric prints the rule, where it places here and every measured row (auto_rule). Docs, help, both MCP servers' descriptions, the guide and the changelog say so; agent docs re-blessed. The placement legs hold auto to the measurements on each host. Co-Authored-By: Claude Opus 5.5 <[email protected]>
151 lines
6.3 KiB
Rust
151 lines
6.3 KiB
Rust
//! The measurements `auto` places a state vector by
|
|
//! (`engine::fabric_measured`), taken again on this host: whole jobs of the
|
|
//! measured circuit on the CPU and on each member found here, their runs
|
|
//! alternating, at each fusion width and qubit count asked, printed as the
|
|
//! table's rows.
|
|
//!
|
|
//! ```sh
|
|
//! cargo run --release --example fabric_timing \
|
|
//! --features quantum_engine,quantum_fabric,quantum_fabric_cuda,quantum_fabric_opencl -- \
|
|
//! --runs 5 --qubits 18,20,22,24,26 --fusion 2,3 [--members key,key]
|
|
//! ```
|
|
//!
|
|
//! Every placed job's answers are checked against the CPU's, byte for byte,
|
|
//! before its time is kept. A size whose state would not fit in half the
|
|
//! host's free memory, or past what a member holds, is left out and said so.
|
|
|
|
use serde_json::Value;
|
|
use std::time::Instant;
|
|
use wai_quantum::engine::{catalog, fabric, fabric_measured, receipt, run};
|
|
|
|
fn arg(name: &str) -> Option<String> {
|
|
let args: Vec<String> = std::env::args().collect();
|
|
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1).cloned())
|
|
}
|
|
|
|
fn list<T: std::str::FromStr>(name: &str, default: &str) -> Vec<T> {
|
|
arg(name).unwrap_or_else(|| default.to_string()).split(',').filter(|s| !s.is_empty()).map(|s| s.parse().unwrap_or_else(|_| panic!("{name}: {s}"))).collect()
|
|
}
|
|
|
|
/// `MemAvailable` from the kernel, in bytes, or 8 GiB where it cannot be read.
|
|
fn available() -> u64 {
|
|
std::fs::read_to_string("/proc/meminfo")
|
|
.ok()
|
|
.and_then(|m| m.lines().find(|l| l.starts_with("MemAvailable")).and_then(|l| l.split_whitespace().nth(1)).and_then(|k| k.parse::<u64>().ok()))
|
|
.map_or(8 << 30, |kib| kib << 10)
|
|
}
|
|
|
|
fn load() -> String {
|
|
std::fs::read_to_string("/proc/loadavg").map(|l| l.split_whitespace().take(3).collect::<Vec<_>>().join(" ")).unwrap_or_else(|_| "unread".into())
|
|
}
|
|
|
|
/// The answers, without where the state ran and how long it took.
|
|
fn answers(out: &Value) -> Vec<u8> {
|
|
let mut o = out.clone();
|
|
if let Some(m) = o.as_object_mut() {
|
|
m.remove("fabric");
|
|
m.remove("elapsed_ms");
|
|
}
|
|
receipt::canonical(&o)
|
|
}
|
|
|
|
/// One timed job, and its answers.
|
|
fn timed(v: &Value, cap: u64) -> (f64, Vec<u8>) {
|
|
let t = Instant::now();
|
|
let p = catalog::plan(v, cap).unwrap_or_else(|e| panic!("plan: {e}"));
|
|
let out = run::run(v, &p).unwrap_or_else(|e| panic!("run: {e}"));
|
|
(t.elapsed().as_secs_f64() * 1e3, answers(&out))
|
|
}
|
|
|
|
fn spread(v: &[f64]) -> (f64, f64, f64) {
|
|
let mut s = v.to_vec();
|
|
s.sort_by(f64::total_cmp);
|
|
let median = if s.len() % 2 == 1 { s[s.len() / 2] } else { (s[s.len() / 2 - 1] + s[s.len() / 2]) / 2.0 };
|
|
(s[0], median, s[s.len() - 1])
|
|
}
|
|
|
|
fn main() {
|
|
let runs: usize = arg("--runs").map_or(5, |r| r.parse().expect("--runs"));
|
|
let qubits: Vec<u32> = list("--qubits", "18,20,22,24,26");
|
|
let fusions: Vec<u32> = list("--fusion", "3");
|
|
let only: Vec<String> = list("--members", "");
|
|
let cap = available() / 2;
|
|
let t = Instant::now();
|
|
let listed = fabric::list(cap);
|
|
let discovery_ms = t.elapsed().as_secs_f64() * 1e3;
|
|
let host = fabric::host();
|
|
println!("host {host} discovery_ms {discovery_ms:.0} load {} memory_available {} MiB", load(), available() >> 20);
|
|
let members: Vec<(String, String, u64)> = listed["members"]
|
|
.as_array()
|
|
.into_iter()
|
|
.flatten()
|
|
.filter(|m| m["key"] != "cpu")
|
|
.map(|m| (m["member"].as_str().unwrap_or("").to_string(), m["key"].as_str().unwrap_or("").to_string(), m["detail"]["max_qubits"].as_u64().unwrap_or(0)))
|
|
.filter(|(_, key, _)| only.is_empty() || only.contains(key))
|
|
.collect();
|
|
for (id, key, max) in &members {
|
|
println!("member {key} {id} up to {max} qubits");
|
|
}
|
|
let mut rows = Vec::new();
|
|
let mut warmed = false;
|
|
for &fusion in &fusions {
|
|
for &n in &qubits {
|
|
// The CPU's state, a member's copy back and the job's own work,
|
|
// with room to spare.
|
|
if (1u64 << n) * 16 * 4 > cap {
|
|
println!("skip fusion={fusion} qubits={n}: the state does not fit in half the free memory");
|
|
continue;
|
|
}
|
|
let here: Vec<&(String, String, u64)> = members.iter().filter(|m| m.2 >= u64::from(n)).collect();
|
|
if here.is_empty() {
|
|
println!("skip fusion={fusion} qubits={n}: no member holds it");
|
|
continue;
|
|
}
|
|
let cpu_job = fabric_measured::job(n, fusion, "cpu");
|
|
let launches = fabric::launches(&cpu_job).expect("the job plans");
|
|
if !warmed {
|
|
timed(&cpu_job, cap);
|
|
for m in &here {
|
|
timed(&fabric_measured::job(n, fusion, &m.0), cap);
|
|
}
|
|
warmed = true;
|
|
}
|
|
let before = load();
|
|
let mut cpu = Vec::new();
|
|
let mut per: Vec<Vec<f64>> = vec![Vec::new(); here.len()];
|
|
for _ in 0..runs {
|
|
let (ms, want) = timed(&cpu_job, cap);
|
|
cpu.push(ms);
|
|
for (i, m) in here.iter().enumerate() {
|
|
let (ms, got) = timed(&fabric_measured::job(n, fusion, &m.0), cap);
|
|
assert_eq!(got, want, "{}: fusion {fusion}, {n} qubits: the answers are not the CPU's", m.1);
|
|
per[i].push(ms);
|
|
}
|
|
}
|
|
let after = load();
|
|
let c = spread(&cpu);
|
|
for (i, m) in here.iter().enumerate() {
|
|
let g = spread(&per[i]);
|
|
println!(
|
|
"row key={} fusion={fusion} qubits={n} launches={launches} runs={runs} cpu={:.1}/{:.1}/{:.1} member={:.1}/{:.1}/{:.1} faster={} load {before} -> {after}",
|
|
m.1,
|
|
c.0,
|
|
c.1,
|
|
c.2,
|
|
g.0,
|
|
g.1,
|
|
g.2,
|
|
g.2 < c.0
|
|
);
|
|
rows.push(format!(
|
|
" Row {{ host: {host:?}, member: {:?}, key: {:?}, fusion: {fusion}, qubits: {n}, launches: {launches}, runs: {runs}, cpu_ms: Spread {{ min: {:.1}, median: {:.1}, max: {:.1} }}, member_ms: Spread {{ min: {:.1}, median: {:.1}, max: {:.1} }} }},",
|
|
m.0, m.1, c.0, c.1, c.2, g.0, g.1, g.2
|
|
));
|
|
}
|
|
}
|
|
}
|
|
println!("rows:");
|
|
for r in rows {
|
|
println!("{r}");
|
|
}
|
|
}
|