# Backends and targets

> How workflow code runs circuits through QsolveBackend, QsolveSampler and QsolveEstimator on simulators, noise models and quantum computers, and what Qsolve meters.

Source: https://qsolve.omlabs.co/docs/build/backends-and-targets
Updated: 2026-11-13

Your quantum method runs circuits through `ctx`: `ctx.backend` is a standard Qiskit `BackendV2`, `ctx.sampler` a `SamplerV2` and `ctx.estimator` an `EstimatorV2`, so ordinary Qiskit code runs unchanged. Only the quantum method may use them; in `reference` and `classical_*` they raise `QuantumNotAllowed`. Every circuit that passes through is recorded and measured.

## Targets

A target is a string. These are all the forms the platform and the SDK know:

| Target | Runs on | Notes |
|---|---|---|
| `sim-cpu` | Qiskit Aer on a CPU | The default. At most 26 qubits per circuit. Needs `cpu` in the manifest's `targets.simulators`. |
| `sim-gpu` | Qiskit Aer on a GPU | At most 30 qubits per circuit. Needs `gpu` in `targets.simulators`, and `qiskit-aer-gpu` when run locally. The platform offers it only while it has GPU capacity. |
| `fake:<device>[@scale]` | Aer with a real IBM device's published noise model | For example `fake:fez` or `fake:fez@1.6`. Needs the `ibm` extra. Free locally. **Not selectable on the platform**: the run API refuses it with `target_not_supported`. It is for local runs and for `target:` in benchmark cases. |
| `ibm:<device>` | An IBM quantum computer | `<device>` is the IBM backend name, matching `^[A-Za-z0-9_.-]{1,64}$`. Needs `qpu: [ibm]` in the manifest. Locally it uses your token in `QSOLVE_IBM_TOKEN` and optionally `QSOLVE_IBM_INSTANCE`, and needs the `ibm` extra. |

Anything else is an error: locally `Unknown target 'x'. Use sim-cpu, sim-gpu, ibm:<device> or fake:<device>[@scale].`, and on the platform `target_not_supported`. Only IBM QPUs are supported today. There is no Amazon Braket target: a manifest cannot list `braket` under `qpu`, the run API answers `target_not_supported` for a `braket:` target, and the platform has no Braket price.

`targets.min_qubits_qpu` is advisory. It is accepted and stored, but the platform does not use it to accept, refuse or price a run; use it to tell researchers the smallest problem worth sending to a QPU.

### Noise-model targets

`fake:<device>` looks up the device by name among IBM's published fake backends, so `<device>` is the lowercase device name, for example `fez`, `marrakesh` or `torino`; an unknown name fails with `no fake device 'x' (e.g. fez, marrakesh, torino)`. The circuit is compiled for that device and sampled with its calibrated gate errors, readout errors and T1/T2 times. The qubit limit is the device's own qubit count.

`@scale` multiplies every error rate and divides T1 and T2 by it. It must be between 1 and 10 inclusive, or the target fails with `noise scale must be between 1 and 10`. Scaled gate errors are capped at 0.5 and readout errors at 0.75. `fake:fez@1.6` is a pessimistic rehearsal: real chips tend to be noisier than their published models, and 1.6 is a rule of thumb. A benchmark case that sets `target: fake:fez@1.6` is how you test your workflow against noise during validation; see [Benchmarks](/docs/build/benchmarks).

### What the target controls on the platform

| Stage | What decides it |
|---|---|
| Whether a run is accepted | `targets` in your [manifest](/docs/build/manifest), checked first; then whether the platform has a machine for the target. |
| Which circuits are allowed | At most `envelope.max_qubits` qubits per circuit (default 32 if a stored manifest lacks it), and at most 100,000 shots per circuit in one send. Beyond either, the executor refuses the send with `circuit exceeds the allowed qubit count` or `shots must be in 1..100000`. The shot cap is checked earlier too: a run whose `shots` parameter is above 100,000 is refused when it is created with `422 shots_too_high`, and a workflow whose `shots` parameter has a `default`, a `max` or a benchmark-case value above 100,000 is rejected at submission (before the sandbox gates) with a message naming the cap. |
| Price | The target's entry in the platform rate card: `sim-cpu`, `sim-gpu` and (for noise models) `sim-cpu`; `qpu:ibm` for `ibm:<device>`. |
| Time limit | Waiting for hardware does not count against `resources.timeout_s`; see [The manifest](/docs/build/manifest#resources). |

Your workflow code never sees credentials or provider names. On the platform it gets a backend that forwards circuits to a trusted executor, which decides the real device. Which targets a researcher may request is set by `targets` in your manifest; see [Machines and targets](/docs/use/machines-and-targets).

## Running ordinary Qiskit code

With the sampler:

```python
measured = qc.copy()
measured.measure_all()
counts = ctx.sampler.run([(measured, theta, shots)]).result()[0].data.meas.get_counts()
```

A pub is `(circuit, parameter_values, shots)`. The data field is named after the circuit's classical register: `meas` after `measure_all()`, or the name you chose, as `max-cut` does with `result.data.c.get_counts()`. With no shots in the pub and no `shots=` argument, the sampler uses 1024.

With the backend:

```python
job = ctx.backend.run([circuit_a, circuit_b], shots=2000)
counts = job.result().get_counts(0)
```

The job is finished when `run` returns. `backend.run` also defaults to 1024 shots.

With the estimator:

```python
result = ctx.estimator.run([(circuit, observable, theta)], precision=0.01).result()
```

The estimator groups the observable's Pauli terms into qubit-wise commuting groups, measures each group with one circuit and combines the results. It uses `ceil(1 / precision**2)` shots per group; the default precision of 0.015625 gives 4096. It applies no error mitigation and returns `evs` and `stds`.

Send circuits as you built them; Qsolve compiles them for the target. The gate set includes `h`, `x`, `y`, `z`, `s`, `t`, `sx`, `rx`, `ry`, `rz`, `p`, `u`, `cx`, `cy`, `cz`, `ch`, `swap`, `crx`, `cry`, `crz`, `cp`, `rxx`, `ryy`, `rzz`, `ecr` and `ccx`, among others. A sampler result always holds exactly the number of shots you asked for.

## Batching circuits

All circuits of one `sampler.run` call with the same shot count go to the target as one batch. On a quantum computer that is one job: one queue wait, one per-job overhead. A pub with an array of parameter values is one circuit per row.

```python
ctx.sampler.run([(circuit, theta_grid, 2000)])    # many circuits, one batch
```

Pubs with different shot counts are separate batches, and batches over 300 circuits are split automatically. `quantum-kernel-classifier` sends a whole Gram matrix through one `sampler.run`.

## Shots and seeds

Shots are most of the cost on hardware, so ask for what the answer needs. More shots reduce statistical noise, not systematic device error.

Every circuit gets a seed built from `ctx.seed`, so on a simulator the same seed gives the same counts. Hardware counts are not reproducible, which is why the `reproducible` gate runs on a simulator. Seed all your own randomness from `ctx.seed`.

## What each result carries

Each pub result has per-circuit metadata under `metadata["qsolve"]`, one dictionary per circuit:

```python
result = ctx.sampler.run([(circuit, None, shots)]).result()[0]
meta = (result.metadata.get("qsolve") or [{}])[0]
dispatch = meta.get("dispatch")     # the index to cite in report_mitigation
readout = meta.get("readout")       # per measured bit: [P(1|0), P(0|1)]
```

| Key | Present on | Meaning |
|---|---|---|
| `dispatch` | every target | The index of this circuit among everything the run sent. You cite it in `report_mitigation`. |
| `readout` | noise-model and hardware targets | Per measured classical bit, the readout error `[P(1 given 0), P(0 given 1)]` of its qubit. Can be `None`. |
| `two_qubit_gates` | noise-model and hardware targets | Two-qubit gates in the compiled circuit. |
| `physical_qubits` | noise-model and hardware targets | Qubits used in the compiled circuit. |

Use `.get` and handle a missing key: a simulator has no readout error to correct.

## What is metered

Qsolve records every circuit, its shot count and the counts that came back, at the backend.

| Measure | How |
|---|---|
| Circuits and shots | Counted at the backend. |
| Qubits | The most qubits any sent circuit uses. |
| Depth and two-qubit gates | Each distinct circuit is transpiled to the basis `cx, rz, sx, x`; the maximum over circuits is reported. Up to 40 distinct circuits are measured, largest first, then an even spread. |
| Fidelity | The counts compared with the ideal output of the same circuit. See [Error mitigation and fidelity](/docs/build/error-mitigation-and-fidelity). |

Training on a local statevector simulator, with NumPy or `qiskit.quantum_info`, never touches the backend and is not metered. The `train_on: backend` parameter in the examples trains through `ctx.sampler` instead, which sends and meters one circuit per optimiser step, so the example's `estimate()` counts them.

The cost quote comes from your `estimate()`; a run that needs more than was quoted stops with `budget_exhausted`. See [Credits and pricing](/docs/use/credits-and-pricing).

## Designing for hardware

- Keep circuits shallow. Fidelity falls with depth and two-qubit gate count. The examples cap QAOA at two layers on noise-model and hardware targets, because a third only added noise.
- Rehearse on `fake:<device>@1.6` before spending hardware time.
- Optimise on a local simulator and use the target only for the final measurement, as the examples do.
- Circuits above 20 qubits cannot be scored for fidelity, because the ideal output is simulated exactly only up to 20 qubits.
