wai-quantum 0.4.3: metered runs, placement by a run's energy, and a fabric that falls back #226
13 changed files with 1479 additions and 49 deletions
12
.github/workflows/wai-conformance.yml
vendored
12
.github/workflows/wai-conformance.yml
vendored
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
wai/tools/quantum-wgsl-check/Cargo.lock
generated
2
wai/tools/quantum-wgsl-check/Cargo.lock
generated
|
|
@ -599,7 +599,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-pack/Cargo.lock
generated
2
wai/wai-pack/Cargo.lock
generated
|
|
@ -698,7 +698,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-quantum-serve/Cargo.lock
generated
2
wai/wai-quantum-serve/Cargo.lock
generated
|
|
@ -589,7 +589,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-quantum/Cargo.lock
generated
2
wai/wai-quantum/Cargo.lock
generated
|
|
@ -995,7 +995,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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")))]
|
||||
|
|
|
|||
|
|
@ -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
2
wai/wai-rs/Cargo.lock
generated
|
|
@ -4459,7 +4459,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-web/Cargo.lock
generated
2
wai/wai-web/Cargo.lock
generated
|
|
@ -1226,7 +1226,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
Loading…
Reference in a new issue