Skip to main content

prism_q/backend/distributed_statevector/
mod.rs

1//! Distributed state vector backend.
2//!
3//! Splits the `2^n` amplitude vector across `P = 2^p` ranks. The low `n - p`
4//! qubits index the local slice. The top `p` qubits select the rank. Each rank
5//! stores `2^(n - p)` amplitudes in an inner [`StatevectorBackend`].
6//!
7//! # Memory layout
8//!
9//! Global index: `rank * 2^(n - p) + local_index`. If `q < n - p`, qubit `q` is
10//! bit `q` of `local_index`; otherwise it is bit `q - (n - p)` of the rank id.
11//! Qubit 0 is the least significant bit. `|0...0>` is index 0 on rank 0.
12//!
13//! # Gate support
14//!
15//! Implemented: local gates, rank bit one qubit gates, two qubit gates, controlled
16//! gates across rank bits, `probabilities`, and `export_statevector`. A global
17//! control is constant on a rank, so it gates the whole slice with no
18//! communication. Diagonal controlled gates never communicate. With one rank
19//! ([`SerialComm`](crate::distributed::SerialComm)), every qubit is local.
20//!
21//! Fusion runs in every mode. Local fused gates dispatch to the inner SIMD
22//! kernels. Fused or batched gates that span rank bits are decomposed into the
23//! paths above. A general two qubit gate over one global qubit needs one
24//! pairwise exchange; over two global qubits it runs a two step butterfly
25//! across the group of four ranks that share the other rank bits.
26//!
27//! Once a rank resolves its global qubit bits, the remaining gate is local and
28//! dispatches to the inner backend. The only manual amplitude loops combine the
29//! received buffers after communication.
30//!
31//! Measurement, reset, and classical conditionals are supported. Measurement
32//! probabilities are summed with `Allreduce`. Each rank uses the same seeded RNG,
33//! so ranks agree without exchanging the draw. Reset runs one trajectory of the
34//! reset channel, as [`Backend::reset`] specifies: sample the outcome, collapse
35//! onto it, and apply X when it is 1.
36//!
37//! Per-qubit probabilities and Pauli expectation values answer from rank-local
38//! sums plus one `Allreduce`, at any register width. `probabilities` and
39//! `export_statevector` are the only queries that gather, so they carry the
40//! dense output cap; `Simulate::run` rejects a beyond-cap register before the
41//! run rather than returning no distribution after it.
42//!
43//! # When to prefer this backend
44//!
45//! - Amplitude vectors too large for one host, split across MPI ranks.
46//!   Requested via `BackendKind::StatevectorDistributed`; Auto never selects it.
47//! - Evaluating qubit routing strategies through the exchange counters, on one
48//!   host via the serial or loopback transports.
49//!
50//! # When NOT to use this backend
51//!
52//! - Circuits that fit one host; the inner statevector backend does the same
53//!   work without collectives.
54//! - Per-shot noise trajectories; `run_shots_with_noise` rejects the backend
55//!   (see the `backend` module docs).
56//!
57//! # Qubit relabeling
58//!
59//! At more than one rank the backend keeps a circuit-to-physical qubit map
60//! (on by default, see [`crate::distributed::relabel_enabled`]). SWAP becomes a
61//! map update: no amplitudes move and no rank communicates, at any local or
62//! global split. Before a gate applies non-diagonal action to a qubit in a rank
63//! bit position, the qubit is relabeled into a local position by exchanging the
64//! half slice whose local bit differs from the rank bit, evicting the least
65//! recently used local qubit. The gate and every later gate on that qubit then
66//! run on the inner SIMD kernels with no further communication, until the qubit
67//! is evicted again. Diagonal action and control bits stay free on global
68//! qubits, so they never trigger a relabel.
69//!
70//! Gate targets and the qubit indices inside batched gate data (`MultiFused`,
71//! `Multi2q`, `BatchPhase`, `BatchRzz`, `DiagonalBatch`) are translated through
72//! the map at apply time. `probabilities` and `export_statevector` reorder the
73//! gathered vector back to circuit qubit order; measurement, reset, and
74//! `qubit_probability` translate the qubit index. The direct per-gate exchange
75//! paths remain for relabeling disabled and for instructions whose qubits
76//! cannot all be made local (no eviction victim).
77//!
78//! Relabeling wins whenever gate activity has qubit locality: SWAP networks,
79//! repeated gates on the same qubits, and working sets that fit the local
80//! positions. A cyclic scan, a gate wall over more hot qubits than local
81//! positions repeated layer after layer, defeats least recently used eviction,
82//! so `apply_instructions` plans ahead: it cuts the stream into windows whose
83//! non-diagonal targets fit the local positions, relabels each window's
84//! global targets before its first gate, and evicts the local qubits whose
85//! next required use is furthest away. Gates inside a window then dispatch
86//! locally. A gate applied on its own through `apply` keeps the per-gate
87//! relabel with least recently used eviction. `PRISM_DIST_RELABEL=0` restores
88//! direct exchange.
89//!
90//! # Communication cost
91//!
92//! Only gates that are not diagonal and touch a global target communicate. With
93//! relabeling, a global SWAP costs nothing and the first non-diagonal gate on a
94//! global qubit costs a half-slice relabel exchange that also makes later gates
95//! on that qubit local. On the direct paths, a global one qubit gate, or a two
96//! qubit gate over one global qubit, costs one pairwise exchange of the local
97//! slice; a two qubit gate over two global qubits costs two, one per rank bit.
98//! Every direct exchange and the relabel exchange are tiled by
99//! [`crate::distributed::exchange_chunk`], which bounds the transfer buffers.
100//!
101//! [`DistributedStatevectorBackend::exchange_messages`] and
102//! [`DistributedStatevectorBackend::exchange_amplitudes`] expose rank local
103//! communication volume. Use these counters to evaluate qubit reordering and
104//! routing, since one host cannot measure real network latency.
105//!
106//! # Shot sampling
107//!
108//! Circuits whose measurements are terminal sample shots without gathering the
109//! dense state or probability vector on any rank; communication scales with
110//! the rank count and shot count, never with the state size. See
111//! [`DistributedStatevectorBackend::sample_state_indices`] for the algorithm,
112//! which [`Backend::sample_basis_states`] exposes at the trait level. Circuits
113//! with mid-circuit measurements fall back to one lockstep run per shot.
114
115mod 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/// Shard length at which the combine loops below fan out to Rayon: the same
150/// `2^14` amplitudes the inner backend uses for its own kernels.
151#[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
165/// Scale the amplitudes whose index has every bit of `mask` set.
166fn 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
196/// `dst[i] = c_self * dst[i] + c_remote * remote[i]` over a received block.
197fn 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
213/// `pack[k] = state[index_of(k)]` for every `k`.
214fn 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
236/// `state[index_of(k)] = f(state[index_of(k)], recv[k])` for every `k`.
237/// `index_of` must be injective on `0..recv.len()`: the parallel arm relies on
238/// it to keep the tasks' writes disjoint.
239fn 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                    // SAFETY: `index_of` is injective and maps into `state`, so
255                    // each index is read and written by exactly one task and no
256                    // two tasks touch the same amplitude.
257                    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
268/// Visit the `(lo, hi)` halves of every `2^(local_q + 1)` block of `state`
269/// together with the same halves of `recv`, as four equal-length tiles whose
270/// `k`-th elements share a basis index apart from the `local_q` bit.
271fn 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
309/// Butterfly step over a received block: `pack[i] = forward[0] * state[i] +
310/// forward[1] * remote[i]` is the partial sum forwarded to the next partner,
311/// then `state[i] = keep[0] * state[i] + keep[1] * remote[i]`.
312fn 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
338/// `dst[i] += src[i]` over a received block.
339fn 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
355/// `sum |a|^2` over the `qubit == outcome` half of every block of a shard,
356/// parallel above `MIN_PAR_REDUCE_ELEMS` like `state_norm_sqr`.
357fn 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
392/// Visit every circuit qubit an instruction touches: the instruction targets
393/// plus qubit indices stored inside batched gate data. Indices may repeat.
394fn 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
436/// Circuit qubits that must occupy local positions for the gate to apply
437/// without a per-gate amplitude exchange. Diagonal action and control bits
438/// are free on global qubits, so only non-diagonal application targets count.
439fn 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
491/// Distributed state vector backend over an `Arc`-shared [`DistributedContext`].
492pub struct DistributedStatevectorBackend {
493    context: Arc<DistributedContext>,
494    inner: StatevectorBackend,
495    num_qubits: usize,
496    global_qubits: usize,
497    /// Receive buffer for the direct exchange paths. Grows to the largest tile
498    /// requested and never shrinks; callers take a `[..len]` view.
499    recv: Vec<Complex64>,
500    seed: u64,
501    /// Max amplitudes exchanged per message on the direct exchange paths.
502    /// Tiling bounds `recv` and `pack` to one tile; the two qubit paths round
503    /// the tile down to whole pair blocks.
504    exchange_chunk: usize,
505    /// Count of `sendrecv` messages issued by this rank, and the total
506    /// amplitudes exchanged. Reorder and routing passes should minimize these
507    /// counters.
508    exchange_messages: u64,
509    exchange_amplitudes: u64,
510    /// RNG for measurement decisions, seeded identically on every rank and
511    /// advanced in lockstep. Outcomes are derived from `Allreduce`d global
512    /// probabilities, so all ranks agree without exchanging the draw.
513    meas_rng: ChaCha8Rng,
514    /// Circuit qubit to physical position. Positions below `local_qubits()`
515    /// index the local slice; the rest are rank bits. Identity until a SWAP or
516    /// a relabel exchange moves a qubit.
517    qubit_map: Vec<usize>,
518    /// Physical position to circuit qubit. Inverse of `qubit_map`.
519    phys_map: Vec<usize>,
520    /// Fast path flag: true while `qubit_map` is the identity.
521    map_identity: bool,
522    /// Whether gates relabel global qubits into local positions instead of
523    /// exchanging amplitudes per gate.
524    relabel: bool,
525    /// Instruction tick at which each circuit qubit was last referenced.
526    /// Drives least recently used eviction for relabel victims.
527    last_used: Vec<u64>,
528    tick: u64,
529    /// Send-side buffer for the indexed exchanges and the forwarded partial of
530    /// the two global butterfly. Grow-only like `recv`.
531    pack: Vec<Complex64>,
532    /// Armed by `init`, spent by the first instruction batch, which is where the
533    /// circuit fingerprint is cross-checked.
534    circuit_check_pending: bool,
535}
536
537impl DistributedStatevectorBackend {
538    /// Create a backend bound to the given rank context and RNG seed.
539    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    /// Override the exchange chunk size in amplitudes. Tests use this to cover
563    /// the tiled path without using the process environment.
564    #[cfg(test)]
565    pub(crate) fn set_exchange_chunk(&mut self, chunk: usize) {
566        self.exchange_chunk = chunk.max(1);
567    }
568
569    /// Enable or disable qubit relabeling for this backend instance, overriding
570    /// the `PRISM_DIST_RELABEL` default. With relabeling off, every gate on a
571    /// global qubit uses the direct per-gate exchange paths.
572    pub fn set_relabel(&mut self, enabled: bool) {
573        self.relabel = enabled;
574    }
575
576    /// Number of `sendrecv` messages this rank has issued since `init`.
577    ///
578    /// Cost proxy for this backend. One host cannot measure real network
579    /// latency, so routing changes are evaluated against this count. Counts
580    /// gate and relabel exchanges; the query paths take `&self` and cannot
581    /// record theirs.
582    pub fn exchange_messages(&self) -> u64 {
583        self.exchange_messages
584    }
585
586    /// Total amplitudes this rank has sent across all exchanges since `init`.
587    pub fn exchange_amplitudes(&self) -> u64 {
588        self.exchange_amplitudes
589    }
590
591    /// Record a pairwise exchange of `amplitudes` for the cost counters.
592    #[inline]
593    fn count_exchange(&mut self, amplitudes: usize) {
594        self.exchange_messages += 1;
595        self.exchange_amplitudes += amplitudes as u64;
596    }
597
598    /// Grow `recv` to at least `len` amplitudes. Never shrinks, so paths that
599    /// alternate between chunk and slice lengths reuse one allocation and skip
600    /// the refill.
601    #[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    /// `pack` counterpart of [`Self::ensure_recv`].
609    #[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    /// Exchange chunk rounded down to whole `2^block_bits` blocks, at least one
617    /// block and at most `len`, so a tile holds complete pair blocks.
618    #[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    /// Bit position within the rank id for global qubit `q` (`q >= local`).
635    #[inline]
636    fn global_bit(&self, q: usize) -> usize {
637        q - self.local_qubits()
638    }
639
640    /// Whether this rank holds the `|1>` half of global qubit `q`.
641    #[inline]
642    fn rank_bit_set(&self, q: usize) -> bool {
643        (self.context.rank() >> self.global_bit(q)) & 1 == 1
644    }
645
646    /// Advance the instruction tick and mark every circuit qubit the
647    /// instruction references. Marked qubits are exempt from eviction until the
648    /// next instruction. Identical on every rank because the instruction stream
649    /// is identical.
650    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    /// Apply SWAP as a pure relabeling: exchange the two circuit qubits' map
661    /// entries. No amplitudes move and no rank communicates.
662    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    /// Local position holding the least recently used circuit qubit that the
674    /// current instruction does not reference. `None` when every local qubit is
675    /// referenced this tick.
676    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    /// Bring each requested circuit qubit into a local position. Best effort:
693    /// stops when no eviction victim remains, leaving the rest to the direct
694    /// exchange paths. Each relabel costs one half-slice exchange. Inside a
695    /// planned window every requested qubit is already local and this returns
696    /// without work.
697    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    /// Exchange the moving half of a local/global SWAP with the partner rank.
711    /// Only amplitudes whose local bit differs from this rank's bit of the
712    /// global position move, so each rank exchanges half its slice. Both ranks
713    /// enumerate their moving halves in ascending index order, which the
714    /// single-bit XOR relation between the two sets preserves, so the k-th
715    /// received amplitude lands at the k-th moving index. Tiled by
716    /// `exchange_chunk` like the direct global exchange. Pure data movement:
717    /// the qubit map is untouched.
718    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    /// Physically swap the qubits at a local and a global position, then update
750    /// the map.
751    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    /// Physically swap the qubits at positions `a < b` and update the map.
763    /// When both positions are local, this runs the inner SWAP kernel. When one
764    /// position is local and one is global, this reuses the half-slice relabel
765    /// exchange. When both positions are global, this exchanges full slices
766    /// between rank pairs whose two bits differ.
767    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    /// Physically reorder the state until every circuit qubit occupies its own
795    /// position. Runs in lockstep on every rank because the maps are identical.
796    /// Each misplaced qubit costs at most one exchange. The identity map
797    /// returns without work.
798    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    /// Translate an instruction into physical positions: map the targets and
809    /// rewrite qubit indices stored inside batched gate data. Borrows the gate
810    /// unchanged while the map is the identity.
811    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        // A payload whose every index maps to itself is borrowed unchanged:
821        // once any relabel leaves the map non-identity, a per-application deep
822        // clone of every batched payload is the steady state otherwise.
823        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    /// Whether every physical position the translated instruction touches,
901    /// including indices inside batched gate data, is below the local boundary.
902    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    /// Fingerprint of every setting the collective sequence assumes is shared.
910    /// Rank id and rank count are excluded; they legitimately differ.
911    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    /// Fold the instruction stream into the value ranks compare.
927    ///
928    /// Hashes the `Debug` rendering rather than walking the structure. `Gate`
929    /// has variants carrying dense matrices and no fingerprint of its own, and
930    /// `f64`'s `Debug` round-trips, so ranks differing in any field of any
931    /// instruction hash differently and a new variant is covered without being
932    /// listed here. Runs once per multi-rank run, not per gate.
933    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    /// Reject a run whose ranks were handed different circuits.
951    ///
952    /// [`Self::check_config_agreement`] compares seed, register shape, and the
953    /// tuning knobs, all of which two ranks can agree on while still executing
954    /// different gate streams. That desynchronizes the exchange sequence and
955    /// hangs the job at the first collective the two streams disagree about.
956    ///
957    /// One collective, on the first instruction batch of a run. It cannot catch
958    /// a rank that never reaches the run at all: that one hangs in this
959    /// allgather instead of a later one.
960    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    /// Reject a run whose ranks disagree about anything the collective sequence
977    /// depends on.
978    ///
979    /// Without this the mismatch surfaces as a hang (a diverging collective
980    /// order) or as silently wrong amplitudes (measurement branches drawn from
981    /// different seeds), both far from the setting that caused them.
982    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    /// Shared prologue of `init` and `init_from_amplitudes`: agree the register
1000    /// shape across ranks, check the rank count against it, and reset the qubit
1001    /// map to the identity. Returns the local qubit count for the shard the
1002    /// caller loads next.
1003    fn prepare_shard(&mut self, num_qubits: usize, num_classical_bits: usize) -> Result<usize> {
1004        let size = self.context.size();
1005        // Before the local validations: those read `num_qubits`, so ranks given
1006        // different circuits could disagree about whether to reject and leave
1007        // one side alone at the next collective.
1008        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    /// Translate a circuit-qubit bit mask into physical positions.
1044    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    /// Reorder a gathered dense vector from physical to circuit qubit order.
1056    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    /// Apply a one qubit gate whose target is stored in the rank id.
1072    ///
1073    /// Exchange with the partner rank, then write this rank's half of the 2x2
1074    /// result. The combine is elementwise, so the exchange is tiled in chunks of
1075    /// [`crate::distributed::exchange_chunk`] amplitudes, bounding the receive
1076    /// buffer to `chunk` instead of a full slice copy. The default chunk is
1077    /// the whole slice (single message), so behavior is unchanged unless tuned.
1078    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    /// Apply a diagonal one qubit gate whose target is stored in the rank id.
1102    ///
1103    /// The rank bit is constant across the local slice, so this only scales the
1104    /// slice by `d0` or `d1`.
1105    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    /// Apply a 2x2 matrix to a local target qubit, gated by a set of local
1111    /// control qubits (all must be 1). The whole operation is local, so it
1112    /// dispatches to the inner backend's SIMD and parallel controlled kernels.
1113    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    /// Apply a 2x2 matrix to a global target qubit, gated by local control
1137    /// qubits (all must be 1). Only the control-selected sublattice is
1138    /// consumed, so with `k` controls each rank packs and exchanges `len / 2^k`
1139    /// amplitudes instead of the full slice. Both ranks enumerate the same
1140    /// sublattice in ascending index order, so the exchange stays aligned.
1141    /// Tiled by `exchange_chunk` like the direct global exchange.
1142    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    /// Apply a controlled gate (one target, zero or more controls) whose qubit
1194    /// set may span local and global qubits. Covers Cx, Cu, and Mcu uniformly.
1195    /// A diagonal target matrix needs no communication regardless of the split.
1196    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                // A zero global control disables the gate on this rank.
1209                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            // A global diagonal target contributes its rank bit: scale the
1217            // control-selected sublattice by the selected diagonal entry.
1218            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    /// Apply a controlled diagonal gate `diag(1, phase)` on the all ones corner
1235    /// of its qubit set. Covers Cz, controlled phase, and diagonal Mcu with no
1236    /// communication: a global qubit contributes a constant rank bit, and
1237    /// local qubits restrict which slice indices receive the phase.
1238    ///
1239    /// The residual on local qubits is another controlled phase gate, so it uses
1240    /// the inner backend kernels.
1241    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                // A zero global corner bit makes the gate inactive on this rank.
1249                return;
1250            }
1251        }
1252        self.apply_local_corner_phase(&local_qubits, phase);
1253    }
1254
1255    /// Apply `phase` on the all ones corner through the inner backend.
1256    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    /// Apply `Rzz(theta)` across any local or global split. Rzz is diagonal,
1283    /// `phase = exp(-i theta/2)` when the two qubit bits agree and
1284    /// `exp(i theta/2)` when they differ, so no communication is needed: a
1285    /// global qubit contributes a constant rank bit to the parity.
1286    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    /// Apply a parity diagonal two qubit phase. Shared by `Rzz` and
1293    /// `Parity2q`; it needs no communication.
1294    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                // Both qubits are local, so use the inner diagonal batch kernel.
1306                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                // One global qubit is fixed on this rank. The residual is a
1330                // diagonal one qubit gate.
1331                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                // Local bit 0 uses parity gbit. Local bit 1 uses gbit ^ 1.
1334                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    /// Apply `SWAP(a, b)` across any local or global split.
1345    ///
1346    /// Local pairs delegate to the inner kernel. With a global qubit, only the
1347    /// `|01>` and `|10>` amplitudes move.
1348    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    /// Exchange whole slices between the rank pairs whose bits at global
1368    /// positions `a` and `b` differ; a rank with equal bits holds fixed points
1369    /// of the SWAP and skips. The copy is elementwise, so the exchange streams
1370    /// in `exchange_chunk` tiles.
1371    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    /// Apply a general 4x4 two qubit unitary across any local or global split.
1394    ///
1395    /// `mat` uses basis index `2*b0 + b1`, with `q0` as the high bit. Local
1396    /// pairs delegate to the inner kernel. One global qubit needs one exchange;
1397    /// two global qubits gather the rank group that shares both rank bits.
1398    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    /// Apply a fully local 4x4 gate through the inner backend's tiled kernel.
1408    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    /// One qubit is local and one is global. Exchange with the partner rank,
1413    /// then recompute each amplitude from the four inputs of the 2x2 block.
1414    /// Each pair block of `2^(local_q + 1)` amplitudes is self-contained, so the
1415    /// exchange streams in tiles of whole blocks.
1416    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        // Basis index in `mat` is `2*b_q0 + b_q1`.
1430        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        // Columns in input order: own lo, own hi, partner lo, partner hi.
1438        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            // Both outputs of a pair read both inputs, so each pair is finished
1450            // before either slot is written.
1451            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    /// Apply a run of two qubit gates that all pair a local qubit with the same
1470    /// global qubit. One exchange serves the whole run: after it this rank holds
1471    /// both halves of the pair subspace, so each entry updates the local slice
1472    /// and the mirrored partner copy together, exactly as the partner does with
1473    /// the roles flipped. Sums run in canonical basis order so both ranks
1474    /// produce bit-identical copies and stay in lockstep without further
1475    /// communication. Every entry's pair block fits inside a tile of whole
1476    /// blocks of the widest entry, so the exchange streams in such tiles and
1477    /// the run is applied tile by tile.
1478    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                // Slot order: state lo, state hi, recv lo, recv hi.
1514                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, &amp) 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    /// Both qubits global. The four `(q0, q1)` slices live on four ranks that
1545    /// share every other rank bit. Two pairwise exchanges of one slice each
1546    /// replace a gather of the other three. Write `a(c0, c1)` for the slice on
1547    /// the rank whose bits are `(c0, c1)`, `b(c0, c1) = 2 c0 + c1` for the basis
1548    /// index, `m = mat[b(g0, g1)]` for this rank's row, and `m' = mat[b(g0, 1 - g1)]`
1549    /// for the row of the partner across `q1`:
1550    ///
1551    /// 1. Exchange with `rank ^ bit(q0)`: send `a(g0, g1)`, receive `a(1 - g0, g1)`.
1552    ///    Form in place `p = m[b(g0, g1)] a(g0, g1) + m[b(1 - g0, g1)] a(1 - g0, g1)`
1553    ///    and, into the pack buffer, the partial the `q1` partner needs,
1554    ///    `u = m'[b(g0, g1)] a(g0, g1) + m'[b(1 - g0, g1)] a(1 - g0, g1)`.
1555    /// 2. Exchange with `rank ^ bit(q1)`: send `u`, receive the mirrored partial
1556    ///    `t = m[b(g0, 1 - g1)] a(g0, 1 - g1) + m[b(1 - g0, 1 - g1)] a(1 - g0, 1 - g1)`.
1557    ///    The output row is `p + t`.
1558    ///
1559    /// Both steps are elementwise, so each `exchange_chunk` tile runs through
1560    /// both exchanges before the next tile starts; the transient buffers are
1561    /// two tiles rather than three slices. Volume is `2 len` amplitudes per rank
1562    /// against `3 len` for the gather. The association `(own + q0 partner) +
1563    /// (q1 partner + diagonal partner)` differs from the gather's left to right
1564    /// sum by rounding only.
1565    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    /// Dispatch a gate that spans at least one global qubit.
1610    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                // Consecutive entries sharing one global qubit have the same
1658                // partner (a CNOT star onto one qubit); one exchange serves
1659                // the whole run.
1660                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    /// Apply a single [`DiagEntry`] across any local or global split.
1727    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                // These are the parity phases for Rzz(theta).
1752                self.apply_rzz_phases_dist(q0, q1, same, diff);
1753            }
1754        }
1755    }
1756
1757    /// Total scaled weight of the `qubit == outcome` subspace across ranks.
1758    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    /// Total weight of the `qubit == 1` subspace across all ranks. Used by
1771    /// measurement and as `P(qubit = 1)`.
1772    fn prob_one_global(&self, qubit: usize) -> f64 {
1773        self.prob_outcome_global(qubit, true)
1774    }
1775
1776    /// Measure `qubit`, collapse the state, and record the bit. Deterministic
1777    /// across ranks: the outcome is drawn from the lockstep `meas_rng` against an
1778    /// `Allreduce`d probability, so every rank collapses to the same branch.
1779    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    /// Physical position of a circuit qubit. Identity before `init` runs the
1789    /// map setup or while no relabeling has occurred.
1790    #[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    /// Sample `num_shots` computational basis indices in circuit qubit order
1800    /// without gathering the dense state or probability vector on any rank.
1801    ///
1802    /// Relabeled qubits are first restored to their circuit positions with
1803    /// bounded exchanges, so each rank owns a contiguous slice in circuit
1804    /// order. Each rank then builds a cumulative distribution for its local
1805    /// slice. One gather shares a single mass value from each rank. Every rank
1806    /// assigns each shot to an owning rank from the same seeded draw stream, so
1807    /// every rank knows the owner sequence. Each owner samples its local
1808    /// distribution for its shots, one variable-count gather concatenates the
1809    /// owned indices in rank order, and each rank scatters them back into shot
1810    /// order. Buffers scale with the rank count and shot count, not the global
1811    /// state size.
1812    ///
1813    /// Collective: every rank must call this with identical `num_shots` and
1814    /// `seed`. The result is identical on every rank and reproduces the dense
1815    /// sampling path draw for draw, independent of the rank count, except
1816    /// when accumulated rounding differences move a draw across an interval
1817    /// edge in the cumulative distribution.
1818    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        // The rank-local CDF is a working buffer half the size of the slice
1826        // this rank already holds, so the dense output cap does not gate it.
1827        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            // First rank whose cumulative mass reaches r. The strict
1855            // comparison matches the dense binary search at exact boundary
1856            // hits and never selects an empty interval.
1857            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    /// Zero the amplitudes inconsistent with `qubit == outcome`.
1889    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            // This rank holds the eliminated branch entirely.
1919            zero_shard(&mut self.inner.state);
1920        }
1921    }
1922
1923    /// Reset `qubit` to `|0>` as one trajectory of the reset channel: sample
1924    /// the outcome, collapse onto it, then apply X when it is 1. The draw
1925    /// comes from the rank-replicated measurement stream, so every rank
1926    /// selects the same branch.
1927    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    /// Route a gate to the local fast path or the distributed paths.
1940    ///
1941    /// With relabeling on, SWAP becomes a map update, and qubits that need
1942    /// non-diagonal application are moved into local positions first, so the
1943    /// per-gate exchange paths below only fire when no eviction victim exists
1944    /// or relabeling is disabled.
1945    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        // Fusion runs in every mode. Fully local fused gates dispatch to the
2006        // inner backend's tiled SIMD kernels; fused or batched gates that span a
2007        // rank bit are decomposed into primitives at apply time.
2008        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        // inner.init seeds index 0 on every rank; only rank 0 owns |0...0>.
2037        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    /// Load this rank's shard from the full `2^n` vector.
2050    ///
2051    /// Every rank receives the whole vector and keeps the `2^(n - p)` amplitudes
2052    /// from `rank * 2^(n - p)`, the identity layout `init` establishes; a map
2053    /// left permuted by an earlier relabeled run is reset, not written into.
2054    /// Collective: every rank must call it with an identical vector.
2055    fn init_from_amplitudes(
2056        &mut self,
2057        amplitudes: Vec<Complex64>,
2058        num_classical_bits: usize,
2059    ) -> Result<()> {
2060        crate::backend::validate_initial_amplitudes(&amplitudes)?;
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(&amplitudes[start..start + len]);
2072        Ok(())
2073    }
2074
2075    fn apply(&mut self, instruction: &Instruction) -> Result<()> {
2076        match instruction {
2077            // Measurement routes through the distributed path even at a single
2078            // rank, so `meas_rng` is the sole measurement RNG and one seed
2079            // draws one outcome stream. Outcomes then agree across rank counts
2080            // except where the `Allreduce` association order moves a summed
2081            // probability across the drawn value, the caveat
2082            // `sample_state_indices` documents for the same reason.
2083            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            // Every rank draws measurement outcomes from the same seeded RNG
2105            // against an `Allreduce`d probability, so every rank holds the same
2106            // classical bits and takes the same branch without a consensus
2107            // exchange.
2108            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    /// Trait-level entry to [`DistributedStatevectorBackend::sample_state_indices`],
2149    /// so a caller holding a `dyn Backend` gets the same rank-local draw the
2150    /// shot route takes instead of falling back to the dense vector.
2151    ///
2152    /// Collective: every rank must call it with identical `num_shots` and
2153    /// `seed`.
2154    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    /// Evaluate each observable on the sharded state with no dense gather.
2168    ///
2169    /// A Z factor on a rank bit is a constant sign for the whole slice, so an
2170    /// observable whose X and Y factors are all local costs one `Allreduce` and
2171    /// no transfer. X and Y factors on rank bits displace the bra by the same
2172    /// rank offset for every amplitude, so however many there are they name one
2173    /// partner rank, and one slice exchange covers them. That is the direct
2174    /// route rather than a relabel because relabeling mutates the state, which
2175    /// a `&self` query cannot do.
2176    ///
2177    /// Collective: every rank must call it with identical observables.
2178    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            // The total is real, so summing the per-rank real parts loses
2218            // nothing even where a rank's own term is not.
2219            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    /// Apply a 2x2 matrix to one circuit qubit across the rank split, on the same
2230    /// route `apply_gate` takes for a one-qubit gate: relabel a non-diagonal
2231    /// target into a local position when a victim exists, apply locally when the
2232    /// physical position is local, otherwise exchange with the partner rank.
2233    ///
2234    /// Collective when the target is global, so every rank must call it with the
2235    /// same qubit.
2236    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}