Skip to content
This repository has been archived by the owner on Jul 24, 2024. It is now read-only.

Commit

Permalink
Add option to no pad idle qubits in scheduling passes
Browse files Browse the repository at this point in the history
This commit adds a new flag to the scheduling padding passes,
schedule_idle_qubits, which is used to opt-in to scheduling idle qubits.
By default these passes will only schedule qubits which are active. This
is a change in behavior from before where the previous default is all
qubits were scheduled. This was undesireable because it's adding
uneceesary instructions to the job payload which will need to be
processed which are effectively a no-op. The only real use case for
adding delays to idle wires is visualization, so it's left as an option
to re-enable the previous default.

Fixes #723

Co-authored-by: Thomas Alexander <[email protected]>
  • Loading branch information
mtreinish and taalexander committed Sep 12, 2023
1 parent b03b14d commit 1eb804c
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class BlockBasePadder(TransformationPass):
which may result in violation of hardware alignment constraints.
"""

def __init__(self) -> None:
def __init__(self, schedule_idle_qubits: bool = False) -> None:
self._node_start_time = None
self._node_block_dags = None
self._idle_after: Optional[Dict[Qubit, int]] = None
Expand All @@ -84,7 +84,8 @@ def __init__(self) -> None:

self._dirty_qubits: Set[Qubit] = set()
# Qubits that are dirty in the circuit.

self._schedule_idle_qubits = schedule_idle_qubits
self._idle_qubits = set()
super().__init__()

def run(self, dag: DAGCircuit) -> DAGCircuit:
Expand All @@ -100,6 +101,10 @@ def run(self, dag: DAGCircuit) -> DAGCircuit:
TranspilerError: When a particular node is not scheduled, likely some transform pass
is inserted before this node is called.
"""
if not self._schedule_idle_qubits:
self._idle_qubits = set(
wire for wire in dag.idle_wires() if isinstance(wire, Qubit)
)
self._pre_runhook(dag)

self._init_run(dag)
Expand Down Expand Up @@ -420,7 +425,7 @@ def _visit_control_flow_op(self, node: DAGNode) -> None:
self._add_block_terminating_barrier(block_idx, t0, node)

# Only pad non-fast path nodes
fast_path_node = node in self._fast_path_nodes
fast_path_node = node in self._fast_path_nodes or not self._schedule_idle_qubits

# TODO: This is a hack required to tie nodes of control-flow
# blocks across the scheduler and block_base_padder. This is
Expand Down Expand Up @@ -503,6 +508,8 @@ def _visit_generic(self, node: DAGNode) -> None:
self._block_duration = max(self._block_duration, t1)

for bit in self._map_wires(node.qargs):
if bit in self._idle_qubits:
continue
# Fill idle time with some sequence
if t0 - self._idle_after.get(bit, 0) > 0:
# Find previous node on the wire, i.e. always the latest node on the wire
Expand Down Expand Up @@ -549,6 +556,8 @@ def _terminate_block(self, block_duration: int, block_idx: int) -> None:
def _pad_until_block_end(self, block_duration: int, block_idx: int) -> None:
# Add delays until the end of circuit.
for bit in self._block_dag.qubits:
if bit in self._idle_qubits:
continue
idle_after = self._idle_after.get(bit, 0)
if block_duration - idle_after > 0:
node = self._block_dag.output_map[bit]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def __init__(
insert_multiple_cycles: bool = False,
coupling_map: CouplingMap = None,
alt_spacings: Optional[Union[List[List[float]], List[float]]] = None,
schedule_idle_qubits: bool = False,
):
"""Dynamical decoupling initializer.
Expand Down Expand Up @@ -173,6 +174,9 @@ def __init__(
alt_spacings: A list of lists of spacings between the DD gates, for the second subcircuit,
as determined by the coupling map. If None, a balanced spacing that is staggered with
respect to the first subcircuit will be used [d, d, d, ..., d, d, 0].
schedule_idle_qubits: Set to true if you'd like a delay inserted on idle qubits.
This is useful for timeline visualizations, but may cause issues
for execution on large backends.
Raises:
TranspilerError: When invalid DD sequence is specified.
TranspilerError: When pulse gate with the duration which is
Expand Down
7 changes: 5 additions & 2 deletions qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,16 @@ class PadDelay(BlockBasePadder):
See :class:`BlockBasePadder` pass for details.
"""

def __init__(self, fill_very_end: bool = True):
def __init__(self, fill_very_end: bool = True, schedule_idle_qubits: bool = False):
"""Create new padding delay pass.
Args:
fill_very_end: Set ``True`` to fill the end of circuit with delay.
schedule_idle_qubits: Set to true if you'd like a delay inserted on idle qubits.
This is useful for timeline visualizations, but may cause issues for execution
on large backends.
"""
super().__init__()
super().__init__(schedule_idle_qubits=schedule_idle_qubits)
self.fill_very_end = fill_very_end

def _pad(
Expand Down
20 changes: 20 additions & 0 deletions releasenotes/notes/scheduling-no-idle-bbadd1e95d2a71b8.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
features:
- |
Added a new flag, ``schedule_idle_qubits`` to the constructor for the
:class:`.PadDelay` and :class:`.PadDynamicalDecoupling` passes. This
flag when set to ``True`` will have the scheduling passes insert a full
circuit duration delay on any idle qubits in the circuit.
upgrade:
- |
The default behavior of the :class:`.PadDelay` and
:class:`.PadDynamicalDecoupling` passes for idle qubits in the circuit
have changed. Previously, by default the passes would schedule any idle
qubits in the circuit by inserting a delay equal to the full circuit
duration. This has been changed so by default only active qubits are
scheduled. This change was made because the extra delays were additional
overhead in the job payload that were effectively a no-op so they added
extra overhead to job submission for no gain. If you need to restore
the previous behavior you can instantiate :class:`.PadDelay` or
:class:`.PadDynamicalDecoupling` with the keyword argument
``schedule_idle_qubits=True`` which will restore the previous behavior.
2 changes: 1 addition & 1 deletion test/unit/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,6 @@ def test_list(self):
).to_saved_format(),
}
), self.subTest("filtered list of accounts"):

accounts = list(AccountManager.list(channel="ibm_quantum").keys())
self.assertEqual(len(accounts), 2)
self.assertListEqual(accounts, ["key2", _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM])
Expand Down Expand Up @@ -321,6 +320,7 @@ def test_delete_auth(self):
"urls": {"https": "127.0.0.1", "username_ntlm": "", "password_ntlm": ""}
}


# TODO: update and reenable test cases to work with qiskit-ibm-provider
# NamedTemporaryFiles not supported in Windows
@skipIf(os.name == "nt", "Test not supported in Windows")
Expand Down
100 changes: 90 additions & 10 deletions test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def test_insert_dd_ghz(self):
dd_sequence,
pulse_alignment=1,
sequence_min_length_ratios=[1.0],
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -122,7 +123,11 @@ def test_insert_dd_ghz_one_qubit(self):
[
ASAPScheduleAnalysis(self.durations),
PadDynamicalDecoupling(
self.durations, dd_sequence, qubits=[0], pulse_alignment=1
self.durations,
dd_sequence,
qubits=[0],
pulse_alignment=1,
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -158,6 +163,7 @@ def test_insert_dd_ghz_everywhere(self):
skip_reset_qubits=False,
pulse_alignment=1,
sequence_min_length_ratios=[0.0],
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -195,6 +201,7 @@ def test_insert_dd_ghz_xy4(self):
dd_sequence,
pulse_alignment=1,
sequence_min_length_ratios=[1.0],
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -234,7 +241,12 @@ def test_insert_midmeas_hahn(self):
pm = PassManager(
[
ASAPScheduleAnalysis(self.durations),
PadDynamicalDecoupling(self.durations, dd_sequence, pulse_alignment=1),
PadDynamicalDecoupling(
self.durations,
dd_sequence,
pulse_alignment=1,
schedule_idle_qubits=True,
),
]
)

Expand Down Expand Up @@ -291,6 +303,7 @@ def uhrig(k):
spacings=spacing,
sequence_min_length_ratios=[0.0],
pulse_alignment=1,
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -332,7 +345,11 @@ def test_asymmetric_xy4_in_t2(self):
[
ASAPScheduleAnalysis(self.durations),
PadDynamicalDecoupling(
self.durations, dd_sequence, pulse_alignment=1, spacings=spacing
self.durations,
dd_sequence,
pulse_alignment=1,
spacings=spacing,
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -375,6 +392,7 @@ def test_dd_after_reset(self):
skip_reset_qubits=True,
pulse_alignment=1,
sequence_min_length_ratios=[0.0],
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -410,7 +428,9 @@ def test_insert_dd_bad_sequence(self):
pm = PassManager(
[
ASAPScheduleAnalysis(self.durations),
PadDynamicalDecoupling(self.durations, dd_sequence),
PadDynamicalDecoupling(
self.durations, dd_sequence, schedule_idle_qubits=True
),
]
)

Expand Down Expand Up @@ -444,7 +464,9 @@ def test_dd_with_calibrations_with_parameters(self, param_value):
pm = PassManager(
[
ASAPScheduleAnalysis(durations),
PadDynamicalDecoupling(durations, dd_sequence),
PadDynamicalDecoupling(
durations, dd_sequence, schedule_idle_qubits=True
),
]
)
dd_circuit = pm.run(circ)
Expand All @@ -466,6 +488,7 @@ def test_insert_dd_ghz_xy4_with_alignment(self):
pulse_alignment=10,
extra_slack_distribution="edges",
sequence_min_length_ratios=[1.0],
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -510,16 +533,25 @@ def test_dd_can_sequentially_called(self):
pm1 = PassManager(
[
ASAPScheduleAnalysis(self.durations),
PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0]),
PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[1]),
PadDynamicalDecoupling(
self.durations, dd_sequence, qubits=[0], schedule_idle_qubits=True
),
PadDynamicalDecoupling(
self.durations, dd_sequence, qubits=[1], schedule_idle_qubits=True
),
]
)
circ1 = pm1.run(self.ghz4)

pm2 = PassManager(
[
ASAPScheduleAnalysis(self.durations),
PadDynamicalDecoupling(self.durations, dd_sequence, qubits=[0, 1]),
PadDynamicalDecoupling(
self.durations,
dd_sequence,
qubits=[0, 1],
schedule_idle_qubits=True,
),
]
)
circ2 = pm2.run(self.ghz4)
Expand All @@ -538,6 +570,7 @@ def test_back_to_back_if_test(self):
dd_sequence,
pulse_alignment=1,
sequence_min_length_ratios=[0.0],
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -594,6 +627,7 @@ def test_dd_if_test(self):
dd_sequence,
pulse_alignment=1,
sequence_min_length_ratios=[0.0],
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -677,14 +711,18 @@ def test_reproducible(self):
pm0 = PassManager(
[
ASAPScheduleAnalysis(self.durations),
PadDynamicalDecoupling(self.durations, dd_sequence),
PadDynamicalDecoupling(
self.durations, dd_sequence, schedule_idle_qubits=True
),
]
)

pm1 = PassManager(
[
ASAPScheduleAnalysis(self.durations),
PadDynamicalDecoupling(self.durations, dd_sequence),
PadDynamicalDecoupling(
self.durations, dd_sequence, schedule_idle_qubits=True
),
]
)
qc_dd0 = pm0.run(qc)
Expand All @@ -704,6 +742,7 @@ def test_nested_block_dd(self):
dd_sequence,
pulse_alignment=1,
sequence_min_length_ratios=[0.0],
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -760,6 +799,7 @@ def test_multiple_dd_sequences(self):
dd_sequence,
pulse_alignment=1,
sequence_min_length_ratios=[1.5, 0.0],
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -838,6 +878,7 @@ def test_multiple_dd_sequence_cycles(self):
pulse_alignment=1,
sequence_min_length_ratios=[10.0],
insert_multiple_cycles=True,
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -869,6 +910,7 @@ def test_staggered_dd(self):
dd_sequence,
coupling_map=self.coupling_map,
alt_spacings=[0.1, 0.8, 0.1],
schedule_idle_qubits=True,
),
]
)
Expand Down Expand Up @@ -988,6 +1030,7 @@ def test_disjoint_coupling_map(self):
self.durations,
dd_sequence,
coupling_map=CouplingMap([[0, 1], [1, 2], [3, 4]]),
schedule_idle_qubits=True,
),
]
)
Expand All @@ -1003,3 +1046,40 @@ def test_disjoint_coupling_map(self):
self.assertNotEqual(delay_dict[1], delay_dict[2])
self.assertNotEqual(delay_dict[3], delay_dict[4])
self.assertEqual(delay_dict[0], delay_dict[2])

def test_no_unused_qubits(self):
"""Test DD with if_test circuit that unused qubits are untouched and not scheduled.
This ensures that programs don't have unnecessary information for unused qubits.
Which might hurt performance in later executon stages.
"""

dd_sequence = [XGate(), XGate()]
pm = PassManager(
[
ASAPScheduleAnalysis(self.durations),
PadDynamicalDecoupling(
self.durations,
dd_sequence,
pulse_alignment=1,
sequence_min_length_ratios=[0.0],
),
]
)

qc = QuantumCircuit(3, 1)
qc.measure(0, 0)
qc.x(1)
with qc.if_test((0, True)):
qc.x(1)
qc.measure(0, 0)
with qc.if_test((0, True)):
qc.x(0)
qc.x(1)

qc_dd = pm.run(qc)

dont_use = qc_dd.qubits[-1]
for op in qc_dd.data:
if op.operation.name != "barrier":
self.assertNotIn(dont_use, op.qubits)
Loading

0 comments on commit 1eb804c

Please sign in to comment.