Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fully port InverseCancellation to Rust #13013

Merged
merged 6 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions crates/accelerate/src/inverse_cancellation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// 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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like DAGCircuit::collect_runs returns an Option only because it directly returns the result of the Rustworkx core function, which uses None to represent a cycle in the graph (...which is questionable lol).

Perhaps it is beyond the scope of this PR, but it might be nice if you can change DAGCircuit::collect_runs to unwrap that option internally. If it fails, it should be on DAGCircuit for getting itself into an invalid state.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm fine with making this change to collect_runs() but I'd say lets save this for a follow-up PR so we can do it in isolation.

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 {
if !chunk.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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! I think this is the first example I've seen of the intern IDs saving us some time :)

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 [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<String> = [&gate_0, &gate_1]
.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, &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]);
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<()> {
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,
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 @@ -24,6 +24,7 @@ pub mod edge_collections;
pub mod error_map;
pub mod euler_one_qubit_decomposer;
pub mod filter_op_nodes;
pub mod inverse_cancellation;
pub mod isometry;
pub mod nlayout;
pub mod optimize_1q_gates;
Expand Down
94 changes: 54 additions & 40 deletions crates/circuit/src/dag_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4575,45 +4575,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 || !self.has_control_flow() {
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.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<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 @@ -4745,7 +4709,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 @@ -6365,6 +6329,56 @@ impl DAGCircuit {
}
}

/// Return the op name counts in the circuit
///
/// Args:
/// py: The python token necessary for control flow recursion
/// recurse: Whether to recurse into control flow ops or not
pub fn count_ops(
&self,
py: Python,
recurse: bool,
) -> PyResult<IndexMap<String, usize, RandomState>> {
if !recurse || !self.has_control_flow() {
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's worth adding a count_ops method to CircuitData, and calling that directly instead of converting to a DAGCircuit first.

I'm not saying that we should necessarily bother maintaining op counts in CircuitData like we do in a DAGCircuit, but computing on the fly should still be faster than recursively converting everything to a DAGCircuit.

Not necessary for this PR, but I wouldn't be opposed to doing it here either.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's funny you mention this, that was added last week: 5fc1635

but the implementation there is governed by the api of the python space method and not how I would implement it for performance in the transpiler.

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)
}
}

/// Extends the DAG with valid instances of [PackedInstruction]
pub fn extend<I>(&mut self, py: Python, iter: I) -> PyResult<Vec<NodeIndex>>
where
Expand Down
6 changes: 4 additions & 2 deletions crates/pyext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ use qiskit_accelerate::{
commutation_analysis::commutation_analysis, commutation_checker::commutation_checker,
convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout,
error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer,
filter_op_nodes::filter_op_nodes_mod, isometry::isometry, nlayout::nlayout,
optimize_1q_gates::optimize_1q_gates, pauli_exp_val::pauli_expval,
filter_op_nodes::filter_op_nodes_mod, inverse_cancellation::inverse_cancellation_mod,
isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates,
pauli_exp_val::pauli_expval,
remove_diagonal_gates_before_measure::remove_diagonal_gates_before_measure, 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,
Expand Down Expand Up @@ -48,6 +49,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, filter_op_nodes_mod, "filter_op_nodes")?;
add_submodule(m, isometry, "isometry")?;
add_submodule(m, nlayout, "nlayout")?;
Expand Down
1 change: 1 addition & 0 deletions qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
sys.modules["qiskit._accelerate.commutation_checker"] = _accelerate.commutation_checker
sys.modules["qiskit._accelerate.commutation_analysis"] = _accelerate.commutation_analysis
sys.modules["qiskit._accelerate.synthesis.linear_phase"] = _accelerate.synthesis.linear_phase
sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation
sys.modules["qiskit._accelerate.check_map"] = _accelerate.check_map
sys.modules["qiskit._accelerate.filter_op_nodes"] = _accelerate.filter_op_nodes

Expand Down
Loading
Loading