Skip to content

Commit 9ca951c

Browse files
mtreinishraynelfss
andauthored
Perform BarrierBeforeFinalMeasurements analysis in parallel (#13411)
* Use OnceLock instead of OnceCell OnceLock is a thread-safe version of OnceCell that enables us to use PackedInstruction from a threaded environment. There is some overhead associated with this, primarily in memory as the OnceLock is a larger type than a OnceCell. But the tradeoff is worth it to start leverage multithreading for circuits. Fixes #13219 * Update twirling too * Perform BarrierBeforeFinalMeasurements analysis in paralle With #13410 removing the non-threadsafe structure from our circuit representation we're now able to read and iterate over a DAGCircuit from multiple threads. This commit is the first small piece doing this, it moves the analysis portion of the BarrierBeforeFinalMeasurements pass to execure in parallel. The pass checks every node to ensure all it's decendents are either a measure or a barrier before reaching the end of the circuit. This commit iterates over all the nodes and does the check in parallel. * Remove allocation for node scan * Refactor pass to optimize search and set parallel threshold This commit updates the logic in the pass to simplify the search algorithm and improve it's overall efficiency. Previously the pass would search the entire dag for all barrier and measurements and then did a BFS from each found node to check that all descendants are either barriers or measurements. Then with the set of nodes matching that condition a full topological sort of the dag was run, then the topologically ordered nodes were filtered for the matching set. That sorted set is then used for filtering This commit refactors this to do a reverse search from the output nodes which reduces the complexity of the algorithm. This new algorithm is also conducive for parallel execution because it does a search starting from each qubit's output node. Doing a test with a quantum volume circuit from 10 to 1000 qubits which scales linearly in depth and number of qubits a crossover point between the parallel and serial implementations was found around 150 qubits. * Update crates/circuit/src/dag_circuit.rs Co-authored-by: Raynel Sanchez <[email protected]> * Rework logic to check using StandardInstruction * Add comments explaining the search function * Update crates/circuit/src/dag_circuit.rs Co-authored-by: Raynel Sanchez <[email protected]> --------- Co-authored-by: Raynel Sanchez <[email protected]>
1 parent fb7648e commit 9ca951c

File tree

2 files changed

+129
-32
lines changed

2 files changed

+129
-32
lines changed

crates/accelerate/src/barrier_before_final_measurement.rs

+106-29
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@
1010
// copyright notice, and modified files need to carry a notice indicating
1111
// that they have been altered from the originals.
1212

13-
use hashbrown::HashSet;
1413
use pyo3::prelude::*;
14+
use rayon::prelude::*;
1515
use rustworkx_core::petgraph::stable_graph::NodeIndex;
1616

1717
use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes;
1818
use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType};
19-
use qiskit_circuit::operations::{Operation, StandardInstruction};
19+
use qiskit_circuit::operations::{OperationRef, StandardInstruction};
2020
use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation};
2121
use qiskit_circuit::Qubit;
2222

23-
static FINAL_OP_NAMES: [&str; 2] = ["measure", "barrier"];
23+
const PARALLEL_THRESHOLD: usize = 150;
2424

2525
#[pyfunction]
2626
#[pyo3(signature=(dag, label=None))]
@@ -29,39 +29,116 @@ pub fn barrier_before_final_measurements(
2929
dag: &mut DAGCircuit,
3030
label: Option<String>,
3131
) -> PyResult<()> {
32-
let is_exactly_final = |inst: &PackedInstruction| FINAL_OP_NAMES.contains(&inst.op.name());
33-
let final_ops: HashSet<NodeIndex> = dag
34-
.op_nodes(true)
35-
.filter_map(|(node, inst)| {
36-
if !is_exactly_final(inst) {
37-
return None;
38-
}
39-
dag.bfs_successors(node)
40-
.all(|(_, child_successors)| {
41-
child_successors.iter().all(|suc| match dag[*suc] {
42-
NodeType::Operation(ref suc_inst) => is_exactly_final(suc_inst),
32+
// Get a list of the node indices which are final measurement or barriers that are ancestors
33+
// of a given qubit's output node.
34+
let find_final_nodes = |[_in_index, out_index]: &[NodeIndex; 2]| -> Vec<NodeIndex> {
35+
// Next nodes is the stack of parent nodes to investigate. It starts with any predecessors
36+
// of a qubit's output node that are Barrier or Measure
37+
let mut next_nodes: Vec<NodeIndex> = dag
38+
.quantum_predecessors(*out_index)
39+
.filter(|index| {
40+
let node = &dag[*index];
41+
match node {
42+
NodeType::Operation(inst) => {
43+
if let OperationRef::StandardInstruction(op) = inst.op.view() {
44+
if matches!(
45+
op,
46+
StandardInstruction::Measure | StandardInstruction::Barrier(_)
47+
) {
48+
dag.bfs_successors(*index).all(|(_, child_successors)| {
49+
child_successors.iter().all(|suc| match &dag[*suc] {
50+
NodeType::Operation(suc_inst) => match suc_inst.op.view() {
51+
OperationRef::StandardInstruction(suc_op) => {
52+
matches!(
53+
suc_op,
54+
StandardInstruction::Measure
55+
| StandardInstruction::Barrier(_)
56+
)
57+
}
58+
_ => false,
59+
},
60+
_ => true,
61+
})
62+
})
63+
} else {
64+
false
65+
}
66+
} else {
67+
false
68+
}
69+
}
70+
_ => false,
71+
}
72+
})
73+
.collect();
74+
let mut nodes: Vec<NodeIndex> = Vec::new();
75+
// Reverse traverse the dag from next nodes until we encounter no more barriers or measures
76+
while let Some(node_index) = next_nodes.pop() {
77+
// If node on the stack is a barrier or measure we can add it to the output list
78+
if node_index != *out_index
79+
&& dag.bfs_successors(node_index).all(|(_, child_successors)| {
80+
child_successors.iter().all(|suc| match &dag[*suc] {
81+
NodeType::Operation(suc_inst) => match suc_inst.op.view() {
82+
OperationRef::StandardInstruction(suc_op) => matches!(
83+
suc_op,
84+
StandardInstruction::Measure | StandardInstruction::Barrier(_)
85+
),
86+
_ => false,
87+
},
4388
_ => true,
4489
})
4590
})
46-
.then_some(node)
47-
})
48-
.collect();
91+
{
92+
nodes.push(node_index);
93+
}
94+
// For this node if any parent nodes are barrier or measure add those to the stack
95+
for pred in dag.quantum_predecessors(node_index) {
96+
match &dag[pred] {
97+
NodeType::Operation(inst) => {
98+
if let OperationRef::StandardInstruction(op) = inst.op.view() {
99+
if matches!(
100+
op,
101+
StandardInstruction::Measure | StandardInstruction::Barrier(_)
102+
) {
103+
next_nodes.push(pred)
104+
}
105+
}
106+
}
107+
_ => continue,
108+
}
109+
}
110+
}
111+
nodes.reverse();
112+
nodes
113+
};
114+
115+
let final_ops: Vec<NodeIndex> =
116+
if dag.num_qubits() >= PARALLEL_THRESHOLD && crate::getenv_use_multiple_threads() {
117+
dag.qubit_io_map()
118+
.par_iter()
119+
.flat_map(find_final_nodes)
120+
.collect()
121+
} else {
122+
dag.qubit_io_map()
123+
.iter()
124+
.flat_map(find_final_nodes)
125+
.collect()
126+
};
127+
49128
if final_ops.is_empty() {
50129
return Ok(());
51130
}
52-
let ordered_node_indices: Vec<NodeIndex> = dag
53-
.topological_op_nodes()?
54-
.filter(|node| final_ops.contains(node))
55-
.collect();
56-
let final_packed_ops: Vec<PackedInstruction> = ordered_node_indices
131+
let final_packed_ops: Vec<PackedInstruction> = final_ops
57132
.into_iter()
58-
.map(|node| {
59-
let NodeType::Operation(ref inst) = dag[node] else {
60-
unreachable!()
61-
};
62-
let res = inst.clone();
63-
dag.remove_op_node(node);
64-
res
133+
.filter_map(|node| match dag.dag().node_weight(node) {
134+
Some(weight) => {
135+
let NodeType::Operation(_) = weight else {
136+
return None;
137+
};
138+
let res = dag.remove_op_node(node);
139+
Some(res)
140+
}
141+
None => None,
65142
})
66143
.collect();
67144
let qargs: Vec<Qubit> = (0..dag.num_qubits() as u32).map(Qubit).collect();

crates/circuit/src/dag_circuit.rs

+23-3
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ use rustworkx_core::petgraph::visit::{
6060
};
6161
use rustworkx_core::petgraph::Incoming;
6262
use rustworkx_core::traversal::{
63-
ancestors as core_ancestors, bfs_successors as core_bfs_successors,
64-
descendants as core_descendants,
63+
ancestors as core_ancestors, bfs_predecessors as core_bfs_predecessors,
64+
bfs_successors as core_bfs_successors, descendants as core_descendants,
6565
};
6666

6767
use std::cmp::Ordering;
@@ -4830,6 +4830,12 @@ def _format(operand):
48304830
}
48314831

48324832
impl DAGCircuit {
4833+
/// Returns an immutable view of the qubit io map
4834+
#[inline(always)]
4835+
pub fn qubit_io_map(&self) -> &[[NodeIndex; 2]] {
4836+
&self.qubit_io_map
4837+
}
4838+
48334839
/// Returns an immutable view of the inner StableGraph managed by the circuit.
48344840
#[inline(always)]
48354841
pub fn dag(&self) -> &StableDiGraph<NodeType, Wire> {
@@ -5639,7 +5645,11 @@ impl DAGCircuit {
56395645
/// Remove an operation node n.
56405646
///
56415647
/// Add edges from predecessors to successors.
5642-
pub fn remove_op_node(&mut self, index: NodeIndex) {
5648+
///
5649+
/// # Returns
5650+
///
5651+
/// The removed [PackedInstruction] is returned
5652+
pub fn remove_op_node(&mut self, index: NodeIndex) -> PackedInstruction {
56435653
let mut edge_list: Vec<(NodeIndex, NodeIndex, Wire)> = Vec::new();
56445654
for (source, in_weight) in self
56455655
.dag
@@ -5664,6 +5674,7 @@ impl DAGCircuit {
56645674
Some(NodeType::Operation(packed)) => {
56655675
let op_name = packed.op.name();
56665676
self.decrement_op(op_name);
5677+
packed
56675678
}
56685679
_ => panic!("Must be called with valid operation node!"),
56695680
}
@@ -5688,6 +5699,15 @@ impl DAGCircuit {
56885699
core_bfs_successors(&self.dag, node).filter(move |(_, others)| !others.is_empty())
56895700
}
56905701

5702+
/// Returns an iterator of tuples of (DAGNode, [DAGNodes]) where the DAGNode is the current node
5703+
/// and [DAGNode] is its predecessors in BFS order.
5704+
pub fn bfs_predecessors(
5705+
&self,
5706+
node: NodeIndex,
5707+
) -> impl Iterator<Item = (NodeIndex, Vec<NodeIndex>)> + '_ {
5708+
core_bfs_predecessors(&self.dag, node).filter(move |(_, others)| !others.is_empty())
5709+
}
5710+
56915711
fn pack_into(&mut self, py: Python, b: &Bound<PyAny>) -> Result<NodeType, PyErr> {
56925712
Ok(if let Ok(in_node) = b.downcast::<DAGInNode>() {
56935713
let in_node = in_node.borrow();

0 commit comments

Comments
 (0)