トランスパイラーのステージ
Package versions
このページのコードは、以下の環境で開発されました。 これらのバージョン以上を使用することを推奨します。
qiskit[all]~=2.5.2
このページでは、Qiskit SDKに組み込まれているトランスパイルパイプラインの各ステージについて説明します。ステージは6つあります:
-
init -
layout -
routing -
translation -
optimization -
scheduling
generate_preset_pass_manager 関数は、これらのステージから構成されるプリセットのステージ付きパスマネージャーを生成します。各ステージを構成する具体的なパスは、generate_preset_pass_manager に渡す引数によって異なります。optimization_level は必須の位置引数であり、0、1、2、3 のいずれかの整数で指定します。値が大きいほど最適化が強力になりますが、計算コストも高くなります(トランスパイルのデフォルト設定と構成オプションを参照)。
回路をトランスパイルする推奨方法は、プリセットのステージ付きパスマネージャーを作成し、パスマネージャーによるトランスパイルで説明されているように、そのパスマネージャーを回路に対して実行することです。ただし、よりシンプルでカスタマイズ性の低い代替方法として、transpile関数を使用する方法があります。この関数は回路を直接引数として受け取ります。generate_preset_pass_managerと同様に、使用される具体的なトランスパイラーパスは、transpileに渡されるoptimization_levelなどの引数に依存します。実際、内部ではtranspile関数がgenerate_preset_pass_managerを呼び出してプリセットのステージ付きパスマネージャーを作成し、それを回路に対して実行します。
Init ステージ
最初のステージは、デフォルトではほとんど何も行いません。独自の初期最適化を追加したい場合に主に活用されます。また、多くのレイアウトおよびルーティングアルゴリズムは単一・二量子ビットゲートのみを対象として設計されているため、このステージでは三量子ビット以上のゲートを一・二量子ビットゲートに変換する処理も行われます。
このステージに独自の初期最適化を実装する方法については、プラグインおよびパスマネージャーのカスタマイズに関するセクションを参照してください。
Layout ステージ
次のステージは、回路を送信するバックエンドのレイアウト(接続構造)に関するものです。一般に、量子回路は抽象的な存在であり、その量子ビットは実際の計算に使用される物理的な量子ビットの「仮想」または「論理的」な表現です。ゲートのシーケンスを実行するには、「仮想」量子ビットと実際の量子デバイスの「物理」量子ビットとの一対一の対応付けが必要です。このマッピングは Layout オブジェクトとして保持され、バックエンドの命令セットアーキテクチャ(ISA)で定義される制約の一部です。
マッピングの選択は、入力回路をデバイスのトポロジーにマッピングするために必要なSWAP演算の数を最小限に抑え、最も適切にキャリブレーションされた量子ビットが使用されるようにするために極めて重要です。この段階の重要性から、プリセットパスマネージャーは最適なレイアウトを見つけるためにいくつかの異なる方法を試みます。通常、これには2つのステップが含まれます。まず、「完全な」レイアウト(SWAP演算を全く必要としないレイアウト)を見つけようとし、次に、完全なレイアウトが見つからない場合に使用する最適なレイアウトを見つけようとするヒューリスティック・パスを実行します。この最初のステップには、通常2つのPassesが使用されます。
-
TrivialLayout:各仮想量子ビットを、デバイス上の同じ番号の物理量子ビットに素朴にマッピングします(すなわち、[0,1,1,3] -> [0,1,1,3])。これは、完全なレイアウトを見つけるためにoptimzation_level=1でのみ使用される歴史的な動作です。失敗した場合、次にVF2Layoutが試されます。 -
VF2Layout: このステージをサブグラフ同型問題として扱い、VF2++ アルゴリズムで解くことで理想的なレイアウトを選択するAnalysisPassです。複数のレイアウトが見つかった場合は、スコアリングヒューリスティックを実行して平均エラーが最も低いマッピングを選択します。
ヒューリスティックステージでは、デフォルトで以下の二つのパスが使用されます。
-
DenseLayout:回路と同じ数の量子ビットを持ち、最も接続性の高いデバイスのサブグラフを見つけます(回路内に(IfElseOpなどの)制御フロー演算が存在する場合、optimization level 1で使用されます)。 -
SabreLayout: ランダムな初期レイアウトから出発し、SabreSwapアルゴリズムを繰り返し実行することでレイアウトを選択するパスです。VF2Layoutパスで完全なレイアウトが見つからない場合に、最適化レベル 1、2、3 でのみ使用されます。このアルゴリズムの詳細については、論文 arXiv:1809.02573 を参照してください。
Routing ステージ
量子デバイス上で直接接続されていない量子ビット間の二量子ビットゲートを実行するには、回路に一つ以上の SWAP ゲートを挿入して、量子ビットの状態をデバイスのゲートマップ上で隣接するまで移動させる必要があります。各 SWAP ゲートは高コストでノイズの多い操作です。したがって、回路を所定のデバイスにマッピングするために必要な SWAP ゲートの最小数を求めることは、トランスパイルプロセスにおける重要なステップです。効率化のため、このステージは通常、デフォルトで Layout ステージと一緒に計算されますが、論理的には両者は独立しています。Layout ステージは使用するハードウェア量子ビットを選択し、Routing ステージは選択したレイアウトで回路を実行するために必要な SWAP ゲートを適切な数だけ挿入します。
しかし、最適な SWAP マッピングを求めることは困難です。実際、これは NP 困難問題であり、ごく小さな量子デバイスや入力回路を除けば、計算コストが大きすぎて事実上計算できません。これを回避するため、Qiskit では SabreSwap という確率的なヒューリスティックアルゴリズムを使用して、最適ではないかもしれませんが良質な SWAP マッピングを計算します。確率的な手法を使用するため、生成される回路は実行ごとに同一になるとは限りません。実際、同じ回路を繰り返し実行すると、出力の回路深さとゲート数が分布として現れます。そのため、多くのユーザーはルーティング関数(または StagedPassManager 全体)を何度も実行し、出力の分布から最も深さの小さい回路を選ぶ、という方法をとります。
例として、15 量子ビットの GHZ 回路を、「悪い」(非接続な)initial_layout を使用して 100 回実行してみましょう。
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "qiskit": "qiskit"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import matplotlib.pyplot as plt
from qiskit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.providers.fake_provider import GenericBackendV2
backend = GenericBackendV2(15)
ghz = QuantumCircuit(15)
ghz.h(0)
ghz.cx(0, range(1, 15))
depths = []
for seed in range(100):
pass_manager = generate_preset_pass_manager(
optimization_level=1,
backend=backend,
layout_method="trivial", # Fixed layout mapped in circuit order
seed_transpiler=seed, # For reproducible results
)
depths.append(pass_manager.run(ghz).depth())
plt.figure(figsize=(8, 6))
plt.hist(depths, align="left", color="#AC557C")
plt.xlabel("Depth", fontsize=14)
plt.ylabel("Counts", fontsize=14)
Text(0, 0.5, 'Counts')
この幅広い分布は、SWAP マッパーが最適なマッピングを計算することがいかに難しいかを示しています。より深く理解するために、実行される回路とハードウェア上で選ばれた量子ビットの両方を確認してみましょう。
ghz.draw("mpl", idle_wires=False)
from qiskit.visualization import plot_circuit_layout
# Plot the hardware graph and indicate which hardware qubits were chosen to run the circuit
transpiled_circ = pass_manager.run(ghz)
plot_circuit_layout(transpiled_circ, backend)
ご覧のとおり、この回路は接続グラフ上で非常に遠い位置にある量子ビット 0 と 14 の間で二量子ビットゲートを実行する必要があります。そのため、この回路を実行するには SabreSwap パスですべての二量子ビットゲートを実行できるよう SWAP ゲートを挿入する必要があります。
なお、SabreSwap アルゴリズムは前のステージで使用した SabreLayout メソッドとは異なるものであることにも注意してください。デフォルトでは、SabreLayout はレイアウトとルーティングの両方を実行し、変換後の回路を返します。これはパスの API リファレンスページに記載されているいくつかの技術的な理由によるものです。
Translation ステージ
量子回路を記述する際は、任意の量子ゲート(ユニタリ操作)を自由に使用できます。また、量子ビットの測定やリセット命令などのゲート以外の操作も利用できます。ただし、ほとんどの量子デバイスがネイティブにサポートするのは、ごく少数の量子ゲートおよびゲート以外の操作に限られています。これらのネイティブゲートはターゲットの ISA の定義の一部であり、このプリセット PassManagers のステージは、回路に指定されたゲートを指定されたバックエンドのネイティブ基底ゲートに変換(アンロール)します。これは回路をバックエンドで実行するための重要なステップですが、通常は深さとゲート数の増加を招きます。
特に重要な二つの特殊なケースを取り上げて、このステージが何を行うかを説明します。
- SWAP ゲートがターゲットバックエンドのネイティブゲートでない場合、三つの CNOT ゲートが必要です。
print("native gates:" + str(sorted(backend.operation_names)))
qc = QuantumCircuit(2)
qc.swap(0, 1)
qc.decompose().draw("mpl")
native gates:['cx', 'delay', 'id', 'measure', 'reset', 'rz', 'sx', 'x']
SWAP は三つの CNOT ゲートの積であるため、ノイズの多い量子デバイスでは高コストな操作です。しかし、多くのデバイスが持つ限られたゲート接続性に回路を埋め込むためには、通常こうした操作が必要になります。したがって、回路内の SWAP ゲートの数を最小化することがトランスパイルプロセスの主要な目標の一つです。
- Toffoli ゲート、すなわち controlled-controlled-not ゲート(
ccx)は三量子ビットゲートです。基底ゲートセットに単一・二量子ビットゲートしか含まれていない場合、このゲートを分解する必要があります。ただし、そのコストはかなり大きくなります。
qc = QuantumCircuit(3)
qc.ccx(0, 1, 2)
qc.decompose().draw("mpl")
量子回路内の Toffoli ゲート一つにつき、ハードウェアは最大で六つの CNOT ゲートといくつかの単一量子ビットゲートを実行する可能性があります。この例は、複数の Toffoli ゲートを使用するアルゴリズムは深さの大きな回路になり、ノイズの影響を大きく受けることを示しています。
Optimization ステージ
このステージは、量子回路をターゲットデバイスの基底ゲートセットに分解することを中心としており、Layout ステージと Routing ステージによる深さの増加に対抗する必要があります。幸い、ゲートを結合または除去することで回路を最適化する手法が数多く存在します。場合によっては、これらの手法が非常に効果的であり、レイアウトおよびルーティング後でも出力回路の深さが入力より小さくなることがあります。一方、最適化の余地が少なくノイズの多いデバイスでの実行が困難なケースもあります。このステージで各最適化レベルの違いが現れ始めます。
-
optimization_level=1の場合、このステージはOptimize1qGatesDecompositionとCXCancellationを用意し、単一量子ビットゲートの連鎖を結合し、連続する CNOT ゲートをキャンセルします。 -
optimization_level=2の場合、このステージはCXCancellationの代わりにCommutativeCancellationパスを使用し、可換性を活用して冗長なゲートを削除します。 -
optimization_level=3の場合、このステージは以下のパスを用意します。
さらに、このステージでは回路内のすべての命令がターゲットバックエンドで利用可能な基底ゲートで構成されているかどうかを確認する最終チェックも実行されます。
以下に示す GHZ 状態の例は、最適化レベルの設定が回路深さとゲート数に与える影響を示しています。
トランスパイルの出力は確率的な SWAP マッパーによって異なります。したがって、以下の数値はコードを実行するたびに変わる可能性があります。
以下のコードは、15量子ビットのGHZ状態を構築し、結果として生じる回路の深さ、ゲート数、複数量子ビットゲート数の観点から、トランスパイルのoptimization_levelsを比較します。
ghz = QuantumCircuit(15)
ghz.h(0)
ghz.cx(0, range(1, 15))
depths = []
gate_counts = []
multiqubit_gate_counts = []
levels = [str(x) for x in range(4)]
for level in range(4):
pass_manager = generate_preset_pass_manager(
optimization_level=level,
backend=backend,
seed_transpiler=1234,
)
circ = pass_manager.run(ghz)
depths.append(circ.depth())
gate_counts.append(sum(circ.count_ops().values()))
multiqubit_gate_counts.append(circ.count_ops()["cx"])
fig, (ax1, ax2) = plt.subplots(2, 1)
ax1.bar(levels, depths, label="Depth")
ax1.set_xlabel("Optimization Level")
ax1.set_ylabel("Depth")
ax1.set_title("Output Circuit Depth")
ax2.bar(levels, gate_counts, label="Number of Circuit Operations")
ax2.bar(levels, multiqubit_gate_counts, label="Number of CX gates")
ax2.set_xlabel("Optimization Level")
ax2.set_ylabel("Number of gates")
ax2.legend()
ax2.set_title("Number of output circuit gates")
fig.tight_layout()
plt.show()
Scheduling ステージ
この最後のステージは、明示的に呼び出された場合にのみ実行され(Initステージと同様)、デフォルトでは実行されません(ただし、generate_preset_pass_managerを呼び出す際にscheduling_method引数を設定することで、方式を指定できます)。Scheduling ステージは、通常、回路がターゲット基底に変換され、デバイスにマッピングされ、最適化された後に使用されます。これらのパスは、回路内のすべてのアイドル時間を考慮することに焦点を当てています。大まかに言えば、スケジューリング・パスは、ゲート実行間のアイドル時間を考慮するために遅延命令を明示的に挿入し、回路がバックエンド上でどれくらいの時間実行されるかを調べるものと考えることができます。
以下に例を示します。
ghz = QuantumCircuit(5)
ghz.h(0)
ghz.cx(0, range(1, 5))
# Use fake backend
backend = GenericBackendV2(5)
# Run with optimization level 3 and 'asap' scheduling pass
pass_manager = generate_preset_pass_manager(
optimization_level=3,
backend=backend,
scheduling_method="asap",
seed_transpiler=1234,
)
circ = pass_manager.run(ghz)
circ.draw(output="mpl", idle_wires=False)
トランスパイラーは各量子ビットのアイドル時間を考慮するために Delay 命令を挿入しました。回路のタイミングをより詳しく把握するために、timeline.draw() 関数で表示することもできます。
Scheduling a circuit involves two parts: analysis and constraint mapping, followed by a padding pass. The first part requires running a scheduling analysis pass (by default this is
ALAPSchedulingAnalysis), which analyzes the circuit and records the start time of each instruction in the circuit into a schedule. Once the circuit has an initial schedule, additional passes can be run to account for any timing constraints on the target backend. Finally, a padding pass such as PadDelay or PadDynamicalDecoupling can be executed.
次のステップ
generate_preset_passmanager関数の使い方を学ぶには、トランスパイルのデフォルト設定と構成オプションのトピックから始めてください。- パスマネージャーによるトランスパイルのトピックでトランスパイルについて学習を続けてください。
- トランスパイラー設定の比較ガイドをお試しください。
- Transpile API ドキュメントを参照してください。