尝试为pystruct模块中的并行化做出贡献,并在讨论中试图解释为什么我希望尽可能早地实例化池并尽可能长时间地保存它们,重用它们,我意识到我知道最好这样做,但我不完全知道为什么。
我知道* nix系统上的声明是池工作器子进程在写入时从父进程中的所有全局变量进行复制。总的来说情况确实如此,但我认为应该补充说,当其中一个全局变量是一个特别密集的数据结构,如numpy或scipy矩阵时,似乎任何引用都被复制到工作者中实际上是漂亮的即使整个对象没有被复制也是相当大的,因此在执行后期产生新池会导致内存问题。我发现最好的做法是尽早产生一个池,这样任何数据结构都很小。
我已经知道了一段时间并在工作中的应用程序中进行了设计,但我得到的最佳解释是我在帖子中发布的内容:
https://github.com/pystruct/pystruct/pull/129#issuecomment-68898032
从下面的python脚本看,基本上,你会期望第一次运行中创建的池中的空闲内存和第二次运行中创建的矩阵步骤基本相同,就像在最终池终止的调用中一样。但是它们从来都不是,当你首先创建池时,总会有(除非机器上还有其它东西)更多的空闲内存。在创建池时,这种影响会随着全局命名空间中数据结构的复杂性(和大小)而增加(我认为)。有没有人对此有一个很好的解释?
我使用bash循环和下面的R脚本来制作这个小图片,以显示创建池和矩阵后的整体可用内存,具体取决于顺序:
pool_memory_test.py:
import numpy as np
import multiprocessing as mp
import logging
def memory():
"""
Get node total memory and memory usage
"""
with open('/proc/meminfo', 'r') as mem:
ret = {}
tmp = 0
for i in mem:
sline = i.split()
if str(sline[0]) == 'MemTotal:':
ret['total'] = int(sline[1])
elif str(sline[0]) in ('MemFree:', 'Buffers:', 'Cached:'):
tmp += int(sline[1])
ret['free'] = tmp
ret['used'] = int(ret['total']) - int(ret['free'])
return ret
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--pool_first', action='store_true')
parser.add_argument('--call_map', action='store_true')
args = parser.parse_args()
if args.pool_first:
logging.debug('start:\n\t {}\n'.format(' '.join(['{}: {}'.format(k,v)
for k,v in memory().items()])))
p = mp.Pool()
logging.debug('pool created:\n\t {}\n'.format(' '.join(['{}: {}'.format(k,v)
for k,v in memory().items()])))
biggish_matrix = np.ones((50000,5000))
logging.debug('matrix created:\n\t {}\n'.format(' '.join(['{}: {}'.format(k,v)
for k,v in memory().items()])))
print memory()['free']
else:
logging.debug('start:\n\t {}\n'.format(' '.join(['{}: {}'.format(k,v)
for k,v in memory().items()])))
biggish_matrix = np.ones((50000,5000))
logging.debug('matrix created:\n\t {}\n'.format(' '.join(['{}: {}'.format(k,v)
for k,v in memory().items()])))
p = mp.Pool()
logging.debug('pool created:\n\t {}\n'.format(' '.join(['{}: {}'.format(k,v)
for k,v in memory().items()])))
print memory()['free']
if args.call_map:
row_sums = p.map(sum, biggish_matrix)
logging.debug('sum mapped:\n\t {}\n'.format(' '.join(['{}: {}'.format(k,v)
for k,v in memory().items()])))
p.terminate()
p.join()
logging.debug('pool terminated:\n\t {}\n'.format(' '.join(['{}: {}'.format(k,v)
for k,v in memory().items()])))
pool_memory_test.sh
#! /bin/bash
rm pool_first_obs.txt > /dev/null 2>&1;
rm matrix_first_obs.txt > /dev/null 2>&1;
for ((n=0;n<100;n++)); do
python pool_memory_test.py --pool_first >> pool_first_obs.txt;
python pool_memory_test.py >> matrix_first_obs.txt;
done
pool_memory_test_plot.R:
library(ggplot2)
library(reshape2)
pool_first = as.numeric(readLines('pool_first_obs.txt'))
matrix_first = as.numeric(readLines('matrix_first_obs.txt'))
df = data.frame(i=seq(1,100), pool_first, matrix_first)
ggplot(data=melt(df, id.vars='i'), aes(x=i, y=value, color=variable)) +
geom_point() + geom_smooth() + xlab('iteration') +
ylab('free memory') + ggsave('multiprocessing_pool_memory.png')
编辑:修复由于过度搜索/替换和重新运行固定
导致的脚本中的小错误EDIT2:&#34; -0&#34;切片?你能做到吗? :)
EDIT3:更好的python脚本,bash循环和可视化,现在可以用这个兔子洞完成:)
答案 0 :(得分:2)
你的问题涉及几个松散耦合的机制。并且它也是一个似乎是额外业力点的简单目标,但你可以感觉到一些错误,3个小时之后这是一个完全不同的问题。因此,为了获得我所拥有的所有乐趣,您可能会发现以下有用信息。
TL; DR :测量已用内存,而非免费。这为我提供了(几乎)相同的结果(对于池/矩阵顺序和大对象大小)。
def memory():
import resource
# RUSAGE_BOTH is not always available
self = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
children = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss
return self + children
在回答你没有提出的问题之前,那些密切相关的问题,这里有一些背景知识。
最广泛的实现,CPython(2和3版本)都使用引用计数内存管理[1]。每当您使用Python对象作为值时,它的引用计数器会增加1,并在引用丢失时减少。计数器是一个在C struct中定义的整数,用于保存每个Python对象的数据[2]。外卖:参考计数器一直在变化,它与其余的对象数据一起存储。
大多数“Unix灵感操作系统”(BSD系列,Linux,OSX等)都具有写时复制[3]内存访问语义。在fork()
之后,两个进程具有指向相同物理页面的不同内存页表。但OS已将页面标记为写保护,因此当您执行任何内存写入时,CPU会引发内存访问异常,操作系统会将原始页面复制到新位置。它走路和嘎嘎似的过程有孤立的内存,但是,嘿,让我们节省一些时间(复制)和RAM,而内存的一部分是相同的。外卖:fork
(或mp.Pool
)创建新进程,但他们(几乎)暂时不使用任何额外内存。
CPython在大型游泳池(竞技场)中存储“小”物体[4]。在您创建和销毁大量小对象(例如,函数内的临时变量)的常见场景中,您不希望过于频繁地调用OS内存管理。其他编程语言(至少是大多数编译语言)使用堆栈来实现此目的。
mp.Pool()
之后不同的内存使用情况,没有任何工作由池完成:multiprocessing.Pool.__init__
创建N(对于检测到的CPU数)工作进程。写时复制语义就此开始。numpy.ones
和Python list
的不同内存使用情况:matrix = [[1,1,...],[1,2,...],...]
是Python整数Python列表的Python列表。很多Python对象=很多PyObject_HEAD =大量的ref-counters。在分叉环境中访问所有这些将触及所有引用计数器,因此将复制其内存页面。 matrix = numpy.ones((50000, 5000))
是numpy.array
类型的Python对象。就是这样,一个Python对象,一个ref-counter。其余的是存储在内存中的纯低级别数字,彼此相邻,不涉及参考计数器。为简单起见,您可以使用data = '.'*size
[5] - 这也会在内存中创建单个对象。