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, 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};
use std::borrow::Cow;
use std::num::NonZeroU64;
use std::sync::{Arc, Mutex};
pub const SUBMIT_BUDGET: u64 = 1 << 28;
pub const RING_RECORDS: u32 = 4096;
pub const READBACK_CHUNK: u64 = 64 << 20;
pub const POLL_TIMEOUT: core::time::Duration = core::time::Duration::from_secs(300);
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum WebGpuError {
NoAdapter(String),
Unsuitable { adapter: String, why: String },
Device(String),
Memory(String),
Timeout,
Pending,
Circuit(QuantumError),
TooWide { n_qubits: u8, max: u8 },
OutOfRange { gate: u32 },
Internal(u32),
Probe { a: i32, b: i32 },
}
impl std::fmt::Display for WebGpuError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WebGpuError::NoAdapter(why) => write!(f, "no adapter: {why}"),
WebGpuError::Unsuitable { adapter, why } => write!(f, "adapter {adapter:?} cannot host the kernel: {why}"),
WebGpuError::Device(e) => write!(f, "device: {e}"),
WebGpuError::Memory(e) => write!(f, "out of memory: {e}"),
WebGpuError::Timeout => write!(f, "a wait on the device passed {} s", POLL_TIMEOUT.as_secs()),
WebGpuError::Pending => write!(f, "a native future was pending after one poll"),
WebGpuError::Circuit(e) => write!(f, "{e}"),
WebGpuError::TooWide { n_qubits, max } => write!(f, "{n_qubits} qubits; this member holds at most {max}"),
WebGpuError::OutOfRange { gate } => write!(f, "the result of op {gate} leaves 32 bits"),
WebGpuError::Internal(bits) => write!(f, "host defect, report bits {bits:#x}"),
WebGpuError::Probe { a, b } => write!(f, "the device's arithmetic differs from the CPU's on {a} × {b}"),
}
}
}
impl std::error::Error for WebGpuError {}
#[derive(Debug)]
pub struct Refusal {
pub adapter: String,
pub key: String,
pub error: WebGpuError,
}
#[derive(Debug)]
pub struct Discovery {
pub found: Vec<WebGpuMember>,
pub refused: Vec<Refusal>,
}
pub async fn discover() -> Discovery {
let mut d = Discovery { found: Vec::new(), refused: Vec::new() };
if wgpu::Instance::enabled_backend_features().is_empty() {
return d;
}
for adapter in instance().enumerate_adapters(wgpu::Backends::PRIMARY).await {
match open(adapter).await {
Ok(m) => d.found.push(m),
Err(r) => d.refused.push(r),
}
}
d
}
fn instance() -> wgpu::Instance {
let mut d = wgpu::InstanceDescriptor::new_without_display_handle();
d.backends = wgpu::Backends::PRIMARY;
d.flags = wgpu::InstanceFlags::empty();
d.backend_options.dx12.shader_compiler = wgpu::Dx12Compiler::Fxc;
wgpu::Instance::new(d)
}
fn backend_label(b: wgpu::Backend) -> Option<&'static str> {
match b {
wgpu::Backend::Metal => Some("metal"),
wgpu::Backend::Vulkan => Some("vulkan"),
wgpu::Backend::Dx12 => Some("dx12"),
wgpu::Backend::BrowserWebGpu => Some("webgpu"),
_ => None,
}
}
fn device_type_label(t: wgpu::DeviceType) -> &'static str {
match t {
wgpu::DeviceType::DiscreteGpu => "discrete",
wgpu::DeviceType::IntegratedGpu => "integrated",
wgpu::DeviceType::VirtualGpu => "virtual",
wgpu::DeviceType::Cpu => "cpu",
wgpu::DeviceType::Other => "other",
}
}
fn translator(b: wgpu::Backend) -> &'static str {
match b {
wgpu::Backend::Dx12 => "naga+dxbc",
wgpu::Backend::BrowserWebGpu => "user-agent",
_ => "naga",
}
}
fn identity(info: &wgpu::AdapterInfo, max_storage_binding: u64, max_buffer: u64) -> AdapterIdentity {
AdapterIdentity {
backend: backend_label(info.backend).map_or_else(|| format!("{:?}", info.backend).to_lowercase(), str::to_string),
name: info.name.clone(),
vendor: info.vendor,
device: info.device,
device_type: device_type_label(info.device_type).into(),
pci_bus: info.device_pci_bus_id.clone(),
driver: info.driver.clone(),
driver_info: info.driver_info.clone(),
translator: translator(info.backend).into(),
max_storage_binding,
max_buffer,
}
}
fn qubits_for(max_storage_binding: u64, max_buffer: u64) -> u8 {
let words = max_storage_binding.min(max_buffer) / 4;
if words < 2 { 0 } else { (words.ilog2() as u8).min(MAX_QUBITS) }
}
#[derive(Default)]
struct Sink {
messages: Vec<String>,
lost: Option<String>,
}
#[derive(Clone)]
struct Pipelines {
apply_layout: wgpu::BindGroupLayout,
probe_layout: wgpu::BindGroupLayout,
apply: wgpu::ComputePipeline,
probe: wgpu::ComputePipeline,
tile: Option<(wgpu::BindGroupLayout, wgpu::ComputePipeline)>,
}
#[derive(Clone)]
pub struct WebGpuMember {
device: wgpu::Device,
queue: wgpu::Queue,
pipelines: Pipelines,
identity: AdapterIdentity,
id: String,
key: String,
class: FabricClass,
backend: &'static str,
detail: String,
max_qubits: u8,
max_x: u32,
config: Config,
ring_records: u32,
readback_chunk: u64,
stride: u64,
implementation: String,
sink: Arc<Mutex<Sink>>,
}
impl std::fmt::Debug for WebGpuMember {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WebGpuMember").field("id", &self.id).field("key", &self.key).field("implementation", &self.implementation).field("config", &self.config).field("max_x", &self.max_x).field("ring_records", &self.ring_records).field("readback_chunk", &self.readback_chunk).field("detail", &self.detail).finish()
}
}
async fn open(adapter: wgpu::Adapter) -> Result<WebGpuMember, Refusal> {
let info = adapter.get_info();
let offered = adapter.limits();
let mut identity = identity(&info, offered.max_storage_buffer_binding_size, offered.max_buffer_size);
let refuse = |identity: &AdapterIdentity, error: WebGpuError| Refusal { adapter: identity.name.clone(), key: identity.key(), error };
let unsuitable = |identity: &AdapterIdentity, why: String| refuse(identity, WebGpuError::Unsuitable { adapter: identity.name.clone(), why });
let Some(backend) = backend_label(info.backend) else {
return Err(unsuitable(&identity, format!("backend {:?} is not supported", info.backend)));
};
if !adapter.get_downlevel_capabilities().flags.contains(wgpu::DownlevelFlags::COMPUTE_SHADERS) {
return Err(unsuitable(&identity, "no compute shaders".into()));
}
let mut short: Option<String> = None;
wgpu::Limits::downlevel_defaults().check_limits_with_fail_fn(&offered, true, |name, needed, has| {
short.get_or_insert_with(|| format!("limit {name}: the kernel needs {needed}, the adapter offers {has}"));
});
if let Some(why) = short {
return Err(unsuitable(&identity, why));
}
let required_limits = wgpu::Limits { max_storage_buffer_binding_size: offered.max_storage_buffer_binding_size, max_buffer_size: offered.max_buffer_size, ..wgpu::Limits::downlevel_defaults() };
let descriptor = wgpu::DeviceDescriptor { label: Some("wai-quantum fabric"), required_features: wgpu::Features::empty(), required_limits, ..Default::default() };
let (device, queue) = match adapter.request_device(&descriptor).await {
Ok(dq) => dq,
Err(e) => return Err(refuse(&identity, WebGpuError::Device(e.to_string()))),
};
let sink = Arc::new(Mutex::new(Sink::default()));
let errors = Arc::clone(&sink);
device.on_uncaptured_error(Arc::new(move |e: wgpu::Error| lock(&errors).messages.push(e.to_string())));
let lost = Arc::clone(&sink);
device.set_device_lost_callback(move |reason, message| {
let mut s = lock(&lost);
if s.lost.is_none() {
s.lost = Some(format!("device lost ({reason:?}): {message}"));
}
});
let pipelines = match kernel_pipelines(&device, Cow::Borrowed(wgsl::SHADER), false).await {
Ok(p) => p,
Err(e) => return Err(refuse(&identity, e)),
};
let granted = device.limits();
identity.max_storage_binding = granted.max_storage_buffer_binding_size;
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; 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 {
stride: u64::from(granted.min_uniform_buffer_offset_alignment).max(wgsl::GATE_RECORD_BYTES as u64),
device,
queue,
pipelines,
id: identity.member_id(),
key: identity.key(),
class: identity.class(),
backend,
detail,
max_qubits,
max_x: wgsl::X_GROUPS_MAX,
config: Config::PER_GATE,
ring_records: RING_RECORDS,
readback_chunk: READBACK_CHUNK,
implementation: wgsl::implementation_id(),
identity,
sink,
};
let pairs = probe_pairs(4096, 0x5eed);
match member.probe(&pairs).await {
Ok(out) => match first_mismatch(&pairs, &out) {
None => Ok(member),
Some((a, b)) => Err(refuse(&member.identity, WebGpuError::Probe { a, b })),
},
Err(e) => Err(refuse(&member.identity, e)),
}
}
fn lock(sink: &Mutex<Sink>) -> std::sync::MutexGuard<'_, Sink> {
sink.lock().unwrap_or_else(|p| p.into_inner())
}
#[doc(hidden)]
pub fn probe_pairs(random: usize, seed: u64) -> Vec<(i32, i32)> {
lanes::probe_pairs(random, seed)
}
#[doc(hidden)]
pub fn first_mismatch(pairs: &[(i32, i32)], out: &[((u32, u32), (u32, u32))]) -> Option<(i32, i32)> {
lanes::first_mismatch(pairs, out)
}
struct Scopes {
internal: wgpu::ErrorScopeGuard,
validation: wgpu::ErrorScopeGuard,
memory: wgpu::ErrorScopeGuard,
}
impl Scopes {
fn push(device: &wgpu::Device) -> Scopes {
let memory = device.push_error_scope(wgpu::ErrorFilter::OutOfMemory);
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
let internal = device.push_error_scope(wgpu::ErrorFilter::Internal);
Scopes { internal, validation, memory }
}
async fn close(self) -> Result<(), WebGpuError> {
let Scopes { internal, validation, memory } = self;
let internal = internal.pop();
let validation = validation.pop();
let memory = memory.pop();
let (internal, validation, memory) = (internal.await, validation.await, memory.await);
if let Some(e) = memory {
return Err(WebGpuError::Memory(e.to_string()));
}
match validation.or(internal) {
Some(e) => Err(WebGpuError::Device(e.to_string())),
None => Ok(()),
}
}
}
async fn kernel_pipelines(device: &wgpu::Device, source: Cow<'_, str>, tiles: bool) -> Result<Pipelines, WebGpuError> {
let buffer = |binding: u32, read_only: bool| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only }, has_dynamic_offset: false, min_binding_size: None },
count: None,
};
let storage = |binding: u32| buffer(binding, false);
let uniform = |binding: u32, dynamic: bool, size: u64| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: dynamic, min_binding_size: NonZeroU64::new(size) },
count: None,
};
let scopes = Scopes::push(device);
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("wai-quantum lanes32"), source: wgpu::ShaderSource::Wgsl(source) });
let apply_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("wai-quantum apply"),
entries: &[storage(0), storage(1), storage(2), uniform(3, true, wgsl::GATE_RECORD_BYTES as u64)],
});
let probe_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("wai-quantum probe"), entries: &[storage(4), uniform(5, false, 16)] });
let pipeline = |layout: &wgpu::BindGroupLayout, entry: &str| {
let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("wai-quantum"), bind_group_layouts: &[Some(layout)], immediate_size: 0 });
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(entry),
layout: Some(&pl),
module: &module,
entry_point: Some(entry),
compilation_options: Default::default(),
cache: None,
})
};
let apply = pipeline(&apply_layout, "apply");
let probe = pipeline(&probe_layout, "probe");
let tile = tiles.then(|| {
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("wai-quantum apply_tile"),
entries: &[storage(0), storage(1), storage(2), buffer(6, true), uniform(7, true, wgsl::TILE_RUN_RECORD_BYTES as u64)],
});
let tile = pipeline(&layout, "apply_tile");
(layout, tile)
});
scopes.close().await?;
Ok(Pipelines { apply_layout, probe_layout, apply, probe, tile })
}
struct Mapped {
slot: Arc<Mutex<MapSlot>>,
#[cfg(not(target_arch = "wasm32"))]
device: wgpu::Device,
#[cfg(not(target_arch = "wasm32"))]
sink: Arc<Mutex<Sink>>,
#[cfg(not(target_arch = "wasm32"))]
index: wgpu::SubmissionIndex,
}
#[derive(Default)]
struct MapSlot {
result: Option<Result<(), wgpu::BufferAsyncError>>,
waker: Option<Waker>,
}
impl Future for Mapped {
type Output = Result<(), WebGpuError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
#[cfg(not(target_arch = "wasm32"))]
if let Err(e) = wait(&self.device, &self.sink, self.index.clone()) {
return Poll::Ready(Err(e));
}
let mut s = self.slot.lock().unwrap_or_else(|p| p.into_inner());
match s.result.take() {
Some(r) => Poll::Ready(r.map_err(|e| WebGpuError::Device(e.to_string()))),
None => {
s.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn wait(device: &wgpu::Device, sink: &Mutex<Sink>, index: wgpu::SubmissionIndex) -> Result<(), WebGpuError> {
guarded(sink, || device.poll(wgpu::PollType::Wait { submission_index: Some(index), timeout: Some(POLL_TIMEOUT) }))
}
#[cfg(not(target_arch = "wasm32"))]
fn guarded(sink: &Mutex<Sink>, poll: impl FnOnce() -> Result<wgpu::PollStatus, wgpu::PollError>) -> Result<(), WebGpuError> {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(poll)) {
Ok(Ok(_)) => Ok(()),
Ok(Err(wgpu::PollError::Timeout)) => Err(WebGpuError::Timeout),
Ok(Err(e)) => Err(WebGpuError::Device(e.to_string())),
Err(payload) => {
let message = payload.downcast_ref::<String>().cloned().or_else(|| payload.downcast_ref::<&str>().map(|m| m.to_string())).unwrap_or_else(|| "no message".into());
let why = format!("the device poll failed: {message}");
let mut s = lock(sink);
if s.lost.is_none() {
s.lost = Some(why.clone());
}
Err(WebGpuError::Device(why))
}
}
}
enum Init<'a> {
Basis(usize),
Lanes(&'a [Lane]),
}
const PROBE_RECORD: u64 = 32;
fn bytes_of(words: impl IntoIterator<Item = u32>, len: usize) -> Result<Vec<u8>, WebGpuError> {
let mut v = Vec::new();
v.try_reserve_exact(len.saturating_mul(4)).map_err(|e| WebGpuError::Memory(e.to_string()))?;
words.into_iter().for_each(|w| v.extend_from_slice(&w.to_le_bytes()));
Ok(v)
}
impl WebGpuMember {
pub fn id(&self) -> &str {
&self.id
}
pub fn key(&self) -> &str {
&self.key
}
pub fn class(&self) -> FabricClass {
self.class
}
pub fn backend(&self) -> &'static str {
self.backend
}
pub fn detail(&self) -> &str {
&self.detail
}
pub fn max_qubits(&self) -> u8 {
self.max_qubits
}
pub fn implementation(&self) -> &str {
&self.implementation
}
pub fn identity(&self) -> &AdapterIdentity {
&self.identity
}
pub async fn probe(&self, pairs: &[(i32, i32)]) -> Result<Vec<((u32, u32), (u32, u32))>, WebGpuError> {
self.probe_with_sentinel(pairs, 0xA5A5_A5A5).await
}
#[doc(hidden)]
pub async fn probe_with_sentinel(&self, pairs: &[(i32, i32)], sentinel: u32) -> Result<Vec<((u32, u32), (u32, u32))>, WebGpuError> {
self.alive()?;
let mut out = Vec::new();
out.try_reserve_exact(pairs.len()).map_err(|e| WebGpuError::Memory(e.to_string()))?;
let per_batch = usize::try_from(self.identity.max_storage_binding / PROBE_RECORD).unwrap_or(usize::MAX).clamp(1, 1 << 24);
for batch in pairs.chunks(per_batch) {
self.probe_batch(batch, sentinel, &mut out).await?;
}
Ok(out)
}
async fn probe_batch(&self, pairs: &[(i32, i32)], sentinel: u32, out: &mut Vec<((u32, u32), (u32, u32))>) -> Result<(), WebGpuError> {
let count = u32::try_from(pairs.len()).map_err(|_| WebGpuError::Internal(2))?;
let bytes = u64::from(count) * PROBE_RECORD;
let records = bytes_of(pairs.iter().flat_map(|&(a, b)| [a as u32, b as u32, sentinel, sentinel, sentinel, sentinel, sentinel, sentinel]), pairs.len() * 8)?;
let grid = wgsl::grid(count, wgsl::X_GROUPS_MAX);
let grid_bytes = bytes_of([count, grid.row_stride, 0, 0], 4)?;
let scopes = Scopes::push(&self.device);
let cases = self.buffer("wai-quantum probe cases", bytes, wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST);
let probe_grid = self.buffer("wai-quantum probe grid", 16, wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST);
let staging = self.buffer("wai-quantum probe staging", bytes.min(self.readback_chunk), wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST);
self.queue.write_buffer(&cases, 0, &records);
self.queue.write_buffer(&probe_grid, 0, &grid_bytes);
let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("wai-quantum probe"),
layout: &self.pipelines.probe_layout,
entries: &[wgpu::BindGroupEntry { binding: 4, resource: cases.as_entire_binding() }, wgpu::BindGroupEntry { binding: 5, resource: probe_grid.as_entire_binding() }],
});
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("wai-quantum probe") });
{
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("wai-quantum probe"), timestamp_writes: None });
pass.set_pipeline(&self.pipelines.probe);
pass.set_bind_group(0, &bind, &[]);
pass.dispatch_workgroups(grid.x, grid.y, 1);
}
let _index = self.queue.submit([encoder.finish()]);
drop(records);
scopes.close().await?;
#[cfg(not(target_arch = "wasm32"))]
wait(&self.device, &self.sink, _index)?;
let start = out.len();
let mut words: Vec<u32> = Vec::new();
words.try_reserve_exact(pairs.len() * 8).map_err(|e| WebGpuError::Memory(e.to_string()))?;
self.read(&cases, bytes, &staging, |_, chunk| words.extend(chunk.chunks_exact(4).map(|w| u32::from_le_bytes([w[0], w[1], w[2], w[3]])))).await?;
out.extend(words.chunks_exact(8).map(|r| ((r[2], r[3]), (r[4], r[5]))));
if out.len() - start != pairs.len() {
return Err(WebGpuError::Device("the probe's readback is short".into()));
}
self.settled()
}
pub fn config(&self) -> Config {
self.config
}
pub async fn run_lanes(&self, state: &[Lane], gates: &[GateWords]) -> Result<LaneRun, WebGpuError> {
let len = state.len();
if len < 2 || !len.is_power_of_two() {
return Err(WebGpuError::Internal(2));
}
let n = len.ilog2();
if n > u32::from(self.max_qubits) {
return Err(WebGpuError::TooWide { n_qubits: n.min(255) as u8, max: self.max_qubits });
}
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);
Ok(self.run(n as u8, Init::Lanes(state), &p.gates, &p.steps, &mut Unmetered).await?.0)
}
pub async fn run_program(&self, p: &Program) -> Result<LaneRun, WebGpuError> {
Ok(self.run_program_metered(p, &mut Unmetered).await?.0)
}
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 }, (0, None)));
}
if n > self.max_qubits {
return Err(WebGpuError::TooWide { n_qubits: n, max: self.max_qubits });
}
if n > MAX_QUBITS || p.start >= 1usize << n {
return Err(WebGpuError::Internal(2));
}
self.run(n, Init::Basis(p.start), &p.gates, &p.steps, meter).await
}
pub async fn simulate_from(&self, c: &Circuit, start: usize) -> Result<StateVector, WebGpuError> {
Ok(self.simulate_with(c, start, &mut Unmetered).await?.0)
}
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, (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 }, joules_micro, class))
}
pub async fn simulate(&self, c: &Circuit) -> Result<StateVector, WebGpuError> {
self.simulate_from(c, 0).await
}
pub async fn certify(&self, gs: Gateset, corpus: Corpus) -> Result<Certificate, WebGpuError> {
Ok(self.certify_metered(gs, corpus, &mut Unmetered).await?.0)
}
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, (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)), calibration.finish()))
}
pub async fn member(&self, corpus: Corpus) -> Result<Member, WebGpuError> {
self.member_with(corpus, &mut Unmetered).await
}
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, 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() })
}
#[doc(hidden)]
pub fn with_max_qubits(&self, max_qubits: u8) -> WebGpuMember {
WebGpuMember { max_qubits: max_qubits.min(self.max_qubits), ..self.clone() }
}
#[doc(hidden)]
pub fn with_max_x(&self, max_x: u32) -> WebGpuMember {
WebGpuMember { max_x: max_x.max(1), ..self.clone() }
}
#[doc(hidden)]
pub fn with_ring_records(&self, records: u32) -> WebGpuMember {
WebGpuMember { ring_records: records.clamp(1, RING_RECORDS), ..self.clone() }
}
#[doc(hidden)]
pub fn with_readback_chunk(&self, bytes: u64) -> WebGpuMember {
WebGpuMember { readback_chunk: (bytes & !3).clamp(4, READBACK_CHUNK), ..self.clone() }
}
#[doc(hidden)]
pub async fn with_shader(&self, source: &str) -> Result<WebGpuMember, WebGpuError> {
self.alive()?;
let pipelines = kernel_pipelines(&self.device, Cow::Owned(source.to_string()), self.pipelines.tile.is_some()).await?;
Ok(WebGpuMember { pipelines, implementation: "mutant".into(), ..self.clone() })
}
pub async fn tiled(&self) -> Result<WebGpuMember, WebGpuError> {
self.alive()?;
let pipelines = kernel_pipelines(&self.device, Cow::Borrowed(wgsl::TILE_SHADER), true).await?;
let member = WebGpuMember { pipelines, implementation: wgsl::tile_implementation_id(), config: Config::TILED, ..self.clone() };
let pairs = probe_pairs(4096, 0x5eed);
let out = member.probe(&pairs).await?;
match first_mismatch(&pairs, &out) {
None => Ok(member),
Some((a, b)) => Err(WebGpuError::Probe { a, b }),
}
}
fn alive(&self) -> Result<(), WebGpuError> {
match &lock(&self.sink).lost {
Some(why) => Err(WebGpuError::Device(why.clone())),
None => Ok(()),
}
}
fn settled(&self) -> Result<(), WebGpuError> {
let mut s = lock(&self.sink);
if let Some(why) = &s.lost {
return Err(WebGpuError::Device(why.clone()));
}
if s.messages.is_empty() {
return Ok(());
}
Err(WebGpuError::Device(std::mem::take(&mut s.messages).join("; ")))
}
fn tile_grid(&self, tiles: u32) -> wgsl::TileGrid {
wgsl::tile_grid(tiles, self.max_x.min(wgsl::TILE_X_GROUPS_MAX))
}
fn buffer(&self, label: &str, size: u64, usage: wgpu::BufferUsages) -> wgpu::Buffer {
self.device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), size, usage, mapped_at_creation: false })
}
async fn read(&self, src: &wgpu::Buffer, bytes: u64, staging: &wgpu::Buffer, mut take: impl FnMut(u64, &[u8])) -> Result<(), WebGpuError> {
let piece = staging.size();
let mut offset = 0;
while offset < bytes {
let len = (bytes - offset).min(piece);
let scopes = Scopes::push(&self.device);
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("wai-quantum readback") });
encoder.copy_buffer_to_buffer(src, offset, staging, 0, len);
let _index = self.queue.submit([encoder.finish()]);
let slot = Arc::new(Mutex::new(MapSlot::default()));
let filled = Arc::clone(&slot);
staging.map_async(wgpu::MapMode::Read, 0..len, move |r| {
let waker = {
let mut s = filled.lock().unwrap_or_else(|p| p.into_inner());
s.result = Some(r);
s.waker.take()
};
if let Some(w) = waker {
w.wake();
}
});
scopes.close().await?;
Mapped {
slot,
#[cfg(not(target_arch = "wasm32"))]
device: self.device.clone(),
#[cfg(not(target_arch = "wasm32"))]
sink: Arc::clone(&self.sink),
#[cfg(not(target_arch = "wasm32"))]
index: _index,
}
.await?;
{
let view = staging.get_mapped_range(0..len).map_err(|e| WebGpuError::Device(e.to_string()))?;
take(offset, &view);
}
staging.unmap();
offset += len;
}
Ok(())
}
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;
let grid = wgsl::grid(1u32 << (n - 1), self.max_x);
let ring_size = u64::from(self.ring_records) * self.stride;
let pairs = 1u64 << (n - 1);
if !gates.iter().all(PlannedGate::is_well_formed) {
return Err(WebGpuError::Internal(4));
}
let per_dispatch = (SUBMIT_BUDGET / pairs).clamp(1, u64::from(u32::MAX)) as u32;
let mut laid: Vec<Step> = Vec::new();
laid.try_reserve(steps.len()).map_err(|e| WebGpuError::Memory(e.to_string()))?;
let mut longest = 0usize;
for step in steps {
match *step {
Step::Gate { gate } => {
if gate as usize >= gates.len() {
return Err(WebGpuError::Internal(4));
}
laid.push(*step);
}
Step::Tile { first, count, size } => {
let end = (first as usize).checked_add(count as usize);
let tiles = if size == 0 || size as usize > len { 0 } else { (len / size as usize) as u32 };
if self.pipelines.tile.is_none() || end.is_none_or(|e| e > gates.len()) || tiles == 0 || self.tile_grid(tiles).y > 65535 {
return Err(WebGpuError::Internal(4));
}
if count == 0 {
laid.push(*step);
}
let mut at = first;
while at < first + count {
let part = per_dispatch.min(first + count - at);
laid.push(Step::Tile { first: at, count: part, size });
longest = longest.max(part as usize);
at += part;
}
}
}
}
let steps = laid.as_slice();
let tile_steps = steps.iter().any(|s| matches!(s, Step::Tile { .. }));
let tile_capacity = (self.ring_records as usize).max(longest);
let scopes = Scopes::push(&self.device);
let state_usage = wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST;
let re = self.buffer("wai-quantum re", plane, state_usage);
let im = self.buffer("wai-quantum im", plane, state_usage);
let report = self.buffer("wai-quantum report", 8, state_usage);
let ring = self.buffer("wai-quantum gates", ring_size, wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST);
let staging = self.buffer("wai-quantum staging", plane.min(self.readback_chunk), wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST);
let report_staging = self.buffer("wai-quantum report staging", 8, wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST);
let upload = match init {
Init::Basis(start) => {
self.queue.write_buffer(&re, 4 * start as u64, &(ONE as u32).to_le_bytes());
Ok(())
}
Init::Lanes(state) => bytes_of(state.iter().map(|l| l[0] as u32), len).and_then(|r| {
self.queue.write_buffer(&re, 0, &r);
drop(r);
let i = bytes_of(state.iter().map(|l| l[1] as u32), len)?;
self.queue.write_buffer(&im, 0, &i);
Ok(())
}),
};
self.queue.write_buffer(&report, 0, &[0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff]);
let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("wai-quantum apply"),
layout: &self.pipelines.apply_layout,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: re.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: im.as_entire_binding() },
wgpu::BindGroupEntry { binding: 2, resource: report.as_entire_binding() },
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &ring, offset: 0, size: NonZeroU64::new(wgsl::GATE_RECORD_BYTES as u64) }),
},
],
});
let tiles = match (&self.pipelines.tile, tile_steps) {
(Some((layout, pipeline)), true) => {
let runs = self.buffer("wai-quantum tile runs", ring_size, wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST);
let tile_gates = self.buffer("wai-quantum tile gates", (tile_capacity * wgsl::TILE_GATE_RECORD_BYTES) as u64, wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST);
let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("wai-quantum apply_tile"),
layout,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: re.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: im.as_entire_binding() },
wgpu::BindGroupEntry { binding: 2, resource: report.as_entire_binding() },
wgpu::BindGroupEntry { binding: 6, resource: tile_gates.as_entire_binding() },
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &runs, offset: 0, size: NonZeroU64::new(wgsl::TILE_RUN_RECORD_BYTES as u64) }),
},
],
});
Some((pipeline, runs, tile_gates, bind))
}
_ => None,
};
scopes.close().await?;
upload?;
let mut window = Window::new(meter);
let ring_records = self.ring_records as usize;
let stride = self.stride as usize;
let mut next = 0;
while next < steps.len() {
let first = next;
let (mut cost, mut gate_steps, mut tile_runs, mut tile_gates) = (0u64, 0usize, 0usize, 0usize);
while next < steps.len() {
let (work, is_tile, held) = match steps[next] {
Step::Gate { .. } => (pairs, false, 0),
Step::Tile { count, .. } => (pairs.saturating_mul(u64::from(count)), true, count as usize),
};
let full = cost.saturating_add(work) > SUBMIT_BUDGET || if is_tile { tile_runs == ring_records || tile_gates + held > tile_capacity } else { gate_steps == ring_records };
if next > first && full {
break;
}
cost = cost.saturating_add(work);
if is_tile {
(tile_runs, tile_gates) = (tile_runs + 1, tile_gates + held);
} else {
gate_steps += 1;
}
next += 1;
}
let chunk = &steps[first..next];
let mut gate_records = vec![0u8; gate_steps * stride];
let mut run_records = vec![0u8; tile_runs * stride];
let mut tile_records = Vec::with_capacity(tile_gates * wgsl::TILE_GATE_RECORD_BYTES);
let (mut g, mut t) = (0usize, 0usize);
for step in chunk {
match *step {
Step::Gate { gate } => {
let pg = &gates[gate as usize];
gate_records[g * stride..g * stride + wgsl::GATE_RECORD_BYTES].copy_from_slice(&wgsl::gate_record(&pg.words, grid.items, grid.row_stride, pg.index));
g += 1;
}
Step::Tile { first, count, size } => {
let at = (tile_records.len() / wgsl::TILE_GATE_RECORD_BYTES) as u32;
for pg in &gates[first as usize..(first + count) as usize] {
tile_records.extend_from_slice(&wgsl::tile_gate_record(pg));
}
let tg = self.tile_grid((len / size as usize) as u32);
run_records[t * stride..t * stride + wgsl::TILE_RUN_RECORD_BYTES].copy_from_slice(&wgsl::tile_run_record(at, count, size, tg.x, tg.tiles));
t += 1;
}
}
}
let scopes = Scopes::push(&self.device);
if !gate_records.is_empty() {
self.queue.write_buffer(&ring, 0, &gate_records);
}
if let Some((_, runs, tile_gate_buffer, _)) = &tiles
&& !run_records.is_empty()
{
self.queue.write_buffer(runs, 0, &run_records);
self.queue.write_buffer(tile_gate_buffer, 0, &tile_records);
}
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("wai-quantum gates") });
{
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("wai-quantum gates"), timestamp_writes: None });
let (mut g, mut t, mut on_tiles) = (0usize, 0usize, None);
for step in chunk {
match (*step, &tiles) {
(Step::Gate { .. }, _) => {
if on_tiles != Some(false) {
pass.set_pipeline(&self.pipelines.apply);
on_tiles = Some(false);
}
pass.set_bind_group(0, &bind, &[(g * stride) as u32]);
pass.dispatch_workgroups(grid.x, grid.y, 1);
g += 1;
}
(Step::Tile { size, .. }, Some((pipeline, _, _, tile_bind))) => {
if on_tiles != Some(true) {
pass.set_pipeline(pipeline);
on_tiles = Some(true);
}
let tg = self.tile_grid((len / size as usize) as u32);
pass.set_bind_group(0, tile_bind, &[(t * stride) as u32]);
pass.dispatch_workgroups(tg.x, tg.y, 1);
t += 1;
}
(Step::Tile { .. }, None) => unreachable!("tile steps were refused above without the tile kernel"),
}
}
}
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)?;
}
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() {
words[k] = u32::from_le_bytes([w[0], w[1], w[2], w[3]]);
}
})
.await?;
self.settled()?;
if words[0] & 6 != 0 {
return Err(WebGpuError::Internal(words[0]));
}
let mut amps: Vec<Lane> = Vec::new();
amps.try_reserve_exact(len).map_err(|e| WebGpuError::Memory(e.to_string()))?;
amps.resize(len, [0, 0]);
for (part, plane_buffer) in [(0, &re), (1, &im)] {
self.read(plane_buffer, plane, &staging, |offset, b| {
let first = (offset / 4) as usize;
for (k, w) in b.chunks_exact(4).enumerate() {
amps[first + k][part] = u32::from_le_bytes([w[0], w[1], w[2], w[3]]) as i32;
}
})
.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 }, reading))
}
}
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()),
}
}
}
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?)
})
}
}
#[cfg(not(target_arch = "wasm32"))]
pub mod blocking {
use super::{Discovery, WebGpuError, WebGpuMember};
use crate::quantum::{Circuit, Gateset, StateVector};
use crate::quantum_energy::EnergyClass;
use crate::quantum_fabric::{Certificate, Corpus, Member, RunMeter};
use core::future::Future;
use core::task::{Context, Poll, Waker};
pub fn resolve<F: Future>(f: F) -> Result<F::Output, WebGpuError> {
let mut f = core::pin::pin!(f);
match f.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
Poll::Ready(v) => Ok(v),
Poll::Pending => Err(WebGpuError::Pending),
}
}
pub fn discover() -> Result<Discovery, WebGpuError> {
resolve(super::discover())
}
pub fn simulate_from(m: &WebGpuMember, c: &Circuit, start: usize) -> Result<StateVector, WebGpuError> {
resolve(m.simulate_from(c, start))?
}
pub fn certify(m: &WebGpuMember, gs: Gateset, corpus: Corpus) -> Result<Certificate, WebGpuError> {
resolve(m.certify(gs, corpus))?
}
pub fn member(m: &WebGpuMember, corpus: Corpus) -> Result<Member, WebGpuError> {
resolve(m.member(corpus))?
}
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))?
}
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")))]
mod tests {
use super::*;
#[test]
fn a_failed_native_wait_is_a_typed_loss() {
let sink = Mutex::new(Sink::default());
assert_eq!(guarded(&sink, || Ok(wgpu::PollStatus::WaitSucceeded)), Ok(()));
assert_eq!(guarded(&sink, || Err(wgpu::PollError::Timeout)), Err(WebGpuError::Timeout));
assert!(matches!(guarded(&sink, || Err(wgpu::PollError::WrongSubmissionIndex(2, 1))), Err(WebGpuError::Device(_))));
assert!(lock(&sink).lost.is_none());
let r = guarded(&sink, || panic!("Error in {}: {}", "Device::poll", "the device was lost"));
let Err(WebGpuError::Device(why)) = &r else { panic!("{r:?}") };
assert_eq!(why, "the device poll failed: Error in Device::poll: the device was lost");
assert_eq!(lock(&sink).lost.as_deref(), Some(why.as_str()));
assert_eq!(guarded(&sink, || panic!("again")), Err(WebGpuError::Device("the device poll failed: again".into())));
assert_eq!(lock(&sink).lost.as_deref(), Some(why.as_str()));
}
}