Skip to content

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

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

@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.

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

Copy link
Contributor Author

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)

Comment on lines 125 to 151
@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

Copy link

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.

Copy link
Contributor

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.

Copy link

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

Copy link

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

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

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?

Copy link

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

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 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 ?

@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 :)

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.

3 participants