MPI: передача сообщений для кластеров

Урок про модель программирования для кластеров, где у каждого узла своя память.

MPI (Message Passing Interface) — стандарт параллельных вычислений на распределённой памяти: процессы не делят память, а явно обмениваются данными через сообщения.

Зачем MPI, если есть OpenMP

OpenMP работает в пределах одной машины — общая память ограничена одним сервером (десятки ядер). Чтобы задействовать тысячи ядер кластера из множества машин, общей памяти нет: узлы соединены сетью. Тогда единственный способ поделиться данными — послать сообщение. MPI стандартизирует это: запускается N процессов (часто по одному на ядро узлов), каждый имеет свой номер (rank), и они координируются, пересылая данные.

Базовые операции

Точечный обмен: MPI_Send (послать данные процессу с номером X) и MPI_Recv (принять). Каждый процесс знает свой rank и общее число процессов size, и по этим числам решает, что ему делать. Ниже — C, исполняется только в MPI-окружении кластера, поэтому блок нечитаемый для браузерного раннера.

#include <mpi.h>
#include <stdio.h>

int main(int argc, char** argv) {
    MPI_Init(&argc, &argv);
    int rank, size;
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);   // мой номер
    MPI_Comm_size(MPI_COMM_WORLD, &size);   // сколько нас всего

    if (rank == 0) {
        int data = 42;
        MPI_Send(&data, 1, MPI_INT, 1, 0, MPI_COMM_WORLD);  // -> процессу 1
        printf("процесс 0 отправил %d\n", data);
    } else if (rank == 1) {
        int recv;
        MPI_Recv(&recv, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
        printf("процесс 1 получил %d\n", recv);
    }
    MPI_Finalize();
    return 0;
}

Коллективные операции

Слать данные парами вручную утомительно и медленно. MPI даёт коллективы — операции с участием всех процессов сразу, реализованные оптимально (деревом за log p шагов):

ОперацияЧто делает
MPI_Bcastразослать данные от одного всем
MPI_Scatterраздать куски массива по процессам
MPI_Gatherсобрать куски обратно к одному
MPI_Reduceсвернуть значения всех в одно (сумма, макс)
MPI_Allreduceсвернуть и раздать результат всем

MPI_Reduce — это та самая параллельная редукция деревом из раздела 4, но между узлами сети. MPI_Allreduce особенно важен в распределённом обучении нейросетей: после каждого шага все узлы складывают свои градиенты и получают общую сумму.

Эмуляция scatter-reduce (на Python)

Покажем логику «раздать куски, посчитать локально, свернуть» последовательно.

data = list(range(1, 13))     # 1..12, "хозяин" — процесс 0
procs = 4
chunk = len(data) // procs

# scatter: каждому процессу — свой кусок
local = [data[p*chunk:(p+1)*chunk] for p in range(procs)]
# каждый считает локальную сумму
local_sums = [sum(part) for part in local]
print("куски:", local)
print("локальные суммы:", local_sums)
# reduce: свернуть в одну
print("MPI_Reduce(+) =", sum(local_sums))

Вывод:

куски: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
локальные суммы: [6, 15, 24, 33]
MPI_Reduce(+) = 78

Как работает под капотом

MPI-программа — это SPMD (Single Program, Multiple Data): один и тот же бинарник запускается на всех процессах, а ветвление по rank заставляет их делать разное. Коллективы реализованы умно: Bcast и Reduce используют деревья, чтобы за log p шагов охватить все процессы, а не за p. Главная стоимость — сеть: задержка на сообщение (микросекунды) и пропускная способность. Алгоритм проектируют так, чтобы слать редкие крупные сообщения, а не частые мелкие.

Переход от OpenMP к MPI — это смена мировоззрения, а не просто другой API. В OpenMP память общая, и поделиться данными — значит просто прочитать переменную; «отправки» нет вовсе. В MPI общей памяти нет в принципе: если процессу нужны данные другого, он обязан их явно запросить и дождаться. Это заставляет программиста постоянно думать о том, кто чем владеет и кто кому что шлёт, — дисциплина утомительная, но именно она позволяет масштабироваться на тысячи узлов, где общей памяти физически быть не может. Парадоксально, но более «неудобная» модель MPI масштабируется дальше именно потому, что она честна насчёт стоимости коммуникации и не прячет её за иллюзией общей памяти.

Частые ошибки

  • Несогласованные Send/Recv — если все ждут приёма и никто не шлёт, дедлок.
  • Слать много мелких сообщений вместо коллективов — сеть станет узким местом.
  • Думать, что у процессов общая память — её нет, любые данные нужно явно пересылать.

Итоги

  • MPI — модель распределённой памяти: процессы обмениваются явными сообщениями.
  • Точечные Send/Recv и коллективы (Bcast, Scatter, Gather, Reduce, Allreduce).
  • Allreduce — ключ к распределённому обучению; коллективы работают деревом за log p.
Проверьте себя
1. В какой модели памяти работает MPI?
AРазделяемая память
BРаспределённая память — процессы обмениваются сообщениями
CБез памяти
DТолько кэш
2. Что делает коллективная операция MPI_Allreduce?
AСортирует данные
BСворачивает значения всех процессов в одно и раздаёт результат всем
CСоздаёт новые процессы
DУдаляет данные
3. Почему коллективы предпочтительнее ручных Send/Recv?
AОни проще писать и реализованы оптимально (деревом за log p шагов)
BОни не используют сеть
CОни работают только на одном узле
DОни медленнее