mod plan;
#[cfg(test)]
mod tests;
#[cfg(any(test, feature = "bench-internal"))]
pub mod tiled;
use std::borrow::Cow;
use std::sync::Arc;
use num_complex::Complex64;
use rand::{RngExt, SeedableRng};
use rand_chacha::ChaCha8Rng;
use crate::backend::simd;
#[cfg(feature = "parallel")]
use crate::backend::statevector::SendPtr;
use crate::backend::statevector::StatevectorBackend;
use crate::backend::{
Backend, BasisSamples, dense_probability_len, dense_statevector_len, measurement_inv_norm,
};
#[cfg(feature = "parallel")]
use crate::backend::{MIN_PAR_ELEMS, MIN_PAR_REDUCE_ELEMS, chunk_min_len};
use crate::circuit::{Instruction, SmallVec, smallvec};
use crate::distributed::DistributedContext;
use crate::error::{PrismError, Result};
use crate::gates::{DiagEntry, Gate, is_diagonal_2x2};
use crate::sim::unified_pauli::PauliTerm;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
const BACKEND_NAME: &str = "distributed_statevector";
#[cfg(feature = "parallel")]
const PAR_SHARD_LEN: usize = 1 << 14;
fn scale_shard(state: &mut [Complex64], factor: Complex64) {
#[cfg(feature = "parallel")]
if state.len() >= PAR_SHARD_LEN {
state
.par_chunks_mut(MIN_PAR_ELEMS)
.for_each(|tile| simd::scale_complex_slice(tile, factor));
return;
}
simd::scale_complex_slice(state, factor);
}
fn scale_shard_masked(state: &mut [Complex64], mask: usize, factor: Complex64) {
let tile = |base: usize, tile: &mut [Complex64]| {
for (k, amp) in tile.iter_mut().enumerate() {
if (base + k) & mask == mask {
*amp *= factor;
}
}
};
#[cfg(feature = "parallel")]
if state.len() >= PAR_SHARD_LEN {
state
.par_chunks_mut(MIN_PAR_ELEMS)
.enumerate()
.for_each(|(t, chunk)| tile(t * MIN_PAR_ELEMS, chunk));
return;
}
tile(0, state);
}
fn zero_shard(state: &mut [Complex64]) {
#[cfg(feature = "parallel")]
if state.len() >= PAR_SHARD_LEN {
state
.par_chunks_mut(MIN_PAR_ELEMS)
.for_each(simd::zero_slice);
return;
}
simd::zero_slice(state);
}
fn combine_shard(
dst: &mut [Complex64],
remote: &[Complex64],
c_self: Complex64,
c_remote: Complex64,
) {
#[cfg(feature = "parallel")]
if dst.len() >= PAR_SHARD_LEN {
dst.par_chunks_mut(MIN_PAR_ELEMS)
.zip(remote.par_chunks(MIN_PAR_ELEMS))
.for_each(|(d, r)| simd::combine_global_half(d, r, c_self, c_remote));
return;
}
simd::combine_global_half(dst, remote, c_self, c_remote);
}
fn gather_indexed(
pack: &mut [Complex64],
state: &[Complex64],
index_of: impl Fn(usize) -> usize + Sync,
) {
#[cfg(feature = "parallel")]
if pack.len() >= PAR_SHARD_LEN {
pack.par_chunks_mut(MIN_PAR_ELEMS)
.enumerate()
.for_each(|(t, tile)| {
let base = t * MIN_PAR_ELEMS;
for (k, slot) in tile.iter_mut().enumerate() {
*slot = state[index_of(base + k)];
}
});
return;
}
for (k, slot) in pack.iter_mut().enumerate() {
*slot = state[index_of(k)];
}
}
fn scatter_indexed(
state: &mut [Complex64],
recv: &[Complex64],
index_of: impl Fn(usize) -> usize + Sync,
f: impl Fn(Complex64, Complex64) -> Complex64 + Sync,
) {
#[cfg(feature = "parallel")]
if recv.len() >= PAR_SHARD_LEN {
let ptr = SendPtr(state.as_mut_ptr());
recv.par_chunks(MIN_PAR_ELEMS)
.enumerate()
.for_each(|(t, tile)| {
let base = t * MIN_PAR_ELEMS;
for (k, &r) in tile.iter().enumerate() {
let i = index_of(base + k);
unsafe { ptr.store(i, f(ptr.load(i), r)) };
}
});
return;
}
for (k, &r) in recv.iter().enumerate() {
let i = index_of(k);
state[i] = f(state[i], r);
}
}
fn for_each_pair_tile<F>(state: &mut [Complex64], recv: &mut [Complex64], local_q: usize, f: F)
where
F: Fn(&mut [Complex64], &mut [Complex64], &mut [Complex64], &mut [Complex64]) + Sync,
{
let half = 1usize << local_q;
let block = half << 1;
#[cfg(feature = "parallel")]
if state.len() >= PAR_SHARD_LEN {
if state.len() / block >= 4 {
state
.par_chunks_mut(block)
.zip(recv.par_chunks_mut(block))
.with_min_len(chunk_min_len(block))
.for_each(|(s, r)| {
let (lo, hi) = s.split_at_mut(half);
let (rlo, rhi) = r.split_at_mut(half);
f(lo, hi, rlo, rhi);
});
} else {
for (s, r) in state.chunks_mut(block).zip(recv.chunks_mut(block)) {
let (lo, hi) = s.split_at_mut(half);
let (rlo, rhi) = r.split_at_mut(half);
lo.par_chunks_mut(MIN_PAR_ELEMS)
.zip(hi.par_chunks_mut(MIN_PAR_ELEMS))
.zip(rlo.par_chunks_mut(MIN_PAR_ELEMS))
.zip(rhi.par_chunks_mut(MIN_PAR_ELEMS))
.for_each(|(((lo, hi), rlo), rhi)| f(lo, hi, rlo, rhi));
}
}
return;
}
for (s, r) in state.chunks_mut(block).zip(recv.chunks_mut(block)) {
let (lo, hi) = s.split_at_mut(half);
let (rlo, rhi) = r.split_at_mut(half);
f(lo, hi, rlo, rhi);
}
}
fn butterfly_shard(
state: &mut [Complex64],
remote: &[Complex64],
pack: &mut [Complex64],
keep: [Complex64; 2],
forward: [Complex64; 2],
) {
let tile = |state: &mut [Complex64], remote: &[Complex64], pack: &mut [Complex64]| {
for ((s, &r), p) in state.iter_mut().zip(remote).zip(pack.iter_mut()) {
let own = *s;
*p = forward[0] * own + forward[1] * r;
*s = keep[0] * own + keep[1] * r;
}
};
#[cfg(feature = "parallel")]
if state.len() >= PAR_SHARD_LEN {
state
.par_chunks_mut(MIN_PAR_ELEMS)
.zip(remote.par_chunks(MIN_PAR_ELEMS))
.zip(pack.par_chunks_mut(MIN_PAR_ELEMS))
.for_each(|((s, r), p)| tile(s, r, p));
return;
}
tile(state, remote, pack);
}
fn add_shard(dst: &mut [Complex64], src: &[Complex64]) {
let tile = |dst: &mut [Complex64], src: &[Complex64]| {
for (d, &s) in dst.iter_mut().zip(src) {
*d += s;
}
};
#[cfg(feature = "parallel")]
if dst.len() >= PAR_SHARD_LEN {
dst.par_chunks_mut(MIN_PAR_ELEMS)
.zip(src.par_chunks(MIN_PAR_ELEMS))
.for_each(|(d, s)| tile(d, s));
return;
}
tile(dst, src);
}
fn half_norm_sqr(state: &[Complex64], qubit: usize, outcome: bool) -> f64 {
fn select(block: &[Complex64], half: usize, outcome: bool) -> &[Complex64] {
if outcome {
&block[half..]
} else {
&block[..half]
}
}
let half = 1usize << qubit;
let block = half << 1;
#[cfg(feature = "parallel")]
if state.len() >= MIN_PAR_REDUCE_ELEMS {
if state.len() / block >= 4 {
return state
.par_chunks(block)
.with_min_len(chunk_min_len(block))
.map(|b| simd::norm_sqr_sum(select(b, half, outcome)))
.sum();
}
return state
.chunks(block)
.map(|b| {
select(b, half, outcome)
.par_chunks(MIN_PAR_ELEMS)
.map(simd::norm_sqr_sum)
.sum::<f64>()
})
.sum();
}
state
.chunks(block)
.map(|b| simd::norm_sqr_sum(select(b, half, outcome)))
.sum()
}
fn for_each_gate_qubit(gate: &Gate, targets: &[usize], mut f: impl FnMut(usize)) {
for &q in targets {
f(q);
}
match gate {
Gate::BatchPhase(data) => {
for &(target, _) in &data.phases {
f(target);
}
}
Gate::BatchRzz(data) => {
for &(q0, q1, _) in &data.edges {
f(q0);
f(q1);
}
}
Gate::MultiFused(data) => {
for &(q, _) in &data.gates {
f(q);
}
}
Gate::Multi2q(data) => {
for &(q0, q1, _) in &data.gates {
f(q0);
f(q1);
}
}
Gate::DiagonalBatch(data) => {
for entry in &data.entries {
match *entry {
DiagEntry::Phase1q { qubit, .. } => f(qubit),
DiagEntry::Phase2q { q0, q1, .. } | DiagEntry::Parity2q { q0, q1, .. } => {
f(q0);
f(q1);
}
}
}
}
_ => {}
}
}
fn required_local_qubits(gate: &Gate, targets: &[usize]) -> SmallVec<[usize; 8]> {
let mut req: SmallVec<[usize; 8]> = SmallVec::new();
fn push(req: &mut SmallVec<[usize; 8]>, q: usize) {
if !req.contains(&q) {
req.push(q);
}
}
match gate {
Gate::Cx => push(&mut req, targets[1]),
Gate::Cz
| Gate::Swap
| Gate::Rzz(_)
| Gate::BatchPhase(_)
| Gate::BatchRzz(_)
| Gate::DiagonalBatch(_) => {}
Gate::Cu(_) | Gate::Mcu(_) => {
if gate.controlled_phase().is_none() {
let (target, mat) = match gate {
Gate::Mcu(data) => (targets[data.num_controls as usize], &data.mat),
Gate::Cu(mat) => (targets[1], &**mat),
_ => unreachable!("outer match arm is Cu | Mcu"),
};
if !is_diagonal_2x2(mat) {
push(&mut req, target);
}
}
}
Gate::Fused2q(_) => {
push(&mut req, targets[0]);
push(&mut req, targets[1]);
}
Gate::Multi2q(data) => {
for &(q0, q1, _) in &data.gates {
push(&mut req, q0);
push(&mut req, q1);
}
}
Gate::MultiFused(data) => {
for &(q, ref mat) in &data.gates {
if !is_diagonal_2x2(mat) {
push(&mut req, q);
}
}
}
Gate::Unitary(_) => {
for &q in targets {
push(&mut req, q);
}
}
g if g.num_qubits() == 1 && !g.is_diagonal_1q() => {
push(&mut req, targets[0]);
}
_ => {}
}
req
}
pub struct DistributedStatevectorBackend {
context: Arc<DistributedContext>,
inner: StatevectorBackend,
num_qubits: usize,
global_qubits: usize,
recv: Vec<Complex64>,
seed: u64,
exchange_chunk: usize,
exchange_messages: u64,
exchange_amplitudes: u64,
meas_rng: ChaCha8Rng,
qubit_map: Vec<usize>,
phys_map: Vec<usize>,
map_identity: bool,
relabel: bool,
last_used: Vec<u64>,
tick: u64,
pack: Vec<Complex64>,
circuit_check_pending: bool,
}
impl DistributedStatevectorBackend {
pub fn new(context: Arc<DistributedContext>, seed: u64) -> Self {
Self {
context,
inner: StatevectorBackend::new(seed),
num_qubits: 0,
global_qubits: 0,
recv: Vec::new(),
seed,
exchange_chunk: crate::distributed::exchange_chunk(),
exchange_messages: 0,
exchange_amplitudes: 0,
meas_rng: ChaCha8Rng::seed_from_u64(seed),
qubit_map: Vec::new(),
phys_map: Vec::new(),
map_identity: true,
relabel: crate::distributed::relabel_enabled(),
last_used: Vec::new(),
tick: 0,
pack: Vec::new(),
circuit_check_pending: true,
}
}
#[cfg(test)]
pub(crate) fn set_exchange_chunk(&mut self, chunk: usize) {
self.exchange_chunk = chunk.max(1);
}
pub fn set_relabel(&mut self, enabled: bool) {
self.relabel = enabled;
}
pub fn exchange_messages(&self) -> u64 {
self.exchange_messages
}
pub fn exchange_amplitudes(&self) -> u64 {
self.exchange_amplitudes
}
#[inline]
fn count_exchange(&mut self, amplitudes: usize) {
self.exchange_messages += 1;
self.exchange_amplitudes += amplitudes as u64;
}
#[inline]
fn ensure_recv(&mut self, len: usize) {
if self.recv.len() < len {
self.recv.resize(len, Complex64::new(0.0, 0.0));
}
}
#[inline]
fn ensure_pack(&mut self, len: usize) {
if self.pack.len() < len {
self.pack.resize(len, Complex64::new(0.0, 0.0));
}
}
#[inline]
fn block_chunk(&self, block_bits: usize, len: usize) -> usize {
let block = 1usize << block_bits;
((self.exchange_chunk / block).max(1) * block).min(len)
}
#[inline]
fn local_qubits(&self) -> usize {
self.num_qubits - self.global_qubits
}
#[inline]
fn is_single_rank(&self) -> bool {
self.context.size() == 1
}
#[inline]
fn global_bit(&self, q: usize) -> usize {
q - self.local_qubits()
}
#[inline]
fn rank_bit_set(&self, q: usize) -> bool {
(self.context.rank() >> self.global_bit(q)) & 1 == 1
}
fn touch_instruction(&mut self, gate: &Gate, targets: &[usize]) {
self.tick += 1;
let tick = self.tick;
for_each_gate_qubit(gate, targets, |q| self.last_used[q] = tick);
}
fn refresh_map_identity(&mut self) {
self.map_identity = self.qubit_map.iter().enumerate().all(|(q, &p)| q == p);
}
fn swap_circuit_qubits(&mut self, a: usize, b: usize) {
if a == b {
return;
}
let pa = self.qubit_map[a];
let pb = self.qubit_map[b];
self.qubit_map.swap(a, b);
self.phys_map.swap(pa, pb);
self.refresh_map_identity();
}
fn pick_victim(&self) -> Option<usize> {
let local = self.local_qubits();
let mut best: Option<(u64, usize)> = None;
for pos in 0..local {
let used = self.last_used[self.phys_map[pos]];
if used == self.tick {
continue;
}
match best {
Some((b, _)) if used >= b => {}
_ => best = Some((used, pos)),
}
}
best.map(|(_, pos)| pos)
}
fn make_local(&mut self, req: &[usize]) {
for &q in req {
let pos = self.qubit_map[q];
if pos < self.local_qubits() {
continue;
}
let Some(victim) = self.pick_victim() else {
return;
};
self.relabel_swap(victim, pos);
}
}
fn half_slice_swap(&mut self, local_pos: usize, global_pos: usize) {
let partner = self.context.rank() ^ (1usize << self.global_bit(global_pos));
let gbit = self.rank_bit_set(global_pos);
let stride = 1usize << local_pos;
let fixed = if gbit { 0 } else { stride };
let moving = self.inner.state.len() / 2;
let chunk = self.exchange_chunk.min(moving).max(1);
self.ensure_pack(chunk);
self.ensure_recv(chunk);
let index_of =
|flat: usize| ((flat >> local_pos) << (local_pos + 1)) | fixed | (flat & (stride - 1));
let mut off = 0;
while off < moving {
let count = (off + chunk).min(moving) - off;
gather_indexed(&mut self.pack[..count], &self.inner.state, |k| {
index_of(off + k)
});
self.count_exchange(count);
self.context
.comm()
.sendrecv_c64(partner, &self.pack[..count], &mut self.recv[..count]);
scatter_indexed(
&mut self.inner.state,
&self.recv[..count],
|k| index_of(off + k),
|_, remote| remote,
);
off += count;
}
}
fn relabel_swap(&mut self, local_pos: usize, global_pos: usize) {
self.half_slice_swap(local_pos, global_pos);
let local_q = self.phys_map[local_pos];
let global_q = self.phys_map[global_pos];
self.qubit_map[local_q] = global_pos;
self.qubit_map[global_q] = local_pos;
self.phys_map.swap(local_pos, global_pos);
self.refresh_map_identity();
}
fn swap_physical_positions(&mut self, a: usize, b: usize) {
debug_assert!(
a < b,
"positions must be ordered: branch selection assumes a < b"
);
let local = self.local_qubits();
if b < local {
self.inner
.apply(&Instruction::Gate {
gate: Gate::Swap,
targets: smallvec![a, b],
})
.expect("local SWAP cannot fail");
} else if a < local {
self.relabel_swap(a, b);
return;
} else {
self.swap_global_slices(a, b);
}
let qa = self.phys_map[a];
let qb = self.phys_map[b];
self.qubit_map[qa] = b;
self.qubit_map[qb] = a;
self.phys_map.swap(a, b);
self.refresh_map_identity();
}
fn restore_identity_map(&mut self) {
while !self.map_identity {
let Some(pos) = (0..self.num_qubits).find(|&p| self.phys_map[p] != p) else {
break;
};
let src = self.qubit_map[pos];
self.swap_physical_positions(pos, src);
}
}
fn to_physical<'g>(
&self,
gate: &'g Gate,
targets: &[usize],
) -> (Cow<'g, Gate>, SmallVec<[usize; 4]>) {
if self.map_identity {
return (Cow::Borrowed(gate), targets.into());
}
let ptargets: SmallVec<[usize; 4]> = targets.iter().map(|&q| self.qubit_map[q]).collect();
let fixed = |q: usize| self.qubit_map[q] == q;
let pgate = match gate {
Gate::MultiFused(data) => {
if data.gates.iter().all(|&(q, _)| fixed(q)) {
Cow::Borrowed(gate)
} else {
let mut data = data.clone();
for entry in &mut data.gates {
entry.0 = self.qubit_map[entry.0];
}
Cow::Owned(Gate::MultiFused(data))
}
}
Gate::Multi2q(data) => {
if data.gates.iter().all(|&(q0, q1, _)| fixed(q0) && fixed(q1)) {
Cow::Borrowed(gate)
} else {
let mut data = data.clone();
for entry in &mut data.gates {
entry.0 = self.qubit_map[entry.0];
entry.1 = self.qubit_map[entry.1];
}
Cow::Owned(Gate::Multi2q(data))
}
}
Gate::BatchPhase(data) => {
if data.phases.iter().all(|&(q, _)| fixed(q)) {
Cow::Borrowed(gate)
} else {
let mut data = data.clone();
for entry in &mut data.phases {
entry.0 = self.qubit_map[entry.0];
}
Cow::Owned(Gate::BatchPhase(data))
}
}
Gate::BatchRzz(data) => {
if data.edges.iter().all(|&(q0, q1, _)| fixed(q0) && fixed(q1)) {
Cow::Borrowed(gate)
} else {
let mut data = data.clone();
for entry in &mut data.edges {
entry.0 = self.qubit_map[entry.0];
entry.1 = self.qubit_map[entry.1];
}
Cow::Owned(Gate::BatchRzz(data))
}
}
Gate::DiagonalBatch(data) => {
let entry_fixed = |entry: &DiagEntry| match *entry {
DiagEntry::Phase1q { qubit, .. } => fixed(qubit),
DiagEntry::Phase2q { q0, q1, .. } | DiagEntry::Parity2q { q0, q1, .. } => {
fixed(q0) && fixed(q1)
}
};
if data.entries.iter().all(entry_fixed) {
Cow::Borrowed(gate)
} else {
let mut data = data.clone();
for entry in &mut data.entries {
match entry {
DiagEntry::Phase1q { qubit, .. } => *qubit = self.qubit_map[*qubit],
DiagEntry::Phase2q { q0, q1, .. }
| DiagEntry::Parity2q { q0, q1, .. } => {
*q0 = self.qubit_map[*q0];
*q1 = self.qubit_map[*q1];
}
}
}
Cow::Owned(Gate::DiagonalBatch(data))
}
}
_ => Cow::Borrowed(gate),
};
(pgate, ptargets)
}
fn instruction_qubits_local(&self, gate: &Gate, targets: &[usize]) -> bool {
let local = self.local_qubits();
let mut all = true;
for_each_gate_qubit(gate, targets, |q| all &= q < local);
all
}
fn config_fingerprint(&self, num_qubits: usize, num_classical_bits: usize) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::hash::DefaultHasher::new();
(
self.seed,
self.exchange_chunk,
self.relabel,
crate::distributed::min_local_qubits(),
num_qubits,
num_classical_bits,
)
.hash(&mut hasher);
hasher.finish()
}
fn circuit_fingerprint(instructions: &[Instruction]) -> u64 {
use std::fmt::Write as _;
use std::hash::Hasher;
struct HashSink<'a>(&'a mut std::hash::DefaultHasher);
impl std::fmt::Write for HashSink<'_> {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.0.write(s.as_bytes());
Ok(())
}
}
let mut hasher = std::hash::DefaultHasher::new();
let _ = write!(HashSink(&mut hasher), "{instructions:?}");
hasher.finish()
}
fn check_circuit_agreement(&self, instructions: &[Instruction]) -> Result<()> {
let local = Self::circuit_fingerprint(instructions);
let all = self.context.comm().allgather_u64(&[local]);
match all.iter().position(|&other| other != local) {
None => Ok(()),
Some(other) => Err(PrismError::BackendUnsupported {
backend: BACKEND_NAME.to_string(),
operation: format!(
"the circuit on rank {} differs from rank {other}: every rank enters every \
collective, so every rank must run the same circuit",
self.context.rank()
),
}),
}
}
fn check_config_agreement(&self, num_qubits: usize, num_classical_bits: usize) -> Result<()> {
let local = self.config_fingerprint(num_qubits, num_classical_bits);
let all = self.context.comm().allgather_u64(&[local]);
match all.iter().position(|&other| other != local) {
None => Ok(()),
Some(other) => Err(PrismError::BackendUnsupported {
backend: BACKEND_NAME.to_string(),
operation: format!(
"configuration on rank {} differs from rank {other}: seed, relabel mode, \
exchange chunk, local qubit floor, and register shape must be identical \
on every rank",
self.context.rank()
),
}),
}
}
fn prepare_shard(&mut self, num_qubits: usize, num_classical_bits: usize) -> Result<usize> {
let size = self.context.size();
if size > 1 {
self.check_config_agreement(num_qubits, num_classical_bits)?;
}
self.circuit_check_pending = true;
if !size.is_power_of_two() {
return Err(PrismError::BackendUnsupported {
backend: BACKEND_NAME.to_string(),
operation: format!("rank count {size} is not a power of two"),
});
}
let p = size.trailing_zeros() as usize;
let min_local = crate::distributed::min_local_qubits();
if size > 1 && num_qubits < p + min_local {
return Err(PrismError::BackendUnsupported {
backend: BACKEND_NAME.to_string(),
operation: format!(
"{num_qubits} qubits across {size} ranks leaves fewer than \
{min_local} local qubits per rank"
),
});
}
self.num_qubits = num_qubits;
self.global_qubits = p;
self.meas_rng = ChaCha8Rng::seed_from_u64(self.seed);
self.exchange_messages = 0;
self.exchange_amplitudes = 0;
self.qubit_map = (0..num_qubits).collect();
self.phys_map = (0..num_qubits).collect();
self.map_identity = true;
self.last_used = vec![0; num_qubits];
self.tick = 0;
Ok(num_qubits - p)
}
fn to_physical_mask(&self, mask: usize) -> usize {
if self.map_identity {
return mask;
}
let mut out = 0usize;
for (q, &pos) in self.qubit_map.iter().enumerate() {
out |= ((mask >> q) & 1) << pos;
}
out
}
fn unpermuted<T: Copy + Default>(&self, phys: Vec<T>) -> Vec<T> {
if self.map_identity {
return phys;
}
let mut out = vec![T::default(); phys.len()];
for (c, slot) in out.iter_mut().enumerate() {
let mut p = 0usize;
for (q, &pos) in self.qubit_map.iter().enumerate() {
p |= ((c >> q) & 1) << pos;
}
*slot = phys[p];
}
out
}
fn apply_global_1q(&mut self, target: usize, mat: [[Complex64; 2]; 2]) {
let partner = self.context.rank() ^ (1usize << self.global_bit(target));
let (c_self, c_remote) = if self.rank_bit_set(target) {
(mat[1][1], mat[1][0])
} else {
(mat[0][0], mat[0][1])
};
let len = self.inner.state.len();
let chunk = self.exchange_chunk.min(len).max(1);
self.ensure_recv(chunk);
let mut off = 0;
while off < len {
let end = (off + chunk).min(len);
self.count_exchange(end - off);
let recv = &mut self.recv[..end - off];
self.context
.comm()
.sendrecv_c64(partner, &self.inner.state[off..end], recv);
combine_shard(&mut self.inner.state[off..end], recv, c_self, c_remote);
off = end;
}
}
fn apply_global_diagonal_1q(&mut self, target: usize, d0: Complex64, d1: Complex64) {
let factor = if self.rank_bit_set(target) { d1 } else { d0 };
scale_shard(&mut self.inner.state, factor);
}
fn apply_local_controlled_1q(
&mut self,
local_controls: &[usize],
target: usize,
mat: [[Complex64; 2]; 2],
) {
let gate = match local_controls.len() {
0 => {
self.inner
.apply_1q_matrix(target, &mat)
.expect("local 1q matrix");
return;
}
1 => Gate::cu(mat),
n => Gate::mcu(mat, n as u8),
};
let mut targets: SmallVec<[usize; 4]> = local_controls.iter().copied().collect();
targets.push(target);
self.inner
.apply(&Instruction::Gate { gate, targets })
.expect("local controlled 1q");
}
fn apply_global_controlled_1q(
&mut self,
local_controls: &[usize],
target: usize,
mat: [[Complex64; 2]; 2],
) {
if local_controls.is_empty() {
self.apply_global_1q(target, mat);
return;
}
let partner = self.context.rank() ^ (1usize << self.global_bit(target));
let (c_self, c_remote) = if self.rank_bit_set(target) {
(mat[1][1], mat[1][0])
} else {
(mat[0][0], mat[0][1])
};
let mut ctrl_pos: SmallVec<[usize; 4]> = local_controls.iter().copied().collect();
ctrl_pos.sort_unstable();
let index_of = |flat: usize| {
let mut i = flat;
for &p in &ctrl_pos {
let low = i & ((1usize << p) - 1);
i = ((i >> p) << (p + 1)) | (1usize << p) | low;
}
i
};
let moving = self.inner.state.len() >> ctrl_pos.len();
let chunk = self.exchange_chunk.min(moving).max(1);
self.ensure_pack(chunk);
self.ensure_recv(chunk);
let mut off = 0;
while off < moving {
let count = (off + chunk).min(moving) - off;
gather_indexed(&mut self.pack[..count], &self.inner.state, |k| {
index_of(off + k)
});
self.count_exchange(count);
self.context
.comm()
.sendrecv_c64(partner, &self.pack[..count], &mut self.recv[..count]);
scatter_indexed(
&mut self.inner.state,
&self.recv[..count],
|k| index_of(off + k),
|own, remote| c_self * own + c_remote * remote,
);
off += count;
}
}
fn apply_controlled_dist(
&mut self,
controls: &[usize],
target: usize,
mat: [[Complex64; 2]; 2],
) {
let local = self.local_qubits();
let mut local_controls: SmallVec<[usize; 4]> = SmallVec::new();
for &c in controls {
if c < local {
local_controls.push(c);
} else if !self.rank_bit_set(c) {
return;
}
}
if target < local {
self.apply_local_controlled_1q(&local_controls, target, mat);
} else if is_diagonal_2x2(&mat) {
let d = if self.rank_bit_set(target) {
mat[1][1]
} else {
mat[0][0]
};
if local_controls.is_empty() {
scale_shard(&mut self.inner.state, d);
return;
}
let ctrl_mask: usize = local_controls.iter().map(|&c| 1usize << c).sum();
scale_shard_masked(&mut self.inner.state, ctrl_mask, d);
} else {
self.apply_global_controlled_1q(&local_controls, target, mat);
}
}
fn apply_controlled_phase_dist(&mut self, qubits: &[usize], phase: Complex64) {
let local = self.local_qubits();
let mut local_qubits: SmallVec<[usize; 8]> = SmallVec::new();
for &q in qubits {
if q < local {
local_qubits.push(q);
} else if !self.rank_bit_set(q) {
return;
}
}
self.apply_local_corner_phase(&local_qubits, phase);
}
fn apply_local_corner_phase(&mut self, local_qubits: &[usize], phase: Complex64) {
let z = Complex64::new(0.0, 0.0);
let one = Complex64::new(1.0, 0.0);
match local_qubits.len() {
0 => scale_shard(&mut self.inner.state, phase),
1 => self
.inner
.apply_1q_matrix(local_qubits[0], &[[one, z], [z, phase]])
.expect("local diagonal phase"),
n => {
let mat = [[one, z], [z, phase]];
let gate = if n == 2 {
Gate::cu(mat)
} else {
Gate::mcu(mat, (n - 1) as u8)
};
self.inner
.apply(&Instruction::Gate {
gate,
targets: local_qubits.iter().copied().collect(),
})
.expect("local controlled phase");
}
}
}
fn apply_rzz_dist(&mut self, q0: usize, q1: usize, theta: f64) {
let phase_same = Complex64::from_polar(1.0, -theta / 2.0);
let phase_diff = Complex64::from_polar(1.0, theta / 2.0);
self.apply_rzz_phases_dist(q0, q1, phase_same, phase_diff);
}
fn apply_rzz_phases_dist(
&mut self,
q0: usize,
q1: usize,
phase_same: Complex64,
phase_diff: Complex64,
) {
let local = self.local_qubits();
match (q0 < local, q1 < local) {
(true, true) => {
use crate::gates::{DiagEntry, DiagonalBatchData};
let entry = DiagEntry::Parity2q {
q0,
q1,
same: phase_same,
diff: phase_diff,
};
self.inner
.apply(&Instruction::Gate {
gate: Gate::DiagonalBatch(Box::new(DiagonalBatchData {
entries: vec![entry],
})),
targets: smallvec![q0, q1],
})
.expect("local parity diagonal");
}
(false, false) => {
let parity =
((self.rank_bit_set(q0) as usize) ^ (self.rank_bit_set(q1) as usize)) & 1;
let factor = [phase_same, phase_diff][parity];
scale_shard(&mut self.inner.state, factor);
}
(true, false) | (false, true) => {
let (local_q, global_q) = if q0 < local { (q0, q1) } else { (q1, q0) };
let gbit = self.rank_bit_set(global_q) as usize;
let d0 = [phase_same, phase_diff][gbit];
let d1 = [phase_same, phase_diff][gbit ^ 1];
let z = Complex64::new(0.0, 0.0);
self.inner
.apply_1q_matrix(local_q, &[[d0, z], [z, d1]])
.expect("local parity residual");
}
}
}
fn apply_swap_dist(&mut self, a: usize, b: usize) {
let local = self.local_qubits();
match (a < local, b < local) {
(true, true) => {
self.inner
.apply(&Instruction::Gate {
gate: Gate::Swap,
targets: smallvec![a, b],
})
.expect("local swap");
}
(false, false) => self.swap_global_slices(a, b),
(true, false) | (false, true) => {
let (local_q, global_q) = if a < local { (a, b) } else { (b, a) };
self.half_slice_swap(local_q, global_q);
}
}
}
fn swap_global_slices(&mut self, a: usize, b: usize) {
if self.rank_bit_set(a) == self.rank_bit_set(b) {
return;
}
let partner =
self.context.rank() ^ (1usize << self.global_bit(a)) ^ (1usize << self.global_bit(b));
let len = self.inner.state.len();
let chunk = self.exchange_chunk.min(len).max(1);
self.ensure_recv(chunk);
let mut off = 0;
while off < len {
let end = (off + chunk).min(len);
self.count_exchange(end - off);
let recv = &mut self.recv[..end - off];
self.context
.comm()
.sendrecv_c64(partner, &self.inner.state[off..end], recv);
self.inner.state[off..end].copy_from_slice(recv);
off = end;
}
}
fn apply_2q_dist(&mut self, q0: usize, q1: usize, mat: &[[Complex64; 4]; 4]) {
let local = self.local_qubits();
match (q0 < local, q1 < local) {
(true, true) => self.apply_local_fused_2q(q0, q1, mat),
(true, false) | (false, true) => self.apply_2q_one_global(q0, q1, mat),
(false, false) => self.apply_2q_two_global(q0, q1, mat),
}
}
fn apply_local_fused_2q(&mut self, q0: usize, q1: usize, mat: &[[Complex64; 4]; 4]) {
self.inner.apply_fused_2q(q0, q1, mat);
}
fn apply_2q_one_global(&mut self, q0: usize, q1: usize, mat: &[[Complex64; 4]; 4]) {
let local = self.local_qubits();
let (local_q, global_q, global_is_q0) = if q0 < local {
(q0, q1, false)
} else {
(q1, q0, true)
};
let partner = self.context.rank() ^ (1usize << self.global_bit(global_q));
let len = self.inner.state.len();
let chunk = self.block_chunk(local_q + 1, len);
self.ensure_recv(chunk);
let g = self.rank_bit_set(global_q) as usize;
let basis = |gbit: usize, lbit: usize| -> usize {
if global_is_q0 {
(gbit << 1) | lbit
} else {
(lbit << 1) | gbit
}
};
let cols = [basis(g, 0), basis(g, 1), basis(1 - g, 0), basis(1 - g, 1)];
let coeffs = |row: usize| cols.map(|c| mat[row][c]);
let (m_lo, m_hi) = (coeffs(basis(g, 0)), coeffs(basis(g, 1)));
let mut off = 0;
while off < len {
let end = (off + chunk).min(len);
self.count_exchange(end - off);
let recv = &mut self.recv[..end - off];
self.context
.comm()
.sendrecv_c64(partner, &self.inner.state[off..end], recv);
for_each_pair_tile(
&mut self.inner.state[off..end],
recv,
local_q,
|lo, hi, rlo, rhi| {
for (((s0, s1), &r0), &r1) in
lo.iter_mut().zip(hi).zip(rlo.iter()).zip(rhi.iter())
{
let (own0, own1) = (*s0, *s1);
*s0 = m_lo[0] * own0 + m_lo[1] * own1 + m_lo[2] * r0 + m_lo[3] * r1;
*s1 = m_hi[0] * own0 + m_hi[1] * own1 + m_hi[2] * r0 + m_hi[3] * r1;
}
},
);
off = end;
}
}
fn apply_2q_run_one_global(
&mut self,
global_q: usize,
entries: &[(usize, usize, [[Complex64; 4]; 4])],
) {
let partner = self.context.rank() ^ (1usize << self.global_bit(global_q));
let local_of = |q0: usize, q1: usize| if q0 == global_q { q1 } else { q0 };
let widest = entries
.iter()
.map(|&(q0, q1, _)| local_of(q0, q1))
.max()
.expect("a run has at least one entry");
let len = self.inner.state.len();
let chunk = self.block_chunk(widest + 1, len);
self.ensure_recv(chunk);
let g = self.rank_bit_set(global_q) as usize;
let zero = Complex64::new(0.0, 0.0);
let mut off = 0;
while off < len {
let end = (off + chunk).min(len);
self.count_exchange(end - off);
let recv = &mut self.recv[..end - off];
self.context
.comm()
.sendrecv_c64(partner, &self.inner.state[off..end], recv);
for &(q0, q1, ref mat) in entries {
let (local_q, global_is_q0) = (local_of(q0, q1), q0 == global_q);
let basis = |gbit: usize, lbit: usize| -> usize {
if global_is_q0 {
(gbit << 1) | lbit
} else {
(lbit << 1) | gbit
}
};
let slots = [basis(g, 0), basis(g, 1), basis(1 - g, 0), basis(1 - g, 1)];
for_each_pair_tile(
&mut self.inner.state[off..end],
recv,
local_q,
|lo, hi, rlo, rhi| {
for (((s0, s1), r0), r1) in lo.iter_mut().zip(hi).zip(rlo).zip(rhi) {
let mut by_col = [zero; 4];
by_col[slots[0]] = *s0;
by_col[slots[1]] = *s1;
by_col[slots[2]] = *r0;
by_col[slots[3]] = *r1;
let mut outs = [zero; 4];
for (out, &row) in outs.iter_mut().zip(slots.iter()) {
for (c, &) in by_col.iter().enumerate() {
*out += mat[row][c] * amp;
}
}
*s0 = outs[0];
*s1 = outs[1];
*r0 = outs[2];
*r1 = outs[3];
}
},
);
}
off = end;
}
}
fn apply_2q_two_global(&mut self, q0: usize, q1: usize, mat: &[[Complex64; 4]; 4]) {
let rank = self.context.rank();
let bit0 = 1usize << self.global_bit(q0);
let bit1 = 1usize << self.global_bit(q1);
let g0 = (rank & bit0 != 0) as usize;
let g1 = (rank & bit1 != 0) as usize;
let basis = |c0: usize, c1: usize| (c0 << 1) | c1;
let (row, forward_row) = (basis(g0, g1), basis(g0, 1 - g1));
let (own, across) = (basis(g0, g1), basis(1 - g0, g1));
let keep = [mat[row][own], mat[row][across]];
let forward = [mat[forward_row][own], mat[forward_row][across]];
let len = self.inner.state.len();
let chunk = self.exchange_chunk.min(len).max(1);
self.ensure_recv(chunk);
self.ensure_pack(chunk);
let mut off = 0;
while off < len {
let end = (off + chunk).min(len);
let count = end - off;
self.count_exchange(count);
self.context.comm().sendrecv_c64(
rank ^ bit0,
&self.inner.state[off..end],
&mut self.recv[..count],
);
butterfly_shard(
&mut self.inner.state[off..end],
&self.recv[..count],
&mut self.pack[..count],
keep,
forward,
);
self.count_exchange(count);
self.context.comm().sendrecv_c64(
rank ^ bit1,
&self.pack[..count],
&mut self.recv[..count],
);
add_shard(&mut self.inner.state[off..end], &self.recv[..count]);
off = end;
}
}
fn apply_global_multi_qubit(&mut self, gate: &Gate, targets: &[usize]) -> Result<()> {
match gate {
Gate::Cx => {
self.apply_controlled_dist(&targets[..1], targets[1], Gate::X.matrix_2x2());
Ok(())
}
Gate::Cz => {
self.apply_controlled_phase_dist(
&[targets[0], targets[1]],
-Complex64::new(1.0, 0.0),
);
Ok(())
}
Gate::Swap => {
self.apply_swap_dist(targets[0], targets[1]);
Ok(())
}
Gate::Rzz(theta) => {
self.apply_rzz_dist(targets[0], targets[1], *theta);
Ok(())
}
Gate::Cu(mat) => {
if let Some(phase) = gate.controlled_phase() {
self.apply_controlled_phase_dist(&[targets[0], targets[1]], phase);
} else {
self.apply_controlled_dist(&targets[..1], targets[1], **mat);
}
Ok(())
}
Gate::Mcu(data) => {
let num_ctrl = data.num_controls as usize;
let controls = &targets[..num_ctrl];
let target = targets[num_ctrl];
if let Some(phase) = gate.controlled_phase() {
let mut corner: Vec<usize> = controls.to_vec();
corner.push(target);
self.apply_controlled_phase_dist(&corner, phase);
} else {
self.apply_controlled_dist(controls, target, data.mat);
}
Ok(())
}
Gate::Fused2q(mat) => {
self.apply_2q_dist(targets[0], targets[1], mat);
Ok(())
}
Gate::Multi2q(data) => {
let local = self.local_qubits();
let one_global_on = |entry: &(usize, usize, [[Complex64; 4]; 4])| {
let (q0, q1, _) = *entry;
match (q0 < local, q1 < local) {
(true, false) => Some(q1),
(false, true) => Some(q0),
_ => None,
}
};
let mut i = 0;
while i < data.gates.len() {
let Some(g) = one_global_on(&data.gates[i]) else {
let (q0, q1, ref mat) = data.gates[i];
self.apply_2q_dist(q0, q1, mat);
i += 1;
continue;
};
let mut end = i + 1;
while end < data.gates.len() && one_global_on(&data.gates[end]) == Some(g) {
end += 1;
}
if end - i == 1 {
let (q0, q1, ref mat) = data.gates[i];
self.apply_2q_one_global(q0, q1, mat);
} else {
self.apply_2q_run_one_global(g, &data.gates[i..end]);
}
i = end;
}
Ok(())
}
Gate::MultiFused(data) => {
for &(q, ref mat) in &data.gates {
if q < self.local_qubits() {
self.inner.apply_1q_matrix(q, mat).expect("local 1q matrix");
} else if is_diagonal_2x2(mat) {
self.apply_global_diagonal_1q(q, mat[0][0], mat[1][1]);
} else {
self.apply_global_1q(q, *mat);
}
}
Ok(())
}
Gate::BatchPhase(data) => {
let control = targets[0];
for &(target, phase) in &data.phases {
self.apply_controlled_phase_dist(&[control, target], phase);
}
Ok(())
}
Gate::BatchRzz(data) => {
for &(q0, q1, theta) in &data.edges {
self.apply_rzz_dist(q0, q1, theta);
}
Ok(())
}
Gate::DiagonalBatch(data) => {
for entry in &data.entries {
self.apply_diag_entry_dist(entry);
}
Ok(())
}
Gate::Unitary(_) => Err(self.unsupported(&format!(
"dense multi-qubit gate `{}` on more qubits than one rank holds locally",
gate.name()
))),
_ => Err(self.unsupported("gate spanning a global qubit")),
}
}
fn apply_diag_entry_dist(&mut self, entry: &crate::gates::DiagEntry) {
use crate::gates::DiagEntry;
match *entry {
DiagEntry::Phase1q { qubit, d0, d1 } => {
if qubit < self.local_qubits() {
self.inner
.apply_1q_matrix(
qubit,
&[
[d0, Complex64::new(0.0, 0.0)],
[Complex64::new(0.0, 0.0), d1],
],
)
.expect("local diagonal 1q");
} else {
self.apply_global_diagonal_1q(qubit, d0, d1);
}
}
DiagEntry::Phase2q { q0, q1, phase } => {
self.apply_controlled_phase_dist(&[q0, q1], phase);
}
DiagEntry::Parity2q {
q0, q1, same, diff, ..
} => {
self.apply_rzz_phases_dist(q0, q1, same, diff);
}
}
}
fn prob_outcome_global(&self, qubit: usize, outcome: bool) -> f64 {
let norm_sq = self.inner.pending_norm * self.inner.pending_norm;
let local_prob = if qubit < self.local_qubits() {
half_norm_sqr(&self.inner.state, qubit, outcome)
} else if self.rank_bit_set(qubit) == outcome {
crate::backend::state_norm_sqr(&self.inner.state)
} else {
0.0
};
self.context.comm().allreduce_sum_f64(local_prob) * norm_sq
}
fn prob_one_global(&self, qubit: usize) -> f64 {
self.prob_outcome_global(qubit, true)
}
fn measure_dist(&mut self, qubit: usize, classical_bit: usize) {
let qubit = self.physical_qubit(qubit);
let prob_one = self.prob_one_global(qubit);
let outcome = self.meas_rng.random::<f64>() < prob_one;
self.inner.classical_bits[classical_bit] = outcome;
self.collapse(qubit, outcome);
self.inner.pending_norm *= measurement_inv_norm(outcome, prob_one);
}
#[inline]
fn physical_qubit(&self, qubit: usize) -> usize {
if self.map_identity {
qubit
} else {
self.qubit_map[qubit]
}
}
pub fn sample_state_indices(&mut self, num_shots: usize, seed: u64) -> Result<Vec<u64>> {
if num_shots == 0 {
return Ok(Vec::new());
}
self.restore_identity_map();
debug_assert!(self.map_identity);
let mut local_cdf = self.inner.host_probability_vector();
let mut acc = 0.0f64;
for p in &mut local_cdf {
acc += *p;
*p = acc;
}
let masses = self.context.comm().allgather_f64(&[acc]);
let mut rank_cdf = Vec::with_capacity(masses.len());
let mut total = 0.0f64;
for &m in &masses {
total += m;
rank_cdf.push(total);
}
if let Some(last) = rank_cdf.last_mut() {
*last = 1.0;
}
let rank = self.context.rank();
let size = self.context.size();
let local_qubits = self.local_qubits();
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut owners = Vec::with_capacity(num_shots);
let mut counts = vec![0usize; size];
let mut owned = Vec::new();
for _ in 0..num_shots {
let r: f64 = rng.random();
let owner = rank_cdf.partition_point(|&c| c < r);
owners.push(owner);
counts[owner] += 1;
if owner != rank {
continue;
}
let residual = if owner == 0 {
r
} else {
r - rank_cdf[owner - 1]
};
let local_idx = crate::sim::shots::sample_from_cdf(&local_cdf, residual);
owned.push(((rank as u64) << local_qubits) | local_idx as u64);
}
let gathered = self.context.comm().allgatherv_u64(&owned, &counts);
let mut next = vec![0usize; size];
for r in 1..size {
next[r] = next[r - 1] + counts[r - 1];
}
let indices = owners
.iter()
.map(|&owner| {
let i = next[owner];
next[owner] += 1;
gathered[i]
})
.collect();
Ok(indices)
}
fn collapse(&mut self, qubit: usize, outcome: bool) {
if qubit < self.local_qubits() {
fn dropped(block: &mut [Complex64], half: usize, outcome: bool) -> &mut [Complex64] {
let (lo, hi) = block.split_at_mut(half);
if outcome { lo } else { hi }
}
let half = 1usize << qubit;
let block_size = half << 1;
#[cfg(feature = "parallel")]
if self.inner.state.len() >= PAR_SHARD_LEN {
if self.inner.state.len() / block_size >= 4 {
self.inner
.state
.par_chunks_mut(block_size)
.with_min_len(chunk_min_len(block_size))
.for_each(|block| simd::zero_slice(dropped(block, half, outcome)));
} else {
for block in self.inner.state.chunks_mut(block_size) {
dropped(block, half, outcome)
.par_chunks_mut(MIN_PAR_ELEMS)
.for_each(simd::zero_slice);
}
}
return;
}
for block in self.inner.state.chunks_mut(block_size) {
simd::zero_slice(dropped(block, half, outcome));
}
} else if self.rank_bit_set(qubit) != outcome {
zero_shard(&mut self.inner.state);
}
}
fn reset_dist(&mut self, qubit: usize) -> Result<()> {
let physical = self.physical_qubit(qubit);
let prob_one = self.prob_one_global(physical);
let outcome = self.meas_rng.random::<f64>() < prob_one;
self.collapse(physical, outcome);
self.inner.pending_norm *= measurement_inv_norm(outcome, prob_one);
if outcome {
self.apply_gate(&Gate::X, &[qubit])?;
}
Ok(())
}
fn apply_gate(&mut self, gate: &Gate, targets: &[usize]) -> Result<()> {
if self.global_qubits == 0 {
self.inner.dispatch_gate(gate, targets);
return Ok(());
}
if matches!(gate, Gate::MultiPauliRot(_)) {
return Err(self.unsupported("Pauli rotation batch over a sharded state"));
}
if self.relabel {
self.touch_instruction(gate, targets);
if matches!(gate, Gate::Swap) {
self.swap_circuit_qubits(targets[0], targets[1]);
return Ok(());
}
let req = required_local_qubits(gate, targets);
if !req.is_empty() {
self.make_local(&req);
}
}
if matches!(gate, Gate::QftBlock { .. }) && !self.map_identity {
return Err(self.unsupported("QftBlock with a permuted qubit map"));
}
let (pgate, ptargets) = self.to_physical(gate, targets);
if self.instruction_qubits_local(&pgate, &ptargets) {
self.inner.dispatch_gate(pgate.as_ref(), &ptargets);
return Ok(());
}
let pgate = pgate.as_ref();
if pgate.num_qubits() == 1 {
let target = ptargets[0];
let mat = pgate.matrix_2x2();
if pgate.is_diagonal_1q() {
self.apply_global_diagonal_1q(target, mat[0][0], mat[1][1]);
} else {
self.apply_global_1q(target, mat);
}
return Ok(());
}
self.apply_global_multi_qubit(pgate, &ptargets)
}
fn unsupported(&self, operation: &str) -> PrismError {
PrismError::BackendUnsupported {
backend: BACKEND_NAME.to_string(),
operation: operation.to_string(),
}
}
}
impl crate::backend::sealed::Sealed for DistributedStatevectorBackend {}
impl Backend for DistributedStatevectorBackend {
fn name(&self) -> &'static str {
BACKEND_NAME
}
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn resolved(&self) -> crate::sim::ResolvedBackend {
crate::sim::ResolvedBackend::Distributed
}
fn supports_fused_gates(&self) -> bool {
true
}
fn supports_qft_block(&self) -> bool {
self.is_single_rank() && self.inner.supports_qft_block()
}
fn supports_pauli_rotation(&self) -> bool {
self.is_single_rank() && self.inner.supports_pauli_rotation()
}
fn apply_instructions(&mut self, instructions: &[Instruction]) -> Result<()> {
if std::mem::take(&mut self.circuit_check_pending) && self.context.size() > 1 {
self.check_circuit_agreement(instructions)?;
}
if self.relabel && self.global_qubits > 0 {
return self.apply_planned(instructions);
}
for instruction in instructions {
self.apply(instruction)?;
}
Ok(())
}
fn init(&mut self, num_qubits: usize, num_classical_bits: usize) -> Result<()> {
let local_qubits = self.prepare_shard(num_qubits, num_classical_bits)?;
self.inner.init(local_qubits, num_classical_bits)?;
if self.context.rank() != 0
&& let Some(amp) = self.inner.state.get_mut(0)
{
*amp = Complex64::new(0.0, 0.0);
}
Ok(())
}
fn supports_initial_state(&self) -> bool {
true
}
fn init_from_amplitudes(
&mut self,
amplitudes: Vec<Complex64>,
num_classical_bits: usize,
) -> Result<()> {
crate::backend::validate_initial_amplitudes(&litudes)?;
let num_qubits = amplitudes.len().trailing_zeros() as usize;
let local_qubits = self.prepare_shard(num_qubits, num_classical_bits)?;
if self.is_single_rank() {
return self.inner.init_from_state(amplitudes, num_classical_bits);
}
self.inner.init(local_qubits, num_classical_bits)?;
let len = 1usize << local_qubits;
let start = self.context.rank() * len;
self.inner
.state
.copy_from_slice(&litudes[start..start + len]);
Ok(())
}
fn apply(&mut self, instruction: &Instruction) -> Result<()> {
match instruction {
Instruction::Measure {
qubit,
classical_bit,
} => {
self.measure_dist(*qubit, *classical_bit);
Ok(())
}
Instruction::Reset { qubit } => self.reset_dist(*qubit),
Instruction::Barrier { .. } => Ok(()),
Instruction::Save { label, .. } => Err(crate::backend::save_not_applied(
"StatevectorDistributed",
label,
)),
Instruction::Conditional {
condition,
gate,
targets,
} => {
if condition.evaluate(self.inner.classical_results()) {
self.apply_gate(gate, targets)
} else {
Ok(())
}
}
Instruction::Gate { gate, targets } => self.apply_gate(gate, targets),
Instruction::Region(region) => self.apply_region(region),
}
}
fn classical_results(&self) -> &[bool] {
self.inner.classical_results()
}
fn probabilities(&self) -> Result<Vec<f64>> {
let local = self.inner.probabilities()?;
if self.global_qubits == 0 {
return Ok(local);
}
dense_probability_len(BACKEND_NAME, self.num_qubits)?;
let gathered = self.context.comm().allgather_f64(&local);
Ok(self.unpermuted(gathered))
}
fn num_qubits(&self) -> usize {
self.num_qubits
}
fn export_statevector(&self) -> Result<Vec<Complex64>> {
let local = self.inner.export_statevector()?;
if self.global_qubits == 0 {
return Ok(local);
}
dense_statevector_len(BACKEND_NAME, "statevector export", self.num_qubits)?;
let gathered = self.context.comm().allgather_c64(&local);
Ok(self.unpermuted(gathered))
}
fn qubit_probability(&self, qubit: usize) -> Result<f64> {
Ok(self.prob_one_global(self.physical_qubit(qubit)))
}
fn supports_native_sampling(&self) -> bool {
true
}
fn sample_basis_states(&mut self, num_shots: usize, seed: u64) -> Result<BasisSamples> {
let indices = self.sample_state_indices(num_shots, seed)?;
let mut samples = BasisSamples::new(num_shots, self.num_qubits);
for (shot, &index) in indices.iter().enumerate() {
samples.set_index(shot, index as usize);
}
Ok(samples)
}
fn supports_pauli_expectation(&self) -> bool {
true
}
fn pauli_expectations(&self, observables: &[Vec<PauliTerm>]) -> Result<Vec<f64>> {
let comm = self.context.comm();
let norm = comm.allreduce_sum_f64(crate::backend::state_norm_sqr(&self.inner.state));
let local_qubits = self.local_qubits();
let local_mask = (1usize << local_qubits) - 1;
let mut recv: Vec<Complex64> = Vec::new();
let mut values = Vec::with_capacity(observables.len());
for observable in observables {
let (xmask, zmask, num_y) = crate::sim::pauli_masks(observable, self.num_qubits)?;
let xphys = self.to_physical_mask(xmask);
let zphys = self.to_physical_mask(zmask);
let partner_bits = xphys >> local_qubits;
if partner_bits != 0 {
recv.resize(self.inner.state.len(), Complex64::new(0.0, 0.0));
comm.sendrecv_c64(
self.context.rank() ^ partner_bits,
&self.inner.state,
&mut recv,
);
}
let bra: &[Complex64] = if partner_bits == 0 {
&self.inner.state
} else {
&recv
};
let sandwich = crate::sim::pauli_sandwich(
bra,
&self.inner.state,
xphys & local_mask,
zphys & local_mask,
num_y,
);
let rank_parity = (self.context.rank() & (zphys >> local_qubits)).count_ones() & 1;
let signed = if rank_parity == 1 {
-sandwich.re
} else {
sandwich.re
};
let total = comm.allreduce_sum_f64(signed);
values.push(if norm == 0.0 { 0.0 } else { total / norm });
}
Ok(values)
}
fn reset(&mut self, qubit: usize) -> Result<()> {
self.reset_dist(qubit)
}
fn apply_1q_matrix(&mut self, qubit: usize, matrix: &[[Complex64; 2]; 2]) -> Result<()> {
if self.global_qubits == 0 {
return self.inner.apply_1q_matrix(qubit, matrix);
}
let diagonal = is_diagonal_2x2(matrix);
if self.relabel {
self.tick += 1;
self.last_used[qubit] = self.tick;
if !diagonal {
self.make_local(&[qubit]);
}
}
let target = self.physical_qubit(qubit);
if target < self.local_qubits() {
return self.inner.apply_1q_matrix(target, matrix);
}
if diagonal {
self.apply_global_diagonal_1q(target, matrix[0][0], matrix[1][1]);
} else {
self.apply_global_1q(target, *matrix);
}
Ok(())
}
}