From 170148c8194c1218b9ede1f13f704a78125303ab Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 21 Aug 2024 13:18:40 -0400 Subject: [PATCH 1/4] Fully port InverseCancellation to Rust This commit builds off of #12959 and the other data model in Rust infrastructure and migrates the InverseCancellation pass to operate fully in Rust. The full path of the transpiler pass now never leaves Rust until it has finished modifying the DAGCircuit. There is still some python interaction necessary to handle parts of the data model that are still in Python, mainly for handling parameter expressions. But otherwise the entirety of the pass operates in rust now. This is just a first pass at the migration here, it moves the pass to use loops in rust. The next steps here are to look at operating the pass in parallel. There is no data dependency between the optimizations being done for different inverse gates/pairs so we should be able to the throughput of the pass by leveraging multithreading to handle each inverse option in parallel. This commit does not attempt this though, because of the Python dependency and also the data structures around gates and the dag aren't really setup for multithreading yet and there likely will need to be some work to support that. Fixes #12271 Part of #12208 --- crates/accelerate/src/inverse_cancellation.rs | 193 ++++++++++++++++++ crates/accelerate/src/lib.rs | 1 + crates/circuit/src/dag_circuit.rs | 89 ++++---- crates/pyext/src/lib.rs | 4 +- qiskit/__init__.py | 1 + .../optimization/inverse_cancellation.py | 101 +-------- 6 files changed, 256 insertions(+), 133 deletions(-) create mode 100644 crates/accelerate/src/inverse_cancellation.rs diff --git a/crates/accelerate/src/inverse_cancellation.rs b/crates/accelerate/src/inverse_cancellation.rs new file mode 100644 index 000000000000..b150297f4917 --- /dev/null +++ b/crates/accelerate/src/inverse_cancellation.rs @@ -0,0 +1,193 @@ +// 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 ahash::RandomState; +use hashbrown::HashSet; +use indexmap::IndexMap; +use pyo3::prelude::*; +use rustworkx_core::petgraph::stable_graph::NodeIndex; + +use qiskit_circuit::circuit_instruction::OperationFromPython; +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; +use qiskit_circuit::operations::Operation; +use qiskit_circuit::packed_instruction::PackedInstruction; + +fn gate_eq(py: Python, gate_a: &PackedInstruction, gate_b: &OperationFromPython) -> PyResult { + if gate_a.op.name() != gate_b.operation.name() { + return Ok(false); + } + let a_params = gate_a.params_view(); + if a_params.len() != gate_b.params.len() { + return Ok(false); + } + let mut param_eq = true; + for (a, b) in a_params.iter().zip(&gate_b.params) { + if !a.is_close(py, b, 1e-10)? { + param_eq = false; + break; + } + } + Ok(param_eq) +} + +fn run_on_self_inverse( + py: Python, + dag: &mut DAGCircuit, + op_counts: &IndexMap, + self_inverse_gate_names: HashSet, + self_inverse_gates: Vec, +) -> PyResult<()> { + if !self_inverse_gate_names + .iter() + .any(|name| op_counts.contains_key(name)) + { + return Ok(()); + } + for gate in self_inverse_gates { + let gate_count = op_counts.get(gate.operation.name()).unwrap_or(&0); + if *gate_count <= 1 { + continue; + } + let mut collect_set: HashSet = HashSet::with_capacity(1); + collect_set.insert(gate.operation.name().to_string()); + let gate_runs: Vec> = dag.collect_runs(collect_set).unwrap().collect(); + for gate_cancel_run in gate_runs { + let mut partitions: Vec> = Vec::new(); + let mut chunk: Vec = Vec::new(); + let max_index = gate_cancel_run.len() - 1; + for (i, cancel_gate) in gate_cancel_run.iter().enumerate() { + let node = &dag.dag[*cancel_gate]; + if let NodeType::Operation(inst) = node { + if gate_eq(py, inst, &gate)? { + chunk.push(*cancel_gate); + } else { + let is_empty: bool = chunk.is_empty(); + if !is_empty { + partitions.push(std::mem::take(&mut chunk)); + } + continue; + } + if i == max_index { + partitions.push(std::mem::take(&mut chunk)); + } else { + let next_qargs = if let NodeType::Operation(next_inst) = + &dag.dag[gate_cancel_run[i + 1]] + { + next_inst.qubits + } else { + panic!("Not an op node") + }; + if inst.qubits != next_qargs { + partitions.push(std::mem::take(&mut chunk)); + } + } + } else { + panic!("Not an op node"); + } + } + for chunk in partitions { + if chunk.len() % 2 == 0 { + dag.remove_op_node(chunk[0]); + } + for node in &chunk[1..] { + dag.remove_op_node(*node); + } + } + } + } + Ok(()) +} +fn run_on_inverse_pairs( + py: Python, + dag: &mut DAGCircuit, + op_counts: &IndexMap, + inverse_gate_names: HashSet, + inverse_gates: Vec<[OperationFromPython; 2]>, +) -> PyResult<()> { + if !inverse_gate_names + .iter() + .any(|name| op_counts.contains_key(name)) + { + return Ok(()); + } + for pair in inverse_gates { + let gate_0_name = pair[0].operation.name(); + let gate_1_name = pair[1].operation.name(); + if !op_counts.contains_key(gate_0_name) || !op_counts.contains_key(gate_1_name) { + continue; + } + let names: HashSet = pair + .iter() + .map(|x| x.operation.name().to_string()) + .collect(); + let runs: Vec> = dag.collect_runs(names).unwrap().collect(); + for nodes in runs { + let mut i = 0; + while i < nodes.len() - 1 { + if let NodeType::Operation(inst) = &dag.dag[nodes[i]] { + if let NodeType::Operation(next_inst) = &dag.dag[nodes[i + 1]] { + if inst.qubits == next_inst.qubits + && ((gate_eq(py, inst, &pair[0])? && gate_eq(py, next_inst, &pair[1])?) + || (gate_eq(py, inst, &pair[1])? + && gate_eq(py, next_inst, &pair[0])?)) + { + dag.remove_op_node(nodes[i]); + dag.remove_op_node(nodes[i + 1]); + i += 2; + } else { + i += 1; + } + } else { + panic!("Not an op node") + } + } else { + panic!("Not an op node") + } + } + } + } + Ok(()) +} + +#[pyfunction] +pub fn inverse_cancellation( + py: Python, + dag: &mut DAGCircuit, + inverse_gates: Vec<[OperationFromPython; 2]>, + self_inverse_gates: Vec, + inverse_gate_names: HashSet, + self_inverse_gate_names: HashSet, +) -> PyResult<()> { + let op_counts = if !self_inverse_gate_names.is_empty() || !inverse_gate_names.is_empty() { + dag.count_ops(py, true)? + } else { + IndexMap::default() + }; + if !self_inverse_gate_names.is_empty() { + run_on_self_inverse( + py, + dag, + &op_counts, + self_inverse_gate_names, + self_inverse_gates, + )?; + } + if !inverse_gate_names.is_empty() { + run_on_inverse_pairs(py, dag, &op_counts, inverse_gate_names, inverse_gates)?; + } + Ok(()) +} + +pub fn inverse_cancellation_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(inverse_cancellation))?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 5414183d22cc..0aba530ab91a 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -20,6 +20,7 @@ pub mod dense_layout; pub mod edge_collections; pub mod error_map; pub mod euler_one_qubit_decomposer; +pub mod inverse_cancellation; pub mod isometry; pub mod nlayout; pub mod optimize_1q_gates; diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index afe6797596d9..9174fcf93e88 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -4570,45 +4570,9 @@ def _format(operand): /// /// Returns: /// Mapping[str, int]: a mapping of operation names to the number of times it appears. - #[pyo3(signature = (*, recurse=true))] - fn count_ops(&self, py: Python, recurse: bool) -> PyResult { - if !recurse || !self.has_control_flow() { - Ok(self.op_names.to_object(py)) - } else { - fn inner( - py: Python, - dag: &DAGCircuit, - counts: &mut HashMap, - ) -> PyResult<()> { - for (key, value) in dag.op_names.iter() { - counts - .entry(key.clone()) - .and_modify(|count| *count += value) - .or_insert(*value); - } - let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); - for node in dag.dag.node_weights() { - let NodeType::Operation(node) = node else { - continue; - }; - if !node.op.control_flow() { - continue; - } - let OperationRef::Instruction(inst) = node.op.view() else { - panic!("control flow op must be an instruction") - }; - let blocks = inst.instruction.bind(py).getattr("blocks")?; - for block in blocks.iter()? { - let inner_dag: &DAGCircuit = &circuit_to_dag.call1((block?,))?.extract()?; - inner(py, inner_dag, counts)?; - } - } - Ok(()) - } - let mut counts = HashMap::with_capacity(self.op_names.len()); - inner(py, self, &mut counts)?; - Ok(counts.to_object(py)) - } + #[pyo3(name = "count_ops", signature = (*, recurse=true))] + fn py_count_ops(&self, py: Python, recurse: bool) -> PyResult { + self.count_ops(py, recurse).map(|x| x.into_py(py)) } /// Count the occurrences of operation names on the longest path. @@ -4740,7 +4704,7 @@ def _format(operand): ("qubits", self.num_qubits().into_py(py)), ("bits", self.num_clbits().into_py(py)), ("factors", self.num_tensor_factors().into_py(py)), - ("operations", self.count_ops(py, true)?), + ("operations", self.py_count_ops(py, true)?), ])) } @@ -6277,6 +6241,51 @@ impl DAGCircuit { Err(DAGCircuitError::new_err("Specified node is not an op node")) } } + + pub fn count_ops( + &self, + py: Python, + recurse: bool, + ) -> PyResult> { + if !recurse || !self.has_control_flow() { + Ok(self.op_names.clone()) + } else { + fn inner( + py: Python, + dag: &DAGCircuit, + counts: &mut IndexMap, + ) -> PyResult<()> { + for (key, value) in dag.op_names.iter() { + counts + .entry(key.clone()) + .and_modify(|count| *count += value) + .or_insert(*value); + } + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); + for node in dag.dag.node_weights() { + let NodeType::Operation(node) = node else { + continue; + }; + if !node.op.control_flow() { + continue; + } + let OperationRef::Instruction(inst) = node.op.view() else { + panic!("control flow op must be an instruction") + }; + let blocks = inst.instruction.bind(py).getattr("blocks")?; + for block in blocks.iter()? { + let inner_dag: &DAGCircuit = &circuit_to_dag.call1((block?,))?.extract()?; + inner(py, inner_dag, counts)?; + } + } + Ok(()) + } + let mut counts = + IndexMap::with_capacity_and_hasher(self.op_names.len(), RandomState::default()); + inner(py, self, &mut counts)?; + Ok(counts) + } + } } /// Add to global phase. Global phase can only be Float or ParameterExpression so this diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index e8971bc87629..8d201b6170c3 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -15,7 +15,8 @@ use pyo3::prelude::*; use qiskit_accelerate::{ circuit_library::circuit_library, convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout, error_map::error_map, - euler_one_qubit_decomposer::euler_one_qubit_decomposer, isometry::isometry, nlayout::nlayout, + euler_one_qubit_decomposer::euler_one_qubit_decomposer, + inverse_cancellation::inverse_cancellation_mod, isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, pauli_exp_val::pauli_expval, results::results, sabre::sabre, sampled_exp_val::sampled_exp_val, sparse_pauli_op::sparse_pauli_op, star_prerouting::star_prerouting, stochastic_swap::stochastic_swap, synthesis::synthesis, @@ -44,6 +45,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, dense_layout, "dense_layout")?; add_submodule(m, error_map, "error_map")?; add_submodule(m, euler_one_qubit_decomposer, "euler_one_qubit_decomposer")?; + add_submodule(m, inverse_cancellation_mod, "inverse_cancellation")?; add_submodule(m, isometry, "isometry")?; add_submodule(m, nlayout, "nlayout")?; add_submodule(m, optimize_1q_gates, "optimize_1q_gates")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 33933fd8fd7e..700b02e02a16 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -87,6 +87,7 @@ sys.modules["qiskit._accelerate.synthesis.linear"] = _accelerate.synthesis.linear sys.modules["qiskit._accelerate.synthesis.clifford"] = _accelerate.synthesis.clifford sys.modules["qiskit._accelerate.synthesis.linear_phase"] = _accelerate.synthesis.linear_phase +sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/transpiler/passes/optimization/inverse_cancellation.py b/qiskit/transpiler/passes/optimization/inverse_cancellation.py index f5523432c26e..40876679e8d9 100644 --- a/qiskit/transpiler/passes/optimization/inverse_cancellation.py +++ b/qiskit/transpiler/passes/optimization/inverse_cancellation.py @@ -20,6 +20,8 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit._accelerate.inverse_cancellation import inverse_cancellation + class InverseCancellation(TransformationPass): """Cancel specific Gates which are inverses of each other when they occur back-to- @@ -81,96 +83,11 @@ def run(self, dag: DAGCircuit): Returns: DAGCircuit: Transformed DAG. """ - if self.self_inverse_gates: - dag = self._run_on_self_inverse(dag) - if self.inverse_gate_pairs: - dag = self._run_on_inverse_pairs(dag) - return dag - - def _run_on_self_inverse(self, dag: DAGCircuit): - """ - Run self-inverse gates on `dag`. - - Args: - dag: the directed acyclic graph to run on. - self_inverse_gates: list of gates who cancel themeselves in pairs - - Returns: - DAGCircuit: Transformed DAG. - """ - op_counts = dag.count_ops() - if not self.self_inverse_gate_names.intersection(op_counts): - return dag - # Sets of gate runs by name, for instance: [{(H 0, H 0), (H 1, H 1)}, {(X 0, X 0}] - for gate in self.self_inverse_gates: - gate_name = gate.name - gate_count = op_counts.get(gate_name, 0) - if gate_count <= 1: - continue - gate_runs = dag.collect_runs([gate_name]) - for gate_cancel_run in gate_runs: - partitions = [] - chunk = [] - max_index = len(gate_cancel_run) - 1 - for i, cancel_gate in enumerate(gate_cancel_run): - if cancel_gate.op == gate: - chunk.append(cancel_gate) - else: - if chunk: - partitions.append(chunk) - chunk = [] - continue - if i == max_index or cancel_gate.qargs != gate_cancel_run[i + 1].qargs: - partitions.append(chunk) - chunk = [] - # Remove an even number of gates from each chunk - for chunk in partitions: - if len(chunk) % 2 == 0: - dag.remove_op_node(chunk[0]) - for node in chunk[1:]: - dag.remove_op_node(node) - return dag - - def _run_on_inverse_pairs(self, dag: DAGCircuit): - """ - Run inverse gate pairs on `dag`. - - Args: - dag: the directed acyclic graph to run on. - inverse_gate_pairs: list of gates with inverse angles that cancel each other. - - Returns: - DAGCircuit: Transformed DAG. - """ - op_counts = dag.count_ops() - if not self.inverse_gate_pairs_names.intersection(op_counts): - return dag - - for pair in self.inverse_gate_pairs: - gate_0_name = pair[0].name - gate_1_name = pair[1].name - if gate_0_name not in op_counts or gate_1_name not in op_counts: - continue - gate_cancel_runs = dag.collect_runs([gate_0_name, gate_1_name]) - for dag_nodes in gate_cancel_runs: - i = 0 - while i < len(dag_nodes) - 1: - if ( - dag_nodes[i].qargs == dag_nodes[i + 1].qargs - and dag_nodes[i].op == pair[0] - and dag_nodes[i + 1].op == pair[1] - ): - dag.remove_op_node(dag_nodes[i]) - dag.remove_op_node(dag_nodes[i + 1]) - i = i + 2 - elif ( - dag_nodes[i].qargs == dag_nodes[i + 1].qargs - and dag_nodes[i].op == pair[1] - and dag_nodes[i + 1].op == pair[0] - ): - dag.remove_op_node(dag_nodes[i]) - dag.remove_op_node(dag_nodes[i + 1]) - i = i + 2 - else: - i = i + 1 + inverse_cancellation( + dag, + self.inverse_gate_pairs, + self.self_inverse_gates, + self.inverse_gate_pairs_names, + self.self_inverse_gate_names, + ) return dag From efeeda5534e7bf778547674d77cdb819409e1c83 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 6 Sep 2024 16:18:14 -0400 Subject: [PATCH 2/4] Remove temporary variable for chunk empty check --- crates/accelerate/src/inverse_cancellation.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/accelerate/src/inverse_cancellation.rs b/crates/accelerate/src/inverse_cancellation.rs index b150297f4917..cc32a45937d5 100644 --- a/crates/accelerate/src/inverse_cancellation.rs +++ b/crates/accelerate/src/inverse_cancellation.rs @@ -70,8 +70,7 @@ fn run_on_self_inverse( if gate_eq(py, inst, &gate)? { chunk.push(*cancel_gate); } else { - let is_empty: bool = chunk.is_empty(); - if !is_empty { + if !chunk.is_empty() { partitions.push(std::mem::take(&mut chunk)); } continue; From 00153dd8e0e9c1b6aeb3135d044c9d08d937bbc5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 6 Sep 2024 16:25:09 -0400 Subject: [PATCH 3/4] Destructure gate pairs --- crates/accelerate/src/inverse_cancellation.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/accelerate/src/inverse_cancellation.rs b/crates/accelerate/src/inverse_cancellation.rs index cc32a45937d5..d5fc0cc08e75 100644 --- a/crates/accelerate/src/inverse_cancellation.rs +++ b/crates/accelerate/src/inverse_cancellation.rs @@ -118,13 +118,13 @@ fn run_on_inverse_pairs( { return Ok(()); } - for pair in inverse_gates { - let gate_0_name = pair[0].operation.name(); - let gate_1_name = pair[1].operation.name(); + for [gate_0, gate_1] in inverse_gates { + let gate_0_name = gate_0.operation.name(); + let gate_1_name = gate_1.operation.name(); if !op_counts.contains_key(gate_0_name) || !op_counts.contains_key(gate_1_name) { continue; } - let names: HashSet = pair + let names: HashSet = [&gate_0, &gate_1] .iter() .map(|x| x.operation.name().to_string()) .collect(); @@ -135,9 +135,9 @@ fn run_on_inverse_pairs( if let NodeType::Operation(inst) = &dag.dag[nodes[i]] { if let NodeType::Operation(next_inst) = &dag.dag[nodes[i + 1]] { if inst.qubits == next_inst.qubits - && ((gate_eq(py, inst, &pair[0])? && gate_eq(py, next_inst, &pair[1])?) - || (gate_eq(py, inst, &pair[1])? - && gate_eq(py, next_inst, &pair[0])?)) + && ((gate_eq(py, inst, &gate_0)? && gate_eq(py, next_inst, &gate_1)?) + || (gate_eq(py, inst, &gate_1)? + && gate_eq(py, next_inst, &gate_0)?)) { dag.remove_op_node(nodes[i]); dag.remove_op_node(nodes[i + 1]); From 7dee70219033ca759be1b603680d7722765230bb Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 6 Sep 2024 16:25:23 -0400 Subject: [PATCH 4/4] Rework short circuit logic --- crates/accelerate/src/inverse_cancellation.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/accelerate/src/inverse_cancellation.rs b/crates/accelerate/src/inverse_cancellation.rs index d5fc0cc08e75..0b3404c92ffb 100644 --- a/crates/accelerate/src/inverse_cancellation.rs +++ b/crates/accelerate/src/inverse_cancellation.rs @@ -166,11 +166,10 @@ pub fn inverse_cancellation( inverse_gate_names: HashSet, self_inverse_gate_names: HashSet, ) -> PyResult<()> { - let op_counts = if !self_inverse_gate_names.is_empty() || !inverse_gate_names.is_empty() { - dag.count_ops(py, true)? - } else { - IndexMap::default() - }; + if self_inverse_gate_names.is_empty() && inverse_gate_names.is_empty() { + return Ok(()); + } + let op_counts = dag.count_ops(py, true)?; if !self_inverse_gate_names.is_empty() { run_on_self_inverse( py,