Неожиданные различия в размерах памяти при возникновении пула многопроцессорности python

Попытка внести некоторую оптимизацию для распараллеливания в модуле pystruct и в дискуссиях, пытающихся объяснить мое мышление о том, почему я хотел бы создать пулы как можно раньше, и держать их как можно дольше, повторно используя их, я понял, что знаю, что он работает лучше всего, но я не знаю, почему.

Я знаю, что претензия в системах * nix заключается в том, что подпроцесс рабочего пула копируется при записи из всех глобалов в родительском процессе. Это, безусловно, имеет место в целом, но я думаю, что следует добавить оговорку, что, когда один из этих глобалов является особенно плотной структурой данных, такой как numpy или scipy matrix, кажется, что любые ссылки, скопированные в рабочий, на самом деле довольно даже если весь объект не копируется, и поэтому появление новых пулов в конце выполнения может вызвать проблемы с памятью. Я нашел, что лучшая практика заключается в том, чтобы порождать пул как можно раньше, чтобы любые структуры данных были небольшими.

Я знаю это некоторое время и разрабатывал его в приложениях на работе, но лучшее объяснение, которое я получил, - это то, что я написал в этом разделе:

https://github.com/pystruct/pystruct/pull/129#issuecomment-68898032

Глядя на python script ниже, по существу, вы ожидаете, что свободная память в созданном пуле шаге в первом запуске и шаг, созданный матрицей во втором, будут в основном равными, как и в завершающих вызовах окончательного пула. Но они никогда не бывают, всегда (если, конечно, что-то еще происходит на машине), больше свободной памяти при создании пула в первую очередь. Этот эффект увеличивается со сложностью (и размером) структур данных в глобальном пространстве имен во время создания пула (я думаю). У кого-нибудь есть хорошее объяснение этому?

Я сделал эту маленькую картинку с контуром bash и R script также ниже, чтобы проиллюстрировать, показывая полную свободную память после создания пула и матрицы в зависимости от порядка:

free memory trend plot, both ways

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')

РЕДАКТИРОВАТЬ: исправление небольшой ошибки в script, вызванное чрезмерным обнаружением/заменой и повторным запуском

EDIT2: "-0" нарезка? Вы можете сделать это?:)

EDIT3: лучший питон script, bash петли и визуализация, теперь сделайте это отверстие для кроликов на данный момент:)

Ответ 1

В вашем вопросе затрагивается несколько слабо связанных механик. И это тоже одно, что кажется легкой целью для дополнительных точек кармы, но вы можете почувствовать что-то не так, и через 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 в качестве значения, его счетчик ссылок увеличивается на единицу и уменьшается обратно, когда ссылка теряется. Счетчик представляет собой целое число, определенное в данных хранения структуры C каждого объекта Python [2]. Takeaway: счетчик ссылок постоянно меняется, он сохраняется вместе с остальными данными объекта.

Большинство "Unix-вдохновленных ОС" (семейство BSD, Linux, OSX и т.д.), спорт-копирование на запись [3]. После fork() два процесса имеют разные таблицы страниц памяти, указывающие на одни и те же физические страницы. Но ОС помечает страницы как защищенные от записи, поэтому, когда вы производите запись в память, CPU вызывает исключение доступа к памяти, которое обрабатывается ОС для копирования исходной страницы на новое место. Он подходит и крякает, как процесс имеет изолированную память, но эй, пусть сэкономит некоторое время (при копировании) и ОЗУ, в то время как части памяти эквивалентны. Takeaway: fork (или mp.Pool) создает новые процессы, но они (почти) еще не используют лишнюю память.

CPython хранит "маленькие" объекты в больших пулах (аренах) [4]. В общем случае, когда вы создаете и уничтожаете большое количество небольших объектов, например временных переменных внутри функции, вы не хотите слишком часто обращаться к управлению памятью ОС. Другие языки программирования (большинство скомпилированных, по крайней мере) используют стек для этой цели.

Связанные вопросы

  • Различное использование памяти сразу после mp.Pool() без какой-либо работы пула: multiprocessing.Pool.__init__ создает N (для количества обнаруженных CPU) рабочих процессов. На этом этапе начинается семантика копирования на запись.
  • "претензия в системах * nix заключается в том, что подпроцесс рабочего пула копируется при записи из всех глобалов в родительском процессе": многопроцессор копирует глобальные переменные из него "контекст", а не глобалы из вашего модуля, и он делает это безоговорочно, на любой ОС. [5]
  • Различное использование памяти numpy.ones и Python list: matrix = [[1,1,...],[1,2,...],...] - это список Python списков Python для целых чисел Python. Множество объектов Python = много PyObject_HEAD = много ref-counters. Доступ ко всем из них в разветвленной среде коснется всех ref-counters, поэтому скопирует их страницы памяти. matrix = numpy.ones((50000, 5000)) - объект Python типа numpy.array. Это он, один объект Python, один ref-counter. Остальные - это чистые номера низкого уровня, хранящиеся в памяти рядом друг с другом, не задействованные рефлектометры. Для простоты вы можете использовать data = '.'*size [5] - также создающий единый объект в памяти.

Источники