qiboteam / qibo

A framework for quantum computing
https://qibo.science
Apache License 2.0
275 stars 55 forks source link

Transpiler handling across backends #1309

Open alecandido opened 2 months ago

alecandido commented 2 months ago

The main problem is the following:

Circuits often have to be transpiled for hardware execution, but they often should not be transpiled for simulation.

Stated this way, it is clear that it is mainly concerning defaults, and thus UI.

This combines with the decision about what to do in the case in which the circuit spans a subset of the platform, and it is handled very different on hardware and simulation.

On hardware, the platform is fixed, and connectivity is a subset of possible edges In simulation, the circuit itself is establishing the number of qubits of the platform, and full connectivity is assumed.

This differentiates once more the two scenarios, requiring a different padding strategy (no padding in simulation, to avoid wasting resources, suitable padding on hardware).

Another relevant observation is that, even on hardware, two different transpilations could be interesting:

  1. transpile the padded circuit, making use of all connections
  2. just transpile the circuit, with the connections among involved qubits, and then pad

(note that 2. is always less efficient, and sometimes impossible, even when 1. is possible - but it could allow running multiple circuits in parallel on the same hardware)

It is clear that having multiple building blocks would allow composing them in the preferred way, but defaults are required

The goal is to find a unified strategy, to handle transpilation uniformly across different platforms, including user-friendly defaults, but decoupling the individual backends from the transpilation (and keeping all modules as independent as possible in general).

Proposal

Let's assume the qibo-core scenario, in which the execution backend is a completely decoupled entity, possibly not relying on qibo itself. Thus, transpilation can not be accessed by the backend (neither simulation nor hardware nor anything), but it can be requested.

The manual solution is:

circuit = Circuit(...)
backend = Backend(...)

# transform
transpiler = Transpiler(...)

circuit_padded0 = Circuit(n0).extend(circuit, map=...)
transpiled = transpiler.transpile(circuit_padded0, backend.connectivity)
circuit_padded1 = Circuit(n1).extend(transpiled, map=...)

# execute
backend.execute(circuit_padded1)

If transpilation is not wanted, commenting it would be sufficient. Same for padding (only one of the padding at a time is meaningful, but I added the two options to show it can happen before or after transpilation).

However, in both cases some user input is required, since the transpiler parameters (or instance) and the mapping and position of padding have to be declared. Let's distinguish two different kinds of automated users (since the manual one is already catered for).

Advanced intermediate user (e.g. Qibocal)

In the Qibocal case, you might have no control on the specific circuit or backend, since they could be more or less defined by the user (e.g. controlling the platform or the number of qubits). So, it should be possible to query the objects to understand whether and what transformation are required.

To make transpilation conditional to the backend, but without manually specifying which backends, we could define some default backend.defaults.transpilation: bool and just add the branching. A similar strategy could be adopted for the padding, with backend.defaults.padding: bool or a three-valued option: BEFORE, AFTER, or NONE.

The other parameters could be decided or inferred by Qibocal:

So, something like:

def ___(executor, circuit, backend):
    transpiler = executor.transpiler
    if backend.defaults.transpiler and transpiler is None:
        transpiler = Transpiler(...)

    if backend.defaults.padding is Padding.BEFORE:
        circuit = Circuit(n0).extend(circuit, map=executor.qubits)
    if transpiler is not None:
        circuit = transpiler.transpile(circuit, backend.connectivity)
    if backend.defaults.padding is Padding.BEFORE:
        circuit = Circuit(n1).extend(transpiled, map=executor.qubits)

    backend.execute(circuit)

Fully automated end user

In this case, the user should just define the circuit and pick a backend, but everything else should be managed internally (though partially overwritable).

Thus, we need an execute function:

execute(circuit, on=backend)

that will take care of everything.

This execute() function may look similar to the one above for Qibolab, but with no executor, thus a default transpiler should be defined for it, and a default mapping for padding (that might also depend on the backend, that could select some optimal qubits for restricted execution, but missing that info, it will always default to the first n).

In the present Qibo, circuits are executed with circuit(), i.e. the .__call__() method, without even specifying a backend. This mechanism rely on the GlobalBackend, thus all the features described for the execute() function could be exposed to it as well.

In practice, only one between execute() and GlobalBackend is required to be implemented, the other one just calling it passing suitable options. And, at the moment, I would just use the GlobalBackend, since it's the Qibo-way, adding a set_transpiler() and things like that (though I'd reconsider this strategy, to limit the global state).

alecandido commented 2 months ago

@andrea-pasquale @Edoardo-Pedicillo @stavros11 @hay-k