-
Notifications
You must be signed in to change notification settings - Fork 61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Transpiler handling across backends #1309
Comments
@csookim it seems I had already written the issue before (in April), and forgot it ^^ There is written everything I told you, even though is slightly more general. Please, ignore the Qibocal part (instead, you will have to face the padding anyhow, so feel free to comment the proposal above). Rereading it, I believe I could have been a bit more careful in the interface part. execute(circuit, on=backend, transpiler=transpiler) i.e. there should be nothing implicit in this function, and though we can have an extremely basic default transpiler, i.e. the identity, something like an instance of: class Nop(Transpiler):
def __call__(self, c: Circuit) -> Circuit:
return c despite it causing failures for non-trivial platforms. While we can't afford a similar trivial backend, since execution can't be trivial. Instead, some more involved defaults can be set in the qibo/src/qibo/backends/__init__.py Lines 69 to 75 in 540d2b6
and replace ._instance with the ._backend and ._transpiler attributes.
|
@alecandido Thanks for your clarification. I’ve read your proposal and would like to write a short proposal based on my idea as well. Please feel free to share any comments. AssumptionSuppose we have a Currently, we need to define the custom_pipeline = Passes([Placer, ... , Router, ... , Unroller])
transpiled_circuit, _ = custom_pipeline(circuit) The custom_pipeline = Passes([Placer, ... , Router, ... , Unroller])
tp = Transpiler()
# Load platform data into the transpiler and set the connectivity and fidelity information
tp.load_platform_config(platform)
# Set connectivity for transpilation
tp.set_connectivity(connectivity)
# Set passes
tp.set_passes(custom_pipeline)
...
# Transpilation
tp.transpile(circuit) Implementation of Your ProposalBased on your proposal, we can add the variable qibo/src/qibo/backends/abstract.py Line 6 in 3d87ba7
class Backend(abc.ABC):
def __init__(self):
super().__init__()
self.name = "backend"
self.platform = None
self.transpiler = None # transpiler And we can pass the transpiler using qibo/src/qibo/backends/__init__.py Line 85 in 3d87ba7
cls._instance = construct_backend(backend, platform=platform, transpiler=transpiler) However, this approach requires different implementations of transpilation for simulation backends and hardware backends. We would need to modify the Proposal
1. Circuit ExecutionCircuit execution is done using: execute(circuit, on=backend, transpiler=transpiler) 2. Transpilation in
|
def execute(self, initial_state=None, nshots=1000): |
def execute(self, initial_state=None, nshots=1000, transpiler=None):
...
from qibo.backends import GlobalBackend
### modified
gbackend = GlobalBackend()
if gbackend.sim == True:
if transpiler != None: # If a custom transpiler is defined
transpiler.transpile(self)
else:
if transpiler != None: # If a custom transpiler is defined
transpiler.transpile(self)
else: # If a custom transpiler is not defined
default_transpiler = Transpiler()
# Default transpiler using hardware configuration (connectivity, native gates, etc.)
default_transpiler.load_platform_config(gbackend.platform)
default_transpiler.transpile(self)
###
if self.accelerators: # pragma: no cover
return gbackend.execute_distributed_circuit(
self, initial_state, nshots
)
else:
return gbackend.execute_circuit(self, initial_state, nshots)
3. Simulation Flag in Backend
To enable the transpiling logic, a simulation flag is added to the Backend
class. This flag is set to True
for simulation backends (numpy, qibojit, etc.).
class Backend(abc.ABC):
def __init__(self):
super().__init__()
self.name = "backend"
self.platform = None
self.sim = None # flag
Sorry for the delay in the reply.
The
Not the qibo/src/qibo/backends/__init__.py Line 64 in 3d87ba7
And this is the exact reason to use the
This I agree with. But it's exactly what I wrote above :)
I would avoid a simulation flag, and instead expose a Notice that the properties don't have to be necessarily properties, they could be implemented even as functions to check belonging, e.g. class Backend:
def __contains__(self, element: Union[QubitId, QubitPairId, Gate]) -> bool:
... and the implementation for simulation, without anything specified, should be just Even non-trivial transpilers should be trivial when applied to full-connectivity all-native (unless they are tuned for a specific set of natives, in which case it is just wrong to use them in simulation, unless you really want to restrict to those natives). |
@alecandido Thank you for your reply. I’d like to confirm my understanding. Please let me know if I’ve misunderstood any part of your approach.
The pseudocode for # execute the circuit in main
c.execute(initial_state=None, nshots=1000, transpiler=None)
class Circuit:
...
def execute(self, initial_state=None, nshots=1000, transpiler=None):
...
return GlobalBackend().execute_circuit(self, initial_state, nshots, transpiler)
class QibolabBackend(GlobalBackend):
...
def execute_circuit(self, circuit, initial_state=None, nshots=1000, transpiler):
transpiled_circuit = super().transpile(circuit, transpiler, self.platform)
...
class GlobalBackend(Backend):
...
def transpile(self, circuit, transpiler: Passes, platform: Platform):
connectivity = platform. ...
qubits = platform. ...
native_gates = platform. ...
if the transpiler settings do not match the platform:
raise error
if transpiler is None:
transpiler_pass = ... # default transpiler
transpiled_circuit, _ = transpiler_pass(circuit)
return transpiled_circuit
Here are some topics that might be discussed in the future:
|
I believe there is a misunderstanding about what You can see it's main role here: qibo/src/qibo/models/circuit.py Lines 1082 to 1109 in 37e1010
(forget about self.compile , just look at the else branch).
The point is that in: from qibo import Circuit
c = Circuit(1)
c.add(...)
res = c() the last instruction GlobalBackend().execute_circuit(self, initial_state, nshots) above (it could end up in the accelerator branch, or you may pass through compilation - but that doesn't change the substance, so just forget). Let's call the class from now on just class Global:
backend: Backend
transpiler: Transpiler
@classmethod
def execute(cls, circuit: Circuit):
# the following is the function discussed above: it can be implented as
# a standalone function and used here (which I personally like) or even
# be implemented here inline
return execute(circuit, on=cls.backend, transpiler=cls.transpiler)
@classmethod
def set_backend(cls, backend: str):
cls.backend = construct_backend(backend)
@classmethod
def set_transpiler(cls, transpiler: Transpiler):
cls.transpiler = transpiler
class Circuit:
def execute(self):
return Global.execute(self) I simplified some details (no initial state, no shots, no backend platform, no
It is fine not to address the full problem immediately. But I'd suggest to just crash for unsupported situations, in whatever way is happening. Then, we can plan more advanced features, e.g. registering transpilers by name (as we're doing with the backends), or associating different default transpilers per-backend (such that the |
Notice that currently However, the only two places where it's used are the one above and: qibo/src/qibo/models/error_mitigation.py Line 1067 in 37e1010
and in both cases is used for the circuit execution (as it should). https://github.com/search?q=repo%3Aqiboteam%2Fqibo+GlobalBackend+path%3A%2F%5Esrc%5C%2Fqibo%5C%2F%2F&type=code So, we can abandon the In any case, the global backend will still be accessible as class Global:
_backend: Backend
@classmethod
def backend(cls):
if cls._backend is not None:
return cls._backend
backend = os.environ.get("QIBO_BACKEND")
if backend: # pragma: no cover
# Create backend specified by user
platform = os.environ.get("QIBO_PLATFORM")
cls._backend = construct_backend(backend, platform=platform)
else:
# Create backend according to default order
for kwargs in cls._default_order:
try:
cls._backend = construct_backend(**kwargs)
break
except (ModuleNotFoundError, ImportError):
pass
if cls._backend is None:
raise RuntimeError(...)
return cls._backend and access it as (Unfortunately the option of having a classmethod property has been added in py3.9, and removed again in py3.11 - so we have to choose between |
@alecandido Thank you for your response. I’d like to confirm my understanding again:
class
|
Yes, exactly, now we are on the same page! I believe we can discuss further about the interface. Currently, there are "aliases" functions that the user is supposed to use, instead of accessing them through def set_backend(backend, **kwargs):
Global.set_backend(backend, **kwargs) Moreover, I have nothing against Concerning |
I renamed class _Global:
_backend: Backend = None
_transpiler: Passes = None
_dtypes = ...
_default_order = ...
@classmethod
def backend(cls) -> Backend: # backend is not provided
...
@classmethod
def transpiler(cls) -> Passes: # transpiler is not provided
...
@classmethod
def set_backend(cls, backend, **kwargs):
...
@classmethod
def set_transpiler(cls, transpiler: Passes):
cls._transpiler = transpiler
@classmethod
def get_transpiler(cls):
return cls._transpiler
@classmethod
def resolve_global(cls):
if cls._backend is None:
cls._backend = cls.backend()
if cls._transpiler is None:
cls._transpiler = cls.transpiler() # in circuit.py
class Circuit:
...
def execute(self, initial_state=None, nshots=1000):
...
# resolve the backend and transpiler
_Global.resolve_global()
transpiled_circuit, _ = _Global.get_transpiler()(self)
return _Global._backend.execute_circuit(transpiled_circuit, initial_state=None, nshots=1000) |
To double check, the reason we're trying to set However, for an advanced transpiler, we might also consider additional information such as coherence time, qubit fidelity, SWAP fidelity, etc. Could we add these as variables under
Could you provide some refs on how other backends store their hw configurations? |
I would have done differently, but with the same result. The user interface is just the
I'm not sure what you're referring to, since we never mentioned these attributes before, nor they are in the current
We can, if needed. But, as above, I would add them to the
Simulation has no hardware configuration. [*]: this is essentially what |
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, with
backend.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 the
GlobalBackend
, 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).The text was updated successfully, but these errors were encountered: