Increasing value of `OMP_NUM_THREADS` reduces performance even when controlling for `n_workers` and `threads_per_worker`
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 35/100
- Issue 类型
- 缺陷
- 描述清晰度
- 基本清楚
- 活跃度
- 停滞
- 技术栈
- numpy, python
调研方向
首先运行使用 LocalCluster、Client 和 op 函数的自包含 Python 示例,然后比较 ThreadPoolExecutor、ProcessPoolExecutor 和 distributed 的耗时。检查 LocalCluster 的环境处理和 client.submit 如何使用 OMP_NUM_THREADS 执行 NumPy 操作。完成的标准是确定线程数增加会导致性能下降的原因,并添加一个有针对性的回归检查或记录解决方案。
由索引模型根据 Issue 内容生成。
描述
Description:
I am working with external libraries that rely on NumPy's internal parallelization for certain heavy operations (like .dot between large matrices). I would like to distribute a certain number of these large calculations with distributed, but I encounter bad performance. In particular, when I change OMP_NUM_THREADS (and related variables) to the number of desired threads, performance gets worse!
I make sure not to oversubscribe the CPU, because I explicitly balance the number of OMP threads with the number of workers/worker threads, to match the system number of cores.
The example here below shows using one worker with one thread.
The performance of distributed is already underwhelming with one thread (even compared to the ProcessPoolExecutor), but with more threads it gets progressively worse.
As a side note, the CPU utilization on the system does indeed raise to the number of threads selected, despite the lower performance.
I am not aware of any other issue open on this subject or similar subjects.
Minimal Complete Verifiable Example:
(The commented lines were used to make sure the correct value of the variables were used, and yes, tqdm should not be used for performance assessment, but the difference is pretty clear, it is just a convenience tool)
from distributed import Client, get_worker, LocalCluster
from mkl import get_max_threads
import numpy as np
from tqdm.auto import tqdm
from numpy.typing import NDArray
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
NUM_THREADS = 16
def get_env(num_threads: int = NUM_THREADS) -> dict[str, str]:
return {var: f"{num_threads}" for var in ["OMP_NUM_THREADS", "MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS"]}
def op(M: NDArray, y: NDArray, ii: int) -> float:
# try:
# print(f"{ii = } - {get_worker() = }, {get_max_threads() = }")
# except:
# print(f"{ii = } - {get_max_threads() = }")
x = np.zeros(5_000)
n1 = np.abs(M).dot(np.ones_like(x))
n2 = np.abs(M).T.dot(np.ones_like(y))
for _ in range(1_000):
x += M.T.dot((y - M.dot(x)) / n1) / n2
return float(np.linalg.norm(y - M.dot(x)))
if __name__ == "__main__":
M = np.random.randn(500, 5_000)
y = np.random.randn(500)
N_TRIES = 11
res_f = [op(M, y, ii) for ii in tqdm(range(N_TRIES), desc="For loop")]
with ThreadPoolExecutor(max_workers=1) as executor:
futures = [executor.submit(op, M, y, ii) for ii in range(N_TRIES)]
res_d = [f.result() for f in tqdm(futures, desc=f"ThreadPoolExecutor ({NUM_THREADS})", total=N_TRIES)]
with ProcessPoolExecutor(max_workers=1) as executor:
futures = [executor.submit(op, M, y, ii) for ii in range(N_TRIES)]
res_d = [f.result() for f in tqdm(futures, desc=f"ProcessPoolExecutor ({NUM_THREADS})", total=N_TRIES)]
with LocalCluster(n_workers=1, threads_per_worker=1) as cluster:
with Client(cluster) as client:
print(client.dashboard_link)
M_dd = client.scatter(M, broadcast=True)
y_dd = client.scatter(y, broadcast=True)
futures = [client.submit(op, M_dd, y_dd, ii) for ii in range(N_TRIES)]
res_d = [f.result() for f in tqdm(futures, desc="Distributed (1)", total=N_TRIES)]
with LocalCluster(n_workers=1, threads_per_worker=1, env=get_env()) as cluster:
with Client(cluster) as client:
print(client.dashboard_link)
M_dd = client.scatter(M, broadcast=True)
y_dd = client.scatter(y, broadcast=True)
futures = [client.submit(op, M_dd, y_dd, ii) for ii in range(N_TRIES)]
res_d = [f.result() for f in tqdm(futures, desc=f"Distributed ({NUM_THREADS})", total=N_TRIES)]
Output of the script:
ThreadPoolExecutor (16): 100%|██████████████████████████████████████████████████████| 11/11 [00:02<00:00, 5.14it/s]
ProcessPoolExecutor (16): 100%|█████████████████████████████████████████████████████| 11/11 [00:03<00:00, 2.98it/s]
http://127.0.0.1:8787/status
Distributed (1): 100%|██████████████████████████████████████████████████████████████| 11/11 [02:30<00:00, 13.72s/it]
http://127.0.0.1:8787/status
Distributed (16): 100%|█████████████████████████████████████████████████████████████| 11/11 [03:16<00:00, 17.88s/it]
Environment:
- Dask version: 2024.8.2
- Python version: any from 3.10 until 3.12 included
- Operating System: Linux (ubuntu 2020.4)
- Install method (conda, pip, source): conda
- 主要语言
- Python
- 星标
- 1.7k
- 派生
- 781
- PR 合并指标
- 30 天内没有已合并 PR
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
dask/distributed 的其他 Issue
-
needs triage
难度 2/5 1-3 小时 新手友好度 72/100
dask/distributed#9366 ·
-
Scipy's spmatrix deprecation keeps the Upstream nightly red after #9351 — all 14 test_serialize_scipy_sparse cases fail可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭needs triage
难度 2/5 1-3 小时 新手友好度 84/100
dask/distributed#9353 · 1 条评论 ·
-
documentation
难度 1/5 1-3 小时 新手友好度 82/100
dask/distributed#8304 ·
-
难度 2/5 1-3 小时 新手友好度 74/100
dask/distributed#4816 · 2 条评论 ·
-
Client.scheduler_info needs better documentation可能已有人在做 @nil68657 于 5 天前认领。 未关闭documentation good first issue
难度 2/5 1-3 小时 新手友好度 74/100
dask/distributed#2378 · 2 条评论 ·
相似的 Issue
-
bug
难度 2/5 1-3 小时 新手友好度 76/100
Graphify-Labs/graphify#4524 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
topoteretes/cognee#5982 ·
维护者通常 1 天内回复
-
Patches of derivations with `__structuredAttrs` are ignored可能已有人在做 @KiaraGrouwstra 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 81/100
nix-community/vulnix#184 ·
-
New Internship未关闭new_internship
难度 1/5 1 小时以内 新手友好度 65/100
-
难度 2/5 1-3 小时 新手友好度 64/100
aws-samples/sample-per-model-bedrock#295 ·
维护者通常 1 天内回复