Skip to content

Commit

Permalink
Add config option to leverage all cores for sabre (#12780)
Browse files Browse the repository at this point in the history
* Add config option to leverage all cores for sabre

By default when running sabre in parallel we use a fixed number of
threads (depending on optimization level). This was a tradeoff made for
having deterministic results across multiple systems with a fixed seed
set. However when running qiskit on systems with a lot of CPUs
available we're leaving potential performance on the table by not using
all the available cores. This new flag lets users opt-in to running
sabre with n trials for n CPUs to potentially get better output results
from the transpiler, with minimal to no runtime overhead, at the cost of
the results not necessarily being reproducible when run on a different
computer.

* Apply suggestions from code review

Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com>

* Rework logic to use the default if larger than CPU_COUNT

This commit refactors the logic added in the previous commit to a single
helper function. This reduces the code duplication and makes it easier
to work with. While doing this the logic has been updated so that when
the flag is set and the default number of trials is larger than the
CPU_COUNT we use the default. This means the logic when the flag is set
is to run `max(default_trials, CPU_COUNT)` which should better match
user expectations around the flag.

---------

Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com>
  • Loading branch information
mtreinish and ElePT authored Jul 29, 2024
1 parent 7cd2c41 commit f8ac2ad
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 18 deletions.
70 changes: 52 additions & 18 deletions qiskit/transpiler/preset_passmanagers/builtin_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

"""Built-in transpiler stage plugins for preset pass managers."""

import os

from qiskit.transpiler.passmanager import PassManager
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.passes import BasicSwap
Expand Down Expand Up @@ -63,6 +65,10 @@
SXGate,
SXdgGate,
)
from qiskit.utils.parallel import CPU_COUNT
from qiskit import user_config

CONFIG = user_config.get_config()


class DefaultInitPassManager(PassManagerStagePlugin):
Expand Down Expand Up @@ -397,11 +403,12 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana
pass_manager_config.initial_layout,
)
if optimization_level == 0:
trial_count = _get_trial_count(5)
routing_pass = SabreSwap(
coupling_map_routing,
heuristic="basic",
seed=seed_transpiler,
trials=5,
trials=trial_count,
)
return common.generate_routing_passmanager(
routing_pass,
Expand All @@ -411,11 +418,12 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana
use_barrier_before_measurement=True,
)
if optimization_level == 1:
trial_count = _get_trial_count(5)
routing_pass = SabreSwap(
coupling_map_routing,
heuristic="decay",
seed=seed_transpiler,
trials=5,
trials=trial_count,
)
return common.generate_routing_passmanager(
routing_pass,
Expand All @@ -429,11 +437,13 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana
use_barrier_before_measurement=True,
)
if optimization_level == 2:
trial_count = _get_trial_count(20)

routing_pass = SabreSwap(
coupling_map_routing,
heuristic="decay",
seed=seed_transpiler,
trials=10,
trials=trial_count,
)
return common.generate_routing_passmanager(
routing_pass,
Expand All @@ -446,11 +456,12 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana
use_barrier_before_measurement=True,
)
if optimization_level == 3:
trial_count = _get_trial_count(20)
routing_pass = SabreSwap(
coupling_map_routing,
heuristic="decay",
seed=seed_transpiler,
trials=20,
trials=trial_count,
)
return common.generate_routing_passmanager(
routing_pass,
Expand Down Expand Up @@ -737,12 +748,15 @@ def _swap_mapped(property_set):
max_trials=2500, # Limits layout scoring to < 600ms on ~400 qubit devices
)
layout.append(ConditionalController(choose_layout_1, condition=_layout_not_perfect))

trial_count = _get_trial_count(5)

choose_layout_2 = SabreLayout(
coupling_map,
max_iterations=2,
seed=pass_manager_config.seed_transpiler,
swap_trials=5,
layout_trials=5,
swap_trials=trial_count,
layout_trials=trial_count,
skip_routing=pass_manager_config.routing_method is not None
and pass_manager_config.routing_method != "sabre",
)
Expand All @@ -769,12 +783,15 @@ def _swap_mapped(property_set):
layout.append(
ConditionalController(choose_layout_0, condition=_choose_layout_condition)
)

trial_count = _get_trial_count(20)

choose_layout_1 = SabreLayout(
coupling_map,
max_iterations=2,
seed=pass_manager_config.seed_transpiler,
swap_trials=20,
layout_trials=20,
swap_trials=trial_count,
layout_trials=trial_count,
skip_routing=pass_manager_config.routing_method is not None
and pass_manager_config.routing_method != "sabre",
)
Expand All @@ -801,12 +818,15 @@ def _swap_mapped(property_set):
layout.append(
ConditionalController(choose_layout_0, condition=_choose_layout_condition)
)

trial_count = _get_trial_count(20)

choose_layout_1 = SabreLayout(
coupling_map,
max_iterations=4,
seed=pass_manager_config.seed_transpiler,
swap_trials=20,
layout_trials=20,
swap_trials=trial_count,
layout_trials=trial_count,
skip_routing=pass_manager_config.routing_method is not None
and pass_manager_config.routing_method != "sabre",
)
Expand Down Expand Up @@ -902,42 +922,50 @@ def _swap_mapped(property_set):
layout = PassManager()
layout.append(_given_layout)
if optimization_level == 0:
trial_count = _get_trial_count(5)

layout_pass = SabreLayout(
coupling_map,
max_iterations=1,
seed=pass_manager_config.seed_transpiler,
swap_trials=5,
layout_trials=5,
swap_trials=trial_count,
layout_trials=trial_count,
skip_routing=pass_manager_config.routing_method is not None
and pass_manager_config.routing_method != "sabre",
)
elif optimization_level == 1:
trial_count = _get_trial_count(5)

layout_pass = SabreLayout(
coupling_map,
max_iterations=2,
seed=pass_manager_config.seed_transpiler,
swap_trials=5,
layout_trials=5,
swap_trials=trial_count,
layout_trials=trial_count,
skip_routing=pass_manager_config.routing_method is not None
and pass_manager_config.routing_method != "sabre",
)
elif optimization_level == 2:
trial_count = _get_trial_count(20)

layout_pass = SabreLayout(
coupling_map,
max_iterations=2,
seed=pass_manager_config.seed_transpiler,
swap_trials=20,
layout_trials=20,
swap_trials=trial_count,
layout_trials=trial_count,
skip_routing=pass_manager_config.routing_method is not None
and pass_manager_config.routing_method != "sabre",
)
elif optimization_level == 3:
trial_count = _get_trial_count(20)

layout_pass = SabreLayout(
coupling_map,
max_iterations=4,
seed=pass_manager_config.seed_transpiler,
swap_trials=20,
layout_trials=20,
swap_trials=trial_count,
layout_trials=trial_count,
skip_routing=pass_manager_config.routing_method is not None
and pass_manager_config.routing_method != "sabre",
)
Expand All @@ -957,3 +985,9 @@ def _swap_mapped(property_set):
embed = common.generate_embed_passmanager(coupling_map)
layout.append(ConditionalController(embed.to_flow_controller(), condition=_swap_mapped))
return layout


def _get_trial_count(default_trials=5):
if CONFIG.get("sabre_all_threads", None) or os.getenv("QISKIT_SABRE_ALL_THREADS"):
return max(CPU_COUNT, default_trials)
return default_trials
9 changes: 9 additions & 0 deletions qiskit/user_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class UserConfig:
transpile_optimization_level = 1
parallel = False
num_processes = 4
sabre_all_threads = true
"""

Expand Down Expand Up @@ -168,6 +169,13 @@ def read_config_file(self):
)
self.settings["num_processes"] = num_processes

# Parse sabre_all_threads
sabre_all_threads = self.config_parser.getboolean(
"default", "sabre_all_threads", fallback=None
)
if sabre_all_threads is not None:
self.settings["sabre_all_threads"] = sabre_all_threads


def set_config(key, value, section=None, file_path=None):
"""Adds or modifies a user configuration
Expand Down Expand Up @@ -208,6 +216,7 @@ def set_config(key, value, section=None, file_path=None):
"transpile_optimization_level",
"parallel",
"num_processes",
"sabre_all_threads",
}

if section in [None, "default"]:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
features_transpiler:
- |
Added a new user config file option ``sabre_all_threads`` and a
corresponding environment variable ``QISKIT_SABRE_ALL_THREADS``. When this
flag is set the preset pass managers will run the :class:`.SabreLayout`
and :class:`.SabreSwap` transpiler passes using all the available
CPUs on the local system. Using this option is a tradeoff between
determinism of output between different computers and potentially better
output with fewer :class:`.SwapGate`\s.
These transpiler passes run multiple random trials in parallel and pick
the output which results in the fewest :class:`.SwapGate`\s. As a rule of
thumb, if you run more trials, this provides the algorithm more opportunities
to find a better result. By default, the preset pass managers use a fixed
number of trials, in this release 5 trials for levels 0 and 1, and 20
trials for levels 2 and 3, but these numbers may change in future releases
(and were different in historical releases). Using a fixed number of
trials results in deterministic results regardless of the local system,
because even with a fixed seed if you were to default to the number of
local CPUs available the results would different when running between
different computers.
If the default number of trials for a given optimization level is higher
than the number of local CPUs it will use the optimization level default
which is higher.

0 comments on commit f8ac2ad

Please sign in to comment.