Память, мультипроцессоры, кластеры и GRID
Две фундаментальные модели памяти
Play ITЗагрузка интерактивного демо…
Любую параллельную систему можно описать через то, как процессоры видят память:
Главный вопрос для новичка: "Могу ли я, как программист, обратиться к переменной X с другого ядра так же, как с текущего?"
| Ответ | Модель | Как обычно программируют |
|---|---|---|
| Да, одна куча на всех | Shared memory | Потоки, OpenMP, std::thread |
| Нет, у каждого свой RAM | Distributed memory | MPI, отдельные процессы, Send/Recv |
Общая память (Shared Memory)
Все процессоры обращаются к одному адресному пространству. Процессор P₁ пишет в ячейку X, процессор P₂ читает X — оба видят одни и те же данные (с учётом когерентности кэша).
┌─────────┐ ┌─────────┐
│ CPU 0 │ │ CPU 1 │
└────┬────┘ └────┬────┘
│ общая │
└───────┬───────┘
▼
[ RAM / шина ]
Плюсы: простой обмен через переменные; хорошо ложится на многопоточность.
Минусы: при росте числа процессоров шина памяти становится узким местом; нужна синхронизация (мьютексы, атомики) — см. потоки.
Примеры — односокетный многоядерный PC, сервер с SMP, NUMA-система (память "ближе" к одному сокету).
Распределённая память (Distributed Memory)
У каждого процессора своя локальная память. Узел A не может просто прочитать адрес узла B — только через явный обмен (сообщения, сеть).
Аналогия: у каждого сотрудника свой шкаф с документами. Чтобы коллега увидел ваш файл, вы отправляете копию (сообщение), а не указываете "возьми из моей памяти по адресу 0x7fff…". В MPI каждый rank — отдельный процесс со своим адресным пространством; общие данные — только через MPI_Send, MPI_Recv, MPI_Bcast.
┌──────────┐ сеть ┌──────────┐
│ CPU + RAM│◄──────►│ CPU + RAM│
└──────────┘ └──────────┘
Плюсы: хорошо масштабируется на тысячи узлов; нет единой точки contention за память.
Минусы: программист (или библиотека) явно отправляет и принимает данные; ошибки в протоколе обмена.
Примеры:
- кластеры HPC;
- MPI-программы;
- распределённое обучение на нескольких серверах.
"Несколько потоков в одной программе" → почти всегда shared memory. "Несколько процессов на разных машинах" → distributed memory + MPI или RPC.
Мультипроцессор и мультикомпьютер
| Мультипроцессор | Мультикомпьютер | |
|---|---|---|
| Память | Общая (логически) | Распределённая |
| Связь | Шина, crossbar, NUMA | Сеть (Ethernet, InfiniBand) |
| Масштаб | Обычно 1–8 сокетов, до сотен ядер | От десятков до миллионов ядер |
| Программирование | OpenMP, pthreads | MPI, PGAS |
| Типичный пример | Workstation, сервер | Кластер / суперкомпьютер |
NUMA (Non-Uniform Memory Access) — промежуточный случай — память общая, но скорость доступа зависит от того, "свой" это узел или "чужой". Без NUMA-aware размещения потоков и данных производительность падает на 30–50 %.
UMA и NUMA
- UMA (Uniform Memory Access) — все ядра видят RAM с одинаковой задержкой (типичный односокетный PC).
- NUMA — несколько узлов памяти, каждый привязан к группе ядер. Локальная память быстрее удалённой в 1.5–3 раза.
Рекомендации для NUMA:
- Привязать поток к ядру (
taskset, OpenMPproc_bind). - Выделять данные на том же узле, где они обрабатываются (
numactl --membind). - Минимизировать false sharing — когда разные ядра пишут в разные переменные, но в одной кэш-линии.
Кластеры вычислений
Кластер — группа связанных узлов (нод), работающих как единая вычислительная система. В HPC это чаще Beowulf-класс — commodity-серверы + быстрая сеть + общее ПО (Linux, Slurm, MPI).
Компоненты:
| Компонент | Роль |
|---|---|
| Узлы вычисления | Запускают задачи |
| Head / login node | Вход пользователя, сборка, отправка job |
| Сеть | InfiniBand, Omni-Path, иногда 100 Gb Ethernet |
| Планировщик | Slurm, PBS, LSF — очередь задач |
| Общее хранилище | NFS, Lustre, GPFS — большие данные |
Суперкомпьютер — по сути очень большой tightly-coupled кластер с топовой сетью и охлаждением.
Kubernetes-кластер в DevOps — тоже кластер, но цель другая (оркестрация контейнеров), не линейная speedup одной численной задачи. Для HPC иногда используют K8s + MPI, но классика — bare metal + Slurm.
GRID и метакомпьютинг
GRID (вычислительная сетка) — объединение разнородных ресурсов (университеты, лаборатории, добровольцы) в единую инфраструктуру для крупных задач. Отличие от кластера:
| Кластер | GRID | |
|---|---|---|
| Администрирование | Один центр | Федерация организаций |
| Однородность | Высокая | Низкая (разное железо, ОС) |
| Связь | Низкая latency | Часто интернет, высокая latency |
| Пример | Суперкомпьютер в МГУ | LHC Computing Grid, BOINC |
Метакомпьютинг — уровень над отдельными системами — пользователь отправляет задачу "в сеть", а middleware (Globus, Univa Grid Engine) выбирает, где её выполнить, переносит данные и собирает результат. Программист часто не знает физический узел.
Примеры метакомпьютинга "для всех":
- SETI@home, Folding@home — добровольные вычисления на домашних ПК.
- Облачный burst — докупить 500 VM на время расчёта.
Отображение задачи на систему (проблема mapping)
Проблема отображения (mapping) — три связанных подзадачи:
- Выбрать решающее поле — сколько логических процессов P и какая топология между ними (линия, кольцо, решётка, полный граф).
- Разместить ветви параллельной программы на процессорах — кто выполняет какой фрагмент графа алгоритма.
- Удовлетворить ограничениям — к моменту старта операции все её данные уже на нужном узле; один процессор не назначается двум конфликтующим операциям одновременно.
Цель — минимизировать время с учётом:
- коммуникаций (на distributed);
- contention за память (на shared);
- простоя (load imbalance);
- средней длины пути в сети (чем короче типичный маршрут, тем дешевле halo и collectives).
В общем виде задача NP-трудная; на практике — эвристики: блоки матрицы → соседние ядра; 1 MPI-процесс на NUMA-узел; static chunk в OpenMP; torus для stencil.
Мини-пример: stencil 2D на 4 процессах — логическая решётка 2×2 совпадает с физической; halo обменивают только соседи по рёбрам, а не все сразу.
Формальные модели отображения связаны с топологиями сетей и графом алгоритма.
Программные интерфейсы под модель памяти
| Модель | API | Когда выбирать |
|---|---|---|
| Shared, 1 машина | OpenMP, TBB, std::thread | До ~64 ядер, один узел |
| Shared + GPU | CUDA, OpenMP target, SYCL | Массовый data-parallel |
| Distributed | MPI | Кластер, несколько узлов |
| Гибрид | MPI + OpenMP | Крупнейшие суперкомпьютеры: MPI между узлами, OpenMP внутри |
Справочник по OpenMP/MPI в контексте Fortran: Справочник по языку Fortran.
Когерентность кэша (MESI — обзор)
В shared memory у каждого ядра свой L1/L2. Как все видят одну переменную X?
Протокол MESI (Modified, Exclusive, Shared, Invalid) — строка кэша в одном из состояний:
| Состояние | Смысл |
|---|---|
| M | Только это ядро изменило строку; память устарела |
| E | Только это ядро держит копию, не менял |
| S | Несколько ядер читают одну строку |
| I | Копия недействительна |
Запись другим ядром → invalidation → cache miss и повторная загрузка. Отсюда false sharing: два потока пишут в разные переменные в одной cache line (64 B) → постоянные invalidations.
Практика: padding структур, локальные буферы, reduction вместо atomic++ на каждый элемент.
First-touch на NUMA
First-touch — страница RAM выделяется на NUMA-узле того ядра, которое первым записало в эту страницу.
Псевдокод (правильная инициализация)
АЛГОРИТМ ИНИЦИАЛИЗИРОВАТЬ_МАССИВ_С_NUMA(a, n)
параллельно для i от 0 до n − 1
a[i] := 0 // поток "касается" своего диапазона → память рядом с его ядром
конец параллельно
КОНЕЦ
Псевдокод (ошибка)
// один поток обнуляет весь массив
для i от 0 до n − 1
a[i] := 0
// затем все потоки считают по a[i] — страницы могли остаться на "чужом" узле
Потеря 30–50 % скорости на двухсокетном сервере — типичный эффект.
Настройка среды (shell)
numactl --hardware
export OMP_PROC_BIND=close
Справочно на C++ (OpenMP)
#pragma omp parallel for
for (int i = 0; i < n; ++i)
a[i] = 0;
| Элемент | Смысл |
|---|---|
a[i] = 0 | Запись — "касание" страницы памяти |
| Параллельный цикл | Каждый поток трогает свой диапазон i |
Подробнее про привязку потоков — практика OpenMP.
Сеть кластера — порядок величин
| Технология | Latency (тип.) | Bandwidth | Комментарий |
|---|---|---|---|
| Ethernet 1 Gb | ~100 µs | ~100 MB/s | Учебный кластер |
| 100 Gb Ethernet | ~10 µs | ~10 GB/s | HPC entry |
| InfiniBand HDR | ~1 µs | ~200 GB/s | Топовые суперкомпьютеры |
| NVLink (GPU) | sub-µs | сотни GB/s | Внутри узла |
Правило: одно сообщение ≥ нескольких KB (лучше MB), иначе доминирует latency, а не bandwidth — см. MPI-практику.
Гибрид — узел как SMP, кластер как distributed
Современный HPC-узел — 32–128 ядер, shared/NUMA память. Кластер из N таких узлов:
[ Node 0: OpenMP × 64 ] ←──MPI──→ [ Node 1: OpenMP × 64 ] …
- Внутри узла — shared memory, halo через shared buffer или OpenMP.
- Между узлами — MPI, только необходимые блоки.
Так минимизируют число MPI-процессов и не дублируют всю память на каждый rank.