1mod plan;
116#[cfg(test)]
117mod tests;
118#[cfg(any(test, feature = "bench-internal"))]
119pub mod tiled;
120
121use std::borrow::Cow;
122use std::sync::Arc;
123
124use num_complex::Complex64;
125use rand::{RngExt, SeedableRng};
126use rand_chacha::ChaCha8Rng;
127
128use crate::backend::simd;
129#[cfg(feature = "parallel")]
130use crate::backend::statevector::SendPtr;
131use crate::backend::statevector::StatevectorBackend;
132use crate::backend::{
133 Backend, BasisSamples, dense_probability_len, dense_statevector_len, measurement_inv_norm,
134};
135#[cfg(feature = "parallel")]
136use crate::backend::{
137 MIN_PAR_ELEMS, MIN_PAR_REDUCE_ELEMS, PARALLEL_THRESHOLD_QUBITS, chunk_min_len,
138};
139use crate::circuit::{Instruction, SmallVec, smallvec};
140use crate::distributed::DistributedContext;
141use crate::error::{PrismError, Result};
142use crate::gates::{DiagEntry, Gate, is_diagonal_2x2};
143use crate::sim::unified_pauli::PauliTerm;
144#[cfg(feature = "parallel")]
145use rayon::prelude::*;
146
147const BACKEND_NAME: &str = "distributed_statevector";
148
149#[cfg(feature = "parallel")]
152const PAR_SHARD_LEN: usize = 1 << PARALLEL_THRESHOLD_QUBITS;
153
154fn scale_shard(state: &mut [Complex64], factor: Complex64) {
155 #[cfg(feature = "parallel")]
156 if state.len() >= PAR_SHARD_LEN {
157 state
158 .par_chunks_mut(MIN_PAR_ELEMS)
159 .for_each(|tile| simd::scale_complex_slice(tile, factor));
160 return;
161 }
162 simd::scale_complex_slice(state, factor);
163}
164
165fn scale_shard_masked(state: &mut [Complex64], mask: usize, factor: Complex64) {
167 let tile = |base: usize, tile: &mut [Complex64]| {
168 for (k, amp) in tile.iter_mut().enumerate() {
169 if (base + k) & mask == mask {
170 *amp *= factor;
171 }
172 }
173 };
174 #[cfg(feature = "parallel")]
175 if state.len() >= PAR_SHARD_LEN {
176 state
177 .par_chunks_mut(MIN_PAR_ELEMS)
178 .enumerate()
179 .for_each(|(t, chunk)| tile(t * MIN_PAR_ELEMS, chunk));
180 return;
181 }
182 tile(0, state);
183}
184
185fn zero_shard(state: &mut [Complex64]) {
186 #[cfg(feature = "parallel")]
187 if state.len() >= PAR_SHARD_LEN {
188 state
189 .par_chunks_mut(MIN_PAR_ELEMS)
190 .for_each(simd::zero_slice);
191 return;
192 }
193 simd::zero_slice(state);
194}
195
196fn combine_shard(
198 dst: &mut [Complex64],
199 remote: &[Complex64],
200 c_self: Complex64,
201 c_remote: Complex64,
202) {
203 #[cfg(feature = "parallel")]
204 if dst.len() >= PAR_SHARD_LEN {
205 dst.par_chunks_mut(MIN_PAR_ELEMS)
206 .zip(remote.par_chunks(MIN_PAR_ELEMS))
207 .for_each(|(d, r)| simd::combine_global_half(d, r, c_self, c_remote));
208 return;
209 }
210 simd::combine_global_half(dst, remote, c_self, c_remote);
211}
212
213fn gather_indexed(
215 pack: &mut [Complex64],
216 state: &[Complex64],
217 index_of: impl Fn(usize) -> usize + Sync,
218) {
219 #[cfg(feature = "parallel")]
220 if pack.len() >= PAR_SHARD_LEN {
221 pack.par_chunks_mut(MIN_PAR_ELEMS)
222 .enumerate()
223 .for_each(|(t, tile)| {
224 let base = t * MIN_PAR_ELEMS;
225 for (k, slot) in tile.iter_mut().enumerate() {
226 *slot = state[index_of(base + k)];
227 }
228 });
229 return;
230 }
231 for (k, slot) in pack.iter_mut().enumerate() {
232 *slot = state[index_of(k)];
233 }
234}
235
236fn scatter_indexed(
240 state: &mut [Complex64],
241 recv: &[Complex64],
242 index_of: impl Fn(usize) -> usize + Sync,
243 f: impl Fn(Complex64, Complex64) -> Complex64 + Sync,
244) {
245 #[cfg(feature = "parallel")]
246 if recv.len() >= PAR_SHARD_LEN {
247 let ptr = SendPtr(state.as_mut_ptr());
248 recv.par_chunks(MIN_PAR_ELEMS)
249 .enumerate()
250 .for_each(|(t, tile)| {
251 let base = t * MIN_PAR_ELEMS;
252 for (k, &r) in tile.iter().enumerate() {
253 let i = index_of(base + k);
254 unsafe { ptr.store(i, f(ptr.load(i), r)) };
258 }
259 });
260 return;
261 }
262 for (k, &r) in recv.iter().enumerate() {
263 let i = index_of(k);
264 state[i] = f(state[i], r);
265 }
266}
267
268fn for_each_pair_tile<F>(state: &mut [Complex64], recv: &mut [Complex64], local_q: usize, f: F)
272where
273 F: Fn(&mut [Complex64], &mut [Complex64], &mut [Complex64], &mut [Complex64]) + Sync,
274{
275 let half = 1usize << local_q;
276 let block = half << 1;
277 #[cfg(feature = "parallel")]
278 if state.len() >= PAR_SHARD_LEN {
279 if state.len() / block >= 4 {
280 state
281 .par_chunks_mut(block)
282 .zip(recv.par_chunks_mut(block))
283 .with_min_len(chunk_min_len(block))
284 .for_each(|(s, r)| {
285 let (lo, hi) = s.split_at_mut(half);
286 let (rlo, rhi) = r.split_at_mut(half);
287 f(lo, hi, rlo, rhi);
288 });
289 } else {
290 for (s, r) in state.chunks_mut(block).zip(recv.chunks_mut(block)) {
291 let (lo, hi) = s.split_at_mut(half);
292 let (rlo, rhi) = r.split_at_mut(half);
293 lo.par_chunks_mut(MIN_PAR_ELEMS)
294 .zip(hi.par_chunks_mut(MIN_PAR_ELEMS))
295 .zip(rlo.par_chunks_mut(MIN_PAR_ELEMS))
296 .zip(rhi.par_chunks_mut(MIN_PAR_ELEMS))
297 .for_each(|(((lo, hi), rlo), rhi)| f(lo, hi, rlo, rhi));
298 }
299 }
300 return;
301 }
302 for (s, r) in state.chunks_mut(block).zip(recv.chunks_mut(block)) {
303 let (lo, hi) = s.split_at_mut(half);
304 let (rlo, rhi) = r.split_at_mut(half);
305 f(lo, hi, rlo, rhi);
306 }
307}
308
309fn butterfly_shard(
313 state: &mut [Complex64],
314 remote: &[Complex64],
315 pack: &mut [Complex64],
316 keep: [Complex64; 2],
317 forward: [Complex64; 2],
318) {
319 let tile = |state: &mut [Complex64], remote: &[Complex64], pack: &mut [Complex64]| {
320 for ((s, &r), p) in state.iter_mut().zip(remote).zip(pack.iter_mut()) {
321 let own = *s;
322 *p = forward[0] * own + forward[1] * r;
323 *s = keep[0] * own + keep[1] * r;
324 }
325 };
326 #[cfg(feature = "parallel")]
327 if state.len() >= PAR_SHARD_LEN {
328 state
329 .par_chunks_mut(MIN_PAR_ELEMS)
330 .zip(remote.par_chunks(MIN_PAR_ELEMS))
331 .zip(pack.par_chunks_mut(MIN_PAR_ELEMS))
332 .for_each(|((s, r), p)| tile(s, r, p));
333 return;
334 }
335 tile(state, remote, pack);
336}
337
338fn add_shard(dst: &mut [Complex64], src: &[Complex64]) {
340 let tile = |dst: &mut [Complex64], src: &[Complex64]| {
341 for (d, &s) in dst.iter_mut().zip(src) {
342 *d += s;
343 }
344 };
345 #[cfg(feature = "parallel")]
346 if dst.len() >= PAR_SHARD_LEN {
347 dst.par_chunks_mut(MIN_PAR_ELEMS)
348 .zip(src.par_chunks(MIN_PAR_ELEMS))
349 .for_each(|(d, s)| tile(d, s));
350 return;
351 }
352 tile(dst, src);
353}
354
355fn half_norm_sqr(state: &[Complex64], qubit: usize, outcome: bool) -> f64 {
358 fn select(block: &[Complex64], half: usize, outcome: bool) -> &[Complex64] {
359 if outcome {
360 &block[half..]
361 } else {
362 &block[..half]
363 }
364 }
365 let half = 1usize << qubit;
366 let block = half << 1;
367 #[cfg(feature = "parallel")]
368 if state.len() >= MIN_PAR_REDUCE_ELEMS {
369 if state.len() / block >= 4 {
370 return state
371 .par_chunks(block)
372 .with_min_len(chunk_min_len(block))
373 .map(|b| simd::norm_sqr_sum(select(b, half, outcome)))
374 .sum();
375 }
376 return state
377 .chunks(block)
378 .map(|b| {
379 select(b, half, outcome)
380 .par_chunks(MIN_PAR_ELEMS)
381 .map(simd::norm_sqr_sum)
382 .sum::<f64>()
383 })
384 .sum();
385 }
386 state
387 .chunks(block)
388 .map(|b| simd::norm_sqr_sum(select(b, half, outcome)))
389 .sum()
390}
391
392fn for_each_gate_qubit(gate: &Gate, targets: &[usize], mut f: impl FnMut(usize)) {
395 for &q in targets {
396 f(q);
397 }
398 match gate {
399 Gate::BatchPhase(data) => {
400 for &(target, _) in &data.phases {
401 f(target);
402 }
403 }
404 Gate::BatchRzz(data) => {
405 for &(q0, q1, _) in &data.edges {
406 f(q0);
407 f(q1);
408 }
409 }
410 Gate::MultiFused(data) => {
411 for &(q, _) in &data.gates {
412 f(q);
413 }
414 }
415 Gate::Multi2q(data) => {
416 for &(q0, q1, _) in &data.gates {
417 f(q0);
418 f(q1);
419 }
420 }
421 Gate::DiagonalBatch(data) => {
422 for entry in &data.entries {
423 match *entry {
424 DiagEntry::Phase1q { qubit, .. } => f(qubit),
425 DiagEntry::Phase2q { q0, q1, .. } | DiagEntry::Parity2q { q0, q1, .. } => {
426 f(q0);
427 f(q1);
428 }
429 }
430 }
431 }
432 _ => {}
433 }
434}
435
436fn required_local_qubits(gate: &Gate, targets: &[usize]) -> SmallVec<[usize; 8]> {
440 let mut req: SmallVec<[usize; 8]> = SmallVec::new();
441 fn push(req: &mut SmallVec<[usize; 8]>, q: usize) {
442 if !req.contains(&q) {
443 req.push(q);
444 }
445 }
446 match gate {
447 Gate::Cx => push(&mut req, targets[1]),
448 Gate::Cz
449 | Gate::Swap
450 | Gate::Rzz(_)
451 | Gate::BatchPhase(_)
452 | Gate::BatchRzz(_)
453 | Gate::DiagonalBatch(_) => {}
454 Gate::Cu(_) | Gate::Mcu(_) => {
455 if gate.controlled_phase().is_none() {
456 let (target, mat) = match gate {
457 Gate::Mcu(data) => (targets[data.num_controls as usize], &data.mat),
458 Gate::Cu(mat) => (targets[1], &**mat),
459 _ => unreachable!("outer match arm is Cu | Mcu"),
460 };
461 if !is_diagonal_2x2(mat) {
462 push(&mut req, target);
463 }
464 }
465 }
466 Gate::Fused2q(_) => {
467 push(&mut req, targets[0]);
468 push(&mut req, targets[1]);
469 }
470 Gate::Multi2q(data) => {
471 for &(q0, q1, _) in &data.gates {
472 push(&mut req, q0);
473 push(&mut req, q1);
474 }
475 }
476 Gate::MultiFused(data) => {
477 for &(q, ref mat) in &data.gates {
478 if !is_diagonal_2x2(mat) {
479 push(&mut req, q);
480 }
481 }
482 }
483 g if g.num_qubits() == 1 && !g.is_diagonal_1q() => {
484 push(&mut req, targets[0]);
485 }
486 _ => {}
487 }
488 req
489}
490
491pub struct DistributedStatevectorBackend {
493 context: Arc<DistributedContext>,
494 inner: StatevectorBackend,
495 num_qubits: usize,
496 global_qubits: usize,
497 recv: Vec<Complex64>,
500 seed: u64,
501 exchange_chunk: usize,
505 exchange_messages: u64,
509 exchange_amplitudes: u64,
510 meas_rng: ChaCha8Rng,
514 qubit_map: Vec<usize>,
518 phys_map: Vec<usize>,
520 map_identity: bool,
522 relabel: bool,
525 last_used: Vec<u64>,
528 tick: u64,
529 pack: Vec<Complex64>,
532 circuit_check_pending: bool,
535}
536
537impl DistributedStatevectorBackend {
538 pub fn new(context: Arc<DistributedContext>, seed: u64) -> Self {
540 Self {
541 context,
542 inner: StatevectorBackend::new(seed),
543 num_qubits: 0,
544 global_qubits: 0,
545 recv: Vec::new(),
546 seed,
547 exchange_chunk: crate::distributed::exchange_chunk(),
548 exchange_messages: 0,
549 exchange_amplitudes: 0,
550 meas_rng: ChaCha8Rng::seed_from_u64(seed),
551 qubit_map: Vec::new(),
552 phys_map: Vec::new(),
553 map_identity: true,
554 relabel: crate::distributed::relabel_enabled(),
555 last_used: Vec::new(),
556 tick: 0,
557 pack: Vec::new(),
558 circuit_check_pending: true,
559 }
560 }
561
562 #[cfg(test)]
565 pub(crate) fn set_exchange_chunk(&mut self, chunk: usize) {
566 self.exchange_chunk = chunk.max(1);
567 }
568
569 pub fn set_relabel(&mut self, enabled: bool) {
573 self.relabel = enabled;
574 }
575
576 pub fn exchange_messages(&self) -> u64 {
583 self.exchange_messages
584 }
585
586 pub fn exchange_amplitudes(&self) -> u64 {
588 self.exchange_amplitudes
589 }
590
591 #[inline]
593 fn count_exchange(&mut self, amplitudes: usize) {
594 self.exchange_messages += 1;
595 self.exchange_amplitudes += amplitudes as u64;
596 }
597
598 #[inline]
602 fn ensure_recv(&mut self, len: usize) {
603 if self.recv.len() < len {
604 self.recv.resize(len, Complex64::new(0.0, 0.0));
605 }
606 }
607
608 #[inline]
610 fn ensure_pack(&mut self, len: usize) {
611 if self.pack.len() < len {
612 self.pack.resize(len, Complex64::new(0.0, 0.0));
613 }
614 }
615
616 #[inline]
619 fn block_chunk(&self, block_bits: usize, len: usize) -> usize {
620 let block = 1usize << block_bits;
621 ((self.exchange_chunk / block).max(1) * block).min(len)
622 }
623
624 #[inline]
625 fn local_qubits(&self) -> usize {
626 self.num_qubits - self.global_qubits
627 }
628
629 #[inline]
630 fn is_single_rank(&self) -> bool {
631 self.context.size() == 1
632 }
633
634 #[inline]
636 fn global_bit(&self, q: usize) -> usize {
637 q - self.local_qubits()
638 }
639
640 #[inline]
642 fn rank_bit_set(&self, q: usize) -> bool {
643 (self.context.rank() >> self.global_bit(q)) & 1 == 1
644 }
645
646 fn touch_instruction(&mut self, gate: &Gate, targets: &[usize]) {
651 self.tick += 1;
652 let tick = self.tick;
653 for_each_gate_qubit(gate, targets, |q| self.last_used[q] = tick);
654 }
655
656 fn refresh_map_identity(&mut self) {
657 self.map_identity = self.qubit_map.iter().enumerate().all(|(q, &p)| q == p);
658 }
659
660 fn swap_circuit_qubits(&mut self, a: usize, b: usize) {
663 if a == b {
664 return;
665 }
666 let pa = self.qubit_map[a];
667 let pb = self.qubit_map[b];
668 self.qubit_map.swap(a, b);
669 self.phys_map.swap(pa, pb);
670 self.refresh_map_identity();
671 }
672
673 fn pick_victim(&self) -> Option<usize> {
677 let local = self.local_qubits();
678 let mut best: Option<(u64, usize)> = None;
679 for pos in 0..local {
680 let used = self.last_used[self.phys_map[pos]];
681 if used == self.tick {
682 continue;
683 }
684 match best {
685 Some((b, _)) if used >= b => {}
686 _ => best = Some((used, pos)),
687 }
688 }
689 best.map(|(_, pos)| pos)
690 }
691
692 fn make_local(&mut self, req: &[usize]) {
698 for &q in req {
699 let pos = self.qubit_map[q];
700 if pos < self.local_qubits() {
701 continue;
702 }
703 let Some(victim) = self.pick_victim() else {
704 return;
705 };
706 self.relabel_swap(victim, pos);
707 }
708 }
709
710 fn half_slice_swap(&mut self, local_pos: usize, global_pos: usize) {
719 let partner = self.context.rank() ^ (1usize << self.global_bit(global_pos));
720 let gbit = self.rank_bit_set(global_pos);
721 let stride = 1usize << local_pos;
722 let fixed = if gbit { 0 } else { stride };
723 let moving = self.inner.state.len() / 2;
724 let chunk = self.exchange_chunk.min(moving).max(1);
725 self.ensure_pack(chunk);
726 self.ensure_recv(chunk);
727 let index_of =
728 |flat: usize| ((flat >> local_pos) << (local_pos + 1)) | fixed | (flat & (stride - 1));
729 let mut off = 0;
730 while off < moving {
731 let count = (off + chunk).min(moving) - off;
732 gather_indexed(&mut self.pack[..count], &self.inner.state, |k| {
733 index_of(off + k)
734 });
735 self.count_exchange(count);
736 self.context
737 .comm()
738 .sendrecv_c64(partner, &self.pack[..count], &mut self.recv[..count]);
739 scatter_indexed(
740 &mut self.inner.state,
741 &self.recv[..count],
742 |k| index_of(off + k),
743 |_, remote| remote,
744 );
745 off += count;
746 }
747 }
748
749 fn relabel_swap(&mut self, local_pos: usize, global_pos: usize) {
752 self.half_slice_swap(local_pos, global_pos);
753
754 let local_q = self.phys_map[local_pos];
755 let global_q = self.phys_map[global_pos];
756 self.qubit_map[local_q] = global_pos;
757 self.qubit_map[global_q] = local_pos;
758 self.phys_map.swap(local_pos, global_pos);
759 self.refresh_map_identity();
760 }
761
762 fn swap_physical_positions(&mut self, a: usize, b: usize) {
768 debug_assert!(
769 a < b,
770 "positions must be ordered: branch selection assumes a < b"
771 );
772 let local = self.local_qubits();
773 if b < local {
774 self.inner
775 .apply(&Instruction::Gate {
776 gate: Gate::Swap,
777 targets: smallvec![a, b],
778 })
779 .expect("local SWAP cannot fail");
780 } else if a < local {
781 self.relabel_swap(a, b);
782 return;
783 } else {
784 self.swap_global_slices(a, b);
785 }
786 let qa = self.phys_map[a];
787 let qb = self.phys_map[b];
788 self.qubit_map[qa] = b;
789 self.qubit_map[qb] = a;
790 self.phys_map.swap(a, b);
791 self.refresh_map_identity();
792 }
793
794 fn restore_identity_map(&mut self) {
799 while !self.map_identity {
800 let Some(pos) = (0..self.num_qubits).find(|&p| self.phys_map[p] != p) else {
801 break;
802 };
803 let src = self.qubit_map[pos];
804 self.swap_physical_positions(pos, src);
805 }
806 }
807
808 fn to_physical<'g>(
812 &self,
813 gate: &'g Gate,
814 targets: &[usize],
815 ) -> (Cow<'g, Gate>, SmallVec<[usize; 4]>) {
816 if self.map_identity {
817 return (Cow::Borrowed(gate), targets.into());
818 }
819 let ptargets: SmallVec<[usize; 4]> = targets.iter().map(|&q| self.qubit_map[q]).collect();
820 let fixed = |q: usize| self.qubit_map[q] == q;
824 let pgate = match gate {
825 Gate::MultiFused(data) => {
826 if data.gates.iter().all(|&(q, _)| fixed(q)) {
827 Cow::Borrowed(gate)
828 } else {
829 let mut data = data.clone();
830 for entry in &mut data.gates {
831 entry.0 = self.qubit_map[entry.0];
832 }
833 Cow::Owned(Gate::MultiFused(data))
834 }
835 }
836 Gate::Multi2q(data) => {
837 if data.gates.iter().all(|&(q0, q1, _)| fixed(q0) && fixed(q1)) {
838 Cow::Borrowed(gate)
839 } else {
840 let mut data = data.clone();
841 for entry in &mut data.gates {
842 entry.0 = self.qubit_map[entry.0];
843 entry.1 = self.qubit_map[entry.1];
844 }
845 Cow::Owned(Gate::Multi2q(data))
846 }
847 }
848 Gate::BatchPhase(data) => {
849 if data.phases.iter().all(|&(q, _)| fixed(q)) {
850 Cow::Borrowed(gate)
851 } else {
852 let mut data = data.clone();
853 for entry in &mut data.phases {
854 entry.0 = self.qubit_map[entry.0];
855 }
856 Cow::Owned(Gate::BatchPhase(data))
857 }
858 }
859 Gate::BatchRzz(data) => {
860 if data.edges.iter().all(|&(q0, q1, _)| fixed(q0) && fixed(q1)) {
861 Cow::Borrowed(gate)
862 } else {
863 let mut data = data.clone();
864 for entry in &mut data.edges {
865 entry.0 = self.qubit_map[entry.0];
866 entry.1 = self.qubit_map[entry.1];
867 }
868 Cow::Owned(Gate::BatchRzz(data))
869 }
870 }
871 Gate::DiagonalBatch(data) => {
872 let entry_fixed = |entry: &DiagEntry| match *entry {
873 DiagEntry::Phase1q { qubit, .. } => fixed(qubit),
874 DiagEntry::Phase2q { q0, q1, .. } | DiagEntry::Parity2q { q0, q1, .. } => {
875 fixed(q0) && fixed(q1)
876 }
877 };
878 if data.entries.iter().all(entry_fixed) {
879 Cow::Borrowed(gate)
880 } else {
881 let mut data = data.clone();
882 for entry in &mut data.entries {
883 match entry {
884 DiagEntry::Phase1q { qubit, .. } => *qubit = self.qubit_map[*qubit],
885 DiagEntry::Phase2q { q0, q1, .. }
886 | DiagEntry::Parity2q { q0, q1, .. } => {
887 *q0 = self.qubit_map[*q0];
888 *q1 = self.qubit_map[*q1];
889 }
890 }
891 }
892 Cow::Owned(Gate::DiagonalBatch(data))
893 }
894 }
895 _ => Cow::Borrowed(gate),
896 };
897 (pgate, ptargets)
898 }
899
900 fn instruction_qubits_local(&self, gate: &Gate, targets: &[usize]) -> bool {
903 let local = self.local_qubits();
904 let mut all = true;
905 for_each_gate_qubit(gate, targets, |q| all &= q < local);
906 all
907 }
908
909 fn config_fingerprint(&self, num_qubits: usize, num_classical_bits: usize) -> u64 {
912 use std::hash::{Hash, Hasher};
913 let mut hasher = std::hash::DefaultHasher::new();
914 (
915 self.seed,
916 self.exchange_chunk,
917 self.relabel,
918 crate::distributed::min_local_qubits(),
919 num_qubits,
920 num_classical_bits,
921 )
922 .hash(&mut hasher);
923 hasher.finish()
924 }
925
926 fn circuit_fingerprint(instructions: &[Instruction]) -> u64 {
934 use std::fmt::Write as _;
935 use std::hash::Hasher;
936
937 struct HashSink<'a>(&'a mut std::hash::DefaultHasher);
938 impl std::fmt::Write for HashSink<'_> {
939 fn write_str(&mut self, s: &str) -> std::fmt::Result {
940 self.0.write(s.as_bytes());
941 Ok(())
942 }
943 }
944
945 let mut hasher = std::hash::DefaultHasher::new();
946 let _ = write!(HashSink(&mut hasher), "{instructions:?}");
947 hasher.finish()
948 }
949
950 fn check_circuit_agreement(&self, instructions: &[Instruction]) -> Result<()> {
961 let local = Self::circuit_fingerprint(instructions);
962 let all = self.context.comm().allgather_u64(&[local]);
963 match all.iter().position(|&other| other != local) {
964 None => Ok(()),
965 Some(other) => Err(PrismError::BackendUnsupported {
966 backend: BACKEND_NAME.to_string(),
967 operation: format!(
968 "the circuit on rank {} differs from rank {other}: every rank enters every \
969 collective, so every rank must run the same circuit",
970 self.context.rank()
971 ),
972 }),
973 }
974 }
975
976 fn check_config_agreement(&self, num_qubits: usize, num_classical_bits: usize) -> Result<()> {
983 let local = self.config_fingerprint(num_qubits, num_classical_bits);
984 let all = self.context.comm().allgather_u64(&[local]);
985 match all.iter().position(|&other| other != local) {
986 None => Ok(()),
987 Some(other) => Err(PrismError::BackendUnsupported {
988 backend: BACKEND_NAME.to_string(),
989 operation: format!(
990 "configuration on rank {} differs from rank {other}: seed, relabel mode, \
991 exchange chunk, local qubit floor, and register shape must be identical \
992 on every rank",
993 self.context.rank()
994 ),
995 }),
996 }
997 }
998
999 fn prepare_shard(&mut self, num_qubits: usize, num_classical_bits: usize) -> Result<usize> {
1004 let size = self.context.size();
1005 if size > 1 {
1009 self.check_config_agreement(num_qubits, num_classical_bits)?;
1010 }
1011 self.circuit_check_pending = true;
1012 if !size.is_power_of_two() {
1013 return Err(PrismError::BackendUnsupported {
1014 backend: BACKEND_NAME.to_string(),
1015 operation: format!("rank count {size} is not a power of two"),
1016 });
1017 }
1018 let p = size.trailing_zeros() as usize;
1019 let min_local = crate::distributed::min_local_qubits();
1020 if size > 1 && num_qubits < p + min_local {
1021 return Err(PrismError::BackendUnsupported {
1022 backend: BACKEND_NAME.to_string(),
1023 operation: format!(
1024 "{num_qubits} qubits across {size} ranks leaves fewer than \
1025 {min_local} local qubits per rank"
1026 ),
1027 });
1028 }
1029
1030 self.num_qubits = num_qubits;
1031 self.global_qubits = p;
1032 self.meas_rng = ChaCha8Rng::seed_from_u64(self.seed);
1033 self.exchange_messages = 0;
1034 self.exchange_amplitudes = 0;
1035 self.qubit_map = (0..num_qubits).collect();
1036 self.phys_map = (0..num_qubits).collect();
1037 self.map_identity = true;
1038 self.last_used = vec![0; num_qubits];
1039 self.tick = 0;
1040 Ok(num_qubits - p)
1041 }
1042
1043 fn to_physical_mask(&self, mask: usize) -> usize {
1045 if self.map_identity {
1046 return mask;
1047 }
1048 let mut out = 0usize;
1049 for (q, &pos) in self.qubit_map.iter().enumerate() {
1050 out |= ((mask >> q) & 1) << pos;
1051 }
1052 out
1053 }
1054
1055 fn unpermuted<T: Copy + Default>(&self, phys: Vec<T>) -> Vec<T> {
1057 if self.map_identity {
1058 return phys;
1059 }
1060 let mut out = vec![T::default(); phys.len()];
1061 for (c, slot) in out.iter_mut().enumerate() {
1062 let mut p = 0usize;
1063 for (q, &pos) in self.qubit_map.iter().enumerate() {
1064 p |= ((c >> q) & 1) << pos;
1065 }
1066 *slot = phys[p];
1067 }
1068 out
1069 }
1070
1071 fn apply_global_1q(&mut self, target: usize, mat: [[Complex64; 2]; 2]) {
1079 let partner = self.context.rank() ^ (1usize << self.global_bit(target));
1080 let (c_self, c_remote) = if self.rank_bit_set(target) {
1081 (mat[1][1], mat[1][0])
1082 } else {
1083 (mat[0][0], mat[0][1])
1084 };
1085 let len = self.inner.state.len();
1086 let chunk = self.exchange_chunk.min(len).max(1);
1087 self.ensure_recv(chunk);
1088 let mut off = 0;
1089 while off < len {
1090 let end = (off + chunk).min(len);
1091 self.count_exchange(end - off);
1092 let recv = &mut self.recv[..end - off];
1093 self.context
1094 .comm()
1095 .sendrecv_c64(partner, &self.inner.state[off..end], recv);
1096 combine_shard(&mut self.inner.state[off..end], recv, c_self, c_remote);
1097 off = end;
1098 }
1099 }
1100
1101 fn apply_global_diagonal_1q(&mut self, target: usize, d0: Complex64, d1: Complex64) {
1106 let factor = if self.rank_bit_set(target) { d1 } else { d0 };
1107 scale_shard(&mut self.inner.state, factor);
1108 }
1109
1110 fn apply_local_controlled_1q(
1114 &mut self,
1115 local_controls: &[usize],
1116 target: usize,
1117 mat: [[Complex64; 2]; 2],
1118 ) {
1119 let gate = match local_controls.len() {
1120 0 => {
1121 self.inner
1122 .apply_1q_matrix(target, &mat)
1123 .expect("local 1q matrix");
1124 return;
1125 }
1126 1 => Gate::cu(mat),
1127 n => Gate::mcu(mat, n as u8),
1128 };
1129 let mut targets: SmallVec<[usize; 4]> = local_controls.iter().copied().collect();
1130 targets.push(target);
1131 self.inner
1132 .apply(&Instruction::Gate { gate, targets })
1133 .expect("local controlled 1q");
1134 }
1135
1136 fn apply_global_controlled_1q(
1143 &mut self,
1144 local_controls: &[usize],
1145 target: usize,
1146 mat: [[Complex64; 2]; 2],
1147 ) {
1148 if local_controls.is_empty() {
1149 self.apply_global_1q(target, mat);
1150 return;
1151 }
1152 let partner = self.context.rank() ^ (1usize << self.global_bit(target));
1153 let (c_self, c_remote) = if self.rank_bit_set(target) {
1154 (mat[1][1], mat[1][0])
1155 } else {
1156 (mat[0][0], mat[0][1])
1157 };
1158
1159 let mut ctrl_pos: SmallVec<[usize; 4]> = local_controls.iter().copied().collect();
1160 ctrl_pos.sort_unstable();
1161 let index_of = |flat: usize| {
1162 let mut i = flat;
1163 for &p in &ctrl_pos {
1164 let low = i & ((1usize << p) - 1);
1165 i = ((i >> p) << (p + 1)) | (1usize << p) | low;
1166 }
1167 i
1168 };
1169 let moving = self.inner.state.len() >> ctrl_pos.len();
1170 let chunk = self.exchange_chunk.min(moving).max(1);
1171 self.ensure_pack(chunk);
1172 self.ensure_recv(chunk);
1173 let mut off = 0;
1174 while off < moving {
1175 let count = (off + chunk).min(moving) - off;
1176 gather_indexed(&mut self.pack[..count], &self.inner.state, |k| {
1177 index_of(off + k)
1178 });
1179 self.count_exchange(count);
1180 self.context
1181 .comm()
1182 .sendrecv_c64(partner, &self.pack[..count], &mut self.recv[..count]);
1183 scatter_indexed(
1184 &mut self.inner.state,
1185 &self.recv[..count],
1186 |k| index_of(off + k),
1187 |own, remote| c_self * own + c_remote * remote,
1188 );
1189 off += count;
1190 }
1191 }
1192
1193 fn apply_controlled_dist(
1197 &mut self,
1198 controls: &[usize],
1199 target: usize,
1200 mat: [[Complex64; 2]; 2],
1201 ) {
1202 let local = self.local_qubits();
1203 let mut local_controls: SmallVec<[usize; 4]> = SmallVec::new();
1204 for &c in controls {
1205 if c < local {
1206 local_controls.push(c);
1207 } else if !self.rank_bit_set(c) {
1208 return;
1210 }
1211 }
1212
1213 if target < local {
1214 self.apply_local_controlled_1q(&local_controls, target, mat);
1215 } else if is_diagonal_2x2(&mat) {
1216 let d = if self.rank_bit_set(target) {
1219 mat[1][1]
1220 } else {
1221 mat[0][0]
1222 };
1223 if local_controls.is_empty() {
1224 scale_shard(&mut self.inner.state, d);
1225 return;
1226 }
1227 let ctrl_mask: usize = local_controls.iter().map(|&c| 1usize << c).sum();
1228 scale_shard_masked(&mut self.inner.state, ctrl_mask, d);
1229 } else {
1230 self.apply_global_controlled_1q(&local_controls, target, mat);
1231 }
1232 }
1233
1234 fn apply_controlled_phase_dist(&mut self, qubits: &[usize], phase: Complex64) {
1242 let local = self.local_qubits();
1243 let mut local_qubits: SmallVec<[usize; 8]> = SmallVec::new();
1244 for &q in qubits {
1245 if q < local {
1246 local_qubits.push(q);
1247 } else if !self.rank_bit_set(q) {
1248 return;
1250 }
1251 }
1252 self.apply_local_corner_phase(&local_qubits, phase);
1253 }
1254
1255 fn apply_local_corner_phase(&mut self, local_qubits: &[usize], phase: Complex64) {
1257 let z = Complex64::new(0.0, 0.0);
1258 let one = Complex64::new(1.0, 0.0);
1259 match local_qubits.len() {
1260 0 => scale_shard(&mut self.inner.state, phase),
1261 1 => self
1262 .inner
1263 .apply_1q_matrix(local_qubits[0], &[[one, z], [z, phase]])
1264 .expect("local diagonal phase"),
1265 n => {
1266 let mat = [[one, z], [z, phase]];
1267 let gate = if n == 2 {
1268 Gate::cu(mat)
1269 } else {
1270 Gate::mcu(mat, (n - 1) as u8)
1271 };
1272 self.inner
1273 .apply(&Instruction::Gate {
1274 gate,
1275 targets: local_qubits.iter().copied().collect(),
1276 })
1277 .expect("local controlled phase");
1278 }
1279 }
1280 }
1281
1282 fn apply_rzz_dist(&mut self, q0: usize, q1: usize, theta: f64) {
1287 let phase_same = Complex64::from_polar(1.0, -theta / 2.0);
1288 let phase_diff = Complex64::from_polar(1.0, theta / 2.0);
1289 self.apply_rzz_phases_dist(q0, q1, phase_same, phase_diff);
1290 }
1291
1292 fn apply_rzz_phases_dist(
1295 &mut self,
1296 q0: usize,
1297 q1: usize,
1298 phase_same: Complex64,
1299 phase_diff: Complex64,
1300 ) {
1301 let local = self.local_qubits();
1302
1303 match (q0 < local, q1 < local) {
1304 (true, true) => {
1305 use crate::gates::{DiagEntry, DiagonalBatchData};
1307 let entry = DiagEntry::Parity2q {
1308 q0,
1309 q1,
1310 same: phase_same,
1311 diff: phase_diff,
1312 };
1313 self.inner
1314 .apply(&Instruction::Gate {
1315 gate: Gate::DiagonalBatch(Box::new(DiagonalBatchData {
1316 entries: vec![entry],
1317 })),
1318 targets: smallvec![q0, q1],
1319 })
1320 .expect("local parity diagonal");
1321 }
1322 (false, false) => {
1323 let parity =
1324 ((self.rank_bit_set(q0) as usize) ^ (self.rank_bit_set(q1) as usize)) & 1;
1325 let factor = [phase_same, phase_diff][parity];
1326 scale_shard(&mut self.inner.state, factor);
1327 }
1328 (true, false) | (false, true) => {
1329 let (local_q, global_q) = if q0 < local { (q0, q1) } else { (q1, q0) };
1332 let gbit = self.rank_bit_set(global_q) as usize;
1333 let d0 = [phase_same, phase_diff][gbit];
1335 let d1 = [phase_same, phase_diff][gbit ^ 1];
1336 let z = Complex64::new(0.0, 0.0);
1337 self.inner
1338 .apply_1q_matrix(local_q, &[[d0, z], [z, d1]])
1339 .expect("local parity residual");
1340 }
1341 }
1342 }
1343
1344 fn apply_swap_dist(&mut self, a: usize, b: usize) {
1349 let local = self.local_qubits();
1350 match (a < local, b < local) {
1351 (true, true) => {
1352 self.inner
1353 .apply(&Instruction::Gate {
1354 gate: Gate::Swap,
1355 targets: smallvec![a, b],
1356 })
1357 .expect("local swap");
1358 }
1359 (false, false) => self.swap_global_slices(a, b),
1360 (true, false) | (false, true) => {
1361 let (local_q, global_q) = if a < local { (a, b) } else { (b, a) };
1362 self.half_slice_swap(local_q, global_q);
1363 }
1364 }
1365 }
1366
1367 fn swap_global_slices(&mut self, a: usize, b: usize) {
1372 if self.rank_bit_set(a) == self.rank_bit_set(b) {
1373 return;
1374 }
1375 let partner =
1376 self.context.rank() ^ (1usize << self.global_bit(a)) ^ (1usize << self.global_bit(b));
1377 let len = self.inner.state.len();
1378 let chunk = self.exchange_chunk.min(len).max(1);
1379 self.ensure_recv(chunk);
1380 let mut off = 0;
1381 while off < len {
1382 let end = (off + chunk).min(len);
1383 self.count_exchange(end - off);
1384 let recv = &mut self.recv[..end - off];
1385 self.context
1386 .comm()
1387 .sendrecv_c64(partner, &self.inner.state[off..end], recv);
1388 self.inner.state[off..end].copy_from_slice(recv);
1389 off = end;
1390 }
1391 }
1392
1393 fn apply_2q_dist(&mut self, q0: usize, q1: usize, mat: &[[Complex64; 4]; 4]) {
1399 let local = self.local_qubits();
1400 match (q0 < local, q1 < local) {
1401 (true, true) => self.apply_local_fused_2q(q0, q1, mat),
1402 (true, false) | (false, true) => self.apply_2q_one_global(q0, q1, mat),
1403 (false, false) => self.apply_2q_two_global(q0, q1, mat),
1404 }
1405 }
1406
1407 fn apply_local_fused_2q(&mut self, q0: usize, q1: usize, mat: &[[Complex64; 4]; 4]) {
1409 self.inner.apply_fused_2q(q0, q1, mat);
1410 }
1411
1412 fn apply_2q_one_global(&mut self, q0: usize, q1: usize, mat: &[[Complex64; 4]; 4]) {
1417 let local = self.local_qubits();
1418 let (local_q, global_q, global_is_q0) = if q0 < local {
1419 (q0, q1, false)
1420 } else {
1421 (q1, q0, true)
1422 };
1423 let partner = self.context.rank() ^ (1usize << self.global_bit(global_q));
1424 let len = self.inner.state.len();
1425 let chunk = self.block_chunk(local_q + 1, len);
1426 self.ensure_recv(chunk);
1427
1428 let g = self.rank_bit_set(global_q) as usize;
1429 let basis = |gbit: usize, lbit: usize| -> usize {
1431 if global_is_q0 {
1432 (gbit << 1) | lbit
1433 } else {
1434 (lbit << 1) | gbit
1435 }
1436 };
1437 let cols = [basis(g, 0), basis(g, 1), basis(1 - g, 0), basis(1 - g, 1)];
1439 let coeffs = |row: usize| cols.map(|c| mat[row][c]);
1440 let (m_lo, m_hi) = (coeffs(basis(g, 0)), coeffs(basis(g, 1)));
1441 let mut off = 0;
1442 while off < len {
1443 let end = (off + chunk).min(len);
1444 self.count_exchange(end - off);
1445 let recv = &mut self.recv[..end - off];
1446 self.context
1447 .comm()
1448 .sendrecv_c64(partner, &self.inner.state[off..end], recv);
1449 for_each_pair_tile(
1452 &mut self.inner.state[off..end],
1453 recv,
1454 local_q,
1455 |lo, hi, rlo, rhi| {
1456 for (((s0, s1), &r0), &r1) in
1457 lo.iter_mut().zip(hi).zip(rlo.iter()).zip(rhi.iter())
1458 {
1459 let (own0, own1) = (*s0, *s1);
1460 *s0 = m_lo[0] * own0 + m_lo[1] * own1 + m_lo[2] * r0 + m_lo[3] * r1;
1461 *s1 = m_hi[0] * own0 + m_hi[1] * own1 + m_hi[2] * r0 + m_hi[3] * r1;
1462 }
1463 },
1464 );
1465 off = end;
1466 }
1467 }
1468
1469 fn apply_2q_run_one_global(
1479 &mut self,
1480 global_q: usize,
1481 entries: &[(usize, usize, [[Complex64; 4]; 4])],
1482 ) {
1483 let partner = self.context.rank() ^ (1usize << self.global_bit(global_q));
1484 let local_of = |q0: usize, q1: usize| if q0 == global_q { q1 } else { q0 };
1485 let widest = entries
1486 .iter()
1487 .map(|&(q0, q1, _)| local_of(q0, q1))
1488 .max()
1489 .expect("a run has at least one entry");
1490 let len = self.inner.state.len();
1491 let chunk = self.block_chunk(widest + 1, len);
1492 self.ensure_recv(chunk);
1493
1494 let g = self.rank_bit_set(global_q) as usize;
1495 let zero = Complex64::new(0.0, 0.0);
1496 let mut off = 0;
1497 while off < len {
1498 let end = (off + chunk).min(len);
1499 self.count_exchange(end - off);
1500 let recv = &mut self.recv[..end - off];
1501 self.context
1502 .comm()
1503 .sendrecv_c64(partner, &self.inner.state[off..end], recv);
1504 for &(q0, q1, ref mat) in entries {
1505 let (local_q, global_is_q0) = (local_of(q0, q1), q0 == global_q);
1506 let basis = |gbit: usize, lbit: usize| -> usize {
1507 if global_is_q0 {
1508 (gbit << 1) | lbit
1509 } else {
1510 (lbit << 1) | gbit
1511 }
1512 };
1513 let slots = [basis(g, 0), basis(g, 1), basis(1 - g, 0), basis(1 - g, 1)];
1515 for_each_pair_tile(
1516 &mut self.inner.state[off..end],
1517 recv,
1518 local_q,
1519 |lo, hi, rlo, rhi| {
1520 for (((s0, s1), r0), r1) in lo.iter_mut().zip(hi).zip(rlo).zip(rhi) {
1521 let mut by_col = [zero; 4];
1522 by_col[slots[0]] = *s0;
1523 by_col[slots[1]] = *s1;
1524 by_col[slots[2]] = *r0;
1525 by_col[slots[3]] = *r1;
1526 let mut outs = [zero; 4];
1527 for (out, &row) in outs.iter_mut().zip(slots.iter()) {
1528 for (c, &) in by_col.iter().enumerate() {
1529 *out += mat[row][c] * amp;
1530 }
1531 }
1532 *s0 = outs[0];
1533 *s1 = outs[1];
1534 *r0 = outs[2];
1535 *r1 = outs[3];
1536 }
1537 },
1538 );
1539 }
1540 off = end;
1541 }
1542 }
1543
1544 fn apply_2q_two_global(&mut self, q0: usize, q1: usize, mat: &[[Complex64; 4]; 4]) {
1566 let rank = self.context.rank();
1567 let bit0 = 1usize << self.global_bit(q0);
1568 let bit1 = 1usize << self.global_bit(q1);
1569 let g0 = (rank & bit0 != 0) as usize;
1570 let g1 = (rank & bit1 != 0) as usize;
1571 let basis = |c0: usize, c1: usize| (c0 << 1) | c1;
1572 let (row, forward_row) = (basis(g0, g1), basis(g0, 1 - g1));
1573 let (own, across) = (basis(g0, g1), basis(1 - g0, g1));
1574 let keep = [mat[row][own], mat[row][across]];
1575 let forward = [mat[forward_row][own], mat[forward_row][across]];
1576
1577 let len = self.inner.state.len();
1578 let chunk = self.exchange_chunk.min(len).max(1);
1579 self.ensure_recv(chunk);
1580 self.ensure_pack(chunk);
1581 let mut off = 0;
1582 while off < len {
1583 let end = (off + chunk).min(len);
1584 let count = end - off;
1585 self.count_exchange(count);
1586 self.context.comm().sendrecv_c64(
1587 rank ^ bit0,
1588 &self.inner.state[off..end],
1589 &mut self.recv[..count],
1590 );
1591 butterfly_shard(
1592 &mut self.inner.state[off..end],
1593 &self.recv[..count],
1594 &mut self.pack[..count],
1595 keep,
1596 forward,
1597 );
1598 self.count_exchange(count);
1599 self.context.comm().sendrecv_c64(
1600 rank ^ bit1,
1601 &self.pack[..count],
1602 &mut self.recv[..count],
1603 );
1604 add_shard(&mut self.inner.state[off..end], &self.recv[..count]);
1605 off = end;
1606 }
1607 }
1608
1609 fn apply_global_multi_qubit(&mut self, gate: &Gate, targets: &[usize]) -> Result<()> {
1611 match gate {
1612 Gate::Cx => {
1613 self.apply_controlled_dist(&targets[..1], targets[1], Gate::X.matrix_2x2());
1614 Ok(())
1615 }
1616 Gate::Cz => {
1617 self.apply_controlled_phase_dist(
1618 &[targets[0], targets[1]],
1619 -Complex64::new(1.0, 0.0),
1620 );
1621 Ok(())
1622 }
1623 Gate::Swap => {
1624 self.apply_swap_dist(targets[0], targets[1]);
1625 Ok(())
1626 }
1627 Gate::Rzz(theta) => {
1628 self.apply_rzz_dist(targets[0], targets[1], *theta);
1629 Ok(())
1630 }
1631 Gate::Cu(mat) => {
1632 if let Some(phase) = gate.controlled_phase() {
1633 self.apply_controlled_phase_dist(&[targets[0], targets[1]], phase);
1634 } else {
1635 self.apply_controlled_dist(&targets[..1], targets[1], **mat);
1636 }
1637 Ok(())
1638 }
1639 Gate::Mcu(data) => {
1640 let num_ctrl = data.num_controls as usize;
1641 let controls = &targets[..num_ctrl];
1642 let target = targets[num_ctrl];
1643 if let Some(phase) = gate.controlled_phase() {
1644 let mut corner: Vec<usize> = controls.to_vec();
1645 corner.push(target);
1646 self.apply_controlled_phase_dist(&corner, phase);
1647 } else {
1648 self.apply_controlled_dist(controls, target, data.mat);
1649 }
1650 Ok(())
1651 }
1652 Gate::Fused2q(mat) => {
1653 self.apply_2q_dist(targets[0], targets[1], mat);
1654 Ok(())
1655 }
1656 Gate::Multi2q(data) => {
1657 let local = self.local_qubits();
1661 let one_global_on = |entry: &(usize, usize, [[Complex64; 4]; 4])| {
1662 let (q0, q1, _) = *entry;
1663 match (q0 < local, q1 < local) {
1664 (true, false) => Some(q1),
1665 (false, true) => Some(q0),
1666 _ => None,
1667 }
1668 };
1669 let mut i = 0;
1670 while i < data.gates.len() {
1671 let Some(g) = one_global_on(&data.gates[i]) else {
1672 let (q0, q1, ref mat) = data.gates[i];
1673 self.apply_2q_dist(q0, q1, mat);
1674 i += 1;
1675 continue;
1676 };
1677 let mut end = i + 1;
1678 while end < data.gates.len() && one_global_on(&data.gates[end]) == Some(g) {
1679 end += 1;
1680 }
1681 if end - i == 1 {
1682 let (q0, q1, ref mat) = data.gates[i];
1683 self.apply_2q_one_global(q0, q1, mat);
1684 } else {
1685 self.apply_2q_run_one_global(g, &data.gates[i..end]);
1686 }
1687 i = end;
1688 }
1689 Ok(())
1690 }
1691 Gate::MultiFused(data) => {
1692 for &(q, ref mat) in &data.gates {
1693 if q < self.local_qubits() {
1694 self.inner.apply_1q_matrix(q, mat).expect("local 1q matrix");
1695 } else if is_diagonal_2x2(mat) {
1696 self.apply_global_diagonal_1q(q, mat[0][0], mat[1][1]);
1697 } else {
1698 self.apply_global_1q(q, *mat);
1699 }
1700 }
1701 Ok(())
1702 }
1703 Gate::BatchPhase(data) => {
1704 let control = targets[0];
1705 for &(target, phase) in &data.phases {
1706 self.apply_controlled_phase_dist(&[control, target], phase);
1707 }
1708 Ok(())
1709 }
1710 Gate::BatchRzz(data) => {
1711 for &(q0, q1, theta) in &data.edges {
1712 self.apply_rzz_dist(q0, q1, theta);
1713 }
1714 Ok(())
1715 }
1716 Gate::DiagonalBatch(data) => {
1717 for entry in &data.entries {
1718 self.apply_diag_entry_dist(entry);
1719 }
1720 Ok(())
1721 }
1722 _ => Err(self.unsupported("gate spanning a global qubit")),
1723 }
1724 }
1725
1726 fn apply_diag_entry_dist(&mut self, entry: &crate::gates::DiagEntry) {
1728 use crate::gates::DiagEntry;
1729 match *entry {
1730 DiagEntry::Phase1q { qubit, d0, d1 } => {
1731 if qubit < self.local_qubits() {
1732 self.inner
1733 .apply_1q_matrix(
1734 qubit,
1735 &[
1736 [d0, Complex64::new(0.0, 0.0)],
1737 [Complex64::new(0.0, 0.0), d1],
1738 ],
1739 )
1740 .expect("local diagonal 1q");
1741 } else {
1742 self.apply_global_diagonal_1q(qubit, d0, d1);
1743 }
1744 }
1745 DiagEntry::Phase2q { q0, q1, phase } => {
1746 self.apply_controlled_phase_dist(&[q0, q1], phase);
1747 }
1748 DiagEntry::Parity2q {
1749 q0, q1, same, diff, ..
1750 } => {
1751 self.apply_rzz_phases_dist(q0, q1, same, diff);
1753 }
1754 }
1755 }
1756
1757 fn prob_outcome_global(&self, qubit: usize, outcome: bool) -> f64 {
1759 let norm_sq = self.inner.pending_norm * self.inner.pending_norm;
1760 let local_prob = if qubit < self.local_qubits() {
1761 half_norm_sqr(&self.inner.state, qubit, outcome)
1762 } else if self.rank_bit_set(qubit) == outcome {
1763 crate::backend::state_norm_sqr(&self.inner.state)
1764 } else {
1765 0.0
1766 };
1767 self.context.comm().allreduce_sum_f64(local_prob) * norm_sq
1768 }
1769
1770 fn prob_one_global(&self, qubit: usize) -> f64 {
1773 self.prob_outcome_global(qubit, true)
1774 }
1775
1776 fn measure_dist(&mut self, qubit: usize, classical_bit: usize) {
1780 let qubit = self.physical_qubit(qubit);
1781 let prob_one = self.prob_one_global(qubit);
1782 let outcome = self.meas_rng.random::<f64>() < prob_one;
1783 self.inner.classical_bits[classical_bit] = outcome;
1784 self.collapse(qubit, outcome);
1785 self.inner.pending_norm *= measurement_inv_norm(outcome, prob_one);
1786 }
1787
1788 #[inline]
1791 fn physical_qubit(&self, qubit: usize) -> usize {
1792 if self.map_identity {
1793 qubit
1794 } else {
1795 self.qubit_map[qubit]
1796 }
1797 }
1798
1799 pub fn sample_state_indices(&mut self, num_shots: usize, seed: u64) -> Result<Vec<u64>> {
1819 if num_shots == 0 {
1820 return Ok(Vec::new());
1821 }
1822 self.restore_identity_map();
1823 debug_assert!(self.map_identity);
1824
1825 let mut local_cdf = self.inner.host_probability_vector();
1828 let mut acc = 0.0f64;
1829 for p in &mut local_cdf {
1830 acc += *p;
1831 *p = acc;
1832 }
1833
1834 let masses = self.context.comm().allgather_f64(&[acc]);
1835 let mut rank_cdf = Vec::with_capacity(masses.len());
1836 let mut total = 0.0f64;
1837 for &m in &masses {
1838 total += m;
1839 rank_cdf.push(total);
1840 }
1841 if let Some(last) = rank_cdf.last_mut() {
1842 *last = 1.0;
1843 }
1844
1845 let rank = self.context.rank();
1846 let size = self.context.size();
1847 let local_qubits = self.local_qubits();
1848 let mut rng = ChaCha8Rng::seed_from_u64(seed);
1849 let mut owners = Vec::with_capacity(num_shots);
1850 let mut counts = vec![0usize; size];
1851 let mut owned = Vec::new();
1852 for _ in 0..num_shots {
1853 let r: f64 = rng.random();
1854 let owner = rank_cdf.partition_point(|&c| c < r);
1858 owners.push(owner);
1859 counts[owner] += 1;
1860 if owner != rank {
1861 continue;
1862 }
1863 let residual = if owner == 0 {
1864 r
1865 } else {
1866 r - rank_cdf[owner - 1]
1867 };
1868 let local_idx = crate::sim::shots::sample_from_cdf(&local_cdf, residual);
1869 owned.push(((rank as u64) << local_qubits) | local_idx as u64);
1870 }
1871
1872 let gathered = self.context.comm().allgatherv_u64(&owned, &counts);
1873 let mut next = vec![0usize; size];
1874 for r in 1..size {
1875 next[r] = next[r - 1] + counts[r - 1];
1876 }
1877 let indices = owners
1878 .iter()
1879 .map(|&owner| {
1880 let i = next[owner];
1881 next[owner] += 1;
1882 gathered[i]
1883 })
1884 .collect();
1885 Ok(indices)
1886 }
1887
1888 fn collapse(&mut self, qubit: usize, outcome: bool) {
1890 if qubit < self.local_qubits() {
1891 fn dropped(block: &mut [Complex64], half: usize, outcome: bool) -> &mut [Complex64] {
1892 let (lo, hi) = block.split_at_mut(half);
1893 if outcome { lo } else { hi }
1894 }
1895 let half = 1usize << qubit;
1896 let block_size = half << 1;
1897 #[cfg(feature = "parallel")]
1898 if self.inner.state.len() >= PAR_SHARD_LEN {
1899 if self.inner.state.len() / block_size >= 4 {
1900 self.inner
1901 .state
1902 .par_chunks_mut(block_size)
1903 .with_min_len(chunk_min_len(block_size))
1904 .for_each(|block| simd::zero_slice(dropped(block, half, outcome)));
1905 } else {
1906 for block in self.inner.state.chunks_mut(block_size) {
1907 dropped(block, half, outcome)
1908 .par_chunks_mut(MIN_PAR_ELEMS)
1909 .for_each(simd::zero_slice);
1910 }
1911 }
1912 return;
1913 }
1914 for block in self.inner.state.chunks_mut(block_size) {
1915 simd::zero_slice(dropped(block, half, outcome));
1916 }
1917 } else if self.rank_bit_set(qubit) != outcome {
1918 zero_shard(&mut self.inner.state);
1920 }
1921 }
1922
1923 fn reset_dist(&mut self, qubit: usize) -> Result<()> {
1928 let physical = self.physical_qubit(qubit);
1929 let prob_one = self.prob_one_global(physical);
1930 let outcome = self.meas_rng.random::<f64>() < prob_one;
1931 self.collapse(physical, outcome);
1932 self.inner.pending_norm *= measurement_inv_norm(outcome, prob_one);
1933 if outcome {
1934 self.apply_gate(&Gate::X, &[qubit])?;
1935 }
1936 Ok(())
1937 }
1938
1939 fn apply_gate(&mut self, gate: &Gate, targets: &[usize]) -> Result<()> {
1946 if self.global_qubits == 0 {
1947 self.inner.dispatch_gate(gate, targets);
1948 return Ok(());
1949 }
1950 if self.relabel {
1951 self.touch_instruction(gate, targets);
1952 if matches!(gate, Gate::Swap) {
1953 self.swap_circuit_qubits(targets[0], targets[1]);
1954 return Ok(());
1955 }
1956 let req = required_local_qubits(gate, targets);
1957 if !req.is_empty() {
1958 self.make_local(&req);
1959 }
1960 }
1961 if matches!(gate, Gate::QftBlock { .. }) && !self.map_identity {
1962 return Err(self.unsupported("QftBlock with a permuted qubit map"));
1963 }
1964 let (pgate, ptargets) = self.to_physical(gate, targets);
1965 if self.instruction_qubits_local(&pgate, &ptargets) {
1966 self.inner.dispatch_gate(pgate.as_ref(), &ptargets);
1967 return Ok(());
1968 }
1969 let pgate = pgate.as_ref();
1970 if pgate.num_qubits() == 1 {
1971 let target = ptargets[0];
1972 let mat = pgate.matrix_2x2();
1973 if pgate.is_diagonal_1q() {
1974 self.apply_global_diagonal_1q(target, mat[0][0], mat[1][1]);
1975 } else {
1976 self.apply_global_1q(target, mat);
1977 }
1978 return Ok(());
1979 }
1980 self.apply_global_multi_qubit(pgate, &ptargets)
1981 }
1982
1983 fn unsupported(&self, operation: &str) -> PrismError {
1984 PrismError::BackendUnsupported {
1985 backend: BACKEND_NAME.to_string(),
1986 operation: operation.to_string(),
1987 }
1988 }
1989}
1990
1991impl Backend for DistributedStatevectorBackend {
1992 fn name(&self) -> &'static str {
1993 BACKEND_NAME
1994 }
1995
1996 fn as_any(&self) -> Option<&dyn std::any::Any> {
1997 Some(self)
1998 }
1999
2000 fn resolved(&self) -> crate::sim::ResolvedBackend {
2001 crate::sim::ResolvedBackend::Distributed
2002 }
2003
2004 fn supports_fused_gates(&self) -> bool {
2005 true
2009 }
2010
2011 fn supports_qft_block(&self) -> bool {
2012 self.is_single_rank() && self.inner.supports_qft_block()
2013 }
2014
2015 fn supports_pauli_rotation(&self) -> bool {
2016 self.is_single_rank() && self.inner.supports_pauli_rotation()
2017 }
2018
2019 fn apply_instructions(&mut self, instructions: &[Instruction]) -> Result<()> {
2020 if std::mem::take(&mut self.circuit_check_pending) && self.context.size() > 1 {
2021 self.check_circuit_agreement(instructions)?;
2022 }
2023 if self.relabel && self.global_qubits > 0 {
2024 return self.apply_planned(instructions);
2025 }
2026 for instruction in instructions {
2027 self.apply(instruction)?;
2028 }
2029 Ok(())
2030 }
2031
2032 fn init(&mut self, num_qubits: usize, num_classical_bits: usize) -> Result<()> {
2033 let local_qubits = self.prepare_shard(num_qubits, num_classical_bits)?;
2034 self.inner.init(local_qubits, num_classical_bits)?;
2035
2036 if self.context.rank() != 0 {
2038 if let Some(amp) = self.inner.state.get_mut(0) {
2039 *amp = Complex64::new(0.0, 0.0);
2040 }
2041 }
2042 Ok(())
2043 }
2044
2045 fn supports_initial_state(&self) -> bool {
2046 true
2047 }
2048
2049 fn init_from_amplitudes(
2056 &mut self,
2057 amplitudes: Vec<Complex64>,
2058 num_classical_bits: usize,
2059 ) -> Result<()> {
2060 crate::backend::validate_initial_amplitudes(&litudes)?;
2061 let num_qubits = amplitudes.len().trailing_zeros() as usize;
2062 let local_qubits = self.prepare_shard(num_qubits, num_classical_bits)?;
2063 if self.is_single_rank() {
2064 return self.inner.init_from_state(amplitudes, num_classical_bits);
2065 }
2066 self.inner.init(local_qubits, num_classical_bits)?;
2067 let len = 1usize << local_qubits;
2068 let start = self.context.rank() * len;
2069 self.inner
2070 .state
2071 .copy_from_slice(&litudes[start..start + len]);
2072 Ok(())
2073 }
2074
2075 fn apply(&mut self, instruction: &Instruction) -> Result<()> {
2076 match instruction {
2077 Instruction::Measure {
2084 qubit,
2085 classical_bit,
2086 } => {
2087 self.measure_dist(*qubit, *classical_bit);
2088 Ok(())
2089 }
2090 Instruction::Reset { qubit } => self.reset_dist(*qubit),
2091 Instruction::Barrier { .. } => Ok(()),
2092 Instruction::Conditional {
2093 condition,
2094 gate,
2095 targets,
2096 } => {
2097 if condition.evaluate(self.inner.classical_results()) {
2098 self.apply_gate(gate, targets)
2099 } else {
2100 Ok(())
2101 }
2102 }
2103 Instruction::Gate { gate, targets } => self.apply_gate(gate, targets),
2104 Instruction::Region(region) => self.apply_region(region),
2109 }
2110 }
2111
2112 fn classical_results(&self) -> &[bool] {
2113 self.inner.classical_results()
2114 }
2115
2116 fn probabilities(&self) -> Result<Vec<f64>> {
2117 let local = self.inner.probabilities()?;
2118 if self.global_qubits == 0 {
2119 return Ok(local);
2120 }
2121 dense_probability_len(BACKEND_NAME, self.num_qubits)?;
2122 let gathered = self.context.comm().allgather_f64(&local);
2123 Ok(self.unpermuted(gathered))
2124 }
2125
2126 fn num_qubits(&self) -> usize {
2127 self.num_qubits
2128 }
2129
2130 fn export_statevector(&self) -> Result<Vec<Complex64>> {
2131 let local = self.inner.export_statevector()?;
2132 if self.global_qubits == 0 {
2133 return Ok(local);
2134 }
2135 dense_statevector_len(BACKEND_NAME, "statevector export", self.num_qubits)?;
2136 let gathered = self.context.comm().allgather_c64(&local);
2137 Ok(self.unpermuted(gathered))
2138 }
2139
2140 fn qubit_probability(&self, qubit: usize) -> Result<f64> {
2141 Ok(self.prob_one_global(self.physical_qubit(qubit)))
2142 }
2143
2144 fn supports_native_sampling(&self) -> bool {
2145 true
2146 }
2147
2148 fn sample_basis_states(&mut self, num_shots: usize, seed: u64) -> Result<BasisSamples> {
2155 let indices = self.sample_state_indices(num_shots, seed)?;
2156 let mut samples = BasisSamples::new(num_shots, self.num_qubits);
2157 for (shot, &index) in indices.iter().enumerate() {
2158 samples.set_index(shot, index as usize);
2159 }
2160 Ok(samples)
2161 }
2162
2163 fn supports_pauli_expectation(&self) -> bool {
2164 true
2165 }
2166
2167 fn pauli_expectations(&self, observables: &[Vec<PauliTerm>]) -> Result<Vec<f64>> {
2179 let comm = self.context.comm();
2180 let norm = comm.allreduce_sum_f64(crate::backend::state_norm_sqr(&self.inner.state));
2181 let local_qubits = self.local_qubits();
2182 let local_mask = (1usize << local_qubits) - 1;
2183 let mut recv: Vec<Complex64> = Vec::new();
2184
2185 let mut values = Vec::with_capacity(observables.len());
2186 for observable in observables {
2187 let (xmask, zmask, num_y) = crate::sim::pauli_masks(observable, self.num_qubits)?;
2188 let xphys = self.to_physical_mask(xmask);
2189 let zphys = self.to_physical_mask(zmask);
2190 let partner_bits = xphys >> local_qubits;
2191 if partner_bits != 0 {
2192 recv.resize(self.inner.state.len(), Complex64::new(0.0, 0.0));
2193 comm.sendrecv_c64(
2194 self.context.rank() ^ partner_bits,
2195 &self.inner.state,
2196 &mut recv,
2197 );
2198 }
2199 let bra: &[Complex64] = if partner_bits == 0 {
2200 &self.inner.state
2201 } else {
2202 &recv
2203 };
2204 let sandwich = crate::sim::pauli_sandwich(
2205 bra,
2206 &self.inner.state,
2207 xphys & local_mask,
2208 zphys & local_mask,
2209 num_y,
2210 );
2211 let rank_parity = (self.context.rank() & (zphys >> local_qubits)).count_ones() & 1;
2212 let signed = if rank_parity == 1 {
2213 -sandwich.re
2214 } else {
2215 sandwich.re
2216 };
2217 let total = comm.allreduce_sum_f64(signed);
2220 values.push(if norm == 0.0 { 0.0 } else { total / norm });
2221 }
2222 Ok(values)
2223 }
2224
2225 fn reset(&mut self, qubit: usize) -> Result<()> {
2226 self.reset_dist(qubit)
2227 }
2228
2229 fn apply_1q_matrix(&mut self, qubit: usize, matrix: &[[Complex64; 2]; 2]) -> Result<()> {
2237 if self.global_qubits == 0 {
2238 return self.inner.apply_1q_matrix(qubit, matrix);
2239 }
2240
2241 let diagonal = is_diagonal_2x2(matrix);
2242 if self.relabel {
2243 self.tick += 1;
2244 self.last_used[qubit] = self.tick;
2245 if !diagonal {
2246 self.make_local(&[qubit]);
2247 }
2248 }
2249
2250 let target = self.physical_qubit(qubit);
2251 if target < self.local_qubits() {
2252 return self.inner.apply_1q_matrix(target, matrix);
2253 }
2254 if diagonal {
2255 self.apply_global_diagonal_1q(target, matrix[0][0], matrix[1][1]);
2256 } else {
2257 self.apply_global_1q(target, *matrix);
2258 }
2259 Ok(())
2260 }
2261}