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:
transpile the padded circuit, making use of all connections
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.
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:
the mapping is inferred by the qubits specified in the executor (or, at a higher level, in the runcard)
which transpiler could be decided from the platform connectivity, and local routine choices
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).
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:
(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 onqibo
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:
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
orbackend
, 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, withbackend.defaults.padding: bool
or a three-valued option:BEFORE
,AFTER
, orNONE
.The other parameters could be decided or inferred by Qibocal:
So, something like:
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:
that will take care of everything.
This
execute()
function may look similar to the one above for Qibolab, but with noexecutor
, 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 theGlobalBackend
, thus all the features described for theexecute()
function could be exposed to it as well.In practice, only one between
execute()
andGlobalBackend
is required to be implemented, the other one just calling it passing suitable options. And, at the moment, I would just use theGlobalBackend
, since it's the Qibo-way, adding aset_transpiler()
and things like that (though I'd reconsider this strategy, to limit the global state).