Zum Hauptinhalt springen

Migriere von Sampler zu Executor

Dieser Leitfaden beschreibt, wie du Quanten-Sampling-Workloads vom IBM Quantum® Sampler-Primitive zum Executor-Primitive migrierst.

Beta-Version

Das Executor-Primitive ist Teil des gerichteten Ausführungsmodells. Alle Komponenten des gerichteten Ausführungsmodells befinden sich derzeit in der Beta-Phase und sind möglicherweise nicht stabil. Du bist eingeladen, sie zu testen und Feedback zu geben, indem du ein Issue in den GitHub-Repositories Samplomatic oder qiskit-ibm-runtime erstellst.

Solltest du migrieren?

Nicht jeder sollte von Sampler zu Executor migrieren. Es gibt viele Unterschiede zwischen den Primitives, aber die folgenden Hinweise können dir helfen zu entscheiden, ob du migrieren solltest:

Migriere zu Executor, wenn du ein Quanteninformationswissenschaftler bist, der Experimente im Utility-Maßstab durchführt und eine feingranulare, reproduzierbare Kontrolle über Techniken wie Pauli-Twirling, das Lernen und Injizieren von Rauschmodellen sowie Basiswechsel benötigt — oder eine der zusätzlichen Fähigkeiten von Executor benötigt.

Verwende weiterhin Sampler, wenn du eine einfache, High-Level-Schnittstelle möchtest und das Primitive die Fehlerunterdrückung und -minderung für dich übernehmen soll.

Einschränkungen und Vorbehalte

Da sich Executor und das gerichtete Ausführungsmodell in der Beta-Phase befinden, beachte Folgendes, bevor du dich für eine Migration entscheidest:

  • Noch keine Simulator-Unterstützung: Im Gegensatz zu Sampler, das über eine AerSampler-Implementierung in qiskit-aer für die lokale Simulation verfügt, gibt es derzeit kein Simulator-Backend für Executor. Simulator-Unterstützung wird voraussichtlich bald verfügbar sein. In der Zwischenzeit kannst du den Template-Circuit weiterhin lokal untersuchen und samplen, um deinen Workflow zu validieren, bevor du ihn an die Hardware sendest.

  • Dieser Leitfaden behandelt nur Sampler, nicht Estimator. Die Migration von Estimator zu Executor ist wesentlich aufwendiger als die Migration von Sampler, da Estimator Erwartungswerte berechnet, anstatt Rohdaten (Samples) zurückzugeben. Um das Verhalten von Estimator mit Executor nachzubilden, ist zusätzliche Nachbearbeitung erforderlich. Hilfsfunktionen für die Migration von Estimator zu Executor befinden sich noch in der Entwicklung, daher beschreibt dieser Leitfaden absichtlich nur den Sampler-Workflow.

Wichtige Unterschiede zwischen Executor und Sampler

Sampler und Executor samplen beide die Ausgaberegister von Quanten-Circuits, richten sich aber an unterschiedliche Nutzer:

  • Sampler ist eine High-Level-Abstraktion mit den folgenden Eigenschaften:

    • Es verfügt über integrierte Fehlerunterdrückung (Dynamical Decoupling und Twirling).

    • Es trifft implizite Entscheidungen für dich.

    • Es ist so konzipiert, dass sich Algorithmus-Entwickler auf Innovation statt auf Datenkonvertierung konzentrieren können.

  • Executor ist Teil des gerichteten Ausführungsmodells. Es unterscheidet sich in vielerlei Hinsicht von Sampler und hat die folgenden Eigenschaften:

    • Es hat keine integrierte Fehlerunterdrückung oder -minderung. Stattdessen erfasst du deine Design-Absicht auf der Client-Seite (mittels Circuit-Annotationen und einer Samplex), und die aufwendige Erzeugung von Circuit-Varianten wird auf die Serverseite verlagert.

    • Es trifft keine impliziten Entscheidungen. Es folgt deinen Anweisungen exakt und bietet dir volle Kontrolle und Transparenz.

    • Executor und Samplomatic bieten zusammen zusätzliche Fähigkeiten, die Sampler nicht bietet, darunter (aber nicht beschränkt auf) die folgenden:

      • Mehr Twirling-Gruppen: Mit Samplomatic kannst du auswählen, welche Twirling-Gruppe pro Box angewendet wird, statt auf die einzige Strategie beschränkt zu sein, die Sampler für dich anwendet. Es unterstützt außerdem andere Twirling-Gruppen als Pauli, wie die "local_c1"-Twirling-Gruppe.
      • Kernelisierte und klassifizierte Messungen zusammen: Durch Setzen von QuantumProgram.meas_level = "both" (hinzugefügt in qiskit-ibm-runtime v0.48.0) wird angefordert, dass sowohl klassifizierte als auch kernelisierte Messungen in den Ergebnissen enthalten sind, statt pro Job nur einen einzigen Messtyp auszuwählen.
      • Twirling für Circuits mit Fractional Gates: Executor kann Twirling auf Circuits anwenden, die Fractional Gates enthalten.
      • Feingranulare, kombinierbare Fehlerminderung: Zum Beispiel die Auswahl, welche Circuit-Schichten gemindert werden sollen, und die Anpassung der in den Circuit injizierten Rauschraten.
      Hinweise
      • Es wird erwartet, dass zukünftige neue Fähigkeiten zuerst für Executor veröffentlicht werden und möglicherweise nicht auf Sampler übertragen werden. Wenn du auf den Zugriff auf die neuesten Funktionen angewiesen bist, ist Executor die zukunftssicherere Wahl.
      • Das Basis-Qiskit-Paket bietet noch keine Basisklasse für das Executor-Primitive (für SamplerV2 schon).

Konzeptionelle Zuordnung

Die folgende Tabelle zeigt, wie Sampler-Konzepte auf Executor abgebildet werden.

KonzeptSamplerExecutor
Importfrom qiskit_ibm_runtime import SamplerV2from qiskit_ibm_runtime import Executor
EingabeListe von PUBs (Tupel)Ein QuantumProgram aus QuantumProgramItem-Objekten
Circuit und Parameter(circuit, params, shots)-Tupelprogram.append_circuit_item(circuit, circuit_arguments=...)
TwirlingTwirlingOptionsExplizit über annotierte Boxen und eine Samplex (append_samplex_item)
Run-Aufrufsampler.run([pub, ...])executor.run(program)
ErgebnistypPrimitiveResult aus SamplerPubResultQuantumProgramResult (iterierbar)
Daten abrufenresult[0].data.<register> (BitArray)result[0]["<register>"] (np.ndarray)
Rauschen verwaltenIntegrierte OptionenMuss manuell zusammengesetzt werden (Annotationen, Samplex, NoiseLearnerV3)

Überblick über die Migrationsschritte

  1. Samplomatic installieren.

  2. Die Imports ändern.

  3. PUB-Tupel ersetzen.

  4. Ändern, wie Shots ausgedrückt werden.

  5. Andere Optionen bei Bedarf aktualisieren.

  6. Den run-Befehl aktualisieren.

  7. Die Ergebnisverarbeitung aktualisieren.

  8. Twirling rückgängig machen.

Schritt 1: Installiere die erforderlichen Pakete

Executor und das gerichtete Ausführungsmodell erfordern das samplomatic-Paket:

pip install qiskit qiskit-ibm-runtime samplomatic

# For visualization support:
# pip install samplomatic[vis]
Versionshinweise
  • qiskit-ibm-runtime v0.48.0 wird empfohlen, da es die Option meas_level = "both" und die local_c1-Twirling-Gruppe hinzufügt.
  • qiskit >= 2.3.0 ist erforderlich.
  • samplomatic >= 0.18.0 ist erforderlich.

Schritt 2: Ändere die Imports

Sampler:

from qiskit_ibm_runtime import SamplerV2 as Sampler

Executor:

from qiskit_ibm_runtime import Executor, QuantumProgram

Schritt 3: Ersetze PUB-Tupel durch ein QuantumProgram

Anstatt eine Liste von Tupeln (PUBs) zu übergeben, erstellst du bei der Verwendung von Executor ein QuantumProgram und fügst ihm Items hinzu.

Ein QuantumProgram akzeptiert Circuit-Items und Samplex-Items:

  • append_circuit_item: Fügt ein CircuitItem hinzu, das aus einem Circuit und (optional) dessen Parameterwerten besteht. Es wird unverändert ausgeführt, ohne jegliche Randomisierung.

    Verwende dies, wenn du einen Circuit einfach nur samplen möchtest, genau so, wie es Sampler mit einem PUB ohne Twirling tun würde; zum Beispiel beim Absenden eines einfachen Sampling-Jobs, oder wenn du bereits manuell alle gewünschten Varianten eingebunden hast.

  • append_samplex_item: Fügt ein samplexItem hinzu, das aus einem Template-Circuit plus einer Samplex besteht, die randomisierte Parametersätze auf der Serverseite erzeugt.

    Verwende dies, wenn der Inhalt des Circuits randomisiert werden soll. Der wichtigste Anwendungsfall ist Twirling (Gate- oder Messtwirling) oder Rauschinjektion. Diese Fähigkeit ersetzt das integrierte Twirling von Sampler.

Ein einzelnes QuantumProgram kann beide Item-Typen akzeptieren; jedes hinzugefügte Item wird als eigenständige Aufgabe ausgeführt und erzeugt einen eigenen Eintrag in den Ergebnissen. Verwende im Allgemeinen append_circuit_item, wenn dein Circuit nicht randomisiert werden muss. Andernfalls verwende append_samplex_item.

Die nächsten Abschnitte zeigen beides der Reihe nach: parametrisierte Circuits, die append_circuit_item verwenden, sowie die Migration von Twirling mittels append_samplex_item.

In den folgenden Codebeispielen bezieht sich isa_circuit auf den Circuit, der so transpiliert wurde, dass er der Instruction Set Architecture (ISA) des Ziel-Backends entspricht. Dieser isa_circuit enthält zwei Parameter.

Schritt 3a: Migriere parametrisierte Circuits

Bei Sampler sind Parameterwerte das zweite Element des PUB-Tupels. Bei Executor übergibst du sie als circuit_arguments an append_circuit_item.

Sampler:

params = np.random.rand(10, circuit.num_parameters) # 10 parameter sets
pubs = (isa_circuit, params)

Executor

program = QuantumProgram(shots=1024)
program.append_circuit_item(
isa_circuit,
circuit_arguments=np.random.rand(10, circuit.num_parameters), # 10 sets
)

# CircuitItem result shape: (parameter_sets, shots, register_bits) -> (10, 1024, 2)
result_0 = result[0]["meas"]

Schritt 3b: Migriere integriertes Twirling zu expliziten Annotationen

Dies ist die bedeutendste Änderung. Sampler wendet Twirling mithilfe von Optionen für dich an. Bei Executor erklärst du diese Absicht explizit mithilfe annotierter Boxen und einer Samplex (aus Samplomatic).

Sampler (Twirling mithilfe von Optionen):

sampler = Sampler(mode=backend)
sampler.options.twirling.enable_gates = True
sampler.options.twirling.enable_measure = True

Executor (Twirling mithilfe von Boxen und einer Samplex):

from samplomatic import build
from samplomatic.transpiler import generate_boxing_pass_manager

# 1. Group gates and measurements into annotated boxes with twirling annotations
boxes_pm = generate_boxing_pass_manager(
enable_gates=True, # gate twirling
enable_measures=True, # measurement twirling
)
boxed_circuit = boxes_pm.run(isa_circuit)

# 2. Build the (template circuit, samplex) pair.
# The template circuit's single-qubit gates are replaced by parameterized gates;
# the samplex encodes how to generate the randomized parameters at runtime.
template_circuit, samplex = build(boxed_circuit)

# 3. Append as a samplex item, specifying the number of randomizations
program = QuantumProgram(shots=1024)
program.append_samplex_item(
template_circuit,
samplex=samplex,
samplex_arguments={
"parameter_values": np.random.rand(10, 2), # original circuit params
},
shape=(28, 10), # 28 randomizations x 10 parameter sets
)

Da der Template-Circuit und die Samplex auf der Client-Seite erstellt werden, kannst du sie lokal untersuchen und samplen, um die Ausgabe zu überprüfen, bevor du etwas an die Hardware sendest.

Verifizierung: Sample den Template-Circuit lokal

Du kannst Randomisierungen aus der Samplex ziehen und sie an den Template-Circuit binden, um zu bestätigen, dass die Samplex die erwarteten Parameterwerte erzeugt. Die von samplex.sample zurückgegebenen Parameterwerte sind direkt mit den Parametern des Template-Circuits kompatibel.

# Check which inputs the samplex requires (for the twirling example above,
# this is just the original circuit's parameter values).
print(samplex.inputs())

# Bind the required inputs, then draw a few randomizations locally.
inputs = samplex.inputs().bind(
parameter_values=np.random.rand(2), # one set of the original circuit's params
)
outputs = samplex.sample(inputs, num_randomizations=3)

# Assign one randomization's parameter values to the template circuit and inspect it.
bound_template = template_circuit.assign_parameters(outputs["parameter_values"][0])
bound_template.draw("mpl", idle_wires=False)

Um noch weiter zu gehen, kannst du überprüfen, ob jede Randomisierung logisch äquivalent zum ursprünglichen Circuit ist, indem du zum Beispiel beide in Operator-Objekte umwandelst und ihre unitären Implementierungen vergleichst (nachdem du die outputs["measurement_flips.<register>"]-Korrekturen berücksichtigt hast, die das Messtwirling rückgängig machen), oder indem du Erwartungswerte aus einem lokalen StatevectorSampler- oder StatevectorEstimator-Lauf vergleichst. Eine vollständige Anleitung findest du im Samplomatic-Leitfaden Samplex-Eingaben und -Ausgaben.

Schritt 4: Ändere, wie Shots angefordert werden

Verschiebe Shots vom PUB zu QuantumProgram(shots=...). Bei Executor gilt shots für den gesamten Job. Reiche mehrere Jobs ein, wenn du unterschiedliche Shot-Anzahlen benötigst.

Sampler:

# Run — shots are passed to run()
sampler = Sampler(mode=backend)
job = sampler.run([(isa_circuit, None, 25)])

Executor:

# Build a QuantumProgram — shots are on the program
program = QuantumProgram(shots=25)
program.append_circuit_item(isa_circuit)

Schritt 5: Aktualisiere Optionen bei Bedarf

Executor stehen weniger Optionen zur Verfügung als Sampler, da Entscheidungen zur Fehlerminderung jetzt in deinen Annotationen und der Samplex statt in Optionen liegen.

Es gibt auch einen strukturellen Unterschied darin, wo Einstellungen liegen.

  • Bei Sampler wird alles, einschließlich Entscheidungen, die die Nachbearbeitung der Ergebnisse betreffen, über die Optionen des Primitives oder im PUB konfiguriert.

  • Bei Executor werden Entscheidungen, die beeinflussen, wie die Job-Ergebnisse strukturiert und nachbearbeitet werden, auf dem QuantumProgram festgelegt, nicht auf ExecutorOptions.

Beispiele:

SamplerExecutor
shotsQuantumProgram(shots=...)
meas_typeQuantumProgram(meas_level=...)

ExecutorOptions enthält nur Low-Level-Ausführungs- und Umgebungseinstellungen, die die Struktur der zurückgegebenen Daten nicht verändern. Es hat drei Gruppen auf oberster Ebene:

Bemerkenswert ist, dass die Optionen twirling und dynamical_decoupling bei Sampler existieren, nicht aber bei Executor. Stattdessen werden diese Optionswerte über das gerichtete Ausführungsmodell ausgedrückt.

Beispiel:

from qiskit_ibm_runtime import Executor, ExecutorOptions

options = ExecutorOptions(
environment={"log_level": "INFO"},
execution={"init_qubits": True},
)
# or mutate after construction:
options = ExecutorOptions()
options.environment.log_level = "INFO"
options.execution.init_qubits = True

executor = Executor(mode=backend, options=options)

Schritt 6: Aktualisiere den run-Befehl

Die Eingabe für einen Executor-Job ist das Programm anstelle von PUBs.

Sampler:

# Submit a job
sampler.run([(isa_circuit, parameter_values)])

Executor:

# Submit a job
executor.run(program)

Schritt 7: Ändere, wie du auf Ergebnisse zugreifst

Bei Executor sind Ergebnisse NumPy-Arrays, keine BitArray-Objekte. Verwende den Namens-String als Index (result[0]["meas"]) und erhalte ein np.ndarray zurück. Du musst dir den Attributpfad .data.<register> nicht merken.

Um von Sampler zu Executor zu aktualisieren, ändere result[i].data.<reg> (BitArray) zu result[i]["<reg>"] (np.ndarray) und schreibe dann get_counts-basierte Nachbearbeitung als NumPy-Operationen um.

AufgabeSamplerExecutor
Registerdaten abrufenresult[0].data.measresult[0]["meas"]
DatentypBitArraynp.ndarray
Counts-Dictionaryresult[0].data.meas.get_counts()Array manuell nachbearbeiten
Mehrere Registerresult[0].data.<name> pro Registerresult[0]["<name>"] pro Register
CircuitItem-Array-Form-(parameter_sets, shots, register_bits)
SamplexItem-Array-Form-(randomizations, parameter_sets, shots, register_bits)
Messtwirling rückgängig machenAutomatischresult[i]["measurement_flips.<name>"] + XOR
hinweis

Das BitArray von Sampler bietet Hilfsfunktionen (get_counts, slice_bits, slice_shots, expectation_values und Post-Selection-Masken). Executor gibt rohe NumPy-Arrays zurück, sodass du diese Nachbearbeitung mit standardmäßigen NumPy-Operationen durchführen kannst.

Schritt 8: Verarbeite getwirlte Ergebnisse (Bit-Flip-Korrekturen)

Wenn du Messtwirling über ein SamplexItem anwendest, gibt Executor die rohen (getwirlten) Messungen sowie die Bit-Flip-Korrekturen zurück, die zum Rückgängigmachen des Twirlings benötigt werden. Du musst sie manuell anwenden; nichts wird implizit korrigiert.

Wenn du Executor verwendest, mache Twirling explizit rückgängig, indem du die measurement_flips.<reg>-Korrekturen und ein XOR verwendest, wie im folgenden Beispiel gezeigt:

# SamplexItem result shape: (randomizations, parameter_sets, shots, register_bits)
result_1 = result[1]["meas"] # example: (28, 10, 1024, 2)

# Bit-flip corrections to undo measurement twirling
flips_1 = result[1]["measurement_flips.meas"] # example: (28, 10, 1, 2)

# Undo the twirling through classical XOR (broadcasts over the shots axis)
unflipped_result_1 = result_1 ^ flips_1

Es gibt keinen entsprechenden Schritt bei Sampler, da es Twirling für dich rückgängig macht.

Vollständiges Beispiel: Migriere einen einfachen Sampling-Job

Sampler

import numpy as np
from qiskit.circuit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler

# 1. Account + backend
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)

# 2. Circuit
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.h(1)
circuit.cz(0, 1)
circuit.h(1)
circuit.measure_all()

# 3. Transpile to ISA
pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
isa_circuit = pm.run(circuit)

# 4. Run — shots are passed to run()
sampler = Sampler(mode=backend)
job = sampler.run([(isa_circuit,)], shots=25)
result = job.result()

# 5. Access results: a BitArray keyed by register name
counts = result[0].data.meas.get_counts()

Executor

import numpy as np
from qiskit.circuit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, Executor
from qiskit_ibm_runtime.quantum_program import QuantumProgram

# 1. Account + backend (unchanged)
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)

# 2. Circuit (unchanged)
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.h(1)
circuit.cz(0, 1)
circuit.h(1)
circuit.measure_all()

# 3. Transpile to ISA (unchanged)
pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
isa_circuit = pm.run(circuit)

# 4. Build a QuantumProgram — shots are on the program
program = QuantumProgram(shots=25)
program.append_circuit_item(isa_circuit)

# 5. Run
executor = Executor(mode=backend)
job = executor.run(program)
result = job.result()

# 6. Access results: a plain np.ndarray keyed by register name
# shape = (shots, register_bits)
meas = result[0]["meas"]

Nächste Schritte