1

我正在做一个项目,我打算通过使用joblib带有共享内存的并行函数来提高效率。

但是,我还打算通过使用不同的参数(即没有共享内存)多次运行进程来对程序进行参数化研究。

我想知道这在 Python/ 中是否可行joblib


编辑:2020-06-19

正如另一位用户提到的,我应该澄清我想要并行化的代码中的内容。本质上,我有一个代表一些物理空间的 3D numpy 数组,我用大量截断的高斯函数填充该数组(仅影响有限数量的元素)。由于瓶颈是内存访问,没有发现完全矢量化可以特别加快代码速度,我想尝试并行化,因为我遍历所有ith- 高斯中心并将其贡献添加到整个领域。(这些循环将在一定程度上共享变量)

并行代码中并行代码的想法是,我还希望使用在线访问的集群同时运行大量此类进程,以便对项目的整体性能进行参数研究未指定的指标。因此,这些循环将是完全独立的。

此处发布了内部循环的修改摘录。不幸的是,它似乎并没有提高性能,而且在我没有将高斯中心列表拆分为每个核心的两个数组的情况下,情况更糟,我目前正在对此进行调查。


import numpy as np
import time
from joblib import Parallel, delayed, parallel_backend
from extra_fns import *

time.perf_counter()
nj = 2
set_par = True
split_var = True

# define 3d grid
nd = 3
nx = 250
ny = 250
nz = 250
x = np.linspace(0, 1, nx)
y = np.linspace(0, 1, ny)
z = np.linspace(0, 1, nz)

# positions of gaussians in space
pgrid = np.linspace(0.05, 0.95 , 20)
Xp, Yp, Zp = np.meshgrid(pgrid,pgrid,pgrid)
xp = Xp.ravel()
yp = Yp.ravel()
zp = Zp.ravel()
Np = np.size(xp)
s = np.ones(Np) # intensity of each gaussian
# compact gaussian representation
sigma = x[1]-x[0]
max_dist = sigma*(-2*np.log(10e-3))

# 3D domain: 
I = np.zeros((ny, nx, nz))
dx = x[1] - x[0]
dy = y[1] - y[0]
dz = z[1] - z[0]


dix = np.ceil(max_dist/dx)
diy = np.ceil(max_dist/dy)
diz = np.ceil(max_dist/dz)

def run_test(set_par, split_var, xp, yp, zp, s):
    def add_loc_gaussian(i):
        ix = round((xp[i] - x[0]) / dx)
        iy = round((yp[i] - y[0]) / dy)
        iz = round((zp[i] - z[0]) / dz)
        iix = np.arange(max(0, ix - dix), min(nx, ix + dix), 1, dtype=int)
        iiy = np.arange(max(0, iy - diy), min(ny, iy + diy), 1, dtype=int)
        iiz = np.arange(max(0, iz - diz), min(nz, iz + diz), 1, dtype=int)
        ddx = dx * iix - xp[i]
        ddy = dy * iiy - yp[i]
        ddz = dz * iiz - zp[i]
        gx = np.exp(-1 / (2 * sigma ** 2) * ddx ** 2)
        gy = np.exp(-1 / (2 * sigma ** 2) * ddy ** 2)
        gz = np.exp(-1 / (2 * sigma ** 2) * ddz ** 2)
        gx = gx[np.newaxis,:, np.newaxis]
        gy = gy[:,np.newaxis, np.newaxis]
        gz = gz[np.newaxis, np.newaxis, :]
        I[np.ix_(iiy, iix, iiz)] += s[i] * gy*gx*gz

    if set_par and split_var: # case 1
        mp = int(Np/nj) # hard code this test fn for two cores
        xp_list = [xp[:mp],xp[mp:]]
        yp_list = [yp[:mp],yp[mp:]]
        zp_list = [zp[:mp],zp[mp:]]
        sp_list = [s[:mp],s[mp:]]

        def core_loop(j):
            xpt = xp_list[j]
            ypt = yp_list[j]
            zpt = zp_list[j]
            spt = sp_list[j]

            def add_loc_gaussian_s(i):
                ix = round((xpt[i] - x[0]) / dx)
                iy = round((ypt[i] - y[0]) / dy)
                iz = round((zpt[i] - z[0]) / dz)
                iix = np.arange(max(0, ix - dix), min(nx, ix + dix), 1, dtype=int)
                iiy = np.arange(max(0, iy - diy), min(ny, iy + diy), 1, dtype=int)
                iiz = np.arange(max(0, iz - diz), min(nz, iz + diz), 1, dtype=int)
                ddx = dx * iix - xpt[i]
                ddy = dy * iiy - ypt[i]
                ddz = dz * iiz - zpt[i]
                gx = np.exp(-1 / (2 * sigma ** 2) * ddx ** 2)
                gy = np.exp(-1 / (2 * sigma ** 2) * ddy ** 2)
                gz = np.exp(-1 / (2 * sigma ** 2) * ddz ** 2)
                gx = gx[np.newaxis, :, np.newaxis]
                gy = gy[:, np.newaxis, np.newaxis]
                gz = gz[np.newaxis, np.newaxis, :]
                I[np.ix_(iiy, iix, iiz)] += spt[i] * gy * gx * gz

            for i in range(np.size(xpt)):
                add_loc_gaussian_s(i)

        Parallel(n_jobs=2, require='sharedmem')(delayed(core_loop)(i) for i in range(2))

    elif set_par: # case 2
        Parallel(n_jobs=nj, require='sharedmem')(delayed(add_loc_gaussian)(i) for i in range(Np))

    else: # case 3
        for i in range(0,Np):
            add_loc_gaussian(i)

run_test(set_par, split_var, xp, yp, zp, s)
print("Time taken: {} s".format(time.perf_counter()))
4

1 回答 1

1

“……在 Python中可行joblib/ ……”

概念意图没有问题,但......

“……我打算提高效率……”

这是故事中最难的部分


为什么 ?

CPU 微操作NOP(什么都不做)占用~ 0.1 [ns]2020/2H。

CPU微操作大约需要2020/2H。~ 0.3 [ns] ADD/SUB, ~ 10 [ns] DIV

CPU 可以有多个内核,CISC 架构可以在每个这样的 CPU 内核上运行一对硬件线程。

在此处输入图像描述

CPU 可以进化,将会进化,但不会做出任何超越物理定律限制在游戏中的现实的神奇“跳跃” 。绝不。

CPU 可以由 O/S 调度程序调度以交错更多的软件线程(代码执行流),因为这种交错的代码执行为我们生成,速度很慢,具有大约 25-Hz 的皮层视觉采样理解,使用不超过仅一个(语音)或双手输入“设备”,多任务操作系统的错觉,但所有这些工作都足够了(对非实时(HRT)操作系统没有保证)放入几对CPU 核心线程。

CPU 可以实现最高效的处理,如果计算任务不被广泛交错。越少越好。

在这种“紧凑”的工作流编排中,CPU 将保持在~ 0.3 ~ 10 [ns]每 uop(CPU 硬件机器指令)范围内,并且如果不去其他任何地方获取数据而是进入它自己的硬件寄存器(L1 缓存“成本”~ 0.5 [ns]来获取数据,而 L2~ 8x更“昂贵”,L3~ 40x更昂贵,RAM 可以从任何地方~70 .. 3++ [ns]获取数据)。因此,交错的进程执行支付了大量的开销成本,只是为了将多次从昂贵的 RAM 预取的数据恢复到更便宜的 L3、L2 和 L1 缓存存储中(只是重新支付了~ 300 ~ 350 [ns]每次要重新获取一条数据时,因为在调度程序从 CPU 内核中删除该线程后,交错的进程执行不会保留一次预取的数据,以便为调度程序队列中的另一个执行腾出空间)。

如果不等待来自 RAM 的数据,CPU 可以做到最好(内存通道和 I/O 瓶颈是众所周知的 HPC 效率/ CPU 饥饿的敌人)。


这些硬件开销还不够“足够”,您将不得不付出更多:

Python/joblib.Parallel()delayed()构造函数的类型是微不足道的,而不是为了将性能微调到最大效率。

在 CPU 硬件性能限制下,使用默认值njobs(或任何幼稚的手动设置)可能并且通常会降低处理方式的实际效率。

产生的进程必须支付非零附加成本。joblib在所有情况下,这些都为从 RAM 重新取回的每个数据项支付 CPU 硬件附加成本(现在重新选择了 O/S 调度程序)CPU 核心 L3/L2/L1 缓存(~ 3++ [ns]数百纳秒)再加上它“共享” CPU 核心代码执行时间的弱优先级份额(参考 O/S 参数化和调度程序属性以获取有关最大性能/效率设置的详细信息)

最后但并非最不重要
的是有巨大的(规模~ hundreds of [us] if not [ms])流程实例化、流程到流程调用签名参数传输的附加成本(读取 SER/DES 成本(通常是pickle.dumps() / pickle.loads()) 关于参数的数据转换 + Process-2-Process 压缩数据通信交换...( time, time, time... )... , process-results 的数据传回(如果存在),即再次SER/DES 管道 + P2P 通信成本……(时间、时间、时间……)……加上流程终止附加成本。

在靠近 CPU 硬件最高性能能力上限的任何地方做这一切总是很困难,在 Python-GIL-lock 限制代码执行 + joblib.Parallel()-spawned processes + Cython-ised 模块的轻松多样的生态系统中更是如此控制/调整其产生的子进程的实际数量的舒适度,是吗?)共存,这种已经“效率”的调整狂野混合允许在普通的、COTS 级用户 MMI 为中心的 O/S 中操作.


如果以上这些都很容易被“吞下”,那么共享的附加成本就来了:

虽然存在共享变量协议,但我会采取任何措施来避免为“使用”它们支付巨大的代码执行附加成本。

谁愿意为了早上 9:00 懒懒地骑车上学,有时又在下午晚些时候返回“租”一辆劳斯莱斯,而付出巨大的代价?可行,但却是一项极其昂贵的“策略”。肯定有方法可以避免共享变量,而零共享对于任何旨在实现最高性能和效率的 HPC 级软件来说都是必须的。


“我正在做一个项目……”

费尽心思才XY-[man*months]付账可能为时已晚:

分析处理策略和所有实际的附加成本,先于决策。

迟来的惊喜是最昂贵的。

即使是忽略阿姆达尔定律的原始、开销天真和工作原子性也表明,存在一个您永远无法规避的主要限制 - 收益递减定律。这就是忽略乐观模型的附加成本。

在此处输入图像描述

现实不符合你的意愿,以获得更好的表现。如果宏观(与多处理相关的)附加成本占据(而且他们很快就会做到)主导地位,那么它就会越多。添加共享变量通信协议会使效率降低许多数量级(不仅~2为缓存/RAM重新获取增加了数量级的延迟成本,而且进程到进程的重新同步成本“阻塞”了最高效的 CPU 核心驻留处理,因为对其他非 CPU 核心进程的依赖会出现类似屏障的阻塞状态,当检查/重新传播共享变量的状态以保持系统范围内的一致性时......代价是浪费时间和效率会造成严重破坏......只是为了 Python 共享变量“舒适”的语法糖的易用性。

是的,现实生活是残酷的……

但谁曾告诉我们它不是?

祝您安全、正确地管理项目!

学习生活的艺术,一路艰难前行,总能让我们充实,不是吗?

:o)


可能想阅读更多关于此的内容,
也许是代码示例:

如果有兴趣进一步阅读joblib和修订阿姆达尔定律的影响,请随时潜入。魔鬼隐藏在细节中。一如既往。

于 2020-06-18T07:11:49.140 回答