Skip to content

Commit

Permalink
Fix custom constraints in transpile with BackendV2 (#12042)
Browse files Browse the repository at this point in the history
* Give priority over backend target to custom constraints when using transpile and BackendV2.

* Add reno

* Add backend_properties to skip_target

* Add target to pulse analysis passes

* Add tests

* Update reno

* Fix lint

* Add table to docs

* Update qiskit/compiler/transpiler.py

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

* Update explanation

---------

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
  • Loading branch information
ElePT and mtreinish authored Mar 26, 2024
1 parent 67ceaaa commit 090b2b1
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 14 deletions.
39 changes: 35 additions & 4 deletions qiskit/compiler/transpiler.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2019.
# (C) Copyright IBM 2017, 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
Expand Down Expand Up @@ -72,9 +72,32 @@ def transpile( # pylint: disable=too-many-return-statements
"""Transpile one or more circuits, according to some desired transpilation targets.
Transpilation is potentially done in parallel using multiprocessing when ``circuits``
is a list with > 1 :class:`~.QuantumCircuit` object depending on the local environment
is a list with > 1 :class:`~.QuantumCircuit` object, depending on the local environment
and configuration.
The prioritization of transpilation target constraints works as follows: if a ``target``
input is provided, it will take priority over any ``backend`` input or loose constraints
(``basis_gates``, ``inst_map``, ``coupling_map``, ``backend_properties``, ``instruction_durations``,
``dt`` or ``timing_constraints``). If a ``backend`` is provided together with any loose constraint
from the list above, the loose constraint will take priority over the corresponding backend
constraint. This behavior is independent of whether the ``backend`` instance is of type
:class:`.BackendV1` or :class:`.BackendV2`, as summarized in the table below. The first column
in the table summarizes the potential user-provided constraints, and each cell shows whether
the priority is assigned to that specific constraint input or another input
(`target`/`backend(V1)`/`backend(V2)`).
============================ ========= ======================== =======================
User Provided target backend(V1) backend(V2)
============================ ========= ======================== =======================
**basis_gates** target basis_gates basis_gates
**coupling_map** target coupling_map coupling_map
**instruction_durations** target instruction_durations instruction_durations
**inst_map** target inst_map inst_map
**dt** target dt dt
**timing_constraints** target timing_constraints timing_constraints
**backend_properties** target backend_properties backend_properties
============================ ========= ======================== =======================
Args:
circuits: Circuit(s) to transpile
backend: If set, the transpiler will compile the input circuit to this target
Expand Down Expand Up @@ -325,8 +348,16 @@ def callback_func(**kwargs):
backend_properties = target_to_backend_properties(target)
# If target is not specified and any hardware constraint object is
# manually specified then do not use the target from the backend as
# it is invalidated by a custom basis gate list or a custom coupling map
elif basis_gates is not None or coupling_map is not None:
# it is invalidated by a custom basis gate list, custom coupling map,
# custom dt or custom instruction_durations
elif (
basis_gates is not None # pylint: disable=too-many-boolean-expressions
or coupling_map is not None
or dt is not None
or instruction_durations is not None
or backend_properties is not None
or timing_constraints is not None
):
_skip_target = True
else:
target = getattr(backend, "target", None)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
# (C) Copyright IBM 2021, 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
Expand All @@ -14,6 +14,7 @@
from qiskit.circuit.delay import Delay
from qiskit.dagcircuit import DAGCircuit
from qiskit.transpiler.basepasses import AnalysisPass
from qiskit.transpiler import Target


class InstructionDurationCheck(AnalysisPass):
Expand All @@ -28,11 +29,7 @@ class InstructionDurationCheck(AnalysisPass):
of the hardware alignment constraints, which is true in general.
"""

def __init__(
self,
acquire_alignment: int = 1,
pulse_alignment: int = 1,
):
def __init__(self, acquire_alignment: int = 1, pulse_alignment: int = 1, target: Target = None):
"""Create new duration validation pass.
The alignment values depend on the control electronics of your quantum processor.
Expand All @@ -42,10 +39,16 @@ def __init__(
trigger acquisition instruction in units of ``dt``.
pulse_alignment: Integer number representing the minimum time resolution to
trigger gate instruction in units of ``dt``.
target: The :class:`~.Target` representing the target backend, if
``target`` is specified then this argument will take
precedence and ``acquire_alignment`` and ``pulse_alignment`` will be ignored.
"""
super().__init__()
self.acquire_align = acquire_alignment
self.pulse_align = pulse_alignment
if target is not None:
self.acquire_align = target.acquire_alignment
self.pulse_align = target.pulse_alignment

def run(self, dag: DAGCircuit):
"""Run duration validation passes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from qiskit.pulse import Play
from qiskit.transpiler.basepasses import AnalysisPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler import Target


class ValidatePulseGates(AnalysisPass):
Expand Down Expand Up @@ -43,6 +44,7 @@ def __init__(
self,
granularity: int = 1,
min_length: int = 1,
target: Target = None,
):
"""Create new pass.
Expand All @@ -53,10 +55,16 @@ def __init__(
min_length: Integer number representing the minimum data point length to
define the pulse gate in units of ``dt``. This value depends on
the control electronics of your quantum processor.
target: The :class:`~.Target` representing the target backend, if
``target`` is specified then this argument will take
precedence and ``granularity`` and ``min_length`` will be ignored.
"""
super().__init__()
self.granularity = granularity
self.min_length = min_length
if target is not None:
self.granularity = target.granularity
self.min_length = target.min_length

def run(self, dag: DAGCircuit):
"""Run the pulse gate validation attached to ``dag``.
Expand Down
10 changes: 9 additions & 1 deletion qiskit/transpiler/passes/scheduling/alignments/reschedule.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
# (C) Copyright IBM 2022, 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
Expand All @@ -20,6 +20,7 @@
from qiskit.dagcircuit import DAGCircuit, DAGOpNode, DAGOutNode
from qiskit.transpiler.basepasses import AnalysisPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler import Target


class ConstrainedReschedule(AnalysisPass):
Expand Down Expand Up @@ -63,6 +64,7 @@ def __init__(
self,
acquire_alignment: int = 1,
pulse_alignment: int = 1,
target: Target = None,
):
"""Create new rescheduler pass.
Expand All @@ -73,10 +75,16 @@ def __init__(
trigger acquisition instruction in units of ``dt``.
pulse_alignment: Integer number representing the minimum time resolution to
trigger gate instruction in units of ``dt``.
target: The :class:`~.Target` representing the target backend, if
``target`` is specified then this argument will take
precedence and ``acquire_alignment`` and ``pulse_alignment`` will be ignored.
"""
super().__init__()
self.acquire_align = acquire_alignment
self.pulse_align = pulse_alignment
if target is not None:
self.acquire_align = target.acquire_alignment
self.pulse_align = target.pulse_alignment

@classmethod
def _get_next_gate(cls, dag: DAGCircuit, node: DAGOpNode) -> Generator[DAGOpNode, None, None]:
Expand Down
3 changes: 3 additions & 0 deletions qiskit/transpiler/preset_passmanagers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,13 +584,15 @@ def _require_alignment(property_set):
InstructionDurationCheck(
acquire_alignment=timing_constraints.acquire_alignment,
pulse_alignment=timing_constraints.pulse_alignment,
target=target,
)
)
scheduling.append(
ConditionalController(
ConstrainedReschedule(
acquire_alignment=timing_constraints.acquire_alignment,
pulse_alignment=timing_constraints.pulse_alignment,
target=target,
),
condition=_require_alignment,
)
Expand All @@ -599,6 +601,7 @@ def _require_alignment(property_set):
ValidatePulseGates(
granularity=timing_constraints.granularity,
min_length=timing_constraints.min_length,
target=target,
)
)
if scheduling_method:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
fixes:
- |
A bug in :func:`.transpile` has been fixed where custom ``instruction_durations``, ``dt`` and ``backend_properties``
constraints would be ignored when provided at the same time as a backend of type :class:`.BackendV2`. The behavior
after the fix is now independent of whether the provided backend is of type :class:`.BackendV1` or
type :class:`.BackendV2`. Similarly, custom ``timing_constraints`` are now overridden by ``target`` inputs
but take precedence over :class:`.BackendV1` and :class:`.BackendV2` inputs.
features_transpiler:
- |
The following analysis passes now accept constraints encoded in a :class:`.Target` thanks to a new ``target``
input argument:
* :class:`.InstructionDurationCheck`
* :class:`.ConstrainedReschedule`
* :class:`.ValidatePulseGates`
The target constraints will have priority over user-provided constraints, for coherence with the rest of
the transpiler pipeline.
150 changes: 147 additions & 3 deletions test/python/compiler/test_transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,26 @@
from qiskit.dagcircuit import DAGOpNode, DAGOutNode
from qiskit.exceptions import QiskitError
from qiskit.providers.backend import BackendV2
from qiskit.providers.fake_provider import Fake20QV1, GenericBackendV2
from qiskit.providers.backend_compat import BackendV2Converter
from qiskit.providers.fake_provider import Fake20QV1, Fake27QPulseV1, GenericBackendV2
from qiskit.providers.basic_provider import BasicSimulator
from qiskit.providers.options import Options
from qiskit.pulse import InstructionScheduleMap
from qiskit.pulse import InstructionScheduleMap, Schedule, Play, Gaussian, DriveChannel
from qiskit.quantum_info import Operator, random_unitary
from qiskit.utils import parallel
from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass
from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget
from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout
from qiskit.transpiler.passmanager_config import PassManagerConfig
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager, level_0_pass_manager
from qiskit.transpiler.target import InstructionProperties, Target
from qiskit.transpiler.target import (
InstructionProperties,
Target,
TimingConstraints,
InstructionDurations,
target_to_backend_properties,
)

from test import QiskitTestCase, combine, slow_test # pylint: disable=wrong-import-order

from ..legacy_cmaps import MELBOURNE_CMAP, RUESCHLIKON_CMAP
Expand Down Expand Up @@ -1499,6 +1507,142 @@ def test_scheduling_backend_v2(self):
self.assertIn("delay", out[0].count_ops())
self.assertIn("delay", out[1].count_ops())

def test_scheduling_timing_constraints(self):
"""Test that scheduling-related loose transpile constraints
work with both BackendV1 and BackendV2."""

backend_v1 = Fake27QPulseV1()
backend_v2 = BackendV2Converter(backend_v1)
# the original timing constraints are granularity = min_length = 16
timing_constraints = TimingConstraints(granularity=32, min_length=64)
error_msgs = {
65: "Pulse duration is not multiple of 32",
32: "Pulse gate duration is less than 64",
}

for backend, duration in zip([backend_v1, backend_v2], [65, 32]):
with self.subTest(backend=backend, duration=duration):
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
qc.add_calibration(
"h", [0], Schedule(Play(Gaussian(duration, 0.2, 4), DriveChannel(0))), [0, 0]
)
qc.add_calibration(
"cx",
[0, 1],
Schedule(Play(Gaussian(duration, 0.2, 4), DriveChannel(1))),
[0, 0],
)
with self.assertRaisesRegex(TranspilerError, error_msgs[duration]):
_ = transpile(
qc,
backend=backend,
timing_constraints=timing_constraints,
)

def test_scheduling_instruction_constraints(self):
"""Test that scheduling-related loose transpile constraints
work with both BackendV1 and BackendV2."""

backend_v1 = Fake27QPulseV1()
backend_v2 = BackendV2Converter(backend_v1)
qc = QuantumCircuit(2)
qc.h(0)
qc.delay(500, 1, "dt")
qc.cx(0, 1)
# update durations
durations = InstructionDurations.from_backend(backend_v1)
durations.update([("cx", [0, 1], 1000, "dt")])

for backend in [backend_v1, backend_v2]:
with self.subTest(backend=backend):
scheduled = transpile(
qc,
backend=backend,
scheduling_method="alap",
instruction_durations=durations,
layout_method="trivial",
)
self.assertEqual(scheduled.duration, 1500)

def test_scheduling_dt_constraints(self):
"""Test that scheduling-related loose transpile constraints
work with both BackendV1 and BackendV2."""

backend_v1 = Fake27QPulseV1()
backend_v2 = BackendV2Converter(backend_v1)
qc = QuantumCircuit(1, 1)
qc.x(0)
qc.measure(0, 0)
original_dt = 2.2222222222222221e-10
original_duration = 3504

for backend in [backend_v1, backend_v2]:
with self.subTest(backend=backend):
# halve dt in sec = double duration in dt
scheduled = transpile(
qc, backend=backend, scheduling_method="asap", dt=original_dt / 2
)
self.assertEqual(scheduled.duration, original_duration * 2)

def test_backend_props_constraints(self):
"""Test that loose transpile constraints
work with both BackendV1 and BackendV2."""

backend_v1 = Fake20QV1()
backend_v2 = BackendV2Converter(backend_v1)
qr1 = QuantumRegister(3, "qr1")
qr2 = QuantumRegister(2, "qr2")
qc = QuantumCircuit(qr1, qr2)
qc.cx(qr1[0], qr1[1])
qc.cx(qr1[1], qr1[2])
qc.cx(qr1[2], qr2[0])
qc.cx(qr2[0], qr2[1])

# generate a fake backend with same number of qubits
# but different backend properties
fake_backend = GenericBackendV2(num_qubits=20, seed=42)
custom_backend_properties = target_to_backend_properties(fake_backend.target)

# expected layout for custom_backend_properties
# (different from expected layout for Fake20QV1)
vf2_layout = {
18: Qubit(QuantumRegister(3, "qr1"), 1),
13: Qubit(QuantumRegister(3, "qr1"), 2),
19: Qubit(QuantumRegister(3, "qr1"), 0),
14: Qubit(QuantumRegister(2, "qr2"), 0),
9: Qubit(QuantumRegister(2, "qr2"), 1),
0: Qubit(QuantumRegister(15, "ancilla"), 0),
1: Qubit(QuantumRegister(15, "ancilla"), 1),
2: Qubit(QuantumRegister(15, "ancilla"), 2),
3: Qubit(QuantumRegister(15, "ancilla"), 3),
4: Qubit(QuantumRegister(15, "ancilla"), 4),
5: Qubit(QuantumRegister(15, "ancilla"), 5),
6: Qubit(QuantumRegister(15, "ancilla"), 6),
7: Qubit(QuantumRegister(15, "ancilla"), 7),
8: Qubit(QuantumRegister(15, "ancilla"), 8),
10: Qubit(QuantumRegister(15, "ancilla"), 9),
11: Qubit(QuantumRegister(15, "ancilla"), 10),
12: Qubit(QuantumRegister(15, "ancilla"), 11),
15: Qubit(QuantumRegister(15, "ancilla"), 12),
16: Qubit(QuantumRegister(15, "ancilla"), 13),
17: Qubit(QuantumRegister(15, "ancilla"), 14),
}

for backend in [backend_v1, backend_v2]:
with self.subTest(backend=backend):
result = transpile(
qc,
backend=backend,
backend_properties=custom_backend_properties,
optimization_level=2,
seed_transpiler=42,
)

self.assertEqual(result._layout.initial_layout._p2v, vf2_layout)

@data(1, 2, 3)
def test_no_infinite_loop(self, optimization_level):
"""Verify circuit cost always descends and optimization does not flip flop indefinitely."""
Expand Down

0 comments on commit 090b2b1

Please sign in to comment.