Skip to content

QFw: build the Qiskit backend's target from the QPM's device telemetry - #163

Merged
DougSO merged 2 commits into
mainfrom
qiskit-device-target
Oct 21, 2026
Merged

DougSO merged 2 commits into
mainfrom
qiskit-device-target

Conversation

@DougSO

@DougSO DougSO commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

What

QFwBackend.target was build_qfw_target(400): a generic 400-qubit all-to-all target with a fixed 39-gate list, whatever QPM the backend was bound to. A workflow that wanted the device for target-aware transpilation (layout search, RZZ handling) found no public API and called the shim driver's private _build_backend_target in-process.

The client now binds the QPM's telemetry API binding next to the execution one, on the same managed binding (qfw_lookup_service.get_qpm_telemetry), and exposes it as backend.telemetry (QFwTelemetry: get_backend_info, get_device_info, get_coupling_graph, get_calibration_snapshot, get_dynamic_backend_info, each carrying the backend's shim library and reservation token). backend.target is built from it once per backend instance:

What the QPM reports What backend.target holds
IBM through the shim: the raw BackendConfiguration under static_architecture and, new in the shim's get_backend_info, the raw BackendProperties under backend_properties qiskit_ibm_runtime.convert_to_target(BackendConfiguration.from_dict(...), BackendProperties.from_dict(...)), the conversion _build_backend_target does; without the package the configuration alone gives the qubits, basis gates and coupling map
qhw records (IQM through the shim, any QDMI device) the qubits, the native operations on their loci (prx -> r, reset_wait -> reset, both orders of a symmetric two-qubit locus), error and duration per locus and T1/T2 per qubit from the calibration snapshot's iqm.v1 / qdmi.fomac.v1 extensions
a plain description (the fake IQM's qubit count and couplings list) the qubits and edges with QFw's generic gates on them
metadata_supported: false (NWQSim), a failing telemetry call, no managed binding the generic target, with the reason logged and recorded in backend.target_source

The legacy configuration() dict follows the target. Docs: a Device Target section in usage.md, a note in the shim provider guide (step 5) and in qpmd-supported-apis.md.

Tests

  • tests/mock/test_qfw_backend_target.py: the binding, the shim-library/token context, the fake-IQM shape, the qhw-record shape with IQM calibration observations, the QDMI extension, opaque device operations, the IBM path through a converter seam and without the converter, the three fallbacks with their logged reasons, caching, an explicit target, optional secondary records, configuration().
  • tests/mock/test_qfw_lookup_service.py: the telemetry API is selected from the service record the execution client's lifecycle binding already holds (no second directory query).
  • tests/mock/test_qrmi_ibm_dispatch.py: backend_properties in the IBM composite record.
  • tests/qiskit/test_qfw_target.py (real qiskit): a qhw-built IQM target transpiles a GHZ to r/cz/measure; a FakeManilaV2 configuration + properties through the real convert_to_target.
  • The mock suite's qiskit stubs gain InstructionProperties, Gate, Parameter and a Target that records what was added on which qargs.

Local run in c1 against this branch: ci-syntax clean, mock 669 passed / 1 skipped, unit 130 passed, tests/qiskit 53 passed. The builder was also run on a saved live QRMI target() dump of the ORNL q20 through the qhw_iqm normalizers: 20 qubits, cz on 60 directed qargs (30 edges), r/measure/reset on 20, errors and durations filled (e.g. cz(0,1): duration 6e-08 s, error 0.0044), T1/T2 per qubit, and a GHZ transpiles to r/cz/measure.

Live validation on the cluster

Inside qfw-slurm jobs as user-a (sbatch --qpu=... --workload-kind=quantum --circ-count=1 --max-qubits=5 --max-depth=16 --max-shots=100, then qfw-setup / qfw-srun probe.py / qfw-teardown), with this branch's backends/ ahead of the image's installed copy on PYTHONPATH. The probe prints the telemetry records, the target, transpiles a 3-qubit GHZ against it and samples it through the reservation.

--qpu=fake-iqm-20q, provider fake-iqm (Slurm job 13, COMPLETED, 13 s on c1):

qfw_qiskit from /workspace/qfw-container-base/QFw-target/backends/qfw_qiskit/__init__.py
telemetry binding: QFwTelemetry
get_backend_info: 80 bytes, keys=['backend', 'metadata_supported', 'target_id']
    {"backend": "fake-iqm", "metadata_supported": true, "target_id": "fake-iqm-20q"}
get_device_info: 118 bytes, keys=['backend', 'max_shots', 'metadata_supported', 'num_qubits', 'target_id']
    {"backend": "fake-iqm", "max_shots": 10000, "metadata_supported": true, "num_qubits": 20, "target_id": "fake-iqm-20q"}
get_coupling_graph: 266 bytes, keys=['backend', 'calibration_set_id', 'couplings', 'target_id']
    {"backend": "fake-iqm", "calibration_set_id": null, "couplings": [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10], [10, 11], [11, 12], [12, 13], [13, 14], [14, 15], [15, 16], [16, 17], [17, 18], [18, 19]], "t
get_calibration_snapshot: 80 bytes, keys=['backend', 'calibration_set_id', 'target_id']
    {"backend": "fake-iqm", "calibration_set_id": null, "target_id": "fake-iqm-20q"}
target source: device: QFw fake-iqm fake-iqm-20q target
target: 'QFw fake-iqm fake-iqm-20q target', 20 qubits, 37 operations
operation names: ['ch', 'cp', 'crx', 'cry', 'crz', 'cs', 'csdg', 'csx', 'cu', 'cx', 'cy', 'cz', 'h', 'id', 'measure', 'p', 'reset', 'rx', 'rxx', 'ry', 'ryy', 'rz', 'rzz', 's', 'sdg', 'swap', 'sx', 'sxdg', 't', 'tdg', 'u', 'u1', 'u2', 'u3', 'x', 'y', 'z']
coupling map: [(0, 1), (1, 0), (1, 2), (2, 1), (2, 3), (3, 2), (3, 4), (4, 3), (4, 5), (5, 4), (5, 6), (6, 5), (6, 7), (7, 6), (7, 8), (8, 7), (8, 9), (9, 8), (9, 10), (10, 9), (10, 11), (11, 10), (11, 12), (12, 11), (12, 13), (13, 12), (13, 14), (14, 13), (14, 15), (15, 14), (15, 16), (16, 15), (16, 17), (17, 16), (17, 18), (18, 17), (18, 19), (19, 18)]
  cx: 38 qargs, first [(0, 1), (1, 0), (1, 2), (2, 1)]
  cz: 38 qargs, first [(0, 1), (1, 0), (1, 2), (2, 1)]
  rzz: 38 qargs, first [(0, 1), (1, 0), (1, 2), (2, 1)]
  measure: 20 qargs, first [(0,), (1,), (2,), (3,)]
GHZ(3) transpiled against the target: {'measure': 3, 'cx': 2, 'h': 1, 'barrier': 1}, layout [2, 1, 0]
run on reservation 3: {'000': 100}
probe done

(The fake IQM QPM names no gate set, so the target carries QFw's generic gates on the device's 20 qubits and linear coupling; three-qubit gates are left out, the transpiler decomposes them. Its canned result is all zeros.)

--qpu=nwqsim, provider nwqsim (Slurm job 14, COMPLETED, 13 s on c1):

qfw_qiskit from /workspace/qfw-container-base/QFw-target/backends/qfw_qiskit/__init__.py
telemetry binding: QFwTelemetry
get_backend_info: 50 bytes, keys=['backend', 'metadata_supported']
    {"backend": "nwqsim", "metadata_supported": false}
get_device_info: 50 bytes, keys=['backend', 'metadata_supported']
    {"backend": "nwqsim", "metadata_supported": false}
get_coupling_graph: 78 bytes, keys=['backend', 'calibration_set_id', 'metadata_supported']
    {"backend": "nwqsim", "calibration_set_id": null, "metadata_supported": false}
get_calibration_snapshot: 78 bytes, keys=['backend', 'calibration_set_id', 'metadata_supported']
    {"backend": "nwqsim", "calibration_set_id": null, "metadata_supported": false}
target source: generic: QPM nwqsim reports no device metadata
target: 'Quantum Framework (QFw) Target', 400 qubits, 39 operations
operation names: ['ccx', 'ch', 'cp', 'crx', 'cry', 'crz', 'cs', 'csdg', 'cswap', 'csx', 'cu', 'cx', 'cy', 'cz', 'h', 'id', 'measure', 'p', 'reset', 'rx', 'rxx', 'ry', 'ryy', 'rz', 'rzz', 's', 'sdg', 'swap', 'sx', 'sxdg', 't', 'tdg', 'u', 'u1', 'u2', 'u3', 'x', 'y', 'z']
coupling map: None
  cx: None qargs, first None
  cz: None qargs, first None
  rzz: None qargs, first None
  measure: None qargs, first None
GHZ(3) transpiled against the target: {'measure': 3, 'cx': 2, 'h': 1, 'barrier': 1}, layout None
run on reservation 5: {'000': 52, '111': 48}
probe done

--qpu=ornl-shim-20q, provider shim (Slurm job 16, COMPLETED, 31 s on c1), q20 tunnel up, shim-head's device-access URL on the tunnel port, a user-a record in shim-head's credential DB:

qfw_qiskit from /workspace/qfw-container-base/QFw-target/backends/qfw_qiskit/__init__.py
telemetry binding: QFwTelemetry
get_backend_info: FAILED: DEFwError: ... failed to read QRMI target(): ... QRMI driver could not resolve device access for provider 'iqm': ... QPU credential DB does not contain an enabled entitlement and API key for user 'root' and device 'ornl-iqm-20q'
get_device_info / get_coupling_graph / get_calibration_snapshot: FAILED (the same reason)
target source: generic: the QPM's device telemetry failed: DEFwError: ... user 'root' and device 'ornl-iqm-20q'
target: 'Quantum Framework (QFw) Target', 400 qubits, 39 operations
coupling map: None
GHZ(3) transpiled against the target: {'measure': 3, 'cx': 2, 'h': 1, 'barrier': 1}, layout None
run on reservation 1: {'000': 46, '001': 3, '011': 1, '101': 2, '110': 3, '111': 45}
probe done

This exercised the failure path for real. The reservation was granted on user-a's record and the GHZ ran on the real q20 through the shim (hardware counts), while the four telemetry calls resolved the credential of the user the QPM process runs as (root, from USER in the service's environment; credential-mode: required), which has no record on shim-head, so the backend kept the generic target with the reason in target_source. The device target from the q20 therefore needs a record for the service user, as the 2026-10-12 site-route runs had. usage.md and the shim guide now say so.

With that root record added on shim-head, --qpu=ornl-shim-20q as user-a, library unset (Slurm job 17, COMPLETED, 24 s on c1): the device target from the real q20, through the site shim as deployed (QRMI serves get_backend_info, the shim's QDMI preference serves the three records):

get_backend_info: 2052 bytes, keys=['active_qubits', 'backend', 'calibration_set_id', 'metadata_supported', 'qhw_device', 'static_architecture']
get_device_info: 806 bytes ... "metadata": {"source": "qdmi", "via": "mqt.core.qdmi"} ... "qubits": [{"id": "QB1", "index": 0}, ...
get_coupling_graph: 2332 bytes ... "edges": [["QB1", "QB2"], ["QB1", "QB4"], ... (30 couplers) ... "operations": cz, measure, prx, prx_12
get_calibration_snapshot: 6567 bytes ... "calibration_set_id": "8fadf8ba-f61d-4c31-913a-f3308981a59f", "quality_metric_count": 70 ...
target source: device: QFw iqm ornl-iqm-20q target
target: 'QFw iqm ornl-iqm-20q target', 20 qubits, 3 operations
operation names: ['cz', 'measure', 'r']
coupling map: [(0, 1), (0, 3), (1, 0), (1, 4), (2, 3), (2, 7), ... 60 directed edges]
  cz: 60 qargs, first [(0, 1), (0, 3), (1, 0), (1, 4)]
  r: 20 qargs, first [(0,), (1,), (2,), (3,)]
  measure: 20 qargs, first [(0,), (1,), (2,), (3,)]
GHZ(3) transpiled against the target: {'r': 10, 'measure': 3, 'cz': 2, 'barrier': 1}, layout [10, 5, 6]
run on reservation 2: {'000': 39, '011': 4, '100': 2, '101': 1, '110': 1, '111': 53}
probe done

Second commit: get_backend_info from the QDMI driver

The client leads with get_backend_info; the QDMI driver had no such method and DEFAULT_CAPS routed the call to QRMI alone, so a resource wired for QDMI only (the Braket entries) answered library 'qdmi' does not implement 'get_backend_info' and the client stayed generic, although the driver's device, coupling and calibration records are exactly what the builder reads. Now:

  • QdmiDriver.get_backend_info returns the composite the QRMI driver returns for a device that is not IBM: backend, metadata_supported, active_qubits (the site labels), calibration_set_id from the provider's custom slot (fomac_normalize.calibration_set_id, the public name for the lookup the snapshot already does) and qhw_device from to_device_record(extract_topology(device)) with the profile's technology; static_architecture is {} (QDMI has no raw provider architecture; a QRMI older than 0.22.0 reports it empty too). The IQM profile now names its technology (superconducting), so the QDMI record matches qhw_iqm's.
  • DEFAULT_CAPS["get_backend_info"] = ["qdmi", "qrmi"], the order the other introspection calls use; get_dynamic_backend_info stays with QRMI.
  • Tests: tests/mock/test_qdmi_backend_info.py (the composite with a stubbed and a real device record, the Frontend routing with and without a preference, a QDMI-only resource served, lib="qrmi" still refused there), the client building its target through QDMI records with lib="qdmi" in test_qfw_backend_target.py, the profile-defaults test updated. Docs: qpmd-supported-apis.md, the shim guide, qpu-frontend-contract.md.

Offline, from a saved QDMI extraction of the q20 (the four records read in-process on shim-head with this branch's QdmiDriver, device access resolved by the driver from the site's file; saved as shared-dir/tmp/p31/q20-qdmi-records.json, device data only):

get_backend_info: {"backend": "iqm", "metadata_supported": true, "static_architecture": {}, "active_qubits": ["QB1", ..., "QB20"], "calibration_set_id": "8fadf8ba-f61d-4c31-913a-f3308981a59f"}
qhw_device: {"id": "ornl-iqm-20q", "num_qubits": 20, "technology": "superconducting"} schema qhw-device-v1 qubits 20
coupling: edges 30 operations [('cz', 2, 30), ('measure', 1, 20), ('prx', 1, 20), ('prx_12', 1, 20)]
calibration: duration_unit us gate_metrics 70 qubit_metrics 20
target: 'QFw iqm ornl-iqm-20q target', 20 qubits, operations ['cz', 'measure', 'r']
  cz: 60 qargs, 60 with error (median 0.0066), 0 with duration
  measure: 20 qargs, 20 with error (median 0.0266), 0 with duration
  r: 20 qargs, 20 with error (median 0.00050), 0 with duration
coupling map edges: 60
qubit 0 properties: QubitProperties(t1=0.000044, t2=0.000007, frequency=None)
GHZ(3) transpiled: {'r': 10, 'measure': 3, 'cz': 2, 'barrier': 1} layout [10, 5, 6]

The IQM QDMI adapter reports per-operation fidelities and per-qubit T1/T2 but no per-operation durations, so a QDMI-built IQM target carries errors and coherence times only; the QRMI-built one (the target() dump above) carries durations as well.

On the q20 through QDMI, before and after, both as the client sees it:

  • Before, the shim as deployed (image code), QFW_SHIM_LIB=qdmi, telemetry only (Slurm job 18, user-a, COMPLETED): get_backend_info: FAILED ... library 'qdmi' does not implement 'get_backend_info' for resource 'ornl-iqm-20q'; the three record calls succeeded through QDMI; target source: generic: the QPM's device telemetry failed: ..., 400 qubits, no coupling map.
  • After, with this branch's code serving: a job-owned shim QPM from an install of the branch (shared-dir/qfw-target-install), the shim smoke's flow with a required-credential manifest and a site config naming shim-head's device-access file, placed on shim-head under a root salloc and reached through the stand-in reservation driver as owner root, QFW_SHIM_LIB=qdmi for the telemetry and the run (Slurm job 19, COMPLETED, 46 s):
get_backend_info: 1160 bytes, keys=['active_qubits', 'backend', 'calibration_set_id', 'metadata_supported', 'qhw_device', 'static_architecture']
get_device_info: 839 bytes ... "device": {"id": "ornl-iqm-20q", "num_qubits": 20, "technology": "superconducting"} ... "source": "qdmi"
get_coupling_graph: 2332 bytes ... 30 couplers
get_calibration_snapshot: 6567 bytes ... "calibration_set_id": "8fadf8ba-f61d-4c31-913a-f3308981a59f"
target source: device: QFw iqm ornl-iqm-20q target
target: 'QFw iqm ornl-iqm-20q target', 20 qubits, 3 operations
operation names: ['cz', 'measure', 'r']
  cz: 60 qargs | r: 20 qargs | measure: 20 qargs
GHZ(3) transpiled against the target: {'r': 10, 'measure': 3, 'cz': 2, 'barrier': 1}, layout [10, 5, 6]
run on reservation 1: {'000': 42, '001': 1, '010': 1, '011': 2, '100': 1, '101': 2, '110': 3, '111': 48}
probe done

Cluster configuration, not in this PR: descriptor._copy_caps takes a device's own caps block as the whole table (caps or DEFAULT_CAPS, no merge). The cluster repo's config/device-access.yaml lists execution caps only (run_circuit, get_task_timing, get_task_metadata) for ibm-156-nh, aws, aws-ionq-aria-1 and aws-rigetti-ankaa, so those services serve no introspection call at all and the client gets the generic target for them. Those entries need get_backend_info, get_device_info, get_coupling_graph and get_calibration_snapshot added ([qdmi] for the Braket entries, [qrmi] for IBM), or their caps blocks dropped to inherit the defaults.

Notes

  • The calibration's per-locus values are not in the qhw-calibration-v1 core (it carries identity, counts and summaries), so the client reads the two extensions the shim writes, iqm.v1 (IQM's observation sets) and qdmi.fomac.v1, and ignores any other namespace. A neutral per-locus metric list published by the shim would let the client drop that knowledge; a follow-up.
  • The fake IQM QPM could declare an IQM-like native gate set in its telemetry so the fake target matches a real IQM's; today it reports only the qubit count and couplings.
  • On a credential-mode: required shim the telemetry calls resolve the service user's record, not the reservation owner's, because get_backend_info and the record calls carry no reservation context (job 16 above; job 17 once the root record existed). Letting the QPM use the reservation's bound credential for introspection (the telemetry methods already take token) would make the device target work for any entitled user without a record for the service user; a follow-up.

QFwBackend.target was build_qfw_target(400): a generic 400-qubit
all-to-all target with a fixed gate list, whatever QPM the backend was
bound to. A workflow that needed the device for target-aware
transpilation (layout search, RZZ handling) had no public API and
called the shim driver's private _build_backend_target in-process.

The client now binds the QPM's telemetry API next to the execution API,
on the same managed binding (qfw_lookup_service.get_qpm_telemetry), and
exposes it as backend.telemetry: a QFwTelemetry with get_backend_info,
get_device_info, get_coupling_graph, get_calibration_snapshot and
get_dynamic_backend_info, each carrying the backend's shim library and
reservation token. backend.target is built from it once per backend:

- an IBM record, the raw BackendConfiguration under static_architecture
  and, new in the shim's get_backend_info, the raw BackendProperties
  under backend_properties, goes through qiskit_ibm_runtime's
  convert_to_target, the conversion _build_backend_target does; without
  the package the configuration alone gives the qubits, basis gates and
  coupling map;
- qhw records (IQM through the shim, any QDMI device) give the qubits,
  the native operations on their loci (prx -> r, reset_wait -> reset,
  both orders of a symmetric two-qubit locus) and the calibration
  snapshot's error and duration per locus and T1/T2 per qubit, read
  from the iqm.v1 and qdmi.fomac.v1 extensions;
- a plain description (the fake IQM's qubit count and couplings list)
  gives the qubits and edges with QFw's generic gates on them;
- metadata_supported false (NWQSim), a failing telemetry call, or a
  connection without a managed binding keeps the generic target, with
  the reason logged and recorded in backend.target_source.

The legacy configuration() dict follows the target. The mock suite's
qiskit stubs gain InstructionProperties, Gate, Parameter and a Target
that records what was added on which qargs; tests/qiskit runs the real
conversion on a FakeManilaV2 configuration and transpiles a GHZ against
a qhw-built IQM target. Verified on the cluster inside qfw-slurm jobs:
provider fake-iqm gets a 20-qubit target with the device's linear
coupling, provider nwqsim keeps the generic target, and a run against
the real q20 through the shim reached the device under the reservation
while the telemetry fell back, with its reason recorded, for want of
the service user's credential record.

Signed-off-by: Doug Oucharek <dougso@me.com>
@DougSO
DougSO force-pushed the qiskit-device-target branch from 566dfad to 931f9a4 Compare October 21, 2026 04:02
The Qiskit client leads with get_backend_info when it builds its device
target and keeps the generic target when the call fails. The QDMI driver
had no get_backend_info, and the default capability table routed the
call to QRMI alone, so every resource wired for QDMI only (the Braket
entries) answered "library 'qdmi' does not implement 'get_backend_info'"
and the client stayed generic, although the driver's device, coupling
and calibration records are exactly what the builder reads.

QdmiDriver.get_backend_info now returns the composite the QRMI driver
returns for a device that is not IBM: the provider, metadata_supported,
the active qubits (the site labels), the provider's calibration set id
from its custom slot (fomac_normalize.calibration_set_id, the public
name for the lookup the snapshot already does) and the qhw device
record from the topology, with the profile's technology. QDMI has no
raw provider architecture, so static_architecture is empty, as it is
for a QRMI too old to report one. The IQM profile names its technology
(superconducting), so the QDMI record matches qhw_iqm's. DEFAULT_CAPS
offers get_backend_info on both libraries, in the order the other
introspection calls use. get_dynamic_backend_info stays with QRMI.

Tests cover the composite, the real device record, the Frontend routing
with and without a preference and for a QDMI-only resource, and the
client building its target through QDMI records with lib qdmi. Verified
on the real q20 through QDMI: in-process with the branch's driver, and
end to end with a job-owned shim QPM from an install of the branch,
where the client built the device target through the QDMI library and
ran a GHZ through it. The docs say both libraries serve the call.

Signed-off-by: Doug Oucharek <dougso@me.com>
@DougSO
DougSO merged commit 6f50618 into main Oct 21, 2026
9 checks passed
@DougSO
DougSO deleted the qiskit-device-target branch October 21, 2026 17:41
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.

1 participant