Skip to content

Commit

Permalink
Fully port InverseCancellation to Rust
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mtreinish committed Aug 22, 2024
1 parent 0c8b668 commit 7614689
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 139 deletions.
193 changes: 193 additions & 0 deletions crates/accelerate/src/inverse_cancellation.rs
Original file line number Diff line number Diff line change
@@ -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<bool> {
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<String, usize, RandomState>,
self_inverse_gate_names: HashSet<String>,
self_inverse_gates: Vec<OperationFromPython>,
) -> 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<String> = HashSet::with_capacity(1);
collect_set.insert(gate.operation.name().to_string());
let gate_runs: Vec<Vec<NodeIndex>> = dag.collect_runs(collect_set).unwrap().collect();
for gate_cancel_run in gate_runs {
let mut partitions: Vec<Vec<NodeIndex>> = Vec::new();
let mut chunk: Vec<NodeIndex> = 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<String, usize, RandomState>,
inverse_gate_names: HashSet<String>,
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<String> = pair
.iter()
.map(|x| x.operation.name().to_string())
.collect();
let runs: Vec<Vec<NodeIndex>> = 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<OperationFromPython>,
inverse_gate_names: HashSet<String>,
self_inverse_gate_names: HashSet<String>,
) -> 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<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(inverse_cancellation))?;
Ok(())
}
1 change: 1 addition & 0 deletions crates/accelerate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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;
Expand Down
91 changes: 50 additions & 41 deletions crates/circuit/src/dag_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4576,46 +4576,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<PyObject> {
if !recurse
|| !CONTROL_FLOW_OP_NAMES
.iter()
.any(|x| self.op_names.contains_key(*x))
{
Ok(self.op_names.to_object(py))
} else {
fn inner(
py: Python,
dag: &DAGCircuit,
counts: &mut HashMap<String, usize>,
) -> 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.py_op_nodes(
py,
Some(imports::CONTROL_FLOW_OP.get_bound(py).downcast()?),
true,
)? {
let raw_blocks = node.getattr(py, "op")?.getattr(py, "blocks")?;
let blocks: &Bound<PyTuple> = raw_blocks.downcast_bound::<PyTuple>(py)?;
for block in blocks.iter() {
let inner_dag: &DAGCircuit =
&circuit_to_dag.call1((block.clone(),))?.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<PyObject> {
self.count_ops(py, recurse).map(|x| x.into_py(py))
}

/// Count the occurrences of operation names on the longest path.
Expand Down Expand Up @@ -4747,7 +4710,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)?),
]))
}

Expand Down Expand Up @@ -6279,6 +6242,52 @@ impl DAGCircuit {
Err(DAGCircuitError::new_err("Specified node is not an op node"))
}
}

pub fn count_ops(
&self,
py: Python,
recurse: bool,
) -> PyResult<IndexMap<String, usize, RandomState>> {
if !recurse
|| !CONTROL_FLOW_OP_NAMES
.iter()
.any(|x| self.op_names.contains_key(*x))
{
Ok(self.op_names.clone())
} else {
fn inner(
py: Python,
dag: &DAGCircuit,
counts: &mut IndexMap<String, usize, RandomState>,
) -> 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.py_op_nodes(
py,
Some(imports::CONTROL_FLOW_OP.get_bound(py).downcast()?),
true,
)? {
let raw_blocks = node.getattr(py, "op")?.getattr(py, "blocks")?;
let blocks: &Bound<PyTuple> = raw_blocks.downcast_bound::<PyTuple>(py)?;
for block in blocks.iter() {
let inner_dag: &DAGCircuit =
&circuit_to_dag.call1((block.clone(),))?.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
Expand Down
13 changes: 7 additions & 6 deletions crates/pyext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ use pyo3::prelude::*;
use qiskit_accelerate::{
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, 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, target_transpiler::target,
two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils,
vf2_layout::vf2_layout,
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,
target_transpiler::target, two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate,
utils::utils, vf2_layout::vf2_layout,
};

#[inline(always)]
Expand All @@ -43,6 +43,7 @@ fn _accelerate(m: &Bound<PyModule>) -> 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")?;
Expand Down
1 change: 1 addition & 0 deletions qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,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

Expand Down
Loading

0 comments on commit 7614689

Please sign in to comment.