Skip to content

Add debug logging for threading and parallelization dec… #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions crates/accelerate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,43 @@ pub mod results;
pub mod sampled_exp_val;
pub mod twirling;
pub mod uc_gate;
pub mod unitary_synthesis;
pub mod utils;
pub mod vf2_layout;

mod rayon_ext;
#[cfg(test)]
mod test;
mod unitary_compose;

#[inline]
pub fn getenv_use_multiple_threads() -> bool {
let parallel_context = env::var("QISKIT_IN_PARALLEL")
.unwrap_or_else(|_| "FALSE".to_string())
.to_uppercase()
== "TRUE";
let force_threads = env::var("QISKIT_FORCE_THREADS")
.unwrap_or_else(|_| "FALSE".to_string())
.to_uppercase()
== "TRUE";

let result = !parallel_context || force_threads;

// Log threading decision if debug logging is enabled
if env::var("QISKIT_DEBUG_THREADING")
.unwrap_or_else(|_| "FALSE".to_string())
.to_uppercase()
== "TRUE"
{
eprintln!(
Copy link
Preview

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Using eprintln! for debug output is inconsistent with typical Rust logging practices. Consider using the log crate to emit debug-level messages and integrate with existing logging infrastructure.

Copilot uses AI. Check for mistakes.

"Rust threading decision: {} (parallel_context={}, force_threads={})",
if result { "MULTI_THREADED" } else { "SINGLE_THREADED" },
parallel_context,
force_threads
);
}

result
}
import_exception!(qiskit.exceptions, QiskitError);
import_exception!(qiskit.circuit.exceptions, CircuitError);
12 changes: 10 additions & 2 deletions qiskit/passmanager/passmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import dill

from qiskit.utils.parallel import parallel_map, should_run_in_parallel
from qiskit.utils.parallel import parallel_map, should_run_in_parallel, CPU_COUNT
from .base_tasks import Task, PassManagerIR
from .exceptions import PassManagerError
from .flow_controllers import FlowControllerLinear
Expand Down Expand Up @@ -233,7 +233,12 @@ def callback_func(**kwargs):

# If we're not going to run in parallel, we want to avoid spending time `dill` serializing
# ourselves, since that can be quite expensive.
if len(in_programs) == 1 or not should_run_in_parallel(num_processes):
use_parallel = should_run_in_parallel(num_processes)
if len(in_programs) == 1 or not use_parallel:
if len(in_programs) == 1:
logger.debug("PassManager running single program serially")
else:
logger.debug("PassManager running %d programs serially (parallel disabled)", len(in_programs))
out = [
_run_workflow(
program=program,
Expand All @@ -251,6 +256,9 @@ def callback_func(**kwargs):
del callback
del kwargs

logger.debug("PassManager running %d programs in parallel with %d processes",
Copy link
Preview

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serial execution branch assigns to out but lacks a return, so execution always falls through to the parallel path. Add a return statement inside the if-block to avoid unintended parallel runs.

Copilot uses AI. Check for mistakes.

len(in_programs), num_processes or CPU_COUNT)

# Pass manager may contain callable and we need to serialize through dill rather than pickle.
# See https://github.com/Qiskit/qiskit-terra/pull/3290
# Note that serialized object is deserialized as a different object.
Expand Down
16 changes: 13 additions & 3 deletions qiskit/transpiler/preset_passmanagers/builtin_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@

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

import logging
import os

logger = logging.getLogger(__name__)

from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayout
from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries
from qiskit.transpiler.passmanager import PassManager
Expand Down Expand Up @@ -1039,9 +1042,16 @@ def _swap_mapped(property_set):


def _get_trial_count(default_trials=5):
if CONFIG.get("sabre_all_threads", None) or os.getenv("QISKIT_SABRE_ALL_THREADS"):
return max(default_num_processes(), default_trials)
return default_trials
use_all_threads = CONFIG.get("sabre_all_threads", None) or os.getenv("QISKIT_SABRE_ALL_THREADS")
if use_all_threads:
trial_count = max(CPU_COUNT, default_trials)
logger.debug("SABRE using all threads: %d trials (CPU_COUNT=%d, default=%d)",
trial_count, CPU_COUNT, default_trials)
return trial_count
else:
logger.debug("SABRE using default thread configuration: %d trials", default_trials)
return default_trials



class CliffordTOptimizationPassManager(PassManagerStagePlugin):
Expand Down
78 changes: 63 additions & 15 deletions qiskit/utils/parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@

from __future__ import annotations

import logging
import contextlib
import functools
import multiprocessing

import os
import platform
import sys
Expand All @@ -65,6 +67,8 @@

from qiskit import user_config

logger = logging.getLogger(__name__)


CONFIG = user_config.get_config()

Expand Down Expand Up @@ -186,8 +190,28 @@ def should_run_in_parallel(num_processes: int | None = None) -> bool:
.. autofunction:: qiskit.utils::should_run_in_parallel.ignore_user_settings

Args:
num_processes: the maximum number of processes requested for use (``None`` implies the
default).
num_processes: the number of processes requested for use (if given).
"""
num_processes = CPU_COUNT if num_processes is None else num_processes
in_parallel = os.getenv("QISKIT_IN_PARALLEL", "FALSE") == "TRUE"
parallel_enabled = CONFIG.get("parallel_enabled", PARALLEL_DEFAULT)

result = (
num_processes > 1
and not in_parallel
and parallel_enabled
)

logger.debug(
"Parallelization decision: %s (num_processes=%d, in_parallel=%s, parallel_enabled=%s)",
"PARALLEL" if result else "SERIAL",
num_processes,
in_parallel,
parallel_enabled
)

return result


Examples:
Temporarily override the configured settings to disable parallelism::
Expand Down Expand Up @@ -303,16 +327,40 @@ def func(_):
"""
task_kwargs = {} if task_kwargs is None else task_kwargs
if num_processes is None:
num_processes = default_num_processes()
if len(values) < 2 or not should_run_in_parallel(num_processes):
return [task(value, *task_args, **task_kwargs) for value in values]
work_items = ((task, value, task_args, task_kwargs) for value in values)

# This isn't a user-set variable; we set this to talk to our own child processes.
previous_in_parallel = os.getenv("QISKIT_IN_PARALLEL", _IN_PARALLEL_ALLOW_PARALLELISM)
os.environ["QISKIT_IN_PARALLEL"] = _IN_PARALLEL_FORBID_PARALLELISM
try:
with ProcessPoolExecutor(max_workers=num_processes) as executor:
return list(executor.map(_task_wrapper, work_items))
finally:
os.environ["QISKIT_IN_PARALLEL"] = previous_in_parallel
num_processes = CPU_COUNT
if len(values) == 0:
return []
if len(values) == 1:
return [task(values[0], *task_args, **task_kwargs)]

if should_run_in_parallel(num_processes):
logger.debug("Executing parallel_map with %d processes for %d items", num_processes, len(values))
os.environ["QISKIT_IN_PARALLEL"] = "TRUE"
try:
results = []
with ProcessPoolExecutor(max_workers=num_processes) as executor:
param = ((task, value, task_args, task_kwargs) for value in values)
future = executor.map(_task_wrapper, param)

results = list(future)
logger.debug("Parallel execution completed successfully")

except (KeyboardInterrupt, Exception) as error:
logger.debug("Parallel execution failed: %s", str(error))
if isinstance(error, KeyboardInterrupt):
os.environ["QISKIT_IN_PARALLEL"] = "FALSE"
raise QiskitError("Keyboard interrupt in parallel_map.") from error
# Otherwise just reset parallel flag and error
os.environ["QISKIT_IN_PARALLEL"] = "FALSE"
raise error

os.environ["QISKIT_IN_PARALLEL"] = "FALSE"
return results

logger.debug("Executing parallel_map serially for %d items", len(values))
results = []
for _, value in enumerate(values):
result = task(value, *task_args, **task_kwargs)
results.append(result)
return results