Багатопроцесійна обробка Python: розуміння логіки, яка стоїть за `chunksize`


84

Які фактори визначають оптимальний chunksizeаргумент до таких методів multiprocessing.Pool.map()? .map()Метод представляється використовувати довільну евристику для його по замовчуванням chunksize (описано нижче); що мотивує цей вибір і чи існує більш продуманий підхід, заснований на певній ситуації / установці?

Приклад - скажіть, що я:

  • Перехід iterableдо, .map()який має ~ 15 мільйонів елементів;
  • Робота на машині з 24 ядрами та використання типових параметрів processes = os.cpu_count()у межах multiprocessing.Pool().

Моє наївне мислення полягає в тому, щоб дати кожному з 24 робітників однаковий розмір, тобто 15_000_000 / 24625 000. Великі шматки повинні зменшити товарообіг / накладні витрати при повному використанні всіх робітників. Але, схоже, тут відсутні деякі потенційні мінуси надання великих партій кожному працівникові. Це неповна картина, і чого мені не вистачає?


Частина мого запитання випливає із типової логіки для if chunksize=None: both .map()та .starmap()call .map_async(), яка виглядає так:

def _map_async(self, func, iterable, mapper, chunksize=None, callback=None,
               error_callback=None):
    # ... (materialize `iterable` to list if it's an iterator)
    if chunksize is None:
        chunksize, extra = divmod(len(iterable), len(self._pool) * 4)  # ????
        if extra:
            chunksize += 1
    if len(iterable) == 0:
        chunksize = 0

Яка логіка divmod(len(iterable), len(self._pool) * 4)? Це означає, що збивання буде ближче до 15_000_000 / (24 * 4) == 156_250. Який намір помножити len(self._pool)на 4?

Це робить отримане зменшення коефіцієнта на 4 менше, ніж моя "наївна логіка" згори, що складається з простого ділення довжини ітерабельного на кількість робітників у pool._pool.

Нарешті, є також цей фрагмент з документів Python, .imap()який ще більше викликає мою цікавість:

chunksizeАргумент є таким же , як той , що використовується map() методом. Для дуже довгих ітерацій із використанням великого значення for chunksizeможна зробити роботу набагато швидшою, ніж використання значення за замовчуванням 1.


Відповідна відповідь, яка є корисною, але трохи занадто високою: Питома обробка Python: чому великі шматки зменшуються повільніше? .


1
4Це довільно, і весь розрахунок chunksize є евристичним. Відповідним фактором є те, наскільки може змінюватися ваш фактичний час обробки. Трохи більше про це тут, поки я не встигну відповісти, якщо тоді все ще знадобиться.
Дарконаут

Ви перевіряли це питання ?
Ендрю Нагіб

1
Дякую @AndrewNaguib, я насправді якось не натрапив на це
Бред Соломон,

1
Тільки щоб повідомити: я не забув цього питання. Насправді, я працюю над канонічною відповіддю біблійних вимірів (безліч корисних фрагментів коду та вишуканої графіки) з того дня, як ви просили. Щедрість все-таки прийшла на 1-2 тижні занадто рано, щоб все закінчити, але я впевнений, що зможу кинути щось досить близько до закінчення терміну.
Дарконаут,

@BradSolomon Ласкаво просимо :). Це відповідає на ваше запитання?
Ендрю Нагіб

Відповіді:


193

Коротка відповідь

Алгоритм chunksize Пула є евристичним. Він надає просте рішення для всіх можливих сценаріїв проблем, які ви намагаєтесь вбити в методи Пулу. Як наслідок, його не можна оптимізувати для будь-якого конкретного сценарію.

Алгоритм довільно ділить ітерабельність приблизно в чотири рази більше шматків, ніж наївний підхід. Більша кількість фрагментів означає більше накладних витрат, але збільшена гнучкість планування. Як покаже ця відповідь, це в середньому призводить до більшого використання працівників, але без гарантії коротшого загального часу обчислень для кожного випадку.

"Це приємно знати", можна подумати, "але як знання цього мені допомагає у вирішенні конкретних проблем багатопроцесорної обробки?" Ну, це не так. Більш чесна коротка відповідь: "короткої відповіді немає", "багатопроцесорність - це складно" і "це залежить". Спостережуваний симптом може мати різне коріння, навіть для подібних сценаріїв.

Ця відповідь намагається надати вам основні поняття, які допоможуть вам отримати більш чітке уявлення про чорний ящик планування пулу. Він також намагається надати вам декілька основних інструментів для розпізнавання та уникнення потенційних скель, наскільки вони пов'язані з ущільненням.


Зміст

Частина І

  1. Визначення
  2. Цілі паралелізації
  3. Сценарії паралелізації
  4. Ризики Chunksize> 1
  5. Алгоритм Chunksize басейну
  6. Кількісна оцінка ефективності алгоритму

    6.1 Моделі

    6.2 Паралельний графік

    6.3 Ефективність

    6.3.1 Абсолютна ефективність розподілу (ADE)

    6.3.2 Відносна ефективність розподілу (RDE)

Частина ІІ

  1. Наївний проти алгоритму Чункізі Пула
  2. Перевірка реальності
  3. Висновок

Спочатку необхідно уточнити деякі важливі терміни.


1. Визначення


Шматок

iterableШматок тут - це частка аргументу, зазначена у виклику методу пулу. Тема цієї відповіді - те, як обчислюється збивання та які наслідки це може мати.


Завдання

Фізичне представлення завдання в робочому процесі з точки зору даних можна побачити на малюнку нижче.

фігура0

На малюнку показаний приклад виклику pool.map(), що відображається вздовж рядка коду, взятого з multiprocessing.pool.workerфункції, де завдання, прочитане з файлу, inqueueрозпаковується. workerє основною основною функцією у процесі MainThreadпулу-працівника. func-Argument вказано в басейні-методі буде тільки збігатися з func-змінного всередині worker-функцією для методів одного виклику , як apply_asyncі для imapз chunksize=1. Для решти методів пулу з chunksize-параметром функцією обробки funcбуде функція відображення ( mapstarабо starmapstar). Ця функція відображає вказаний користувачем func-параметр на кожному елементі переданого фрагмента ітерабельного (-> "map-tasks"). Час, який це займає, визначає завданнятакож як одиниця роботи .


Таскель

Хоча використання слова "завдання" для всієї обробки одного фрагмента відповідає коду всередині multiprocessing.pool, немає жодних вказівок на те , яким повинен бути один виклик до вказаного користувачем func, з одним елементом фрагмента в якості аргументів. що стосується. Щоб уникнути плутанини, що виникає внаслідок конфліктів імен (подумайте про maxtasksperchild-параметр __init__методу Pool- методу), ця відповідь буде стосуватися окремих одиниць роботи в рамках завдання, як taskel .

Taskel (від завдання + ель ня) є найменшою одиницею роботи в рамках завдання . Це одноразове виконання функції, зазначеної з func-параметром Pool-методу, що викликається з аргументами, отриманими з одного елемента переданого фрагмента . Завдання складається з chunksize taskels .


Накладні витрати на паралелізацію (PO)

PO складається з внутрішніх службових та накладних витрат на Python для міжпроцесорного зв'язку (IPC). Накладні витрати на завдання в Python постачаються з кодом, необхідним для упаковки та розпакування завдань та їх результатів. Накладні витрати IPC постачаються з необхідною синхронізацією потоків та копіюванням даних між різними адресними просторами (потрібні два кроки копіювання: батьківський -> черга -> дочірній). Сума накладних витрат на IPC залежить від ОС, апаратного забезпечення та розміру даних, що ускладнює узагальнення щодо впливу.


2. Цілі паралелізації

При використанні багатопроцесорної роботи, загальною метою (очевидно) є мінімізація загального часу обробки всіх завдань. Для досягнення цієї загальної мети нашою технічною метою повинна бути оптимізація використання апаратних ресурсів .

Деякі важливі підцілі для досягнення технічної мети:

  • мінімізувати накладні витрати на розпаралелювання (найвідоміше, але не поодинці: IPC )
  • висока ефективність використання всіх процесорних ядер
  • обмеження використання пам’яті, щоб запобігти надмірному підкачуванню ( сміттю ) ОС

Спочатку завдання повинні бути достатньо обчислювальними (інтенсивними), щоб повернути ПЗ, ми повинні заплатити за розпаралелювання. Актуальність PO зменшується із збільшенням абсолютного часу обчислення на таскель. Або, якщо сказати навпаки, чим більший абсолютний час обчислення на таскель для вашої проблеми, тим менш доречною стає потреба у зменшенні PO. Якщо ваші обчислення займуть години на клавішу, накладні витрати IPC будуть незначними в порівнянні. Тут головним завданням є запобігання холостим робочим процесам після розподілу всіх завдань. Зберігаючи всі завантажені ядра засобами, ми проводимо паралелізм, наскільки це можливо.


3. Сценарії паралелізації

Які фактори визначають оптимальний аргумент chunksize для таких методів, як багатопроцесорність.Pool.map ()

Основним фактором, про який йде мова, є те, на скільки час обчислень може змінюватись для окремих завдань. Щоб назвати це, вибір оптимального збивання визначається коефіцієнтом варіації ( CV ) для часу обчислення на таскель.

Два масштабні сценарії масштабу, що випливають із масштабів цієї варіації:

  1. Усім завданням потрібен абсолютно однаковий час обчислення.
  2. На закінчення клавіш може знадобитися секунди або дні.

Для кращого запам'ятовування я буду називати ці сценарії наступними:

  1. Щільний сценарій
  2. Широкий сценарій


Щільний сценарій

У щільному сценарії бажано розподілити всі завдання одразу, щоб мінімальний рівень IPC та перемикання контексту був мінімальним. Це означає, що ми хочемо створити лише стільки фрагментів, скільки робочих процесів. Як вже було зазначено вище, вага PO зростає із зменшенням часу обчислення на таскель.

Для максимальної пропускної здатності ми також хочемо, щоб усі робочі процеси були зайняті, поки всі завдання не будуть оброблені (без робочих режимів). Для цієї мети розподілені шматки повинні бути однакового розміру або близькі до.


Широкий сценарій

Основним прикладом широкого сценарію може бути проблема оптимізації, коли результати або швидко збігаються, або обчислення можуть зайняти години, а то й дні. Зазвичай непередбачувано, яку суміш "легких кистей" і "важких кистей" міститиме завдання в такому випадку, отже, не бажано розподіляти занадто багато кишень в пакеті завдань одночасно. Розподіл одразу менше завдань, ніж це можливо, означає збільшення гнучкості планування. Це потрібно тут для досягнення нашої підцілі - високого використання всіх ядер.

Якщо Poolметоди за замовчуванням будуть повністю оптимізовані для щільного сценарію, вони все частіше створюватимуть неоптимальні терміни для кожної проблеми, розташованої ближче до широкого сценарію.


4. Ризики Chunksize> 1

Розглянемо цей спрощений приклад псевдокоду Широкого сценарію -ітерабельного, який ми хочемо передати в метод пулу:

good_luck_iterable = [60, 60, 86400, 60, 86400, 60, 60, 84600]

Замість фактичних значень ми робимо вигляд, що бачимо необхідний час обчислення в секундах, для простоти лише 1 хвилину або 1 день. Ми вважаємо, що пул має чотири робочі процеси (на чотирьох ядрах) і chunksizeвстановлено значення 2. Оскільки наказ буде дотримано, шматки, що надсилаються робітникам, будуть такими:

[(60, 60), (86400, 60), (86400, 60), (60, 84600)]

Оскільки у нас достатньо робітників, а час обчислення досить великий, ми можемо сказати, що кожен робочий процес отримає шматок, над яким слід працювати в першу чергу. (Це не обов'язково має бути для швидко виконуваних завдань). Далі ми можемо сказати, що вся обробка триватиме близько 86400 + 60 секунд, оскільки це найвищий загальний час обчислення для фрагмента в цьому штучному сценарії, і ми розподіляємо фрагменти лише один раз.

Тепер розглянемо цей ітерабель, який має лише один елемент, що змінює своє положення порівняно з попереднім ітератором:

bad_luck_iterable = [60, 60, 86400, 86400, 60, 60, 60, 84600]

... та відповідні шматки:

[(60, 60), (86400, 86400), (60, 60), (60, 84600)]

Просто невдача з сортуванням нашого ітеративного майже подвоївся (86400 + 86400) наш загальний час обробки! Робітник, який отримує злісний (86400, 86400) шматок, блокує другу важку кисть у своєму завданні від розподілу до одного з бездіяльних робітників, який вже закінчив свої (60, 60) шматки. Ми, очевидно, не ризикували б таким неприємним результатом, якби встановили chunksize=1.

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

Як ми побачимо у розділі 6. Кількісна оцінка ефективності алгоритму , більші шматки також можуть призвести до неоптимальних результатів для щільних сценаріїв .


5. Chunksize-Алгоритм басейну

Нижче ви знайдете дещо змінену версію алгоритму всередині вихідного коду. Як бачите, я відрізав нижню частину і обернув її функцією для chunksizeзовнішнього обчислення аргументу. Я також замінив 4на factorпараметр і передав len()дзвінки на аутсорсинг .

# mp_utils.py

def calc_chunksize(n_workers, len_iterable, factor=4):
    """Calculate chunksize argument for Pool-methods.

    Resembles source-code within `multiprocessing.pool.Pool._map_async`.
    """
    chunksize, extra = divmod(len_iterable, n_workers * factor)
    if extra:
        chunksize += 1
    return chunksize

Щоб переконатися, що ми всі на одній сторінці, ось що divmod:

divmod(x, y)- це вбудована функція, яка повертається (x//y, x%y). x // yє поділом підлоги, повертаючи зменшений округлений коефіцієнт від x / y, тоді x % yяк операція за модулем повертає залишок від x / y. Звідси, наприклад, divmod(10, 3)повернення (3, 1).

Тепер , коли ви подивіться на chunksize, extra = divmod(len_iterable, n_workers * 4), ви побачите n_workersтут є дільником yв x / yі множення 4, без додаткового регулювання через if extra: chunksize +=1пізніше, призводить до первісного chunksize , по крайней мере в чотири рази менше (для len_iterable >= n_workers * 4) , ніж це було б в іншому випадку.

Для перегляду ефекту множення 4на на проміжний результат збиття розглянемо цю функцію:

def compare_chunksizes(len_iterable, n_workers=4):
    """Calculate naive chunksize, Pool's stage-1 chunksize and the chunksize
    for Pool's complete algorithm. Return chunksizes and the real factors by
    which naive chunksizes are bigger.
    """
    cs_naive = len_iterable // n_workers or 1  # naive approach
    cs_pool1 = len_iterable // (n_workers * 4) or 1  # incomplete pool algo.
    cs_pool2 = calc_chunksize(n_workers, len_iterable)

    real_factor_pool1 = cs_naive / cs_pool1
    real_factor_pool2 = cs_naive / cs_pool2

    return cs_naive, cs_pool1, cs_pool2, real_factor_pool1, real_factor_pool2

Функція, наведена вище, обчислює наївний chunksize ( cs_naive) та першого кроку chunksize алгоритму chunksize Пула ( cs_pool1), а також chunksize для повного алгоритму Pool ( cs_pool2). Далі він обчислює реальні коефіцієнти rf_pool1 = cs_naive / cs_pool1 і rf_pool2 = cs_naive / cs_pool2, які повідомляють нам, у скільки разів наївно обчислені розміри більші, ніж внутрішні версії (-и) Пулу.

Нижче ви бачите дві фігури, створені за допомогою цієї функції. На лівій фігурі просто показані шматки n_workers=4до ітерируваної довжини 500. На правому малюнку показано значення для rf_pool1. Для ітерируемой довжини 16дійсним коефіцієнтом стає >=4(для len_iterable >= n_workers * 4), а його максимальним значенням є 7ітерирувана довжина 28-31. Це значне відхилення від початкового фактора, 4до якого збігається алгоритм, для більш тривалих ітерацій. "Довше" тут відносне і залежить від кількості вказаних робітників.

Фігура 1

Пам'ятайте, у chunksize cs_pool1все ще не вистачає -коригування extraз залишком, що divmodміститься в cs_pool2повному алгоритмі.

Алгоритм продовжується:

if extra:
    chunksize += 1

Зараз у випадках, коли є залишок (an extraвід операції divmod), збільшення зменшення на 1, очевидно, не може працювати для кожного завдання. Зрештою, якби це сталося, для початку не було б залишку.

Як ви можете бачити на малюнку нижче, то « екстра-лікування » має ефект, що реальний фактор для rf_pool2нині сходиться в напрямку 4від нижче 4 і відхилення дещо м'якше. Стандартне відхилення для n_workers=4і len_iterable=500падає від 0.5233для rf_pool1до 0.4115для rf_pool2.

малюнок2

Зрештою, збільшення chunksizeна 1 має наслідком, що останнє передане завдання має лише розмір len_iterable % chunksize or chunksize.

Тим цікавішим і як ми побачимо пізніше, тим більше наслідком буде ефект додаткової обробки, проте можна спостерігати за кількістю сформованих шматків ( n_chunks). Для достатньо тривалих ітерацій завершений алгоритм chunksize пулу ( n_pool2на малюнку нижче) стабілізує кількість шматків на n_chunks == n_workers * 4. На відміну від цього, наївний алгоритм (після початкової відрижки) постійно чергується між собою n_chunks == n_workersі n_chunks == n_workers + 1в міру збільшення довжини ітерабельного.

малюнок3

Нижче ви знайдете дві розширені інформаційні функції для Pool і наївний алгоритм chunksize. Результат цих функцій знадобиться в наступному розділі.

# mp_utils.py

from collections import namedtuple


Chunkinfo = namedtuple(
    'Chunkinfo', ['n_workers', 'len_iterable', 'n_chunks',
                  'chunksize', 'last_chunk']
)

def calc_chunksize_info(n_workers, len_iterable, factor=4):
    """Calculate chunksize numbers."""
    chunksize, extra = divmod(len_iterable, n_workers * factor)
    if extra:
        chunksize += 1
    # `+ (len_iterable % chunksize > 0)` exploits that `True == 1`
    n_chunks = len_iterable // chunksize + (len_iterable % chunksize > 0)
    # exploit `0 == False`
    last_chunk = len_iterable % chunksize or chunksize

    return Chunkinfo(
        n_workers, len_iterable, n_chunks, chunksize, last_chunk
    )

Нехай вас не бентежить, мабуть, несподіваний вигляд calc_naive_chunksize_info. Значення extrafrom divmodне використовується для обчислення chunksize.

def calc_naive_chunksize_info(n_workers, len_iterable):
    """Calculate naive chunksize numbers."""
    chunksize, extra = divmod(len_iterable, n_workers)
    if chunksize == 0:
        chunksize = 1
        n_chunks = extra
        last_chunk = chunksize
    else:
        n_chunks = len_iterable // chunksize + (len_iterable % chunksize > 0)
        last_chunk = len_iterable % chunksize or chunksize

    return Chunkinfo(
        n_workers, len_iterable, n_chunks, chunksize, last_chunk
    )

6. Кількісна оцінка ефективності алгоритму

Тепер, після того, як ми побачили, як висновок Poolалгоритму chunksize виглядає по-різному порівняно з результатом наївного алгоритму ...

  • Як визначити, чи підхід Пула насправді щось покращує ?
  • І що саме це могло що - то бути?

Як показано в попередньому розділі, для довших ітерацій (більша кількість завдань) алгоритм chunksize Пула приблизно ділить ітерабель на чотири рази більше шматків, ніж наївний метод. Менші шматки означають більше завдань, а більше завдань - більше накладних витрат на паралелізацію (PO) , вартість яких повинна бути порівняна з перевагою підвищеної гнучкості планування ( нагадуйте "Ризики Chunksize> 1" ).

З цілком очевидних причин базовий алгоритм chunksize не може зрівняти гнучкість планування з PO . Накладні витрати IPC залежать від ОС, апаратного забезпечення та розміру даних. Алгоритм не може знати, на якому обладнанні ми запускаємо наш код, і не має уявлення про те, скільки часу буде потрібно для завершення роботи клавіатури. Це евристика, що забезпечує базову функціональність для всіх можливих сценаріїв. Це означає, що його не можна оптимізувати для будь-якого конкретного сценарію. Як вже згадувалося раніше, PO також стає дедалі меншим занепокоєнням із збільшенням часу обчислення на таскель (негативна кореляція).

Коли ви згадуєте цілі паралелізації з розділу 2, одним пунктом було:

  • висока ефективність використання всіх процесорних ядер

Вищезазначене що - то , БАСЕЙНИ chunksize-алгоритм може спробувати поліпшити є мінімізацією холостого ходу робочих-процеси , відповідно використання CPU-ядер .

Повторне запитання щодо SO multiprocessing.Poolзадають люди, які задаються питанням про невикористані ядра / робочі процеси на холостому ходу в ситуаціях, коли ви очікуєте, що всі робочі процеси зайняті. Хоча це може мати багато причин, простої робочих процесів до кінця обчислення - це спостереження, яке ми часто можемо зробити, навіть із щільними сценаріями (рівний час обчислення на клавішу) у випадках, коли кількість робітників не є дільником числа із шматків ( n_chunks % n_workers > 0).

Питання зараз:

Як ми можемо практично перекласти наше розуміння шматки в щось, що дозволяє пояснити спостереження за використанням працівників або навіть порівняти ефективність різних алгоритмів у цьому відношенні?


6.1 Моделі

Для отримання глибшого розуміння тут нам потрібна форма абстракції паралельних обчислень, яка спрощує надто складну реальність до керованої міри складності, зберігаючи при цьому значення у визначених межах. Така абстракція називається моделлю . Реалізація такої " Моделі паралелізації" (РМ) генерує відображені на робочому місці метадані (позначки часу), як справжні обчислення, якби дані збиралися. Створені моделлю метадані дозволяють прогнозувати метрики паралельних обчислень за певних обмежень.

малюнок4

Однією з двох підмоделей у визначеному тут ПМ є Модель розподілу (СР) . DM пояснює , як атомні одиниці роботи (taskels) розподілені по паралельним робочим і часу , коли немає інших факторів , крім відповідного chunksize-алгоритму, число робочих, введення-ітерація (кількість taskels) і їх тривалості обчислень не зважають . Це означає, що будь-яка форма накладних витрат не включена.

Для отримання повного PM , DM розширюється накладними моделями (OM) , що представляють різні форми паралелізації накладних витрат (PO) . Таку модель потрібно калібрувати для кожного вузла окремо (апаратні, ОС-залежності). Скільки форм накладних витрат представлено в ОМ , залишається відкритим, і тому може існувати безліч ОМ з різним ступенем складності. Який рівень точності потребує реалізований ОМ , визначається загальною вагою ПО для конкретного обчислення. Коротші кисті призводять до більшої ваги PO , що, в свою чергу, вимагає більш точного OM , якби ми намагалися передбачити ефективність паралелізації (PE) .


6.2 Паралельний графік (PS)

Паралельний графік являє собою двовимірне подання паралельних обчислень, де вісь х представляє час і вісь у представляє собою пул паралельних робочих. Кількість робітників і загальний час обчислень позначають протяжність прямокутника, в якому намальовано менші прямокутники. Ці менші прямокутники представляють атомні одиниці роботи (кисті).

Нижче ви знайдете візуалізацію PS, намальовану даними з DM алгоритму chunksize Пула для щільного сценарію .

малюнок5

  • Вісь х розділена на рівні одиниці часу, де кожна одиниця відповідає часу обчислення, необхідного таскелю.
  • Вісь y ділиться на кількість робочих процесів, які використовує пул.
  • Таскель тут відображається як найменший прямокутник блакитного кольору, розміщений у часовій шкалі (графіку) анонімізованого робочого процесу.
  • Завдання - це один або кілька завдань на робочій шкалі часу, які постійно підсвічуються однаковим відтінком.
  • Одиниці часу простою представлені через плитки червоного кольору.
  • Паралельний графік розділений на розділи. Останній розділ - це хвіст.

Назви складених частин видно на малюнку нижче.

рисунок6

У повному ПМ, включаючи ОМ , Частка холостого ходу не обмежується лише хвостом, але також містить простір між завданнями і навіть між кистями.


6.3 Ефективність

Моделі, представлені вище, дозволяють кількісно визначити рівень використання робочих. Ми можемо розрізнити:

  • Ефективність розподілу (DE) - обчислюється за допомогою DM (або спрощеного методу для щільного сценарію ).
  • Ефективність паралелізації (PE) - або обчислюється за допомогою каліброваного PM (прогнозування), або розраховується за метаданими реальних обчислень.

Важливо зазначити, що обчислені коефіцієнти корисної дії не корелюють автоматично з більш швидкими загальними обчисленнями для даної задачі розпаралелювання. Використання працівника в цьому контексті лише розрізняє працівника, який має розпочату, але ще не закінчену таскель, та працівника, який не має такої "відкритої" таскели. Це означає, що можливий холостий хід протягом часового проміжку кисті не реєструється.

Всі вищезазначені коефіцієнти корисної дії в основному отримуються шляхом обчислення коефіцієнта розподілу зайнятості / паралельного розкладу . Різниця між DE та PE полягає в тому, що Busy Share займає меншу частину загального паралельного розкладу для PM, що розширений накладними витратами .

Ця відповідь далі обговорюватиме лише простий метод розрахунку DE для щільного сценарію. Це достатньо адекватно для порівняння різних алгоритмів chunksize, оскільки ...

  1. ... DM - це частина PM , яка змінюється з використанням різних алгоритмів chunksize.
  2. ... Щільний сценарій з однаковою тривалістю обчислення на таскель зображує "стабільний стан", для якого ці часові проміжки випадають із рівняння. Будь-який інший сценарій може просто призвести до випадкових результатів, оскільки порядок виконання завдань має значення.

6.3.1 Абсолютна ефективність розподілу (ADE)

Цю базову ефективність можна розрахувати загалом, розділивши зайняту частку на весь потенціал паралельного розкладу :

Абсолютна ефективність розподілу (ADE) = Графік зайнятості / паралельного розподілу

Для щільного сценарію спрощений код обчислення виглядає так:

# mp_utils.py

def calc_ade(n_workers, len_iterable, n_chunks, chunksize, last_chunk):
    """Calculate Absolute Distribution Efficiency (ADE).

    `len_iterable` is not used, but contained to keep a consistent signature
    with `calc_rde`.
    """
    if n_workers == 1:
        return 1

    potential = (
        ((n_chunks // n_workers + (n_chunks % n_workers > 1)) * chunksize)
        + (n_chunks % n_workers == 1) * last_chunk
    ) * n_workers

    n_full_chunks = n_chunks - (chunksize > last_chunk)
    taskels_in_regular_chunks = n_full_chunks * chunksize
    real = taskels_in_regular_chunks + (chunksize > last_chunk) * last_chunk
    ade = real / potential

    return ade

Якщо немає Холостий Share , Busy частка буде дорівнює до Parallel Schedule , отже , ми отримуємо ADE 100%. У нашій спрощеній моделі це сценарій, коли всі доступні процеси будуть зайняті протягом усього часу, необхідного для обробки всіх завдань. Іншими словами, вся робота фактично паралелізується до 100 відсотків.

Але чому я тримаю в вигляді PE в якості абсолютного PE тут?

Щоб зрозуміти це, ми повинні розглянути можливий випадок для chunksize (cs), який забезпечує максимальну гнучкість планування (також, кількість гірців може бути. Випадковість?):

__________________________________ ~ ОДИН ~ __________________________________

Якщо у нас, наприклад, є чотири робочі процеси та 37 завдань, то будуть і робочі, що працюють в режимі холостого ходу, навіть chunksize=1тому, що n_workers=4це не дільник 37. Залишок від ділення 37/4 дорівнює 1. Цей єдиний залишився таскель повинен бути обробляється єдиним працівником, тоді як решта троє працюють на холостому ходу.

Так само, як і раніше, буде один працівник, що працює в режимі холостого ходу з 39 завданнями, як ви можете бачити на малюнку нижче.

фігура7

Якщо порівняти верхню Parallel розклад для chunksize=1з нижче версії для chunksize=3, ви помітите , що верхня Parallel Розклад менше, терміни на вісь х коротше. Зараз має стати очевидним, як несподівано більші шматки також можуть призвести до збільшення загального часу обчислень, навіть для щільних сценаріїв .

Але чому б просто не використати довжину осі х для розрахунків ефективності?

Оскільки накладні витрати в цій моделі не містяться. Це буде різним для обох послідовностей, отже вісь х насправді не є безпосередньо порівнянною. Накладні витрати все одно можуть призвести до більш тривалого загального часу обчислення, як показано у випадку 2 з малюнка нижче.

малюнок8


6.3.2 Відносна ефективність розподілу (RDE)

Значення ADE не містить інформації, якщо можливий кращий розподіл завдань, якщо chunksize встановлено на 1. Краще тут все-таки означає меншу частку холостого ходу .

Щоб отримати значення DE, скориговане для максимально можливого DE , ми повинні розділити розглянутий ADE на ADE, який ми отримуємо chunksize=1.

Відносна ефективність розподілу ( RDE ) = ADE_cs_x / ADE_cs_1

Ось як це виглядає в коді:

# mp_utils.py

def calc_rde(n_workers, len_iterable, n_chunks, chunksize, last_chunk):
    """Calculate Relative Distribution Efficiency (RDE)."""
    ade_cs1 = calc_ade(
        n_workers, len_iterable, n_chunks=len_iterable,
        chunksize=1, last_chunk=1
    )
    ade = calc_ade(n_workers, len_iterable, n_chunks, chunksize, last_chunk)
    rde = ade / ade_cs1

    return rde

RDE , як тут визначено, по суті є казкою про хвіст паралельного розкладу . На RDE впливає максимально ефективний шунксиз, що міститься в хвості. (Цей хвіст може мати довжину по осі Х chunksizeабо last_chunk.) Це призводить до того, що ПСИ природно сходиться до 100% (навіть) для всіх видів «хвостових виглядає» як показано на малюнку , наведеному нижче.

малюнок9

Низький рівень RDE ...

  • є вагомою підказкою щодо потенціалу оптимізації.
  • природно стає менш імовірним для довших ітерацій, оскільки відносна частина хвоста загального паралельного розкладу зменшується.

Ви можете знайти частина II цієї відповіді тут .


51
Одна з найепічніших відповідей, яку я бачив на SO.
Крістіан Лонг

4
О, це була ваша коротка відповідь: P
d_kennetz

1
Але forreal .. це відмінна відповідь. Я поставив запитання для майбутніх випадків, коли я хочу це краще зрозуміти. Пробирання через нього навчило мене вже багато чому! Дякую
d_kennetz

2
@ L.Iridium Ласкаво просимо! Я використовував matplotlib там, де це можливо, інакше ... LibreOffice calc + Pinta (основне редагування зображень). Так, я знаю ... але це якось працює. ;)
Дарконаут,

2
Перша відповідь із змістом, побаченим на SO.
tly_alex

51

Про цю відповідь

Ця відповідь є частиною II прийнятої вище відповіді .


7. Наївний проти алгоритму Чункізації Пула

Перш ніж вдаватися в подробиці, розгляньте два GIF-файли нижче. Для діапазону різної iterableдовжини вони показують, як два порівняних алгоритму обробляють передане iterable(на той час це буде послідовність) і як можуть розподілятися отримані завдання. Порядок робітників є випадковим, і кількість розподілених завдань на одного працівника насправді може відрізнятися від цих зображень для легких кистей та або кистей у широкому сценарії. Як зазначалося раніше, накладні витрати також сюди не входять. Однак для досить важких завдань у Щільному сценарії з неможливими розмірами переданих даних реальні обчислення створюють дуже схожу картину.

cs_4_50

cs_200_250

Як показано в розділі « 5. пул Chunksize-алгоритмі », з chunksize-алгоритмом пулу кількість порцій стабілізується в n_chunks == n_workers * 4протягом досить великий ітеріруемого, в той час як він тримає перемикання між n_chunks == n_workersі n_chunks == n_workers + 1з наївним підходом. Для наївного алгоритму застосовується: Тому що n_chunks % n_workers == 1це Trueза n_chunks == n_workers + 1новий розділ буде створений , де буде використовуватися тільки один робітник.

Наївний алгоритм чунки:

Можливо, ви думаєте, що створили завдання для однакової кількості робітників, але це буде справедливо лише для випадків, коли немає залишку для len_iterable / n_workers. Якщо є залишок, з’явиться новий розділ із лише одним завданням для одного працівника. На той момент ваші обчислення більше не будуть паралельними.

Нижче ви бачите рисунок, подібний до малюнка, показаного в главі 5, але відображає кількість розділів замість кількості шматків. Для повного алгоритму chunksize-пулу ( n_pool2), n_sectionsвін стабілізується за сумнозвісного, жорстко закодованого фактора 4. Для наївного алгоритму n_sectionsбуде чергуватися один і два.

малюнок10

Для chunksize-алгоритму пулу, стабілізація на n_chunks = n_workers * 4основі раніше згаданих додаткове лікування , перешкоджає створенню нового розділу тут і тримає Холостий Share обмежується одним працівником в протягом досить довго ітеріруемимі. Мало того, але алгоритм буде продовжувати зменшувати відносний розмір холостого ходу , що призводить до значення RDE, що збігається до 100%.

«Досить довго» для n_workers=4це len_iterable=210, наприклад. Для ітерацій, рівних або більших за цей, Частка холостого ходу буде обмежена одним працівником, ознакою, спочатку втраченою через 4-множення в алгоритмі chunksize, насамперед.

фігура11

Наївний алгоритм chunksize також сходиться до 100%, але робить це повільніше. Ефект зближення залежить виключно від того, що відносна частина хвоста скорочується у випадках, коли буде дві секції. Цей хвіст, в якому працює лише один працівник, обмежений довжиною осі х n_workers - 1, можливим максимальним залишком для len_iterable / n_workers.

Як різняться фактичні значення RDE для наївного та алгоритму chunksize Пула?

Нижче ви знайдете два теплові карти , що показують ПСИ значення для всіх Iterable довжини до 5000, для всіх чисел працівників від 2 до 100. Колірна шкала йде від 0,5 до 1 (50% -100%). Ви помітите набагато більше темних областей (нижчі значення RDE) для наївного алгоритму на лівій тепловій карті. На відміну від цього, алгоритм chunksize Пула праворуч малює набагато більш сонячну картину.

фігура12

Діагональний градієнт нижнього лівого темного кута проти верхнього правого яскравого кута знову показує залежність від кількості робочих для того, що можна назвати "довгим ітераційним".

Наскільки погано це може отримати з кожним алгоритмом?

З chunksize-алгоритму Pool це ПСИ значення 81,25% є найменше значення для діапазону робочих і Iterable довжини , зазначених вище:

фігура13

З наївним алгоритмом chunksize все може обернутися набагато гірше. Найнижчий розрахований показник RDE тут становить 50,72%. У цьому випадку майже половину обчислювального часу працює лише один працівник! Тож, пильнуйте, горді власники Knights Landing . ;)

малюнок14


8. Перевірка реальності

У попередніх главах ми розглядали спрощену модель чисто математичної задачі розподілу, позбавлену дрібниць, що роблять багатопроцесорність такою неприємною темою. Щоб краще зрозуміти , як далеко від моделі розподілу (DM) в поодинці може сприяти пояснити бачимо використання робочого насправді, тепер ми будемо приймати деякі погляди на паралельних графіках намальованих реальних обчислень.

Налаштування

Наведені нижче графіки стосуються паралельного виконання простої фіксованої функції, пов'язаної з процесором, яка викликається різними аргументами, щоб ми могли спостерігати, як намальований паралельний графік змінюється в залежності від вхідних значень. "Робота" в цій функції складається лише з ітерації над об'єктом діапазону. Цього вже достатньо для зайнятості ядра, оскільки ми передаємо величезні цифри. За бажання функція бере деяку унікальну додаткову клавішу, dataяка повертається у незмінному вигляді. Оскільки кожна кисть складає точно такий же обсяг роботи, ми все ще маємо справу з щільним сценарієм тут.

Функція прикрашена обгорткою, яка бере позначки часу з роздільною здатністю ns (Python 3.7+). Мітки часу використовуються для обчислення часового інтервалу таскеля і, отже, дозволяють скласти емпіричний паралельний графік.

@stamp_taskel
def busy_foo(i, it, data=None):
    """Dummy function for CPU-bound work."""
    for _ in range(int(it)):
        pass
    return i, data


def stamp_taskel(func):
    """Decorator for taking timestamps on start and end of decorated
    function execution.
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time_ns()
        result = func(*args, **kwargs)
        end_time = time_ns()
        return (current_process().name, (start_time, end_time)), result
    return wrapper

Метод карти пулу також оформлений таким чином, що приурочується лише сам виклик зі карти карти. "Початок" і "кінець" цього виклику визначають мінімум і максимум на осі x створеного паралельного розкладу.

Ми будемо спостерігати за обчисленням 40 завдань на чотирьох робочих процесах на машині з цими специфікаціями: Python 3.7.1, Ubuntu 18.04.2, Процесор Intel® Core ™ i7-2600K при 3,40 ГГц × 8

Вхідні значення, які будуть змінюватися, - це кількість ітерацій у циклі for (30k, 30M, 600M) та додатково розмір даних надсилання (на таскель, numpy-ndarray: 0 MiB, 50 MiB).

...
N_WORKERS = 4
LEN_ITERABLE = 40
ITERATIONS = 30e3  # 30e6, 600e6
DATA_MiB = 0  # 50

iterable = [
    # extra created data per taskel
    (i, ITERATIONS, np.arange(int(DATA_MiB * 2**20 / 8)))  # taskel args
    for i in range(LEN_ITERABLE)
]


with Pool(N_WORKERS) as pool:
    results = pool.starmap(busy_foo, iterable)

Наведені нижче пробіжки були обрані таким чином, щоб мати однакове впорядкування фрагментів, щоб ви могли краще помітити відмінності порівняно з паралельним розкладом із моделі розподілу, але не забувайте, що порядок, в якому працівники отримують своє завдання, є недетермінованим.

Прогнозування ДМ

Знову ж таки, Модель розподілу "передбачає" паралельний графік, як ми вже бачили в главі 6.2:

фігура15

1-й ЗАПУСК: 30 тис. Ітерацій і 0 Мб даних на клавішу

фігура16

Наш перший пробіг тут дуже короткий, кисті дуже "легкі". Загальний pool.starmap()виклик зайняв лише 14,5 мс. Ви помітите, що, на відміну від DM , холостий хід не обмежується лише хвостовим відділом, але також відбувається між завданнями і навіть між кистями. Це тому, що наш реальний графік тут, природно, включає всілякі накладні витрати. Холостий хід тут означає просто все, що не входить до кисті. Можливий справжній холостий хід під час роботи таскеля не враховується, як уже згадувалося раніше.

Далі ви можете бачити, що не всі працівники виконують свої завдання одночасно. Це пов’язано з тим, що всі працівники харчуються спільно, inqueueі лише один працівник може читати з нього одночасно. Те саме стосується і outqueue. Це може спричинити більші збої, як тільки ви передасте не граничні розміри даних, як ми побачимо пізніше.

Крім того, ви можете бачити, що, незважаючи на той факт, що кожна кисть складає однакову кількість роботи, фактичний виміряний проміжок часу для кисті дуже різниться. Завдання, що роздаються працівнику-3 та працівникові-4, потребують більше часу, ніж ті, що обробляються першими двома робітниками. Для цього запуску, я підозрюю, це пов’язано з тим, що на той момент турбопідсилювач більше не доступний на ядрах для worker-3/4, тому вони обробляли свої завдання з меншою тактовою частотою.

Все обчислення настільки легке, що апаратні засоби або фактори хаосу, запроваджені ОС, можуть різко перекосити PS . Обчислення є "листом на вітрі", і DM- прогноз має мало значення, навіть для теоретично придатного сценарію.

2-й ЗАПУСК: 30 М ітерацій і 0 Мб даних на клавішу

фігура17

Збільшення кількості ітерацій у циклі for з 30000 до 30 мільйонів призводить до реального паралельного розкладу, який близький до ідеального збігу з тим, який передбачений даними DM , ура! Тепер обчислення на клавішу достатньо важкі, щоб маргіналізувати частини холостого ходу на початку та між ними, дозволяючи видимим лише велику частку холостого ходу, яку передбачав DM .

3-й ЗАПУСК: 30 М ітерацій і 50 Мб даних на клавішу

фігура18

Зберігаючи 30 мільйонів ітерацій, але додатково надсилаючи 50 Мб на клавішу вперед і назад, зображення знову викривляється. Тут ефект чергування добре видно. Worker-4 повинен чекати довше свого другого завдання, ніж Worker-1. А тепер уявіть цей графік із 70 робітниками!

Якщо обчислювальні завдання дуже обчислювальні, але вони надають помітний обсяг даних як корисне навантаження, вузьке місце однієї спільної черги може запобігти будь-яким додатковим перевагам додавання більшої кількості працівників до пулу, навіть якщо вони підтримуються фізичними ядрами. У такому випадку Worker-1 міг виконати своє перше завдання і очікувати нового ще до того, як Worker-40 отримає своє перше завдання.

Тепер має стати очевидним, чому час обчислень Poolне завжди зменшується відповідно до кількості робітників. Надсилання порівняно великих обсягів даних може призвести до сценаріїв, коли більшу частину часу витрачають на очікування копіювання даних в адресний простір працівника, і лише одного працівника можна подавати одночасно.

4-й РАН: 600М ітерацій і 50 Мб даних на таскель

фігура19

Тут ми знову надсилаємо 50 Мб, але збільшуємо кількість ітерацій з 30 М до 600 М, що збільшує загальний час обчислення з 10 до 152 с. Намальований паралельний розклад знову близький до ідеального збігу з прогнозованим, накладні витрати на копіювання даних маргіналізовані.


9. Висновок

Обговорюване множення шляхом 4збільшення гнучкості планування, але також використовує нерівномірність розподілу таскелів. Без цього множення частка холостого ходу була б обмежена одним працівником навіть для коротких ітерацій (для DM із щільним сценарієм). Алгоритму chunksize Пула потрібні ітератори введення, щоб мати певний розмір, щоб відновити цю рису.

Як ми сподіваємось показали цю відповідь, алгоритм chunksize Пула призводить до кращого використання ядра в порівнянні з наївним підходом, принаймні для середнього випадку, оскільки довгі накладні витрати не враховуються. Наївний алгоритм тут може мати коефіцієнт ефективності розподілу (DE) до ~ 51%, тоді як алгоритм збивання Пула має низький рівень - ~ 81%. DE, однак, не включає накладні витрати на паралелізацію (PO), як IPC. Розділ 8 показав, що DE все ще може мати велику прогностичну силу для Щільного сценарію з маргіналізованими накладними витратами.

Незважаючи на той факт, що алгоритм chunksize Пула досягає більш високого значення DE в порівнянні з наївним підходом, він не забезпечує оптимальних розподілів таскелів для кожного вхідного сузір'я. Хоча простий статичний алгоритм шматування не може оптимізувати (включаючи накладні витрати) ефективність паралелізації (PE), немає жодної невід'ємної причини, чому він не завжди може забезпечити відносну ефективність розподілу (RDE) на 100%, це означає, ту саму DE, що і з chunksize=1. Простий алгоритм chunksize складається лише з базової математики і може будь-яким способом "нарізати пиріг".

На відміну від реалізації Пулом алгоритму "рівного розміру", алгоритм "парного розміру" забезпечує RDE 100% для кожної len_iterable/ n_workersкомбінації. Алгоритм парного розміру з невеликим розміром буде дещо складнішим для реалізації в джерелі Pool, але його можна модулювати поверх існуючого алгоритму, просто упакувавши завдання зовні (я посилаюся звідси на випадок, якщо залишу запитання / відповідь на як це зробити).


6

Я думаю, що частиною того, чого вам не вистачає, є те, що ваша наївна оцінка передбачає, що кожна одиниця роботи займає однакову кількість часу, і в цьому випадку ваша стратегія буде найкращою. Але якщо деякі завдання закінчуються раніше, ніж інші, деякі ядра можуть простоювати, чекаючи закінчення повільних завдань.

Таким чином, розбиваючи шматки на 4 рази більше частин, тоді, якщо один шматок закінчив достроково, це ядро ​​може розпочати наступний шматок (тоді як інші ядра продовжують працювати на своїх більш повільних шматках).

Я не знаю, чому вони вибрали коефіцієнт 4 точно, але це був би компроміс між мінімізацією накладних витрат на код карти (який вимагає найбільших можливих шматків) і балансуванням шматків, що займають різну кількість разів (що вимагає найменшого можливого фрагмента ).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.