wai-quantum 0.4.3: metered runs, placement by a run's energy, and a fabric that falls back #226

Merged
dcharlot merged 4 commits from wai-quantum-fabric-energy into main 2026-10-12 19:59:15 -04:00
13 changed files with 1479 additions and 49 deletions

View file

@ -565,8 +565,9 @@ jobs:
done
# The fabric's groundwork for accelerator members (certification one case at
# a time, the fabric-v1 corpus and its pins, the 32-bit-lane words and their
# range guard, the program a member runs in every planner configuration, the
# WGSL kernels' host layouts, member ids and mutants), each by name: a module
# range guard, run meters, metered calibration, placement for a run and the
# fabric's fallback, the program a member runs in every planner configuration,
# the WGSL kernels' host layouts, member ids and mutants), each by name: a module
# filter is satisfied by any one test of the module, so a test dropped or
# compiled out on wasm32 would otherwise go unnoticed.
for t in quantum_fabric::tests::certification_is_certify \
@ -582,6 +583,13 @@ jobs:
quantum_fabric::tests::the_fabric_v1_corpus_of_each_gateset_is_pinned \
quantum_fabric::tests::fabric_v1_covers_every_dispatch_shape \
quantum_fabric::tests::gate_words_are_the_table \
quantum_fabric::tests::a_meter_reading_is_labelled_or_dropped \
quantum_fabric::tests::a_placement_figure_is_a_measurement_or_nothing \
quantum_fabric::tests::figures_go_on_both_members_or_neither \
quantum_fabric::tests::place_for_sends_small_runs_to_the_cpu_and_large_ones_to_the_gpu \
quantum_fabric::tests::the_fabric_falls_back_and_names_the_member_that_produced_the_result \
quantum_fabric::tests::a_metered_fabric_run_seals_its_reading \
quantum_fabric::tests::a_calibration_gives_the_cpu_reference_its_figure \
quantum_fabric::plan::tests::execute_is_the_native_simulator \
quantum_fabric::plan::tests::every_configuration_is_the_native_simulator \
quantum_fabric::plan::tests::long_circuits_tile_as_they_run_gate_by_gate \

View file

@ -2,13 +2,14 @@
#
# wai-quantum-webgpu-member.sh
#
# Runs the WebGPU member's device tests and fails unless (1) all 13 of them ran
# Runs the WebGPU member's device tests and fails unless (1) all 14 of them ran
# and passed, (2) the adapters found are exactly WAI_QUANTUM_WEBGPU_EXPECT, (3) each
# declared adapter earned one bit-exact certificate per pinned corpus digest under
# each implementation (the per-gate kernel, wgsl-lanes32/, and tile runs,
# wgsl-lanes32-tile/), and (4) each declared adapter killed every shader mutant of
# both. These checks are what make a filtered, skipped or adapter-less run fail
# instead of pass.
# wgsl-lanes32-tile/), (4) each declared adapter killed every shader mutant of
# both, and (5) each declared adapter ran metered and on the fabric, falling back
# and producing as placed. These checks are what make a filtered, skipped or
# adapter-less run fail instead of pass.
#
# WAI_QUANTUM_WEBGPU_EXPECT=<key[,key...]|none> [WAI_QUANTUM_WEBGPU_WIDE=1] \
# scripts/wai-quantum-webgpu-member.sh
@ -23,8 +24,8 @@ export WAI_QUANTUM_WEBGPU_REPORT="$dir/webgpu-member.report"
cargo test --locked --features quantum_fabric_webgpu --test webgpu_member \
-- --nocapture --test-threads=1 2>&1 | tee "$log"
grep -q "test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out" "$log" \
|| { echo "::error::the member's 13 tests did not all run and pass"; exit 1; }
grep -q "test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out" "$log" \
|| { echo "::error::the member's 14 tests did not all run and pass"; exit 1; }
grep -qxF "webgpu adapters: as declared ($WAI_QUANTUM_WEBGPU_EXPECT)" "$WAI_QUANTUM_WEBGPU_REPORT" \
|| { echo "::error::the adapters found are not the declared set"; exit 1; }
@ -48,6 +49,8 @@ for key in $(tr ',' '\n' <<<"$WAI_QUANTUM_WEBGPU_EXPECT" | sort -u); do
[ "$n" -eq "$want" ] || { echo "::error::$key: a mutant survived, or the mutant test did not run"; exit 1; }
n=$(grep -cxF "webgpu tile mutants: key=$key killed 3 of 3" "$WAI_QUANTUM_WEBGPU_REPORT" || true)
[ "$n" -eq "$want" ] || { echo "::error::$key: a tile mutant survived, or the mutant test did not run"; exit 1; }
n=$(grep -cxF "webgpu fabric: key=$key ok" "$WAI_QUANTUM_WEBGPU_REPORT" || true)
[ "$n" -eq "$want" ] || { echo "::error::$key: the metered and fabric test did not run for it"; exit 1; }
if [ "${WAI_QUANTUM_WEBGPU_WIDE:-0}" = 1 ] && [ "${key%%-*}" = gpu ]; then
grep -qE "^webgpu wide: key=$key n=2[56] matches native$" "$WAI_QUANTUM_WEBGPU_REPORT" \
|| { echo "::error::$key: no wide run at the member's full size"; exit 1; }

View file

@ -6,6 +6,70 @@ wire format may change and such changes are recorded here.
## v0.4.0 — current
### wai-quantum 0.4.3: metered runs, placement by run, and the fabric wrapper
Additive. The default features, `full`, both implementations of the WebGPU member, and
every existing pin and digest are unchanged; runs, receipts and members stay unmetered
unless a caller supplies a meter.
- **Run meters.** `quantum_fabric::RunMeter`: `start` just before a run's first
submission, `stop` after its last readback, returning the microjoules in the window and
their `EnergyClass`, or none when the window cannot be attributed to the run alone. The
trait is member-neutral, and no implementation ships in the crate. A reading reaches a
result only when `check_label` accepts it; otherwise the run is unmetered (zero, no
class). Every start is matched by one stop, a refused run's reading is discarded, and a
meter that cannot start is never stopped.
- **The WebGPU member, metered.** `WebGpuMember::simulate_metered(c, start, meter)`
returns the state with the accepted reading. Its window runs from the first submission
to the last readback: the device's work, and the host's waits and copies between them;
planning, buffer creation, the upload's staging and the conversion to a state vector
are outside it, and the member's `detail` says so. A circuit refused
before any submission opens no window. `member_metered(corpus, meter)` certifies as
`member` does and gives each entry the figure its runs earn;
`blocking::{simulate_metered, member_metered}`.
- **Placement figures from metered calibrations only.** `EnergyCalibration` (`new`,
`add`, `runs`, `finish`) turns metered runs into a `KernelEntry::energy` figure: the
total energy over the operations performed (`pair_updates`: one pair of amplitudes under
one gate), in nanojoules rounded up, with the runs' class, whose uncertainty is the
largest of the runs' plus what the rounding adds, over their windows together. An
unmetered run, a refused label, a second class, or an uncertainty that no longer fits
its field once the rounding is added leaves no figure.
`detect_local_metered(corpus, meter)` gives the CPU reference its figures. Because
`place` ranks a known figure before none, figures go on the CPU reference and on the
members placement compares it with, or on none. `place` is unchanged; `place_for` and
`Fabric` hold the rule themselves, and read figures on only some candidates as figures
on none.
- **Placement for a run.** `place_for(members, kernel, required, predict_pj)` places by
the least predicted picojoules when every candidate has a prediction, ties falling as
in `place`. Otherwise predictions are not used, and the order is `place`'s, except that
figures on only some candidates count as figures on none. Under synthetic models with a
fixed cost per GPU run, a 3-qubit circuit stays on the CPU and a 24-qubit one goes to
the GPU.
- **The fabric wrapper.** `Fabric` holds members with their `Runner`s (`CpuReference`,
and `WebGpuMember` for its own implementation) and optional meters (`add`,
`add_metered`, `set_model`, `order`, `run`). `run` places the circuit, falls back down
the placement order when a member refuses with `MemberError::{TooWide, OutOfRange,
Memory, Timeout, Device}` (`falls_back`), stops at an invalid circuit or a defect, and
seals the `FabricReceipt` with the member and implementation that produced the result
and its accepted reading. The refusals (`Refused`) are listed beside the receipt in
`FabricRun`, or in `FabricError`; the receipt's signed bytes are unchanged.
`WebGpuError` converts into `MemberError`. A runner states the member it is
(`Runner::member_id`), and a run that reaches a member paired with a runner of another
id is refused there as a defect, so a receipt cannot name a member that did not produce
the result. `ready(future)` gives the output of a future that is ready on its first
poll, which is how a native caller drives `run` without an executor; `&R` is a `Runner`
and `&mut M` a `RunMeter`.
- **Tests.** CPU-side tests, native and under wasm32-wasip2 (each required by name in the
`quantum-wasip2` job), cover the readings, the calibration, figures on both members or
neither, `place_for`, the fallback, and metered fabric receipts. The device test
`g13_metered_runs_and_the_fabric` runs the member under a test meter, calibrates it on
random-v1 with the pinned digests, and runs the fabric with the member held to three
qubits: a wider circuit falls back to the CPU reference, a narrower one is produced on
the member and metered. It runs the tile implementation the same way, whose receipt
names it, and requires a member paired with another member's runner, or with the other
implementation's, to be refused. The member script requires it, and a fabric line per
declared adapter.
### wai-quantum 0.4.2: tile runs, and the WebGPU member in a browser
Additive. The default features, `full`, the per-gate implementation and every existing pin

View file

@ -599,7 +599,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.4.2"
version = "0.4.3"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -698,7 +698,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
version = "0.4.2"
version = "0.4.3"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -589,7 +589,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
version = "0.4.2"
version = "0.4.3"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -995,7 +995,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.4.2"
version = "0.4.3"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -1,6 +1,6 @@
[package]
name = "wai-quantum"
version = "0.4.2"
version = "0.4.3"
edition = "2024"
rust-version = "1.97"
license = "Apache-2.0"

File diff suppressed because it is too large Load diff

View file

@ -39,9 +39,24 @@
//! - **Placement and receipts** go through
//! [`place`](crate::quantum_fabric::place) and
//! [`FabricReceipt`](crate::quantum_fabric::FabricReceipt) unchanged. A member
//! carries no energy figure, so beside the CPU reference, whose id sorts
//! first, the reference is placed; a caller who wants the GPU places over the
//! GPU member alone.
//! carries no energy figure unless a metered calibration earned one
//! ([`WebGpuMember::member_metered`]), so beside the CPU reference, whose id
//! sorts first, the reference is placed; a caller who wants the GPU places
//! over the GPU member alone, or gives both members figures from metered
//! calibrations
//! ([`detect_local_metered`](crate::quantum_fabric::detect_local_metered))
//! and places by them, or by a model of each run
//! ([`place_for`](crate::quantum_fabric::place_for)).
//! - **Metering** ([`WebGpuMember::simulate_metered`]): a caller's
//! [`RunMeter`] brackets a run from its first submission to its last
//! readback, so the window covers the device's work; planning, buffer
//! creation, the upload's staging and the conversion to a state vector are
//! outside it. The reading comes back only when the crate's label check
//! accepts it.
//! - **On the fabric** ([`Fabric`](crate::quantum_fabric::Fabric)): a member
//! is a [`Runner`] for its own implementation, and its refusals for size,
//! range, memory, time and the device hand a circuit down the placement
//! order.
//!
//! Every runtime entry point is an `async fn`, and the crate brings no
//! executor. Natively, every future the GPU interface returns is already
@ -69,14 +84,19 @@
//! - **A software adapter is class `Cpu`.** Its id begins `local/swgpu-`, which
//! sorts after every `local/cpu-<arch>`, so it never displaces the CPU
//! reference by its name.
//! - **Unmetered.** Receipts over a member's runs carry no joules and no
//! acquisition class.
//! - **Unmetered by default.** Without a meter, receipts over a member's runs
//! carry no joules and no acquisition class, and the member no energy
//! figure. No meter ships in this crate; one belongs with the energy source
//! it reads, and attributing a window to one run (the process being the
//! device's only client, for a per-process counter) is the meter's to
//! judge.
use crate::quantum::{Amp, Circuit, Gateset, MAX_QUBITS, ONE, QuantumError, StateVector};
use crate::quantum_energy::EnergyClass;
use crate::quantum_fabric::lanes::{self, GateWords, Lane};
use crate::quantum_fabric::plan::{self, Config, LaneRun, PlannedGate, Program, Step};
use crate::quantum_fabric::wgsl::{self, AdapterIdentity};
use crate::quantum_fabric::{Certificate, Certification, Corpus, FabricClass, KernelEntry, Member, Presence, apply_kernel};
use crate::quantum_fabric::{Certificate, Certification, Corpus, EnergyCalibration, FabricClass, KernelEntry, Member, MemberError, Presence, RunFuture, RunMeter, Runner, Unmetered, Window, apply_kernel, pair_updates};
use core::future::Future;
use core::pin::Pin;
use core::task::{Context, Poll, Waker};
@ -366,7 +386,7 @@ async fn open(adapter: wgpu::Adapter) -> Result<WebGpuMember, Refusal> {
identity.max_buffer = granted.max_buffer_size;
let max_qubits = qubits_for(identity.max_storage_binding, identity.max_buffer);
let detail = format!(
"{backend}; adapter {:?}; {}; driver {:?} {:?}; vendor {:#x}, device {:#x}; translator {}; storage binding {} bytes, buffer {} bytes; up to {max_qubits} qubits; probe passed in this process",
"{backend}; adapter {:?}; {}; driver {:?} {:?}; vendor {:#x}, device {:#x}; translator {}; storage binding {} bytes, buffer {} bytes; up to {max_qubits} qubits; a metered run's window runs from its first submission to its last readback (the device's work, and the host's waits and copies between them); probe passed in this process",
identity.name, identity.device_type, identity.driver, identity.driver_info, identity.vendor, identity.device, identity.translator, identity.max_storage_binding, identity.max_buffer,
);
let member = WebGpuMember {
@ -625,7 +645,8 @@ impl WebGpuMember {
self.backend
}
/// What discovery found: adapter, driver, translator, granted limits. Not
/// What discovery found: adapter, driver, translator, granted limits, and
/// what a metered run's window covers ([`Self::simulate_metered`]). Not
/// signed.
pub fn detail(&self) -> &str {
&self.detail
@ -730,7 +751,7 @@ impl WebGpuMember {
}
let planned: Vec<PlannedGate> = gates.iter().enumerate().map(|(k, w)| PlannedGate::new(*w, u32::try_from(k).unwrap_or(u32::MAX))).collect();
let p = plan::schedule(n as u8, 0, planned, &self.config);
self.run(n as u8, Init::Lanes(state), &p.gates, &p.steps).await
Ok(self.run(n as u8, Init::Lanes(state), &p.gates, &p.steps, &mut Unmetered).await?.0)
}
/// A planned program ([`plan::compile`], [`plan::compile_with`],
@ -739,9 +760,15 @@ impl WebGpuMember {
/// implementation ([`Self::tiled`]); a member without it, or a step it
/// cannot lay out, is refused as [`WebGpuError::Internal`].
pub async fn run_program(&self, p: &Program) -> Result<LaneRun, WebGpuError> {
Ok(self.run_program_metered(p, &mut Unmetered).await?.0)
}
/// [`Self::run_program`] inside a window of `meter`, with its accepted
/// reading (zero and `None` when nothing was submitted).
async fn run_program_metered<M: RunMeter + ?Sized>(&self, p: &Program, meter: &mut M) -> Result<(LaneRun, (u64, Option<EnergyClass>)), WebGpuError> {
let n = p.n_qubits;
if n == 0 {
return Ok(LaneRun { amps: vec![[ONE as i32, 0]], out_of_range: None });
return Ok((LaneRun { amps: vec![[ONE as i32, 0]], out_of_range: None }, (0, None)));
}
if n > self.max_qubits {
return Err(WebGpuError::TooWide { n_qubits: n, max: self.max_qubits });
@ -749,7 +776,7 @@ impl WebGpuMember {
if n > MAX_QUBITS || p.start >= 1usize << n {
return Err(WebGpuError::Internal(2));
}
self.run(n, Init::Basis(p.start), &p.gates, &p.steps).await
self.run(n, Init::Basis(p.start), &p.gates, &p.steps, meter).await
}
/// Equal to `c.simulate_from(start)`, or a typed refusal: what the CPU
@ -757,18 +784,35 @@ impl WebGpuMember {
/// member holds, or a result that leaves 32 bits. Planned with
/// [`Self::config`].
pub async fn simulate_from(&self, c: &Circuit, start: usize) -> Result<StateVector, WebGpuError> {
Ok(self.simulate_with(c, start, &mut Unmetered).await?.0)
}
/// [`Self::simulate_from`] inside a window of `meter`: the state, and the
/// microjoules and class the meter read, which are zero and `None` unless
/// the crate's label check accepts them (`quantum_energy::check_label`).
/// The window opens just before the run's first submission and closes
/// after its last readback, so it covers the device's work and the host's
/// waits on it; planning, buffer creation, the upload's staging and the
/// conversion to a state vector are outside it. A circuit refused before
/// any submission opens no window; a run refused after it opened stops
/// the meter and discards the reading.
pub async fn simulate_metered(&self, c: &Circuit, start: usize, meter: &mut dyn RunMeter) -> Result<(StateVector, u64, Option<EnergyClass>), WebGpuError> {
self.simulate_with(c, start, meter).await
}
async fn simulate_with<M: RunMeter + ?Sized>(&self, c: &Circuit, start: usize, meter: &mut M) -> Result<(StateVector, u64, Option<EnergyClass>), WebGpuError> {
let p = plan::compile_with(c, start, &self.config).map_err(WebGpuError::Circuit)?;
if p.n_qubits > self.max_qubits {
return Err(WebGpuError::TooWide { n_qubits: p.n_qubits, max: self.max_qubits });
}
let run = self.run_program(&p).await?;
let (run, (joules_micro, class)) = self.run_program_metered(&p, meter).await?;
if let Some(gate) = run.out_of_range {
return Err(WebGpuError::OutOfRange { gate });
}
let mut amps = Vec::new();
amps.try_reserve_exact(run.amps.len()).map_err(|e| WebGpuError::Memory(e.to_string()))?;
amps.extend(run.amps.iter().map(|l| Amp { re: i64::from(l[0]), im: i64::from(l[1]) }));
Ok(StateVector { n_qubits: p.n_qubits, amps })
Ok((StateVector { n_qubits: p.n_qubits, amps }, joules_micro, class))
}
/// [`Self::simulate_from`] the all-zeros state.
@ -782,30 +826,61 @@ impl WebGpuMember {
/// range report inside a corpus is an error: its circuits are valid and
/// within the horizon.
pub async fn certify(&self, gs: Gateset, corpus: Corpus) -> Result<Certificate, WebGpuError> {
Ok(self.certify_metered(gs, corpus, &mut Unmetered).await?.0)
}
/// [`Self::certify`], each run inside its own window of `meter`, and the
/// figure those readings earn ([`EnergyCalibration`]).
async fn certify_metered<M: RunMeter + ?Sized>(&self, gs: Gateset, corpus: Corpus, meter: &mut M) -> Result<(Certificate, Option<(u64, EnergyClass)>), WebGpuError> {
let mut cert = Certification::new();
let mut calibration = EnergyCalibration::new();
for c in corpus.circuits(gs) {
let reference = lanes::reference_output(&c).map_err(WebGpuError::Circuit)?;
let run = self.run_program(&plan::compile_with(&c, 0, &self.config).map_err(WebGpuError::Circuit)?).await?;
let (run, (joules_micro, class)) = self.run_program_metered(&plan::compile_with(&c, 0, &self.config).map_err(WebGpuError::Circuit)?, &mut *meter).await?;
if let Some(gate) = run.out_of_range {
return Err(WebGpuError::OutOfRange { gate });
}
cert.add(&lanes::circuit_bytes(&c), &reference, &lanes::output(&run.amps));
calibration.add(pair_updates(&c), joules_micro, class.as_ref());
}
Ok(cert.finish(apply_kernel(gs), &self.implementation, &self.id, &corpus.generator(gs)))
Ok((cert.finish(apply_kernel(gs), &self.implementation, &self.id, &corpus.generator(gs)), calibration.finish()))
}
/// A fabric [`Member`]: present as [`Presence::Detected`], with one entry
/// per gateset, each certified in this call on `corpus`, and no energy
/// figure.
pub async fn member(&self, corpus: Corpus) -> Result<Member, WebGpuError> {
self.member_with(corpus, &mut Unmetered).await
}
/// [`Self::member`], each corpus run inside its own window of `meter`
/// ([`Self::simulate_metered`]): every entry also carries the energy
/// figure its runs earn ([`EnergyCalibration`]: nanojoules per operation,
/// rounded up, with the readings' class), or none if any run went
/// unmetered. The certificates are the ones [`Self::member`] earns. Place
/// this member beside a CPU reference that carries figures of its own
/// ([`detect_local_metered`](crate::quantum_fabric::detect_local_metered)),
/// or give figures to neither.
pub async fn member_metered(&self, corpus: Corpus, meter: &mut dyn RunMeter) -> Result<Member, WebGpuError> {
self.member_with(corpus, meter).await
}
async fn member_with<M: RunMeter + ?Sized>(&self, corpus: Corpus, meter: &mut M) -> Result<Member, WebGpuError> {
let mut kernels = Vec::new();
for gs in Gateset::ALL {
let certificate = self.certify(gs, corpus).await?;
kernels.push(KernelEntry { kernel: apply_kernel(gs).into(), implementation: self.implementation.clone(), certificate, energy: None });
let (certificate, energy) = self.certify_metered(gs, corpus, &mut *meter).await?;
kernels.push(KernelEntry { kernel: apply_kernel(gs).into(), implementation: self.implementation.clone(), certificate, energy });
}
Ok(Member { id: self.id.clone(), class: self.class, presence: Presence::Detected, kernels, detail: self.detail.clone() })
}
/// The same member holding at most `max_qubits` qubits (no more than it
/// holds), so that tests can make it refuse a circuit as too wide.
#[doc(hidden)]
pub fn with_max_qubits(&self, max_qubits: u8) -> WebGpuMember {
WebGpuMember { max_qubits: max_qubits.min(self.max_qubits), ..self.clone() }
}
/// The same member with another cap on workgroups along x, so that tests
/// can force multi-row and ragged grids at small sizes.
#[doc(hidden)]
@ -947,7 +1022,11 @@ impl WebGpuMember {
/// without the tile kernel, a grid past the per-dimension limit) is
/// refused as [`WebGpuError::Internal`] before anything runs; the kernels
/// refuse the rest with report bit 4.
async fn run(&self, n: u8, init: Init<'_>, gates: &[PlannedGate], steps: &[Step]) -> Result<LaneRun, WebGpuError> {
///
/// `meter`'s window opens just before the first submission and closes
/// after the last readback; the accepted reading comes back beside the
/// lanes. A run that ends early stops the meter and discards the reading.
async fn run<M: RunMeter + ?Sized>(&self, n: u8, init: Init<'_>, gates: &[PlannedGate], steps: &[Step], meter: &mut M) -> Result<(LaneRun, (u64, Option<EnergyClass>)), WebGpuError> {
self.alive()?;
let len = 1usize << n;
let plane = 4u64 << n;
@ -1061,6 +1140,7 @@ impl WebGpuMember {
};
scopes.close().await?;
upload?;
let mut window = Window::new(meter);
// The steps, a chunk per submission, each waited on natively before
// the next, so that POLL_TIMEOUT bounds one chunk's GPU time. A chunk
@ -1154,14 +1234,18 @@ impl WebGpuMember {
}
}
}
let _index = self.queue.submit([encoder.finish()]);
let finished = encoder.finish();
window.open();
let _index = self.queue.submit([finished]);
drop((gate_records, run_records, tile_records));
scopes.close().await?;
#[cfg(not(target_arch = "wasm32"))]
wait(&self.device, &self.sink, _index)?;
}
// The report: a host defect ends the run here.
// The report: a host defect ends the run here. A program without
// steps submits nothing before it, so its window opens here.
window.open();
let mut words = [0u32; 2];
self.read(&report, 8, &report_staging, |_, b| {
for (k, w) in b.chunks_exact(4).take(2).enumerate() {
@ -1186,9 +1270,47 @@ impl WebGpuMember {
})
.await?;
}
let reading = window.close();
self.settled()?;
let out_of_range = (words[0] & 1 != 0).then_some(words[1]);
Ok(LaneRun { amps, out_of_range })
Ok((LaneRun { amps, out_of_range }, reading))
}
}
/// How the fabric reads a member's refusal: size, range, memory, time and the
/// device fall back to the next member; the circuit's own refusal is the CPU
/// reference's; anything else is a defect.
impl From<WebGpuError> for MemberError {
fn from(e: WebGpuError) -> MemberError {
match e {
WebGpuError::TooWide { n_qubits, max } => MemberError::TooWide { n_qubits, max },
WebGpuError::OutOfRange { gate } => MemberError::OutOfRange { gate },
WebGpuError::Memory(why) => MemberError::Memory(why),
WebGpuError::Timeout => MemberError::Timeout,
WebGpuError::Device(why) => MemberError::Device(why),
WebGpuError::Circuit(e) => MemberError::Circuit(e),
other => MemberError::Other(other.to_string()),
}
}
}
/// The member on the fabric ([`Fabric`](crate::quantum_fabric::Fabric)): its
/// own implementation, through [`WebGpuMember::simulate_metered`]. Pair it
/// with a [`Member`] whose entries name that implementation, such as
/// [`WebGpuMember::member`]'s; another implementation is
/// [`MemberError::Other`].
impl Runner for WebGpuMember {
fn member_id(&self) -> String {
self.id.clone()
}
fn run<'r>(&'r self, implementation: &'r str, c: &'r Circuit, start: usize, meter: &'r mut dyn RunMeter) -> RunFuture<'r> {
Box::pin(async move {
if implementation != self.implementation {
return Err(MemberError::Other(format!("member {} runs implementation {}, not {implementation:?}", self.id, self.implementation)));
}
Ok(self.simulate_with(c, start, meter).await?)
})
}
}
@ -1197,7 +1319,8 @@ impl WebGpuMember {
pub mod blocking {
use super::{Discovery, WebGpuError, WebGpuMember};
use crate::quantum::{Circuit, Gateset, StateVector};
use crate::quantum_fabric::{Certificate, Corpus, Member};
use crate::quantum_energy::EnergyClass;
use crate::quantum_fabric::{Certificate, Corpus, Member, RunMeter};
use core::future::Future;
use core::task::{Context, Poll, Waker};
@ -1232,6 +1355,16 @@ pub mod blocking {
pub fn member(m: &WebGpuMember, corpus: Corpus) -> Result<Member, WebGpuError> {
resolve(m.member(corpus))?
}
/// [`WebGpuMember::simulate_metered`].
pub fn simulate_metered(m: &WebGpuMember, c: &Circuit, start: usize, meter: &mut dyn RunMeter) -> Result<(StateVector, u64, Option<EnergyClass>), WebGpuError> {
resolve(m.simulate_metered(c, start, meter))?
}
/// [`WebGpuMember::member_metered`].
pub fn member_metered(m: &WebGpuMember, corpus: Corpus, meter: &mut dyn RunMeter) -> Result<Member, WebGpuError> {
resolve(m.member_metered(corpus, meter))?
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]

View file

@ -26,7 +26,10 @@ use wai_quantum::quantum::{BaseGate, Circuit, Gate, Gateset, MAX_QUBITS, Quantum
use wai_quantum::quantum_fabric::lanes::{self, GateWords, Lane};
use wai_quantum::quantum_fabric::plan::{self, Config, PlannedGate, Program, Step};
use wai_quantum::quantum_fabric::wgsl::{self, Detector, MUTANTS, Mutant, TILE_MUTANTS};
use wai_quantum::quantum_fabric::{Corpus, FABRIC_DEFAULT, FabricClass, FabricReceipt, Parity, Presence, REFERENCE, apply_kernel, detect_local, place};
use wai_quantum::quantum_energy::{CounterFiltering, EnergyClass, Uncertainty};
use wai_quantum::quantum_fabric::{
Corpus, CpuReference, EnergyCalibration, FABRIC_DEFAULT, Fabric, FabricClass, FabricReceipt, Member, MemberError, Parity, Presence, REFERENCE, Refused, RunMeter, apply_kernel, detect_local, pair_updates, place,
};
use wai_quantum::quantum_fabric_webgpu::{Discovery, WebGpuError, WebGpuMember, blocking, probe_pairs};
/// The CPU's corpus digests: random-v1 (seed 7, 200 cases) and fabric-v1
@ -801,3 +804,143 @@ fn g12_tiles_match_per_gate() {
}
}
}
/// A meter for these tests (the crate ships none): a fixed reading, and how
/// often each end was called.
struct FixedMeter {
reading: Option<(u64, EnergyClass)>,
starts: u32,
stops: u32,
}
impl FixedMeter {
fn new(reading: Option<(u64, EnergyClass)>) -> FixedMeter {
FixedMeter { reading, starts: 0, stops: 0 }
}
}
impl RunMeter for FixedMeter {
fn start(&mut self) -> Result<(), String> {
self.starts += 1;
Ok(())
}
fn stop(&mut self) -> Result<Option<(u64, EnergyClass)>, String> {
self.stops += 1;
Ok(self.reading.clone())
}
}
fn counter() -> EnergyClass {
EnergyClass::OnChipCounter { uncertainty: Uncertainty { relative_ppm: 50_000, window_us: 1_000 }, filtering: CounterFiltering::Off }
}
#[test]
fn g13_metered_runs_and_the_fabric() {
let signer = SigningKey::from_bytes(&[7; 32]);
let local = detect_local();
for m in members() {
// A metered run: one window, and the reading only when its label is
// accepted.
let c = random_circuit(8, 60, 0x6d65_7465, Gateset::V2);
let native = c.simulate_from(3).unwrap();
let mut meter = FixedMeter::new(Some((1234, counter())));
let (state, joules, class) = blocking::simulate_metered(m, &c, 3, &mut meter).unwrap();
assert_eq!(state, native, "{}", m.key());
assert_eq!((joules, class, meter.starts, meter.stops), (1234, Some(counter()), 1, 1));
// Under the resolution floor: unmetered, the meter stopped once.
let mut below = FixedMeter::new(Some((1, counter())));
let (state, joules, class) = blocking::simulate_metered(m, &c, 3, &mut below).unwrap();
assert_eq!((state == native, joules, class, below.starts, below.stops), (true, 0, None, 1, 1));
// Nothing submitted, no window: refusals before the device, and the
// register of no qubits.
let mut idle = FixedMeter::new(Some((1234, counter())));
assert!(matches!(blocking::simulate_metered(m, &refused_circuits()[0], 0, &mut idle), Err(WebGpuError::Circuit(_))));
assert_eq!(blocking::simulate_metered(&m.with_max_qubits(3), &c, 0, &mut idle).unwrap_err(), WebGpuError::TooWide { n_qubits: 8, max: 3 });
let (empty, joules, class) = blocking::simulate_metered(m, &Circuit::new(0), 0, &mut idle).unwrap();
assert_eq!((empty, joules, class), (Circuit::new(0).simulate().unwrap(), 0, None));
assert_eq!((idle.starts, idle.stops), (0, 0));
// A metered calibration: the certificates member() earns, and a figure
// from every run's reading.
let mut meter = FixedMeter::new(Some((1000, counter())));
let metered = blocking::member_metered(m, RANDOM_V1, &mut meter).unwrap_or_else(|e| panic!("{}: {e}", m.key()));
assert_eq!((meter.starts, meter.stops), (400, 400));
for ((entry, gs), (_, pin, _)) in metered.kernels.iter().zip(Gateset::ALL).zip(PINS) {
assert_eq!((entry.certificate.parity, hex(&entry.certificate.corpus)), (Parity::BitExact, pin.to_string()), "{} {gs:?}", m.key());
let mut expect = EnergyCalibration::new();
for c in RANDOM_V1.circuits(gs) {
expect.add(pair_updates(&c), 1000, Some(&counter()));
}
assert_eq!(entry.energy, expect.finish());
assert!(entry.energy.as_ref().is_some_and(|(nj, class)| *nj > 0 && class.label() == "OnChipCounter"));
}
// The fabric: the member first by synthetic figures (1 nJ per
// operation against the CPU reference's 10), holding three qubits.
let figured = |member: &Member, nj: u64| {
let mut member = member.clone();
member.kernels.iter_mut().for_each(|k| k.energy = Some((nj, counter())));
member
};
let gpu = figured(&metered, 1);
let mut fabric = Fabric::new();
fabric.add(figured(&local, 10), CpuReference).add_metered(gpu.clone(), m.with_max_qubits(3), FixedMeter::new(Some((1234, counter()))));
// Wider than it holds: it refuses, and the CPU reference produces the
// result.
let wide = random_circuit(6, 40, 0x7769_6465, Gateset::V1);
let out = blocking::resolve(fabric.run(&wide, 0, &Parity::BitExact, &signer, "local/test")).unwrap().unwrap_or_else(|e| panic!("{}: {e}", m.key()));
assert_eq!(out.state, wide.simulate().unwrap());
assert_eq!((out.receipt.member_id.as_str(), out.receipt.implementation.as_str()), (local.id.as_str(), REFERENCE));
assert_eq!((out.receipt.joules_micro, out.receipt.energy_class.as_ref()), (0, None));
assert!(out.receipt.verify());
assert_eq!(out.refusals, [Refused { member_id: m.id().into(), implementation: m.implementation().into(), error: MemberError::TooWide { n_qubits: 6, max: 3 } }]);
// Within it: the member produces the result, metered, and the receipt
// binds its certificate.
let narrow = random_circuit(3, 40, 0x6e61_7272, Gateset::V2);
let out = blocking::resolve(fabric.run(&narrow, 5, &Parity::BitExact, &signer, "local/test")).unwrap().unwrap_or_else(|e| panic!("{}: {e}", m.key()));
assert_eq!(out.state, narrow.simulate_from(5).unwrap());
assert_eq!((out.receipt.member_id.as_str(), out.receipt.implementation.as_str()), (m.id(), m.implementation()));
assert_eq!((out.receipt.joules_micro, out.receipt.energy_class.as_ref()), (1234, Some(&counter())));
assert!(out.receipt.verify());
let entry = gpu.kernels.iter().find(|k| k.kernel == apply_kernel(Gateset::V2)).unwrap();
assert!(out.receipt.certificate_matches(&entry.certificate));
assert!(out.refusals.is_empty());
// The tile implementation on the same device: a metered run gives the
// native state inside one window, and on the fabric its receipt names
// the tile implementation and binds that implementation's certificate.
let tiled = run(m.tiled());
assert_ne!(tiled.implementation(), m.implementation());
let mut meter = FixedMeter::new(Some((1234, counter())));
let (state, joules, class) = blocking::simulate_metered(&tiled, &c, 3, &mut meter).unwrap();
assert_eq!(state, native, "{} tiled", m.key());
assert_eq!((joules, class, meter.starts, meter.stops), (1234, Some(counter()), 1, 1));
let tile_member = figured(&blocking::member(&tiled, RANDOM_V1).unwrap_or_else(|e| panic!("{}: {e}", m.key())), 1);
let mut fabric = Fabric::new();
fabric.add(figured(&local, 10), CpuReference).add_metered(tile_member.clone(), tiled.clone(), FixedMeter::new(Some((1234, counter()))));
let out = blocking::resolve(fabric.run(&narrow, 5, &Parity::BitExact, &signer, "local/test")).unwrap().unwrap_or_else(|e| panic!("{}: {e}", m.key()));
assert_eq!(out.state, narrow.simulate_from(5).unwrap());
assert_eq!((out.receipt.member_id.as_str(), out.receipt.implementation.as_str()), (tiled.id(), tiled.implementation()));
assert_eq!((out.receipt.joules_micro, out.receipt.energy_class.as_ref()), (1234, Some(&counter())));
assert!(out.receipt.verify());
let entry = tile_member.kernels.iter().find(|k| k.kernel == apply_kernel(Gateset::V2)).unwrap();
assert!(out.receipt.certificate_matches(&entry.certificate));
// The tile implementation's entries with the per-gate runner: the
// runner does not run that implementation, which stops the run, and
// nothing is sealed.
let mut fabric = Fabric::new();
fabric.add(figured(&local, 10), CpuReference).add(tile_member.clone(), m.clone());
let e = blocking::resolve(fabric.run(&narrow, 5, &Parity::BitExact, &signer, "local/test")).unwrap().unwrap_err();
assert!(matches!(&e.refusals[..], [Refused { error: MemberError::Other(_), .. }]), "{} {:?}", m.key(), e.refusals);
// A member that is not the runner's: refused before anything runs.
let mut stranger = gpu.clone();
stranger.id = "local/gpu-test-0123456789ab".into();
stranger.kernels.iter_mut().for_each(|k| k.certificate.member = stranger.id.clone());
let mut fabric = Fabric::new();
fabric.add(figured(&local, 10), CpuReference).add(stranger.clone(), m.clone());
let e = blocking::resolve(fabric.run(&narrow, 5, &Parity::BitExact, &signer, "local/test")).unwrap().unwrap_err();
assert!(matches!(&e.refusals[..], [Refused { member_id, error: MemberError::Other(why), .. }] if *member_id == stranger.id && why.contains(m.id())), "{} {:?}", m.key(), e.refusals);
report(&format!("webgpu fabric: key={} ok", m.key()));
}
}

2
wai/wai-rs/Cargo.lock generated
View file

@ -4459,7 +4459,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
version = "0.4.2"
version = "0.4.3"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -1226,7 +1226,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
version = "0.4.2"
version = "0.4.3"
dependencies = [
"blake3",
"ed25519-dalek",