Qiskit / qiskit

Qiskit is an open-source SDK for working with quantum computers at the level of extended quantum circuits, operators, and primitives.
https://www.ibm.com/quantum/qiskit
Apache License 2.0
5.23k stars 2.36k forks source link

Transpiler ignores "basis_gates" if "target" is present #11405

Open fvoichick opened 10 months ago

fvoichick commented 10 months ago

Environment

What is happening?

A BasisTranslator does not translate a gate if its target includes the gate. Thus, the target_basis is essentially ignored, and the resulting circuit includes gates that are outside of the supplied basis_gates.

How can we reproduce the issue?

Run this code:

from qiskit import QuantumCircuit, transpile
from qiskit.providers.fake_provider import FakeCairoV2

circuit = QuantumCircuit(1)
circuit.x(0)

result = transpile(circuit, target=FakeCairoV2().target, basis_gates=["rz", "sx", "cx"])
print(*result.count_ops().items())

Output: ('x', 1)

What should happen?

I expect it to output ('sx', 2) or something similar, as it does when I omit the target argument. I do not expect it to silently ignore the basis_gates argument.

Any suggestions?

Fix BasisTranslator.run, particularly line 157. If you intend to keep the current behavior, then you should warn users who provide both the target and basis_gates (or target_basis) that the presence of the former causes the transpiler to ignore the latter.

jakelishman commented 10 months ago

We can definitely improve the documentation and warning here, though the current output is as expected / designed.

In order to achieve what you want in the short term, you're likely going to want to create a custom Target based on the existing one, overriding the heterogeneous ISA with whatever other data you want. I suspect (but am not sure) that what you intended to happen could be achieved with:

from qiskit import QuantumCircuit, transpile
from qiskit.providers.fake_provider import FakeCairoV2
from qiskit.transpiler import Target
from qiskit.circuit import Measure
from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping

def make_custom_target(base: Target, basis_gates: list[str]):
    name_map = get_standard_gate_name_mapping()
    custom_target = Target()
    # This gets a coupling map from the union of all 2q links, regardless of
    # active gate and directionality.
    base_all_2q_coupling = base.build_coupling_map()
    base_all_2q_coupling.make_symmetric()
    for gate_name in basis_gates:
        gate = name_map[gate_name]
        if gate.num_qubits == 1:
            # Assume that any 1q gate is available as an ideal gate on all
            # qubits. Throws away noise information.
            custom_target.add_instruction(
                gate, {(qubit,): None for qubit in range(base.num_qubits)}
            )
        elif gate.num_qubits == 2:
            # Assume that any 2q gate is availble bidirectionally on all
            # pairs of qubits that have _any_ 2q gate defined on them in
            # the base.  Throws away noise information.
            custom_target.add_instruction(
                gate, {qargs: None for qargs in base_all_2q_coupling}
            )
        else:
            raise ValueError("don't know what to do with this")
    if "measure" not in basis_gates and "measure" in base:
        # Assume that the same individual qubits are measurable.
        custom_target.add_instruction(
            Measure(),
            {qargs: None for qargs in base.qargs_for_operation_name("measure")},
        )
    return custom_target

circuit = QuantumCircuit(1)
circuit.x(0)

result = transpile(
    circuit, target=make_custom_target(FakeCairoV2().target, ["rz", "sx", "cx"])
)
print(*result.count_ops().items())

where hopefully I made clear several of the places where we'd have to make questionable assumptions when trying to override a Target with something like basis_gates, and places where you might want to tweak exactly what's created.


To explain a bit more:

Passing contradictory data to transpile like this is generally going to require us to ignore at least one of the two items. Before target existed, we only had backend, which was at the time a BackendV1 instance, and most of the rest of the arguments to transpile are direct fields on BackendV1. This meant that there was an obvious meaning to supplying both a backend and the other options; you were overriding a single field of the backend.

With Target, the same logic is much trickier, because Target represents far more complex and heterogeneous data than the other individual arguments can. Most of the compiler passes now draw all their data from Target, and only fall back to the other forms if no Target was given. For example, a Target doesn't have a split coupling_map and basis_gates, it stores information on which gates are available on which qubit arguments along with the associated errors, which is more general. In this sense, setting target=..., basis_gates=... in a call to transpile ends up with knock-on effects if we prioritise basis_gates; we now also do not know the effective coupling map, because that's not supplied.

As a concrete example, consider a Target that has:

Now suppose we're asked to transpile to this target, but overridden so that basis_gates=["rz", "sx", "cx"]. What should we assume that the "coupling" of the effective target is? There's lots of potential choices here, and it's not at all clear what the meaning should be - we could:

and in most of those, we also have the question: for any implicit links, what should we assume about the available directionality? Should ecr on (3, 4) imply that cx would be active in the (3, 4) direction, or maybe the (4, 3) direction is also valid?

The point I'm trying to make clear is that it's not really meaningful to override single BackendV1 fields on a Target because of the heterogenous ISA support (which current hardware vendors are starting to have more and more of); it has huge knock-on implications in general. This is why we're suggesting creating your own Target in these situations.

alexanderivrii commented 10 months ago

Note that if the goal is to synthesize a quantum circuit with FakeCairoV2's coupling map and with custom basis gates, then (exactly as @jakelishman explained) one can simply write

result = transpile(circuit, coupling_map=FakeCairoV2().target.build_coupling_map(), basis_gates=["rz", "sx", "cx"])

Additionally, we have the method Target.from_configuration() that can build a Target object given coupling_map, basis_gates, etc..