Skip to content

Commit 0b8bceb

Browse files
Rewrite default UnitarySynthesis to cache decomposers (Qiskit#15492)
* Rewrite default `UnitarySynthesis` to cache decomposers This is a complete rewrite of the default `UnitarySynthesis` plugin to vastly improve the efficiency of 2q decomposition. This is good for approximately a 2x runtime improvement in the `UnitarySynthesis` pass, since before this commit, we were reconstructing each relevant 2q decomposer each time we had a new matrix to decompose. However, constructing a 2q KAK decomposer is about as expensive as using a constructed KAK decomposer on a single matrix. This commit caches the available decomposers for each encountered pair of qubits, and allows the cache to be persisted between calls to `run_unitary_synthesis`. Before the move of the default unitary synthesis plugin to Rust, we effectively had decomposer caching for the "loose constraint" (basis gates + coupling map) hardware description, since we just chose a single decomposer on initialisation and used it throughout. The `Target` form in Python space cached at the `qargs` level, which meant that multiple unitaries on the same qargs pair would use the same set of decomposers, but each qargs pair would be calculated separately on first access, still (generally) leading to multiple constructions of the same decomposer. Both of these types of caching were lost in the move to Rust, but the effects were largely masked by the total runtime still being drastically better than the Python-space versions. This new form reinstates all the previous caching, and additionally caches at the level of individual decomposer construction as well (by caching the arguments used to construct a decomposer), so that (mostly) homogeneous `Target`s will re-use the same decomposer whenever it is valid on more than one 2q link. * Give synthesis/state structs explicit names There's lots of types of "synthesis" in Qiskit... * Correct copyright years * Avoid magic numbers in `ApproximationDegree::is_approximate` Co-authored-by: Alexander Ivrii <alexi@il.ibm.com> --------- Co-authored-by: Alexander Ivrii <alexi@il.ibm.com>
1 parent 826b4f1 commit 0b8bceb

16 files changed

Lines changed: 1938 additions & 1689 deletions

File tree

crates/cext/src/transpiler/passes/unitary_synthesis.rs

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010
// copyright notice, and modified files need to carry a notice indicating
1111
// that they have been altered from the originals.
1212

13-
use hashbrown::HashSet;
14-
1513
use crate::pointers::{const_ptr_as_ref, mut_ptr_as_ref};
1614

15+
use qiskit_circuit::PhysicalQubit;
1716
use qiskit_circuit::circuit_data::CircuitData;
1817
use qiskit_circuit::converters::dag_to_circuit;
1918
use qiskit_circuit::dag_circuit::DAGCircuit;
20-
use qiskit_transpiler::passes::run_unitary_synthesis;
19+
use qiskit_transpiler::passes::{
20+
UnitarySynthesisConfig, UnitarySynthesisState, run_unitary_synthesis,
21+
};
2122
use qiskit_transpiler::target::Target;
2223

2324
/// @ingroup QkTranspilerPasses
@@ -75,7 +76,7 @@ pub unsafe extern "C" fn qk_transpiler_pass_standalone_unitary_synthesis(
7576
// SAFETY: Per documentation, the pointer is non-null and aligned.
7677
let circuit = unsafe { mut_ptr_as_ref(circuit) };
7778
let target = unsafe { const_ptr_as_ref(target) };
78-
let mut dag = match DAGCircuit::from_circuit_data(circuit, false, None, None, None, None) {
79+
let dag = match DAGCircuit::from_circuit_data(circuit, false, None, None, None, None) {
7980
Ok(dag) => dag,
8081
Err(e) => panic!("{}", e),
8182
};
@@ -84,19 +85,21 @@ pub unsafe extern "C" fn qk_transpiler_pass_standalone_unitary_synthesis(
8485
} else {
8586
Some(approximation_degree)
8687
};
87-
let qubit_indices = (0..dag.num_qubits()).collect();
88+
let physical_qubits = (0..dag.num_qubits() as u32)
89+
.map(PhysicalQubit::new)
90+
.collect::<Vec<_>>();
91+
let mut synthesis_state = UnitarySynthesisState::new(UnitarySynthesisConfig {
92+
approximation_degree,
93+
run_python_decomposers: false,
94+
..Default::default()
95+
});
8896
let out_dag = match run_unitary_synthesis(
89-
&mut dag,
90-
qubit_indices,
97+
&dag,
98+
&["unitary".to_string()].into_iter().collect(),
9199
min_qubits,
92-
Some(target),
93-
HashSet::new(),
94-
["unitary".to_string()].into_iter().collect(),
95-
HashSet::new(),
96-
approximation_degree,
97-
None,
98-
None,
99-
false,
100+
&physical_qubits,
101+
&mut synthesis_state,
102+
target.into(),
100103
) {
101104
Ok(dag) => dag,
102105
Err(e) => panic!("{}", e),

crates/cext/src/transpiler/transpile_function.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use std::ffi::c_char;
1515
use qiskit_circuit::circuit_data::CircuitData;
1616
use qiskit_circuit::dag_circuit::DAGCircuit;
1717
use qiskit_transpiler::commutation_checker::get_standard_commutation_checker;
18+
use qiskit_transpiler::passes::{UnitarySynthesisConfig, UnitarySynthesisState};
1819
use qiskit_transpiler::standard_equivalence_library::generate_standard_equivalence_library;
1920
use qiskit_transpiler::target::Target;
2021
use qiskit_transpiler::transpile;
@@ -164,13 +165,19 @@ pub unsafe extern "C" fn qk_transpile_stage_init(
164165
}
165166
Some(options.approximation_degree)
166167
};
168+
let mut synthesis_state = UnitarySynthesisState::new(UnitarySynthesisConfig {
169+
approximation_degree,
170+
run_python_decomposers: false,
171+
..Default::default()
172+
});
167173
let mut commutation_checker = get_standard_commutation_checker();
168174

169175
match init_stage(
170176
dag,
171177
target,
172178
options.optimization_level.into(),
173179
approximation_degree,
180+
&mut synthesis_state,
174181
&mut out_layout,
175182
&mut commutation_checker,
176183
) {
@@ -399,6 +406,11 @@ pub unsafe extern "C" fn qk_transpile_stage_optimization(
399406
}
400407
Some(options.approximation_degree)
401408
};
409+
let mut synthesis_state = UnitarySynthesisState::new(UnitarySynthesisConfig {
410+
approximation_degree,
411+
run_python_decomposers: false,
412+
..Default::default()
413+
});
402414
let mut equiv_lib = generate_standard_equivalence_library();
403415
let mut commutation_checker = get_standard_commutation_checker();
404416

@@ -407,6 +419,7 @@ pub unsafe extern "C" fn qk_transpile_stage_optimization(
407419
target,
408420
options.optimization_level.into(),
409421
approximation_degree,
422+
&mut synthesis_state,
410423
&mut commutation_checker,
411424
&mut equiv_lib,
412425
) {
@@ -500,9 +513,14 @@ pub unsafe extern "C" fn qk_transpile_stage_translation(
500513
}
501514
Some(options.approximation_degree)
502515
};
516+
let mut synthesis_state = UnitarySynthesisState::new(UnitarySynthesisConfig {
517+
approximation_degree,
518+
run_python_decomposers: false,
519+
..Default::default()
520+
});
503521
let mut equiv_lib = generate_standard_equivalence_library();
504522

505-
match translation_stage(dag, target, approximation_degree, &mut equiv_lib) {
523+
match translation_stage(dag, target, &mut synthesis_state, &mut equiv_lib) {
506524
Ok(_) => ExitCode::Success,
507525
Err(e) => {
508526
if !error.is_null() {

crates/circuit/src/dag_circuit.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8634,11 +8634,35 @@ impl DAGCircuitBuilder {
86348634
pub fn insert_qargs(&mut self, qargs: &[Qubit]) -> Interned<[Qubit]> {
86358635
self.dag.qargs_interner.insert(qargs)
86368636
}
8637+
/// Merge the `qargs` in a different [Interner] into this DAG, remapping the qubits.
8638+
///
8639+
/// This is useful for simplifying the direct mapping of [PackedInstruction]s from one DAG to
8640+
/// another, like in substitution methods, or rebuilding a new DAG out of a lot of smaller ones.
8641+
/// See [Interner::merge_map_slice] for more information on the mapping function.
8642+
pub fn merge_qargs(
8643+
&mut self,
8644+
other: &Interner<[Qubit]>,
8645+
map_fn: impl FnMut(&Qubit) -> Option<Qubit>,
8646+
) -> InternedMap<[Qubit]> {
8647+
self.dag.merge_qargs(other, map_fn)
8648+
}
86378649

86388650
/// Packs qargs into the circuit.
86398651
pub fn insert_cargs(&mut self, cargs: &[Clbit]) -> Interned<[Clbit]> {
86408652
self.dag.cargs_interner.insert(cargs)
86418653
}
8654+
/// Merge the `cargs` in a different [Interner] into this DAG, remapping the clbits.
8655+
///
8656+
/// This is useful for simplifying the direct mapping of [PackedInstruction]s from one DAG to
8657+
/// another, like in substitution methods, or rebuilding a new DAG out of a lot of smaller ones.
8658+
/// See [Interner::merge_map_slice] for more information on the mapping function.
8659+
pub fn merge_cargs(
8660+
&mut self,
8661+
other: &Interner<[Clbit]>,
8662+
map_fn: impl FnMut(&Clbit) -> Option<Clbit>,
8663+
) -> InternedMap<[Clbit]> {
8664+
self.dag.merge_cargs(other, map_fn)
8665+
}
86428666

86438667
/// Adds a new value to the global phase of the inner [DAGCircuit].
86448668
pub fn add_global_phase(&mut self, param: &Param) -> PyResult<()> {

crates/circuit/src/operations.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ impl<'a, 'py> FromPyObject<'a, 'py> for Param {
9393
}
9494

9595
impl Param {
96+
/// Get the float value, if one is stored.
97+
pub fn try_float(&self) -> Option<f64> {
98+
match self {
99+
Self::Float(f) => Some(*f),
100+
_ => None,
101+
}
102+
}
96103
pub fn eq(&self, other: &Param) -> PyResult<bool> {
97104
match [self, other] {
98105
[Self::Float(a), Self::Float(b)] => Ok(a == b),

crates/circuit/src/packed_instruction.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::operations::{
2626
use crate::{Block, Clbit, Qubit};
2727
use hashbrown::HashMap;
2828
use nalgebra::Matrix2;
29-
use ndarray::Array2;
29+
use ndarray::{Array2, CowArray, Ix2};
3030
use num_complex::Complex64;
3131
use pyo3::prelude::*;
3232
use pyo3::types::{PyDict, PyType};
@@ -428,6 +428,17 @@ impl PackedOperation {
428428
}
429429
}
430430

431+
/// Does this [PackedOperation] represent an explicit gate?
432+
///
433+
/// This can be either a [StandardGate] or a [PyGate].
434+
#[inline]
435+
pub fn is_gate(&self) -> bool {
436+
matches!(
437+
self.discriminant(),
438+
PackedOperationType::StandardGate | PackedOperationType::PyGate
439+
)
440+
}
441+
431442
/// Create a `PackedOperation` from a `StandardGate`.
432443
#[inline]
433444
pub fn from_standard_gate(standard: StandardGate) -> Self {
@@ -824,6 +835,10 @@ impl PackedInstruction {
824835
Ok(())
825836
}
826837

838+
/// Extract an owned `ndarray` matrix from this instruction, if available.
839+
///
840+
/// The returned value is always owned. If you may be able to handle a read-only reference, see
841+
/// [try_cow_array] instead.
827842
pub fn try_matrix(&self) -> Option<Array2<Complex64>> {
828843
match self.op.view() {
829844
OperationRef::StandardGate(g) => g.matrix(self.params_view()),
@@ -833,6 +848,19 @@ impl PackedInstruction {
833848
}
834849
}
835850

851+
/// Extract an `ndarray` matrix from this instruciton, if available.
852+
///
853+
/// The returned value will preferentially be a view, if the matrix already exists (e.g. for
854+
/// `Unitary`).
855+
pub fn try_cow_array(&self) -> Option<CowArray<Complex64, Ix2>> {
856+
match self.op.view() {
857+
OperationRef::StandardGate(g) => g.matrix(self.params_view()).map(CowArray::from),
858+
OperationRef::Gate(g) => g.matrix().map(CowArray::from),
859+
OperationRef::Unitary(u) => Some(CowArray::from(u.matrix_view())),
860+
_ => None,
861+
}
862+
}
863+
836864
/// Returns a static matrix for 1-qubit gates. Will return `None` when the gate is not 1-qubit.
837865
#[inline]
838866
pub fn try_matrix_as_static_1q(&self) -> Option<[[Complex64; 2]; 2]> {

crates/synthesis/src/discrete_basis/basic_approximations.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ impl Point for BasicPoint {
355355
///
356356
/// This struct allows to construct a tree of basic approximations and to query the closest
357357
/// sequence given an target sequence (or SO(3) matrix).
358-
#[derive(Debug)]
358+
#[derive(Clone, Debug)]
359359
pub struct BasicApproximations {
360360
/// All points as flattened SO(3) matrix stored in a R* tree. This does not include the
361361
/// sequence of gates, see ``approximations``.

crates/synthesis/src/discrete_basis/solovay_kitaev.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use super::math::{self, group_commutator_decomposition};
3333
/// This generates the basic approximation set once as R-tree and re-uses it for
3434
/// each queried decomposition.
3535
#[pyclass]
36+
#[derive(Clone, Debug)]
3637
pub struct SolovayKitaevSynthesis {
3738
/// The set of basic approximations.
3839
basic_approximations: BasicApproximations,

crates/synthesis/src/euler_one_qubit_decomposer.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ pub static EULER_BASIS_NAMES: [EulerBasis; EULER_BASIS_SIZE] = [
605605
];
606606

607607
/// A structure containing a set of supported `EulerBasis` for running 1q synthesis
608-
#[derive(Debug, Clone)]
608+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
609609
pub struct EulerBasisSet {
610610
basis: [bool; EULER_BASIS_SIZE],
611611
initialized: bool,
@@ -620,6 +620,24 @@ impl EulerBasisSet {
620620
}
621621
}
622622

623+
/// Get the set of supported bases given a function that marks whether each basis gate (by name)
624+
/// is supported.
625+
///
626+
/// The `is_supported` function may be called more than once for the same gate name.
627+
pub fn from_support(mut is_supported: impl FnMut(&str) -> bool) -> Self {
628+
let mut out = Self {
629+
basis: EULER_BASES.map(|basis| basis.iter().all(|gate| is_supported(gate))),
630+
initialized: true,
631+
};
632+
if out.basis_supported(EulerBasis::U321) && out.basis_supported(EulerBasis::U3) {
633+
out.remove(EulerBasis::U3);
634+
}
635+
if out.basis_supported(EulerBasis::ZSXX) && out.basis_supported(EulerBasis::ZSX) {
636+
out.remove(EulerBasis::ZSX);
637+
}
638+
out
639+
}
640+
623641
/// Return true if this has been initialized any basis is supported
624642
pub fn initialized(&self) -> bool {
625643
self.initialized

crates/synthesis/src/two_qubit_decompose.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,13 @@ impl TwoQubitGateSequence {
12791279
global_phase: 0.,
12801280
}
12811281
}
1282+
/// Create this sequence from the consituent parts.
1283+
pub fn from_sequence(gates: TwoQubitSequenceVec, global_phase: f64) -> Self {
1284+
Self {
1285+
gates,
1286+
global_phase,
1287+
}
1288+
}
12821289
}
12831290

12841291
impl Default for TwoQubitGateSequence {

crates/transpiler/src/passes/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ pub use remove_diagonal_gates_before_measure::{
9292
pub use remove_identity_equiv::{remove_identity_equiv_mod, run_remove_identity_equiv};
9393
pub use split_2q_unitaries::{run_split_2q_unitaries, split_2q_unitaries_mod};
9494
pub use substitute_pi4_rotations::{py_run_substitute_pi4_rotations, substitute_pi4_rotations_mod};
95-
pub use unitary_synthesis::{run_unitary_synthesis, unitary_synthesis_mod};
95+
pub use unitary_synthesis::{
96+
UnitarySynthesisConfig, UnitarySynthesisState, run_unitary_synthesis, unitary_synthesis_mod,
97+
};
9698
pub use unroll_3q_or_more::{run_unroll_3q_or_more, unroll_3q_or_more_mod};
9799
pub use vf2::{error_map_mod, vf2_layout_mod, vf2_layout_pass_average, vf2_layout_pass_exact};
98100
pub use wrap_angles::{run_wrap_angles, wrap_angles_mod};

0 commit comments

Comments
 (0)