Перейти к основному содержимому

Память, мультипроцессоры, кластеры и GRID

Разработчику Архитектору Инженеру

Две фундаментальные модели памяти

Play ITЗагрузка интерактивного демо…

Любую параллельную систему можно описать через то, как процессоры видят память:

Главный вопрос для новичка: "Могу ли я, как программист, обратиться к переменной X с другого ядра так же, как с текущего?"

ОтветМодельКак обычно программируют
Да, одна куча на всехShared memoryПотоки, OpenMP, std::thread
Нет, у каждого свой RAMDistributed memoryMPI, отдельные процессы, 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, pthreadsMPI, 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, OpenMP proc_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) — три связанных подзадачи:

  1. Выбрать решающее поле — сколько логических процессов P и какая топология между ними (линия, кольцо, решётка, полный граф).
  2. Разместить ветви параллельной программы на процессорах — кто выполняет какой фрагмент графа алгоритма.
  3. Удовлетворить ограничениям — к моменту старта операции все её данные уже на нужном узле; один процессор не назначается двум конфликтующим операциям одновременно.

Цель — минимизировать время с учётом:

  • коммуникаций (на 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 + GPUCUDA, OpenMP target, SYCLМассовый data-parallel
DistributedMPIКластер, несколько узлов
Гибрид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/sHPC 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.


Что дальше