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

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

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

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

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: нарезка "-0"? Вы можете сделать это? :)

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


person Robert E Mealey    schedule 07.01.2015    source источник
comment
-0 не является индексом. это нотация среза   -  person Himal    schedule 07.01.2015
comment
:) спасибо, да, я знал это, фиксированный срок. Я просто не понимал, что -0 был допустимым фрагментом. Я думаю, имеет смысл, что 0 и -0 вернут первый элемент. Однако я не могу придумать подходящий вариант использования для этого.   -  person Robert E Mealey    schedule 07.01.2015
comment
начало ответа: twitter.com/ChristianHeimes/status/552760084162678784 twitter.com/ChristianHeimes/status/552760759428857856   -  person Robert E Mealey    schedule 14.04.2015
comment
Счетчик ссылок в структурах PyObject плохо работает с fork() копирования при записи. Любая запись вызывает копию страницы памяти размером 4096 байт. Я не знаю, как NumPy хранит свои массивы внутри. Может помочь просмотр r/o разделяемой памяти.   -  person Robert E Mealey    schedule 14.04.2015


Ответы (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]. Вывод: счетчик ссылок постоянно меняется, он хранится вместе с остальными данными объекта.

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

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

Похожие вопросы

  • Различное использование памяти сразу после mp.Pool() без какой-либо работы, выполняемой пулом: multiprocessing.Pool.__init__ создает N (для количества обнаруженных ЦП) рабочих процессов. С этого момента начинается семантика копирования при записи.
  • «утверждение в системах * nix заключается в том, что рабочий подпроцесс пула копирует при записи все глобальные переменные в родительском процессе»: многопроцессорная обработка копирует глобальные переменные своего «контекста», а не глобальные переменные из вашего модуля, и делает это безоговорочно, на любом ОПЕРАЦИОННЫЕ СИСТЕМЫ. [5]
  • Различное использование памяти numpy.ones и Python list: matrix = [[1,1,...],[1,2,...],...] — это список Python, состоящий из списков целых чисел Python. Много объектов Python = много PyObject_HEAD = много счетчиков ссылок. Доступ ко всем из них в разветвленной среде коснется всех счетчиков ссылок, поэтому будет скопировано их страницы памяти. matrix = numpy.ones((50000, 5000)) — это объект Python типа numpy.array. Вот и все, один объект Python, один счетчик ссылок. Остальное — это чистые низкоуровневые числа, хранящиеся в памяти рядом друг с другом, без использования счетчиков ссылок. Для простоты вы можете использовать data = '.'*size [5] — это также создает один объект в памяти.

Источники

  1. https://docs.python.org/2/c-api/refcounting.html
  2. https://docs.python.org/2/c-api/structures.html#c.PyObject_HEAD
  3. http://minnie.tuhs.org/CompArch/Lectures/week09.html#tth_sEc2.8
  4. http://www.evanjones.ca/memoryallocator/
  5. https://github.com/python/cpython/search?utf8=%E2%9C%93&q=globals+path%3ALib%2Fmultiprocessing%2F&type=Code
  6. Собираем все вместе https://gist.github.com/temoto/af663106a3da414359fa
person temoto    schedule 24.04.2015
comment
спасибо полезно! На самом деле я думаю так: утверждение в системах * nix заключается в том, что рабочий подпроцесс пула копирует при записи все глобальные переменные в родительском процессе: многопроцессорная обработка копирует глобальные переменные своего контекста, а не глобальные переменные вашего модуля, и делает это безоговорочно, на любая ОС. [5] может стать основой для ответа на мой вопрос. Позвольте мне исследовать, и если да, я пришлю вам суть, и вы можете обновить и получить галочку, так как вы проделали так много работы :) - person Robert E Mealey; 25.04.2015