Init
This commit is contained in:
commit
c394677d07
59 files changed
+5149
No files matched your search
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: привет, MPI — ранги, размер мира, точка-точка.
|
||||
|
||||
Запуск из корня курса:
|
||||
mpiexec -n 4 python lab08_mpi/examples/01_hello_mpi.py
|
||||
|
||||
Каждый ранг — отдельный процесс с копией программы. Ранги обмениваются
|
||||
сообщениями: ранг 0 рассылает своё имя всем, каждый отвечает.
|
||||
Без mpiexec (обычный python) программа работает как единственный ранг.
|
||||
"""
|
||||
from mpi4py import MPI
|
||||
|
||||
comm = MPI.COMM_WORLD
|
||||
rank = comm.Get_rank()
|
||||
size = comm.Get_size()
|
||||
|
||||
print(f"[до обмена] ранг {rank} из {size}, узел {MPI.Get_processor_name()}")
|
||||
|
||||
if rank == 0:
|
||||
# ранг 0 рассылает задание всем остальным (bcast: всем одно и то же)
|
||||
greeting = f"привет от координатора (ранг 0), всего рангов {size}"
|
||||
else:
|
||||
greeting = None
|
||||
|
||||
greeting = comm.bcast(greeting, root=0) # коллективная: вызывают ВСЕ
|
||||
print(f"ранг {rank}: получил «{greeting}»")
|
||||
|
||||
# точка-точка: каждый ранг (кроме 0) отправляет 0 свой номер
|
||||
if rank != 0:
|
||||
comm.send(rank, dest=0, tag=11)
|
||||
print(f"ранг {rank}: отправил свой номер координатору")
|
||||
else:
|
||||
received = [comm.recv(source=r, tag=11) for r in range(1, size)]
|
||||
print(f"ранг 0: получил номера от рангов {received}")
|
||||
|
||||
if size == 1:
|
||||
print("\nВы запустили без mpiexec — работает один ранг.")
|
||||
print("Попробуйте: mpiexec -n 4 python lab08_mpi/examples/01_hello_mpi.py")
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: ping-pong и замер стоимости обмена между рангами.
|
||||
|
||||
Запуск из корня курса:
|
||||
mpiexec -n 2 python lab08_mpi/examples/02_pingpong_mpi.py
|
||||
|
||||
Ранги перекидывают список чисел туда-обратно. Так измеряют латентность и
|
||||
пропускную способность канала между узлами.
|
||||
"""
|
||||
import time
|
||||
|
||||
from mpi4py import MPI
|
||||
|
||||
comm = MPI.COMM_WORLD
|
||||
rank = comm.Get_rank()
|
||||
size = comm.Get_size()
|
||||
|
||||
if size < 2:
|
||||
print("Нужны минимум 2 ранга: mpiexec -n 2 python lab08_mpi/examples/02_pingpong_mpi.py")
|
||||
raise SystemExit(0)
|
||||
|
||||
ROUNDS = 20
|
||||
for n_bytes in (1_000, 100_000, 1_000_000):
|
||||
data = list(range(n_bytes // 8)) # ~n_bytes байт целых чисел
|
||||
comm.barrier() # синхронный старт замера
|
||||
t0 = time.perf_counter()
|
||||
if rank == 0:
|
||||
for _ in range(ROUNDS):
|
||||
comm.send(data, dest=1, tag=0)
|
||||
comm.recv(source=1, tag=0)
|
||||
elif rank == 1:
|
||||
for _ in range(ROUNDS):
|
||||
comm.recv(source=0, tag=0)
|
||||
comm.send(data, dest=0, tag=0)
|
||||
dt = time.perf_counter() - t0
|
||||
|
||||
if rank == 0:
|
||||
per_round = dt / ROUNDS
|
||||
print(f"{n_bytes:>9,} байт: {ROUNDS} раундов за {dt:.3f} c "
|
||||
f"=> {per_round * 1000:.2f} мс/раунд (туда-обратно)".replace(",", " "))
|
||||
|
||||
if rank == 0:
|
||||
print("\nНа одной машине это IPC (быстро), между узлами — сеть.")
|
||||
print("Эта цена обмена и есть накладные расходы распределённых вычислений.")
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: коллективные операции scatter / gather / reduce / bcast.
|
||||
|
||||
Запуск из корня курса:
|
||||
mpiexec -n 4 python lab08_mpi/examples/03_collectives_mpi.py
|
||||
|
||||
Показывает все четыре коллективные операции на маленьком примере.
|
||||
"""
|
||||
from mpi4py import MPI
|
||||
|
||||
comm = MPI.COMM_WORLD
|
||||
rank = comm.Get_rank()
|
||||
size = comm.Get_size()
|
||||
|
||||
# --- bcast: один -> всем ------------------------------------------------
|
||||
config = {"task": "summa_kvadratov", "n": 100} if rank == 0 else None
|
||||
config = comm.bcast(config, root=0)
|
||||
print(f"ранг {rank}: config = {config}")
|
||||
|
||||
# --- scatter: СПИСОК КУСКОВ (ровно size штук), ранг i получает i-й --------
|
||||
if rank == 0:
|
||||
# каждый кусок — свой список; scatter ждёт список из size кусков!
|
||||
numbers = [[i * 10 + j for j in range(10)] for i in range(size)]
|
||||
else:
|
||||
numbers = None
|
||||
my_chunk = comm.scatter(numbers, root=0) # ранг i получает свой кусок
|
||||
print(f"ранг {rank}: мой кусок {my_chunk[0]}..{my_chunk[-1]} ({len(my_chunk)} шт.)")
|
||||
|
||||
# --- каждый ранг считает СВОЮ часть --------------------------------------
|
||||
my_sum = sum(x * x for x in my_chunk)
|
||||
print(f"ранг {rank}: сумма квадратов моего куска = {my_sum}")
|
||||
|
||||
# --- gather: все части -> список на ранге 0 ------------------------------
|
||||
partial_sums = comm.gather(my_sum, root=0)
|
||||
if rank == 0:
|
||||
print(f"ранг 0: частичные суммы = {partial_sums}")
|
||||
print(f"ранг 0: итого через sum(gather) = {sum(partial_sums)}")
|
||||
|
||||
# --- reduce: то же самое, но сразу со свёрткой (проще и быстрее) ----------
|
||||
total = comm.reduce(my_sum, op=MPI.SUM, root=0)
|
||||
if rank == 0:
|
||||
print(f"ранг 0: итого через reduce(MPI.SUM) = {total}")
|
||||
print("\ngather+sum и reduce(SUM) дали одно и то же — reduce короче.")
|
||||
print("В задании mpi_pi вы используете reduce для суммы попаданий.")
|
||||
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: π методом Монте-Карло в MPI (reduce).
|
||||
|
||||
Запуск из корня курса:
|
||||
mpiexec -n 4 python lab08_mpi/examples/04_pi_reduce_mpi.py
|
||||
|
||||
Каждый ранг бросает свою порцию «точек» со СВОИМ зерном (rank-зависимым),
|
||||
reduce(SUM) сворачивает попадания. Число рангов не меняет формулу —
|
||||
меняется только распределение бросков.
|
||||
"""
|
||||
import random
|
||||
import time
|
||||
|
||||
from mpi4py import MPI
|
||||
|
||||
comm = MPI.COMM_WORLD
|
||||
rank = comm.Get_rank()
|
||||
size = comm.Get_size()
|
||||
|
||||
N_PER_RANK = 500_000
|
||||
|
||||
|
||||
def count_inside(n: int, seed: int) -> int:
|
||||
rng = random.Random(seed) # зерно зависит от ранга
|
||||
inside = 0
|
||||
for _ in range(n):
|
||||
x = rng.random()
|
||||
y = rng.random()
|
||||
if x * x + y * y <= 1.0:
|
||||
inside += 1
|
||||
return inside
|
||||
|
||||
|
||||
t0 = time.perf_counter()
|
||||
my_inside = count_inside(N_PER_RANK, seed=1000 + rank)
|
||||
total_inside = comm.reduce(my_inside, op=MPI.SUM, root=0)
|
||||
dt = time.perf_counter() - t0
|
||||
|
||||
if rank == 0:
|
||||
total = N_PER_RANK * size
|
||||
pi = 4.0 * total_inside / total
|
||||
print(f"рангов: {size}, бросков на ранг: {N_PER_RANK:,} (всего {total:,})")
|
||||
print(f"π ≈ {pi:.6f} (погрешность {abs(pi - 3.141592653589793):.6f})")
|
||||
print(f"время: {dt:.2f} c")
|
||||
if size == 1:
|
||||
print("\nПопробуйте: mpiexec -n 4 python lab08_mpi/examples/04_pi_reduce_mpi.py")
|
||||
print("Каждый ранг добавляет 500k бросков: точность растёт, время — нет.")
|
||||
@@ -0,0 +1,124 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Лабораторная работа 8: распределённые вычисления (mpi4py).
|
||||
|
||||
Заполните функции ниже. ИНТЕРФЕЙСЫ МЕНЯТЬ НЕЛЬЗЯ — по ним работают
|
||||
автотесты (tests/test_lab08.py; если mpi4py не установлен — тесты
|
||||
пропускаются, но работа сдаётся с запуском через mpiexec).
|
||||
|
||||
Запуск из корня курса:
|
||||
python lab08_mpi/solution.py (один ранг — отладка)
|
||||
mpiexec -n 4 python lab08_mpi/solution.py (4 ранга — замеры)
|
||||
"""
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
from common.tasks import TASKS, get_variant
|
||||
|
||||
try:
|
||||
from mpi4py import MPI
|
||||
HAVE_MPI = True
|
||||
except ImportError:
|
||||
HAVE_MPI = False
|
||||
|
||||
# TODO: впишите свой номер в журнале (1..20)
|
||||
VARIANT_NUMBER = 0
|
||||
|
||||
|
||||
def _group_for_ranks(chunks, size):
|
||||
"""Распределить куски по рангам round-robin: список из size групп.
|
||||
|
||||
scatter ожидает список ровно из size элементов, а естественных кусков
|
||||
может быть другое число (например, nqueens всегда даёт N кусков) —
|
||||
поэтому группируем.
|
||||
"""
|
||||
# TODO: groups = [[] for _ in range(size)]; groups[i % size].append(ch)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def mpi_run(task_name: str, params: dict):
|
||||
"""CPU-задача на всех рангах: scatter групп -> kernel -> gather -> combine.
|
||||
|
||||
Схема:
|
||||
ранг 0: build -> split(data, size) -> _group_for_ranks
|
||||
scatter раздаёт ГРУППЫ кусков (по одной на ранг)
|
||||
каждый ранг: my_partials = [task["kernel"](ch) for ch in my_group]
|
||||
gather собирает списки частичных результатов
|
||||
ранг 0: развернуть в плоский список -> combine ОДИН раз -> checksum
|
||||
|
||||
Функции пиккелить не нужно (в отличие от multiprocessing) — только данные.
|
||||
|
||||
Для задачи 'life': данные — (grid, steps), параллелятся ПОКОЛЕНИЯ.
|
||||
Ранг 0 держит текущее поле; каждый шаг: scatter полос -> kernel ->
|
||||
gather -> combine -> новое поле. Число шагов раздаётся через bcast.
|
||||
"""
|
||||
if not HAVE_MPI:
|
||||
raise RuntimeError("mpi4py не установлен (см. методичку, раздел Установка)")
|
||||
task = TASKS[task_name]
|
||||
comm = MPI.COMM_WORLD
|
||||
rank = comm.Get_rank()
|
||||
size = comm.Get_size()
|
||||
|
||||
if task_name == "life":
|
||||
# TODO: цикл по поколениям (см. схему выше); в конце bcast поля
|
||||
# с ранга 0 всем, чтобы checksum был у всех рангов
|
||||
raise NotImplementedError
|
||||
|
||||
# TODO: общий случай по схеме выше; вернуть checksum на ранге 0 (None на других)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def mpi_pi(N: int) -> float:
|
||||
"""π методом Монте-Карло на всех рангах (reduce).
|
||||
|
||||
Каждый ранг бросает N//size точек с зерном random.Random(100 + rank).
|
||||
reduce(MPI.SUM) сворачивает попадания на ранге 0.
|
||||
Вернуть π (на ранге 0 — вычисленное значение, на остальных 0.0).
|
||||
"""
|
||||
# TODO: my_n = N // size; цикл бросков; comm.reduce(inside, op=MPI.SUM, root=0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def main():
|
||||
if not HAVE_MPI:
|
||||
print("mpi4py не установлен. Инструкция — в методичке, раздел «Установка».")
|
||||
print("Кратко (Windows): MS-MPI runtime + pip install mpi4py")
|
||||
sys.exit(1)
|
||||
|
||||
v = get_variant(VARIANT_NUMBER)
|
||||
comm = MPI.COMM_WORLD
|
||||
rank = comm.Get_rank()
|
||||
size = comm.Get_size()
|
||||
|
||||
if rank == 0:
|
||||
print(f"Вариант {v['variant']}: {v['describe']}")
|
||||
print(f"Запущено рангов: {size}\n")
|
||||
|
||||
# --- pi через reduce ------------------------------------------------------
|
||||
pi = mpi_pi(1_000_000)
|
||||
if rank == 0:
|
||||
print(f"π ≈ {pi:.6f} (Монте-Карло, 1 000 000 бросков суммарно)")
|
||||
|
||||
# --- основная задача --------------------------------------------------------
|
||||
comm.barrier()
|
||||
t0 = time.perf_counter()
|
||||
checksum = mpi_run(v["task_name"], v["cpu_params"])
|
||||
dt = time.perf_counter() - t0
|
||||
|
||||
if rank == 0:
|
||||
from common.tasks import run_sequential
|
||||
ref, _ = run_sequential(v["task_name"], v["cpu_params"], parts=1)
|
||||
ok = checksum == ref or (isinstance(checksum, float) and
|
||||
abs(checksum - ref) < 1e-6 * max(1, abs(ref)))
|
||||
print(f"задача: {dt:.3f} c, checksum совпадает с последовательным: {ok}")
|
||||
print("\nЗапустите при разном числе рангов для таблицы T(p):")
|
||||
print(" mpiexec -n 1 python lab08_mpi/solution.py")
|
||||
print(" mpiexec -n 2 python lab08_mpi/solution.py")
|
||||
print(" mpiexec -n 4 python lab08_mpi/solution.py")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,140 @@
|
||||
# Лабораторная работа 8. Распределённые вычисления (MPI)
|
||||
|
||||
**Время на работу:** 2–3 пары. **Зависимости:** лабы 1, 4.
|
||||
|
||||
## Цель работы
|
||||
|
||||
Освоить модель передачи сообщений (MPI) — стандарт распределённых вычислений,
|
||||
на котором работают суперкомпьютеры. Понять отличие «общей памяти»
|
||||
(потоки/процессы Python) от «обмена сообщениями» между независимыми узлами.
|
||||
|
||||
## Теория
|
||||
|
||||
### Модель MPI
|
||||
|
||||
MPI (Message Passing Interface) — набор программ на нескольких **узлах**
|
||||
(машинах или процессах), обменивающихся сообщениями. Нет общей памяти: любые
|
||||
данные передаются явно, функциями отправки/приёма. Программа запускается
|
||||
одновременно на N «рангах» (процессах):
|
||||
|
||||
```bat
|
||||
mpiexec -n 4 python script.py # 4 копии script.py
|
||||
```
|
||||
|
||||
Каждая копия узнаёт, кто она:
|
||||
|
||||
```python
|
||||
from mpi4py import MPI
|
||||
comm = MPI.COMM_WORLD # «мир» — все ранги
|
||||
rank = comm.Get_rank() # мой номер: 0..size-1
|
||||
size = comm.Get_size() # всего рангов
|
||||
```
|
||||
|
||||
Ранг 0 обычно координатор: он делит работу и собирает результат.
|
||||
|
||||
### Операции: «точка-точка» и коллективные
|
||||
|
||||
**Точка-точка** (ранг ↔ ранг):
|
||||
|
||||
```python
|
||||
comm.send(data, dest=1, tag=0) # блокирующая отправка
|
||||
data = comm.recv(source=0, tag=0) # блокирующий приём
|
||||
```
|
||||
|
||||
**Коллективные** (все ранги сразу — проще и обычно быстрее):
|
||||
|
||||
| Операция | Что делает |
|
||||
|---|---|
|
||||
| `bcast` | один раздает всем (broadcast) |
|
||||
| `scatter` | список режется на части: ранг i получает i-ю часть |
|
||||
| `gather` | части собираются в список на одном ранге |
|
||||
| `reduce` | части сворачиваются операцией (sum/max/…) на одном ранге |
|
||||
| `barrier` | все ждут, пока все дойдут до этой точки |
|
||||
|
||||
### Схема параллельной задачи в MPI
|
||||
|
||||
```
|
||||
ранг 0: build -> split -> scatter ─┐
|
||||
ранги 1..N: получают кусок │-> kernel -> gather -> combine -> checksum
|
||||
ранг 0: собирает ┘
|
||||
```
|
||||
|
||||
Это ровно ваша схема split→kernel→combine из лаб 1–4, только обмен данными —
|
||||
через сеть/IPC, а не общую память.
|
||||
|
||||
### Установка (Windows)
|
||||
|
||||
1. MS-MPI: скачать и установить **msmpisetup.exe** и **msmpires.msi**
|
||||
(https://www.microsoft.com/en-us/download/details.aspx?id=105289).
|
||||
2. `pip install mpi4py`
|
||||
3. Проверка: `mpiexec -n 2 python -c "from mpi4py import MPI; print(MPI.COMM_WORLD.Get_rank())"`
|
||||
|
||||
На macOS/Linux: `brew install open-mpi` / `apt install libopenmpi-dev`,
|
||||
затем `pip install mpi4py`, запуск `mpiexec` (или `mpirun`).
|
||||
|
||||
Важно: запускать `mpiexec -n 4` — тогда `solution.py` выполнится 4 раза
|
||||
параллельно. Запуск без mpiexec допустим: программа работает как один ранг
|
||||
(полезно для отладки и автотестов).
|
||||
|
||||
## Задание
|
||||
|
||||
Заполните `solution.py`:
|
||||
|
||||
1. **`mpi_run(task_name, params)`** — параллельное выполнение вашей CPU-задачи:
|
||||
ранг 0 строит данные и делит на `size` кусков, `scatter` раздаёт,
|
||||
каждый ранг считает `kernel` (используя `dispatch_kernel` не нужно —
|
||||
в MPI функции не пиккелятся, только данные), `gather` собирает,
|
||||
ранг 0 возвращает checksum. Для задачи «Жизнь» — цикл по поколениям
|
||||
(см. подсказку в заготовке).
|
||||
2. **`mpi_pi(N)`** — учебный пример: π методом Монте-Карло на всех рангах,
|
||||
сборка через `reduce` (сумма попаданий). Каждый ранг использует
|
||||
`random.Random(100 + rank)` — свои броски.
|
||||
3. **`main()`** — checksum и время `mpi_run` на текущем числе рангов
|
||||
(`size`), печать с ранга 0. Запустите при `-n 1, 2, 4` — получите T(p).
|
||||
|
||||
### Ожидаемые результаты
|
||||
|
||||
- checksum совпадает с последовательным запуском (лаба 1).
|
||||
- T(p) на одном компьютере ≈ результатам multiprocessing из лабы 4
|
||||
(плюс небольшие расходы на обмен).
|
||||
- На настоящих нескольких машинах код не меняется — меняется только
|
||||
запуск (это главный смысл MPI).
|
||||
|
||||
### Измерения для отчёта
|
||||
|
||||
```bat
|
||||
mpiexec -n 1 python lab08_mpi\solution.py
|
||||
mpiexec -n 2 python lab08_mpi\solution.py
|
||||
mpiexec -n 4 python lab08_mpi\solution.py
|
||||
```
|
||||
|
||||
## Контрольные вопросы
|
||||
|
||||
1. Чем модель передачи сообщений отличается от модели общей памяти?
|
||||
2. Что делает `scatter` и чем она отличается от `bcast`?
|
||||
3. Почему в MPI функции не нужно пиккелить, а в multiprocessing — нужно?
|
||||
4. Что такое ранг? Как ранг 0 узнаёт, сколько всего рангов?
|
||||
5. Когда MPI выгоднее multiprocessing, даже на одном компьютере?
|
||||
|
||||
## Что сдаётся
|
||||
|
||||
`solution.py` + отчёт: вывод при `-n 1/2/4`, таблица T(p), сравнение с
|
||||
лабой 4, ответы на вопросы.
|
||||
|
||||
## Критерии оценки
|
||||
|
||||
| Пункт | Баллы |
|
||||
|---|---|
|
||||
| mpi_run: checksum корректен при -n 1 и -n 4 | 3 |
|
||||
| mpi_pi через reduce | 2 |
|
||||
| Замеры при разном числе рангов | 2 |
|
||||
| Отчёт и ответы на вопросы | 3 |
|
||||
|
||||
## Типичные ошибки
|
||||
|
||||
- Код с веткой «если rank == 0» выполняется ВСЕМИ рангами — не заворачивайте
|
||||
всю программу в `if rank == 0`.
|
||||
- Разные random на разных рангах без фиксации зерна — невоспроизводимый
|
||||
результат.
|
||||
- Заблокированный `recv` без парной `send` — программа висит. Начинайте с
|
||||
коллективных операций, они безопаснее.
|
||||
Reference in new issue
Block a user