Skip to content

Commit b0297df

Browse files
mtreinishCryoris
authored andcommitted
Add Rust quantum volume function (Qiskit#13238)
* Add Rust quantum volume function This commit adds a new function quantum_volume used for generating a quantum volume model circuit. This new function is defined in Rust and multithreaded to improve the throughput of the circuit generation. This new function will eventually replace the existing QuantumVolume class as part of Qiskit#13046. Since quantum volume is a circuit defined by it's structure using a generator function is inline with the goals of Qiskit#13046. Right now the performance is bottlenecked by the creation of the UnitaryGate objects as these are still defined solely in Python. We'll likely need to port the class to have a rust native representation to further speed up the construction of the circuit. * Adjust type hints on python function Co-authored-by: Julien Gacon <gaconju@gmail.com> * Add missing __future__ import The previous commit was relying on the behavior of the annotations future import but neglected to add it. This commit corrects the oversight. * Add comment on random unitary algorithm * Reduce allocations random_unitaries The previous implementation had 4 heap allocations for each random unitary constructed, this commit uses some fixed sized stack allocated arrays and reduces that to two allocations one for q and r from the factorization. We'll always need at least one for the `Array2` that gets stored in each `UnitaryGate` as a numpy array. But to reduce to just this we'll need a method of computing the QR factorization without an allocation for the result space, nalgebtra might be a path for doing that. While this currently isn't a bottleneck as the `UnitaryGate` python object creation is the largest source of runtime, but assuming that's fixed in the future this might have a larger impact. * Preallocate unitaries for serial path When executing in the serial path we previously were working directly with an iterator where the 2q unitaries we're created on the iterator that we passed directly to circuit constructor. However testing shows that precomputing all the unitaries into a Vec and passing the iterator off of that to the circuit constructor is marginally but consistently faster. So this commit pivots to using that instead. * Fix determinism and error handling of of qv function This commit fixes two issues in the reproducibility of the quantum volume circuit. The first was the output unitary matrices for a fixed seed would differ between the parallel and serial execution path. This was because how the RNGs were used was different in the different code paths. This change results in the serial path being marginally less efficient, but it shouldn't be a big deal when compared to getting different results in different contexts. The second was the seed usage in parallel mode was dependent on the number of threads on the local system. This was problematic because the exact circuit generated between two systems would be different even with a fixed seed. This was fixed to avoid depending on the number of threads to determine how the seeds were used across multiple threads. The last fix here was a change to the error handling so that the CircuitData constructor used to create the circuit object can handle a fallible iterator. Previously we were throwing away the python error and panicking if the Python call to generate the UnitaryGate object raised an error for any reason. * Mention the new function is multithreaded in docstring * Update qiskit/circuit/library/quantum_volume.py Co-authored-by: Julien Gacon <gaconju@gmail.com> --------- Co-authored-by: Julien Gacon <gaconju@gmail.com> Co-authored-by: Julien Gacon <jules.gacon@googlemail.com>
1 parent 40e7521 commit b0297df

9 files changed

Lines changed: 264 additions & 10 deletions

File tree

crates/accelerate/src/circuit_library/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ use pyo3::prelude::*;
1414

1515
mod entanglement;
1616
mod pauli_feature_map;
17+
mod quantum_volume;
1718

1819
pub fn circuit_library(m: &Bound<PyModule>) -> PyResult<()> {
1920
m.add_wrapped(wrap_pyfunction!(pauli_feature_map::pauli_feature_map))?;
2021
m.add_wrapped(wrap_pyfunction!(entanglement::get_entangler_map))?;
22+
m.add_wrapped(wrap_pyfunction!(quantum_volume::quantum_volume))?;
2123
Ok(())
2224
}

crates/accelerate/src/circuit_library/pauli_feature_map.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ fn pauli_evolution(
138138
/// insert_barriers: Whether to insert barriers in between the Hadamard and evolution layers.
139139
/// data_map_func: An accumulation function that takes as input a vector of parameters the
140140
/// current gate acts on and returns a scalar.
141-
///
141+
///
142142
/// Returns:
143143
/// The ``CircuitData`` to construct the Pauli feature map.
144144
#[pyfunction]
@@ -207,7 +207,13 @@ pub fn pauli_feature_map(
207207
}
208208
}
209209

210-
CircuitData::from_packed_operations(py, feature_dimension, 0, packed_insts, Param::Float(0.0))
210+
CircuitData::from_packed_operations(
211+
py,
212+
feature_dimension,
213+
0,
214+
packed_insts.into_iter().map(Ok),
215+
Param::Float(0.0),
216+
)
211217
}
212218

213219
fn _get_h_layer(feature_dimension: u32) -> impl Iterator<Item = Instruction> {
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2024
4+
//
5+
// This code is licensed under the Apache License, Version 2.0. You may
6+
// obtain a copy of this license in the LICENSE.txt file in the root directory
7+
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
//
9+
// Any modifications or derivative works of this code must retain this
10+
// copyright notice, and modified files need to carry a notice indicating
11+
// that they have been altered from the originals.
12+
13+
use pyo3::prelude::*;
14+
15+
use crate::getenv_use_multiple_threads;
16+
use faer_ext::{IntoFaerComplex, IntoNdarrayComplex};
17+
use ndarray::prelude::*;
18+
use num_complex::Complex64;
19+
use numpy::IntoPyArray;
20+
use rand::prelude::*;
21+
use rand_distr::StandardNormal;
22+
use rand_pcg::Pcg64Mcg;
23+
use rayon::prelude::*;
24+
25+
use qiskit_circuit::circuit_data::CircuitData;
26+
use qiskit_circuit::imports::UNITARY_GATE;
27+
use qiskit_circuit::operations::Param;
28+
use qiskit_circuit::operations::PyInstruction;
29+
use qiskit_circuit::packed_instruction::PackedOperation;
30+
use qiskit_circuit::{Clbit, Qubit};
31+
use smallvec::{smallvec, SmallVec};
32+
33+
type Instruction = (
34+
PackedOperation,
35+
SmallVec<[Param; 3]>,
36+
Vec<Qubit>,
37+
Vec<Clbit>,
38+
);
39+
40+
#[inline(always)]
41+
fn random_complex(rng: &mut Pcg64Mcg) -> Complex64 {
42+
Complex64::new(rng.sample(StandardNormal), rng.sample(StandardNormal))
43+
* std::f64::consts::FRAC_1_SQRT_2
44+
}
45+
46+
// This function's implementation was modeled off of the algorithm used in the
47+
// `scipy.stats.unitary_group.rvs()` function defined here:
48+
//
49+
// https://github.com/scipy/scipy/blob/v1.14.1/scipy/stats/_multivariate.py#L4224-L4256
50+
#[inline]
51+
fn random_unitaries(seed: u64, size: usize) -> impl Iterator<Item = Array2<Complex64>> {
52+
let mut rng = Pcg64Mcg::seed_from_u64(seed);
53+
54+
(0..size).map(move |_| {
55+
let raw_numbers: [[Complex64; 4]; 4] = [
56+
[
57+
random_complex(&mut rng),
58+
random_complex(&mut rng),
59+
random_complex(&mut rng),
60+
random_complex(&mut rng),
61+
],
62+
[
63+
random_complex(&mut rng),
64+
random_complex(&mut rng),
65+
random_complex(&mut rng),
66+
random_complex(&mut rng),
67+
],
68+
[
69+
random_complex(&mut rng),
70+
random_complex(&mut rng),
71+
random_complex(&mut rng),
72+
random_complex(&mut rng),
73+
],
74+
[
75+
random_complex(&mut rng),
76+
random_complex(&mut rng),
77+
random_complex(&mut rng),
78+
random_complex(&mut rng),
79+
],
80+
];
81+
82+
let qr = aview2(&raw_numbers).into_faer_complex().qr();
83+
let r = qr.compute_r();
84+
let diag: [Complex64; 4] = [
85+
r[(0, 0)].to_num_complex() / r[(0, 0)].abs(),
86+
r[(1, 1)].to_num_complex() / r[(1, 1)].abs(),
87+
r[(2, 2)].to_num_complex() / r[(2, 2)].abs(),
88+
r[(3, 3)].to_num_complex() / r[(3, 3)].abs(),
89+
];
90+
let mut q = qr.compute_q().as_ref().into_ndarray_complex().to_owned();
91+
q.axis_iter_mut(Axis(0)).for_each(|mut row| {
92+
row.iter_mut()
93+
.enumerate()
94+
.for_each(|(index, val)| *val *= diag[index])
95+
});
96+
q
97+
})
98+
}
99+
100+
const UNITARY_PER_SEED: usize = 50;
101+
102+
#[pyfunction]
103+
pub fn quantum_volume(
104+
py: Python,
105+
num_qubits: u32,
106+
depth: usize,
107+
seed: Option<u64>,
108+
) -> PyResult<CircuitData> {
109+
let width = num_qubits as usize / 2;
110+
let num_unitaries = width * depth;
111+
let mut permutation: Vec<Qubit> = (0..num_qubits).map(Qubit).collect();
112+
113+
let mut build_instruction = |(unitary_index, unitary_array): (usize, Array2<Complex64>),
114+
rng: &mut Pcg64Mcg|
115+
-> PyResult<Instruction> {
116+
let layer_index = unitary_index % width;
117+
if layer_index == 0 {
118+
permutation.shuffle(rng);
119+
}
120+
let unitary = unitary_array.into_pyarray_bound(py);
121+
let unitary_gate = UNITARY_GATE
122+
.get_bound(py)
123+
.call1((unitary.clone(), py.None(), false))?;
124+
let instruction = PyInstruction {
125+
qubits: 2,
126+
clbits: 0,
127+
params: 1,
128+
op_name: "unitary".to_string(),
129+
control_flow: false,
130+
instruction: unitary_gate.unbind(),
131+
};
132+
let qubit = layer_index * 2;
133+
Ok((
134+
PackedOperation::from_instruction(Box::new(instruction)),
135+
smallvec![Param::Obj(unitary.unbind().into())],
136+
vec![permutation[qubit], permutation[qubit + 1]],
137+
vec![],
138+
))
139+
};
140+
141+
let mut per_thread = num_unitaries / UNITARY_PER_SEED;
142+
if per_thread == 0 {
143+
per_thread = 10;
144+
}
145+
let mut outer_rng = match seed {
146+
Some(seed) => Pcg64Mcg::seed_from_u64(seed),
147+
None => Pcg64Mcg::from_entropy(),
148+
};
149+
let seed_vec: Vec<u64> = rand::distributions::Standard
150+
.sample_iter(&mut outer_rng)
151+
.take(num_unitaries)
152+
.collect();
153+
154+
let unitaries: Vec<Array2<Complex64>> = if getenv_use_multiple_threads() && num_unitaries > 200
155+
{
156+
seed_vec
157+
.par_chunks(per_thread)
158+
.flat_map_iter(|seeds| random_unitaries(seeds[0], seeds.len()))
159+
.collect()
160+
} else {
161+
seed_vec
162+
.chunks(per_thread)
163+
.flat_map(|seeds| random_unitaries(seeds[0], seeds.len()))
164+
.collect()
165+
};
166+
CircuitData::from_packed_operations(
167+
py,
168+
num_qubits,
169+
0,
170+
unitaries
171+
.into_iter()
172+
.enumerate()
173+
.map(|x| build_instruction(x, &mut outer_rng)),
174+
Param::Float(0.),
175+
)
176+
}

crates/accelerate/src/two_qubit_decompose.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,18 +2176,18 @@ impl TwoQubitBasisDecomposer {
21762176
.gates
21772177
.into_iter()
21782178
.map(|(gate, params, qubits)| match gate {
2179-
Some(gate) => (
2179+
Some(gate) => Ok((
21802180
PackedOperation::from_standard(gate),
21812181
params.into_iter().map(Param::Float).collect(),
21822182
qubits.into_iter().map(|x| Qubit(x.into())).collect(),
21832183
Vec::new(),
2184-
),
2185-
None => (
2184+
)),
2185+
None => Ok((
21862186
kak_gate.operation.clone(),
21872187
kak_gate.params.clone(),
21882188
qubits.into_iter().map(|x| Qubit(x.into())).collect(),
21892189
Vec::new(),
2190-
),
2190+
)),
21912191
}),
21922192
Param::Float(sequence.global_phase),
21932193
),

crates/circuit/src/circuit_data.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,12 @@ impl CircuitData {
134134
) -> PyResult<Self>
135135
where
136136
I: IntoIterator<
137-
Item = (
137+
Item = PyResult<(
138138
PackedOperation,
139139
SmallVec<[Param; 3]>,
140140
Vec<Qubit>,
141141
Vec<Clbit>,
142-
),
142+
)>,
143143
>,
144144
{
145145
let instruction_iter = instructions.into_iter();
@@ -150,7 +150,8 @@ impl CircuitData {
150150
instruction_iter.size_hint().0,
151151
global_phase,
152152
)?;
153-
for (operation, params, qargs, cargs) in instruction_iter {
153+
for item in instruction_iter {
154+
let (operation, params, qargs, cargs) = item?;
154155
let qubits = res.qargs_interner.insert_owned(qargs);
155156
let clbits = res.cargs_interner.insert_owned(cargs);
156157
let params = (!params.is_empty()).then(|| Box::new(params));

qiskit/circuit/library/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@
318318
HiddenLinearFunction
319319
IQP
320320
QuantumVolume
321+
quantum_volume
321322
PhaseEstimation
322323
GroverOperator
323324
PhaseOracle
@@ -564,7 +565,7 @@
564565
StatePreparation,
565566
Initialize,
566567
)
567-
from .quantum_volume import QuantumVolume
568+
from .quantum_volume import QuantumVolume, quantum_volume
568569
from .fourier_checking import FourierChecking
569570
from .graph_state import GraphState
570571
from .hidden_linear_function import HiddenLinearFunction

qiskit/circuit/library/quantum_volume.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212

1313
"""Quantum Volume model circuit."""
1414

15+
from __future__ import annotations
16+
1517
from typing import Optional, Union
1618

1719
import numpy as np
1820
from qiskit.circuit import QuantumCircuit, CircuitInstruction
1921
from qiskit.circuit.library.generalized_gates import PermutationGate, UnitaryGate
22+
from qiskit._accelerate.circuit_library import quantum_volume as qv_rs
2023

2124

2225
class QuantumVolume(QuantumCircuit):
@@ -113,3 +116,42 @@ def __init__(
113116
base._append(CircuitInstruction(gate, qubits[qubit : qubit + 2]))
114117
if not flatten:
115118
self._append(CircuitInstruction(base.to_instruction(), tuple(self.qubits)))
119+
120+
121+
def quantum_volume(
122+
num_qubits: int,
123+
depth: int | None = None,
124+
seed: int | np.random.Generator | None = None,
125+
) -> QuantumCircuit:
126+
"""A quantum volume model circuit.
127+
128+
The model circuits are random instances of circuits used to measure
129+
the Quantum Volume metric, as introduced in [1].
130+
131+
The model circuits consist of layers of Haar random
132+
elements of SU(4) applied between corresponding pairs
133+
of qubits in a random bipartition.
134+
135+
This function is multithreaded and will launch a thread pool with threads equal to the number
136+
of CPUs by default. You can tune the number of threads with the ``RAYON_NUM_THREADS``
137+
environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would limit the thread pool
138+
to 4 threads.
139+
140+
**Reference Circuit:**
141+
142+
.. plot::
143+
144+
from qiskit.circuit.library import quantum_volume
145+
circuit = quantum_volume(5, 6, seed=10)
146+
circuit.draw('mpl')
147+
148+
**References:**
149+
150+
[1] A. Cross et al. Validating quantum computers using
151+
randomized model circuits, Phys. Rev. A 100, 032328 (2019).
152+
`arXiv:1811.12926 <https://arxiv.org/abs/1811.12926>`__
153+
"""
154+
if isinstance(seed, np.random.Generator):
155+
seed = seed.integers(0, dtype=np.uint64)
156+
depth = depth or num_qubits
157+
return QuantumCircuit._from_circuit_data(qv_rs(num_qubits, depth, seed))
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
features_circuits:
3+
- |
4+
Added a new function :func:`.quantum_volume` for generating a quantum volume
5+
:class:`.QuantumCircuit` object as defined in A. Cross et al. Validating quantum computers
6+
using randomized model circuits, Phys. Rev. A 100, 032328 (2019)
7+
`https://link.aps.org/doi/10.1103/PhysRevA.100.032328 <https://link.aps.org/doi/10.1103/PhysRevA.100.032328>`__.
8+
This new function differs from the existing :class:`.QuantumVolume` class in that it returns
9+
a :class:`.QuantumCircuit` object instead of building a subclass object. The second is
10+
that this new function is multithreaded and implemented in rust so it generates the output
11+
circuit ~10x faster than the :class:`.QuantumVolume` class.

test/python/circuit/library/test_quantum_volume.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from test.utils.base import QiskitTestCase
1818
from qiskit.circuit.library import QuantumVolume
19+
from qiskit.circuit.library.quantum_volume import quantum_volume
1920

2021

2122
class TestQuantumVolumeLibrary(QiskitTestCase):
@@ -35,6 +36,20 @@ def test_qv_seed_reproducibility(self):
3536
right = QuantumVolume(4, 4, seed=2024, flatten=True)
3637
self.assertEqual(left, right)
3738

39+
def test_qv_function_seed_reproducibility(self):
40+
"""Test qv circuit."""
41+
left = quantum_volume(10, 10, seed=128)
42+
right = quantum_volume(10, 10, seed=128)
43+
self.assertEqual(left, right)
44+
45+
left = quantum_volume(10, 10, seed=256)
46+
right = quantum_volume(10, 10, seed=256)
47+
self.assertEqual(left, right)
48+
49+
left = quantum_volume(10, 10, seed=4196)
50+
right = quantum_volume(10, 10, seed=4196)
51+
self.assertEqual(left, right)
52+
3853

3954
if __name__ == "__main__":
4055
unittest.main()

0 commit comments

Comments
 (0)