From 175360fc8bc93c19e465f99ffadb76bae64dbed8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 29 Jul 2024 08:50:08 -0400 Subject: [PATCH] Add config option to leverage all cores for sabre (#12780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> (cherry picked from commit f8ac2ad22c1ada79e2c58ba1a584b95884692140) --- .../preset_passmanagers/builtin_plugins.py | 70 ++++++++++++++----- qiskit/user_config.py | 9 +++ ...e-all-threads-option-ad4ff7a4d045cb2b.yaml | 26 +++++++ 3 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/add-sabre-all-threads-option-ad4ff7a4d045cb2b.yaml diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index f85b4d113c17..5e42c7ba3e3f 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -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 @@ -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): @@ -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, @@ -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, @@ -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, @@ -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, @@ -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", ) @@ -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", ) @@ -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", ) @@ -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", ) @@ -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 diff --git a/qiskit/user_config.py b/qiskit/user_config.py index 0ca52fc5c8c0..22d12406b348 100644 --- a/qiskit/user_config.py +++ b/qiskit/user_config.py @@ -35,6 +35,7 @@ class UserConfig: transpile_optimization_level = 1 parallel = False num_processes = 4 + sabre_all_threads = true """ @@ -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 @@ -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"]: diff --git a/releasenotes/notes/add-sabre-all-threads-option-ad4ff7a4d045cb2b.yaml b/releasenotes/notes/add-sabre-all-threads-option-ad4ff7a4d045cb2b.yaml new file mode 100644 index 000000000000..24b64a28703e --- /dev/null +++ b/releasenotes/notes/add-sabre-all-threads-option-ad4ff7a4d045cb2b.yaml @@ -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.