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

Oxidize the ConsolidateBlocks pass #13368

Merged
merged 26 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ed2b41b
Oxidize the ConsolidateBlocks pass
mtreinish Oct 24, 2024
155a574
Update test to count consolidate_blocks instead of collect_2q_blocks
mtreinish Oct 24, 2024
d3e900b
Fix lint
mtreinish Oct 24, 2024
29bb569
Fix solovay kitaev test
mtreinish Oct 24, 2024
e50fc1c
Add release note
mtreinish Oct 24, 2024
8da1b2f
Restore 2q block collection for synthesis translation plugin
mtreinish Oct 24, 2024
61b831f
Add rust native substitute method
mtreinish Oct 24, 2024
55523bb
Fix final test failures
mtreinish Oct 24, 2024
51fa6a5
Merge branch 'main' into consolidate-blocks
mtreinish Oct 24, 2024
8c43f01
Remove release note and test change
mtreinish Oct 24, 2024
b93df76
Fix comment leftover from rust-analyzer
mtreinish Oct 24, 2024
0423d1c
Remove unused code
mtreinish Oct 25, 2024
f8841f2
Simplify control flow handling
mtreinish Oct 25, 2024
700814e
Remove unnecessary clone from substitute_node
mtreinish Oct 25, 2024
5c4c50f
Preallocate block op names in replace_block_with_py_op
mtreinish Oct 25, 2024
a422990
Remove more unused imports
mtreinish Oct 25, 2024
62df015
Optimize linalg in block collection
mtreinish Oct 25, 2024
e001bab
Merge branch 'main' into consolidate-blocks
mtreinish Oct 25, 2024
73fae86
Merge remote-tracking branch 'origin/main' into consolidate-blocks
mtreinish Nov 5, 2024
864fc51
Use static one qubit identity matrix
mtreinish Nov 5, 2024
11742b4
Remove unnecessary lifetime annotations
mtreinish Nov 5, 2024
f1e645f
Add missing docstring to new rust method
mtreinish Nov 5, 2024
b00e22c
Apply suggestions from code review
mtreinish Nov 5, 2024
b5b0172
Fix lint
mtreinish Nov 5, 2024
223bf2e
Add comment for MAX_2Q_DEPTH constant
mtreinish Nov 5, 2024
b6071ee
Reuse block_qargs for each block
mtreinish Nov 5, 2024
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
319 changes: 319 additions & 0 deletions crates/accelerate/src/consolidate_blocks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2024
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

use hashbrown::{HashMap, HashSet};
use ndarray::{aview2, Array2};
use num_complex::Complex64;
use numpy::{IntoPyArray, PyReadonlyArray2};
use pyo3::intern;
use pyo3::prelude::*;
use rustworkx_core::petgraph::stable_graph::NodeIndex;

use qiskit_circuit::circuit_data::CircuitData;
use qiskit_circuit::dag_circuit::DAGCircuit;
use qiskit_circuit::gate_matrix::{ONE_QUBIT_IDENTITY, TWO_QUBIT_IDENTITY};
use qiskit_circuit::imports::{QI_OPERATOR, QUANTUM_CIRCUIT, UNITARY_GATE};
use qiskit_circuit::operations::{Operation, Param};
use qiskit_circuit::Qubit;

use crate::convert_2q_block_matrix::{blocks_to_matrix, get_matrix_from_inst};
use crate::euler_one_qubit_decomposer::matmul_1q;
use crate::nlayout::PhysicalQubit;
use crate::target_transpiler::Target;
use crate::two_qubit_decompose::TwoQubitBasisDecomposer;

fn is_supported(
target: Option<&Target>,
basis_gates: Option<&HashSet<String>>,
name: &str,
qargs: &[Qubit],
) -> bool {
match target {
Some(target) => {
let physical_qargs = qargs.iter().map(|bit| PhysicalQubit(bit.0)).collect();
target.instruction_supported(name, Some(&physical_qargs))
}
None => match basis_gates {
Some(basis_gates) => basis_gates.contains(name),
None => true,
},
}
}

// If depth > 20, there will be 1q gates to consolidate.
const MAX_2Q_DEPTH: usize = 20;
mtreinish marked this conversation as resolved.
Show resolved Hide resolved

#[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<HashSet<String>>,
blocks: Option<Vec<Vec<usize>>>,
runs: Option<Vec<Vec<usize>>>,
) -> PyResult<()> {
let blocks = match blocks {
Some(runs) => runs
.into_iter()
.map(|run| {
run.into_iter()
.map(NodeIndex::new)
.collect::<Vec<NodeIndex>>()
})
.collect(),
// If runs are specified but blocks are none we're in a legacy configuration where external
// collection passes are being used. In this case don't collect blocks because it's
// unexpected.
None => match runs {
Some(_) => vec![],
None => dag.collect_2q_runs().unwrap(),
},
};

let runs: Option<Vec<Vec<NodeIndex>>> = runs.map(|runs| {
runs.into_iter()
.map(|run| {
run.into_iter()
.map(NodeIndex::new)
.collect::<Vec<NodeIndex>>()
})
.collect()
});
Comment on lines +68 to +94
Copy link
Contributor

Choose a reason for hiding this comment

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

To improve the memory efficiency here, we could simplify the handling of the blocks and runs by unifying them with or_else and unwarp_or_else. This approach would reduces the unnecessary temporary Vec allocations and leveragers iterators for more efficient memory usage.

For instance, something like:

Suggested change
let blocks = match blocks {
Some(runs) => runs
.into_iter()
.map(|run| {
run.into_iter()
.map(NodeIndex::new)
.collect::<Vec<NodeIndex>>()
})
.collect(),
// If runs are specified but blocks are none we're in a legacy configuration where external
// collection passes are being used. In this case don't collect blocks because it's
// unexpected.
None => match runs {
Some(_) => vec![],
None => dag.collect_2q_runs().unwrap(),
},
};
let runs: Option<Vec<Vec<NodeIndex>>> = runs.map(|runs| {
runs.into_iter()
.map(|run| {
run.into_iter()
.map(NodeIndex::new)
.collect::<Vec<NodeIndex>>()
})
.collect()
});
// If `blocks` is `None` but `runs` is specified, `blocks` is set to `runs.clone()`.
// If both are `None`, it defaults to collecting 2q runs from the DAG circuit.
let blocks: Vec<Vec<NodeIndex>> = blocks
.or_else(|| runs.clone())
.unwrap_or_else(|| {
dag.collect_2q_runs()
.unwrap()
.into_iter()
.map(|run| {
run.into_iter()
.map(|node_index| node_index.index())
.collect()
})
.collect()
})
.into_iter()
.map(|run| run.into_iter().map(NodeIndex::new).collect())
.collect();
let runs: Option<Vec<Vec<NodeIndex>>> = runs.map(|runs| {
runs.into_iter()
.map(|run| run.into_iter().map(NodeIndex::new).collect())
.collect()
});

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll see if I can come up with something for this. The behavior of cloning runs like this doesn't feel correct and I think probably wouldn't work correctly because it'll try to call blocks_to_matrix with a single qubit matrix. But I'll see if I can come up with something to avoid temporary vecs. I think I did this to make the borrow checker happy, but it was long enough ago I'm not 100% sure.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure I see a path here, the conflict is on the type mismatch between NodeIndex and the usize input. From a typing perspective rust won't be happy because collect_2q_runs() returns a Vec of NodeIndex and we can't take that as an input. The best idea I had to avoid an extra allocation was to take NodeIndex on the input Vec but we can't define the FromPyObject trait for a type not defined in qiskit.

Your suggestion here doesn't actually avoid any allocations except if blocks is Some(_), but at the cost of a second allocation for blocks is None path. The blocks is None path is the more common path though because that's what we run in the preset pass managers, so I'd rather stick with the bare collect_2q_runs call and have the conversion cost from the blocks list if it's specified.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh yes, I understand now, and that makes sense. You're correct about the typing conflict and the allocation cost trade-offs, especially since the blocks is None path is more common in the preset pass managers. Let’s stick with what you initially suggested with the bare collect_2q_runs call, and handle the conversion cost from the blocks list if it's specified. Thanks for clarifying!

let mut all_block_gates: HashSet<NodeIndex> =
HashSet::with_capacity(blocks.iter().map(|x| x.len()).sum());
let mut block_qargs: HashSet<Qubit> = HashSet::with_capacity(2);
for block in blocks {
block_qargs.clear();
if block.len() == 1 {
let inst_node = block[0];
let inst = dag.dag()[inst_node].unwrap_operation();
if !is_supported(
target,
basis_gates.as_ref(),
inst.op.name(),
dag.get_qargs(inst.qubits),
) {
all_block_gates.insert(inst_node);
let matrix = match get_matrix_from_inst(py, inst) {
Ok(mat) => mat,
Err(_) => continue,
henryzou50 marked this conversation as resolved.
Show resolved Hide resolved
};
let array = matrix.into_pyarray_bound(py);
let unitary_gate = UNITARY_GATE
.get_bound(py)
.call1((array, py.None(), false))?;
dag.substitute_node_with_py_op(py, inst_node, &unitary_gate, false)?;
continue;
}
}
let mut basis_count: usize = 0;
let mut outside_basis = false;
for node in &block {
let inst = dag.dag()[*node].unwrap_operation();
block_qargs.extend(dag.get_qargs(inst.qubits));
all_block_gates.insert(*node);
if inst.op.name() == decomposer.gate_name() {
basis_count += 1;
}
if !is_supported(
target,
basis_gates.as_ref(),
inst.op.name(),
dag.get_qargs(inst.qubits),
) {
outside_basis = true;
}
}
if block_qargs.len() > 2 {
let mut qargs: Vec<Qubit> = block_qargs.iter().copied().collect();
qargs.sort();
let block_index_map: HashMap<Qubit, usize> = qargs
.into_iter()
.enumerate()
.map(|(idx, qubit)| (qubit, idx))
.collect();
let circuit_data = CircuitData::from_packed_operations(
py,
block_qargs.len() as u32,
Copy link
Contributor

Choose a reason for hiding this comment

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

This is perhaps more evidence that we ought to just use usize in any public interface of the circuit crate rather than the internal representation.

(no action requested)

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::<PyReadonlyArray2<Complex64>>()?;
let matrix = array.as_array();
let identity: Array2<Complex64> = Array2::eye(2usize.pow(block_qargs.len() as u32));
if approx::abs_diff_eq!(identity, matrix) {
for node in block {
dag.remove_op_node(node);
}
} else {
let unitary_gate =
UNITARY_GATE
.get_bound(py)
.call1((array.to_object(py), py.None(), false))?;
let clbit_pos_map = HashMap::new();
dag.replace_block_with_py_op(
py,
&block,
unitary_gate,
false,
&block_index_map,
&clbit_pos_map,
)?;
}
} else {
let block_index_map = [
*block_qargs.iter().min().unwrap(),
*block_qargs.iter().max().unwrap(),
];
let matrix = blocks_to_matrix(py, dag, &block, block_index_map).ok();
if let Some(matrix) = matrix {
if force_consolidate
|| decomposer.num_basis_gates_inner(matrix.view()) < basis_count
|| block.len() > MAX_2Q_DEPTH
|| (basis_gates.is_some() && outside_basis)
|| (target.is_some() && outside_basis)
{
if approx::abs_diff_eq!(aview2(&TWO_QUBIT_IDENTITY), matrix) {
for node in block {
dag.remove_op_node(node);
}
} else {
let array = matrix.into_pyarray_bound(py);
let unitary_gate =
UNITARY_GATE
.get_bound(py)
.call1((array, py.None(), false))?;
let qubit_pos_map = block_index_map
.into_iter()
.enumerate()
.map(|(idx, qubit)| (qubit, idx))
.collect();
let clbit_pos_map = HashMap::new();
dag.replace_block_with_py_op(
py,
&block,
unitary_gate,
false,
&qubit_pos_map,
&clbit_pos_map,
)?;
}
}
}
}
}
if let Some(runs) = runs {
for run in runs {
if run.iter().any(|node| all_block_gates.contains(node)) {
continue;
}
let first_inst_node = run[0];
let first_inst = dag.dag()[first_inst_node].unwrap_operation();
let first_qubits = dag.get_qargs(first_inst.qubits);

if run.len() == 1
&& !is_supported(
target,
basis_gates.as_ref(),
first_inst.op.name(),
first_qubits,
)
{
let matrix = match get_matrix_from_inst(py, first_inst) {
Ok(mat) => mat,
Err(_) => continue,
};
let array = matrix.into_pyarray_bound(py);
let unitary_gate = UNITARY_GATE
.get_bound(py)
.call1((array, py.None(), false))?;
dag.substitute_node_with_py_op(py, first_inst_node, &unitary_gate, false)?;
continue;
}
let qubit = first_qubits[0];
let mut matrix = ONE_QUBIT_IDENTITY;

let mut already_in_block = false;
for node in &run {
if all_block_gates.contains(node) {
already_in_block = true;
}
let gate = dag.dag()[*node].unwrap_operation();
let operator = match get_matrix_from_inst(py, gate) {
Ok(mat) => mat,
Err(_) => {
// Set this to skip this run because we can't compute the matrix of the
// operation.
already_in_block = true;
break;
}
};
matmul_1q(&mut matrix, operator);
}
if already_in_block {
continue;
}
if approx::abs_diff_eq!(aview2(&ONE_QUBIT_IDENTITY), aview2(&matrix)) {
for node in run {
dag.remove_op_node(node);
}
} else {
let array = aview2(&matrix).to_owned().into_pyarray_bound(py);
let unitary_gate = UNITARY_GATE
.get_bound(py)
.call1((array, py.None(), false))?;
let mut block_index_map: HashMap<Qubit, usize> = 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<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(consolidate_blocks))?;
Ok(())
}
Loading