Параллельные вычисления
Зачем нужен параллелизм
NVIDIA H100 - 16896 CUDA-ядер. GPT-4 тренировался на 25 000 A100 параллельно. PyTorch по умолчанию задействует все ядра CPU и всю GPU. И при этом наивно написанный код использует одно ядро из всех. Но вот ловушка: параллелизм не ускоряет произвольную программу. Закон Амдала говорит жестко: если 5% кода последовательно - максимум 20x, хоть с миллионом ядер. Именно поэтому PyTorch разделяет data parallelism и model parallelism - это не прихоть, это математика.
- **LLM training:** GPT-4 обучался на 25 000 A100 (~100 дней). Один A100 - то же самое за 6 800 лет. Data parallelism + model parallelism - единственный способ уложиться в бюджет команды
- **GPU inference:** H100 - 79.5 TFLOPS FP32, 3 958 TFLOPS FP8. Разрыв в 50x достигается именно параллелизмом внутри одного чипа - 16 896 ядер считают независимые части тензора одновременно
- **Веб-серверы:** Nginx держит 10 000+ соединений на одном ядре через конкурентность - не параллелизм. Понимать разницу критично: ошибка в выборе модели стоит 10x производительности
Закон Мура и конец частотной гонки
2005 год. Intel тихо убивает Pentium 4 Prescott - чип на 3.8 ГГц, который грелся как утюг. Следующая версия на 4+ ГГц была готова - и отменена. Физика не позволила: мощность процессора растёт как куб от частоты. 10 ГГц потребляли бы сотни ватт, расплавив что угодно. С тех пор транзисторов становится больше, а частота - константа.
| Год | Процессор | Частота | Транзисторов | Ядер |
|---|---|---|---|---|
| 1993 | Pentium | 60 МГц | 3.1M | 1 |
| 2000 | Pentium 4 | 1.5 ГГц | 42M | 1 |
| 2004 | Pentium 4 Prescott | 3.8 ГГц | 125M | 1 |
| 2006 | Core 2 Duo | 2.4 ГГц | 291M | 2 ← поворот! |
| 2024 | Core i9-14900K | 6.0 ГГц | 20,000M | 24 |
**Закон Мура** (1965): количество транзисторов на кристалле удваивается каждые ~2 года. Закон до сих пор работает! Но **следствие** "процессоры становятся быстрее каждый год" перестало выполняться. Транзисторы теперь идут на **больше ядер**, а не на **более быструю частоту**.
Herb Sutter назвал это в 2005-м «The Free Lunch Is Over» - и не преувеличил. Программа, написанная в 2000-м, к 2004-му ускорялась сама: Intel выпускала новый процессор, и всё работало быстрее. После 2005-го этот халявный прирост закончился. Однопоточный код на Core i9-14900K работает примерно столь же быстро, как на Core 2 Quad 2008 года. Транзисторов в 70 раз больше - но однопоточный код их не видит.
Herb Sutter: The Free Lunch Is Over (2005)
Статья Герба Саттера "The Free Lunch Is Over" стала манифестом параллельного программирования. Он предупредил: эра, когда программа ускоряется просто от покупки нового процессора, закончилась. Разработчикам придётся учиться параллелизму - или мириться со стагнацией производительности.
Почему тактовая частота процессоров перестала расти после 2005 года?
Закон Амдала
Добавить ядра - звучит как решение. Джин Амдал в 1967-м показал, почему это иллюзия. **Последовательная часть программы - абсолютный потолок ускорения**. Не важно, сколько ядер добавить - горлышко не расширится. Именно этот закон объясняет, почему PyTorch разделяет data parallelism (каждый GPU - своя копия модели) и model parallelism (слои модели на разных GPU): без разбивки последовательной части никакое количество H100 не помогает.
**Последовательное горлышко:** если 5% кода нельзя распараллелить, максимальное ускорение - 20x. Хоть миллион ядер добавьте. Вот почему оптимизация последовательной части часто важнее добавления ядер.
**Закон Густафсона** (1988) - более оптимистичный взгляд: с ростом числа процессоров мы увеличиваем и **размер задачи**. Если параллельная часть масштабируется с данными, ускорение растёт линейно: S(p) = p - s·(p-1). Это актуально для big data и научных вычислений.
Программа: 20% - последовательный ввод/вывод, 80% - параллельные вычисления. Сколько ядер нужно для 4x ускорения?
Параллелизм vs конкурентность
Два слова, которые смешивают постоянно - и это дорого стоит. Разработчик говорит «сделаем параллельно» и пишет `asyncio`. Получает конкурентность - и удивляется, что CPU-bound код не ускорился. **Параллелизм** - несколько вещей происходят одновременно на разных ядрах. **Конкурентность** - одно ядро умно чередует задачи, не теряя время на ожидание I/O. Роб Пайк сформулировал точно: concurrency про структуру программы, parallelism про исполнение.
| Свойство | Параллелизм (Parallelism) | Конкурентность (Concurrency) |
|---|---|---|
| Суть | Одновременное выполнение | Управление множеством задач |
| Требует | Несколько ядер/процессоров | Может работать на 1 ядре |
| Цель | Ускорение вычислений | Отзывчивость, I/O overlap |
| Пример | Рендер 4 кадров одновременно | Веб-сервер обслуживает 1000 запросов |
| Аналогия | 4 кассы в магазине | 1 касса, но быстрое переключение |
Роб Пайк (создатель Go): "Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once." Конкурентность - про **структуру** программы. Параллелизм - про **выполнение**. Можно иметь конкурентность без параллелизма (async на 1 ядре) и параллелизм без конкурентности (SIMD-инструкции).
Node.js - чистая конкурентность: один поток, event loop, тысячи соединений - но один CPU-bound вызов заморозит всё. NumPy и PyTorch - чистый параллелизм: BLAS-операции на нескольких ядрах через OpenBLAS/MKL, SIMD-инструкции, и ни одного `async` в API. Знать разницу - значит выбрать правильный инструмент вместо правдоподобного.
Веб-сервер на Node.js обрабатывает 10000 одновременных HTTP-запросов на одном ядре. Это пример:
Метрики ускорения
«Мы распараллелили и стало быстрее» - не измерение, а ощущение. Три метрики превращают ощущение в число. **Speedup** - во сколько раз быстрее. **Efficiency** - насколько честно используется каждое ядро. **Scalability** - что происходит при удвоении числа ядер. Без них невозможно сказать: проблема в алгоритме, в железе или в последовательном горлышке.
| Тип масштабируемости | Speedup | Пример |
|---|---|---|
| Линейная (идеальная) | S(p) = p | Полностью независимые задачи |
| Сублинейная (типичная) | S(p) < p | Большинство реальных программ |
| Суперлинейная (редко) | S(p) > p | Данные помещаются в кеши при разбиении |
**Суперлинейное ускорение** (S > p) кажется парадоксом, но возможно: когда данные разбиваются между ядрами, каждый кусок помещается в L2/L3 кеш, а исходный набор - нет. Кеш-эффект даёт дополнительный прирост сверх параллелизма.
**Strong scaling** - фиксированный объём работы, растёт число ядер. Быстро упирается в Амдала. **Weak scaling** - объём работы и число ядер растут вместе. Именно этот режим делает суперкомпьютеры полезными: задача на 1 000 GPU просто в 1 000 раз больше, чем задача на одном - и каждый GPU справляется со своей долей. Distributed training LLM работает именно так.
Не забывайте про **overhead** параллелизма: создание потоков, синхронизация, коммуникация. Для маленьких задач overhead может превысить выигрыш, и параллельная версия окажется **медленнее** однопоточной!
8 ядер = 8x быстрее
Ускорение ограничено последовательной частью кода (закон Амдала) и overhead параллелизма. 8 ядер обычно дают 3-6x ускорения.
Закон Амдала: если 10% кода выполняется последовательно, потолок - 10x при любом числе ядер. Но это теоретический максимум без учёта реального мира. На практике добавляется overhead: создание потоков стоит ~50 мкс каждый, синхронизация через mutex добавляет contention, cache lines передаются между ядрами за десятки нс. PyTorch знает об этом - поэтому `torch.compile` и TorchDynamo оптимизируют именно последовательные секции графа, а не только добавляют ядра.
Программа на 4 ядрах работает за 30с, на 1 ядре - за 100с. Какова эффективность параллелизации?
Ключевые идеи
- **Power wall 2005:** частота стала константой, рост производительности ушёл в ядра - однопоточный код больше не ускоряется автоматически с новым железом
- **Закон Амдала - главный холодный душ:** 5% последовательного кода = потолок 20x при любом количестве ядер. Именно поэтому оптимизация последовательной части ценнее добавления ядер
- **Параллелизм vs конкурентность - разные инструменты:** параллелизм - несколько ядер, одновременное выполнение (PyTorch, NumPy, BLAS); конкурентность - одно ядро, умное чередование I/O (Node.js, asyncio)
- **Метрики не врут:** Speedup = T₁/Tₚ, Efficiency = S/p. 8 ядер при 70% эффективности = 5.6x реального ускорения, а не 8x - overhead и Amdahl съедают остальное
Связанные темы
Параллелизм связан со многими областями CS:
- Потоки и процессы — Базовые примитивы для реализации параллелизма
- Синхронизация — Координация параллельных потоков - mutex, semaphore, barrier
- Операционные системы — ОС управляет процессами, потоками и планированием на ядрах
Вопросы для размышления
- Почему закон Амдала так пессимистичен, а реальные суперкомпьютеры с миллионами ядер всё же полезны?
- Может ли конкурентность ускорить CPU-bound задачу? А параллелизм - I/O-bound?
- Вернёмся к началу: если ваш телефон имеет 8 ядер, почему приложения всё ещё тормозят?
Связанные уроки
- par-02 — Потоки и процессы - базовые примитивы, через которые параллелизм реализуется на практике.
- os-01-intro — ОС управляет планированием ядер; понимание процессов и потоков ОС делает закон Амдала конкретным.
- opt-01 — Параллелизм - инструмент оптимизации производительности; закон Амдала формализует trade-off между последовательной и параллельной частью.
- alg-01-big-o — Сложность алгоритма определяет потолок параллелизации: алгоритм O(n²) с 50% последовательной частью не выиграет от 100 ядер.
- calc-06-derivative-intro — Speedup как функция от числа ядер - та же идея убывающей предельной отдачи, что в математическом анализе и экономике.
- arch-04-cpu
- os-02-processes