use crate::quantum::{Amp, Circuit, Gateset, MAX_QUBITS, ONE, QuantumError, StateVector};
use crate::quantum_energy::EnergyClass;
use crate::quantum_fabric::hip::{self, DeviceIdentity};
use crate::quantum_fabric::lanes::{self, GateWords, Lane};
use crate::quantum_fabric::plan::{self, Config, LaneRun, Program, Step};
use crate::quantum_fabric::{
Certificate, Certification, Corpus, EnergyCalibration, FabricClass, KernelEntry, Member, MemberError, Presence, RunFuture, RunMeter, Runner, Unmetered, Window,
apply_kernel, pair_updates,
};
use std::ffi::{CStr, CString, c_char, c_int, c_void};
use std::sync::{Arc, OnceLock};
pub type ProbeOutput = ((u32, u32), (u32, u32));
type MeteredRun = (LaneRun, (u64, Option<EnergyClass>));
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum HipError {
Unavailable(String),
Compile(String),
Device(String),
Memory(String),
Circuit(QuantumError),
TooWide { n_qubits: u8, max: u8 },
OutOfRange { gate: u32 },
Internal(&'static str),
Probe { a: i32, b: i32 },
}
impl std::fmt::Display for HipError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HipError::Unavailable(why) => write!(f, "HIP is unavailable: {why}"),
HipError::Compile(e) => write!(f, "the kernel did not compile: {e}"),
HipError::Device(e) => write!(f, "device: {e}"),
HipError::Memory(e) => write!(f, "out of memory: {e}"),
HipError::Circuit(e) => write!(f, "{e}"),
HipError::TooWide { n_qubits, max } => write!(f, "{n_qubits} qubits; this member holds at most {max}"),
HipError::OutOfRange { gate } => write!(f, "the result of op {gate} leaves 32 bits"),
HipError::Internal(why) => write!(f, "host defect: {why}"),
HipError::Probe { a, b } => write!(f, "the device's arithmetic differs from the CPU's at ({a}, {b})"),
}
}
}
impl std::error::Error for HipError {}
impl From<HipError> for MemberError {
fn from(e: HipError) -> MemberError {
match e {
HipError::TooWide { n_qubits, max } => MemberError::TooWide { n_qubits, max },
HipError::OutOfRange { gate } => MemberError::OutOfRange { gate },
HipError::Memory(why) => MemberError::Memory(why),
HipError::Device(why) => MemberError::Device(why),
HipError::Circuit(e) => MemberError::Circuit(e),
other => MemberError::Other(other.to_string()),
}
}
}
const HIP_SUCCESS: c_int = 0;
const HIP_ERROR_OUT_OF_MEMORY: c_int = 2;
const HIP_ERROR_NO_DEVICE: c_int = 100;
const HIP_MEMCPY_HOST_TO_DEVICE: c_int = 1;
const HIP_MEMCPY_DEVICE_TO_HOST: c_int = 2;
const PROPS_SIZE: usize = 1472;
const PROPS_NAME: usize = 0;
const PROPS_UUID: usize = 256;
const PROPS_TOTAL_MEMORY: usize = 288;
const PROPS_COMPUTE_UNITS: usize = 388;
const PROPS_PCI_BUS: usize = 584;
const PROPS_PCI_DEVICE: usize = 588;
const PROPS_PCI_DOMAIN: usize = 592;
const PROPS_ARCH: usize = 1160;
const PROPS_BUFFER: usize = 4096;
#[cfg(target_os = "linux")]
mod dl {
use std::ffi::{c_char, c_int, c_void};
#[link(name = "dl")]
unsafe extern "C" {
pub fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void;
pub fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
pub fn dlerror() -> *const c_char;
}
pub const RTLD_NOW: c_int = 2;
}
struct Api {
init: unsafe extern "C" fn(u32) -> c_int,
device_count: unsafe extern "C" fn(*mut c_int) -> c_int,
properties: unsafe extern "C" fn(*mut u8, c_int) -> c_int,
set_device: unsafe extern "C" fn(c_int) -> c_int,
mem_info: unsafe extern "C" fn(*mut usize, *mut usize) -> c_int,
malloc: unsafe extern "C" fn(*mut *mut c_void, usize) -> c_int,
free: unsafe extern "C" fn(*mut c_void) -> c_int,
memcpy: unsafe extern "C" fn(*mut c_void, *const c_void, usize, c_int) -> c_int,
module_load_data: unsafe extern "C" fn(*mut *mut c_void, *const c_void) -> c_int,
module_unload: unsafe extern "C" fn(*mut c_void) -> c_int,
module_get_function: unsafe extern "C" fn(*mut *mut c_void, *mut c_void, *const c_char) -> c_int,
#[allow(clippy::type_complexity)]
launch: unsafe extern "C" fn(*mut c_void, u32, u32, u32, u32, u32, u32, u32, *mut c_void, *mut *mut c_void, *mut *mut c_void) -> c_int,
synchronize: unsafe extern "C" fn() -> c_int,
error_string: unsafe extern "C" fn(c_int) -> *const c_char,
runtime_version: unsafe extern "C" fn(*mut c_int) -> c_int,
rtc_create: unsafe extern "C" fn(*mut *mut c_void, *const c_char, *const c_char, c_int, *const *const c_char, *const *const c_char) -> c_int,
rtc_compile: unsafe extern "C" fn(*mut c_void, c_int, *const *const c_char) -> c_int,
rtc_log_size: unsafe extern "C" fn(*mut c_void, *mut usize) -> c_int,
rtc_log: unsafe extern "C" fn(*mut c_void, *mut c_char) -> c_int,
rtc_code_size: unsafe extern "C" fn(*mut c_void, *mut usize) -> c_int,
rtc_code: unsafe extern "C" fn(*mut c_void, *mut c_char) -> c_int,
rtc_destroy: unsafe extern "C" fn(*mut *mut c_void) -> c_int,
rtc_version: unsafe extern "C" fn(*mut c_int, *mut c_int) -> c_int,
}
static API: OnceLock<Result<Api, String>> = OnceLock::new();
#[cfg(target_os = "linux")]
fn load_first(names: &[&str]) -> Result<*mut c_void, String> {
let mut why = Vec::new();
for name in names {
let c = CString::new(*name).expect("no NUL");
let h = unsafe { dl::dlopen(c.as_ptr(), dl::RTLD_NOW) };
if !h.is_null() {
return Ok(h);
}
let e = unsafe { dl::dlerror() };
why.push(if e.is_null() { format!("{name}: cannot load") } else { unsafe { CStr::from_ptr(e) }.to_string_lossy().into_owned() });
}
Err(why.join("; "))
}
#[cfg(target_os = "linux")]
unsafe fn as_fn<F: Copy>(p: *mut c_void) -> F {
assert_eq!(std::mem::size_of::<F>(), std::mem::size_of::<*mut c_void>(), "a function pointer");
unsafe { std::mem::transmute_copy(&p) }
}
#[cfg(target_os = "linux")]
fn open_api() -> Result<Api, String> {
let rt = load_first(&["libamdhip64.so.7", "libamdhip64.so.6", "libamdhip64.so"]).map_err(|e| format!("no HIP runtime library on this host ({e})"))?;
let rtc = load_first(&["libhiprtc.so.7", "libhiprtc.so.6", "libhiprtc.so"]).map_err(|e| format!("no HIP runtime compiler library on this host ({e})"))?;
let sym = |h: *mut c_void, name: &str| -> Result<*mut c_void, String> {
let n = CString::new(name).expect("no NUL");
let p = unsafe { dl::dlsym(h, n.as_ptr()) };
if p.is_null() { Err(format!("the HIP libraries have no {name}")) } else { Ok(p) }
};
unsafe {
Ok(Api {
init: as_fn(sym(rt, "hipInit")?),
device_count: as_fn(sym(rt, "hipGetDeviceCount")?),
properties: as_fn(sym(rt, "hipGetDevicePropertiesR0600")?),
set_device: as_fn(sym(rt, "hipSetDevice")?),
mem_info: as_fn(sym(rt, "hipMemGetInfo")?),
malloc: as_fn(sym(rt, "hipMalloc")?),
free: as_fn(sym(rt, "hipFree")?),
memcpy: as_fn(sym(rt, "hipMemcpy")?),
module_load_data: as_fn(sym(rt, "hipModuleLoadData")?),
module_unload: as_fn(sym(rt, "hipModuleUnload")?),
module_get_function: as_fn(sym(rt, "hipModuleGetFunction")?),
launch: as_fn(sym(rt, "hipModuleLaunchKernel")?),
synchronize: as_fn(sym(rt, "hipDeviceSynchronize")?),
error_string: as_fn(sym(rt, "hipGetErrorString")?),
runtime_version: as_fn(sym(rt, "hipRuntimeGetVersion")?),
rtc_create: as_fn(sym(rtc, "hiprtcCreateProgram")?),
rtc_compile: as_fn(sym(rtc, "hiprtcCompileProgram")?),
rtc_log_size: as_fn(sym(rtc, "hiprtcGetProgramLogSize")?),
rtc_log: as_fn(sym(rtc, "hiprtcGetProgramLog")?),
rtc_code_size: as_fn(sym(rtc, "hiprtcGetCodeSize")?),
rtc_code: as_fn(sym(rtc, "hiprtcGetCode")?),
rtc_destroy: as_fn(sym(rtc, "hiprtcDestroyProgram")?),
rtc_version: as_fn(sym(rtc, "hiprtcVersion")?),
})
}
}
#[cfg(not(target_os = "linux"))]
fn open_api() -> Result<Api, String> {
Err(format!("the HIP runtime is loaded on Linux; this is {}", std::env::consts::OS))
}
fn api() -> Result<&'static Api, HipError> {
API.get_or_init(open_api).as_ref().map_err(|e| HipError::Unavailable(e.clone()))
}
pub fn available() -> Result<(), String> {
api().map(|_| ()).map_err(|e| match e {
HipError::Unavailable(why) => why,
other => other.to_string(),
})
}
fn status(a: &Api, code: c_int, what: &str) -> Result<(), HipError> {
if code == HIP_SUCCESS {
return Ok(());
}
let text = unsafe {
let p = (a.error_string)(code);
if p.is_null() { String::new() } else { CStr::from_ptr(p).to_string_lossy().into_owned() }
};
let why = format!("{what}: {text} ({code})");
Err(if code == HIP_ERROR_OUT_OF_MEMORY { HipError::Memory(why) } else { HipError::Device(why) })
}
struct DeviceBuffer {
ptr: *mut c_void,
bytes: usize,
}
impl DeviceBuffer {
fn alloc(a: &'static Api, bytes: usize) -> Result<DeviceBuffer, HipError> {
let mut ptr = std::ptr::null_mut();
status(a, unsafe { (a.malloc)(&mut ptr, bytes.max(1)) }, "allocating device memory")?;
Ok(DeviceBuffer { ptr, bytes })
}
fn upload<T: Copy>(a: &'static Api, data: &[T]) -> Result<DeviceBuffer, HipError> {
let bytes = std::mem::size_of_val(data);
let b = DeviceBuffer::alloc(a, bytes)?;
status(a, unsafe { (a.memcpy)(b.ptr, data.as_ptr().cast(), bytes, HIP_MEMCPY_HOST_TO_DEVICE) }, "uploading")?;
Ok(b)
}
fn download<T: Copy + Default>(&self, a: &'static Api) -> Result<Vec<T>, HipError> {
let n = self.bytes / std::mem::size_of::<T>();
let mut out: Vec<T> = Vec::new();
out.try_reserve_exact(n).map_err(|e| HipError::Memory(e.to_string()))?;
out.resize(n, T::default());
status(a, unsafe { (a.memcpy)(out.as_mut_ptr().cast(), self.ptr, n * std::mem::size_of::<T>(), HIP_MEMCPY_DEVICE_TO_HOST) }, "reading back")?;
Ok(out)
}
}
impl Drop for DeviceBuffer {
fn drop(&mut self) {
if let Ok(a) = api() {
unsafe { (a.free)(self.ptr) };
}
}
}
struct Module {
ordinal: c_int,
handle: *mut c_void,
functions: Vec<*mut c_void>,
}
unsafe impl Send for Module {}
unsafe impl Sync for Module {}
impl Drop for Module {
fn drop(&mut self) {
if let Ok(a) = api() {
unsafe {
(a.set_device)(self.ordinal);
(a.module_unload)(self.handle);
}
}
}
}
fn compile(a: &'static Api, ordinal: c_int, identity: &DeviceIdentity, source: &str, options: &[&str], names: &[&str]) -> Result<Module, HipError> {
let src = CString::new(source).map_err(|_| HipError::Internal("a kernel text holding a NUL"))?;
let file = CString::new("wai-quantum.hip").expect("no NUL");
let mut opts: Vec<CString> = vec![CString::new(identity.arch_option()).map_err(|_| HipError::Internal("an architecture holding a NUL"))?];
for o in options {
opts.push(CString::new(*o).expect("no NUL"));
}
let ptrs: Vec<*const c_char> = opts.iter().map(|o| o.as_ptr()).collect();
let mut prog = std::ptr::null_mut();
unsafe {
let r = (a.rtc_create)(&mut prog, src.as_ptr(), file.as_ptr(), 0, std::ptr::null(), std::ptr::null());
if r != 0 {
return Err(HipError::Compile(format!("the runtime compiler did not create the program ({r})")));
}
let r = (a.rtc_compile)(prog, ptrs.len() as c_int, ptrs.as_ptr());
let mut log_size = 0usize;
(a.rtc_log_size)(prog, &mut log_size);
let mut log = vec![0u8; log_size.max(1)];
(a.rtc_log)(prog, log.as_mut_ptr().cast());
let log = String::from_utf8_lossy(&log).trim_end_matches('\0').trim().to_string();
if r != 0 {
(a.rtc_destroy)(&mut prog);
return Err(HipError::Compile(format!("status {r}: {log}")));
}
let mut size = 0usize;
let r = (a.rtc_code_size)(prog, &mut size);
let mut code = vec![0u8; size.max(1)];
let r2 = if r == 0 { (a.rtc_code)(prog, code.as_mut_ptr().cast()) } else { r };
(a.rtc_destroy)(&mut prog);
if r2 != 0 || size == 0 {
return Err(HipError::Compile(format!("no code object ({r2})")));
}
status(a, (a.set_device)(ordinal), "selecting the device")?;
let mut handle = std::ptr::null_mut();
status(a, (a.module_load_data)(&mut handle, code.as_ptr().cast()), "loading the code object")?;
let mut module = Module { ordinal, handle, functions: Vec::new() };
for name in names {
let n = CString::new(*name).expect("no NUL");
let mut f = std::ptr::null_mut();
status(a, (a.module_get_function)(&mut f, handle, n.as_ptr()), name)?;
module.functions.push(f);
}
Ok(module)
}
}
fn arg<T>(v: &T) -> *mut c_void {
(v as *const T).cast_mut().cast()
}
fn launch(a: &'static Api, function: *mut c_void, work: u64, args: &mut [*mut c_void]) -> Result<(), HipError> {
let blocks = blocks_for(work);
status(a, unsafe { (a.launch)(function, blocks, 1, 1, hip::BLOCK, 1, 1, 0, std::ptr::null_mut(), args.as_mut_ptr(), std::ptr::null_mut()) }, "launching")?;
status(a, unsafe { (a.synchronize)() }, "running")
}
fn blocks_for(work: u64) -> u32 {
work.div_ceil(u64::from(hip::BLOCK)).clamp(1, u64::from(hip::GRID_MAX)) as u32
}
#[derive(Debug)]
pub struct Refusal {
pub device: String,
pub key: String,
pub error: HipError,
}
#[derive(Debug)]
pub struct Discovery {
pub found: Vec<HipMember>,
pub refused: Vec<Refusal>,
pub unavailable: Option<String>,
}
pub fn discover() -> Discovery {
let mut d = Discovery { found: Vec::new(), refused: Vec::new(), unavailable: None };
let a = match api() {
Ok(a) => a,
Err(e) => {
d.unavailable = Some(e.to_string());
return d;
}
};
let mut count: c_int = 0;
let started = unsafe { (a.init)(0) };
let listed = if started == HIP_SUCCESS { unsafe { (a.device_count)(&mut count) } } else { started };
if listed == HIP_ERROR_NO_DEVICE {
d.unavailable = Some("the runtime lists no device".into());
return d;
}
if let Err(e) = status(a, listed, "starting the runtime") {
d.unavailable = Some(format!("the runtime did not start: {e}"));
return d;
}
for ordinal in 0..count {
match open(a, ordinal) {
Ok(m) => d.found.push(m),
Err(r) => d.refused.push(r),
}
}
d
}
fn qubits_for(free: u64) -> u8 {
let lanes = free / 2 / 8;
if lanes < 2 { 0 } else { (lanes.ilog2() as u8).min(MAX_QUBITS) }
}
fn text_at(props: &[u8], offset: usize, len: usize) -> String {
let field = &props[offset..offset + len];
let end = field.iter().position(|&b| b == 0).unwrap_or(len);
String::from_utf8_lossy(&field[..end]).into_owned()
}
fn u32_at(props: &[u8], offset: usize) -> u32 {
u32::from_le_bytes(props[offset..offset + 4].try_into().expect("four bytes"))
}
fn identity_from(props: &[u8], runtime: u32, compiler: (u32, u32)) -> Option<DeviceIdentity> {
let name = text_at(props, PROPS_NAME, 256);
let arch = text_at(props, PROPS_ARCH, 256);
let memory = u64::from_le_bytes(props[PROPS_TOTAL_MEMORY..PROPS_TOTAL_MEMORY + 8].try_into().expect("eight bytes"));
if name.is_empty() || !arch.starts_with("gfx") || !arch.bytes().all(|b| b.is_ascii_alphanumeric() || b":+-".contains(&b)) || memory == 0 {
return None;
}
Some(DeviceIdentity {
name,
uuid: props[PROPS_UUID..PROPS_UUID + 16].iter().map(|b| format!("{b:02x}")).collect(),
arch,
pci: format!("{:04x}:{:02x}:{:02x}", u32_at(props, PROPS_PCI_DOMAIN), u32_at(props, PROPS_PCI_BUS), u32_at(props, PROPS_PCI_DEVICE)),
runtime,
compiler,
compute_units: u32_at(props, PROPS_COMPUTE_UNITS),
memory,
})
}
fn open(a: &'static Api, ordinal: c_int) -> Result<HipMember, Refusal> {
let unread = |error: HipError| Refusal { device: format!("device {ordinal}"), key: String::new(), error };
let mut props = vec![0u8; PROPS_BUFFER];
let (mut runtime, mut major, mut minor): (c_int, c_int, c_int) = (0, 0, 0);
unsafe {
status(a, (a.set_device)(ordinal), "selecting the device").map_err(unread)?;
status(a, (a.properties)(props.as_mut_ptr(), ordinal), "reading the device's properties").map_err(unread)?;
(a.runtime_version)(&mut runtime);
(a.rtc_version)(&mut major, &mut minor);
}
let compiler = (u32::try_from(major).unwrap_or(0), u32::try_from(minor).unwrap_or(0));
let identity = identity_from(&props[..PROPS_SIZE], u32::try_from(runtime).unwrap_or(0), compiler)
.ok_or_else(|| unread(HipError::Device("the device's properties do not read as the layout this member reads (hipDeviceProp_tR0600)".into())))?;
let key = identity.key();
let refuse = |error: HipError| Refusal { device: identity.name.clone(), key: key.clone(), error };
let kernel = compile(a, ordinal, &identity, hip::SOURCE, &hip::COMPILE_OPTIONS, &["apply_gate", "probe"]).map_err(refuse)?;
let (mut free, mut total) = (0usize, 0usize);
status(a, unsafe { (a.mem_info)(&mut free, &mut total) }, "reading free memory").map_err(refuse)?;
let max_qubits = qubits_for(free as u64);
let detail = format!(
"hip; device {:?}; {}; {} compute units; uuid {}; pci {}; runtime {}; runtime compiler {}.{}; memory {} bytes, {free} free at discovery; up to {max_qubits} qubits; a metered run's window runs from the first upload to the last readback",
identity.name, identity.arch, identity.compute_units, identity.uuid, identity.pci, identity.runtime, identity.compiler.0, identity.compiler.1, identity.memory,
);
let member = HipMember {
api: a,
ordinal,
kernel: Arc::new(kernel),
id: identity.member_id(),
key: key.clone(),
implementation: hip::implementation_id(),
detail,
max_qubits,
identity: identity.clone(),
};
let pairs = lanes::probe_pairs(4096, 0x5eed);
match member.probe(&pairs) {
Ok(out) => match lanes::first_mismatch(&pairs, &out) {
None => Ok(member),
Some((a, b)) => Err(refuse(HipError::Probe { a, b })),
},
Err(e) => Err(refuse(e)),
}
}
#[derive(Clone)]
pub struct HipMember {
api: &'static Api,
ordinal: c_int,
kernel: Arc<Module>,
identity: DeviceIdentity,
id: String,
key: String,
implementation: String,
detail: String,
max_qubits: u8,
}
impl std::fmt::Debug for HipMember {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HipMember").field("id", &self.id).field("key", &self.key).field("implementation", &self.implementation).field("max_qubits", &self.max_qubits).finish()
}
}
impl HipMember {
pub fn id(&self) -> &str {
&self.id
}
pub fn key(&self) -> &str {
&self.key
}
pub fn class(&self) -> FabricClass {
self.identity.class()
}
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) -> &DeviceIdentity {
&self.identity
}
fn select(&self) -> Result<(), HipError> {
status(self.api, unsafe { (self.api.set_device)(self.ordinal) }, "selecting the device")
}
pub fn probe(&self, pairs: &[(i32, i32)]) -> Result<Vec<ProbeOutput>, HipError> {
if pairs.is_empty() {
return Ok(Vec::new());
}
self.select()?;
let flat: Vec<i32> = pairs.iter().flat_map(|&(a, b)| [a, b]).collect();
let cases = DeviceBuffer::upload(self.api, &flat)?;
let out = DeviceBuffer::upload(self.api, &vec![0u32; pairs.len() * 4])?;
let count = pairs.len() as u64;
let mut args = [arg(&cases.ptr), arg(&out.ptr), arg(&count)];
launch(self.api, self.kernel.functions[1], count, &mut args)?;
let words: Vec<u32> = out.download(self.api)?;
Ok(words.as_chunks::<4>().0.iter().map(|w| ((w[0], w[1]), (w[2], w[3]))).collect())
}
pub fn config(&self) -> Config {
Config::PER_GATE
}
pub fn run_program(&self, p: &Program) -> Result<LaneRun, HipError> {
Ok(self.run_program_metered(p, &mut Unmetered)?.0)
}
fn run_program_metered<M: RunMeter + ?Sized>(&self, p: &Program, meter: &mut M) -> Result<MeteredRun, HipError> {
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(HipError::TooWide { n_qubits: n, max: self.max_qubits });
}
if n > MAX_QUBITS || p.start >= 1usize << n {
return Err(HipError::Internal("the start is outside the state"));
}
let len = 1usize << n;
let mut gates = Vec::with_capacity(p.steps.len());
for step in &p.steps {
let Step::Gate { gate } = *step else { return Err(HipError::Internal("a tile step; this member runs gate steps only")) };
let g = p.gates.get(gate as usize).ok_or(HipError::Internal("a step names a gate the program does not hold"))?;
if !g.is_well_formed() {
return Err(HipError::Internal("malformed gate words for this state"));
}
gates.push((&g.words, g.index));
}
let mut init: Vec<i32> = Vec::new();
init.try_reserve_exact(2 * len).map_err(|e| HipError::Memory(e.to_string()))?;
init.resize(2 * len, 0);
init[2 * p.start] = ONE as i32;
self.execute(init, &gates, meter)
}
pub fn run_lanes(&self, state: &[Lane], gates: &[GateWords]) -> Result<LaneRun, HipError> {
let len = state.len();
if len < 2 || !len.is_power_of_two() {
return Err(HipError::Internal("a state that is not 2^n lanes, n at least 1"));
}
let n = len.trailing_zeros() as u8;
if n > self.max_qubits {
return Err(HipError::TooWide { n_qubits: n, max: self.max_qubits });
}
let planned: Vec<(&GateWords, u32)> = gates.iter().zip(0u32..).collect();
let mut init: Vec<i32> = Vec::new();
init.try_reserve_exact(2 * len).map_err(|e| HipError::Memory(e.to_string()))?;
init.extend(state.iter().flat_map(|l| [l[0], l[1]]));
Ok(self.execute(init, &planned, &mut Unmetered)?.0)
}
fn execute<M: RunMeter + ?Sized>(&self, init: Vec<i32>, gates: &[(&GateWords, u32)], meter: &mut M) -> Result<MeteredRun, HipError> {
let len = init.len() / 2;
for (w, _) in gates {
let (tbit, ctrl) = (w.tbit as usize, w.ctrl as usize);
if tbit == 0 || !tbit.is_power_of_two() || tbit > len / 2 || ctrl & tbit != 0 || ctrl & !(len - 1) != 0 {
return Err(HipError::Internal("malformed gate words for this state"));
}
}
let pairs = (len / 2) as u64;
self.select()?;
let mut window = Window::new(meter);
window.open();
let amps = DeviceBuffer::upload(self.api, &init)?;
drop(init);
let report = DeviceBuffer::upload(self.api, &[u32::MAX])?;
for &(w, index) in gates {
let m: [i32; 8] = w.m.map(|x| x as i32);
let mut args = [
arg(&s.ptr),
arg(&pairs),
arg(&w.tbit),
arg(&w.ctrl),
arg(&m[0]),
arg(&m[1]),
arg(&m[2]),
arg(&m[3]),
arg(&m[4]),
arg(&m[5]),
arg(&m[6]),
arg(&m[7]),
arg(&index),
arg(&report.ptr),
];
launch(self.api, self.kernel.functions[0], pairs, &mut args)?;
}
let bad: Vec<u32> = report.download(self.api)?;
let words: Vec<i32> = amps.download(self.api)?;
let reading = window.close();
let out_of_range = bad.first().copied().filter(|&k| k != u32::MAX);
let mut lanes_out: Vec<Lane> = Vec::new();
lanes_out.try_reserve_exact(len).map_err(|e| HipError::Memory(e.to_string()))?;
lanes_out.extend(words.as_chunks::<2>().0.iter().copied());
Ok((LaneRun { amps: lanes_out, out_of_range }, reading))
}
pub fn simulate_from(&self, c: &Circuit, start: usize) -> Result<StateVector, HipError> {
Ok(self.simulate_with(c, start, &mut Unmetered)?.0)
}
pub fn simulate_metered(&self, c: &Circuit, start: usize, meter: &mut dyn RunMeter) -> Result<(StateVector, u64, Option<EnergyClass>), HipError> {
self.simulate_with(c, start, meter)
}
fn simulate_with<M: RunMeter + ?Sized>(&self, c: &Circuit, start: usize, meter: &mut M) -> Result<(StateVector, u64, Option<EnergyClass>), HipError> {
let p = plan::compile_with(c, start, &self.config()).map_err(HipError::Circuit)?;
if p.n_qubits > self.max_qubits {
return Err(HipError::TooWide { n_qubits: p.n_qubits, max: self.max_qubits });
}
let (run, (joules_micro, class)) = self.run_program_metered(&p, meter)?;
if let Some(gate) = run.out_of_range {
return Err(HipError::OutOfRange { gate });
}
let mut amps = Vec::new();
amps.try_reserve_exact(run.amps.len()).map_err(|e| HipError::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 fn simulate(&self, c: &Circuit) -> Result<StateVector, HipError> {
self.simulate_from(c, 0)
}
pub fn certify(&self, gs: Gateset, corpus: Corpus) -> Result<Certificate, HipError> {
Ok(self.certify_metered(gs, corpus, &mut Unmetered)?.0)
}
fn certify_metered<M: RunMeter + ?Sized>(&self, gs: Gateset, corpus: Corpus, meter: &mut M) -> Result<(Certificate, Option<(u64, EnergyClass)>), HipError> {
let mut cert = Certification::new();
let mut calibration = EnergyCalibration::new();
for c in corpus.circuits(gs) {
let reference = lanes::reference_output(&c).map_err(HipError::Circuit)?;
let (run, (joules_micro, class)) = self.run_program_metered(&plan::compile_with(&c, 0, &self.config()).map_err(HipError::Circuit)?, &mut *meter)?;
if let Some(gate) = run.out_of_range {
return Err(HipError::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 fn member(&self, corpus: Corpus) -> Result<Member, HipError> {
self.member_with(corpus, &mut Unmetered)
}
pub fn member_metered(&self, corpus: Corpus, meter: &mut dyn RunMeter) -> Result<Member, HipError> {
self.member_with(corpus, meter)
}
fn member_with<M: RunMeter + ?Sized>(&self, corpus: Corpus, meter: &mut M) -> Result<Member, HipError> {
let mut kernels = Vec::new();
for gs in Gateset::ALL {
let (certificate, energy) = self.certify_metered(gs, corpus, &mut *meter)?;
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) -> HipMember {
HipMember { max_qubits: max_qubits.min(self.max_qubits), ..self.clone() }
}
#[doc(hidden)]
pub fn with_source(&self, source: &str) -> Result<HipMember, HipError> {
let kernel = compile(self.api, self.ordinal, &self.identity, source, &hip::COMPILE_OPTIONS, &["apply_gate", "probe"])?;
Ok(HipMember { kernel: Arc::new(kernel), implementation: hip::implementation_id_of(source), ..self.clone() })
}
}
#[cfg(feature = "quantum_sv")]
#[derive(Clone)]
pub struct HipSv {
member: HipMember,
kernels: Arc<Module>,
implementation: String,
}
#[cfg(feature = "quantum_sv")]
impl std::fmt::Debug for HipSv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HipSv").field("member", &self.member.id).field("implementation", &self.implementation).finish()
}
}
#[cfg(feature = "quantum_sv")]
impl HipMember {
pub fn sv(&self) -> Result<HipSv, HipError> {
self.sv_with_source(crate::quantum_fabric::sv64::SOURCE)
}
#[doc(hidden)]
pub fn sv_with_source(&self, source: &str) -> Result<HipSv, HipError> {
use crate::quantum_fabric::sv64;
let kernels = compile(self.api, self.ordinal, &self.identity, source, &sv64::HIP_COMPILE_OPTIONS, &["sv_diag", "sv_one", "sv_many"])?;
Ok(HipSv { member: self.clone(), kernels: Arc::new(kernels), implementation: sv64::hip_implementation_id_of(source) })
}
}
#[cfg(feature = "quantum_sv")]
impl HipSv {
pub fn implementation(&self) -> &str {
&self.implementation
}
pub fn run(&self, n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<(Vec<f64>, Vec<f64>), HipError> {
use crate::quantum_fabric::sv64::{Launch, plan};
let launches = plan(n, gates, width).map_err(|_| HipError::Internal("a gate the state vector refuses"))?;
if n == 0 || n > u32::from(self.member.max_qubits.saturating_sub(1)) {
return Err(HipError::TooWide { n_qubits: n.min(255) as u8, max: self.member.max_qubits.saturating_sub(1) });
}
let a = self.member.api;
self.member.select()?;
let len = 1usize << n;
let mut init = vec![0.0f64; len];
init[0] = 1.0;
let re = DeviceBuffer::upload(a, &init)?;
init[0] = 0.0;
let im = DeviceBuffer::upload(a, &init)?;
drop(init);
let flat = |m: &[crate::linalg::C]| -> Vec<f64> { m.iter().flat_map(|c| [c.re, c.im]).collect() };
let [diag, one, many] = [0, 1, 2].map(|k| self.kernels.functions[k]);
for l in &launches {
match l {
Launch::Diag { bitpos, diag: d } => {
let bp = DeviceBuffer::upload(a, bitpos)?;
let dg = DeviceBuffer::upload(a, &flat(d))?;
let (count, k) = (len as u64, bitpos.len() as u32);
launch(a, diag, count, &mut [arg(&re.ptr), arg(&im.ptr), arg(&count), arg(&bp.ptr), arg(&k), arg(&dg.ptr)])?;
}
Launch::One { s, m } => {
let pairs = (len / 2) as u64;
let v: Vec<f64> = flat(m);
let mut args = vec![arg(&re.ptr), arg(&im.ptr), arg(&pairs), arg(s)];
args.extend(v.iter().map(arg));
launch(a, one, pairs, &mut args)?;
}
Launch::Many { sorted, offsets, m } => {
let so = DeviceBuffer::upload(a, sorted)?;
let of = DeviceBuffer::upload(a, offsets)?;
let mm = DeviceBuffer::upload(a, &flat(m))?;
let (groups, k) = ((len >> sorted.len()) as u64, sorted.len() as u32);
launch(a, many, groups, &mut [arg(&re.ptr), arg(&im.ptr), arg(&groups), arg(&so.ptr), arg(&k), arg(&of.ptr), arg(&mm.ptr)])?;
}
}
}
Ok((re.download(a)?, im.download(a)?))
}
pub fn certify(&self, corpus: crate::quantum_fabric::sv64::SvCorpus) -> Result<Certificate, HipError> {
use crate::quantum_fabric::sv64::{KERNEL_SV_RUN, state_output};
let mut cert = Certification::new();
for case in corpus.cases() {
let reference = case.reference().map_err(|_| HipError::Internal("the corpus holds a gate the state vector refuses"))?;
let (re, im) = self.run(case.n, &case.gates, case.width)?;
cert.add(&case.bytes(), &reference, &state_output(&re, &im));
}
Ok(cert.finish(KERNEL_SV_RUN, &self.implementation, &self.member.id, &corpus.generator()))
}
pub fn entry(&self, corpus: crate::quantum_fabric::sv64::SvCorpus) -> Result<KernelEntry, HipError> {
Ok(KernelEntry { kernel: crate::quantum_fabric::sv64::KERNEL_SV_RUN.into(), implementation: self.implementation.clone(), certificate: self.certify(corpus)?, energy: None })
}
}
impl Runner for HipMember {
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)?)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_state_a_member_holds_is_half_its_free_memory_at_most() {
assert_eq!(qubits_for(0), 0);
assert_eq!(qubits_for(31), 0);
assert_eq!(qubits_for(32), 1);
assert_eq!(qubits_for(6 << 30), MAX_QUBITS.min(28));
assert_eq!(qubits_for(u64::MAX), MAX_QUBITS);
}
#[test]
fn the_launch_shape_covers_the_work_and_caps_the_blocks() {
assert_eq!(blocks_for(0), 1);
assert_eq!(blocks_for(1), 1);
assert_eq!(blocks_for(u64::from(hip::BLOCK) + 1), 2);
assert_eq!(blocks_for(1 << 40), hip::GRID_MAX);
}
fn props(name: &str, arch: &str, memory: u64) -> Vec<u8> {
let mut p = vec![0u8; PROPS_SIZE];
p[PROPS_NAME..PROPS_NAME + name.len()].copy_from_slice(name.as_bytes());
p[PROPS_ARCH..PROPS_ARCH + arch.len()].copy_from_slice(arch.as_bytes());
p[PROPS_UUID..PROPS_UUID + 16].copy_from_slice(&[0xab; 16]);
p[PROPS_TOTAL_MEMORY..PROPS_TOTAL_MEMORY + 8].copy_from_slice(&memory.to_le_bytes());
p[PROPS_COMPUTE_UNITS..PROPS_COMPUTE_UNITS + 4].copy_from_slice(&20u32.to_le_bytes());
p[PROPS_PCI_BUS..PROPS_PCI_BUS + 4].copy_from_slice(&0xc2u32.to_le_bytes());
p
}
#[test]
fn properties_read_in_the_frozen_layout_or_not_at_all() {
let id = identity_from(&props("a device", "gfx1151", 64 << 30), 70_152_801, (9, 0)).expect("reads");
assert_eq!((id.name.as_str(), id.arch.as_str(), id.key().as_str()), ("a device", "gfx1151", "gpu-hip-gfx1151"));
assert_eq!((id.pci.as_str(), id.compute_units, id.memory), ("0000:c2:00", 20, 64 << 30));
assert_eq!(id.uuid, "ab".repeat(16));
assert!(identity_from(&props("", "gfx1151", 1), 0, (0, 0)).is_none());
assert!(identity_from(&props("a device", "", 1), 0, (0, 0)).is_none());
assert!(identity_from(&props("a device", "sm_89", 1), 0, (0, 0)).is_none());
assert!(identity_from(&props("a device", "gfx1151", 0), 0, (0, 0)).is_none());
assert!(identity_from(&props("a device", "gfx\u{7f}", 1), 0, (0, 0)).is_none());
}
#[test]
fn the_refusals_that_may_fall_back_do() {
assert!(MemberError::from(HipError::Memory("x".into())).falls_back());
assert!(MemberError::from(HipError::OutOfRange { gate: 3 }).falls_back());
assert!(!MemberError::from(HipError::Probe { a: 1, b: 2 }).falls_back());
assert!(!MemberError::from(HipError::Internal("x")).falls_back());
}
}