From 1b35e8bddf9f3a8acc29537c9c213b502a944b4e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 6 Nov 2024 12:32:31 -0500 Subject: [PATCH] Oxidize the ConsolidateBlocks pass (#13368) * Oxidize the ConsolidateBlocks pass This commit ports the consolidate blocks pass to rust. The logic remains the same and this is just a straight porting. One optimization is that to remove the amount of python processing the Collect2qBlocks pass is no longer run as part of the preset pass managers and this is called directly in rust. This speeds up the pass because it avoids 3 crossing of the language boundary and also the intermediate creation of DAGNode python objects. The pass still supports running with Collect2qBlocks for backwards compatibility and will skip running the pass equivalent internally the field is present in the property set. There are potential improvements that can be investigated here such as avoiding in place dag contraction and moving to rebuilding the dag iteratively. Also changing the logic around estimated error (see #11659) to be more robust. But these can be left for follow up PRs as they change the logic. Realistically we should look at combining ConsolidateBlocks for it's current two usages with Split2qUnitaries and UnitarySynthesis into those passes for more efficiency. We can improve the performance and logic as part of that refactor. See #12007 for more details on this for UnitarySynthesis. Closes #12250 * Update test to count consolidate_blocks instead of collect_2q_blocks * Fix lint * Fix solovay kitaev test * Add release note * Restore 2q block collection for synthesis translation plugin * Add rust native substitute method * Fix final test failures * Remove release note and test change The test failure fixed by a test change was incorrect and masked a logic bug that was fixed in a subsequent commit. This commit reverts that change to the test and removes the release note attempting to document a fix for a bug that only existed during development of this PR. * Fix comment leftover from rust-analyzer * Remove unused code * Simplify control flow handling * Remove unnecessary clone from substitute_node * Preallocate block op names in replace_block_with_py_op * Remove more unused imports * Optimize linalg in block collection This commit reworks the logic to reduce the number of Kronecker products and 2q matrix multiplications we do as part of computing the unitary of the block. It now computes the 1q components individually with 1q matrix multiplications and only calls kron() and a 2q matmul when a 2q gate is encountered. This reduces the number of more expensive operations we need to perform and replaces them with a much faster 1q matmul. * Use static one qubit identity matrix * Remove unnecessary lifetime annotations * Add missing docstring to new rust method * Apply suggestions from code review Co-authored-by: Kevin Hartman * Fix lint * Add comment for MAX_2Q_DEPTH constant * Reuse block_qargs for each block Co-authored-by: Henry Zou <87874865+henryzou50@users.noreply.github.com> --------- Co-authored-by: Kevin Hartman Co-authored-by: Henry Zou <87874865+henryzou50@users.noreply.github.com> --- crates/accelerate/src/consolidate_blocks.rs | 319 +++++++++++ .../accelerate/src/convert_2q_block_matrix.rs | 172 +++--- .../src/euler_one_qubit_decomposer.rs | 3 +- crates/accelerate/src/lib.rs | 1 + crates/accelerate/src/two_qubit_decompose.rs | 11 + crates/circuit/src/dag_circuit.rs | 499 +++++++++--------- crates/circuit/src/gate_matrix.rs | 6 + crates/pyext/src/lib.rs | 2 +- qiskit/__init__.py | 2 +- .../passes/optimization/consolidate_blocks.py | 170 ++---- .../preset_passmanagers/builtin_plugins.py | 4 - .../transpiler/preset_passmanagers/common.py | 4 +- .../rust-consolidation-a791a00380fc78b8.yaml | 13 + .../transpiler/test_preset_passmanagers.py | 12 +- 14 files changed, 760 insertions(+), 458 deletions(-) create mode 100644 crates/accelerate/src/consolidate_blocks.rs create mode 100644 releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml diff --git a/crates/accelerate/src/consolidate_blocks.rs b/crates/accelerate/src/consolidate_blocks.rs new file mode 100644 index 000000000000..1edd592ce877 --- /dev/null +++ b/crates/accelerate/src/consolidate_blocks.rs @@ -0,0 +1,319 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use hashbrown::{HashMap, HashSet}; +use ndarray::{aview2, Array2}; +use num_complex::Complex64; +use numpy::{IntoPyArray, PyReadonlyArray2}; +use pyo3::intern; +use pyo3::prelude::*; +use rustworkx_core::petgraph::stable_graph::NodeIndex; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::dag_circuit::DAGCircuit; +use qiskit_circuit::gate_matrix::{ONE_QUBIT_IDENTITY, TWO_QUBIT_IDENTITY}; +use qiskit_circuit::imports::{QI_OPERATOR, QUANTUM_CIRCUIT, UNITARY_GATE}; +use qiskit_circuit::operations::{Operation, Param}; +use qiskit_circuit::Qubit; + +use crate::convert_2q_block_matrix::{blocks_to_matrix, get_matrix_from_inst}; +use crate::euler_one_qubit_decomposer::matmul_1q; +use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::Target; +use crate::two_qubit_decompose::TwoQubitBasisDecomposer; + +fn is_supported( + target: Option<&Target>, + basis_gates: Option<&HashSet>, + name: &str, + qargs: &[Qubit], +) -> bool { + match target { + Some(target) => { + let physical_qargs = qargs.iter().map(|bit| PhysicalQubit(bit.0)).collect(); + target.instruction_supported(name, Some(&physical_qargs)) + } + None => match basis_gates { + Some(basis_gates) => basis_gates.contains(name), + None => true, + }, + } +} + +// If depth > 20, there will be 1q gates to consolidate. +const MAX_2Q_DEPTH: usize = 20; + +#[allow(clippy::too_many_arguments)] +#[pyfunction] +#[pyo3(signature = (dag, decomposer, force_consolidate, target=None, basis_gates=None, blocks=None, runs=None))] +pub(crate) fn consolidate_blocks( + py: Python, + dag: &mut DAGCircuit, + decomposer: &TwoQubitBasisDecomposer, + force_consolidate: bool, + target: Option<&Target>, + basis_gates: Option>, + blocks: Option>>, + runs: Option>>, +) -> PyResult<()> { + let blocks = match blocks { + Some(runs) => runs + .into_iter() + .map(|run| { + run.into_iter() + .map(NodeIndex::new) + .collect::>() + }) + .collect(), + // If runs are specified but blocks are none we're in a legacy configuration where external + // collection passes are being used. In this case don't collect blocks because it's + // unexpected. + None => match runs { + Some(_) => vec![], + None => dag.collect_2q_runs().unwrap(), + }, + }; + + let runs: Option>> = runs.map(|runs| { + runs.into_iter() + .map(|run| { + run.into_iter() + .map(NodeIndex::new) + .collect::>() + }) + .collect() + }); + let mut all_block_gates: HashSet = + HashSet::with_capacity(blocks.iter().map(|x| x.len()).sum()); + let mut block_qargs: HashSet = HashSet::with_capacity(2); + for block in blocks { + block_qargs.clear(); + if block.len() == 1 { + let inst_node = block[0]; + let inst = dag.dag()[inst_node].unwrap_operation(); + if !is_supported( + target, + basis_gates.as_ref(), + inst.op.name(), + dag.get_qargs(inst.qubits), + ) { + all_block_gates.insert(inst_node); + let matrix = match get_matrix_from_inst(py, inst) { + Ok(mat) => mat, + Err(_) => continue, + }; + let array = matrix.into_pyarray_bound(py); + let unitary_gate = UNITARY_GATE + .get_bound(py) + .call1((array, py.None(), false))?; + dag.substitute_node_with_py_op(py, inst_node, &unitary_gate, false)?; + continue; + } + } + let mut basis_count: usize = 0; + let mut outside_basis = false; + for node in &block { + let inst = dag.dag()[*node].unwrap_operation(); + block_qargs.extend(dag.get_qargs(inst.qubits)); + all_block_gates.insert(*node); + if inst.op.name() == decomposer.gate_name() { + basis_count += 1; + } + if !is_supported( + target, + basis_gates.as_ref(), + inst.op.name(), + dag.get_qargs(inst.qubits), + ) { + outside_basis = true; + } + } + if block_qargs.len() > 2 { + let mut qargs: Vec = block_qargs.iter().copied().collect(); + qargs.sort(); + let block_index_map: HashMap = qargs + .into_iter() + .enumerate() + .map(|(idx, qubit)| (qubit, idx)) + .collect(); + let circuit_data = CircuitData::from_packed_operations( + py, + block_qargs.len() as u32, + 0, + block.iter().map(|node| { + let inst = dag.dag()[*node].unwrap_operation(); + + Ok(( + inst.op.clone(), + inst.params_view().iter().cloned().collect(), + dag.get_qargs(inst.qubits) + .iter() + .map(|x| Qubit::new(block_index_map[x])) + .collect(), + vec![], + )) + }), + Param::Float(0.), + )?; + let circuit = QUANTUM_CIRCUIT + .get_bound(py) + .call_method1(intern!(py, "_from_circuit_data"), (circuit_data,))?; + let array = QI_OPERATOR + .get_bound(py) + .call1((circuit,))? + .getattr(intern!(py, "data"))? + .extract::>()?; + let matrix = array.as_array(); + let identity: Array2 = Array2::eye(2usize.pow(block_qargs.len() as u32)); + if approx::abs_diff_eq!(identity, matrix) { + for node in block { + dag.remove_op_node(node); + } + } else { + let unitary_gate = + UNITARY_GATE + .get_bound(py) + .call1((array.to_object(py), py.None(), false))?; + let clbit_pos_map = HashMap::new(); + dag.replace_block_with_py_op( + py, + &block, + unitary_gate, + false, + &block_index_map, + &clbit_pos_map, + )?; + } + } else { + let block_index_map = [ + *block_qargs.iter().min().unwrap(), + *block_qargs.iter().max().unwrap(), + ]; + let matrix = blocks_to_matrix(py, dag, &block, block_index_map).ok(); + if let Some(matrix) = matrix { + if force_consolidate + || decomposer.num_basis_gates_inner(matrix.view()) < basis_count + || block.len() > MAX_2Q_DEPTH + || (basis_gates.is_some() && outside_basis) + || (target.is_some() && outside_basis) + { + if approx::abs_diff_eq!(aview2(&TWO_QUBIT_IDENTITY), matrix) { + for node in block { + dag.remove_op_node(node); + } + } else { + let array = matrix.into_pyarray_bound(py); + let unitary_gate = + UNITARY_GATE + .get_bound(py) + .call1((array, py.None(), false))?; + let qubit_pos_map = block_index_map + .into_iter() + .enumerate() + .map(|(idx, qubit)| (qubit, idx)) + .collect(); + let clbit_pos_map = HashMap::new(); + dag.replace_block_with_py_op( + py, + &block, + unitary_gate, + false, + &qubit_pos_map, + &clbit_pos_map, + )?; + } + } + } + } + } + if let Some(runs) = runs { + for run in runs { + if run.iter().any(|node| all_block_gates.contains(node)) { + continue; + } + let first_inst_node = run[0]; + let first_inst = dag.dag()[first_inst_node].unwrap_operation(); + let first_qubits = dag.get_qargs(first_inst.qubits); + + if run.len() == 1 + && !is_supported( + target, + basis_gates.as_ref(), + first_inst.op.name(), + first_qubits, + ) + { + let matrix = match get_matrix_from_inst(py, first_inst) { + Ok(mat) => mat, + Err(_) => continue, + }; + let array = matrix.into_pyarray_bound(py); + let unitary_gate = UNITARY_GATE + .get_bound(py) + .call1((array, py.None(), false))?; + dag.substitute_node_with_py_op(py, first_inst_node, &unitary_gate, false)?; + continue; + } + let qubit = first_qubits[0]; + let mut matrix = ONE_QUBIT_IDENTITY; + + let mut already_in_block = false; + for node in &run { + if all_block_gates.contains(node) { + already_in_block = true; + } + let gate = dag.dag()[*node].unwrap_operation(); + let operator = match get_matrix_from_inst(py, gate) { + Ok(mat) => mat, + Err(_) => { + // Set this to skip this run because we can't compute the matrix of the + // operation. + already_in_block = true; + break; + } + }; + matmul_1q(&mut matrix, operator); + } + if already_in_block { + continue; + } + if approx::abs_diff_eq!(aview2(&ONE_QUBIT_IDENTITY), aview2(&matrix)) { + for node in run { + dag.remove_op_node(node); + } + } else { + let array = aview2(&matrix).to_owned().into_pyarray_bound(py); + let unitary_gate = UNITARY_GATE + .get_bound(py) + .call1((array, py.None(), false))?; + let mut block_index_map: HashMap = HashMap::with_capacity(1); + block_index_map.insert(qubit, 0); + let clbit_pos_map = HashMap::new(); + dag.replace_block_with_py_op( + py, + &run, + unitary_gate, + false, + &block_index_map, + &clbit_pos_map, + )?; + } + } + } + + Ok(()) +} + +pub fn consolidate_blocks_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(consolidate_blocks))?; + Ok(()) +} diff --git a/crates/accelerate/src/convert_2q_block_matrix.rs b/crates/accelerate/src/convert_2q_block_matrix.rs index dc4d0b77c4a7..aefc5976e82f 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -12,102 +12,135 @@ use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::PyDict; -use pyo3::wrap_pyfunction; use pyo3::Python; use num_complex::Complex64; use numpy::ndarray::linalg::kron; use numpy::ndarray::{aview2, Array2, ArrayView2}; -use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2}; -use smallvec::SmallVec; +use numpy::PyReadonlyArray2; +use rustworkx_core::petgraph::stable_graph::NodeIndex; -use qiskit_circuit::bit_data::BitData; -use qiskit_circuit::circuit_instruction::CircuitInstruction; -use qiskit_circuit::dag_node::DAGOpNode; +use qiskit_circuit::dag_circuit::DAGCircuit; use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; use qiskit_circuit::imports::QI_OPERATOR; -use qiskit_circuit::operations::Operation; +use qiskit_circuit::operations::{Operation, OperationRef}; +use qiskit_circuit::packed_instruction::PackedInstruction; +use qiskit_circuit::Qubit; +use crate::euler_one_qubit_decomposer::matmul_1q; use crate::QiskitError; -fn get_matrix_from_inst<'py>( - py: Python<'py>, - inst: &'py CircuitInstruction, -) -> PyResult> { - if let Some(mat) = inst.operation.matrix(&inst.params) { +#[inline] +pub fn get_matrix_from_inst(py: Python, inst: &PackedInstruction) -> PyResult> { + if let Some(mat) = inst.op.matrix(inst.params_view()) { Ok(mat) - } else if inst.operation.try_standard_gate().is_some() { + } else if inst.op.try_standard_gate().is_some() { Err(QiskitError::new_err( "Parameterized gates can't be consolidated", )) - } else { + } else if let OperationRef::Gate(gate) = inst.op.view() { Ok(QI_OPERATOR .get_bound(py) - .call1((inst.get_operation(py)?,))? + .call1((gate.gate.clone_ref(py),))? .getattr(intern!(py, "data"))? .extract::>()? .as_array() .to_owned()) + } else { + Err(QiskitError::new_err( + "Can't compute matrix of non-unitary op", + )) } } /// Return the matrix Operator resulting from a block of Instructions. -#[pyfunction] -#[pyo3(text_signature = "(op_list, /")] pub fn blocks_to_matrix( py: Python, - op_list: Vec>, - block_index_map_dict: &Bound, -) -> PyResult>> { - // Build a BitData in block_index_map_dict order. block_index_map_dict is a dict of bits to - // indices mapping the order of the qargs in the block. There should only be 2 entries since - // there are only 2 qargs here (e.g. `{Qubit(): 0, Qubit(): 1}`) so we need to ensure that - // we added the qubits to bit data in the correct index order. - let mut index_map: Vec = (0..block_index_map_dict.len()).map(|_| py.None()).collect(); - for bit_tuple in block_index_map_dict.items() { - let (bit, index): (PyObject, usize) = bit_tuple.extract()?; - index_map[index] = bit; - } - let mut bit_map: BitData = BitData::new(py, "qargs".to_string()); - for bit in index_map { - bit_map.add(py, bit.bind(py), true)?; - } - let identity = aview2(&ONE_QUBIT_IDENTITY); - let first_node = &op_list[0]; - let input_matrix = get_matrix_from_inst(py, &first_node.instruction)?; - let mut matrix: Array2 = match bit_map - .map_bits(first_node.instruction.qubits.bind(py).iter())? - .collect::>() - .as_slice() - { - [0] => kron(&identity, &input_matrix), - [1] => kron(&input_matrix, &identity), - [0, 1] => input_matrix, - [1, 0] => change_basis(input_matrix.view()), - [] => Array2::eye(4), - _ => unreachable!(), + dag: &DAGCircuit, + op_list: &[NodeIndex], + block_index_map: [Qubit; 2], +) -> PyResult> { + let map_bits = |bit: &Qubit| -> u8 { + if *bit == block_index_map[0] { + 0 + } else { + 1 + } }; - for node in op_list.into_iter().skip(1) { - let op_matrix = get_matrix_from_inst(py, &node.instruction)?; - let q_list = bit_map - .map_bits(node.instruction.qubits.bind(py).iter())? - .map(|x| x as u8) - .collect::>(); - - let result = match q_list.as_slice() { - [0] => Some(kron(&identity, &op_matrix)), - [1] => Some(kron(&op_matrix, &identity)), - [1, 0] => Some(change_basis(op_matrix.view())), - [] => Some(Array2::eye(4)), - _ => None, - }; - matrix = match result { - Some(result) => result.dot(&matrix), - None => op_matrix.dot(&matrix), - }; + let mut qubit_0 = ONE_QUBIT_IDENTITY; + let mut qubit_1 = ONE_QUBIT_IDENTITY; + let mut one_qubit_components_modified = false; + let mut output_matrix: Option> = None; + for node in op_list { + let inst = dag.dag()[*node].unwrap_operation(); + let op_matrix = get_matrix_from_inst(py, inst)?; + match dag + .get_qargs(inst.qubits) + .iter() + .map(map_bits) + .collect::>() + .as_slice() + { + [0] => { + matmul_1q(&mut qubit_0, op_matrix); + one_qubit_components_modified = true; + } + [1] => { + matmul_1q(&mut qubit_1, op_matrix); + one_qubit_components_modified = true; + } + [0, 1] => { + if one_qubit_components_modified { + let one_qubits_combined = kron(&aview2(&qubit_1), &aview2(&qubit_0)); + output_matrix = Some(match output_matrix { + None => op_matrix.dot(&one_qubits_combined), + Some(current) => { + let temp = one_qubits_combined.dot(¤t); + op_matrix.dot(&temp) + } + }); + qubit_0 = ONE_QUBIT_IDENTITY; + qubit_1 = ONE_QUBIT_IDENTITY; + one_qubit_components_modified = false; + } else { + output_matrix = Some(match output_matrix { + None => op_matrix, + Some(current) => op_matrix.dot(¤t), + }); + } + } + [1, 0] => { + let matrix = change_basis(op_matrix.view()); + if one_qubit_components_modified { + let one_qubits_combined = kron(&aview2(&qubit_1), &aview2(&qubit_0)); + output_matrix = Some(match output_matrix { + None => matrix.dot(&one_qubits_combined), + Some(current) => matrix.dot(&one_qubits_combined.dot(¤t)), + }); + qubit_0 = ONE_QUBIT_IDENTITY; + qubit_1 = ONE_QUBIT_IDENTITY; + one_qubit_components_modified = false; + } else { + output_matrix = Some(match output_matrix { + None => matrix, + Some(current) => matrix.dot(¤t), + }); + } + } + _ => unreachable!(), + } } - Ok(matrix.into_pyarray_bound(py).unbind()) + Ok(match output_matrix { + Some(matrix) => { + if one_qubit_components_modified { + let one_qubits_combined = kron(&aview2(&qubit_1), &aview2(&qubit_0)); + one_qubits_combined.dot(&matrix) + } else { + matrix + } + } + None => kron(&aview2(&qubit_1), &aview2(&qubit_0)), + }) } /// Switches the order of qubits in a two qubit operation. @@ -123,8 +156,3 @@ pub fn change_basis(matrix: ArrayView2) -> Array2 { } trans_matrix } - -pub fn convert_2q_block_matrix(m: &Bound) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(blocks_to_matrix))?; - Ok(()) -} diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index b5ed6014faaa..eb53b8309b05 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -1242,7 +1242,8 @@ pub(crate) fn optimize_1q_gates_decomposition( Ok(()) } -fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2) { +#[inline(always)] +pub fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2) { *operator = [ [ other[[0, 0]] * operator[0][0] + other[[0, 1]] * operator[1][0], diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index a0f9a6d72731..a3b6e6fa6e69 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -25,6 +25,7 @@ pub mod circuit_library; pub mod commutation_analysis; pub mod commutation_cancellation; pub mod commutation_checker; +pub mod consolidate_blocks; pub mod convert_2q_block_matrix; pub mod dense_layout; pub mod edge_collections; diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index fb8c58baab9d..48ba1fd0ad59 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -1337,6 +1337,17 @@ pub struct TwoQubitBasisDecomposer { q2r: Array2, } impl TwoQubitBasisDecomposer { + /// Return the KAK gate name + pub fn gate_name(&self) -> &str { + self.gate.as_str() + } + + /// Compute the number of basis gates needed for a given unitary + pub fn num_basis_gates_inner(&self, unitary: ArrayView2) -> usize { + let u = unitary.into_faer_complex(); + __num_basis_gates(self.basis_decomposer.b, self.basis_fidelity, u) + } + fn decomp1_inner( &self, target: &TwoQubitWeylDecomposition, diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 135ac289fff1..10551963b6e1 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -2808,117 +2808,14 @@ def _format(operand): } let block_ids: Vec<_> = node_block.iter().map(|n| n.node.unwrap()).collect(); - - let mut block_op_names = Vec::new(); - let mut block_qargs: HashSet = HashSet::new(); - let mut block_cargs: HashSet = HashSet::new(); - for nd in &block_ids { - let weight = self.dag.node_weight(*nd); - match weight { - Some(NodeType::Operation(packed)) => { - block_op_names.push(packed.op.name().to_string()); - block_qargs.extend(self.qargs_interner.get(packed.qubits)); - block_cargs.extend(self.cargs_interner.get(packed.clbits)); - - if let Some(condition) = packed.condition() { - block_cargs.extend( - self.clbits.map_bits( - self.control_flow_module - .condition_resources(condition.bind(py))? - .clbits - .bind(py), - )?, - ); - continue; - } - - // Add classical bits from SwitchCaseOp, if applicable. - if let OperationRef::Instruction(op) = packed.op.view() { - if op.name() == "switch_case" { - let op_bound = op.instruction.bind(py); - let target = op_bound.getattr(intern!(py, "target"))?; - if target.is_instance(imports::CLBIT.get_bound(py))? { - block_cargs.insert(self.clbits.find(&target).unwrap()); - } else if target - .is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? - { - block_cargs.extend( - self.clbits - .map_bits(target.extract::>>()?)?, - ); - } else { - block_cargs.extend( - self.clbits.map_bits( - self.control_flow_module - .node_resources(&target)? - .clbits - .bind(py), - )?, - ); - } - } - } - } - Some(_) => { - return Err(DAGCircuitError::new_err( - "Nodes in 'node_block' must be of type 'DAGOpNode'.", - )) - } - None => { - return Err(DAGCircuitError::new_err( - "Node in 'node_block' not found in DAG.", - )) - } - } - } - - let mut block_qargs: Vec = block_qargs - .into_iter() - .filter(|q| qubit_pos_map.contains_key(q)) - .collect(); - block_qargs.sort_by_key(|q| qubit_pos_map[q]); - - let mut block_cargs: Vec = block_cargs - .into_iter() - .filter(|c| clbit_pos_map.contains_key(c)) - .collect(); - block_cargs.sort_by_key(|c| clbit_pos_map[c]); - - let py_op = op.extract::()?; - - if py_op.operation.num_qubits() as usize != block_qargs.len() { - return Err(DAGCircuitError::new_err(format!( - "Number of qubits in the replacement operation ({}) is not equal to the number of qubits in the block ({})!", py_op.operation.num_qubits(), block_qargs.len() - ))); - } - - let op_name = py_op.operation.name().to_string(); - let qubits = self.qargs_interner.insert_owned(block_qargs); - let clbits = self.cargs_interner.insert_owned(block_cargs); - let weight = NodeType::Operation(PackedInstruction { - op: py_op.operation, - qubits, - clbits, - params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), - extra_attrs: py_op.extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: op.unbind().into(), - }); - - let new_node = self - .dag - .contract_nodes(block_ids, weight, cycle_check) - .map_err(|e| match e { - ContractError::DAGWouldCycle => DAGCircuitError::new_err( - "Replacing the specified node block would introduce a cycle", - ), - })?; - - self.increment_op(op_name.as_str()); - for name in block_op_names { - self.decrement_op(name.as_str()); - } - + let new_node = self.replace_block_with_py_op( + py, + &block_ids, + op, + cycle_check, + &qubit_pos_map, + &clbit_pos_map, + )?; self.get_node(py, new_node) } @@ -3447,140 +3344,17 @@ def _format(operand): }; let py = op.py(); let node_index = node.as_ref().node.unwrap(); - // Extract information from node that is going to be replaced - let old_packed = match self.dag.node_weight(node_index) { - Some(NodeType::Operation(old_packed)) => old_packed.clone(), - Some(_) => { - return Err(DAGCircuitError::new_err( - "'node' must be of type 'DAGOpNode'.", - )) - } - None => return Err(DAGCircuitError::new_err("'node' not found in DAG.")), - }; - // Extract information from new op - let new_op = op.extract::()?; - let current_wires: HashSet = self - .dag - .edges(node_index) - .map(|e| e.weight().clone()) - .collect(); - let mut new_wires: HashSet = self - .qargs_interner - .get(old_packed.qubits) - .iter() - .map(|x| Wire::Qubit(*x)) - .chain( - self.cargs_interner - .get(old_packed.clbits) - .iter() - .map(|x| Wire::Clbit(*x)), - ) - .collect(); - let (additional_clbits, additional_vars) = - self.additional_wires(py, new_op.operation.view(), new_op.extra_attrs.condition())?; - new_wires.extend(additional_clbits.iter().map(|x| Wire::Clbit(*x))); - new_wires.extend( - additional_vars - .iter() - .map(|x| Wire::Var(self.vars.find(x.bind(py)).unwrap())), - ); - - if old_packed.op.num_qubits() != new_op.operation.num_qubits() - || old_packed.op.num_clbits() != new_op.operation.num_clbits() - { - return Err(DAGCircuitError::new_err( - format!( - "Cannot replace node of width ({} qubits, {} clbits) with operation of mismatched width ({} qubits, {} clbits)", - old_packed.op.num_qubits(), old_packed.op.num_clbits(), new_op.operation.num_qubits(), new_op.operation.num_clbits() - ))); - } - - #[cfg(feature = "cache_pygates")] - let mut py_op_cache = Some(op.clone().unbind()); - - let mut extra_attrs = new_op.extra_attrs.clone(); - // If either operation is a control-flow operation, propagate_condition is ignored - if propagate_condition - && !(node.instruction.operation.control_flow() || new_op.operation.control_flow()) - { - // if new_op has a condition, the condition can't be propagated from the old node - if new_op.extra_attrs.condition().is_some() { - return Err(DAGCircuitError::new_err( - "Cannot propagate a condition to an operation that already has one.", - )); - } - if let Some(old_condition) = old_packed.condition() { - if matches!(new_op.operation.view(), OperationRef::Operation(_)) { - return Err(DAGCircuitError::new_err( - "Cannot add a condition on a generic Operation.", - )); - } - extra_attrs.set_condition(Some(old_condition.clone_ref(py))); - - let binding = self - .control_flow_module - .condition_resources(old_condition.bind(py))?; - let condition_clbits = binding.clbits.bind(py); - for bit in condition_clbits { - new_wires.insert(Wire::Clbit(self.clbits.find(&bit).unwrap())); - } - let op_ref = new_op.operation.view(); - if let OperationRef::Instruction(inst) = op_ref { - inst.instruction - .bind(py) - .setattr(intern!(py, "condition"), old_condition)?; - } else if let OperationRef::Gate(gate) = op_ref { - gate.gate.bind(py).call_method1( - intern!(py, "c_if"), - old_condition.downcast_bound::(py)?, - )?; - } - #[cfg(feature = "cache_pygates")] - { - py_op_cache = None; - } - } - }; - if new_wires != current_wires { - // The new wires must be a non-strict subset of the current wires; if they add new - // wires, we'd not know where to cut the existing wire to insert the new dependency. - return Err(DAGCircuitError::new_err(format!( - "New operation '{:?}' does not span the same wires as the old node '{:?}'. New wires: {:?}, old_wires: {:?}.", op.str(), old_packed.op.view(), new_wires, current_wires - ))); - } - + self.substitute_node_with_py_op(py, node_index, op, propagate_condition)?; if inplace { - node.instruction.operation = new_op.operation.clone(); - node.instruction.params = new_op.params.clone(); - node.instruction.extra_attrs = extra_attrs.clone(); + let new_weight = self.dag[node_index].unwrap_operation(); + let temp: OperationFromPython = op.extract()?; + node.instruction.operation = temp.operation; + node.instruction.params = new_weight.params_view().iter().cloned().collect(); + node.instruction.extra_attrs = new_weight.extra_attrs.clone(); #[cfg(feature = "cache_pygates")] { - node.instruction.py_op = py_op_cache - .as_ref() - .map(|ob| OnceCell::from(ob.clone_ref(py))) - .unwrap_or_default(); + node.instruction.py_op = new_weight.py_op.clone(); } - } - // Clone op data, as it will be moved into the PackedInstruction - let new_weight = NodeType::Operation(PackedInstruction { - op: new_op.operation.clone(), - qubits: old_packed.qubits, - clbits: old_packed.clbits, - params: (!new_op.params.is_empty()).then(|| new_op.params.into()), - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: py_op_cache.map(OnceCell::from).unwrap_or_default(), - }); - let node_index = node.as_ref().node.unwrap(); - if let Some(weight) = self.dag.node_weight_mut(node_index) { - *weight = new_weight; - } - - // Update self.op_names - self.decrement_op(old_packed.op.name()); - self.increment_op(new_op.operation.name()); - - if inplace { Ok(node.into_py(py)) } else { self.get_node(py, node_index) @@ -6988,6 +6762,249 @@ impl DAGCircuit { }; Self::from_circuit(py, circ, copy_op, None, None) } + + /// Replace a block of node indices with a new python operation + pub fn replace_block_with_py_op( + &mut self, + py: Python, + block_ids: &[NodeIndex], + op: Bound, + cycle_check: bool, + qubit_pos_map: &HashMap, + clbit_pos_map: &HashMap, + ) -> PyResult { + let mut block_op_names = Vec::with_capacity(block_ids.len()); + let mut block_qargs: HashSet = HashSet::new(); + let mut block_cargs: HashSet = HashSet::new(); + for nd in block_ids { + let weight = self.dag.node_weight(*nd); + match weight { + Some(NodeType::Operation(packed)) => { + block_op_names.push(packed.op.name().to_string()); + block_qargs.extend(self.qargs_interner.get(packed.qubits)); + block_cargs.extend(self.cargs_interner.get(packed.clbits)); + + if let Some(condition) = packed.condition() { + block_cargs.extend( + self.clbits.map_bits( + self.control_flow_module + .condition_resources(condition.bind(py))? + .clbits + .bind(py), + )?, + ); + continue; + } + + // Add classical bits from SwitchCaseOp, if applicable. + if let OperationRef::Instruction(op) = packed.op.view() { + if op.name() == "switch_case" { + let op_bound = op.instruction.bind(py); + let target = op_bound.getattr(intern!(py, "target"))?; + if target.is_instance(imports::CLBIT.get_bound(py))? { + block_cargs.insert(self.clbits.find(&target).unwrap()); + } else if target + .is_instance(imports::CLASSICAL_REGISTER.get_bound(py))? + { + block_cargs.extend( + self.clbits + .map_bits(target.extract::>>()?)?, + ); + } else { + block_cargs.extend( + self.clbits.map_bits( + self.control_flow_module + .node_resources(&target)? + .clbits + .bind(py), + )?, + ); + } + } + } + } + Some(_) => { + return Err(DAGCircuitError::new_err( + "Nodes in 'node_block' must be of type 'DAGOpNode'.", + )) + } + None => { + return Err(DAGCircuitError::new_err( + "Node in 'node_block' not found in DAG.", + )) + } + } + } + + let mut block_qargs: Vec = block_qargs + .into_iter() + .filter(|q| qubit_pos_map.contains_key(q)) + .collect(); + block_qargs.sort_by_key(|q| qubit_pos_map[q]); + + let mut block_cargs: Vec = block_cargs + .into_iter() + .filter(|c| clbit_pos_map.contains_key(c)) + .collect(); + block_cargs.sort_by_key(|c| clbit_pos_map[c]); + + let py_op = op.extract::()?; + + if py_op.operation.num_qubits() as usize != block_qargs.len() { + return Err(DAGCircuitError::new_err(format!( + "Number of qubits in the replacement operation ({}) is not equal to the number of qubits in the block ({})!", py_op.operation.num_qubits(), block_qargs.len() + ))); + } + + let op_name = py_op.operation.name().to_string(); + let qubits = self.qargs_interner.insert_owned(block_qargs); + let clbits = self.cargs_interner.insert_owned(block_cargs); + let weight = NodeType::Operation(PackedInstruction { + op: py_op.operation, + qubits, + clbits, + params: (!py_op.params.is_empty()).then(|| Box::new(py_op.params)), + extra_attrs: py_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: op.unbind().into(), + }); + + let new_node = self + .dag + .contract_nodes(block_ids.iter().copied(), weight, cycle_check) + .map_err(|e| match e { + ContractError::DAGWouldCycle => DAGCircuitError::new_err( + "Replacing the specified node block would introduce a cycle", + ), + })?; + + self.increment_op(op_name.as_str()); + for name in block_op_names { + self.decrement_op(name.as_str()); + } + Ok(new_node) + } + + /// Substitute a give node in the dag with a new operation from python + pub fn substitute_node_with_py_op( + &mut self, + py: Python, + node_index: NodeIndex, + op: &Bound, + propagate_condition: bool, + ) -> PyResult<()> { + // Extract information from node that is going to be replaced + let old_packed = self.dag[node_index].unwrap_operation(); + let op_name = old_packed.op.name().to_string(); + // Extract information from new op + let new_op = op.extract::()?; + let current_wires: HashSet = self + .dag + .edges(node_index) + .map(|e| e.weight().clone()) + .collect(); + let mut new_wires: HashSet = self + .qargs_interner + .get(old_packed.qubits) + .iter() + .map(|x| Wire::Qubit(*x)) + .chain( + self.cargs_interner + .get(old_packed.clbits) + .iter() + .map(|x| Wire::Clbit(*x)), + ) + .collect(); + let (additional_clbits, additional_vars) = + self.additional_wires(py, new_op.operation.view(), new_op.extra_attrs.condition())?; + new_wires.extend(additional_clbits.iter().map(|x| Wire::Clbit(*x))); + new_wires.extend( + additional_vars + .iter() + .map(|x| Wire::Var(self.vars.find(x.bind(py)).unwrap())), + ); + + if old_packed.op.num_qubits() != new_op.operation.num_qubits() + || old_packed.op.num_clbits() != new_op.operation.num_clbits() + { + return Err(DAGCircuitError::new_err( + format!( + "Cannot replace node of width ({} qubits, {} clbits) with operation of mismatched width ({} qubits, {} clbits)", + old_packed.op.num_qubits(), old_packed.op.num_clbits(), new_op.operation.num_qubits(), new_op.operation.num_clbits() + ))); + } + + #[cfg(feature = "cache_pygates")] + let mut py_op_cache = Some(op.clone().unbind()); + + let mut extra_attrs = new_op.extra_attrs.clone(); + // If either operation is a control-flow operation, propagate_condition is ignored + if propagate_condition && !(old_packed.op.control_flow() || new_op.operation.control_flow()) + { + // if new_op has a condition, the condition can't be propagated from the old node + if new_op.extra_attrs.condition().is_some() { + return Err(DAGCircuitError::new_err( + "Cannot propagate a condition to an operation that already has one.", + )); + } + if let Some(old_condition) = old_packed.condition() { + if matches!(new_op.operation.view(), OperationRef::Operation(_)) { + return Err(DAGCircuitError::new_err( + "Cannot add a condition on a generic Operation.", + )); + } + extra_attrs.set_condition(Some(old_condition.clone_ref(py))); + + let binding = self + .control_flow_module + .condition_resources(old_condition.bind(py))?; + let condition_clbits = binding.clbits.bind(py); + for bit in condition_clbits { + new_wires.insert(Wire::Clbit(self.clbits.find(&bit).unwrap())); + } + let op_ref = new_op.operation.view(); + if let OperationRef::Instruction(inst) = op_ref { + inst.instruction + .bind(py) + .setattr(intern!(py, "condition"), old_condition)?; + } else if let OperationRef::Gate(gate) = op_ref { + gate.gate.bind(py).call_method1( + intern!(py, "c_if"), + old_condition.downcast_bound::(py)?, + )?; + } + #[cfg(feature = "cache_pygates")] + { + py_op_cache = None; + } + } + }; + if new_wires != current_wires { + // The new wires must be a non-strict subset of the current wires; if they add new + // wires, we'd not know where to cut the existing wire to insert the new dependency. + return Err(DAGCircuitError::new_err(format!( + "New operation '{:?}' does not span the same wires as the old node '{:?}'. New wires: {:?}, old_wires: {:?}.", op.str(), old_packed.op.view(), new_wires, current_wires + ))); + } + let new_op_name = new_op.operation.name().to_string(); + let new_weight = NodeType::Operation(PackedInstruction { + op: new_op.operation, + qubits: old_packed.qubits, + clbits: old_packed.clbits, + params: (!new_op.params.is_empty()).then(|| new_op.params.into()), + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: py_op_cache.map(OnceCell::from).unwrap_or_default(), + }); + if let Some(weight) = self.dag.node_weight_mut(node_index) { + *weight = new_weight; + } + + // Update self.op_names + self.decrement_op(op_name.as_str()); + self.increment_op(new_op_name.as_str()); + Ok(()) + } } /// Add to global phase. Global phase can only be Float or ParameterExpression so this diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 6b04b8512fb0..c6eabf1064fe 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -19,6 +19,12 @@ use crate::util::{ }; pub static ONE_QUBIT_IDENTITY: GateArray1Q = [[C_ONE, C_ZERO], [C_ZERO, C_ONE]]; +pub static TWO_QUBIT_IDENTITY: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, C_ONE], +]; // Utility for generating static matrices for controlled gates with "n" control qubits. // Assumptions: diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 49070e85db70..2802098d8de9 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -35,7 +35,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::commutation_analysis::commutation_analysis, "commutation_analysis")?; add_submodule(m, ::qiskit_accelerate::commutation_cancellation::commutation_cancellation, "commutation_cancellation")?; add_submodule(m, ::qiskit_accelerate::commutation_checker::commutation_checker, "commutation_checker")?; - add_submodule(m, ::qiskit_accelerate::convert_2q_block_matrix::convert_2q_block_matrix, "convert_2q_block_matrix")?; + add_submodule(m, ::qiskit_accelerate::consolidate_blocks::consolidate_blocks_mod, "consolidate_blocks")?; add_submodule(m, ::qiskit_accelerate::dense_layout::dense_layout, "dense_layout")?; add_submodule(m, ::qiskit_accelerate::equivalence::equivalence, "equivalence")?; add_submodule(m, ::qiskit_accelerate::error_map::error_map, "error_map")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 202ebc32be85..b26b5d49f52a 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -58,7 +58,6 @@ sys.modules["qiskit._accelerate.converters"] = _accelerate.converters sys.modules["qiskit._accelerate.basis"] = _accelerate.basis sys.modules["qiskit._accelerate.basis.basis_translator"] = _accelerate.basis.basis_translator -sys.modules["qiskit._accelerate.convert_2q_block_matrix"] = _accelerate.convert_2q_block_matrix sys.modules["qiskit._accelerate.dense_layout"] = _accelerate.dense_layout sys.modules["qiskit._accelerate.equivalence"] = _accelerate.equivalence sys.modules["qiskit._accelerate.error_map"] = _accelerate.error_map @@ -97,6 +96,7 @@ sys.modules["qiskit._accelerate.commutation_checker"] = _accelerate.commutation_checker sys.modules["qiskit._accelerate.commutation_analysis"] = _accelerate.commutation_analysis sys.modules["qiskit._accelerate.commutation_cancellation"] = _accelerate.commutation_cancellation +sys.modules["qiskit._accelerate.consolidate_blocks"] = _accelerate.consolidate_blocks sys.modules["qiskit._accelerate.synthesis.linear_phase"] = _accelerate.synthesis.linear_phase sys.modules["qiskit._accelerate.synthesis.multi_controlled"] = ( _accelerate.synthesis.multi_controlled diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 49f227e8a746..63dca11f6d2d 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -13,26 +13,22 @@ """Replace each block of consecutive gates by a single Unitary node.""" from __future__ import annotations -import numpy as np - -from qiskit.circuit.classicalregister import ClassicalRegister -from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.dagcircuit.dagnode import DAGOpNode -from qiskit.quantum_info import Operator from qiskit.synthesis.two_qubit import TwoQubitBasisDecomposer -from qiskit.circuit.library.generalized_gates.unitary import UnitaryGate -from qiskit.circuit.library.standard_gates import CXGate +from qiskit.circuit.library.standard_gates import CXGate, CZGate, iSwapGate, ECRGate from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passmanager import PassManager -from qiskit.transpiler.passes.synthesis import unitary_synthesis -from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit._accelerate.convert_2q_block_matrix import blocks_to_matrix -from qiskit.exceptions import QiskitError +from qiskit._accelerate.consolidate_blocks import consolidate_blocks from .collect_1q_runs import Collect1qRuns from .collect_2q_blocks import Collect2qBlocks +KAK_GATE_NAMES = { + "cx": CXGate(), + "cz": CZGate(), + "iswap": iSwapGate(), + "ecr": ECRGate(), +} + class ConsolidateBlocks(TransformationPass): """Replace each block of consecutive gates by a single Unitary node. @@ -78,9 +74,13 @@ def __init__( if kak_basis_gate is not None: self.decomposer = TwoQubitBasisDecomposer(kak_basis_gate) elif basis_gates is not None: - self.decomposer = unitary_synthesis._decomposer_2q_from_basis_gates( - basis_gates, approximation_degree=approximation_degree - ) + kak_gates = KAK_GATE_NAMES.keys() & (basis_gates or []) + if kak_gates: + self.decomposer = TwoQubitBasisDecomposer( + KAK_GATE_NAMES[kak_gates.pop()], basis_fidelity=approximation_degree or 1.0 + ) + else: + self.decomposer = TwoQubitBasisDecomposer(CXGate()) else: self.decomposer = TwoQubitBasisDecomposer(CXGate()) @@ -93,89 +93,22 @@ def run(self, dag): if self.decomposer is None: return dag - blocks = self.property_set["block_list"] or [] - basis_gate_name = self.decomposer.gate.name - all_block_gates = set() - for block in blocks: - if len(block) == 1 and self._check_not_in_basis(dag, block[0].name, block[0].qargs): - all_block_gates.add(block[0]) - dag.substitute_node(block[0], UnitaryGate(block[0].op.to_matrix())) - else: - basis_count = 0 - outside_basis = False - block_qargs = set() - block_cargs = set() - for nd in block: - block_qargs |= set(nd.qargs) - if isinstance(nd, DAGOpNode) and getattr(nd, "condition", None): - block_cargs |= set(getattr(nd, "condition", None)[0]) - all_block_gates.add(nd) - block_index_map = self._block_qargs_to_indices(dag, block_qargs) - for nd in block: - if nd.name == basis_gate_name: - basis_count += 1 - if self._check_not_in_basis(dag, nd.name, nd.qargs): - outside_basis = True - if len(block_qargs) > 2: - q = QuantumRegister(len(block_qargs)) - qc = QuantumCircuit(q) - if block_cargs: - c = ClassicalRegister(len(block_cargs)) - qc.add_register(c) - for nd in block: - qc.append(nd.op, [q[block_index_map[i]] for i in nd.qargs]) - unitary = UnitaryGate(Operator(qc), check_input=False) - else: - try: - matrix = blocks_to_matrix(block, block_index_map) - except QiskitError: - # If building a matrix for the block fails we should not consolidate it - # because there is nothing we can do with it. - continue - unitary = UnitaryGate(matrix, check_input=False) - - max_2q_depth = 20 # If depth > 20, there will be 1q gates to consolidate. - if ( # pylint: disable=too-many-boolean-expressions - self.force_consolidate - or unitary.num_qubits > 2 - or self.decomposer.num_basis_gates(matrix) < basis_count - or len(block) > max_2q_depth - or ((self.basis_gates is not None) and outside_basis) - or ((self.target is not None) and outside_basis) - ): - identity = np.eye(2**unitary.num_qubits) - if np.allclose(identity, unitary.to_matrix()): - for node in block: - dag.remove_op_node(node) - else: - dag.replace_block_with_op( - block, unitary, block_index_map, cycle_check=False - ) - # If 1q runs are collected before consolidate those too - runs = self.property_set["run_list"] or [] - identity_1q = np.eye(2) - for run in runs: - if any(gate in all_block_gates for gate in run): - continue - if len(run) == 1 and not self._check_not_in_basis(dag, run[0].name, run[0].qargs): - dag.substitute_node(run[0], UnitaryGate(run[0].op.to_matrix(), check_input=False)) - else: - qubit = run[0].qargs[0] - operator = run[0].op.to_matrix() - already_in_block = False - for gate in run[1:]: - if gate in all_block_gates: - already_in_block = True - operator = gate.op.to_matrix().dot(operator) - if already_in_block: - continue - unitary = UnitaryGate(operator, check_input=False) - if np.allclose(identity_1q, unitary.to_matrix()): - for node in run: - dag.remove_op_node(node) - else: - dag.replace_block_with_op(run, unitary, {qubit: 0}, cycle_check=False) - + blocks = self.property_set["block_list"] + if blocks is not None: + blocks = [[node._node_id for node in block] for block in blocks] + runs = self.property_set["run_list"] + if runs is not None: + runs = [[node._node_id for node in run] for run in runs] + + consolidate_blocks( + dag, + self.decomposer._inner_decomposer, + self.force_consolidate, + target=self.target, + basis_gates=self.basis_gates, + blocks=blocks, + runs=runs, + ) dag = self._handle_control_flow_ops(dag) # Clear collected blocks and runs as they are no longer valid after consolidation @@ -195,38 +128,15 @@ def _handle_control_flow_ops(self, dag): pass_manager = PassManager() if "run_list" in self.property_set: pass_manager.append(Collect1qRuns()) - if "block_list" in self.property_set: pass_manager.append(Collect2qBlocks()) pass_manager.append(self) - for node in dag.op_nodes(): - if node.name not in CONTROL_FLOW_OP_NAMES: - continue - dag.substitute_node( - node, - node.op.replace_blocks(pass_manager.run(block) for block in node.op.blocks), - propagate_condition=False, - ) + control_flow_nodes = dag.control_flow_op_nodes() + if control_flow_nodes is not None: + for node in control_flow_nodes: + dag.substitute_node( + node, + node.op.replace_blocks(pass_manager.run(block) for block in node.op.blocks), + propagate_condition=False, + ) return dag - - def _check_not_in_basis(self, dag, gate_name, qargs): - if self.target is not None: - return not self.target.instruction_supported( - gate_name, tuple(dag.find_bit(qubit).index for qubit in qargs) - ) - else: - return self.basis_gates and gate_name not in self.basis_gates - - def _block_qargs_to_indices(self, dag, block_qargs): - """Map each qubit in block_qargs to its wire position among the block's wires. - Args: - block_qargs (list): list of qubits that a block acts on - global_index_map (dict): mapping from each qubit in the - circuit to its wire position within that circuit - Returns: - dict: mapping from qarg to position in block - """ - block_indices = [dag.find_bit(q).index for q in block_qargs] - ordered_block_indices = {bit: index for index, bit in enumerate(sorted(block_indices))} - block_positions = {q: ordered_block_indices[dag.find_bit(q).index] for q in block_qargs} - return block_positions diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 9301588c0744..8c928a47f69d 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -40,7 +40,6 @@ from qiskit.transpiler.passes.optimization import ( Optimize1qGatesDecomposition, CommutativeCancellation, - Collect2qBlocks, ConsolidateBlocks, InverseCancellation, ) @@ -176,7 +175,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana ) ) init.append(CommutativeCancellation()) - init.append(Collect2qBlocks()) init.append(ConsolidateBlocks()) # If approximation degree is None that indicates a request to approximate up to the # error rates in the target. However, in the init stage we don't yet know the target @@ -590,7 +588,6 @@ def _opt_control(property_set): elif optimization_level == 3: # Steps for optimization level 3 _opt = [ - Collect2qBlocks(), ConsolidateBlocks( basis_gates=pass_manager_config.basis_gates, target=pass_manager_config.target, @@ -634,7 +631,6 @@ def _unroll_condition(property_set): elif optimization_level == 2: optimization.append( [ - Collect2qBlocks(), ConsolidateBlocks( basis_gates=pass_manager_config.basis_gates, target=pass_manager_config.target, diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 25d21880bd23..c9bcc9a7904c 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -24,9 +24,9 @@ from qiskit.transpiler.passes import Error from qiskit.transpiler.passes import BasisTranslator from qiskit.transpiler.passes import Unroll3qOrMore -from qiskit.transpiler.passes import Collect2qBlocks -from qiskit.transpiler.passes import Collect1qRuns from qiskit.transpiler.passes import ConsolidateBlocks +from qiskit.transpiler.passes import Collect1qRuns +from qiskit.transpiler.passes import Collect2qBlocks from qiskit.transpiler.passes import UnitarySynthesis from qiskit.transpiler.passes import HighLevelSynthesis from qiskit.transpiler.passes import CheckMap diff --git a/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml b/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml new file mode 100644 index 000000000000..c84128888951 --- /dev/null +++ b/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml @@ -0,0 +1,13 @@ +--- +features_transpiler: + - | + The :class:`.ConsolidateGates` pass will now run the equivalent of the + :class:`.Collect2qBlocks` pass internally if it was not run in a pass + manager prior to the pass. Previously it was required that + :class:`.Collect2qBlocks` or :class:`.Collect1qRuns` were run prior to + :class:`.ConsolidateBlocks` for :class:`.ConsolidateBlocks` to do + anything. By doing the collection internally the overhead of the pass + is reduced. If :class:`.Collect2qBlocks` or :class:`.Collect1qRuns` are + run prior to :class:`.ConsolidateBlocks` the collected runs by those + passes from the property set are used and there is no change in behavior + for the pass. diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index ce88f49560ed..d98bd18b6cbd 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -36,7 +36,7 @@ from qiskit.quantum_info import random_unitary from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.transpiler.preset_passmanagers import level0, level1, level2, level3 -from qiskit.transpiler.passes import Collect2qBlocks, GatesInBasis +from qiskit.transpiler.passes import ConsolidateBlocks, GatesInBasis from qiskit.transpiler.preset_passmanagers.builtin_plugins import OptimizationPassManager from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -270,16 +270,16 @@ def test_unroll_only_if_not_gates_in_basis(self): ) qv_circuit = QuantumVolume(3) gates_in_basis_true_count = 0 - collect_2q_blocks_count = 0 + consolidate_blocks_count = 0 # pylint: disable=unused-argument def counting_callback_func(pass_, dag, time, property_set, count): nonlocal gates_in_basis_true_count - nonlocal collect_2q_blocks_count + nonlocal consolidate_blocks_count if isinstance(pass_, GatesInBasis) and property_set["all_gates_in_basis"]: gates_in_basis_true_count += 1 - if isinstance(pass_, Collect2qBlocks): - collect_2q_blocks_count += 1 + if isinstance(pass_, ConsolidateBlocks): + consolidate_blocks_count += 1 transpile( qv_circuit, @@ -288,7 +288,7 @@ def counting_callback_func(pass_, dag, time, property_set, count): callback=counting_callback_func, translation_method="synthesis", ) - self.assertEqual(gates_in_basis_true_count + 2, collect_2q_blocks_count) + self.assertEqual(gates_in_basis_true_count + 2, consolidate_blocks_count) @ddt