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

Практика — OpenMP, MPI и профилирование

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

Практика - паттерны и типичные ошибки параллелизма

Теория из графов и законов становится полезной, когда вы запускаете код на реальном железе. Здесь — минимальный набор паттернов, который покрывает большинство учебных и инженерных задач на одном узле (OpenMP) и кластере (MPI).

Выбор инструмента
Один сервер, shared memory → OpenMP / потоки C++. Несколько узлов → MPI (часто MPI + OpenMP внутри узла). Массовый data-parallel → GPU (CUDA/SYCL) — см. GPU.


OpenMP — параллелизм по данным на одном узле

OpenMP — стандарт для C/C++/Fortran на одной машине с общей памятью. Вы добавляете директивы (#pragma omp ...) — компилятор и runtime создают потоки и делят работу.

Модель fork-join: один главный поток доходит до #pragma omp parallel → создаётся команда потоков → все вместе работают в region → в конце барьер (синхронизация) → снова один поток.

Параллельный цикл

#include <omp.h>

void saxpy(int n, double a, const double* x, double* y) {
#pragma omp parallel for
for (int i = 0; i < n; ++i)
y[i] = a * x[i] + y[i];
}
ФрагментСмысл
#pragma omp parallel forСоздать потоки и распределить итерации цикла for
parallel«Запустить команду потоков»
for«Итерации цикла — общая работа команды»
y[i] = ...Каждый i обычно выполняется одним потоком; разные i — параллельно

Компиляция (GCC/Clang): g++ -fopenmp -O3 saxpy.cpp

Что происходит: runtime делит диапазон 0…n-1 на куски. По умолчанию schedule(static)равные блоки заранее: поток 0 берёт 0…n/p-1, поток 1 — следующий блок и т.д. Подходит, когда все итерации одинаково тяжёлые.

Reduction (суммирование без гонок)

double sum = 0.0;
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < n; ++i)
sum += a[i];

Runtime создаёт локальные копии sum на поток, в конце — дерево сложения. Это реализация паттерна из временного анализа (parallel partial sums + tree).

Dynamic schedule при неравной нагрузке

#pragma omp parallel for schedule(dynamic, 64)
for (int i = 0; i < n; ++i)
heavy(i); // время зависит от i

Помогает при load imbalance — см. введение.

NUMA — привязка потоков

export OMP_PROC_BIND=close
export OMP_PLACES=cores

На многосокетном сервере без этого потоки «гуляют» по NUMA-узлам и теряют до 40 % скорости — память и NUMA.


MPI — распределённая память

MPI (Message Passing Interface) — библиотека для нескольких процессов, часто на разных узлах кластера. У каждого процесса своя память; общение только сообщениями.

Термин MPIСмысл
rankНомер процесса (0, 1, …, size−1)
sizeСколько процессов в группе
communicatorГруппа процессов (MPI_COMM_WORLD — все)
tagМетка сообщения, чтобы отличать типы обмена

Каждый rank — отдельный процесс со своей памятью. Программа — SPMD (модели): один и тот же main, разный rank.

Hello + rank

#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);
printf("Hello from rank %d of %d\n", rank, size);
MPI_Finalize();
return 0;
}

Запуск: mpirun -np 4 ./hello

Point-to-point

if (rank == 0) {
int data = 42;
MPI_Send(&data, 1, MPI_INT, 1, 0, MPI_COMM_WORLD);
} else if (rank == 1) {
int buf;
MPI_Recv(&buf, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
}

Collective — broadcast и reduce

double local_sum = partial_sum();
double global_sum;
MPI_Allreduce(&local_sum, &global_sum, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);

Allreduce — оптимизирован под топологию сети; не пишите свой «каждый шлёт каждому» вручную.

Halo exchange (2D сетка)

Типичный паттерн для domain decomposition:

// rank имеет локальный блок; обмен «призрачными» слоями с соседями
MPI_Sendrecv(halo_send_buf, ..., neighbor_left, ...,
halo_recv_buf, ..., neighbor_left, ..., MPI_COMM_WORLD, &status);

Правило: одно крупное сообщение на направление лучше десятков мелких — latency доминирует.

Overlap — неблокирующий обмен

MPI_Request req;
MPI_Isend(buf, count, MPI_DOUBLE, dest, tag, MPI_COMM_WORLD, &req);
compute_locally(); // пока данные уходят
MPI_Wait(&req, MPI_STATUS_IGNORE);

Гибрид MPI + OpenMP

На суперкомпьютере: 1 MPI-процесс на NUMA-узел или сокет, внутри — OpenMP на ядрах:

#pragma omp parallel for
for (int i = my_start; i < my_end; ++i)
process(i);

Так меньше процессов → меньше дублирования данных и меньше MPI-сообщений, но нужно следить, чтобы OpenMP не создал больше потоков, чем физических ядер (OMP_NUM_THREADS).


Пошаговая отладка параллельного кода

ШагДействие
1Эталон последовательно на малых данных
2OpenMP с 1 потоком — должен совпасть с эталоном
3Рост числа потоков / ranks, сравнение результата (ε для float)
4Профилируйте — где время (не гадать)
5Strong/weak scaling plots — законы

Инструменты

ИнструментНазначение
perf, VTuneCPU, cache misses, NUMA
gprof, -pgГрубый профиль функций
mpiP, TAUMPI: объём и время сообщений
valgrind --tool=helgrindГонки в pthreads
ThreadSanitizer-fsanitize=thread в GCC/Clang

Типичная находка: 80 % времени в одном цикле без параллелизма — сначала #pragma omp parallel for туда, а не «параллелить всё подряд».


Чек-лист перед сдачей HPC-задачи

  1. Результат совпадает с sequential (или документированная ε).
  2. Измерены T_1, T_p, speedup, efficiency на нескольких p.
  3. Нет лишних barrier / MPI_Barrier в hot loop.
  4. Размер сообщений ≥ нескольких KB (или осознанно мелкие).
  5. NUMA / affinity настроены на сервере.
  6. В отчёте указаны: f или s, критический путь (если учебная задача), weak vs strong.

Что дальше


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).