This commit is contained in:
lovinervy committed 2026-10-07 13:06:47 +05:00
commit c394677d07
59 files changed
+5149

No files matched your search

+38
View File
@@ -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")
+44
View File
@@ -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("Эта цена обмена и есть накладные расходы распределённых вычислений.")
+44
View File
@@ -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 для суммы попаданий.")
+47
View File
@@ -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 бросков: точность растёт, время — нет.")
+124
View File
@@ -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()
+140
View File
@@ -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` — программа висит. Начинайте с
коллективных операций, они безопаснее.