Skip to content

Refactoring of the Qrisp Backend structure#331

Draft
PietropaoloFrisoni wants to merge 31 commits intoeclipse-qrisp:mainfrom
PietropaoloFrisoni:NewBackendClass
Draft

Refactoring of the Qrisp Backend structure#331
PietropaoloFrisoni wants to merge 31 commits intoeclipse-qrisp:mainfrom
PietropaoloFrisoni:NewBackendClass

Conversation

@PietropaoloFrisoni
Copy link
Contributor

@PietropaoloFrisoni PietropaoloFrisoni commented Nov 30, 2025

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 SeQuenC project 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 QrispDeprecationWarning in a new module.

  • We use the new QrispDeprecationWarning to deprecate the current BackendClient, BackendServer, and VirtualBackend classes (as well as a few other functionalities that were already marked as deprecated).

  • We introduce a new Backend class in a new module (the latter is described in the associated docstring).

  • We modify all the existing backends so that they inherit from Backend rather than the deprecated backends. These are DefaultBackend, BatchedBackend, and QiskitBackend. 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-runtime dependency to test a specific backend for QiskitBackend (this test was commented out), and we replace qiskit-iqm with iqm-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_sabre repository 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

@positr0nium
Copy link
Contributor

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.

Copy link

@Aerylia Aerylia left a 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

self._options[key] = val

# ----------------------------------------------------------------------
# Optional hardware/backend-specific metadata
Copy link

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?

Copy link
Contributor Author

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)

return None

@property
def error_rates(self):
Copy link

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.

Copy link
Contributor Author

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):
Copy link

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

return None

@property
def gate_set(self):
Copy link

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.

Copy link
Contributor Author

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)


@property
def gate_set(self):
"""Set of gates supported by the backend."""
Copy link

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

Copy link
Contributor Author

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)

# ----------------------------------------------------------------------

@abstractmethod
def run(self, *args, **kwargs) -> Any:
Copy link

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.

Copy link
Contributor Author

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 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backend might also solve this issue (that is, inspect the final circuits that would be submitted for execution before actually submitting them, which can be useful for debugging purposes) with another method such as create_run_request (as IQMBackend currently does). I would not add this method to the base class though

@PietropaoloFrisoni
Copy link
Contributor Author

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 num_qubits, gate_set, etc. are insufficient to fully capture real hardware characteristics.

I think the intent of the base Backend class should be to define only the 'execution contract', while keeping hardware description optional and flexible. It seems to me that hardware-specific information is most of the time vendor-dependent, and different vendors already expose this information through their own typed capability objects, all of which may be absent or irrelevant for simulators. The (abstract) base Backend therefore needs to accommodate all these scenarios without enforcing a single universal structure.

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 IQMBackend inheriting from Backend (currently in the PlasmaSabre package on GitLab), which seems the appropriate place to introduce hardware-specific typing and semantics suitable for IQM. In that context, we can keep using the existing IQM data structures for calibration sets, connectivity, etc., while avoiding typing those properties too narrowly in the base class.

In this model, I think None for hardware metadata should be interpreted as “capability not exposed”. The transpiler can then either require specific capabilities (and raise if they are missing) or ignore them, depending on the mode. For simulators, these can simply be None.
Connectivity selection, overlapping gate availability, calibration identity, and similar concerns can therefore be handled by backend-specific implementations or higher-level policies, keeping the core Backend interface fully hardware-agnostic.

I agree that, in this context, the Backend class might seem too generic, but at the moment I'm not sure how to combine this extreme flexibility with a more rigid structure. Obviously, all of this will need to be explained in the documentation :)

@PietropaoloFrisoni PietropaoloFrisoni changed the title Refactoring of the Qrisp Backend Class Refactoring of the Qrisp Backend structure Dec 17, 2025
Copy link

@Aerylia Aerylia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments from todays meeting

Comment on lines +26 to +29
def run(self, circuit, **kwargs):
shots = kwargs.get("shots", self.options.get("shots", None))
token = kwargs.get("token", self.options.get("token", ""))
return default_run(circuit, shots, token)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def run(self, circuit, **kwargs):
shots = kwargs.get("shots", self.options.get("shots", None))
token = kwargs.get("token", self.options.get("token", ""))
return default_run(circuit, shots, token)
def run(self, circuit, *, shots) -> Job:
token = self._default_options[token] # This should probably get the options from the specific instance
return default_run(circuit, shots, token)
def run_await_results(self, circuit, *, shots, timeout=1000) -> dict[str, int]:
return run(self, circuit, shots=shots).results(timeout=timeout)

Job here should be an abstract class that you can query for results so that it can be run asynchronously

@bastibock
Copy link
Contributor

First of all: thank you everyone for your efforts so far in refactoring the backend class.
I followed your discussions here and also think that this is absolutely necessary!

However, as indicated in your comments, there are some open points, that could and should be discussed together, to shape the backend class, and also the compilation pipeline (where does what step of compilation/transpilation take place), in an optimal way.

Maybe we could start a discussion together about this topic in the beginning of next year.

@PietropaoloFrisoni
Copy link
Contributor Author

Hi @bastibock, nice to meet you!
Sure, the plan was to keep you guys in the loop and discuss with you very soon : )

Right now, we are getting very useful feedback from IQM's devs and Product Team, who have a lot of experience with real physical quantum backends and therefore can provide good suggestions for such a class.

Happy to talk to you next year!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants