gpt4 book ai didi

python - 只读 numpy 数组的快速队列

转载 作者:太空狗 更新时间:2023-10-29 18:22:54 25 4
gpt4 key购买 nike

我有一个多处理工作,我正在排队只读 numpy 数组,作为生产者消费者管道的一部分。

目前它们正在被腌制,因为这是 multiprocessing.Queue 的默认行为。这会降低性能。

是否有任何 pythonic 方法来传递对共享内存的引用而不是酸洗数组?

不幸的是,数组是在消费者启动后生成的,没有简单的方法可以解决这个问题。 (所以全局变量方法会很丑......)。

[请注意,在以下代码中,我们不期望 h(x0) 和 h(x1) 并行计算。相反,我们看到 h(x0) 和 g(h(x1)) 并行计算(就像 CPU 中的流水线)。]

from multiprocessing import Process, Queue
import numpy as np

class __EndToken(object):
pass

def parrallel_pipeline(buffer_size=50):
def parrallel_pipeline_with_args(f):
def consumer(xs, q):
for x in xs:
q.put(x)
q.put(__EndToken())

def parallel_generator(f_xs):
q = Queue(buffer_size)
consumer_process = Process(target=consumer,args=(f_xs,q,))
consumer_process.start()
while True:
x = q.get()
if isinstance(x, __EndToken):
break
yield x

def f_wrapper(xs):
return parallel_generator(f(xs))

return f_wrapper
return parrallel_pipeline_with_args


@parrallel_pipeline(3)
def f(xs):
for x in xs:
yield x + 1.0

@parrallel_pipeline(3)
def g(xs):
for x in xs:
yield x * 3

@parrallel_pipeline(3)
def h(xs):
for x in xs:
yield x * x

def xs():
for i in range(1000):
yield np.random.uniform(0,1,(500,2000))

if __name__ == "__main__":
rs = f(g(h(xs())))
for r in rs:
print r

最佳答案

在线程或进程之间共享内存

使用线程代替多处理

由于您使用的是 numpy,因此您可以利用 the global interpreter lock is released during numpy computations 的优势。 .这意味着您可以使用标准线程和共享内存进行并行处理,而不是多处理和进程间通信。这是您的代码的一个版本,经过调整以使用 threading.Thread 和 Queue.Queue 而不是 multiprocessing.Process 和 multiprocessing.Queue。这通过队列传递一个 numpy ndarray 而不对其进行酸洗。在我的计算机上,它的运行速度比您的代码快 3 倍。 (但是,它只比你的代码的串行版本快 20%。我已经建议了一些其他的方法。)

from threading import Thread
from Queue import Queue
import numpy as np

class __EndToken(object):
pass

def parallel_pipeline(buffer_size=50):
def parallel_pipeline_with_args(f):
def consumer(xs, q):
for x in xs:
q.put(x)
q.put(__EndToken())

def parallel_generator(f_xs):
q = Queue(buffer_size)
consumer_process = Thread(target=consumer,args=(f_xs,q,))
consumer_process.start()
while True:
x = q.get()
if isinstance(x, __EndToken):
break
yield x

def f_wrapper(xs):
return parallel_generator(f(xs))

return f_wrapper
return parallel_pipeline_with_args

@parallel_pipeline(3)
def f(xs):
for x in xs:
yield x + 1.0

@parallel_pipeline(3)
def g(xs):
for x in xs:
yield x * 3

@parallel_pipeline(3)
def h(xs):
for x in xs:
yield x * x

def xs():
for i in range(1000):
yield np.random.uniform(0,1,(500,2000))

rs = f(g(h(xs())))
%time print sum(r.sum() for r in rs) # 12.2s

将 numpy 数组存储在共享内存中

另一种接近您要求的选项是继续使用 multiprocessing 包,但使用存储在共享内存中的数组在进程之间传递数据。下面的代码创建了一个新的 ArrayQueue 类来做到这一点。 ArrayQueue 对象应该在产生子进程之前创建。它创建和管理由共享内存支持的 numpy 数组池。当结果数组被插入队列时,ArrayQueue 将数据从该数组复制到现有的共享内存数组中,然后通过队列传递共享内存数组的 id。这比通过队列发送整个数组要快得多,因为它避免了对数组进行酸洗。这与上面的线程版本具有相似的性能(大约慢 10%),如果全局解释器锁是一个问题(即,你在函数中运行了大量的 python 代码),它可能会更好地扩展。
from multiprocessing import Process, Queue, Array
import numpy as np

class ArrayQueue(object):
def __init__(self, template, maxsize=0):
if type(template) is not np.ndarray:
raise ValueError('ArrayQueue(template, maxsize) must use a numpy.ndarray as the template.')
if maxsize == 0:
# this queue cannot be infinite, because it will be backed by real objects
raise ValueError('ArrayQueue(template, maxsize) must use a finite value for maxsize.')

# find the size and data type for the arrays
# note: every ndarray put on the queue must be this size
self.dtype = template.dtype
self.shape = template.shape
self.byte_count = len(template.data)

# make a pool of numpy arrays, each backed by shared memory,
# and create a queue to keep track of which ones are free
self.array_pool = [None] * maxsize
self.free_arrays = Queue(maxsize)
for i in range(maxsize):
buf = Array('c', self.byte_count, lock=False)
self.array_pool[i] = np.frombuffer(buf, dtype=self.dtype).reshape(self.shape)
self.free_arrays.put(i)

self.q = Queue(maxsize)

def put(self, item, *args, **kwargs):
if type(item) is np.ndarray:
if item.dtype == self.dtype and item.shape == self.shape and len(item.data)==self.byte_count:
# get the ID of an available shared-memory array
id = self.free_arrays.get()
# copy item to the shared-memory array
self.array_pool[id][:] = item
# put the array's id (not the whole array) onto the queue
new_item = id
else:
raise ValueError(
'ndarray does not match type or shape of template used to initialize ArrayQueue'
)
else:
# not an ndarray
# put the original item on the queue (as a tuple, so we know it's not an ID)
new_item = (item,)
self.q.put(new_item, *args, **kwargs)

def get(self, *args, **kwargs):
item = self.q.get(*args, **kwargs)
if type(item) is tuple:
# unpack the original item
return item[0]
else:
# item is the id of a shared-memory array
# copy the array
arr = self.array_pool[item].copy()
# put the shared-memory array back into the pool
self.free_arrays.put(item)
return arr

class __EndToken(object):
pass

def parallel_pipeline(buffer_size=50):
def parallel_pipeline_with_args(f):
def consumer(xs, q):
for x in xs:
q.put(x)
q.put(__EndToken())

def parallel_generator(f_xs):
q = ArrayQueue(template=np.zeros(0,1,(500,2000)), maxsize=buffer_size)
consumer_process = Process(target=consumer,args=(f_xs,q,))
consumer_process.start()
while True:
x = q.get()
if isinstance(x, __EndToken):
break
yield x

def f_wrapper(xs):
return parallel_generator(f(xs))

return f_wrapper
return parallel_pipeline_with_args


@parallel_pipeline(3)
def f(xs):
for x in xs:
yield x + 1.0

@parallel_pipeline(3)
def g(xs):
for x in xs:
yield x * 3

@parallel_pipeline(3)
def h(xs):
for x in xs:
yield x * x

def xs():
for i in range(1000):
yield np.random.uniform(0,1,(500,2000))

print "multiprocessing with shared-memory arrays:"
%time print sum(r.sum() for r in f(g(h(xs())))) # 13.5s

并行处理样本而不是函数

上面的代码仅比单线程版本快 20%(12.2s 与 14.8s 的串行版本如下所示)。那是因为每个函数都运行在单个线程或进程中,并且大部分工作是由 xs() 完成的。上面示例的执行时间几乎与您刚刚运行 %time print sum(1 for x in xs()) 时相同。 .

如果您的实际项目具有更多中间功能和/或它们比您展示的更复杂,那么工作负载可能会在处理器之间更好地分配,这可能不是问题。但是,如果您的工作负载确实与您提供的代码相似,那么您可能需要重构您的代码,为每个线程分配一个样本,而不是为每个线程分配一个函数。这看起来像下面的代码(显示了线程和多处理版本):
import multiprocessing
import threading, Queue
import numpy as np

def f(x):
return x + 1.0

def g(x):
return x * 3

def h(x):
return x * x

def final(i):
return f(g(h(x(i))))

def final_sum(i):
return f(g(h(x(i)))).sum()

def x(i):
# produce sample number i
return np.random.uniform(0, 1, (500, 2000))

def rs_serial(func, n):
for i in range(n):
yield func(i)

def rs_parallel_threaded(func, n):
todo = range(n)
q = Queue.Queue(2*n_workers)

def worker():
while True:
try:
# the global interpreter lock ensures only one thread does this at a time
i = todo.pop()
q.put(func(i))
except IndexError:
# none left to do
q.put(None)
break

threads = []
for j in range(n_workers):
t = threading.Thread(target=worker)
t.daemon=False
threads.append(t) # in case it's needed later
t.start()

while True:
x = q.get()
if x is None:
break
else:
yield x

def rs_parallel_mp(func, n):
pool = multiprocessing.Pool(n_workers)
return pool.imap_unordered(func, range(n))

n_workers = 4
n_samples = 1000

print "serial:" # 14.8s
%time print sum(r.sum() for r in rs_serial(final, n_samples))
print "threaded:" # 10.1s
%time print sum(r.sum() for r in rs_parallel_threaded(final, n_samples))

print "mp return arrays:" # 19.6s
%time print sum(r.sum() for r in rs_parallel_mp(final, n_samples))
print "mp return results:" # 8.4s
%time print sum(r_sum for r_sum in rs_parallel_mp(final_sum, n_samples))

此代码的线程版本仅比我给出的第一个示例略快,仅比串行版本快 30%。这并不像我预期的那样加速。也许 Python 仍然部分地被 GIL 所困?

多处理版本的执行速度明显快于原始多处理代码,主要是因为所有函数都在一个进程中链接在一起,而不是排队(和酸洗)中间结果。但是,它仍然比串行版本慢,因为在 imap_unordered 返回之前,所有结果数组都必须进行腌制(在工作进程中)和取消腌制(在主进程中)。但是,如果您可以安排它以便您的管道返回聚合结果而不是完整的数组,那么您可以避免酸洗开销,并且多处理版本最快:比串行版本快约 43%。

好的,现在为了完整起见,这里是第二个示例的一个版本,它使用多处理和原始生成器函数,而不是上面显示的更精细的函数。这使用了一些技巧来在多个进程之间传播样本,这可能使其不适用于许多工作流程。但是使用生成器似乎比使用更精细的函数稍微快一点,并且与上面显示的串行版本相比,这种方法可以使您获得高达 54% 的加速。但是,这仅在您不需要从工作函数返回完整数组时才可用。
import multiprocessing, itertools, math
import numpy as np

def f(xs):
for x in xs:
yield x + 1.0

def g(xs):
for x in xs:
yield x * 3

def h(xs):
for x in xs:
yield x * x

def xs():
for i in range(1000):
yield np.random.uniform(0,1,(500,2000))

def final():
return f(g(h(xs())))

def final_sum():
for x in f(g(h(xs()))):
yield x.sum()

def get_chunk(args):
"""Retrieve n values (n=args[1]) from a generator function (f=args[0]) and return them as a list.
This runs in a worker process and does all the computation."""
return list(itertools.islice(args[0](), args[1]))

def parallelize(gen_func, max_items, n_workers=4, chunk_size=50):
"""Pull up to max_items items from several copies of gen_func, in small groups in parallel processes.
chunk_size should be big enough to improve efficiency (one copy of gen_func will be run for each chunk)
but small enough to avoid exhausting memory (each worker will keep chunk_size items in memory)."""

pool = multiprocessing.Pool(n_workers)

# how many chunks will be needed to yield at least max_items items?
n_chunks = int(math.ceil(float(max_items)/float(chunk_size)))

# generate a suitable series of arguments for get_chunk()
args_list = itertools.repeat((gen_func, chunk_size), n_chunks)

# chunk_gen will yield a series of chunks (lists of results) from the generator function,
# totaling n_chunks * chunk_size items (which is >= max_items)
chunk_gen = pool.imap_unordered(get_chunk, args_list)

# parallel_gen flattens the chunks, and yields individual items
parallel_gen = itertools.chain.from_iterable(chunk_gen)

# limit the output to max_items items
return itertools.islice(parallel_gen, max_items)


# in this case, the parallel version is slower than a single process, probably
# due to overhead of gathering numpy arrays in imap_unordered (via pickle?)
print "serial, return arrays:" # 15.3s
%time print sum(r.sum() for r in final())
print "parallel, return arrays:" # 24.2s
%time print sum(r.sum() for r in parallelize(final, max_items=1000))


# in this case, the parallel version is more than twice as fast as the single-thread version
print "serial, return result:" # 15.1s
%time print sum(r for r in final_sum())
print "parallel, return result:" # 6.8s
%time print sum(r for r in parallelize(final_sum, max_items=1000))

关于python - 只读 numpy 数组的快速队列,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38666078/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com