Migrer de Sampler vers Executor
Ce guide décrit comment déplacer des charges de travail d'échantillonnage quantique de la primitive Sampler d'IBM Quantum® vers la primitive Executor.
La primitive Executor fait partie du modèle d'exécution dirigée. Tous les composants du modèle d'exécution dirigée sont actuellement en bêta et peuvent ne pas être stables. Tu es invité à les tester et à donner ton avis en ouvrant un ticket dans les dépôts GitHub Samplomatic ou qiskit-ibm-runtime.
Faut-il migrer ?
Tout le monde ne devrait pas migrer de Sampler vers Executor. Il existe de nombreuses différences entre les primitives, mais les conseils suivants peuvent t'aider à décider si tu dois migrer :
Migre vers Executor si tu es un scientifique de l'information quantique qui exécute des expériences à l'échelle utilitaire et a besoin d'un contrôle fin et reproductible sur des techniques comme le twirling de Pauli, l'apprentissage et l'injection de modèles de bruit, et les changements de base — ou qui a besoin de l'une des capacités supplémentaires fournies par Executor.
Continue à utiliser Sampler si tu veux une interface simple et de haut niveau et que tu veux que la primitive gère la suppression et l'atténuation des erreurs à ta place.
Limitations et mises en garde
Parce qu'Executor et le modèle d'exécution dirigée sont en bêta, note ce qui suit avant de décider de migrer :
-
Pas encore de prise en charge des simulateurs : Contrairement à Sampler, qui dispose d'une implémentation
AerSamplerdansqiskit-aerpour la simulation locale, il n'existe actuellement pas de backend simulateur pour Executor. La prise en charge des simulateurs devrait arriver bientôt. En attendant, tu peux toujours inspecter et échantillonner le circuit modèle localement pour valider ton flux de travail avant de le soumettre au matériel. -
Ce guide couvre uniquement Sampler, pas Estimator. Migrer d'Estimator vers Executor est considérablement plus impliqué que de migrer depuis Sampler, car Estimator calcule des valeurs d'espérance plutôt que de renvoyer des échantillons bruts. Reproduire le comportement d'Estimator avec Executor nécessite un post-traitement supplémentaire. Des fonctions utilitaires pour aider à migrer d' Estimator vers Executor sont encore en cours de développement, ce guide décrit donc intentionnellement uniquement le flux de travail de Sampler.
Principales différences entre Executor et Sampler
Sampler et Executor échantillonnent tous deux les registres de sortie des circuits quantiques, mais ils ciblent des utilisateurs différents :
-
Sampler est une abstraction de haut niveau. Il présente les caractéristiques suivantes :
-
Il dispose d'une suppression d'erreurs intégrée (découplage dynamique et twirling).
-
Il prend des décisions implicites à ta place.
-
Il est conçu pour que les développeurs d'algorithmes puissent se concentrer sur l'innovation plutôt que sur la conversion des données.
-
-
Executor fait partie du modèle d'exécution dirigée. Il diffère de Sampler à de nombreux égards et présente les caractéristiques suivantes :
-
Il n'a pas de suppression ou d'atténuation d'erreurs intégrée. Au lieu de cela, tu captures ton intention de conception côté client (en utilisant des annotations de circuit et un samplex), et la génération coûteuse de variantes de circuit est déplacée côté serveur.
-
Il ne prend aucune décision implicite. Il suit tes directives exactement, offrant un contrôle et une transparence complets.
-
Executor et Samplomatic exposent ensemble des capacités supplémentaires que Sampler n'offre pas, notamment (mais sans s'y limiter) les suivantes :
- Plus de groupes de twirling : Samplomatic te permet de choisir quel groupe de twirling appliquer
par boîte, plutôt que d'être limité à la stratégie unique que Sampler applique à ta place. Il prend aussi en charge des groupes de twirling autres que Pauli, comme le groupe de twirling
"local_c1". - Mesures « kerneled » et classifiées ensemble : Définir
QuantumProgram.meas_level = "both"(ajouté dansqiskit-ibm-runtimev0.48.0) demande que les mesures classifiées et « kerneled » soient toutes deux présentes dans les résultats, au lieu de choisir un seul type de mesure par job. - Twirling pour les circuits avec des portes fractionnaires : Executor peut appliquer le twirling à des circuits qui contiennent des portes fractionnaires.
- Atténuation d'erreurs fine et composable : Par exemple, choisir quelles couches de circuit atténuer et ajuster les taux de bruit injectés dans le circuit.
Remarques- Les futures nouvelles capacités devraient être publiées pour Executor en premier et pourraient ne pas être portées vers Sampler. Si tu dépends de l'accès aux dernières fonctionnalités, Executor est le choix le plus pérenne.
- Le package Qiskit de base ne fournit pas encore
de classe de base pour la primitive Executor (il en fournit une pour
SamplerV2).
- Plus de groupes de twirling : Samplomatic te permet de choisir quel groupe de twirling appliquer
par boîte, plutôt que d'être limité à la stratégie unique que Sampler applique à ta place. Il prend aussi en charge des groupes de twirling autres que Pauli, comme le groupe de twirling
-
Correspondance conceptuelle
Le tableau suivant montre comment les concepts de Sampler correspondent à Executor.
| Concept | Sampler | Executor |
|---|---|---|
| Import | from qiskit_ibm_runtime import SamplerV2 | from qiskit_ibm_runtime import Executor |
| Entrée | Liste de PUB (tuples) | Un QuantumProgram d'objets QuantumProgramItem |
| Circuit et paramètres | tuple (circuit, params, shots) | program.append_circuit_item(circuit, circuit_arguments=...) |
| Twirling | TwirlingOptions | Explicite via des boîtes annotées et un samplex (append_samplex_item) |
| Appel d'exécution | sampler.run([pub, ...]) | executor.run(program) |
| Type de résultat | PrimitiveResult de SamplerPubResult | QuantumProgramResult (itérable) |
| Accéder aux données | result[0].data.<register> (BitArray) | result[0]["<register>"] (np.ndarray) |
| Gérer le bruit | Options intégrées | Doit être composé manuellement (annotations, samplex, NoiseLearnerV3) |
Aperçu des étapes de migration
Étape 1. Installer les packages requis
Executor et le modèle d'exécution dirigée nécessitent le package samplomatic :
pip install qiskit qiskit-ibm-runtime samplomatic
# For visualization support:
# pip install samplomatic[vis]
qiskit-ibm-runtimev0.48.0 est recommandé car il ajoute l'optionmeas_level = "both"et le groupe de twirlinglocal_c1.qiskit >= 2.3.0est requis.samplomatic >= 0.18.0est requis.
Étape 2. Changer les imports
Sampler:
from qiskit_ibm_runtime import SamplerV2 as Sampler
Executor:
from qiskit_ibm_runtime import Executor, QuantumProgram
Étape 3. Remplacer les tuples PUB par un QuantumProgram
Au lieu de passer une liste de tuples (PUB), lorsque tu utilises Executor, tu construis un QuantumProgram et tu y ajoutes des éléments.
Un QuantumProgram accepte des éléments de type circuit et samplex :
-
append_circuit_item: Ajoute unCircuitItem, qui est un circuit et (facultativement) ses valeurs de paramètres. Il est exécuté tel quel, sans aucune randomisation.Utilise ceci lorsque tu veux simplement échantillonner un circuit, exactement comme le ferait Sampler avec un PUB sans twirling ; par exemple, lors de la soumission d'un job d'échantillonnage simple, ou lorsque tu as déjà inclus manuellement les variantes que tu souhaites.
-
append_samplex_item: Ajoute unsamplexItem, qui est un circuit modèle plus un samplex qui génère des ensembles de paramètres randomisés côté serveur.Utilise ceci lorsque tu veux que le contenu du circuit soit randomisé. Le cas principal est avec le twirling (de porte ou de mesure) ou l'injection de bruit. Cette capacité remplace le twirling intégré de Sampler.
Un seul QuantumProgram peut accepter les deux types d'éléments ; chaque élément ajouté est exécuté comme une
tâche indépendante et produit sa propre entrée dans les résultats. En général, utilise append_circuit_item lorsque ton circuit n'a pas besoin d'être randomisé. Sinon, utilise append_samplex_item.
Les sections suivantes montrent chacune à tour de rôle : les circuits paramétrés qui utilisent
append_circuit_item, et la migration du twirling en utilisant append_samplex_item.
Dans les exemples de code suivants, isa_circuit fait référence au circuit qui a été transpilé pour se conformer à l'architecture du jeu d'instructions (ISA) du backend cible. Ce isa_circuit contient deux paramètres.
Étape 3a. Migrer les circuits paramétrés
Avec Sampler, les valeurs des paramètres sont le deuxième élément du tuple PUB. Avec Executor,
passe-les en tant que circuit_arguments à 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"]
Étape 3b. Migrer le twirling intégré vers des annotations explicites
C'est le changement le plus important. Sampler applique le twirling à ta place en utilisant des options. Avec Executor, tu déclares cette intention explicitement en utilisant des boîtes annotées et un samplex (depuis Samplomatic).
Sampler (twirling en utilisant des options) :
sampler = Sampler(mode=backend)
sampler.options.twirling.enable_gates = True
sampler.options.twirling.enable_measure = True
Executor (twirling en utilisant des boîtes et un 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
)
Comme le circuit modèle et le samplex sont construits côté client, tu peux les inspecter et les échantillonner localement pour vérifier la sortie avant d'envoyer quoi que ce soit au matériel.
Vérification : Échantillonner le circuit modèle localement
Tu peux tirer des randomisations du samplex et les lier au circuit
modèle pour confirmer que le samplex produit les valeurs de paramètres attendues.
Les valeurs de paramètres renvoyées par samplex.sample sont directement compatibles avec les
paramètres du circuit modèle.
# 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)
Pour aller plus loin, tu peux vérifier que chaque randomisation est logiquement équivalente au
circuit d'origine en, par exemple, convertissant les deux en objets Operator et en comparant leurs implémentations unitaires (après
avoir pris en compte les corrections outputs["measurement_flips.<register>"] qui annulent
le twirling de mesure), ou en comparant des valeurs d'espérance issues d'une exécution locale de
StatevectorSampler ou StatevectorEstimator. Consulte le guide Samplomatic
Entrées et sorties du samplex
pour un tutoriel complet.
Étape 4. Changer comment les shots sont demandés
Déplace les shots du PUB vers QuantumProgram(shots=...). Dans Executor, shots s'applique à l'ensemble du job. Soumets plusieurs jobs si tu as besoin de nombres de shots différents.
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)
Étape 5. Mettre à jour les options si nécessaire
Il y a moins d'options disponibles pour Executor que pour Sampler, car les choix d'atténuation d'erreurs résident maintenant dans tes annotations et ton samplex plutôt que dans les options.
Il existe aussi une différence structurelle dans l'emplacement des paramètres.
-
Avec Sampler, tout, y compris les choix qui affectent le post-traitement des résultats, est configuré dans les options de la primitive ou dans le PUB.
-
Avec Executor, les choix qui influent sur la manière dont les résultats du job sont structurés et post-traités sont définis sur le
QuantumProgram, et non surExecutorOptions.
Examples:
| Sampler | Executor |
|---|---|
shots | QuantumProgram(shots=...) |
meas_type | QuantumProgram(meas_level=...) |
ExecutorOptions ne contient que des paramètres d'exécution et d'environnement de plus bas niveau qui ne modifient pas la structure des données renvoyées. Il comporte trois groupes de premier niveau :
-
environment(EnvironmentOptions) -
execution(ExecutionOptions) : contient moins d'options qu'avec Sampler. Par exemple, il n'y a pas d'option Executormeas_type.
Notamment, les options twirling et dynamical_decoupling existent dans Sampler mais pas dans Executor. À la place, les valeurs de ces options sont exprimées via le modèle d'exécution dirigée.
Example:
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)
Étape 6. Mettre à jour la commande run
L'entrée d'un job Executor est le programme, au lieu de PUB.
Sampler:
# Submit a job
sampler.run([(isa_circuit, parameter_values)])
Executor:
# Submit a job
executor.run(program)
Étape 7. Modifier la façon dont tu accèdes aux résultats
Dans Executor, les résultats sont des tableaux NumPy, et non des objets BitArray. Utilise la chaîne de nom comme index (result[0]["meas"]) pour obtenir un np.ndarray en retour. Il n'est pas nécessaire de se souvenir du chemin d'attribut .data.<register>.
Pour passer de Sampler à Executor, remplace result[i].data.<reg> (BitArray) par result[i]["<reg>"] (np.ndarray), puis réécris le post-traitement basé sur get_counts sous forme d'opérations NumPy.
| Tâche | Sampler | Executor |
|---|---|---|
| Obtenir les données de registre | result[0].data.meas | result[0]["meas"] |
| Type de données | BitArray | np.ndarray |
| Dictionnaire de comptages | result[0].data.meas.get_counts() | Post-traiter le tableau manuellement |
| Registres multiples | result[0].data.<name> par registre | result[0]["<name>"] par registre |
| Forme du tableau CircuitItem | - | (parameter_sets, shots, register_bits) |
| Forme du tableau SamplexItem | - | (randomizations, parameter_sets, shots, register_bits) |
| Annuler le brassage de mesure | Automatique | result[i]["measurement_flips.<name>"] + XOR |
Le BitArray de Sampler propose des assistants (get_counts, slice_bits, slice_shots, expectation_values, et des masques de post-sélection). Executor renvoie des tableaux NumPy bruts afin que tu puisses effectuer ce post-traitement avec des opérations NumPy standard.
Étape 8. Gérer les résultats twirlés (corrections de bit-flip)
Lorsque tu appliques un twirling de mesure via un SamplexItem, Executor renvoie les mesures brutes
(twirlées) ainsi que les corrections de bit-flip nécessaires pour annuler le twirling.
Tu dois les appliquer manuellement ; rien n'est corrigé implicitement.
Lorsque tu utilises Executor, annule le twirling explicitement en utilisant les corrections measurement_flips.<reg> et un XOR, comme le montre l'exemple suivant :
# 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
Il n'y a pas d'étape équivalente dans Sampler, car il annule le twirling pour toi.
Exemple complet : Migrer un job d'échantillonnage basique
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"]