Aller au contenu principal

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.

Version bêta

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 AerSampler dans qiskit-aer pour 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é dans qiskit-ibm-runtime v0.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).

Correspondance conceptuelle

Le tableau suivant montre comment les concepts de Sampler correspondent à Executor.

ConceptSamplerExecutor
Importfrom qiskit_ibm_runtime import SamplerV2from qiskit_ibm_runtime import Executor
EntréeListe de PUB (tuples)Un QuantumProgram d'objets QuantumProgramItem
Circuit et paramètrestuple (circuit, params, shots)program.append_circuit_item(circuit, circuit_arguments=...)
TwirlingTwirlingOptionsExplicite via des boîtes annotées et un samplex (append_samplex_item)
Appel d'exécutionsampler.run([pub, ...])executor.run(program)
Type de résultatPrimitiveResult de SamplerPubResultQuantumProgramResult (itérable)
Accéder aux donnéesresult[0].data.<register> (BitArray)result[0]["<register>"] (np.ndarray)
Gérer le bruitOptions intégréesDoit être composé manuellement (annotations, samplex, NoiseLearnerV3)

Aperçu des étapes de migration

  1. Installer Samplomatic.

  2. Changer les imports.

  3. Remplacer les tuples PUB.

  4. Changer comment les shots sont exprimés.

  5. Mettre à jour les autres options si nécessaire.

  6. Mettre à jour la commande run.

  7. Mettre à jour l'analyse des résultats.

  8. Annuler le twirling.

É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]
Notes de version
  • qiskit-ibm-runtime v0.48.0 est recommandé car il ajoute l'option meas_level = "both" et le groupe de twirling local_c1.
  • qiskit >= 2.3.0 est requis.
  • samplomatic >= 0.18.0 est 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 un CircuitItem, 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 un samplexItem, 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 sur ExecutorOptions.

Examples:

SamplerExecutor
shotsQuantumProgram(shots=...)
meas_typeQuantumProgram(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 :

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âcheSamplerExecutor
Obtenir les données de registreresult[0].data.measresult[0]["meas"]
Type de donnéesBitArraynp.ndarray
Dictionnaire de comptagesresult[0].data.meas.get_counts()Post-traiter le tableau manuellement
Registres multiplesresult[0].data.<name> par registreresult[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 mesureAutomatiqueresult[i]["measurement_flips.<name>"] + XOR
remarque

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"]

Étapes suivantes