From ed2b41b57ed47f5db0132877341bd1d4a82b601b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 11:11:45 -0400 Subject: [PATCH 01/23] 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 --- crates/accelerate/src/consolidate_blocks.rs | 308 ++++++++++++++++++ .../accelerate/src/convert_2q_block_matrix.rs | 94 +++--- .../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 | 241 +++++++------- crates/circuit/src/gate_matrix.rs | 6 + crates/pyext/src/lib.rs | 2 +- qiskit/__init__.py | 2 +- .../passes/optimization/consolidate_blocks.py | 122 ++----- .../preset_passmanagers/builtin_plugins.py | 4 - .../transpiler/preset_passmanagers/common.py | 2 - 12 files changed, 539 insertions(+), 257 deletions(-) create mode 100644 crates/accelerate/src/consolidate_blocks.rs diff --git a/crates/accelerate/src/consolidate_blocks.rs b/crates/accelerate/src/consolidate_blocks.rs new file mode 100644 index 00000000000..ba1c20bd989 --- /dev/null +++ b/crates/accelerate/src/consolidate_blocks.rs @@ -0,0 +1,308 @@ +// This code is part of Qiskit. > This file is not included in any crates, so… +// +// (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, + }, + } +} + +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(), + 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()); + for block in blocks { + 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 = get_matrix_from_inst(py, inst)?; + let array = matrix.into_pyarray_bound(py); + let unitary_gate = UNITARY_GATE + .get_bound(py) + .call1((array, py.None(), false))?; + dag.py_substitute_node( + dag.get_node(py, inst_node)?.bind(py), + &unitary_gate, + false, + false, + )?; + } + continue; + } + let mut basis_count: usize = 0; + let mut outside_basis = false; + let mut block_qargs: HashSet = HashSet::with_capacity(2); + 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)?; + if force_consolidate + || decomposer.num_basis_gates_inner(matrix.view()) < basis_count + || block.len() > MAX_2Q_DEPTH + || 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 { + if !is_supported( + target, + basis_gates.as_ref(), + first_inst.op.name(), + first_qubits, + ) { + let matrix = get_matrix_from_inst(py, first_inst)?; + let array = matrix.into_pyarray_bound(py); + let unitary_gate = + UNITARY_GATE + .get_bound(py) + .call1((array, py.None(), false))?; + dag.py_substitute_node( + dag.get_node(py, first_inst_node)?.bind(py), + &unitary_gate, + false, + false, + )?; + } + continue; + } + let qubit = first_qubits[0]; + let mut matrix = [ + [Complex64::new(1., 0.), Complex64::new(0., 0.)], + [Complex64::new(0., 0.), Complex64::new(1., 0.)], + ]; + + 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 = get_matrix_from_inst(py, gate)?; + 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 dc4d0b77c4a..83da485ce2c 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -12,72 +12,70 @@ 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::QiskitError; -fn get_matrix_from_inst<'py>( +#[inline] +pub fn get_matrix_from_inst<'py>( py: Python<'py>, - inst: &'py CircuitInstruction, + inst: &'py PackedInstruction, ) -> PyResult> { - if let Some(mat) = inst.operation.matrix(&inst.params) { + 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)?; - } + 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 + } + }; 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())? + let first_node = dag.dag()[op_list[0]].unwrap_operation(); + let input_matrix = get_matrix_from_inst(py, first_node)?; + let mut matrix: Array2 = match dag + .get_qargs(first_node.qubits) + .iter() + .map(map_bits) .collect::>() .as_slice() { @@ -88,14 +86,17 @@ pub fn blocks_to_matrix( [] => Array2::eye(4), _ => unreachable!(), }; - 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::>(); + for node in op_list.iter().skip(1) { + let inst = dag.dag()[*node].unwrap_operation(); + let op_matrix = get_matrix_from_inst(py, inst)?; - let result = match q_list.as_slice() { + let result = match dag + .get_qargs(inst.qubits) + .iter() + .map(map_bits) + .collect::>() + .as_slice() + { [0] => Some(kron(&identity, &op_matrix)), [1] => Some(kron(&op_matrix, &identity)), [1, 0] => Some(change_basis(op_matrix.view())), @@ -107,7 +108,7 @@ pub fn blocks_to_matrix( None => op_matrix.dot(&matrix), }; } - Ok(matrix.into_pyarray_bound(py).unbind()) + Ok(matrix) } /// Switches the order of qubits in a two qubit operation. @@ -123,8 +124,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 b5ed6014faa..eb53b8309b0 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 ed3b75d309d..8fd6328cea0 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -21,6 +21,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 fb8c58baab9..48ba1fd0ad5 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 73345f71008..6ef53da6353 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -2828,117 +2828,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) } @@ -6980,6 +6877,128 @@ 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::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.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) + } } /// 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 6b04b8512fb..c6eabf1064f 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 56054145513..ecd282391bb 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 63e5a2a48a4..0ff870ad310 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 49f227e8a74..8ea1726add3 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -22,17 +22,24 @@ 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._accelerate.consolidate_blocks import consolidate_blocks from qiskit.exceptions import QiskitError 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 +85,15 @@ 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 = set(basis_gates or []).intersection(KAK_GATE_NAMES.keys()) + kak_gate = None + if kak_gates: + kak_gate = KAK_GATE_NAMES[kak_gates.pop()] + self.decomposer = TwoQubitBasisDecomposer( + kak_gate, basis_fidelity=approximation_degree or 1.0 + ) + if kak_gate is None: + self.decomposer = TwoQubitBasisDecomposer(CXGate()) else: self.decomposer = TwoQubitBasisDecomposer(CXGate()) @@ -93,89 +106,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 diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 9301588c074..8c928a47f69 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 25d21880bd2..9d7daeda678 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -24,7 +24,6 @@ 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 UnitarySynthesis @@ -502,7 +501,6 @@ def generate_translation_passmanager( qubits_initially_zero=qubits_initially_zero, ), Unroll3qOrMore(target=target, basis_gates=basis_gates), - Collect2qBlocks(), Collect1qRuns(), ConsolidateBlocks( basis_gates=basis_gates, target=target, approximation_degree=approximation_degree From 155a574126df3af25e4f5506ac9b621ef24ca624 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 14:32:09 -0400 Subject: [PATCH 02/23] Update test to count consolidate_blocks instead of collect_2q_blocks --- test/python/transpiler/test_preset_passmanagers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 00fd8944061..8d6e2016a54 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 @@ -262,16 +262,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, @@ -280,7 +280,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 From d3e900b2f8c37694eebb2d5e9b7e3560b00b2d91 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 14:33:53 -0400 Subject: [PATCH 03/23] Fix lint --- .../passes/optimization/consolidate_blocks.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 8ea1726add3..1405adff5f1 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -13,22 +13,12 @@ """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, 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.consolidate_blocks import consolidate_blocks -from qiskit.exceptions import QiskitError from .collect_1q_runs import Collect1qRuns from .collect_2q_blocks import Collect2qBlocks From 29bb56976dba870a005482cfa0f5c07aaef809cd Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 14:55:26 -0400 Subject: [PATCH 04/23] Fix solovay kitaev test --- crates/accelerate/src/consolidate_blocks.rs | 9 ++++++++- test/python/transpiler/test_solovay_kitaev.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/accelerate/src/consolidate_blocks.rs b/crates/accelerate/src/consolidate_blocks.rs index ba1c20bd989..6a52e65aaa2 100644 --- a/crates/accelerate/src/consolidate_blocks.rs +++ b/crates/accelerate/src/consolidate_blocks.rs @@ -73,8 +73,15 @@ pub(crate) fn consolidate_blocks( .collect::>() }) .collect(), - None => dag.collect_2q_runs().unwrap(), + // 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| { diff --git a/test/python/transpiler/test_solovay_kitaev.py b/test/python/transpiler/test_solovay_kitaev.py index 62b811c8e3b..4a12bd7c866 100644 --- a/test/python/transpiler/test_solovay_kitaev.py +++ b/test/python/transpiler/test_solovay_kitaev.py @@ -93,7 +93,7 @@ def test_unitary_synthesis(self): circuit.x(1) _1q = Collect1qRuns() - _cons = ConsolidateBlocks() + _cons = ConsolidateBlocks(basis_gates=["h", "s", "cx"]) _synth = UnitarySynthesis(["h", "s"], method="sk") passes = PassManager([_1q, _cons, _synth]) compiled = passes.run(circuit) From e50fc1c294b1417be0fe9cd2e1ddadf4153be523 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 14:55:46 -0400 Subject: [PATCH 05/23] Add release note --- .../rust-consolidation-a791a00380fc78b8.yaml | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml diff --git a/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml b/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml new file mode 100644 index 00000000000..c3675ab20e2 --- /dev/null +++ b/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml @@ -0,0 +1,22 @@ +--- +features_transpiler: + - | + The :class:`.ConsolidateGates` pass will now run the 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. +fixes: + - | + Fixed an issue with the :class:`.ConsolidateBlocks` pass when it was + processing single qubit runs. Previously if a single qubit run found by + the :class:`.Collect1qRuns` pass consisteted of a single gate, the + substitution of that gate would occur if the gate was in the target if + it was specified. The intent of the logic was to substitute only if the + gate was outside of the target. This has been corrected so the behavior + is restored. From 8da1b2f088cb8cb30147abf7f9c9124bfb655960 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 15:54:13 -0400 Subject: [PATCH 06/23] Restore 2q block collection for synthesis translation plugin --- qiskit/transpiler/preset_passmanagers/common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 9d7daeda678..c9bcc9a7904 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -24,8 +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 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 @@ -501,6 +502,7 @@ def generate_translation_passmanager( qubits_initially_zero=qubits_initially_zero, ), Unroll3qOrMore(target=target, basis_gates=basis_gates), + Collect2qBlocks(), Collect1qRuns(), ConsolidateBlocks( basis_gates=basis_gates, target=target, approximation_degree=approximation_degree From 61b831fa31611ef74b52e8db8c7bd6be0a8aa340 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 16:08:16 -0400 Subject: [PATCH 07/23] Add rust native substitute method --- crates/accelerate/src/consolidate_blocks.rs | 15 +- crates/circuit/src/dag_circuit.rs | 250 ++++++++++---------- 2 files changed, 126 insertions(+), 139 deletions(-) diff --git a/crates/accelerate/src/consolidate_blocks.rs b/crates/accelerate/src/consolidate_blocks.rs index 6a52e65aaa2..3a470ab1518 100644 --- a/crates/accelerate/src/consolidate_blocks.rs +++ b/crates/accelerate/src/consolidate_blocks.rs @@ -91,7 +91,6 @@ pub(crate) fn consolidate_blocks( }) .collect() }); - let mut all_block_gates: HashSet = HashSet::with_capacity(blocks.iter().map(|x| x.len()).sum()); for block in blocks { @@ -110,12 +109,7 @@ pub(crate) fn consolidate_blocks( let unitary_gate = UNITARY_GATE .get_bound(py) .call1((array, py.None(), false))?; - dag.py_substitute_node( - dag.get_node(py, inst_node)?.bind(py), - &unitary_gate, - false, - false, - )?; + dag.substitute_node_with_py_op(py, inst_node, &unitary_gate, false)?; } continue; } @@ -255,12 +249,7 @@ pub(crate) fn consolidate_blocks( UNITARY_GATE .get_bound(py) .call1((array, py.None(), false))?; - dag.py_substitute_node( - dag.get_node(py, first_inst_node)?.bind(py), - &unitary_gate, - false, - false, - )?; + dag.substitute_node_with_py_op(py, first_inst_node, &unitary_gate, false)?; } continue; } diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 6ef53da6353..fe910dba0a4 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -3364,136 +3364,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(x.clone_ref(py)))); - - 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) @@ -6999,6 +6880,123 @@ impl DAGCircuit { } Ok(new_node) } + + 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(x.clone_ref(py)))); + + 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 + ))); + } + + // 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(), + }); + 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.operation.name()); + Ok(()) + } } /// Add to global phase. Global phase can only be Float or ParameterExpression so this From 55523bb766d7ad733a0f8445422148a743d6cab3 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 16:25:02 -0400 Subject: [PATCH 08/23] Fix final test failures --- crates/accelerate/src/consolidate_blocks.rs | 102 +++++++++++--------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/crates/accelerate/src/consolidate_blocks.rs b/crates/accelerate/src/consolidate_blocks.rs index 3a470ab1518..87509e8b924 100644 --- a/crates/accelerate/src/consolidate_blocks.rs +++ b/crates/accelerate/src/consolidate_blocks.rs @@ -104,14 +104,17 @@ pub(crate) fn consolidate_blocks( dag.get_qargs(inst.qubits), ) { all_block_gates.insert(inst_node); - let matrix = get_matrix_from_inst(py, inst)?; + 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; } - continue; } let mut basis_count: usize = 0; let mut outside_basis = false; @@ -193,36 +196,39 @@ pub(crate) fn consolidate_blocks( *block_qargs.iter().min().unwrap(), *block_qargs.iter().max().unwrap(), ]; - let matrix = blocks_to_matrix(py, dag, &block, block_index_map)?; - if force_consolidate - || decomposer.num_basis_gates_inner(matrix.view()) < basis_count - || block.len() > MAX_2Q_DEPTH - || outside_basis - { - if approx::abs_diff_eq!(aview2(&TWO_QUBIT_IDENTITY), matrix) { - for node in block { - dag.remove_op_node(node); + 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, + )?; } - } 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, - )?; } } } @@ -236,21 +242,23 @@ pub(crate) fn consolidate_blocks( let first_inst = dag.dag()[first_inst_node].unwrap_operation(); let first_qubits = dag.get_qargs(first_inst.qubits); - if run.len() == 1 { - if !is_supported( + if run.len() == 1 + && !is_supported( target, basis_gates.as_ref(), first_inst.op.name(), first_qubits, - ) { - let matrix = get_matrix_from_inst(py, first_inst)?; - 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)?; - } + ) + { + 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]; @@ -265,7 +273,15 @@ pub(crate) fn consolidate_blocks( already_in_block = true; } let gate = dag.dag()[*node].unwrap_operation(); - let operator = get_matrix_from_inst(py, gate)?; + 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 { From 8c43f011e55e010ae915f9084c5677c6a4a85fef Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 17:41:26 -0400 Subject: [PATCH 09/23] 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. --- .../notes/rust-consolidation-a791a00380fc78b8.yaml | 9 --------- test/python/transpiler/test_solovay_kitaev.py | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml b/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml index c3675ab20e2..89aa2d6377e 100644 --- a/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml +++ b/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml @@ -11,12 +11,3 @@ features_transpiler: 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. -fixes: - - | - Fixed an issue with the :class:`.ConsolidateBlocks` pass when it was - processing single qubit runs. Previously if a single qubit run found by - the :class:`.Collect1qRuns` pass consisteted of a single gate, the - substitution of that gate would occur if the gate was in the target if - it was specified. The intent of the logic was to substitute only if the - gate was outside of the target. This has been corrected so the behavior - is restored. diff --git a/test/python/transpiler/test_solovay_kitaev.py b/test/python/transpiler/test_solovay_kitaev.py index 4a12bd7c866..62b811c8e3b 100644 --- a/test/python/transpiler/test_solovay_kitaev.py +++ b/test/python/transpiler/test_solovay_kitaev.py @@ -93,7 +93,7 @@ def test_unitary_synthesis(self): circuit.x(1) _1q = Collect1qRuns() - _cons = ConsolidateBlocks(basis_gates=["h", "s", "cx"]) + _cons = ConsolidateBlocks() _synth = UnitarySynthesis(["h", "s"], method="sk") passes = PassManager([_1q, _cons, _synth]) compiled = passes.run(circuit) From b93df7629b8b577efbc34bb006a4e7c659b4f14c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 17:43:43 -0400 Subject: [PATCH 10/23] Fix comment leftover from rust-analyzer --- crates/accelerate/src/consolidate_blocks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/accelerate/src/consolidate_blocks.rs b/crates/accelerate/src/consolidate_blocks.rs index 87509e8b924..4d4de600463 100644 --- a/crates/accelerate/src/consolidate_blocks.rs +++ b/crates/accelerate/src/consolidate_blocks.rs @@ -1,4 +1,4 @@ -// This code is part of Qiskit. > This file is not included in any crates, so… +// This code is part of Qiskit. // // (C) Copyright IBM 2024 // From 0423d1ceb216251d2947ce2380d57d68d6dee652 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 25 Oct 2024 05:49:10 -0400 Subject: [PATCH 11/23] Remove unused code --- .../passes/optimization/consolidate_blocks.py | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 1405adff5f1..f8af842c5b7 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -144,25 +144,3 @@ def _handle_control_flow_ops(self, dag): 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 From f8841f2b3e1b83908097aed111baeba304c73da8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 25 Oct 2024 06:04:44 -0400 Subject: [PATCH 12/23] Simplify control flow handling --- .../passes/optimization/consolidate_blocks.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index f8af842c5b7..199dcc975e5 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -131,16 +131,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 From 700814e42dee59ff8dea2e6fa7545c22157f2df6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 25 Oct 2024 06:14:15 -0400 Subject: [PATCH 13/23] Remove unnecessary clone from substitute_node --- crates/circuit/src/dag_circuit.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index fe910dba0a4..2fc2a21f941 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -6977,10 +6977,9 @@ impl DAGCircuit { "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 ))); } - - // Clone op data, as it will be moved into the PackedInstruction + let new_op_name = new_op.operation.name().to_string(); let new_weight = NodeType::Operation(PackedInstruction { - op: new_op.operation.clone(), + op: new_op.operation, qubits: old_packed.qubits, clbits: old_packed.clbits, params: (!new_op.params.is_empty()).then(|| new_op.params.into()), @@ -6994,7 +6993,7 @@ impl DAGCircuit { // Update self.op_names self.decrement_op(op_name.as_str()); - self.increment_op(new_op.operation.name()); + self.increment_op(new_op_name.as_str()); Ok(()) } } From 5c4c50f06c06068eefb7ef49e5e81340168cb802 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 25 Oct 2024 06:16:45 -0400 Subject: [PATCH 14/23] Preallocate block op names in replace_block_with_py_op --- crates/circuit/src/dag_circuit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 2fc2a21f941..9f3c6ca7dfd 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -6769,7 +6769,7 @@ impl DAGCircuit { qubit_pos_map: &HashMap, clbit_pos_map: &HashMap, ) -> PyResult { - let mut block_op_names = Vec::new(); + 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 { From a42299019e78b3c93c2a989ee830a96c5576d678 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 25 Oct 2024 06:19:06 -0400 Subject: [PATCH 15/23] Remove more unused imports --- qiskit/transpiler/passes/optimization/consolidate_blocks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 199dcc975e5..f0c840c5d2b 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -17,7 +17,6 @@ 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.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit._accelerate.consolidate_blocks import consolidate_blocks from .collect_1q_runs import Collect1qRuns From 62df015e79c895499168c07eaaf42f4ce90aa03b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 25 Oct 2024 13:02:00 -0400 Subject: [PATCH 16/23] 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. --- .../accelerate/src/convert_2q_block_matrix.rs | 97 +++++++++++++------ 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/crates/accelerate/src/convert_2q_block_matrix.rs b/crates/accelerate/src/convert_2q_block_matrix.rs index 83da485ce2c..d1271204c49 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -27,6 +27,7 @@ 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; #[inline] @@ -69,46 +70,80 @@ pub fn blocks_to_matrix( 1 } }; - let identity = aview2(&ONE_QUBIT_IDENTITY); - let first_node = dag.dag()[op_list[0]].unwrap_operation(); - let input_matrix = get_matrix_from_inst(py, first_node)?; - let mut matrix: Array2 = match dag - .get_qargs(first_node.qubits) - .iter() - .map(map_bits) - .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!(), - }; - for node in op_list.iter().skip(1) { + 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)?; - - let result = match dag + match dag .get_qargs(inst.qubits) .iter() .map(map_bits) .collect::>() .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), - }; + [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) + 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. From 864fc51c44060eb5058017881f85ce72bfaecf0d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 5 Nov 2024 08:18:25 -0500 Subject: [PATCH 17/23] Use static one qubit identity matrix --- crates/accelerate/src/consolidate_blocks.rs | 5 +---- crates/circuit/src/dag_circuit.rs | 6 +++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/accelerate/src/consolidate_blocks.rs b/crates/accelerate/src/consolidate_blocks.rs index 4d4de600463..31f45dc58bd 100644 --- a/crates/accelerate/src/consolidate_blocks.rs +++ b/crates/accelerate/src/consolidate_blocks.rs @@ -262,10 +262,7 @@ pub(crate) fn consolidate_blocks( continue; } let qubit = first_qubits[0]; - let mut matrix = [ - [Complex64::new(1., 0.), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), Complex64::new(1., 0.)], - ]; + let mut matrix = ONE_QUBIT_IDENTITY; let mut already_in_block = false; for node in &run { diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 66f98b257cf..c2236842204 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -6917,7 +6917,11 @@ impl DAGCircuit { 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()))); + 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() From 11742b4653a3d0680b565c5cd5bd38b3866f1fba Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 5 Nov 2024 10:06:27 -0500 Subject: [PATCH 18/23] Remove unnecessary lifetime annotations --- crates/accelerate/src/convert_2q_block_matrix.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/accelerate/src/convert_2q_block_matrix.rs b/crates/accelerate/src/convert_2q_block_matrix.rs index d1271204c49..aefc5976e82 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -31,10 +31,7 @@ use crate::euler_one_qubit_decomposer::matmul_1q; use crate::QiskitError; #[inline] -pub fn get_matrix_from_inst<'py>( - py: Python<'py>, - inst: &'py PackedInstruction, -) -> PyResult> { +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.op.try_standard_gate().is_some() { From f1e645fa2c60ade6086d0f5b8d05770da2b3019b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 5 Nov 2024 10:12:05 -0500 Subject: [PATCH 19/23] Add missing docstring to new rust method --- crates/circuit/src/dag_circuit.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index c2236842204..efb4ee7b32e 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -6885,6 +6885,7 @@ impl DAGCircuit { 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, From b00e22c162f8d351a701d60e62c2d2cdb45d4f46 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 5 Nov 2024 10:15:13 -0500 Subject: [PATCH 20/23] Apply suggestions from code review Co-authored-by: Kevin Hartman --- crates/circuit/src/dag_circuit.rs | 2 +- .../transpiler/passes/optimization/consolidate_blocks.py | 7 +++---- .../notes/rust-consolidation-a791a00380fc78b8.yaml | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index efb4ee7b32e..10551963b6e 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -6763,7 +6763,7 @@ impl DAGCircuit { Self::from_circuit(py, circ, copy_op, None, None) } - // Replace a block of node indices with a new python operation + /// Replace a block of node indices with a new python operation pub fn replace_block_with_py_op( &mut self, py: Python, diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index f0c840c5d2b..2e8b63b90ce 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -74,14 +74,13 @@ def __init__( if kak_basis_gate is not None: self.decomposer = TwoQubitBasisDecomposer(kak_basis_gate) elif basis_gates is not None: - kak_gates = set(basis_gates or []).intersection(KAK_GATE_NAMES.keys()) + kak_gates = KAK_GATE_NAMES.keys() & (basis_gates or []) kak_gate = None if kak_gates: - kak_gate = KAK_GATE_NAMES[kak_gates.pop()] self.decomposer = TwoQubitBasisDecomposer( - kak_gate, basis_fidelity=approximation_degree or 1.0 + KAK_GATE_NAMES[kak_gates.pop()], basis_fidelity=approximation_degree or 1.0 ) - if kak_gate is None: + else: self.decomposer = TwoQubitBasisDecomposer(CXGate()) else: self.decomposer = TwoQubitBasisDecomposer(CXGate()) diff --git a/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml b/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml index 89aa2d6377e..c8412888895 100644 --- a/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml +++ b/releasenotes/notes/rust-consolidation-a791a00380fc78b8.yaml @@ -1,7 +1,7 @@ --- features_transpiler: - | - The :class:`.ConsolidateGates` pass will now run the the equivalent of the + 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 From b5b01721c297c59ee293199d70199020d4ec20c0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 5 Nov 2024 10:45:28 -0500 Subject: [PATCH 21/23] Fix lint --- qiskit/transpiler/passes/optimization/consolidate_blocks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 2e8b63b90ce..63dca11f6d2 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -75,7 +75,6 @@ def __init__( self.decomposer = TwoQubitBasisDecomposer(kak_basis_gate) elif basis_gates is not None: kak_gates = KAK_GATE_NAMES.keys() & (basis_gates or []) - kak_gate = None if kak_gates: self.decomposer = TwoQubitBasisDecomposer( KAK_GATE_NAMES[kak_gates.pop()], basis_fidelity=approximation_degree or 1.0 From 223bf2e4efd25011fa80320bdd26b80480fb19cd Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 5 Nov 2024 17:29:27 -0500 Subject: [PATCH 22/23] Add comment for MAX_2Q_DEPTH constant --- crates/accelerate/src/consolidate_blocks.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/accelerate/src/consolidate_blocks.rs b/crates/accelerate/src/consolidate_blocks.rs index 31f45dc58bd..e8bbc53d523 100644 --- a/crates/accelerate/src/consolidate_blocks.rs +++ b/crates/accelerate/src/consolidate_blocks.rs @@ -49,6 +49,7 @@ fn is_supported( } } +// If depth > 20, there will be 1q gates to consolidate. const MAX_2Q_DEPTH: usize = 20; #[allow(clippy::too_many_arguments)] From b6071eecdd286d2b734d92be21aee79592b47017 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 5 Nov 2024 17:33:18 -0500 Subject: [PATCH 23/23] Reuse block_qargs for each block Co-authored-by: Henry Zou <87874865+henryzou50@users.noreply.github.com> --- crates/accelerate/src/consolidate_blocks.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/accelerate/src/consolidate_blocks.rs b/crates/accelerate/src/consolidate_blocks.rs index e8bbc53d523..1edd592ce87 100644 --- a/crates/accelerate/src/consolidate_blocks.rs +++ b/crates/accelerate/src/consolidate_blocks.rs @@ -94,7 +94,9 @@ pub(crate) fn consolidate_blocks( }); 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(); @@ -119,7 +121,6 @@ pub(crate) fn consolidate_blocks( } let mut basis_count: usize = 0; let mut outside_basis = false; - let mut block_qargs: HashSet = HashSet::with_capacity(2); for node in &block { let inst = dag.dag()[*node].unwrap_operation(); block_qargs.extend(dag.get_qargs(inst.qubits));