-
Notifications
You must be signed in to change notification settings - Fork 52
Refactoring of the Qrisp Backend Class #331
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
base: main
Are you sure you want to change the base?
Refactoring of the Qrisp Backend Class #331
Conversation
|
It is important to mention here that this is specifically not intended to fit "only" IQM Backends but we are dedicated to make this as vendor agnostic as possible. Any input or features request from within or outside of the Qrisp development community is welcome. |
Aerylia
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary of changes proposed/discussed during the meeting today
src/qrisp/interface/backend.py
Outdated
| # ---------------------------------------------------------------------- | ||
|
|
||
| @abstractmethod | ||
| def run(self, *args, **kwargs) -> Any: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be more precise so that users can call the run method with similar signature on different Backend child classes. e.g. circuit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. So far I simply specified one more positional argument for the qrisp quantum circuit to execute, but we can revise this if needed (it is also related to the batched backend discussion)
| @classmethod | ||
| def _default_options(cls) -> Mapping: | ||
| """ | ||
| Default runtime options for the backend. | ||
| Child classes may override this method to provide custom default options, | ||
| or the defaults may be overridden entirely by passing an ``options`` | ||
| mapping to the constructor. | ||
| """ | ||
| return {"shots": 1024} | ||
|
|
||
| @property | ||
| def options(self): | ||
| """Current runtime options.""" | ||
| return self._options | ||
|
|
||
| def update_options(self, **kwargs) -> None: | ||
| """Update existing runtime options, rejecting unknown keys.""" | ||
|
|
||
| for key, val in kwargs.items(): | ||
| if key not in self._options: | ||
| raise AttributeError( | ||
| f"'{key}' is not a valid backend option for {self.__class__.__name__}. " | ||
| f"Valid options: {list(self._options.keys())}" | ||
| ) | ||
| self._options[key] = val | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This options construct causes ambiguities with the kwargs in Backend.run. It is probably better to not use them in favour of specifying the run options and their defaults in the overloaded run(self, ..., shots=1024) method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for putting in these comments! This point might have slipped me during the discussion. I don't think allowing kwargs in .run is the preferable architecture because several Qrisp implementations call the .run without the user getting the chance to interfere. This is for instance the case for many algorithms like QAOA or VQE. In other words the .run call is part of the algorithm implementation and can not be modified by the user. If the user wants to try out a different set of options, they can not modify with this architecture. Having the options as a backend attribute therefore seems the more attractive approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yeah, if that is how the architecture works, then options would be best, but the kwargs should then not be used for user level options
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But that would also mean that we would want a strictly fixed function architecture for run
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the comments. I would keep these runtime options in place for the moment.
In case of overlap with run-time options provided to run (as in your example run(self, ..., shots=1024)), we probably just need to clarify what is the priority. In this case, it seems to me that the options provided to the run function directly (if present) should have the higher priority.
| self._options[key] = val | ||
|
|
||
| # ---------------------------------------------------------------------- | ||
| # Optional hardware/backend-specific metadata |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These properties should at least be typed and they can only return None if that is meaningful. i.e. what does it mean for a backend to have backend.num_qubits is None ? And how would the transpiler deal with that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(see answer below regarding hardware metadata)
src/qrisp/interface/backend.py
Outdated
| return None | ||
|
|
||
| @property | ||
| def error_rates(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are calibration dependent, we probably want the backend to somehow track which calibrated state of the hardware it is refering to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(see answer below regarding hardware metadata)
|
|
||
|
|
||
| class BatchedBackend(VirtualBackend): | ||
| class BatchedBackend(Backend): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably want to merge this with the regular backend with a run_batched that by default sequentially calls the run method and can be overloaded by a child instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now, this BatchedBackend class inherits from Backend, but takes care of:
- Enqueuing the backend calls from multiple threads
- Synchronizing multiple threads
- Dispatching the batch to the backend
Therefore, it acts primarily as a scheduler / execution coordinator, not as a real backend in the strict sense (i.e. an object that defines execution semantics for a single circuit).
I don't think this should inherit from Backend (this was the quickest solution to make tests passing since it was inheriting from VirtualBackend before, which now is deprecated), but I am also not convinced about putting everything into a unique Backend class.
If we do that, then we need to take care of execution semantics, batching strategy, scheduling and sync. in the same class, which might be a little too messy.
I think this is something we should revisit once the design of the Backend class is fully finalized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed! We should also gather feedback on how necessary the batching feature is (both from benchmarks and stakeholder interaction). According to Joni (SWE at IQM) the overhead from executing non-batched got significantly reduced.
src/qrisp/interface/backend.py
Outdated
| return None | ||
|
|
||
| @property | ||
| def connectivity(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this connectivity graph be disconnected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One single connected component, up to vendor to decide which one. e.g. largest of best qubits
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(see answer below regarding hardware metadata)
src/qrisp/interface/backend.py
Outdated
| return None | ||
|
|
||
| @property | ||
| def gate_set(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How to deal with overlapping gate sets in the connectivity. e.g. some pairs with CZ, some pairs with iSWAP, some pairs with both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(see answer below regarding hardware metadata)
src/qrisp/interface/backend.py
Outdated
|
|
||
| @property | ||
| def gate_set(self): | ||
| """Set of gates supported by the backend.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the assumptions of the universality of the gate set or the qubits? Are qubits assumed to have a measurement implemented? - Document these assumptions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(see answer below regarding hardware metadata)
src/qrisp/interface/backend.py
Outdated
| # ---------------------------------------------------------------------- | ||
|
|
||
| @abstractmethod | ||
| def run(self, *args, **kwargs) -> Any: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If transpilation is done as part of the run, we will need run(..., transpile_method=Somefunc/object) and we will also want a backend.transpile() that is used in the run() so that users can inspect the transpile result before running the circuit on the hardware.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for bringing this up. Conceptually, I would like to keep transpilation and execution as distinct phases. That is, keep them separable and inspectable. At the same time, I agree that for usability reasons run() may invoke transpilation as a convenience. If that is the case, the run method can be overridden by specifying this keyword argument.
A possible design is to expose a backend.transpile() in this class.
For example, in the PlasmaSabre package we indeed have a transpile_to_iqm function, and I don't see why we cannot implement it in the IQM backend as concrete implementation of the transpile method. But I do not have a lot of experience with transpilation processes for different backends.
What do you think @positr0nium ?
|
I reply here regarding the observations about the hardware metadata (gate_set, connectivity, etc.). Thanks for the very good observations! I agree that raw properties such as I think the intent of the base In practice, I would like vendor backends to reuse data structures already provided by the vendor whenever possible. For example, in the case of IQM, we have an In this model, I think I agree that, in this context, the |
Context: The current backend implementation in Qrisp works as an extremely abstract "one-fits-all" tool. Essentially, the only existing feature is to send quantum circuits and receive results.
Furthermore, it is embedded into a network interface that was part of a requirement of the
SeQuenCproject during the early stages of Qrisp. This network interface eventually never used and should now be deprecated.Description of the Change:
We introduce a custom
QrispDeprecationWarningin a new module.We use the new
QrispDeprecationWarningto deprecate the currentBackendClient,BackendServer, andVirtualBackendclasses (as well as a few other functionalities that were already marked as deprecated).We introduce a new
Backendclass in a new module (the latter is described in the associated docstring).We modify all the existing backends so that they inherit from
Backendrather than the deprecated backends. These areDefaultBackend,BatchedBackend, andQiskitBackend. A few backends are excluded from this:QiskitRuntimeBackend(since I do not have an IBM token to test it) and all the docker backends.We introduce a new
qiskit-ibm-runtimedependency to test a specific backend forQiskitBackend(this test was commented out), and we replaceqiskit-iqmwithiqm-client[qiskit](as the former was causing the following error:RuntimeError: The qiskit-iqm package is obsolete (...))Finally, the changes implemented in this PR are also part of the associated PR in the
plasma_sabrerepository on GitLab (I cannot link it here as it is part of IQM Finland).Benefits: Much better structure and deprecated Qunicorn servers.
Possible Drawbacks: We cannot exclude at 100% edge-case incompatibilities with the backends we could not test because of missing tokens and api accesses (especially considering that several tests were missing and/or commented out).
Related GitHub Issues: None