Skip to content

Commit

Permalink
Start making the transpiler Target aware (#7227)
Browse files Browse the repository at this point in the history
* Start making the BasisTranslator Target/BackendV2 aware

In #5885 we added the next version of the abstract Backend interface
BackendV2 which added the concept of a Target which represents a
compiler target for the transpiler. It contains all the information
about the constraints of a backend for the compiler to use and replaces
the combination of basis_gates, coupling_map, etc and expands the
representation to model more complex devices. However, in #5885 we only
introduced the interface and didn't modify the transpiler to use the
Target natively or any of the extra information it contains. This commit
is the start of the process of updated the transpiler to work with a
target natively. To start if a backend has a target that is now passed
through from transpile to the passmanager_config so we can start passing
it directly to passes as we enable it. Then the basis translator is
updated to work natively with a target instead of the basis gates list
it used before. In addition to using a target directly support is added
for heterogeneous gate sets so that target instructions can work on only
a subset of qargs.

Building off this in the future There are additional features in target
that we might want to expand support for in the BasisTranslator in the
future, such as supporting custom variants of the same gate, or handling
fixed angle rotation gate variants, etc.

* Deduplicate computation of non-global operations

The modifications to the BasisTranslator needed to compute operations in
the basis which weren't global for a particular target and accomplished
this via a dedicated helper function in the pass module. However the
BackendV2 class had an identical function for accomplishing this so it
could log a warning when basis gates were queried. This commit
deduplicates them and creates a dedicated method on the target for
returning the list (and caches it so it's only computed once per target).

* Add missing check for a dag calibration on the target path

* Rename extra_basis_transforms -> qarg_local_basis_transforms

* Make GateDirection and CheckGateDirection Target aware too

In debugging the basis translator changes we realized that we should be
relying on the GateDirection pass to fix directionality on non-symmetric
2q gates instead of trying to handle it natively in the basis translator
for now. To do this with a target we need to make the GateDirection and
CheckGateDirection passes target aware and update the basis translator
to treat all 2q gates as symmetric. This commit makes this change and
updates the preset pass managers to pass targets to GateDirection and
CheckGateDirection (also updates the rule determining if we need to run
them to leverage the target not the coupling map).

* Handle working with a non-global 1q basis

In the case of a non-global 1q basis where there are 1q gates only
available on a subset of qubits the basis translator was not correctly
able to translate multi-qubit gates. This was due to how the local basis
search was done just on the global target basis gates and the local
target basis gates for that argument, with multi-qubit gates the
translations also typically involve a 1q rotation. But if there are no
global 1q gates those rotation gates can't be translated and the search
fails. To address this the 1q local gates for the individual qubits in
the multi-qubit argument are added to the local search for non-global
multi-qubit gates.

* Also use target for gate direction in level 0

* Add release notes

* Consider all non-local subset operations for multiqubit non local gates

* Update qiskit/providers/backend.py

Co-authored-by: Kevin Krsulich <kevin@krsulich.net>

* Finish incomplete strict_direction docstring

* Adjust tests to be fp precision tolerant

* Relax tests further for windows

* Update qiskit/transpiler/target.py

Co-authored-by: Kevin Krsulich <kevin@krsulich.net>

* Correct detection of non-global ops with strict_direction=False

Co-authored-by: georgios-ts <45130028+georgios-ts@users.noreply.github.com>

* Fix handling of non-local subset searching

* Simplify target path in gate direction pass

* Rename extra_source_basis -> local_source_basis

* Rename incomplete_basis -> non_global_operations

* Rename qarg_with_incomplete -> qargs_with_non_global_operation and incomplete_source_basis -> qargs_local_source_basis

* Update target handling in transpile()

This fixes 2 issues found in review with the the transpile() functions
handling of target. First if a target kwarg is passed by a user that
target will be used instead of the backend for not just the target but
also all derived quantities (such as coupling_map and basis_gates).
Also previously the backend_properties field was not being correctly
built from a target (both for a backendv2 or a standalone target). So a
converter helper function was added to go from a target and build a
BackendPropeties object from the data contained in the target. This will
enable noise aware transpilation until the transpiler passes are all
target aware.

* Calculate qargs with non global operation once per pass instance

* Expand docstring to include non-global operation behavior description

* Add test assertion that it matches the target

* Add GateDirection to the optimization loop for level 3

Optimization level 3 is different from the other lower optimization
levels because it uses unitary synthesis by default. The unitary
synthesis pass is basis and coupling map to optimize the synthesized
circuit for a unitary to be hardware efficient. However when using a
target the basis gates and coupling map don't give a complete picture of
the device constraints (mainly the non-global gates if any). To account
for this we need to run the gate direction pass after unitary synthesis
to correct an incorrect decision the unitary synthesis pass might make
based on it's incomplete data. Once UnitarySynthesis is target aware we
probably do not require this anymore.

* Add tests of 5q ghz on non-linear non-global target with different entangling gates

* Use frozenset instead of tuple for local search and transformation tracking

* Fix copy paste error for gate error in _target_to_backend_properties

* Apply suggestions from code review

Co-authored-by: Kevin Krsulich <kevin@krsulich.net>

* Only run gate direction in level 3 with a target

* Move target overrides inside _parse_transpile_args()

Co-authored-by: Kevin Krsulich <kevin@krsulich.net>
Co-authored-by: georgios-ts <45130028+georgios-ts@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 4, 2021
1 parent 37684d4 commit 1a9f938
Show file tree
Hide file tree
Showing 18 changed files with 1,464 additions and 135 deletions.
190 changes: 159 additions & 31 deletions qiskit/compiler/transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# that they have been altered from the originals.

"""Circuit transpile function"""
import datetime
import logging
import warnings
from time import time
Expand Down Expand Up @@ -40,6 +41,7 @@
level_3_pass_manager,
)
from qiskit.transpiler.timing_constraints import TimingConstraints
from qiskit.transpiler.target import Target

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -67,6 +69,7 @@ def transpile(
output_name: Optional[Union[str, List[str]]] = None,
unitary_synthesis_method: str = "default",
unitary_synthesis_plugin_config: dict = None,
target: Target = None,
) -> Union[QuantumCircuit, List[QuantumCircuit]]:
"""Transpile one or more circuits, according to some desired transpilation targets.
Expand Down Expand Up @@ -229,7 +232,10 @@ def callback_func(**kwargs):
the ``unitary_synthesis`` argument. As this is custom for each
unitary synthesis plugin refer to the plugin documentation for how
to use this option.
target: A backend transpiler target. Normally this is specified as part of
the ``backend`` argument, but if you have manually constructed a
:class:`~qiskit.transpiler.Target` object you can specify it manually here.
This will override the target from ``backend``.
Returns:
The transpiled circuit(s).
Expand Down Expand Up @@ -268,6 +274,7 @@ def callback_func(**kwargs):
approximation_degree=approximation_degree,
unitary_synthesis_method=unitary_synthesis_method,
backend=backend,
target=target,
)

warnings.warn(
Expand All @@ -284,7 +291,12 @@ def callback_func(**kwargs):
config = user_config.get_config()
optimization_level = config.get("transpile_optimization_level", 1)

if scheduling_method is not None and backend is None and not instruction_durations:
if (
scheduling_method is not None
and backend is None
and target is None
and not instruction_durations
):
warnings.warn(
"When scheduling circuits without backend,"
" 'instruction_durations' should be usually provided.",
Expand Down Expand Up @@ -314,6 +326,7 @@ def callback_func(**kwargs):
timing_constraints,
unitary_synthesis_method,
unitary_synthesis_plugin_config,
target,
)

_check_circuits_coupling_map(circuits, transpile_args, backend)
Expand Down Expand Up @@ -505,6 +518,7 @@ def _parse_transpile_args(
timing_constraints,
unitary_synthesis_method,
unitary_synthesis_plugin_config,
target,
) -> List[Dict]:
"""Resolve the various types of args allowed to the transpile() function through
duck typing, overriding args, etc. Refer to the transpile() docstring for details on
Expand All @@ -526,6 +540,24 @@ def _parse_transpile_args(
# number of circuits. If single, duplicate to create a list of that size.
num_circuits = len(circuits)

# If a target is specified have it override any implicit selections from a backend
# but if an argument is explicitly passed use that instead of the target version
if target is not None:
if coupling_map is None:
coupling_map = target.coupling_map()
if basis_gates is None:
basis_gates = target.operation_names()
if instruction_durations is None:
instruction_durations = target.durations()
if inst_map is None:
inst_map = target.instruction_schedule_map()
if dt is None:
dt = target.dt
if timing_constraints is None:
timing_constraints = target.timing_constraints()
if backend_properties is None:
backend_properties = _target_to_backend_properties(target)

basis_gates = _parse_basis_gates(basis_gates, backend, circuits)
inst_map = _parse_inst_map(inst_map, backend, num_circuits)
faulty_qubits_map = _parse_faulty_qubits_map(backend, num_circuits)
Expand All @@ -550,6 +582,7 @@ def _parse_transpile_args(
durations = _parse_instruction_durations(backend, instruction_durations, dt, circuits)
scheduling_method = _parse_scheduling_method(scheduling_method, num_circuits)
timing_constraints = _parse_timing_constraints(backend, timing_constraints, num_circuits)
target = _parse_target(backend, target, num_circuits)
if scheduling_method and any(d is None for d in durations):
raise TranspilerError(
"Transpiling a circuit with a scheduling method"
Expand Down Expand Up @@ -579,6 +612,7 @@ def _parse_transpile_args(
"faulty_qubits_map": faulty_qubits_map,
"unitary_synthesis_method": unitary_synthesis_method,
"unitary_synthesis_plugin_config": unitary_synthesis_plugin_config,
"target": target,
}
):
transpile_args = {
Expand All @@ -598,6 +632,7 @@ def _parse_transpile_args(
seed_transpiler=kwargs["seed_transpiler"],
unitary_synthesis_method=kwargs["unitary_synthesis_method"],
unitary_synthesis_plugin_config=kwargs["unitary_synthesis_plugin_config"],
target=kwargs["target"],
),
"optimization_level": kwargs["optimization_level"],
"output_name": kwargs["output_name"],
Expand Down Expand Up @@ -726,38 +761,122 @@ def _parse_coupling_map(coupling_map, backend, num_circuits):
return coupling_map


def _target_to_backend_properties(target: Target):
properties_dict = {
"backend_name": "",
"backend_version": "",
"last_update_date": None,
"general": [],
}
gates = []
qubits = []
for gate, qargs_list in target.items():
if gate != "measure":
for qargs, props in qargs_list.items():
property_list = []
if props is not None:
if props.duration is not None:
property_list.append(
{
"date": datetime.datetime.utcnow(),
"name": "gate_length",
"unit": "s",
"value": props.duration,
}
)
if props.error is not None:
property_list.append(
{
"date": datetime.datetime.utcnow(),
"name": "gate_error",
"unit": "",
"value": props.error,
}
)
if property_list:
gates.append(
{
"gate": gate,
"qubits": list(qargs),
"parameters": property_list,
"name": gate + "_".join([str(x) for x in qargs]),
}
)
else:
qubit_props = {x: None for x in range(target.num_qubits)}
for qargs, props in qargs_list.items():
qubit = qargs[0]
props_list = []
if props.error is not None:
props_list.append(
{
"date": datetime.datetime.utcnow(),
"name": "readout_error",
"unit": "",
"value": props.error,
}
)
if props.duration is not None:
props_list.append(
{
"date": datetime.datetime.utcnow(),
"name": "readout_length",
"unit": "s",
"value": props.duration,
}
)
if not props_list:
qubit_props = {}
break
qubit_props[qubit] = props_list
if qubit_props and all(x is not None for x in qubit_props.values()):
qubits = [qubit_props[i] for i in range(target.num_qubits)]
if gates or qubits:
properties_dict["gates"] = gates
properties_dict["qubits"] = qubits
return BackendProperties.from_dict(properties_dict)
else:
return None


def _parse_backend_properties(backend_properties, backend, num_circuits):
# try getting backend_properties from user, else backend
if backend_properties is None:
if getattr(backend, "properties", None):
backend_properties = backend.properties()
if backend_properties and (
backend_properties.faulty_qubits() or backend_properties.faulty_gates()
):
faulty_qubits = sorted(backend_properties.faulty_qubits(), reverse=True)
faulty_edges = [gates.qubits for gates in backend_properties.faulty_gates()]
# remove faulty qubits in backend_properties.qubits
for faulty_qubit in faulty_qubits:
del backend_properties.qubits[faulty_qubit]

gates = []
for gate in backend_properties.gates:
# remove gates using faulty edges or with faulty qubits (and remap the
# gates in terms of faulty_qubits_map)
faulty_qubits_map = _create_faulty_qubits_map(backend)
if (
any(faulty_qubits_map[qubits] is not None for qubits in gate.qubits)
or gate.qubits in faulty_edges
):
continue
gate_dict = gate.to_dict()
replacement_gate = Gate.from_dict(gate_dict)
gate_dict["qubits"] = [faulty_qubits_map[qubit] for qubit in gate.qubits]
args = "_".join([str(qubit) for qubit in gate_dict["qubits"]])
gate_dict["name"] = "{}{}".format(gate_dict["gate"], args)
gates.append(replacement_gate)

backend_properties.gates = gates
backend_version = getattr(backend, "version", None)
if not isinstance(backend_version, int):
backend_version = 0
if backend_version <= 1:
if getattr(backend, "properties", None):
backend_properties = backend.properties()
if backend_properties and (
backend_properties.faulty_qubits() or backend_properties.faulty_gates()
):
faulty_qubits = sorted(backend_properties.faulty_qubits(), reverse=True)
faulty_edges = [gates.qubits for gates in backend_properties.faulty_gates()]
# remove faulty qubits in backend_properties.qubits
for faulty_qubit in faulty_qubits:
del backend_properties.qubits[faulty_qubit]

gates = []
for gate in backend_properties.gates:
# remove gates using faulty edges or with faulty qubits (and remap the
# gates in terms of faulty_qubits_map)
faulty_qubits_map = _create_faulty_qubits_map(backend)
if (
any(faulty_qubits_map[qubits] is not None for qubits in gate.qubits)
or gate.qubits in faulty_edges
):
continue
gate_dict = gate.to_dict()
replacement_gate = Gate.from_dict(gate_dict)
gate_dict["qubits"] = [faulty_qubits_map[qubit] for qubit in gate.qubits]
args = "_".join([str(qubit) for qubit in gate_dict["qubits"]])
gate_dict["name"] = "{}{}".format(gate_dict["gate"], args)
gates.append(replacement_gate)

backend_properties.gates = gates
else:
backend_properties = _target_to_backend_properties(backend.target)
if not isinstance(backend_properties, list):
backend_properties = [backend_properties] * num_circuits
return backend_properties
Expand Down Expand Up @@ -898,6 +1017,15 @@ def _parse_unitary_plugin_config(unitary_synthesis_plugin_config, num_circuits):
return unitary_synthesis_plugin_config


def _parse_target(backend, target, num_circuits):
backend_target = getattr(backend, "target", None)
if target is None:
target = backend_target
if not isinstance(target, list):
target = [target] * num_circuits
return target


def _parse_seed_transpiler(seed_transpiler, num_circuits):
if not isinstance(seed_transpiler, list):
seed_transpiler = [seed_transpiler] * num_circuits
Expand Down
25 changes: 3 additions & 22 deletions qiskit/providers/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

from abc import ABC
from abc import abstractmethod
from collections import defaultdict
import datetime
import logging
from typing import List, Union, Iterable, Tuple
Expand Down Expand Up @@ -334,7 +333,6 @@ def __init__(
if field not in self._options.data:
raise AttributeError("Options field %s is not valid for this backend" % field)
self._options.update_config(**fields)
self._basis_gates_all = None
self.name = name
self.description = description
self.online_date = online_date
Expand All @@ -350,29 +348,12 @@ def operations(self) -> List[Instruction]:
"""A list of :class:`~qiskit.circuit.Instruction` instances that the backend supports."""
return list(self.target.operations)

def _compute_non_global_basis(self):
incomplete_basis_gates = []
size_dict = defaultdict(int)
size_dict[1] = self.target.num_qubits
for qarg in self.target.qargs:
if len(qarg) == 1:
continue
size_dict[len(qarg)] += 1
for inst, qargs in self.target.items():
qarg_sample = next(iter(qargs))
if qarg_sample is None:
continue
if len(qargs) != size_dict[len(qarg_sample)]:
incomplete_basis_gates.append(inst)
self._basis_gates_all = incomplete_basis_gates

@property
def operation_names(self) -> List[str]:
"""A list of instruction names that the backend supports."""
if self._basis_gates_all is None:
self._compute_non_global_basis()
if self._basis_gates_all:
invalid_str = ",".join(self._basis_gates_all)
non_global_ops = self.target.get_non_global_operation_names(strict_direction=True)
if non_global_ops:
invalid_str = ",".join(non_global_ops)
msg = (
f"This backend's operations: {invalid_str} only apply to a subset of "
"qubits. Using this property to get 'basis_gates' for the "
Expand Down
3 changes: 2 additions & 1 deletion qiskit/test/mock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
from .fake_provider import FakeProvider, FakeLegacyProvider
from .fake_provider import FakeProviderFactory
from .fake_backend import FakeBackend, FakeLegacyBackend
from .fake_backend_v2 import FakeBackendV2
from .fake_backend_v2 import FakeBackendV2, FakeBackend5QV2
from .fake_mumbai_v2 import FakeMumbaiV2
from .fake_job import FakeJob, FakeLegacyJob
from .fake_qobj import FakeQobj

Expand Down
Loading

0 comments on commit 1a9f938

Please sign in to comment.