Skip to content

Commit

Permalink
Enable multiple parallel seed trials for SabreSwap (#8572)
Browse files Browse the repository at this point in the history
* Enable multiple parallel seed trials for SabreSwap

The SabreSwap algorithm's output is quite linked to the random seed used
to run the algorithm. Typically to get the best result a user will run
the pass (or the full transpilation) multiple times with different seeds
and pick the best output to get a better result. Since #8388 the
SabreSwap pass has moved mostly the domain of Rust. This enables us to
leverage multithreading easily to run parallel sabre over multiple seeds
and pick the best result. This commit adds a new argument trials to the
SabreSwap pass which is used to specify the number of random seed trials
to run sabre with. Each trial will perform a complete run of the sabre
algorithm and compute the swaps necessary for the algorithm. Then the
result with the least number of swaps will be selected and used as the
swap mapping for the pass.

* Make parallel case fully deterministic

The parallel trial code was potentially non-deterministic in it's
execution because the way the parallel trials were compared was
dependent on execution speed of the pass. This could result in a
different output if results with equal number of swaps finished
executing in differing amounts of time between runs. This commit
addresses this by first collecting the results into an ordered Vec
first which is then iterated over serially to find the minimum swap
count. This will make the output independent of execution speed of the
individual trials.

* Fix test failures

This commit updates tests which started to fail because of the different
RNG behavior used by the parallel SabreSwap seed trials. For the most
part these are just mechanical changes that either changed the expected
layout with a fixed seed or updating a fixed seed so the output matches
the expected result. The one interesting case was the
TestTranspileLevelsSwap change which was caused by different swaps being
inserted that for optimization level enabled the 2q block optimization
passes to synthesize away the swap as part of its optimization. This was
fixed by changing the seed, but it was a different case than the other
failures.

* Add swap_trials argument to SabreLayout

This commit adds a swap_trials argument to the SabreLayout pass so that
users can control how many trials to run in SabreSwap internally. This
is necessary for reproducibility between systems for the same reason
it's required on SabreSwap.

* Add comment explaining the intermediate Vec usage

* Update layout in new test

* Update releasenotes/notes/multiple-parallel-rusty-sabres-32bc93f79ae48a1f.yaml

Co-authored-by: Kevin Hartman <[email protected]>

* Remove intermediate Vec for parallel trials

In an earlier commit we switched the parallel iterator to collect into
an intermediate `Vec` to ensure the output result was deterministic. The
min_by_key() will have a degree of non-determinism for equal entries as
the parallel iterator's threads finish. However, collecting to a Vec
isn't necessary as we can use the index as an element in a 2 element
array we can get the deterministic evaluation and avoid the overhead of
collecting into a `Vec`. This commit makes this change to improve the
performance of the parallel execution path.

Co-authored-by: Kevin Hartman <[email protected]>

Co-authored-by: Kevin Hartman <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 1, 2022
1 parent a0db581 commit 2ab0ae5
Show file tree
Hide file tree
Showing 12 changed files with 214 additions and 67 deletions.
28 changes: 26 additions & 2 deletions qiskit/transpiler/passes/layout/sabre_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,42 @@ class SabreLayout(AnalysisPass):
`arXiv:1809.02573 <https://arxiv.org/pdf/1809.02573.pdf>`_
"""

def __init__(self, coupling_map, routing_pass=None, seed=None, max_iterations=3):
def __init__(
self, coupling_map, routing_pass=None, seed=None, max_iterations=3, swap_trials=None
):
"""SabreLayout initializer.
Args:
coupling_map (Coupling): directed graph representing a coupling map.
routing_pass (BasePass): the routing pass to use while iterating.
This is mutually exclusive with the ``swap_trials`` argument and
if both are set an error will be raised.
seed (int): seed for setting a random first trial layout.
max_iterations (int): number of forward-backward iterations.
swap_trials (int): The number of trials to run of
:class:`~.SabreSwap` for each iteration. This is equivalent to
the ``trials`` argument on :class:`~.SabreSwap`. If this is not
specified (and ``routing_pass`` isn't set) by default the number
of physical CPUs on your local system will be used. For
reproducibility between environments it is best to set this
to an explicit number because the output will potentially depend
on the number of trials run. This option is mutually exclusive
with the ``routing_pass`` argument and an error will be raised
if both are used.
Raises:
TranspilerError: If both ``routing_pass`` and ``swap_trials`` are
specified
"""
super().__init__()
self.coupling_map = coupling_map
if routing_pass is not None and swap_trials is not None:
raise TranspilerError("Both routing_pass and swap_trials can't be set at the same time")
self.routing_pass = routing_pass
self.seed = seed
self.max_iterations = max_iterations
self.trials = swap_trials
self.swap_trials = swap_trials

def run(self, dag):
"""Run the SabreLayout pass on `dag`.
Expand All @@ -86,7 +108,9 @@ def run(self, dag):
initial_layout = Layout({q: dag.qubits[i] for i, q in enumerate(physical_qubits)})

if self.routing_pass is None:
self.routing_pass = SabreSwap(self.coupling_map, "decay", seed=self.seed, fake_run=True)
self.routing_pass = SabreSwap(
self.coupling_map, "decay", seed=self.seed, fake_run=True, trials=self.swap_trials
)
else:
self.routing_pass.fake_run = True

Expand Down
26 changes: 19 additions & 7 deletions qiskit/transpiler/passes/routing/sabre_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.layout import Layout
from qiskit.dagcircuit import DAGOpNode
from qiskit.tools.parallel import CPU_COUNT

# pylint: disable=import-error
from qiskit._accelerate.sabre_swap import (
Expand Down Expand Up @@ -61,20 +62,19 @@ class SabreSwap(TransformationPass):
scored according to some heuristic cost function. The best SWAP is
implemented and ``current_layout`` updated.
This transpiler pass adds onto the SABRE algorithm in that it will run
multiple trials of the algorithm with different seeds. The best output,
deteremined by the trial with the least amount of SWAPed inserted, will
be selected from the random trials.
**References:**
[1] Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem
for NISQ-era quantum devices." ASPLOS 2019.
`arXiv:1809.02573 <https://arxiv.org/pdf/1809.02573.pdf>`_
"""

def __init__(
self,
coupling_map,
heuristic="basic",
seed=None,
fake_run=False,
):
def __init__(self, coupling_map, heuristic="basic", seed=None, fake_run=False, trials=None):
r"""SabreSwap initializer.
Args:
Expand All @@ -84,6 +84,12 @@ def __init__(
seed (int): random seed used to tie-break among candidate swaps.
fake_run (bool): if true, it only pretend to do routing, i.e., no
swap is effectively added.
trials (int): The number of seed trials to run sabre with. These will
be run in parallel (unless the PassManager is already running in
parallel). If not specified this defaults to the number of physical
CPUs on the local system. For reproducible results it is recommended
that you set this explicitly, as the output will be deterministic for
a fixed number of trials.
Raises:
TranspilerError: If the specified heuristic is not valid.
Expand Down Expand Up @@ -158,6 +164,11 @@ def __init__(
self.seed = np.random.default_rng(None).integers(0, ii32.max, dtype=int)
else:
self.seed = seed
if trials is None:
self.trials = CPU_COUNT
else:
self.trials = trials

self.fake_run = fake_run
self._qubit_indices = None
self._clbit_indices = None
Expand Down Expand Up @@ -216,6 +227,7 @@ def run(self, dag):
self.heuristic,
self.seed,
layout,
self.trials,
)

layout_mapping = layout.layout_mapping()
Expand Down
16 changes: 12 additions & 4 deletions qiskit/transpiler/preset_passmanagers/builtin_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana
pass_manager_config.initial_layout,
)
if optimization_level == 0:
routing_pass = SabreSwap(coupling_map, heuristic="basic", seed=seed_transpiler)
routing_pass = SabreSwap(
coupling_map, heuristic="basic", seed=seed_transpiler, trials=5
)
return common.generate_routing_passmanager(
routing_pass,
target,
Expand All @@ -215,7 +217,9 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana
use_barrier_before_measurement=True,
)
if optimization_level == 1:
routing_pass = SabreSwap(coupling_map, heuristic="lookahead", seed=seed_transpiler)
routing_pass = SabreSwap(
coupling_map, heuristic="lookahead", seed=seed_transpiler, trials=5
)
return common.generate_routing_passmanager(
routing_pass,
target,
Expand All @@ -227,7 +231,9 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana
use_barrier_before_measurement=True,
)
if optimization_level == 2:
routing_pass = SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler)
routing_pass = SabreSwap(
coupling_map, heuristic="decay", seed=seed_transpiler, trials=10
)
return common.generate_routing_passmanager(
routing_pass,
target,
Expand All @@ -238,7 +244,9 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana
use_barrier_before_measurement=True,
)
if optimization_level == 3:
routing_pass = SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler)
routing_pass = SabreSwap(
coupling_map, heuristic="decay", seed=seed_transpiler, trials=20
)
return common.generate_routing_passmanager(
routing_pass,
target,
Expand Down
4 changes: 3 additions & 1 deletion qiskit/transpiler/preset_passmanagers/level0.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ def _choose_layout_condition(property_set):
elif layout_method == "noise_adaptive":
_choose_layout = NoiseAdaptiveLayout(backend_properties)
elif layout_method == "sabre":
_choose_layout = SabreLayout(coupling_map, max_iterations=1, seed=seed_transpiler)
_choose_layout = SabreLayout(
coupling_map, max_iterations=1, seed=seed_transpiler, swap_trials=5
)

toqm_pass = False
# Choose routing pass
Expand Down
4 changes: 3 additions & 1 deletion qiskit/transpiler/preset_passmanagers/level1.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ def _vf2_match_not_found(property_set):
elif layout_method == "noise_adaptive":
_improve_layout = NoiseAdaptiveLayout(backend_properties)
elif layout_method == "sabre":
_improve_layout = SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler)
_improve_layout = SabreLayout(
coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5
)

toqm_pass = False
routing_pm = None
Expand Down
4 changes: 3 additions & 1 deletion qiskit/transpiler/preset_passmanagers/level2.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ def _vf2_match_not_found(property_set):
elif layout_method == "noise_adaptive":
_choose_layout_1 = NoiseAdaptiveLayout(backend_properties)
elif layout_method == "sabre":
_choose_layout_1 = SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler)
_choose_layout_1 = SabreLayout(
coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=10
)

toqm_pass = False
routing_pm = None
Expand Down
4 changes: 3 additions & 1 deletion qiskit/transpiler/preset_passmanagers/level3.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ def _vf2_match_not_found(property_set):
elif layout_method == "noise_adaptive":
_choose_layout_1 = NoiseAdaptiveLayout(backend_properties)
elif layout_method == "sabre":
_choose_layout_1 = SabreLayout(coupling_map, max_iterations=4, seed=seed_transpiler)
_choose_layout_1 = SabreLayout(
coupling_map, max_iterations=4, seed=seed_transpiler, swap_trials=20
)

toqm_pass = False
# TODO: Remove when qiskit-toqm has it's own plugin and we can rely on just the plugin interface
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
features:
- |
The :class:`~.SabreSwap` transpiler pass has a new keyword argument on its
constructor, ``trials``. The ``trials`` argument is used to specify the
number of random seed trials to attempt. The output from the
`SABRE algorithm <https://arxiv.org/abs/1809.02573>`__ can differ greatly
based on the seed used for the random number. :class:`~.SabreSwap` will
now run the algorithm with ``trials`` number of random seeds and pick the
best (with the fewest swaps inserted). If ``trials`` is not specified the
pass will default to use the number of physical CPUs on the local system.
- |
The :class:`~.SabreLayout` transpiler pass has a new keyword argument on
its constructor, ``swap_trials``. The ``swap_trials`` argument is used
to specify how many random seed trials to run on the :class:`~.SabreSwap`
pass internally. It corresponds to the ``trials`` arugment on the
:class:`~.SabreSwap` pass. When set, each iteration of
:class:`~.SabreSwap` will be run internally ``swap_trials`` times.
If ``swap_trials`` is not specified the will default to use
the number of physical CPUs on the local system.
Loading

0 comments on commit 2ab0ae5

Please sign in to comment.