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

Add fast-path construction to DAGCircuit methods #10753

Merged
merged 5 commits into from
Sep 7, 2023
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
14 changes: 7 additions & 7 deletions qiskit/converters/ast_to_dag.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,11 @@ def _process_cnot(self, node):
cx_gate = std.CXGate()
cx_gate.condition = self.condition
if len(id0) > 1 and len(id1) > 1:
self.dag.apply_operation_back(cx_gate, [id0[idx], id1[idx]], [])
self.dag.apply_operation_back(cx_gate, [id0[idx], id1[idx]], [], check=False)
elif len(id0) > 1:
self.dag.apply_operation_back(cx_gate, [id0[idx], id1[0]], [])
self.dag.apply_operation_back(cx_gate, [id0[idx], id1[0]], [], check=False)
else:
self.dag.apply_operation_back(cx_gate, [id0[0], id1[idx]], [])
self.dag.apply_operation_back(cx_gate, [id0[0], id1[idx]], [], check=False)

def _process_measure(self, node):
"""Process a measurement node."""
Expand All @@ -253,7 +253,7 @@ def _process_measure(self, node):
for idx, idy in zip(id0, id1):
meas_gate = Measure()
meas_gate.condition = self.condition
self.dag.apply_operation_back(meas_gate, [idx], [idy])
self.dag.apply_operation_back(meas_gate, [idx], [idy], check=False)

def _process_if(self, node):
"""Process an if node."""
Expand Down Expand Up @@ -335,14 +335,14 @@ def _process_node(self, node):
for qubit in ids:
for j, _ in enumerate(qubit):
qubits.append(qubit[j])
self.dag.apply_operation_back(Barrier(len(qubits)), qubits, [])
self.dag.apply_operation_back(Barrier(len(qubits)), qubits, [], check=False)

elif node.type == "reset":
id0 = self._process_bit_id(node.children[0])
for i, _ in enumerate(id0):
reset = Reset()
reset.condition = self.condition
self.dag.apply_operation_back(reset, [id0[i]], [])
self.dag.apply_operation_back(reset, [id0[i]], [], check=False)

elif node.type == "if":
self._process_if(node)
Expand Down Expand Up @@ -399,7 +399,7 @@ def _create_dag_op(self, name, params, qargs):
"""
op = self._create_op(name, params)
op.condition = self.condition
self.dag.apply_operation_back(op, qargs, [])
self.dag.apply_operation_back(op, qargs, [], check=False)

def _create_op(self, name, params):
if name in self.standard_extension:
Expand Down
2 changes: 1 addition & 1 deletion qiskit/converters/circuit_to_dag.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord
op = instruction.operation
if copy_operations:
op = copy.deepcopy(op)
dagcircuit.apply_operation_back(op, instruction.qubits, instruction.clbits)
dagcircuit.apply_operation_back(op, instruction.qubits, instruction.clbits, check=False)

dagcircuit.duration = circuit.duration
dagcircuit.unit = circuit.unit
Expand Down
112 changes: 69 additions & 43 deletions qiskit/dagcircuit/dagcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"""
from collections import OrderedDict, defaultdict, deque, namedtuple
import copy
import itertools
import math
from typing import Dict, Generator, Any, List

Expand Down Expand Up @@ -569,6 +568,7 @@ def _bits_in_operation(operation):
Returns:
Iterable[Clbit]: the :class:`.Clbit`\\ s involved.
"""
# If updating this, also update the fast-path checker `DAGCirucit._operation_may_have_bits`.
if (condition := getattr(operation, "condition", None)) is not None:
yield from condition_resources(condition).clbits
if isinstance(operation, SwitchCaseOp):
Expand All @@ -580,6 +580,22 @@ def _bits_in_operation(operation):
else:
yield from node_resources(target).clbits

@staticmethod
def _operation_may_have_bits(operation) -> bool:
"""Return whether a given :class:`.Operation` may contain any :class:`.Clbit` instances
in itself (e.g. a control-flow operation).

Args:
operation (qiskit.circuit.Operation): the operation to check.
"""
# This is separate to `_bits_in_operation` because most of the time there won't be any bits,
# so we want a fast path to be able to skip creating and testing a generator for emptiness.
#
# If updating this, also update `DAGCirucit._bits_in_operation`.
return getattr(operation, "condition", None) is not None or isinstance(
operation, SwitchCaseOp
)

Copy link
Contributor

Choose a reason for hiding this comment

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

This optimization is in addition to the optimization discussed in the opening comment, right? Did you benchmark this optimization? It's clear that set(self._bits_in_operation(op)).union(cargs) will be slower than just cargs even when the generator is empty. But I would not be surprised if this is not measurable in benchmarks.

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, looks like I didn't mention this little bit in the commit message. Removing the _may_have_bits check and always eagerly calling _bits_in_operation caused the microbenchmark at the top to go from 27.7(7)ms with the current PR to 29.3(9)ms when timed just now.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's worth the trouble although it adds a bit of clutter

def _increment_op(self, op):
if op.name in self._op_names:
self._op_names[op.name] += 1
Expand All @@ -592,23 +608,6 @@ def _decrement_op(self, op):
else:
self._op_names[op.name] -= 1

def _add_op_node(self, op, qargs, cargs):
"""Add a new operation node to the graph and assign properties.

Args:
op (qiskit.circuit.Operation): the operation associated with the DAG node
qargs (list[Qubit]): list of quantum wires to attach to.
cargs (list[Clbit]): list of classical wires to attach to.
Returns:
int: The integer node index for the new op node on the DAG
"""
# Add a new operation node to the graph
new_node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self)
node_index = self._multi_graph.add_node(new_node)
new_node._node_id = node_index
self._increment_op(op)
return node_index

Comment on lines -595 to -611
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a fairly simple function used in only two places which already necessarily had a lot of duplication, and throws away information that the outer contexts need (the DAGOpNode - we previously had to retrieve it from _multi_graph again), so I just inlined it into its call sites.

@deprecate_func(
additional_msg="Instead, use :meth:`~copy_empty_like()`, which acts identically.",
since="0.20.0",
Expand Down Expand Up @@ -647,13 +646,18 @@ def copy_empty_like(self):

return target_dag

def apply_operation_back(self, op, qargs=(), cargs=()):
def apply_operation_back(self, op, qargs=(), cargs=(), *, check=True):
"""Apply an operation to the output of the circuit.

Args:
op (qiskit.circuit.Operation): the operation associated with the DAG node
qargs (tuple[Qubit]): qubits that op will be applied to
cargs (tuple[Clbit]): cbits that op will be applied to
check (bool): If ``True`` (default), this function will enforce that the
:class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are
:class:`.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must*
uphold these invariants itself, but the cost of several checks will be skipped.
This is most useful when building a new DAG from a source of known-good nodes.
Returns:
DAGOpNode: the node for the op that was added to the dag

Expand All @@ -664,52 +668,74 @@ def apply_operation_back(self, op, qargs=(), cargs=()):
qargs = tuple(qargs) if qargs is not None else ()
cargs = tuple(cargs) if cargs is not None else ()

all_cbits = set(self._bits_in_operation(op)).union(cargs)
if self._operation_may_have_bits(op):
# This is the slow path; most of the time, this won't happen.
all_cbits = set(self._bits_in_operation(op)).union(cargs)
else:
all_cbits = cargs

self._check_condition(op.name, getattr(op, "condition", None))
self._check_bits(qargs, self.output_map)
self._check_bits(all_cbits, self.output_map)
if check:
self._check_condition(op.name, getattr(op, "condition", None))
self._check_bits(qargs, self.output_map)
self._check_bits(all_cbits, self.output_map)

node_index = self._add_op_node(op, qargs, cargs)
node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self)
node._node_id = self._multi_graph.add_node(node)
self._increment_op(op)

# Add new in-edges from predecessors of the output nodes to the
# operation node while deleting the old in-edges of the output nodes
# and adding new edges from the operation node to each output node

al = [qargs, all_cbits]
self._multi_graph.insert_node_on_in_edges_multiple(
node_index, [self.output_map[q]._node_id for q in itertools.chain(*al)]
node._node_id,
[self.output_map[bit]._node_id for bits in (qargs, all_cbits) for bit in bits],
)
return self._multi_graph[node_index]
return node

def apply_operation_front(self, op, qargs=(), cargs=()):
def apply_operation_front(self, op, qargs=(), cargs=(), *, check=True):
"""Apply an operation to the input of the circuit.

Args:
op (qiskit.circuit.Operation): the operation associated with the DAG node
qargs (tuple[Qubit]): qubits that op will be applied to
cargs (tuple[Clbit]): cbits that op will be applied to
check (bool): If ``True`` (default), this function will enforce that the
:class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are
:class:`.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must*
uphold these invariants itself, but the cost of several checks will be skipped.
This is most useful when building a new DAG from a source of known-good nodes.
Returns:
DAGOpNode: the node for the op that was added to the dag

Raises:
DAGCircuitError: if initial nodes connected to multiple out edges
"""
all_cbits = set(self._bits_in_operation(op)).union(cargs)
qargs = tuple(qargs) if qargs is not None else ()
cargs = tuple(cargs) if cargs is not None else ()

if self._operation_may_have_bits(op):
# This is the slow path; most of the time, this won't happen.
all_cbits = set(self._bits_in_operation(op)).union(cargs)
else:
all_cbits = cargs

self._check_condition(op.name, getattr(op, "condition", None))
self._check_bits(qargs, self.input_map)
self._check_bits(all_cbits, self.input_map)
node_index = self._add_op_node(op, qargs, cargs)
if check:
self._check_condition(op.name, getattr(op, "condition", None))
self._check_bits(qargs, self.input_map)
self._check_bits(all_cbits, self.input_map)

node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self)
node._node_id = self._multi_graph.add_node(node)
self._increment_op(op)

# Add new out-edges to successors of the input nodes from the
# operation node while deleting the old out-edges of the input nodes
# and adding new edges to the operation node from each input node
al = [qargs, all_cbits]
self._multi_graph.insert_node_on_out_edges_multiple(
node_index, [self.input_map[q]._node_id for q in itertools.chain(*al)]
node._node_id,
[self.input_map[bit]._node_id for bits in (qargs, all_cbits) for bit in bits],
)
return self._multi_graph[node_index]
return node

def compose(self, other, qubits=None, clbits=None, front=False, inplace=True):
"""Compose the ``other`` circuit onto the output of this circuit.
Expand Down Expand Up @@ -819,7 +845,7 @@ def _reject_new_register(reg):
op.condition = variable_mapper.map_condition(condition, allow_reorder=True)
elif isinstance(op, SwitchCaseOp):
op.target = variable_mapper.map_target(op.target)
dag.apply_operation_back(op, m_qargs, m_cargs)
dag.apply_operation_back(op, m_qargs, m_cargs, check=False)
else:
raise DAGCircuitError("bad node type %s" % type(nd))

Expand Down Expand Up @@ -1191,7 +1217,7 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit
node_wire_order = list(node.qargs) + list(node.cargs)
# If we're not propagating it, the number of wires in the input DAG should include the
# condition as well.
if not propagate_condition:
if not propagate_condition and self._operation_may_have_bits(node.op):
node_wire_order += [
bit for bit in self._bits_in_operation(node.op) if bit not in node_cargs
]
Expand Down Expand Up @@ -1260,7 +1286,7 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit
)
new_op = copy.copy(in_node.op)
new_op.condition = new_condition
in_dag.apply_operation_back(new_op, in_node.qargs, in_node.cargs)
in_dag.apply_operation_back(new_op, in_node.qargs, in_node.cargs, check=False)
else:
in_dag = input_dag

Expand Down Expand Up @@ -1483,7 +1509,7 @@ def _key(x):
subgraph_is_classical = False
if not isinstance(node, DAGOpNode):
continue
new_dag.apply_operation_back(node.op, node.qargs, node.cargs)
new_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False)

# Ignore DAGs created for empty clbits
if not subgraph_is_classical:
Expand Down Expand Up @@ -1801,7 +1827,7 @@ def layers(self):

for node in op_nodes:
# this creates new DAGOpNodes in the new_layer
new_layer.apply_operation_back(node.op, node.qargs, node.cargs)
new_layer.apply_operation_back(node.op, node.qargs, node.cargs, check=False)

# The quantum registers that have an operation in this layer.
support_list = [
Expand Down Expand Up @@ -1829,7 +1855,7 @@ def serial_layers(self):
cargs = copy.copy(next_node.cargs)

# Add node to new_layer
new_layer.apply_operation_back(op, qargs, cargs)
new_layer.apply_operation_back(op, qargs, cargs, check=False)
# Add operation to partition
if not getattr(next_node.op, "_directive", False):
support_list.append(list(qargs))
Expand Down
2 changes: 1 addition & 1 deletion qiskit/quantum_info/synthesis/one_qubit_decompose.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def build_circuit(self, gates, global_phase):
else:
gate = gate_entry

dag.apply_operation_back(gate, [qr[0]])
dag.apply_operation_back(gate, (qr[0],), check=False)
return dag
else:
circuit = QuantumCircuit(qr, global_phase=global_phase)
Expand Down
6 changes: 3 additions & 3 deletions qiskit/synthesis/discrete_basis/gate_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,18 +114,18 @@ def to_dag(self):
"""
from qiskit.dagcircuit import DAGCircuit

qreg = [Qubit()]
qreg = (Qubit(),)
dag = DAGCircuit()
dag.add_qubits(qreg)

if len(self.gates) == 0 and not np.allclose(self.product, np.identity(3)):
su2 = _convert_so3_to_su2(self.product)
dag.apply_operation_back(UnitaryGate(su2), qreg)
dag.apply_operation_back(UnitaryGate(su2), qreg, check=False)
return dag

dag.global_phase = self.global_phase
for gate in self.gates:
dag.apply_operation_back(gate, qreg)
dag.apply_operation_back(gate, qreg, check=False)

return dag

Expand Down
2 changes: 1 addition & 1 deletion qiskit/transpiler/passes/basis/basis_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ def _compose_transforms(basis_transforms, source_basis, source_dag):
dag = DAGCircuit()
qr = QuantumRegister(gate_num_qubits)
dag.add_qreg(qr)
dag.apply_operation_back(placeholder_gate, qr[:], [])
dag.apply_operation_back(placeholder_gate, qr, (), check=False)
mapped_instrs[gate_name, gate_num_qubits] = placeholder_params, dag

for gate_name, gate_num_qubits, equiv_params, equiv in basis_transforms:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,6 @@ def _instruction_to_dag(op: Instruction) -> DAGCircuit:
dag = DAGCircuit()
dag.add_qubits([Qubit() for _ in range(op.num_qubits)])
dag.add_qubits([Clbit() for _ in range(op.num_clbits)])
dag.apply_operation_back(op, dag.qubits, dag.clbits)
dag.apply_operation_back(op, dag.qubits, dag.clbits, check=False)

return dag
4 changes: 2 additions & 2 deletions qiskit/transpiler/passes/layout/apply_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def run(self, dag):
virtual_phsyical_map = layout.get_virtual_bits()
for node in dag.topological_op_nodes():
qargs = [q[virtual_phsyical_map[qarg]] for qarg in node.qargs]
new_dag.apply_operation_back(node.op, qargs, node.cargs)
new_dag.apply_operation_back(node.op, qargs, node.cargs, check=False)
else:
# First build a new layout object going from:
# old virtual -> old phsyical -> new virtual -> new physical
Expand All @@ -94,7 +94,7 @@ def run(self, dag):
# Apply new layout to the circuit
for node in dag.topological_op_nodes():
qargs = [q[new_virtual_to_physical[qarg]] for qarg in node.qargs]
new_dag.apply_operation_back(node.op, qargs, node.cargs)
new_dag.apply_operation_back(node.op, qargs, node.cargs, check=False)
self.property_set["layout"] = full_layout
if (final_layout := self.property_set["final_layout"]) is not None:
final_layout_mapping = {
Expand Down
6 changes: 4 additions & 2 deletions qiskit/transpiler/passes/layout/disjoint_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ def split_barriers(dag: DAGCircuit):
split_dag.add_qubits([Qubit() for _ in range(num_qubits)])
for i in range(num_qubits):
split_dag.apply_operation_back(
Barrier(1, label=barrier_uuid), qargs=[split_dag.qubits[i]]
Barrier(1, label=barrier_uuid),
qargs=(split_dag.qubits[i],),
check=False,
)
dag.substitute_node_with_dag(node, split_dag)

Expand Down Expand Up @@ -191,7 +193,7 @@ def separate_dag(dag: DAGCircuit) -> List[DAGCircuit]:
new_dag.global_phase = 0
for node in dag.topological_op_nodes():
if dag_qubits.issuperset(node.qargs):
new_dag.apply_operation_back(node.op, node.qargs, node.cargs)
new_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False)
idle_clbits = []
for bit, node in new_dag.input_map.items():
succ_node = next(new_dag.successors(node))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ def _resynthesize_run(self, matrix, qubit=None):
return best_synth_circuit

def _gate_sequence_to_dag(self, best_synth_circuit):
qubits = [Qubit()]
qubits = (Qubit(),)
out_dag = DAGCircuit()
out_dag.add_qubits(qubits)
out_dag.global_phase = best_synth_circuit.global_phase

for gate_name, angles in best_synth_circuit:
out_dag.apply_operation_back(NAME_MAP[gate_name](*angles), qubits)
out_dag.apply_operation_back(NAME_MAP[gate_name](*angles), qubits, check=False)
return out_dag

def _substitution_checks(self, dag, old_run, new_circ, basis, qubit):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def run(self, dag):
old_measure_qarg = successor.qargs[0]
new_measure_qarg = swap_qargs[swap_qargs.index(old_measure_qarg) - 1]
measure_layer.apply_operation_back(
Measure(), [new_measure_qarg], [successor.cargs[0]]
Measure(), (new_measure_qarg,), (successor.cargs[0],), check=False
)
dag.compose(measure_layer)
dag.remove_op_node(swap)
Expand Down
Loading