Как уничтожить объекты Python и освободить память - PullRequest
12 голосов
/ 14 мая 2019

Я пытаюсь выполнить итерацию более 100 000 изображений, захватить некоторые функции изображения и сохранить полученный фрейм данных на диске в виде файла маринада.

К сожалению, из-за ограничений ОЗУ я вынужден разделить изображения на 20 000 кусков и выполнить операции с ними перед сохранением результатов на диск.

Код, написанный ниже, должен сохранитьдата-кадр результатов для 20000 изображений перед началом цикла обработки следующих 20000 изображений.

Однако - это, похоже, не решает мою проблему, так как память не освобождается из ОЗУ в конце первого цикла for

Так что где-то при обработке 50-тысячной записи,сбой программы из-за ошибки «Недостаточно памяти».

Я пытался удалить объекты после сохранения их на диск и вызова сборщика мусора, однако использование ОЗУ, похоже, не снижается.

Чтоя скучаю?

#file_list_1 contains 100,000 images
file_list_chunks = list(divide_chunks(file_list_1,20000))
for count,f in enumerate(file_list_chunks):
    # make the Pool of workers
    pool = ThreadPool(64) 
    results = pool.map(get_image_features,f)
    # close the pool and wait for the work to finish 
    list_a, list_b = zip(*results)
    df = pd.DataFrame({'filename':list_a,'image_features':list_b})
    df.to_pickle("PATH_TO_FILE"+str(count)+".pickle")
    del list_a
    del list_b
    del df
    gc.collect()
    pool.close() 
    pool.join()
    print("pool closed")

Ответы [ 8 ]

6 голосов
/ 22 мая 2019

Теперь, может быть, что-то в 50 000-м очень большое, и это вызывает OOM, поэтому, чтобы проверить это, я сначала попробую:

file_list_chunks = list(divide_chunks(file_list_1,20000))[30000:]

Если при 10000 произойдет сбой, это подтвердит, является ли 20k слишком большим для размера фрагмента, или если снова произойдет сбой при 50,000, есть проблема с кодом ...


Хорошо, на код ...

Во-первых, вам не нужен явный конструктор list, в python гораздо лучше итерировать, чем генерировать весь список в память.

file_list_chunks = list(divide_chunks(file_list_1,20000))
# becomes
file_list_chunks = divide_chunks(file_list_1,20000)

Я думаю, вы можете неправильно использовать ThreadPool здесь:

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

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

with ThreadPool(64) as pool: 
    results = pool.map(get_image_features,f)
    # etc.

Явные del s в python фактически не гарантируют освобождение памяти .

Вы должны собрать после соединения / после с:

with ThreadPool(..):
    ...
    pool.join()
gc.collect()

Вы также можете попробовать разделить это на более мелкие части, например. 10000 или даже меньше!


Молоток 1

Одна вещь, которую я хотел бы сделать здесь, вместо того, чтобы использовать pandas DataFrames и большие списки, это использовать базу данных SQL, вы можете сделать это локально с помощью sqlite3 :

import sqlite3
conn = sqlite3.connect(':memory:', check_same_thread=False)  # or, use a file e.g. 'image-features.db'

и использовать менеджер контекста:

with conn:
    conn.execute('''CREATE TABLE images
                    (filename text, features text)''')

with conn:
    # Insert a row of data
    conn.execute("INSERT INTO images VALUES ('my-image.png','feature1,feature2')")

Таким образом, нам не придется обрабатывать объекты большого списка или DataFrame.

Вы можете передать соединение каждому из потоков ... вам может понадобиться что-то немного странное, например:

results = pool.map(get_image_features, zip(itertools.repeat(conn), f))

Затем, после завершения расчета, вы можете выбрать из базы данных все, в какой формат вы хотите. Например. используя read_sql .


Молоток 2

Используйте здесь подпроцесс, вместо того, чтобы запускать его в одном и том же экземпляре python, "shell" для другого.

Поскольку вы можете передавать начало и конец в python как sys.args, вы можете нарезать их следующим образом:

# main.py
# a for loop to iterate over this
subprocess.check_call(["python", "chunk.py", "0", "20000"])

# chunk.py a b
for count,f in enumerate(file_list_chunks):
    if count < int(sys.argv[1]) or count > int(sys.argv[2]):
         pass
    # do stuff

Таким образом, подпроцесс должным образом очистит питон (утечки памяти не будет, поскольку процесс будет прерван).


Моя ставка в том, что Hammer 1 - это то, что нужно, такое ощущение, что вы склеиваете много данных и без необходимости считываете их в списки Python, а использование sqlite3 (или другой базы данных) полностью избегает этого.

1 голос
/ 23 мая 2019

Ваша проблема в том, что вы используете многопоточность, где следует использовать многопроцессорность (привязка ЦП к вводу-выводу).

Я бы реорганизовал ваш код примерно так:

from multiprocessing import Pool

if __name__ == '__main__':
    cpus = multiprocessing.cpu_count()        
    with Pool(cpus-1) as p:
        p.map(get_image_features, file_list_1)

и затем я бы изменил функцию get_image_features, добавив (что-то вроде) эти две строки в конец. Я не могу сказать, как именно вы обрабатываете эти изображения, но идея состоит в том, чтобы сделать каждое изображение внутри каждого процесса, а затем сразу же сохранить его на диск:

df = pd.DataFrame({'filename':list_a,'image_features':list_b})
df.to_pickle("PATH_TO_FILE"+str(count)+".pickle")

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

1 голос
/ 18 мая 2019

Примечание: это не ответ, а быстрый список вопросов и предложений

  • Используете ли вы ThreadPool() from multiprocessing.pool? Это не очень хорошо задокументировано (в python3), и я бы лучше использовал ThreadPoolExecutor , (также см. здесь )
  • попытаться отладить, какие объекты хранятся в памяти в самом конце каждого цикла, например используя это решение , которое опирается на sys.getsizeof(), чтобы вернуть список всех объявленных globals() вместе с их объемом памяти.
  • также звоните del results (хотя это не должно быть слишком большим, я думаю)
0 голосов
/ 24 мая 2019

pd.DataFrame(...) может течь на некоторых сборках Linux (см. Проблему с github и "обходной путь" ), поэтому даже del df может не помочь.

В вашем случае решение от github можетбудет использоваться без «обезьяньих патчей» pd.DataFrame.__del__:

from ctypes import cdll, CDLL
try:
    cdll.LoadLibrary("libc.so.6")
    libc = CDLL("libc.so.6")
    libc.malloc_trim(0)
except (OSError, AttributeError):
    libc = None


if no libc:
    print("Sorry, but pandas.DataFrame may leak over time even if it's instances are deleted...")


CHUNK_SIZE = 20000


#file_list_1 contains 100,000 images
with ThreadPool(64) as pool:
    for count,f in enumerate(divide_chunks(file_list_1, CHUNK_SIZE)):
        # make the Pool of workers
        results = pool.map(get_image_features,f)
        # close the pool and wait for the work to finish 
        list_a, list_b = zip(*results)
        df = pd.DataFrame({'filename':list_a,'image_features':list_b})
        df.to_pickle("PATH_TO_FILE"+str(count)+".pickle")

        del df

        # 2 new lines of code:
        if libc:  # Fix leaking of pd.DataFrame(...)
            libc.malloc_trim(0)

print("pool closed")

PS. Это решение не поможет, если какой-либо отдельный фрейм данных слишком велик.Этому может помочь только уменьшение CHUNK_SIZE

0 голосов
/ 21 мая 2019

Мое решение для такого рода проблем заключается в использовании некоторого инструмента параллельной обработки. Я предпочитаю joblib , поскольку он позволяет распараллеливать даже локально созданные функции (которые являются «деталями реализации», и поэтому лучше не делать их глобальными в модуле). Мой другой совет: не используйте потоки (и пулы потоков) в python, используйте процессы (и пулы процессов) - это почти всегда лучшая идея! Просто убедитесь, что в joblib создан пул как минимум из 2 процессов, в противном случае он будет запускать все в исходном процессе python, и в результате RAM не будет освобожден. Как только рабочие процессы joblib автоматически закрываются, оперативная память, которую они выделяют, будет полностью освобождена ОС. Мое любимое оружие выбора - joblib. Параллельно . Если вам нужно передать работникам большие данные (например, больше 2 ГБ), используйте joblib.dump (чтобы записать объект python в файл в основном процессе) и joblib.load (прочитать его в рабочем процессе).

О del object: в python команда фактически не удаляет объект. Это только уменьшает его счетчик ссылок. Когда вы запускаете import gc; gc.collect(), сборщик мусора сам решает, какую память освободить, а какую оставить выделенной, и я не знаю, как заставить его освободить всю возможную память. Еще хуже, если бы некоторая память была фактически выделена не python, а вместо этого, например, в каком-то внешнем коде C / C ++ / Cython / etc, и код не связывал счетчик ссылок python с памятью, вы бы абсолютно ничего не сделали может сделать это, чтобы освободить его из Python, кроме того, что я написал выше, то есть, завершив процесс Python, который выделил ОЗУ, и в этом случае он будет гарантированно освобожден ОС. Вот почему единственный 100% надежный способ освободить часть памяти в python - это запустить код, который выделяет ее в параллельном процессе, а затем завершить процесс .

0 голосов
/ 21 мая 2019

Я думаю, что это будет возможно с сельдереем , благодаря сельдерею вы можете легко использовать параллелизм и параллелизм с python.

Обработка изображений кажется идемпотентной и атомарной, поэтому это может быть задача сельдерея .

Вы можете запустить нескольких рабочих , которые будут обрабатывать задачи - работать с образом.

Дополнительно он имеет конфигурацию для утечек памяти.

0 голосов
/ 19 мая 2019

Короче говоря, вы не можете освободить память обратно в интерпретаторе Python.Лучше всего было бы использовать многопроцессорность, поскольку каждый процесс может обрабатывать память самостоятельно.

Сборщик мусора «освободит» память, но не в том контексте, который вы ожидаете.Обработка страниц и пулов может быть изучена в исходном коде CPython.Здесь также есть статья высокого уровня: https://realpython.com/python-memory-management/

0 голосов
/ 19 мая 2019

НЕ вызывайте list (), он создает список в памяти того, что возвращается из div_chunks ().Именно здесь, вероятно, возникает проблема с памятью.

Вам не нужны все эти данные в памяти сразу.Просто повторяйте имена файлов по одному, чтобы все данные не помещались в память сразу.

Пожалуйста, опубликуйте трассировку стека, чтобы у нас было больше информации

...