Кластеры и воспроизводимое окружение для расчётов
Планировщик Slurm, массивы задач, MPI из Python, образы Apptainer и конфигурация узлов кодом
Вячеслав Федоров
Лекция 21
Разработка и применение ПО
в физических исследованиях
Изложение. Двадцать первая лекция завершает тему ускорения расчётов. Глава «CUDA и вычисления на GPU» завершается лестницей ускорения: её первые шесть ступеней, от профилирования до видеокарты, всё полнее используют ресурсы одной машины, а седьмая ступень — кластер. Расчёт, которому одной машины недостаточно, переносится на вычислительный кластер, и при этом возникают три задачи: получить ресурсы кластера, разделить между ними работу и сохранить воспроизводимость результата. В рамках лекции рассматриваются планировщик Slurm, у которого на кластере запрашивают ядра, память и время, массивы задач для сотен независимых расчётов, библиотека MPI для расчёта, разделённого между процессами, образы Apptainer, переносящие окружение расчёта на узлы без изменений, и система управления конфигурацией Ansible, описывающая настройку машин кодом; в завершение кратко обсуждаются Kubernetes и Terraform. Сквозным примером служат расчёты ускорительной лаборатории: скан токов квадрупольного дублета, Монте-Карло эффективности сцинтилляционного счётчика и потенциал в камере. Надпись на титуле — sbatch, команда постановки задачи в очередь Slurm, с которой начинается любая работа на кластере.
Демонстрации. Слайды с розовым выделением слова «Демонстрация» отмечают места, где изложение прерывается для работы в терминале виртуальной машины lab с Ubuntu 24.04, далее называемой стендом: на слайде проигрывается запись тех же команд. На стенде развёрнут одноузловой кластер — Slurm 23.11 с munge и ограничением ресурсов через cgroups, Open MPI 4.1 с mpi4py, Apptainer 1.5 и ansible-core 2.21. У узла два ядра, поэтому времена и ускорения в демонстрациях относятся к двум ядрам виртуальной машины. Демонстрации пользователя кластера выполняются от учебного пользователя student, сборка образа и демонстрации Ansible — от root. Номера задач Slurm меняются от прогона к прогону, а результаты Монте-Карло определяются зёрнами и совпадают на стенде, в образе и на рабочем компьютере.
Содержание
От очереди задач к воспроизводимому окружению и настройке машин кодом
Изложение. Лекция состоит из семи частей. Первая рассматривает устройство вычислительного кластера, роль планировщика и заполнение промежутков в очереди. Вторая посвящена Slurm: его службам, командам пользователя, пакетному скрипту, запросу ресурсов, ограничениям и учёту. Третья вводит массивы задач — основной способ распараллелить скан параметров или серию Монте-Карло — и правила выбора зёрен генератора. Четвёртая переходит к связанным расчётам и библиотеке MPI: обмену точка-точка, коллективным операциям, разбиению области, ускорению и запуску под Slurm. Пятая рассматривает воспроизводимое окружение и образы Apptainer, шестая — управление конфигурацией машин с помощью Ansible. Седьмая кратко описывает Kubernetes и Terraform и пределы их применимости к физическим расчётам.
Цель лекции
Перенести расчёт на кластер и сохранить возможность получить тот же результат снова
Планировщик выделяет ресурсы по запросу, массив задач распараллеливает независимые расчёты, MPI — связанные. Образ Apptainer фиксирует окружение, а Ansible описывает настройку машин кодом, который проверяется пробным прогоном и повторяется без изменений.
Изложение. Цель лекции состоит в том, чтобы перенести расчёт с рабочей машины на кластер, не потеряв воспроизводимости результата. Типичный путь лаборатории к кластеру выглядит так. Сначала расчёты запускаются вручную на нескольких рабочих станциях по ssh, и каждый сотрудник помнит, на какой машине свободны ядра. Затем машины объединяются в кластер, ресурсы которого — ядра, память, видеокарты — рассматриваются как общий пул, а распределяет их планировщик: пользователь описывает, что требуется расчёту, и не выбирает машину сам. Далее выясняется, что одна и та же программа даёт на разных узлах разные числа, потому что версии библиотек на узлах различаются, — так возникает задача воспроизводимого окружения. Наконец, узлов становится столько, что настраивать их вручную невозможно: одна машина обновлена, другая нет, третья переименована, и сценарии, написанные для старых имён, перестают работать; настройку описывают кодом и применяют одинаково ко всем машинам. Каждой из этих ступеней соответствует инструмент лекции: Slurm, массивы задач и MPI, Apptainer, Ansible. Таким образом, лекция рассматривает не только ускорение, но и условия, при которых ускоренный расчёт остаётся проверяемым.
От рабочей машины
к кластеру
Задача Инструмент Часть
очередь и запрос ресурсов Slurm: sbatch, squeue, scancel 1, 2
сотни независимых расчётов массив задач, зерно от номера 3
один связанный расчёт MPI: mpi4py, mpiexec, srun 4
одно окружение везде образ Apptainer 5
одинаковая настройка машин Ansible: плейбук, роль 6
службы и облачные машины Kubernetes, Terraform 7
Пояснение
Ресурсы кластера не берут на узле напрямую: их запрашивают у планировщика и получают на время задачи.
Для физика
Следующая ступень лестницы ускорения из главы «CUDA и вычисления на GPU»: после видеокарты — кластер.
Изложение. Таблица связывает задачи, с которыми физик приходит на кластер, с инструментами и частями лекции. Первая задача — получить ресурсы: на кластере ими распоряжается планировщик, которому отправляют скрипт вместе с запросом ядер, памяти и времени, и первые две части посвящены именно ему. Вторая — выполнить сотни независимых расчётов: точки скана, серии Монте-Карло, файлы запусков установки; для них предназначен массив задач, а воспроизводимость обеспечивает зерно генератора, выведенное из номера задачи. Третья — один расчёт, который не помещается на одну машину или выполняется на ней слишком долго, причём его части обмениваются данными на каждом шаге; для него необходима модель передачи сообщений MPI. Четвёртая — одинаковое окружение на рабочей машине и на узлах кластера: образ Apptainer фиксирует версии интерпретатора и библиотек. Пятая — одинаковая настройка машин — узлов кластера, стендов, компьютеров сбора данных; её обеспечивает система управления конфигурацией Ansible. Наконец, долгоживущие службы и арендуемые облачные машины описываются средствами Kubernetes и Terraform, которые рассмотрены обзорно. В главе «CUDA и вычисления на GPU» ускорение расчёта представлено лестницей из семи ступеней: шесть из них, от профилирования до видеокарты, используют ресурсы одной машины, а седьмой является кластер, которому посвящена эта лекция.
Кластер
Зачем кластер и планировщик: узлы, общая файловая система, очередь, резервирование
01
Кластер
Slurm
Массивы задач
MPI
Apptainer
Ansible
Kubernetes
Изложение. Первая часть рассматривает, из чего состоит вычислительный кластер и почему ресурсы на нём не берут, а запрашивают. Планировщик ведёт очередь задач, выделяет каждой задаче запрошенные ядра, память и время и с помощью резервирования и заполнения промежутков добивается высокой загрузки узлов. Демонстрация на модели очереди показывает, как точность запрошенного лимита времени влияет на ожидание самого пользователя.
Устройство
кластера
рабочая машина
редактор, git
ssh
управляющий узел
slurmctld
общая файловая система
home и scratch видны на всех узлах
01
Узел входа
редактирование и постановка задач, не счёт
02
Вычислительные узлы
сотни и тысячи, часть — с видеокартами
03
Общая файловая система
home с копиями, scratch быстрый и без копий
04
Сеть и модули
InfiniBand для MPI; module load gcc openmpi
Для физика
HTC — тысячи независимых задач, например обработка событий детектора; HPC — один расчёт на многих узлах, например пучок с пространственным зарядом.
Изложение. Вычислительный кластер состоит из машин нескольких назначений. Узел входа (login node) является точкой подключения пользователей по ssh: на нём редактируют и собирают программы и ставят задачи в очередь, но не считают. Вычислительные узлы выполняют задачи; на крупных кластерах их сотни и тысячи, и часть из них оснащена видеокартами. На управляющем узле работает планировщик, который принимает задачи, ведёт очередь и следит за состоянием узлов. Все узлы видят общую файловую систему, обычно разделённую на две области: домашний каталог с небольшой квотой и резервными копиями и рабочую область (scratch) — большую и быструю, но без резервных копий и с периодической очисткой. Для небольших кластеров достаточно NFS, крупные используют параллельные файловые системы наподобие Lustre. Узлы соединены быстрой сетью — InfiniBand или Ethernet с RDMA, — по которой процессы одного расчёта обмениваются данными. Программы на узлах подключаются модулями окружения (команда module load системы Lmod), что позволяет держать на кластере несколько версий компиляторов и библиотек одновременно. Различают два режима работы. HPC (high-performance computing) — плотно связанные расчёты, в которых процессы на многих узлах обмениваются данными на каждом шаге: расчёт пучка с пространственным зарядом, решение уравнений поля. HTC (high-throughput computing) — множество независимых задач по одному-два ядра: обработка событий детектора, сканы параметров. Отметим, что для пользователя кластер представляет собой не набор машин, а общий пул ядер и памяти, из которого планировщик выделяет каждой задаче её долю.
Планировщик:
ресурсы запрашивают
задача = скрипт + запрос ресурсов #SBATCH --ntasks =64 # процессы #SBATCH --cpus-per-task =4 # ядра на процесс #SBATCH --mem=8G # память на узел #SBATCH --time=02:00:00 # лимит времени #SBATCH --gres=gpu:1 # видеокарта
Пояснение
Slurm разрабатывается с 2002 года в LLNL, далее — компанией SchedMD; в декабре 2025 года SchedMD купила NVIDIA, объявив, что Slurm останется открытым.
01
Выделение целиком
ядра и память задачи не делятся с чужими
02
Приоритет
ожидание, размер задачи, справедливая доля
03
Лимит времени
обещание: задача сверх лимита снимается
04
Планировщики
Slurm, OpenPBS и PBS Pro, HTCondor, LSF
Важно
Kubernetes распределяет ресурсы между долгоживущими службами; пакетным расчётам нужен планировщик очереди.
Изложение. Ресурсы кластера распределяет планировщик (batch scheduler). Пользователь не выбирает узел сам: он отправляет задачу, которая представляет собой скрипт и запрос ресурсов — числа процессов и ядер, объёма памяти, лимита времени, при необходимости видеокарт. Планировщик ставит задачу в очередь и запускает её, когда запрошенные ресурсы освобождаются, причём на время работы задачи они принадлежат только ей и не делятся с чужими задачами. Порядок запуска определяется приоритетом, в котором учитываются время ожидания, размер задачи и справедливая доля (fair-share): пользователь, много считавший в последние дни, ждёт дольше того, кто считал мало. Лимит времени является обещанием пользователя: задача, превысившая его, снимается, а планировщик опирается на лимиты при построении расписания. Наиболее распространённым планировщиком научных вычислений является Slurm, разработка которого началась в 2002 году в Ливерморской национальной лаборатории имени Лоуренса; сейчас его развивает компания SchedMD, которую в декабре 2025 года купила NVIDIA, объявив, что Slurm останется открытым программным обеспечением, не привязанным к оборудованию одного производителя. Применяются также OpenPBS и PBS Pro, HTCondor, исторически связанный с массовой обработкой данных физики высоких энергий, и LSF. Кластер, обслуживающий долгоживущие службы и виртуальные машины, решает другую задачу: Kubernetes распределяет ресурсы между службами, которые работают постоянно, тогда как расчёту необходимо получить ресурсы, выполниться и освободить их. Эту разницу рассмотрим подробнее в седьмой части.
Backfill:
заполнение промежутков
for job in list(queue[1:]): name, cores, run, limit = job fits_time = t + limit <= reserve if cores <= free and (fits_time or cores <= spare): queue.remove(job) running.append((t + run, t + limit, cores, name)) start[name] = t free -= cores if not fits_time: spare -= cores
Важно
Планировщик знает только лимит задачи, а не её фактическую длительность.
01
FIFO
строгий порядок, ядра простаивают
02
Резерв
голове очереди — ближайший момент старта
03
Заполнение
меньшие задачи, успевающие до резерва
04
Точный --time
выгоден самому пользователю
Пояснение
Мюалем и Фейтельсон (IEEE TPDS, 2001): при оценках, завышенных в несколько раз, средние показатели очереди даже улучшаются. Проигрывает сама задача, запросившая лишнее.
Изложение. Простейшая очередь запускает задачи строго по порядку постановки (FIFO). Её недостаток проявляется, когда в голове очереди находится большая задача: пока для неё освобождаются ядра, остальные ядра простаивают, хотя следом ждут небольшие задачи, которые могли бы выполниться в образовавшемся промежутке. Поэтому планировщики применяют резервирование с заполнением промежутков (backfill). В схеме EASY первой задаче очереди резервируется ближайший момент, когда для неё освободится достаточно ядер, а следующие задачи запускаются раньше неё при условии, что по запрошенному лимиту они закончатся до резерва или займут ядра, которые резерву не нужны. Фрагмент модели backfill.py записывает именно это условие: задача стартует, если для неё есть свободные ядра и либо её лимит истекает до резерва, либо она умещается в запас ядер, остающийся после резерва. Существенно, что планировщик знает только лимит, а не фактическую длительность задачи: задача, запросившая время с большим запасом, в промежуток не помещается. Отсюда практический вывод — точный лимит времени выгоден прежде всего самому пользователю. Для системы в целом завышенные оценки обходятся дешевле, чем можно ожидать: моделирование заполнения промежутков на нагрузке машин IBM SP2, выполненное А. Мюалемом и Д. Фейтельсоном (IEEE TPDS, 2001), привело к неожиданному результату — при оценках, завышенных в несколько раз, средние показатели очереди даже улучшаются. Проигрывает же сама задача, запросившая лишнее, что и покажет демонстрация.
Демонстрация:
очередь
01
Модель: 8 ядер, восемь задач A–H в момент 0
02
У задачи — ядра, длительность и лимит
03
FIFO, backfill и backfill, где H просит 120 мин
Что наблюдается
FIFO — ожидание 88,8 мин, конец 170 мин, загрузка 67 %; backfill — 45,0 мин, 130 мин, 88 %; с лимитом 120 мин задача H стартует в 130 мин вместо 20.
student@lab — запись со стенда
Демонстрация. Сценарий backfill.py представляет собой событийную модель планировщика на 8 ядрах: восемь задач A–H поставлены в очередь одновременно, у каждой заданы число ядер, фактическая длительность и запрошенный лимит. Модель сравнивает три варианта: строгий порядок, заполнение промежутков по схеме EASY и тот же backfill, в котором задача H запросила 120 минут «с запасом» вместо достаточных ей 20. Первая таблица показывает моменты старта каждой задачи. Большая задача B, занимающая все 8 ядер, стартует в 60 минут во всех трёх вариантах — когда завершается задача A. При строгом порядке задачи C, D и E ждут до 90 минут, а H — до 140. При заполнении промежутков C и D стартуют сразу, в 0, а H — в 20 минут, и все три успевают закончиться до резерва для B. Вторая таблица сводит итоги: среднее ожидание сокращается с 88,8 до 45,0 минуты, конец расписания — со 170 до 130 минут, загрузка ядер растёт с 67 до 88 %. Если же H запрашивает 120 минут, она перестаёт помещаться в промежуток до резерва и стартует только в 130 минут; среднее ожидание составляет 58,8 минуты, загрузка — 78 %. Модель выполнена на стенде обычным интерпретатором Python: планировщик для неё не нужен. Она воспроизводит схему EASY, в которой резерв получает только голова очереди; модуль sched/backfill в Slurm поступает осторожнее — задачу с меньшим приоритетом он запускает раньше, только если она не сдвигает ожидаемого старта ни одной более приоритетной задачи, а ожидаемое время старта сообщает команда squeue --start. Для восьми задач модели обе схемы дают одно и то же расписание.
Результат:
три расписания
Что наблюдается
Загрузка ядер растёт с 67 до 88 %, а большая задача B стартует в 60 мин во всех вариантах; лишние 100 минут в лимите задачи H обходятся ей в 110 минут ожидания.
Демонстрация. Рисунок построен по той же модели backfill.py. Три панели показывают расписания: по горизонтали — время от 0 до 175 минут, по вертикали — восемь ядер. Большая задача B, занимающая все ядра, выделена синим, задача H — оранжевым, остальные — серым. На левой панели, при строгом порядке, после окончания B видны пустые участки: ядра простаивают, пока для задачи G освобождаются шесть ядер, а H ждёт в очереди за ней и стартует в 140 минут. На средней панели задачи C, D и H заполняют промежуток слева от B: H стартует в 20 минут на ядрах, освободившихся после C, и расписание заканчивается в 130 минут. На правой панели H с лимитом 120 минут в промежуток не помещается и оказывается в самом конце расписания, начиная в 130 минут. Над панелями приведены среднее ожидание и загрузка ядер: 88,8 мин и 67 %, 45,0 мин и 88 %, 58,8 мин и 78 %. Таким образом, заполнение промежутков повышает загрузку, не задерживая большую задачу, а лишние 100 минут лимита стоят задаче H 110 минут ожидания.
Slurm
Службы, команды, пакетный скрипт, ресурсы и лимиты, учёт
02
Кластер
Slurm
Массивы задач
MPI
Apptainer
Ansible
Kubernetes
Изложение. Вторая часть посвящена Slurm — планировщику, на котором работает большинство научных кластеров. Рассматриваются его службы, команды пользователя, пакетный скрипт с запросом ресурсов, ограничения, которые обеспечивает механизм cgroups, и учёт выполненных задач. Две демонстрации показывают первую задачу на одноузловом кластере стенда и поведение задач, запросивших больше, чем есть или чем им разрешено.
Устройство
Slurm
команды
sbatch, srun, squeue, scancel
slurmdbd
учёт в MySQL или MariaDB
slurmctld
очередь, расписание, узлы
munge: один ключ на всех узлах
01
slurmctld
очередь, расписание, состояние узлов
02
slurmd
на каждом узле: процессы задачи через slurmstepd
03
munge
подлинность сообщений по общему ключу
04
slurmdbd
необязательный учёт в MySQL или MariaDB
Пояснение
Стенд: один узел lab — 2 ядра и 3000 МБ для задач, раздел main, Slurm 23.11.4 из Ubuntu 24.04.
Изложение. Slurm состоит из нескольких служб. Контроллер slurmctld является центром системы: он принимает задачи, ведёт очередь, строит расписание и следит за состоянием узлов. На каждом вычислительном узле работает служба slurmd, которая запускает процессы задачи через вспомогательную программу slurmstepd: для каждого шага задачи создаётся свой slurmstepd, следящий за процессами шага и их ограничениями. Подлинность сообщений между командами, контроллером и узлами обеспечивает служба munge, использующая общий секретный ключ, одинаковый на всех узлах кластера. Необязательная служба slurmdbd записывает учёт выполненных задач в базу MySQL или MariaDB; без неё кластер работает, но полного учёта нет. Узлы объединяются в разделы (partitions), которые выполняют роль очередей с собственными лимитами: короткие отладочные задачи, длинные расчёты, узлы с видеокартами. Пользователь обращается к контроллеру командами sbatch, srun, squeue, scancel и другими, рассмотренными на следующем слайде. Стенд лекции представляет собой минимальный кластер: один узел lab, который одновременно является и управляющим, и вычислительным, два ядра и 3000 МБ памяти, отведённые задачам, один раздел main с лимитом времени в один час. Используется Slurm 23.11.4 из пакетов Ubuntu 24.04; актуальной на осень 2026 года является ветка 26.05, однако для изложенного в лекции различия между версиями несущественны.
Команды
пользователя
Команда Назначение
sinfo состояние разделов и узлов
squeue очередь: состояние и причина ожидания
sbatch скрипт постановка пакетной задачи
srun программа шаг задачи; вне задачи — выделение и запуск
salloc выделение для интерактивной работы
scancel номер снятие задачи
scontrol show job|node подробности о задаче или узле
sacct учёт завершённых задач
PD ожидает: Resources, Priority, Dependency, PartitionConfig R выполняется CG завершается CD завершена успешно F завершилась с ошибкой CA снята TO превышен лимит времени OOM превышена память
Пояснение
job — выделение ресурсов, step — запуск srun внутри выделения, task — процесс шага.
Изложение. Пользователь работает с кластером несколькими командами. Команда sinfo показывает разделы и узлы с их состоянием, squeue — очередь: задачи, их состояние и причину ожидания. Команда sbatch ставит в очередь пакетную задачу и сразу возвращает управление, напечатав её номер; srun запускает шаг внутри уже выделенной задачи, а вызванная вне задачи — сама получает выделение и запускает программу; salloc выделяет ресурсы для интерактивной работы. Команда scancel снимает задачу, scontrol show job и scontrol show node выводят подробные сведения о задаче или узле, а sacct — учёт завершённых задач. В терминологии Slurm задача (job) представляет собой выделение ресурсов, шаг (step) — запуск программы внутри выделения командой srun, а процессы шага (tasks) — экземпляры этой программы; у задачи из одного шага на одном ядре все три понятия совпадают, у расчёта MPI на нескольких узлах — нет. Состояние задачи обозначается кратким кодом: PD — ожидает, R — выполняется, CG — завершается, CD — завершена успешно, F — завершилась с ошибкой, CA — снята, TO — превысила лимит времени, OOM — превысила память. Для ожидающей задачи squeue указывает причину: Resources — ресурсы заняты, Priority — впереди задачи с большим приоритетом, Dependency — задача ждёт завершения другой, PartitionConfig — запрос превышает возможности раздела. Отметим, что формат вывода squeue по умолчанию шире 80 колонок, поэтому в демонстрациях поля задаются ключом -o.
Пакетный
скрипт
#!/bin/bash #SBATCH --job-name=hello #SBATCH --cpus-per-task =1 #SBATCH --mem=100M #SBATCH --time=00:01:00 #SBATCH --output=hello-%j.out # Первая задача: что планировщик выделил и что видит процесс echo "задача $SLURM_JOB_ID, узел $SLURMD_NODENAME, раздел $SLURM_JOB_PARTITION" echo "ядер: $SLURM_CPUS_ON_NODE, память: $SLURM_MEM_PER_NODE МБ" python3 -c 'import os; print("доступны ядра:", sorted(os.sched_getaffinity(0)))' sleep 5
Пояснение
Строки #SBATCH — комментарии для оболочки и параметры для sbatch; %j в имени файла вывода заменяется номером задачи.
Важно
Окружение оболочки наследуется (--export=ALL ): python3 в задаче — тот же, что при постановке, например из venv.
Изложение. Задача описывается скриптом командной оболочки, в начале которого строки #SBATCH задают запрос ресурсов. Для оболочки эти строки являются комментариями, а sbatch читает из них параметры так, как если бы они были переданы в командной строке; параметр, указанный в самой командной строке, имеет приоритет над строкой скрипта. Скрипт hello.sh запрашивает одно ядро, 100 МБ памяти и одну минуту, а вывод направляет в файл hello-%j.out, где %j заменяется номером задачи; для постановки достаточно команды sbatch hello.sh. Тело скрипта печатает, что выделил планировщик: номер задачи, узел и раздел из переменных окружения SLURM_JOB_ID, SLURMD_NODENAME и SLURM_JOB_PARTITION, число ядер и объём памяти из SLURM_CPUS_ON_NODE и SLURM_MEM_PER_NODE. Строка Python выводит ядра, на которых процессу разрешено выполняться: функция os.sched_getaffinity возвращает множество, заданное ограничением ядер, а пауза sleep 5 оставляет время увидеть задачу в очереди. По умолчанию задача наследует окружение командной оболочки, из которой её поставили (--export=ALL), поэтому python3 в задаче — тот же интерпретатор, что был найден при постановке, например из виртуального окружения. Наследование окружения удобно, но делает результат зависимым от состояния оболочки в момент постановки; к этому вопросу вернёмся в пятой части. Рассмотрим, что покажет этот скрипт на стенде.
Демонстрация:
первая задача
01
sinfo: раздел main и узел lab
02
scontrol show node: ядра, память, состояние
03
sbatch slurm/hello.sh: номер задачи
04
squeue с коротким набором полей
05
Через 6 с: файл вывода задачи
Что наблюдается
Узел lab: 2 ядра, 3000 МБ, idle; задача 60 сразу в состоянии R; в выводе — «ядер: 1, память: 100 МБ» и «доступны ядра: [0]», одно ядро из двух.
student@lab — запись со стенда
Демонстрация. Демонстрация выполняется от учебного пользователя student в каталоге /tmp/l21rec. Команда sinfo показывает раздел main, помеченный звёздочкой как раздел по умолчанию, в состоянии up с лимитом времени в один час и единственный узел lab в состоянии idle, то есть свободный. Строки вывода scontrol show node, выбранные grep, сообщают, что у узла 2 ядра (CPUTot=2), задачам отведено 3000 МБ памяти (RealMemory=3000), свободной памяти на машине, по данным системы, 2871 МБ, и узел простаивает (State=IDLE). Команда sbatch slurm/hello.sh ставит задачу в очередь и печатает её номер — 60. Команда squeue с коротким набором полей показывает задачу 60 с именем hello от пользователя student в состоянии R на узле lab: свободных ресурсов достаточно, и задача запущена сразу. Через шесть секунд файл hello-60.out содержит три строки: «задача 60, узел lab, раздел main», «ядер: 1, память: 100 МБ» — ровно то, что запрошено строками #SBATCH, — и «доступны ядра: [0]». Последняя строка показывает действие ограничения ядер, которое обеспечивает механизм cgroups: узел свободен и ядер у него два, однако процессу задачи разрешено выполняться только на одном из них, выделенном планировщиком. Без такого ограничения задача видела бы оба ядра и могла бы занять ядро, выделенное соседней задаче.
Ресурсы
и лимиты
01
--cpus-per-task
ядра на процесс: потоки OpenMP и BLAS
02
--ntasks
число процессов, например MPI
03
--mem и --mem-per-cpu
память на узел или на ядро
04
cgroup v2: только выделенное
ConstrainCores и ConstrainRAMSpace
export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK память сверх --mem → OUT_OF_MEMORY время сверх --time → TIMEOUT -c 4, в разделе 2 ядра → PD (PartitionConfig)
Важно
Запрос с запасом выполним, но задача дольше ждёт, не помещается в промежутки очереди и занимает место соседей.
Изложение. Запрос ресурсов различает процессы и потоки. Параметр --ntasks задаёт число процессов и является основным запросом программ MPI; --cpus-per-task задаёт число ядер на процесс, то есть потоков OpenMP или библиотеки BLAS, которой пользуется NumPy. Если ограничение ядер на кластере не включено, библиотеки видят все ядра узла и запускают по потоку на каждое, мешая соседним задачам, поэтому число потоков необходимо задать явно: export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK. Память запрашивается на узел ключом --mem или на ядро ключом --mem-per-cpu. Выделенное обеспечивается механизмом cgroups ядра Linux, рассмотренным в лекции 3 и в главе «Внутреннее устройство Linux»: процессы задачи помещаются в отдельную группу, ограниченную выделенными ядрами и памятью; в Slurm это включают параметры ConstrainCores и ConstrainRAMSpace файла cgroup.conf. Процесс, превысивший память, завершается ядром по нехватке памяти, и задача получает состояние OUT_OF_MEMORY; задача, превысившая лимит времени, снимается с состоянием TIMEOUT. Запрос, превышающий возможности раздела в целом, например четыре ядра в разделе из одного двухъядерного узла, как на стенде, по умолчанию принимается и ожидает с причиной PartitionConfig, пока его не снимут или не изменят конфигурацию раздела; значения ANY и ALL параметра EnforcePartLimits позволяют отклонять такие задачи сразу при постановке. Если же ядер в разделе достаточно, но ни на одном его узле нет четырёх, sbatch отклоняет задачу сразу при любом значении этого параметра: «CPU count per node can not be satisfied». Запрос с запасом выполним, но обходится дорого: задача дольше ждёт освобождения ресурсов, не помещается в промежутки очереди и занимает место, которое могли бы использовать другие.
Учёт
выполненных задач
01
sacct
JobID, State, Start, Elapsed, ExitCode, MaxRSS
02
slurmdbd
полный учёт: шаги, пиковая память, seff
03
jobcomp/filetxt
строка на задачу, без шагов и памяти
04
Запрос по измерению
память × 1,2, время × 1,5 от измеренного
# стенд: Slurm 23.11.4 без slurmdbd squeue -t all # завершённые задачи # в течение MinJobAge, 300 с python3 timeline.py /var/log/slurm/jobcomp.log sacct -c # только root и slurm
Важно
В 23.11.4 sacct -c открывает журнал jobcomp на дозапись, и обычный пользователь получает отказ в доступе.
Изложение. Подбирать запрос ресурсов по измерениям позволяет учёт. Команда sacct показывает для завершённых задач номер, состояние, время старта, длительность и код завершения, а при полном учёте — и пиковую память MaxRSS, по которой выбирают --mem; сценарий seff из дополнительных программ Slurm сравнивает запрошенное с использованным, и многие вычислительные центры его устанавливают. Распространённое практическое правило — запрашивать примерно на 20 % больше измеренной памяти и в полтора раза больше измеренного времени. Полный учёт обеспечивает служба slurmdbd с базой MySQL или MariaDB: в ней хранятся задачи, шаги и потреблённые ресурсы. На небольшом кластере или стенде вместо неё можно вести журнал завершения задач (JobCompType=jobcomp/filetxt): одна строка на задачу с её состоянием и временами, без шагов и памяти. Команда sacct -c читает такой журнал без базы данных, однако в Slurm 23.11.4 она работает только от root и от пользователя slurm: при загрузке модуля jobcomp/filetxt sacct открывает журнал на дозапись, и обычный пользователь получает отказ в доступе, хотя читать файл может любой. Поэтому на стенде недавние задачи пользователь смотрит командой squeue -t all — контроллер помнит завершённые задачи в течение MinJobAge, по умолчанию 300 с, — а временную диаграмму массива строит сценарий timeline.py, читающий журнал напрямую. На кластере с полным учётом обе задачи решает sacct.
Демонстрация:
ресурсы и лимиты
01
-c 4 на узле с двумя ядрами
02
Две задачи по 2 ядра со sleep 10
03
squeue: причины ожидания
04
scancel -t PD: снятие ожидающих
05
--mem=200M, программа на 400 МиБ
06
Через 12 с: вывод задачи 64
07
squeue -t all: итог по задачам
Что наблюдается
61 принята и ждёт: PartitionConfig; 63 ждёт ядра: Resources; 64 — oom_kill после «занято 150 МиБ», OUT_OF_MEMORY ; 62 — COMPLETED за 0:10.
student@lab — запись со стенда
Демонстрация. Первая команда запрашивает 4 ядра на узле, у которого их два, для команды hostname, переданной ключом --wrap без отдельного скрипта. Задача 61 принята: по умолчанию Slurm не отклоняет запрос, превышающий возможности раздела, а оставляет его в очереди. Цикл ставит две задачи по два ядра, каждая из которых 10 с спит; они получают номера 62 и 63. Через три секунды squeue показывает три задачи: 62 выполняется на узле lab, 63 ждёт с причиной Resources — оба ядра заняты задачей 62, — а 61 ждёт с причиной PartitionConfig: такой запрос не будет выполнен никогда, и задача останется в очереди, пока её не снимут. Команда scancel -t PD -u student снимает все ожидающие задачи пользователя, то есть 61 и 63. Следующая задача запускает программу hog.py, которая занимает память блоками по 50 МиБ до 400 МиБ, с запросом --mem=200M; она получает номер 64 и стартует после окончания задачи 62, занимавшей оба ядра. Через 12 с последние строки файлов вывода показывают «занято 100 МиБ», «занято 150 МиБ», сообщение Killed, которое является отчётом bash о завершении процесса python3 сигналом, и строку slurmstepd об одном событии oom_kill в шаге 64.batch: на четвёртом блоке процесс превысил выделенные 200 МиБ и был завершён ядром, а не планировщиком. Команда fold лишь переносит длинную строку slurmstepd по словам, чтобы она уместилась в окне. Последняя команда, squeue -t all, показывает и завершённые задачи: 61 и 63 — CANCELLED, 62 — COMPLETED за 10 с, задача 60 первой демонстрации — COMPLETED за 5 с, 64 — OUT_OF_MEMORY. Контроллер хранит завершённые задачи в течение MinJobAge, по умолчанию 300 с, поэтому здесь достаточно squeue.
Правила
общего кластера
01
Узел входа
редактирование и постановка задач, не счёт
02
Рабочая область
данные — в scratch, результаты — в архив
03
Мелкие задачи
массивом или объединением в порции
04
Квоты
на объём и на число файлов
05
Окружение
модули кластера, venv или образ
06
Лимит по измерению
с запасом, но без кратного завышения
Пояснение
Сессия tmux на узле входа служит для интерактивной работы; долгий расчёт отправляется в очередь командой sbatch.
Изложение. Кластер разделяют десятки пользователей, и правила вычислительных центров коллективного пользования похожи. На узле входа не выполняют расчёты: их отправляют в очередь, а для интерактивной работы запрашивают узел командой salloc или srun --pty bash. Сессия tmux на узле входа, описанная в главе «Инструменты Linux», служит для интерактивной работы, а не для многочасового счёта. Данные расчёта размещают в рабочей области, а результаты своевременно забирают, поскольку рабочую область периодически чистят и резервных копий в ней нет. Тысячи задач длительностью в несколько секунд ставят не по отдельности, а массивом или объединяют в более крупные порции: накладные расходы планировщика на запуск задачи сравнимы с такой работой, что покажет демонстрация третьей части. Квоты ограничивают не только объём файлов, но и их число, поэтому миллион маленьких файлов результата хуже одного файла HDF5. Окружение берут из модулей кластера или переносят своё — виртуальным окружением либо образом контейнера, которому посвящена пятая часть. Лимит времени выбирают по измерению с запасом, но без кратного завышения, а справедливая доля означает, что вчерашний большой расчёт увеличивает сегодняшнее ожидание.
Массивы задач
Скан параметров одним скриптом, сбор по зависимости, зёрна генератора
03
Кластер
Slurm
Массивы задач
MPI
Apptainer
Ansible
Kubernetes
Изложение. Третья часть посвящена массивам задач — основному способу распараллелить на кластере независимые расчёты: точки скана параметров, серии Монте-Карло, обработку файлов запусков установки. Рассматриваются постановка массива, сбор результатов задачей с зависимостью и выбор зёрен генератора, при котором задачи массива независимы и воспроизводимы.
Массив
задач
#!/bin/bash #SBATCH --job-name=scan #SBATCH --array=0-11 #SBATCH --cpus-per-task =1 #SBATCH --mem=300M #SBATCH --time=00:05:00 #SBATCH --output=logs/scan-%A_%a.out # Строка k двумерного скана токов дублета: I_F = 2k А, I_D = 0…24 А python3 scan_point.py "$SLURM_ARRAY_TASK_ID" results/
01
SLURM_ARRAY_TASK_ID
номер задачи 0…11; параметр — в программе
02
%A_%a
номер массива и номер задачи в имени вывода
03
-d afterok:ID
сбор — после успешного завершения всех задач
Пояснение
Суффикс %2 (--array=0-11 %2) ограничивает число одновременных задач массива; MaxArraySize по умолчанию 1001, номера 0–1000 . Каталог logs для вывода Slurm с версии 23.02 создаёт сам.
Изложение. Большая часть расчётов лаборатории состоит из независимых частей, и для них в Slurm предусмотрен массив задач, который представляет собой множество однотипных задач одного скрипта и позволяет поставить их в очередь одной командой. Строка #SBATCH --array=0-11 превращает скрипт scan.sh в двенадцать задач, каждая из которых получает свой номер в переменной окружения SLURM_ARRAY_TASK_ID; отображение номера на параметр выполняет сама программа — по формуле, как здесь, или по строке таблицы параметров. В имени файла вывода %A заменяется номером массива, а %a — номером задачи в нём, поэтому выводы задач не смешиваются. Суффикс %2, записанный как --array=0-11%2, ограничивает число одновременно выполняемых задач массива и тем самым долю кластера, которую занимает один пользователь; на стенде с двумя ядрами задачи по одному ядру и без него выполняются не более чем по две. Задачи массива независимы, и порядок их выполнения не гарантирован. Наибольший номер задачи определяется параметром MaxArraySize конфигурации, по умолчанию равным 1001, то есть допустимы номера от 0 до 1000. Результаты всех задач собирает отдельная задача, которой задана зависимость: sbatch -d afterok:ID collect.sh запускает сбор только после успешного завершения всех задач массива с номером ID; номер удобно получить ключом --parsable, который печатает только его. Недостающий каталог для файлов вывода, такой как logs, Slurm начиная с версии 23.02 создаёт сам; в более ранних версиях задача с несуществующим каталогом в пути вывода завершается ошибкой, и такие каталоги на старых кластерах необходимо создавать заранее. Каталог results создаёт сама программа scan_point.py.
Скан токов
дублета
Bρ = p/e ≈ 0.0183 Тл·м # электроны 5 МэВ 1/f = G·L/(Bρ·(1 + δ)), G = 0.02 Тл/(м·А)·I ε = 1 мм·мрад, β = 2 м, α = -1, δ: 2 % задача k: I_F = 2k А, I_D = 0…24 А точка: 4·10⁶ частиц, зерно [скан, k, j]
01
Задача массива
строка карты: 25 точек тока QD
02
Результат
файл results/row-KK.json
03
collect.py
пропуски и лучшая пара токов
Для физика
Сила линзы зависит от импульса частицы, поэтому долю прошедших частиц считают методом Монте-Карло , а не по огибающей.
Изложение. Рассмотрим подбор токов квадрупольного дублета, проводящего пучок электронов с кинетической энергией 5 МэВ через коллиматор радиусом 3 мм. Магнитная жёсткость пучка составляет Bρ = p/e ≈ 0,0183 Тл·м. Канал состоит из дрейфа 0,5 м, линзы QF, промежутка 0,3 м, линзы QD и дрейфа 1,5 м до коллиматора. Линзы длиной 0,1 м считаются тонкими, градиент пропорционален току с коэффициентом 0,02 Тл/(м·А), и оптическая сила линзы равна 1/f = GL/(Bρ). Линза QF фокусирует пучок по x и дефокусирует по y, линза QD — наоборот. Пучок на входе гауссов, с эмиттансом 1 мм·мрад в каждой плоскости, функциями Твисса β = 2 м и α = −1 и разбросом импульса 2 %. Из-за разброса импульса сила линз у разных частиц различна — это хроматизм, — поэтому долю прошедших частиц определяют методом Монте-Карло: частицы переносятся через канал, и подсчитываются попавшие в апертуру. Полная карта по двум токам естественно делится на строки: задача массива с номером k вычисляет строку с током QF, равным 2k А, и 25 значениями тока QD от 0 до 24 А, по 4·10⁶ частиц в каждой точке, и записывает её в файл results/row-KK.json. Зерно генератора каждой точки составлено из общего зерна скана, номера задачи и номера точки, поэтому любая точка воспроизводима независимо от того, какая задача и на каком узле её считала. Сценарий collect.py читает все файлы, проверяет, что ни одна строка не пропущена, и находит лучшую пару токов. Модель учебная: тонкие линзы без аберраций высших порядков и без пространственного заряда.
Демонстрация:
скан токов
01
Массив scan.sh: номер — в переменной J
02
squeue: задачи массива в очереди
03
Сбор: зависимость afterok от массива
04
Через 30 с: таблица collect.py
05
timeline.py: расписание по журналу
Что наблюдается
Массив 79: 79_0 и 79_1 — R, 79_[2-11] — PD (Resources); 12 строк без пропусков, лучшая пара 18 и 19 А — 94,78 %; 35 с счёта за 22 с.
student@lab — запись со стенда
Демонстрация. Демонстрация выполняется от пользователя student; каталоги logs и results созданы заранее. Первая команда ставит массив в очередь с ключом --parsable и сохраняет его номер в переменной J: это массив 79. Команда squeue показывает две задачи массива, 79_0 и 79_1, в состоянии R на узле lab и строку 79_[2-11] — остальные десять задач в состоянии PD с причиной Resources: у узла два ядра, и каждая задача занимает одно. Третья команда ставит задачу сбора с зависимостью afterok от массива; она получает номер 82 и ожидает завершения всех задач массива. Через 30 с файл вывода задачи сбора содержит таблицу collect.py: строк 12, пропущенных нет; для каждого тока QF — лучший ток QD, доля прошедших частиц, размеры пучка на коллиматоре и время счёта задачи — от 2,8 до 3,0 с. Наибольшее прохождение, 94,78 %, достигается при I_F = 18 А и I_D = 19 А, а сумма времени всех задач составляет 35 с. Последняя команда, timeline.py, читает журнал завершения задач Slurm и восстанавливает расписание массива: задачи шли парами — 0 и 1 с нулевой по третью секунду, 2 и 3 с шестой по девятую, затем пары стартовали через каждые 3–4 с, и последняя пара, 10 и 11, закончилась на 22-й секунде от постановки массива. Таким образом, 35 с счёта выполнены за 22 с, одновременно — не более двух задач. Промежутки между парами представляют собой задержку планировщика между завершением одних задач и запуском следующих; журнал хранит время с точностью до секунды, поэтому длительности 2–3 с округлены. Для задач в несколько секунд такая задержка составляет заметную долю времени, поэтому на настоящем кластере задачи массива делают длительностью в минуты, объединяя точки в порции.
Результат:
карта и задачи массива
Что наблюдается
Максимум 94,8 % при I_F = 18 А и I_D = 19 А; задачи массива шли парами по 2–3 с, промежутки между парами — задержка планировщика; сбор — после всех 12 задач.
Демонстрация. Рисунок построен по результатам записи: массив 79 и задача сбора 82. Слева — карта прохождения пучка через коллиматор: 12 строк соответствуют задачам массива с током QF от 0 до 22 А с шагом 2 А, 25 столбцов — току QD от 0 до 24 А; цвет показывает долю прошедших частиц от 0 до 100 %. Оранжевый крест отмечает максимум, 94,8 % при I_F = 18 А и I_D = 19 А. Область высокого прохождения вытянута наискосок: каждому току QF соответствует свой лучший ток QD, растущий вместе с ним, поскольку дублет фокусирует пучок в обеих плоскостях, только если силы линз согласованы. В лучшей точке, по таблице сбора, размеры пучка на коллиматоре составляют 1,52 мм по x и 0,50 мм по y; при меньшем токе QD пучок не успевает сфокусироваться по y, а при большем перефокусируется по y и снова расширяется к коллиматору. Справа — задачи массива во времени по журналу jobcomp/filetxt: две полосы соответствуют двум ядрам узла, на полосах — номера задач; журнал не хранит, на каком ядре шла задача, поэтому задачи разложены по полосам в порядке старта. Задачи идут парами; после первой пары промежуток составляет около 3 с, далее — около 1 с: это задержка планировщика между завершением задач и запуском следующих. Оранжевая отметка «сбор» — задача сбора, запущенная зависимостью afterok после окончания последней пары. Отметим, что 12 задач по 3 с — заведомо слишком мелкое деление для настоящего кластера: при задачах в несколько минут та же задержка составила бы доли процента времени.
Зёрна
генератора
rng = np.random.default_rng(42) # (а) rng = np.random.default_rng(int(time.time())) # (б) rng = np.random.default_rng([20261005, k]) # (в)
01
(а) одно зерно
одинаковые результаты, разброс равен нулю
02
(б) время старта
задачи массива стартуют в одну секунду
03
(в) [общее зерно, номер]
SeedSequence: независимые воспроизводимые потоки
Пояснение
Политика совместимости NumPy: тот же поток гарантирован для одной сборки в одном окружении; между версиями и платформами возможны редкие отличия — версию фиксирует образ.
Для физика
В файл результата записываются зерно, версии Python и NumPy.
Изложение. Задачи массива Монте-Карло должны получать независимые последовательности случайных чисел, и способ задания зерна влияет на результат сильнее, чем может показаться. Сценарий с фиксированным зерном, скопированный в каждую задачу, даёт одинаковые результаты во всех задачах: разброс между ними равен нулю, и погрешность, оценённая по разбросу, тоже равна нулю. Зерно, взятое из текущего времени с точностью до секунды, на кластере повторяется, поскольку задачи массива стартуют почти одновременно, и задачи, стартовавшие в одну секунду, повторяют друг друга. Надёжный способ в NumPy состоит в передаче генератору последовательности из общего зерна расчёта и номера задачи: default_rng([20261005, k]) строит по ней с помощью SeedSequence потоки и тем самым обеспечивает их статистическую независимость при полной воспроизводимости. Версии существенны: политика совместимости NumPy гарантирует одинаковую последовательность для одного зерна при одной сборке библиотеки в одном окружении на одной машине, а между версиями и платформами в редких случаях возможны отличия. Поэтому в файл результата необходимо записывать зерно, версии Python и NumPy, а сами версии фиксирует образ, рассмотренный в пятой части. Демонстрация сравнивает три способа на модели сцинтилляционного счётчика.
Демонстрация:
зёрна
01
Восемь задач по 200 000 квантов: счётчик NaI(Tl)
02
(а) зерно 42 во всех задачах
03
(б) зерно — время старта, по секундам
04
(в) зерно [общее зерно, номер задачи]
Что наблюдается
Квадратура 0,8808 %: (а) один результат на все 8 задач, разброс 0; (б) три различных, −2,5 σ; (в) восемь различных, −0,5 σ при ожидаемой погрешности 0,0074 %.
student@lab — запись со стенда
Демонстрация. Сценарий seeds.py моделирует восемь задач массива, каждая из которых считает эффективность сцинтилляционного счётчика NaI(Tl) размером 2×2 дюйма для точечного изотропного источника на расстоянии 10 см по 200 000 квантам. Точное значение эффективности, вычисленное квадратурой, составляет 0,8808 %. Для каждого способа задания зерна сценарий печатает число различных результатов среди восьми, среднее, погрешность среднего по разбросу задач и отклонение среднего от точного значения в единицах этой погрешности. При зерне 42 во всех задачах результат один, 0,8890 %, разброс равен нулю, и отклонение формально бесконечно: погрешность, оценённая по разбросу, ничего не говорит о настоящей. При зерне, равном времени старта, различных результатов три — по числу различных секунд старта в модели, — и среднее 0,8672 % отклоняется от точного на −2,5 погрешности, поскольку повторы занижают разброс. Последовательность из общего зерна и номера задачи даёт восемь различных результатов, среднее 0,8776 % и отклонение −0,5 погрешности, что согласуется со статистикой: ожидаемая погрешность среднего по восьми задачам составляет 0,0074 %, а оценённая по разбросу — 0,0060 %. Таким образом, зерно задачи необходимо выводить из её номера, а не из времени.
MPI из Python
Обмен сообщениями, коллективные операции, разбиение области, ускорение, запуск под Slurm
04
Кластер
Slurm
Массивы задач
MPI
Apptainer
Ansible
Kubernetes
Изложение. Четвёртая часть переходит от независимых расчётов к связанным, части которых обмениваются данными на каждом шаге. Рассматриваются модель передачи сообщений MPI и её привязка к Python mpi4py, обмен точка-точка и коллективные операции, разбиение расчётной области между процессами, измерение ускорения и закон Амдала, а также запуск программы MPI внутри задачи Slurm. Четыре демонстрации выполняются на двух ядрах стенда.
Модель
MPI
from mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() # 0, 1, …, size - 1 size = comm.Get_size() $ mpiexec -n 2 python3 pingpong.py
Пояснение
multiprocessing запускает процессы только на одной машине — глава «Многопоточность и GIL».
01
SPMD
одна программа в нескольких процессах, у каждого — ранг
02
Без общей памяти
данные передаются сообщениями, процессы — на разных узлах
03
Реализации
Open MPI и MPICH; MPI 5.0 (2025) — с единым ABI
04
Массив или MPI
независимые расчёты — массив, связанные — MPI
Изложение. Если части расчёта обмениваются данными на каждом шаге, как при решении уравнения поля на сетке, разделённой между узлами, массив задач не подходит: необходима модель с явной передачей данных между процессами. Стандартом такой модели является MPI (Message Passing Interface). Программа MPI запускается в нескольких экземплярах одновременно — обычно одна и та же программа, модель SPMD, — и каждый процесс получает свой номер, ранг, в коммуникаторе COMM_WORLD, объединяющем все процессы. Общей памяти у процессов нет: данные передаются сообщениями, и процессы могут находиться на разных узлах. Распространены две свободные реализации стандарта — Open MPI и MPICH. В июне 2025 года утверждён стандарт MPI 5.0, впервые определивший единый двоичный интерфейс: программа, собранная с одной поддерживающей его реализацией, может работать с другой. Привязкой Python к MPI является пакет mpi4py; начиная с версии 4.1 он распространяется готовыми колёсами для Linux и macOS, работающими как с системными Open MPI и MPICH, так и с их колёсами в том же виртуальном окружении. На стенде mpi4py 4.1.2 установлен колесом и использует Open MPI 4.1.6 из пакетов Ubuntu. Программа запускается командой mpiexec -n 2 python3 и имя программы. Модуль multiprocessing, рассмотренный в главе «Многопоточность и GIL», тоже запускает несколько процессов, но только на одной машине и без средств обмена между узлами. Массив задач остаётся предпочтительным для независимых расчётов: он не требует обмена, а отказ одной задачи затрагивает только её.
Обмен
точка-точка
# pingpong.py, процесс 0 if rank == 0: if pickled: comm.send(buf, dest=1) buf = comm.recv(source=1) else: comm.Send(buf, dest=1) comm.Recv(buf, source=1)
Важно
Два процесса, одновременно отправляющие друг другу большие сообщения через Send, могут ждать друг друга бесконечно.
01
send и recv
любой объект Python через pickle
02
Send и Recv
буфер NumPy без сериализации
03
Isend, Irecv и Wait
неблокирующий обмен
04
Sendrecv
встречный обмен одной операцией
Изложение. Каждое сообщение имеет отправителя, получателя и метку. В mpi4py методы со строчной буквы, send и recv, передают произвольный объект Python, сериализуя его модулем pickle, а методы с прописной, Send и Recv, передают содержимое буфера — например, массива NumPy — без сериализации; приёмный буфер нужного размера и типа создаётся заранее. Фрагмент сценария pingpong.py показывает обе формы: процесс 0 отправляет массив процессу 1 и ждёт его обратно. Обмен блокирующий: recv ждёт сообщения, а send большого сообщения может ждать, пока получатель не начнёт приём. Поэтому два процесса, которые одновременно отправляют друг другу большие сообщения и лишь затем принимают, могут заблокировать друг друга навсегда: каждый ждёт, пока другой начнёт приём. Встречный обмен записывают одной операцией Sendrecv либо неблокирующими Isend и Irecv, которые возвращают объект запроса, и завершают ожиданием Wait. Цена сообщения складывается из задержки, не зависящей от размера, и времени передачи данных, пропорционального размеру; демонстрация измеряет обе составляющие на стенде.
Демонстрация:
пинг-понг
01
Два процесса на узле, Open MPI 4.1.6
02
Сообщения от 8 Б до 16 МиБ туда и обратно
03
Send/Recv — буфер NumPy, send/recv — pickle
Что наблюдается
8 Б: 0,4 мкс буфером против 4,3 мкс через pickle; 1 МиБ: 36 мкс, 29 ГБ/с, против 179 мкс; 16 МиБ: 1131 мкс против 3909 мкс.
student@lab — запись со стенда
Демонстрация. Сценарий pingpong.py запускается в двух процессах на одном узле: процесс 0 отправляет массив процессу 1, тот возвращает его обратно, и время одного пути равно половине среднего времени оборота. Первая строка вывода — версия библиотеки: Open MPI 4.1.6 из пакетов Ubuntu. Для каждого размера сообщения от 8 Б до 16 МиБ сценарий печатает время в одну сторону и скорость для буферных Send и Recv и для send и recv через pickle. Сообщение в 8 Б передаётся за 0,4 мкс буферным методом и за 4,3 мкс через pickle: для малых сообщений время определяется задержкой, и сериализация увеличивает её на порядок. Сообщение в 1 МиБ буферным методом занимает 36,1 мкс, что соответствует 29 ГБ/с через общую память, а через pickle — 178,6 мкс, впятеро дольше. Для 16 МиБ скорость буферного обмена снижается до 15 ГБ/с — вероятно, сообщение уже не помещается в кеши процессора, — а pickle требует 3909 мкс против 1131. Сравним с арифметикой: одна операция над числом занимает доли наносекунды, а самое короткое сообщение — сотни наносекунд даже внутри одной машины; между узлами кластера задержка составляет микросекунды в специализированных сетях и десятки микросекунд в обычном Ethernet. Следовательно, массивы передают буферными методами, а параллельная программа тем эффективнее, чем реже она обменивается данными и чем крупнее её сообщения. Числа времени меняются от запуска к запуску и зависят от машины.
Коллективные
операции
# reduce_mc.py params = comm.bcast(params, root=0) mine = range(rank, params["chunks"], size) hits = sum(count_hits( np.random.default_rng([params["seed"], i]), params["per_chunk"]) for i in mine) total = comm.reduce(hits, op=MPI.SUM, root=0) info = comm.gather((len(mine), hits, elapsed), root=0)
Важно
Коллективную операцию вызывают все процессы коммуникатора в одном порядке, иначе программа зависает.
01
bcast
объект от одного процесса всем
02
scatter и gather
раздать части и собрать результаты
03
reduce и allreduce
сумма или максимум: на одном или на всех
04
Дерево обменов
около log₂ p шагов вместо p
Изложение. Операции, в которых участвуют все процессы коммуникатора, называются коллективными. Операция bcast рассылает объект от одного процесса всем остальным, scatter раздаёт части, gather собирает результаты на одном процессе, reduce объединяет значения операцией — суммой, максимумом — на одном процессе, а allreduce делает результат доступным всем; Barrier синхронизирует процессы. Реализации MPI выполняют коллективные операции деревом обменов за число шагов порядка log₂ p, а не за p шагов. Коллективную операцию необходимо вызвать во всех процессах коммуникатора, причём в одном и том же порядке: процесс, пропустивший вызов, оставляет остальных в ожидании, и программа зависает. Фрагмент сценария reduce_mc.py распределяет между процессами расчёт эффективности счётчика. Процесс 0 рассылает параметры операцией bcast. Работа разбита на 64 порции; порция i считается генератором с зерном [общее зерно, i] и достаётся процессу i mod size; число зарегистрированных квантов суммируется на процессе 0 операцией reduce, а число порций и время каждого процесса собираются операцией gather. Существенно, что зерно привязано к порции, а не к рангу процесса: если бы генератор зависел от ранга, результат менялся бы с числом процессов и не воспроизводился бы на другом числе узлов.
Демонстрация:
reduce
01
64 порции по 2·10⁶ квантов, зерно — номер порции
02
Один процесс: все 64 порции
03
Два процесса: по 32 порции, сумма — reduce
Что наблюдается
На 1 и 2 процессах зарегистрировано 1 128 023 из 128 000 000, 0,8813 % ± 0,0008 %; 0,49 с на одном процессе и по 0,26 с на двух.
student@lab — запись со стенда
Демонстрация. Сценарий reduce_mc.py запускается сначала в одном процессе, затем в двух. Расчёт состоит из 64 порций по 2·10⁶ квантов, всего 128 000 000 квантов. На одном процессе все 64 порции посчитаны за 0,49 с, зарегистрировано 1 128 023 кванта, эффективность 0,8813 % ± 0,0008 % при значении квадратуры 0,8808 %. На двух процессах каждый получил по 32 порции: процесс 0 зарегистрировал 564 092 кванта, процесс 1 — 563 931, каждый — за 0,26 с, а операция reduce дала ту же сумму 1 128 023 и ту же эффективность. Совпадение до последнего кванта является следствием привязки зерна к порции: при любом числе процессов каждая порция считается тем же генератором. Ускорение составляет 0,49/0,26 ≈ 1,9 на двух ядрах: обмен в этом расчёте сводится к трём коллективным операциям, и его доля мала. Отметим, что отклонение результата от квадратуры, 0,0005 %, меньше погрешности 0,0008 %, то есть согласуется со статистикой.
Разбиение
области
процесс 0
строки 1…26 сетки 51×51
теневая строка ← процесс 1
теневая строка ← процесс 0
обмен граничными строками: Sendrecv
up = rank - 1 if rank > 0 else MPI.PROC_NULL down = rank + 1 if rank < size - 1 else MPI.PROC_NULL def exchange(a): comm.Sendrecv(a[1, :], dest=up, recvbuf=a[-1, :], source=down) comm.Sendrecv(a[-2, :], dest=down, recvbuf=a[0, :], source=up)
Пояснение
Счёт на итерацию растёт как N²/p, обмен — как N: с ростом сетки доля обмена убывает.
Для физика
Итерации Якоби поточечны: параллельный результат совпадает с последовательным до бита.
Изложение. Связанная задача требует обмена на каждом шаге. Рассмотрим потенциал в квадратной камере, верхняя стенка которой находится под потенциалом 1000 В, а остальные заземлены. Внутри камеры потенциал удовлетворяет уравнению Лапласа, и на сетке его можно найти итерациями Якоби: новое значение в узле равно среднему значений в четырёх соседних узлах. Точное значение в центре известно: сложив четыре поворота задачи, получим камеру, все стенки которой находятся под потенциалом 1000 В, и потенциал в центре такой камеры равен 1000 В; следовательно, в исходной задаче он равен V0/4 = 250 В. Сетка делится между процессами полосами строк: при сетке 51×51 процесс 0 получает строки с 1-й по 26-ю, процесс 1 — с 27-й по 51-ю. Для обновления крайних строк полосы процессу необходимо знать строки соседа, поэтому у каждой полосы есть теневые строки — копии граничных строк соседей, — которые обновляются обменом перед каждой итерацией. Функция exchange выполняет его двумя операциями Sendrecv: первая отправляет первую собственную строку соседу сверху и принимает в нижнюю теневую строку первую строку соседа снизу, вторая выполняет встречный обмен; у крайних процессов вместо соседа указан MPI.PROC_NULL, и обмен с ним ничего не делает. Объём счёта на итерацию растёт как N²/p, а обмен — только как N, поэтому с ростом сетки доля обмена убывает, и на стенде для ускорения выше 1,8 оказалось достаточно, чтобы на каждый процесс приходилось около сотни строк. Итерации Якоби поточечны: значение в каждом узле вычисляется по тем же четырём числам, что и в последовательной программе, поэтому параллельный результат совпадает с последовательным до последнего бита. Это свойство позволяет проверять программу: ненулевое расхождение указывает на ошибку в обмене, а не на округление.
Демонстрация:
потенциал в камере
01
Сетка 51×51, 6000 итераций: до сходимости
02
Сетка 801×801, 400 итераций: замер времени
03
Ряд сеток от 101 до 1601: speedup.csv
Что наблюдается
51×51: 249,99 В при точных 250,00 В, расхождение 0, ускорение 0,96; 801×801: 1,87; ряд сеток — 1,56 при N = 101, 1,83–1,88 при 201–801 , 1,76 при 1601.
student@lab — запись со стенда
Демонстрация. Сценарий laplace.py решает задачу о камере на двух процессах и повторяет тот же расчёт на одном процессе, чтобы сравнить результаты и время; время — лучшее из трёх повторов. На сетке 51×51 после 6000 итераций потенциал в центре равен 249,99 В при точном значении 250,00 В, расхождение с расчётом на одном процессе — 0,0e+00 В, то есть результаты совпадают до бита. Однако ускорения нет: один процесс тратит 0,034 с, два — 0,035 с, ускорение 0,96. Каждая итерация на такой сетке содержит слишком мало работы, и обмен строками вместе с накладными расходами Python на итерацию обходится дороже выигрыша от деления счёта. На сетке 801×801 400 итераций недостаточно для сходимости — потенциал в центре ещё равен нулю, и сценарий об этом предупреждает, — но для замера времени это неважно: 0,457 с на одном процессе против 0,244 с на двух, ускорение 1,87, расхождение снова нулевое. Третья команда проходит ряд сеток от 101 до 1601 при постоянном объёме работы, около 4·10⁸ обновлений узлов, и записывает таблицу в файл speedup.csv. Ускорение составляет 1,56 при N = 101, от 1,83 до 1,88 при N от 201 до 801 и снижается до 1,76 при N = 1601; эффективность — от 0,78 до 0,94. На малой сетке велика доля обмена, а на самой большой скорость обоих процессов, по-видимому, ограничивает общая пропускная способность памяти. Числа времени относятся к двум ядрам виртуальной машины и меняются от запуска к запуску.
Ускорение
и закон Амдала
S = T₁ / T_p, E = S / p Амдал: S ≤ 1 / (f + (1 - f) / p) f = 5 % → S < 20 при любом p Густафсон: S = p - f·(p - 1)Карп—Флэтт : e = (1/S - 1/p) / (1 - 1/p)
Пояснение
Эффективность больше 1 возможна из-за кеша: часть задачи помещается в кеш, целая — нет. На стенде её нет: E ≤ 0,94.
01
Амдал
фиксированная задача: предел — последовательная доля
02
Густафсон
задача растёт с p: подробнее сетка, больше частиц
03
Карп—Флэтт
доля по измерению; её рост — накладные расходы обмена
Для физика
Тот же закон для части программы — в главе «Причины низкой скорости Python».
Изложение. Ускорение параллельной программы на p процессах определяется как S = T₁/T_p, а эффективность — как E = S/p. Если доля f времени работы программы не распараллеливается, закон Амдала ограничивает ускорение величиной 1/(f + (1 − f)/p): при f = 5 % ускорение не превысит 20 при любом числе процессов. Тот же закон для ускорения части программы приводился в главе «Причины низкой скорости Python». Закон Густафсона рассматривает другую постановку: задача растёт вместе с числом процессов так, что время её решения остаётся прежним, и масштабированное ускорение S = p − f(p − 1), где f — последовательная доля времени расчёта на p процессах, а не на одном, как у Амдала, растёт почти линейно. Для физика вторая постановка часто естественнее: больше узлов означает более подробную сетку или больше частиц, а не тот же расчёт быстрее. По измеренному ускорению последовательную долю оценивает метрика Карпа—Флэтта e = (1/S − 1/p)/(1 − 1/p). Оценим её по измерениям стенда: при N = 801 ускорение 1,88 даёт e ≈ 0,06, при N = 101 ускорение 1,56 — e ≈ 0,28. Рост этой доли при уменьшении сетки или увеличении числа процессов указывает на накладные расходы обмена, а не на исходно последовательную часть программы. Эффективность больше единицы, то есть сверхлинейное ускорение, возможна из-за кеша: часть задачи помещается в кеш процессора, а целая задача — нет. На стенде такого эффекта нет: наибольшая эффективность составляет 0,94.
Результат:
ускорение на двух ядрах
Что наблюдается
На стенде ускорение 1,56 при N = 101, 1,83–1,88 при N = 201–801 и 1,76 при N = 1601; по закону Амдала при 64 процессах доля 5 % ограничивает ускорение 15,4, доля 1 % — 39,3.
Демонстрация. Слева — измеренное ускорение laplace.py на двух процессах стенда в зависимости от размера сетки N, лучшее из трёх запусков, по файлу speedup.csv записи; штриховая линия — идеальное ускорение 2. Точки 1,56, 1,83, 1,85, 1,88 и 1,76 образуют пологую дугу: на малой сетке выигрыш от деления счёта почти полностью уходит на обмен, на средних сетках ускорение ближе всего к идеалу, на самой большой снова снижается. Справа — закон Амдала для последовательной доли 1, 5 и 10 % при числе процессов до 64; штриховая линия — идеальное ускорение S = p. При 64 процессах ускорение составляет 39,3, 15,4 и 8,8 соответственно: даже 1 % последовательной работы отнимает почти 40 % возможного ускорения. Таким образом, предел роста задают последовательная доля и обмен, а на малом числе процессов, как на стенде, решающим оказывается отношение счёта к обмену на итерацию.
Под Slurm:
srun или mpirun
#!/bin/bash #SBATCH --job-name=laplace #SBATCH --ntasks =2 #SBATCH --mem-per-cpu =300M #SBATCH --time=00:05:00 #SBATCH --output=laplace-%j.out # Один расчёт MPI двумя способами запуска внутри одного выделения echo "== srun" srun --mpi=pmix python3 laplace.py 801 400 echo "== mpirun" mpirun python3 laplace.py 801 400
01
srun --mpi=pmix
Slurm сам запускает процессы и сообщает ранги через PMIx
02
mpirun
узнаёт выделение у Slurm и запускает процессы его средствами
03
ssh в обход
процессы вне учёта и ограничений
Пояснение
--ntasks — процессы, --cpus-per-task — потоки каждого; на стенде MpiDefault=none.
Изложение. На кластере программа MPI запускается внутри выделения Slurm. Документация Slurm называет предпочтительным прямой запуск процессов командой srun: Slurm сам создаёт процессы и привязывает их к выделенным ядрам, а интерфейс PMIx обеспечивает обмен рангами и адресами соседей. Тип интерфейса задаётся ключом --mpi=pmix или параметром MpiDefault конфигурации; на стенде MpiDefault=none, поэтому ключ указан явно. Команда mpirun Open MPI внутри выделения тоже работает: она получает от Slurm список выделенных узлов и запускает процессы средствами Slurm, а не по ssh. Запуск по ssh в обход планировщика документация не рекомендует: такие процессы выпадают из учёта и ограничений. Скрипт laplace.sh запрашивает два процесса (--ntasks=2) по 300 МБ памяти на ядро и выполняет один и тот же расчёт двумя способами — через srun и через mpirun. Ключ --ntasks задаёт число процессов, каждый с одним ядром; программе с потоками внутри процесса дополнительно задают --cpus-per-task, а на кластере с несколькими узлами к запросу добавляют --nodes и при необходимости --ntasks-per-node.
Демонстрация:
MPI в Slurm
01
srun --mpi=list: интерфейсы запуска MPI
02
sbatch --wait slurm/laplace.sh
03
Вывод: тот же расчёт через srun и mpirun
Что наблюдается
Плагины none, pmix, pmi2, cray_shasta, версия pmix_v5; задача 92: srun — ускорение 1,93, mpirun — 1,87, расхождение в обоих случаях 0.
student@lab — запись со стенда
Демонстрация. Первая команда, srun --mpi=list, перечисляет доступные интерфейсы запуска MPI: none, pmix, pmi2 и cray_shasta, а также версию плагина PMIx — pmix_v5. Вторая ставит в очередь скрипт laplace.sh с ключом --wait, при котором sbatch возвращает управление только после завершения задачи; задача получила номер 92. Файл вывода содержит два блока, после строк «== srun» и «== mpirun». Оба расчёта на сетке 801×801 дают расхождение с расчётом на одном процессе 0,0e+00 В; при запуске через srun ускорение составляет 1,93 (0,459 с против 0,238 с), через mpirun — 1,87 (0,449 с против 0,241 с). Разница между способами запуска лежит в пределах разброса от запуска к запуску: в демонстрации без Slurm та же сетка дала 1,87. Существенно другое: srun запускает процессы программы сам, без промежуточных служб Open MPI, и они образуют шаг задачи, который Slurm учитывает отдельно и привязывает к выделенным ядрам, поэтому документация Slurm и называет такой запуск предпочтительным. Процессы, запущенные mpirun, тоже остаются в пределах выделения задачи.
Apptainer
Воспроизводимое окружение: источники расхождений, файл определения, образ SIF, запуск без root
05
Кластер
Slurm
Массивы задач
MPI
Apptainer
Ansible
Kubernetes
Изложение. Пятая часть посвящена воспроизводимому окружению. Рассматриваются источники расхождения результатов между машинами, уровни фиксации окружения, контейнеры Apptainer как способ переносить окружение на кластер без прав администратора, файл определения образа и запуск образа с привязкой каталогов. Две демонстрации показывают сборку образа и его запуск от обычного пользователя.
Источники
невоспроизводимости
01
Версии
Python, NumPy, BLAS, glibc, ОС узла
02
Переменные окружения
OMP_NUM_THREADS и подобные
03
Порядок суммирования
реализация BLAS и число потоков
04
Зёрна
из номера задачи, а не из времени
Уровень фиксации Что фиксирует
pip freeze, lock-файл, хеши пакеты Python
окружение conda и библиотеки не на Python
модули кластера сборки администраторов
образ контейнера всё, кроме ядра и драйверов
Для физика
Образ и файл определения хранятся рядом с результатом и публикуются вместе со статьёй.
Изложение. Результат расчёта зависит не только от кода и входных данных. На него влияют версии интерпретатора и библиотек; реализация BLAS, к которой обращается NumPy: разные реализации и разное число потоков суммируют в разном порядке, и результат может отличаться в последних знаках; системные библиотеки и операционная система узла; переменные окружения наподобие OMP_NUM_THREADS; зёрна генераторов. Расчёт, который на рабочей машине давал одни числа, на узле кластера через год может дать другие, и причину приходится искать долго. Фиксация окружения выполняется на нескольких уровнях. Простейший уровень представляет собой список зависимостей с точными версиями, а лучше — с контрольными суммами: pip freeze, файл блокировки менеджера пакетов, установка с ключом --require-hashes. Следующий уровень — окружение conda, которое включает и библиотеки, написанные не на Python. На кластере доступны модули, собранные администраторами. Наиболее полную фиксацию обеспечивает образ контейнера: в нём зафиксировано всё, кроме ядра операционной системы и драйверов устройств. Образ и файл, по которому он собран, хранят рядом с результатом и публикуют вместе со статьёй; к этой мысли подводит раздел о контейнерах в науке главы «Docker».
Apptainer
и Docker на кластере
Свойство Docker Apptainer
служба демон от имени root нет: процесс пользователя
доступ группа docker — права root обычный пользователь
пользователь внутри root по умолчанию тот же UID, что снаружи
каталоги только подключённые явно $HOME, текущий, /tmp — сразу
образ слои в хранилище службы один файл SIF (squashfs)
GPU и MPI --gpus --nv; MPI хоста и образа
Пояснение
Singularity: LBNL, 2015 год; с ноября 2021 года — Apptainer в Linux Foundation, а Sylabs развивает ответвление SingularityCE.
Важно
Членство в группе docker равносильно правам root на узле, поэтому на общих кластерах Docker не устанавливают.
Изложение. На общих кластерах Docker обычно не устанавливают. Его служба работает от имени root, а пользователь, которому разрешено к ней обращаться, фактически получает права root на узле: документация Docker прямо указывает, что группа docker даёт привилегии уровня root. Для вычислительных центров был создан Apptainer, до 2021 года называвшийся Singularity: проект начат в Национальной лаборатории имени Лоуренса в Беркли в 2015 году, в ноябре 2021 года сообщество перевело его в Linux Foundation под именем Apptainer, а компания Sylabs продолжает развивать ответвление SingularityCE. Apptainer отличается от Docker в нескольких отношениях, существенных для кластера. Службы нет: команда apptainer запускает процесс контейнера сама и от имени того же пользователя, с тем же идентификатором внутри контейнера, поэтому файлы, созданные расчётом, принадлежат пользователю, а не root. По умолчанию в контейнер подключаются домашний каталог, текущий каталог и /tmp, так что программа видит свои данные без дополнительных настроек. Образ формата SIF представляет собой один неизменяемый файл со сжатой файловой системой squashfs; это позволяет копировать образ на кластер как обычный файл, подписывать его и проверять подпись. Видеокарта становится доступна ключом --nv, подключающим в контейнер драйвер хоста, а программа MPI в контейнере запускается по гибридной схеме: процессы создаёт srun или mpirun хоста, а библиотека MPI в образе должна быть совместима с MPI и сетью кластера. В Ubuntu 24.04 создание пространств имён обычным пользователем ограничено политикой AppArmor; пакет Apptainer устанавливает профиль, разрешающий их своей программе запуска.
Файл
определения
Bootstrap: docker-daemon From: python:3.12-slim %files numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl /opt/wheels/ detector.py /opt/mc/ mc_detector.py /opt/mc/ %post python3 -m pip install --no-index --no-cache-dir /opt/wheels/numpy-*.whl rm -rf /opt/wheels %environment export PYTHONDONTWRITEBYTECODE=1 %runscript exec python3 /opt/mc/mc_detector.py "$@" %test python3 -c "import numpy; print('numpy', numpy.__version__)"
Пояснение
apptainer/mc.def без разделов %labels и %help; определение хранится в образе: apptainer inspect --deffile mc.sif.
Изложение. Образ собирается по файлу определения (definition file), который представляет собой текстовое описание источника, файлов и шагов сборки. Заголовок указывает источник базового образа: Bootstrap: docker — реестр образов, docker-daemon — образ в локальной службе Docker, localimage — существующий файл SIF; строка From называет сам образ. Разделы задают файлы, копируемые в образ (%files), команды, выполняемые при сборке внутри образа (%post), переменные окружения при запуске (%environment), программу, которую выполняет apptainer run (%runscript), проверку собранного образа (%test), а также метки (%labels) и справку (%help), опущенные на слайде. Образ mc.sif строится на базе python:3.12-slim из локальной службы Docker стенда: для сборки достаточно этой службы, и сеть не нужна. NumPy устанавливается из заранее скачанного колеса с точной версией 2.5.3 ключом --no-index, без обращения к каталогу пакетов, а программа расчёта копируется в образ; раздел %runscript делает образ исполняемым, так что apptainer run mc.sif 2000000 запускает расчёт с аргументом. Отметим две детали воспроизводимости. Архитектура указана в имени колеса — aarch64, — и образ, собранный для x86-64, на узлах ARM не запустится, и наоборот. Тег python:3.12-slim со временем начинает указывать на новую сборку, поэтому при сборке из реестра базовый образ указывают не тегом, а дайджестом. Файл определения сохраняется внутри SIF и выводится командой apptainer inspect --deffile, так что описание сборки неотделимо от образа.
Демонстрация:
сборка образа
01
Заголовок и разделы mc.def
02
apptainer build: последние строки журнала
03
Размер образа и apptainer test
04
inspect --deffile: определение в образе
Что наблюдается
Заголовок docker-daemon и семь разделов; при сборке %test печатает «numpy 2.5.3», затем «Build complete: mc.sif»; образ — 63 МиБ, определение хранится внутри.
root@lab — запись со стенда
Демонстрация. Сборка выполняется от root: базовый образ берётся из локальной службы Docker, а доступ к её сокету есть только у root и у группы docker. Первая команда выбирает из mc.def заголовок и имена разделов: Bootstrap: docker-daemon, From: python:3.12-slim и семь разделов — %labels, %help, %files, %post, %environment, %runscript и %test. Вторая собирает образ; из журнала сборки показаны четыре последние строки: Apptainer выполняет раздел %test, который печатает «numpy 2.5.3», затем упаковывает файловую систему в файл SIF и сообщает «Build complete: mc.sif». Сборка включает экспорт базового образа из Docker, установку NumPy из колеса и сжатие файловой системы squashfs. Третья команда показывает размер образа — 63 МиБ: интерпретатор Python, NumPy и программа расчёта в сжатом виде, — и повторяет проверку командой apptainer test, снова «numpy 2.5.3». Четвёртая выводит начало файла определения, сохранённого внутри образа: Bootstrap: docker-daemon и From: python:3.12-slim. Через год по этим строкам можно установить, из чего собран образ, даже если исходный файл определения утерян. После записи файл mc.sif передан пользователю student, от имени которого выполняется следующая демонстрация.
Запуск:
exec, run, привязка
#!/bin/bash #SBATCH --job-name=mc-sif #SBATCH --cpus-per-task =1 #SBATCH --mem=300M #SBATCH --time=00:02:00 #SBATCH --output=mc-sif-%j.out # Расчёт из образа: окружение — внутри mc.sif, данные — в привязанном каталоге srun apptainer run --bind "$PWD/data:/data" mc.sif 2000000 /data/mc.json
01
exec и run
любая команда или программа %runscript
02
-B хост:контейнер
каталог данных или рабочей области
03
--containall
без $HOME, /tmp и окружения хоста
Пояснение
Под Slurm образ запускается шагом задачи — srun apptainer, — и ограничения cgroups действуют на процесс контейнера, как на любой другой.
Изложение. Команда apptainer exec выполняет в контейнере произвольную команду, apptainer run — программу раздела %runscript, а apptainer shell открывает в контейнере командную оболочку. Ключ -B, или --bind, подключает каталог хоста по указанному пути внутри контейнера; так в контейнер передают входные данные и рабочую область кластера. Ключ --contain, напротив, подставляет вместо домашнего каталога и /tmp хоста пустые временные каталоги, а --containall вдобавок изолирует процессы и межпроцессное взаимодействие и не передаёт в контейнер переменные окружения хоста; это позволяет проверить, не зависит ли расчёт от случайных файлов в домашнем каталоге и от окружения, например от OMP_NUM_THREADS. Процесс в контейнере работает от имени пользователя, запустившего образ, поэтому и файлы, созданные расчётом в привязанном каталоге, принадлежат этому пользователю. Под Slurm образ запускается внутри задачи командой srun apptainer, как в скрипте mc_container.sh на слайде: окружение расчёта находится в mc.sif, данные — в привязанном каталоге data, а ограничения cgroups действуют на процесс контейнера так же, как на любой другой процесс задачи. Перенос образа имеет пределы: ядро и драйверы видеокарты берутся с хоста, архитектура процессора узлов должна совпадать с архитектурой образа, а библиотека MPI в образе — соответствовать реализации MPI кластера. Таким образом, образ фиксирует пользовательскую часть окружения, а совместимость с аппаратурой кластера остаётся заботой того, кто образ собирает.
Демонстрация:
запуск образа
01
apptainer run mc.sif: расчёт в образе
02
Тот же расчёт вне образа, /opt/l9venv
03
ОС образа и пользователь внутри
04
Привязка -B $PWD/data:/data и владелец файла
05
docker run от student и права сокета
Что наблюдается
В образе и вне его — 17 529 из 2 000 000; внутри — Debian 13 и пользователь student, mc.json принадлежит student; docker — permission denied.
student@lab — запись со стенда
Демонстрация. Демонстрация выполняется от пользователя student. Первая команда запускает расчёт эффективности счётчика из образа: программа сообщает версии Python 3.12.14 и NumPy 2.5.3 из образа и зерно 20261005; из 2 000 000 испущенных квантов зарегистрировано 17 529, эффективность 0,8764 % ± 0,0066 % при значении квадратуры 0,8808 %, отклонение −0,66 погрешности. Вторая команда запускает ту же программу интерпретатором стенда из окружения /opt/l9venv: Python здесь другой, 3.12.3, NumPy та же, 2.5.3, и результат совпадает до кванта — 17 529. Совпадение обеспечивают одинаковые сборки NumPy и одинаковое зерно, а различие версий Python на результат не влияет. Третья команда показывает операционную систему внутри образа — Debian GNU/Linux 13 (trixie), тогда как сам стенд работает под Ubuntu 24.04. Четвёртая печатает имя пользователя снаружи и внутри контейнера: оба раза student. Пятая подключает каталог data стенда по пути /data внутри контейнера и записывает туда результат в формате JSON, а шестая показывает, что файл mc.json размером 134 байта принадлежит пользователю student, а не root. Последние две команды показывают, почему на кластерах используют Apptainer: попытка student запустить контейнер Docker завершается отказом в доступе к сокету службы /var/run/docker.sock, а сам сокет принадлежит root и группе docker и доступен на чтение и запись только им. Включение пользователя в группу docker дало бы ему возможность запускать контейнеры от имени root, то есть фактически права root на узле.
Ansible
Управление конфигурацией: конфигурация как код, инвентарь, плейбук, идемпотентность, роли
06
Кластер
Slurm
Массивы задач
MPI
Apptainer
Ansible
Kubernetes
Изложение. Шестая часть посвящена управлению конфигурацией машин — узлов кластера, стендов, компьютеров сбора данных. Рассматриваются конфигурация как код и идемпотентность, четыре распространённые системы управления конфигурацией, инвентарь и разовые команды Ansible, плейбук с пробным прогоном и обработчиками и роли с шаблонами. Три демонстрации выполняются на стенде от root; плейбуки демонстраций пишут файлы во временный каталог, а не в системные пути.
Конфигурация
как код
01
Дрейф конфигурации
машины, настроенные вручную, расходятся
02
Код в git
ревью, история, откат
03
Пробный прогон
что изменится — до применения
04
Идемпотентность
повторный прогон ничего не меняет
Для физика
Стенды, машины сбора данных, кластер лаборатории и учебный класс пересоздаются за минуты и остаются одинаковыми.
репозиторий git
плейбук, роли, инвентарь
ревью и --check
что изменится
применение
ansible-playbook
Изложение. Машины, настроенные вручную, со временем расходятся. Один администратор обновил пакет на половине узлов, другой исправил конфигурационный файл прямо на сервере и забыл перенести исправление на остальные, третий переименовал машины, а сценарий выкатки о новых именах не знает. Это явление называют дрейфом конфигурации, и оно приводит к отказам, причину которых ищут долго: на одних узлах расчёт работает, на других — нет. Управление конфигурацией описывает желаемое состояние машин кодом — какие пакеты установлены, какие файлы и с каким содержимым лежат, какие службы запущены, — хранит этот код в git, проверяет его ревью и пробным прогоном и применяет одинаково ко всем машинам. Важнейшим свойством такого кода является идемпотентность: повторное применение к машине, которая уже находится в описанном состоянии, ничего не меняет. Поэтому код конфигурации можно применять сколько угодно раз, а повторный прогон без изменений служит проверкой того, что машина соответствует описанию. Отметим, что код не устраняет ошибки, а тиражирует их на все машины сразу; уменьшают их ревью, пробный прогон и постепенное применение — сначала к одной машине. Для лаборатории управление конфигурацией означает, что стенд, машину сбора данных или узел кластера можно пересоздать за минуты, а машины учебного класса действительно настроены одинаково.
Инвентарь
и модули
# Узлы стенда; файлы демонстраций пишутся в stand_root, а не в корень [cluster] lab ansible_connection=local [cluster:vars] ansible_python_interpreter="{{ ansible_playbook_python }}" stand_root=/tmp/l21rec/stand slurm_partition=main
01
Инвентарь
группы узлов и их переменные; подключение ssh или local
02
Модуль
ping, setup, command, file, template, apt, systemd_service, pip, user
03
Разовая команда
ansible группа -m модуль -a аргументы
Пояснение
На узле нужен Python 3.9–3 .14 (ansible-core 2.21); без него работают только модули raw и script.
Изложение. Ansible узнаёт об управляемых машинах из инвентаря — списка узлов, объединённых в группы, с переменными для групп и узлов. Инвентарь стенда содержит группу cluster из одного узла lab, которым Ansible управляет на той же машине: параметр ansible_connection=local заменяет подключение по SSH локальным выполнением. Переменные группы задаются в разделе [cluster:vars]: интерпретатор для модулей — тот же Python, которым запущен сам Ansible, то есть виртуальное окружение /opt/ansible; каталог stand_root, в который демонстрационные плейбуки пишут файлы вместо корня файловой системы; имя раздела Slurm. Действие над узлом выполняет модуль: ping проверяет связь и наличие Python, setup собирает факты об узле — число ядер, объём памяти, семейство ОС, — command выполняет команду, file и template управляют файлами, apt, systemd_service, pip и user — пакетами, службами, пакетами Python и пользователями. Разовая команда вызывает один модуль для группы узлов, например ansible cluster -m ping. Для большинства модулей на узле необходимо иметь Python — для ansible-core 2.21 версии от 3.9 до 3.14; без него работают только модули raw и script. На настоящем кластере в инвентаре перечисляются узлы с их адресами и пользователем для подключения, а секреты — ключи и пароли — в инвентарь не записывают: для них предназначены ключи SSH и зашифрованные файлы ansible-vault.
Демонстрация:
инвентарь
01
Инвентарь: группа cluster, узел lab
02
ansible cluster -m ping
03
Факт ansible_processor_vcpus модулем setup
04
ansible cluster -a nproc: модуль command
Что наблюдается
ping — SUCCESS и pong; факт ansible_processor_vcpus равен 2; nproc — CHANGED, rc=0, вывод 2: модуль command всегда сообщает об изменении.
root@lab — запись со стенда
Демонстрация. Демонстрации Ansible выполняются от root в каталоге /tmp/l21rec/ansible, где файл ansible.cfg указывает на инвентарь inventory.ini и отключает лишний вывод. Первая команда показывает инвентарь: группа cluster с узлом lab и подключением local, переменные группы — интерпретатор {{ ansible_playbook_python }}, то есть Python самого Ansible, каталог stand_root=/tmp/l21rec/stand и раздел main. Команда ansible cluster -m ping отвечает lab | SUCCESS с полями "changed": false и "ping": "pong": модуль выполнен на узле, и Python там есть. Фильтр позволяет вывести модулем setup один факт — "ansible_processor_vcpus": 2, число ядер узла; факты Ansible по умолчанию собирает в начале каждого сценария и использует в условиях и шаблонах. Последняя команда без ключа -m вызывает модуль command и печатает вывод nproc — 2, — причём со статусом CHANGED и кодом возврата 0: модуль command не знает, изменила ли команда что-нибудь, и поэтому всегда сообщает об изменении. Это первый пример неидемпотентного модуля, к которому вернёмся на следующем слайде.
Плейбук
и идемпотентность
tasks: - name: Каталог конфигурации ansible.builtin.file: path: "{{ stand_root }}/etc/slurm" state: directory - name: Файлы конфигурации из шаблонов ansible.builtin.template: src: "{{ item.src }}" dest: "{{ stand_root }}/etc/slurm/{{ item.dest }}" loop: - {src: slurm.j2, dest: slurm.conf} - {src: cgroup.j2, dest: cgroup.conf} loop_control: label: "{{ item.dest }}" notify: Перезапуск Slurm handlers: - name: Перезапуск Slurm ansible.builtin.debug: msg: перезапуск slurmctld и slurmd
01
Сценарий (play)
группа узлов и список задач
02
Задача
модуль с описанием состояния: ok или changed
03
--check и --diff
пробный прогон и изменения файлов
04
Обработчик
один раз в конце и только при изменениях
Важно
command и shell неидемпотентны: их ограничивают параметрами creates и removes или условием changed_when.
Изложение. Последовательность действий записывается плейбуком, который представляет собой файл YAML. Плейбук состоит из одного или нескольких сценариев (plays), каждый из которых относится к группе узлов и содержит задачи; задача вызывает модуль с параметрами, описывающими желаемое состояние. На слайде — задачи и обработчик плейбука stand.yml, который создаёт каталог конфигурации Slurm и два файла из шаблонов Jinja2; заголовок сценария с переменными и строки с правами доступа mode опущены. Модуль file с параметром state: directory проверяет, существует ли каталог, и создаёт его только при необходимости; модуль template подставляет переменные в шаблон и заменяет файл, только если результат отличается от существующего. Поэтому каждая задача сообщает, изменила ли она что-нибудь: changed или ok. Обработчик (handler) выполняется один раз в конце сценария и только если хотя бы одна задача, уведомившая его ключом notify, сообщила об изменении; таким образом, служба перезапускается лишь при изменении её конфигурации. В учебном плейбуке обработчик только печатает сообщение, а в плейбуке настоящего стенда он перезапускает службы модулем systemd_service. Перед применением необходимо выполнить пробный прогон: ключ --check выполняет плейбук, не изменяя узел, и сообщает, что было бы изменено, а --diff показывает, как изменятся файлы. Модули command и shell выполняют произвольные команды и поэтому неидемпотентны: при каждом прогоне они сообщают об изменении и в пробном прогоне не выполняются. Их делают идемпотентными параметрами creates и removes — команда выполняется, только если указанного файла нет или, наоборот, он есть, — либо условием changed_when. В демонстрациях вывод ansible-playbook пропускается через tr -s ' *\n', которая лишь сжимает повторы пробелов, звёздочек и переводов строк, чтобы вывод уместился в окно 80×22.
Демонстрация:
плейбук
01
Пробный прогон --check: что изменится
02
Применение плейбука stand.yml
03
Повторное применение: последние три строки
Что наблюдается
--check и первое применение — changed для каталога и двух файлов, обработчик, ok=3 changed=2; повторное применение — ok=2 changed=0, обработчик не вызван.
root@lab — запись со стенда
Демонстрация. Перед записью каталог /tmp/l21rec/stand удалён, поэтому узел находится не в описанном состоянии. Первая команда выполняет пробный прогон stand.yml с ключом --check: задача «Каталог конфигурации» и оба файла из шаблонов, slurm.conf и cgroup.conf, помечены changed, вызван обработчик «Перезапуск Slurm», который печатает сообщение, а итоговая строка PLAY RECAP сообщает ok=3 changed=2: без ошибок выполнены обе задачи и обработчик, а изменили бы узел обе задачи — задача шаблонов учитывается один раз, хотя касается двух файлов. Файлы при этом не созданы. Вторая команда применяет плейбук: вывод совпадает с пробным прогоном, но теперь каталог и файлы действительно созданы в /tmp/l21rec/stand/etc/slurm. Третья команда применяет тот же плейбук повторно, и последние три строки вывода показывают ok: [lab] для файла cgroup.conf и итог ok=2 changed=0: ничего не изменилось, обработчик не вызывался, и поэтому выполненных задач в итоге две, а не три. Повторный прогон без изменений является проверкой идемпотентности плейбука и одновременно проверкой того, что узел находится в описанном состоянии.
Роли
и шаблоны
roles/slurm_node/ defaults/main.yml # значения по умолчанию handlers/main.yml # перезапуск служб tasks/main.yml # каталог, шаблоны, службы templates/cgroup.j2 templates/slurm.j2 # шаблон slurm.conf # site.yml - name: Узел Slurm из роли hosts: cluster gather_facts: false roles: - slurm_node
01
Роль
tasks, handlers, templates, defaults, vars, files, meta
02
Шаблон Jinja2
slurm.j2 получает переменные роли
03
Старшинство
22 уровня: defaults — второй снизу, -e — высший
04
lab-stand.yml
пакеты, munge, роль, mpi4py, пользователь
Пояснение
Плейбук стенда курса собран из той же роли: пробный прогон на готовом стенде не меняет ничего.
Изложение. Плейбук, выросший до десятков задач, разбивают на роли — каталоги стандартной структуры: tasks для задач, handlers для обработчиков, templates для шаблонов Jinja2, files для копируемых файлов, defaults для значений переменных по умолчанию, vars для переменных роли и meta для зависимостей. Роль slurm_node стенда состоит из пяти файлов: значения по умолчанию, обработчики, задачи и два шаблона. Плейбук site.yml подключает её одной строкой roles, а переменные позволяют применять ту же роль в разных плейбуках с разными значениями. Шаблон slurm.j2 формирует slurm.conf из переменных, например строку описания узла NodeName с подстановками {{ slurm_host }}, {{ slurm_cpus }} и {{ slurm_mem_mb }}. Переменные Ansible задаются во многих местах, и порядок их старшинства в документации насчитывает 22 уровня: значения из defaults роли — второй уровень снизу, переменные инвентаря, сценария и роли — выше, а переменные, переданные в командной строке ключом -e, имеют высший приоритет. Поэтому в defaults помещают разумные значения, которые пользователь роли переопределяет. Из той же роли собран плейбук lab-stand.yml, описывающий учебный стенд целиком: установка пакетов Slurm, munge, Open MPI и Apptainer из локального каталога файлов .deb, поскольку сети на стенде нет, проверка ключа munge, роль slurm_node с перезапуском настоящих служб, установка mpi4py в виртуальное окружение из локальных колёс и создание учебного пользователя student. Установка пакетов записана модулем shell с параметром creates: если программа slurmctld уже установлена, задача не выполняется.
Демонстрация:
роль
02
site.yml -e slurm_mem_mb=2800 -CD
03
lab-stand.yml --check: стенд целиком
Что наблюдается
Роль — пять файлов; с -e slurm_mem_mb=2800 дифф одной строки RealMemory=3000 → 2800, ok=3 changed=1 skipped=2; стенд целиком — ok=8 changed=0.
root@lab — запись со стенда
Демонстрация. Первая команда перечисляет файлы роли slurm_node: defaults/main.yml, handlers/main.yml, tasks/main.yml и шаблоны cgroup.j2 и slurm.j2. Вторая выполняет плейбук site.yml, подключающий роль, с переменной slurm_mem_mb=2800, переданной ключом -e, в режиме пробного прогона с показом различий: -C равносильно --check, -D — --diff. Каталог уже существует — ok. Для slurm.conf Ansible выводит дифф: заголовок называет текущий файл и временный файл, полученный из шаблона, а строка описания узла с RealMemory=3000 заменилась бы строкой с RealMemory=2800; соседние строки ReturnToService=2 и PartitionName=main показаны как контекст. Файл slurm.conf помечен changed, cgroup.conf — ok, вызывается обработчик, а итог — ok=3 changed=1 skipped=2: пропущены задача запуска служб и обработчик настоящего перезапуска, которые выполняются только при manage_services=true, то есть на настоящем стенде. Переменная из командной строки переопределила значение 3000 из defaults роли без правки самой роли. Последняя команда выполняет пробный прогон плейбука lab-stand.yml, описывающего весь стенд, и показывает итог: ok=8 changed=0. Стенд, настроенный этим плейбуком, находится в описанном состоянии: пакеты установлены, конфигурация совпадает с шаблонами, пользователь существует, и ни одна из восьми задач ничего не изменила бы.
Kubernetes и Terraform
Обзор: службы в контейнерах, облачные ресурсы кодом, инфраструктура как код
07
Кластер
Slurm
Массивы задач
MPI
Apptainer
Ansible
Kubernetes
Изложение. Седьмая часть кратко рассматривает два инструмента, с которыми физик встречается реже, чем с планировщиком, но которые определяют современную эксплуатацию программ: Kubernetes, управляющий контейнерами долгоживущих служб, и Terraform с открытым ответвлением OpenTofu, описывающие кодом облачные ресурсы. Демонстраций в этой части нет: оба инструмента требуют кластера или облака, а для физических расчётов важнее понимать, где их применение оправдано.
Kubernetes
на одном слайде
управляющий уровень (control plane)
kube-apiserver
все запросы
kube-scheduler
узел для пода
controller-manager
желаемое → фактическое
рабочий узел
kubelet: поды, пробыkube-proxy : правила Service под: контейнеры
рабочий узел
kubelet: поды, пробыkube-proxy : правила Service под: контейнеры
01
Под
один или несколько контейнеров; sidecar — стабильны с 1.33
02
Deployment
число экземпляров и выкатка новой версии
03
Service
постоянное имя и адрес для подов
04
Job и CronJob
пакетные задачи до завершения
Пояснение
Желаемое состояние описывается декларативно, а контроллеры постоянно приводят к нему фактическое.
Изложение. Kubernetes представляет собой систему управления контейнерами на кластере, ориентированную прежде всего на долгоживущие службы. Минимальной единицей является под (pod): один или несколько контейнеров с общей сетью и томами; вспомогательные контейнеры пода (sidecar) получили собственную поддержку, стабильную с версии 1.33 (апрель 2025 года). Объект Deployment поддерживает заданное число одинаковых подов через ReplicaSet и выкатывает новые версии, Service даёт подам постоянное имя и адрес, Job и CronJob выполняют пакетные задачи до завершения — однократно или по расписанию. Управляющий уровень кластера (control plane) состоит из сервера API kube-apiserver, через который проходят все запросы, хранилища состояния etcd, планировщика kube-scheduler, выбирающего узел для пода, и менеджера контроллеров kube-controller-manager. На каждом рабочем узле служба kubelet запускает контейнеры подов и перезапускает контейнер при отказе пробы живости, а kube-proxy настраивает в ядре правила для адресов Service. Пользователь описывает желаемое состояние декларативно, например «три экземпляра службы определённой версии», а контроллеры постоянно сравнивают его с фактическим и устраняют расхождение. Собственные типы объектов вместе с собственными контроллерами — операторами — позволяют управлять через Kubernetes практически любыми ресурсами.
Kubernetes
или Slurm
Задача Kubernetes Slurm
долгоживущие службы основное назначение не предназначен
пакетные задачи Job, CronJob; очереди — расширения основное назначение
связанный расчёт MPI через расширения srun, PMIx, быстрая сеть
ядра, память, GPU requests и limits пода точное распределение по задачам
учёт и справедливая доля квоты, расширения fair-share , sacct
Для физика
Kubernetes уместен для JupyterHub, веб-доступа к данным установки, архива и мониторинга, конвейеров обработки.
Важно
Для кластера лаборатории из нескольких узлов он избыточен, а на суперкомпьютере коллективного пользования обычно недоступен.
Изложение. Kubernetes и Slurm распределяют ресурсы кластера, но для разных нагрузок. Kubernetes рассчитан на долгоживущие службы, которые должны работать постоянно, масштабироваться и восстанавливаться при отказах. Для физической лаборатории он уместен там, где работают службы: JupyterHub для сотрудников, веб-доступ к данным установки, архив и мониторинг, конвейеры обработки данных. Пакетные задачи в Kubernetes тоже выполняются объектами Job, а для очередей с приоритетами и справедливой долей существуют расширения, например Kueue и Volcano. Slurm рассчитан на пакетные расчёты: он точно распределяет ядра, память и видеокарты между задачами, запускает плотно связанные программы MPI через PMIx на узлах с быстрой сетью, ведёт учёт ресурсов по пользователям и проектам и обеспечивает справедливую долю. Kubernetes запрашивает ресурсы пода полями requests и limits, но одновременный старт сотен процессов MPI и их связь по быстрой сети сам не обеспечивает. Предел применимости для физика таков: для кластера лаборатории из нескольких узлов Kubernetes избыточен — его управляющий уровень требует отдельных машин и сопровождения, — а на суперкомпьютере коллективного пользования он, как правило, недоступен, и расчёты ставятся в очередь Slurm или другого пакетного планировщика.
Terraform
и OpenTofu
terraform init # провайдеры: программы на Go terraform plan # описание против состояния terraform apply # создать и изменить ресурсы terraform destroy # удалить ресурсы terraform.tfstate # связь кода с ресурсами, # общее хранилище с блокировкой
Для физика
Облачные машины на время расчёта или учебного класса; на собственном оборудовании лаборатории Terraform не нужен.
01
HCL
описание облачных машин, дисков и сетей
02
Состояние
tfstate: что создано этим кодом
03
Модули
переиспользуемые блоки описаний
04
Лицензии
Terraform — BSL 1.1, OpenTofu — MPL 2.0
Изложение. Если вычислительные ресурсы арендуются в облаке, машины, диски и сети тоже описывают кодом. Terraform описывает облачные ресурсы на языке HCL. Команда terraform init загружает провайдеры; каждый провайдер является отдельной программой на Go для конкретного облака, с которой ядро Terraform общается по gRPC; terraform plan сравнивает описание с фактическим состоянием и показывает, что будет создано, изменено и удалено; terraform apply выполняет изменения, а terraform destroy удаляет созданные ресурсы. Связь между кодом и созданными ресурсами обеспечивает файл состояния terraform.tfstate: именно по нему plan вычисляет разницу, а при работе нескольких человек его держат в общем хранилище с блокировкой, чтобы два применения не выполнялись одновременно. Повторяющиеся описания оформляют модулями, которые служат для повторного использования и сокрытия подробностей. В августе 2023 года компания HashiCorp перевела Terraform на лицензию Business Source License 1.1, после чего сообщество под эгидой Linux Foundation создало открытое ответвление OpenTofu под лицензией MPL 2.0, общедоступное с января 2024 года; язык и протокол провайдеров у них общие, а команда OpenTofu называется tofu. С февраля 2025 года HashiCorp принадлежит IBM. Для физика Terraform полезен, когда под расчёт или учебный класс на время поднимаются облачные машины: для создания машин по описанию достаточно одной команды, а другая команда удаляет их, не оставляя оплачиваемых ресурсов. На собственном оборудовании лаборатории он не нужен.
Инфраструктура как код:
итог
01
Описание в git
ревью, история, откат
02
Пробный прогон
terraform plan, ansible-playbook --check
03
Изменяемая
Ansible приводит работающие машины к описанию
04
Неизменяемая
новый образ вместо правки: SIF, контейнер, образ машины
Важно
Риски: смена лицензий и владельцев (Terraform, Puppet), прекращение доступа к реестрам пакетов и образов.
Для физика
Копии пакетов, колёс и образов расчёта хранятся у себя: стенд курса установлен без сети, из файлов, проверенных по контрольным суммам.
Изложение. Ansible, Terraform и файлы определения образов реализуют один подход — инфраструктуру как код: описание хранится в git, изменения проходят ревью и пробный прогон — terraform plan, ansible-playbook --check, — применяются автоматически и повторяемо. Различают изменяемую инфраструктуру, в которой работающие машины приводятся к новому состоянию, как это делает Ansible, и неизменяемую, в которой вместо изменения создаётся новый экземпляр из нового образа: образа контейнера, образа виртуальной машины, файла SIF. Неизменяемость упрощает воспроизведение и откат, поскольку прежний образ остаётся нетронутым, но требует пересборки образа при каждом изменении. У подхода есть цена. Инструменты требуют освоения, и описание, в котором разбирается один человек, немногим лучше ручной настройки, поэтому в группе заранее договариваются о наборе инструментов и правилах их использования. Инструменты меняют лицензии и владельцев: смена лицензии Terraform в 2023 году и объявленный в 2024 году перевод новых сборок Puppet под лицензионное соглашение привели к появлению ответвлений OpenTofu и OpenVox. Доступ к внешним реестрам пакетов и образов может прекратиться. Поэтому копии пакетов, колёс и образов, необходимых для расчёта, хранят у себя: стенд курса установлен целиком из заранее скачанных и проверенных по контрольным суммам файлов, без доступа к сети. Таким образом, воспроизводимость расчёта обеспечивается не только кодом и зерном, но и хранимым у себя окружением.
Итоги
лекции
01
Ресурсы запрашивают по измерению: точный лимит сокращает ожидание.
02
Независимые расчёты — массивом задач, зерно — от номера задачи.
03
Связанные — MPI: ускорение ограничено обменом и последовательной долей.
04
Окружение — неизменяемым образом, который запускается без root.
05
Настройка машин — кодом: пробный прогон и повтор без изменений.
06
Копии пакетов, колёс и образов — у себя.
Изложение. Подведём итоги. Ресурсы кластера запрашивают у планировщика, и запрос выбирают по измерению: точный лимит времени позволяет задаче попасть в промежутки очереди и сокращает ожидание, а запас по памяти защищает от завершения по нехватке памяти. Независимые расчёты ставятся массивом задач, а зерно генератора каждой задачи выводится из её номера, что делает задачи независимыми и воспроизводимыми. Связанный расчёт делится между процессами MPI так, чтобы обмена было мало относительно счёта; ускорение измеряется и сравнивается с законом Амдала, а предел ему задают обмен и последовательная доля. Окружение расчёта фиксируется образом Apptainer, который запускается на узлах от имени пользователя, без прав администратора, и хранится вместе с результатом. Машины, на которых всё это работает, описываются кодом с пробным прогоном и повтором без изменений, а копии пакетов, колёс и образов хранятся у себя. Каждый из этих шагов обеспечивает одно свойство — возможность получить тот же результат снова, на другой машине и через годы.
Упражнения
и задание
01
Массив со своим параметром
скан градиента или энергии, сбор зависимостью afterok
02
Обмен в laplace.py
Isend и Irecv или разбиение блоками, замер ускорения
03
Свой образ
расчёт в образе Apptainer, запуск через srun
04
Роль Ansible
пакет и конфигурация с обработчиком перезапуска
Пояснение
Задание: плейбук, поднимающий на чистой машине одноузловой Slurm и запускающий проверочную задачу; повторный прогон — без изменений.
Для физика
Массив для своей задачи — скан параметра установки или серия Монте-Карло детектора.
Изложение. Упражнения служат самопроверке. Первое — поставить массив задач со своим параметром, например скан градиента линз или энергии пучка, и собрать результаты задачей с зависимостью afterok, проверив отсутствие пропусков. Второе — заменить в laplace.py встречный обмен неблокирующими операциями Isend и Irecv или разбить область не полосами, а блоками, и измерить ускорение на нескольких размерах сетки. Третье — собрать образ Apptainer со своим расчётом, записать в результат зерно и версии библиотек и запустить образ внутри задачи Slurm командой srun. Четвёртое — написать роль Ansible, устанавливающую пакет и конфигурационный файл из шаблона с обработчиком перезапуска службы, и убедиться, что повторный прогон ничего не меняет. Задание объединяет части лекции: плейбук Ansible, поднимающий на чистой машине одноузловой Slurm и запускающий проверочную задачу, повторный прогон которого не сообщает ни об одном изменении.
Источники
1
Книга курса: главы «Кластеры и воспроизводимое окружение для расчётов», «CUDA и вычисления на GPU», «Docker», «Внутреннее устройство Linux», «Многопоточность и GIL»
phys-dev.github.io/soft-dev-book
2
Документация Slurm 23.11: slurm.conf, sbatch, srun, sacct, массивы задач, MPI Users Guide, cgroup.conf; Yoo A. B., Jette M. A., Grondona M. SLURM // JSSPP 2003
slurm.schedmd.com
3
MPI 5.0 (MPI Forum, 2025); Dalcin L., Fang Y.-L. L. mpi4py: Status Update After 12 Years of Development // Computing in Science & Engineering, 2021
mpi4py.readthedocs.io
4
Документация Apptainer 1.5; Kurtzer G. M., Sochat V., Bauer M. W. Singularity: Scientific containers for mobility of compute // PLoS ONE, 2017
apptainer.org/docs
5
Документация Ansible, Kubernetes, OpenTofu; Morris K. Infrastructure as Code. 3rd ed. – O'Reilly, 2025
docs.ansible.com
6
Amdahl G. M. (1967); Gustafson J. L. (1988); Karp A. H., Flatt H. P. (1990); Mu'alem A. W., Feitelson D. G. // IEEE TPDS, 2001
Изложение. Лекции соответствует глава книги «Кластеры и воспроизводимое окружение для расчётов» части «Ускорение расчётов», продолжающая главу «CUDA и вычисления на GPU»; контейнеры подробно рассмотрены в главе «Docker», механизм cgroups — в главе «Внутреннее устройство Linux», процессы на одной машине — в главе «Многопоточность и GIL». Справочником по Slurm служит его документация: описания slurm.conf, sbatch, srun, sacct, массивов задач, руководство по MPI и описание cgroup.conf, а исходная архитектура системы изложена в статье Ю, Джетта и Грондоны 2003 года. Стандарт MPI 5.0 опубликован MPI Forum, привязка mpi4py описана в работе Далсина и Фанга. Apptainer описан в его документации и в статье Курцера и соавторов о Singularity. Документация Ansible содержит описание плейбуков, ролей и порядка старшинства переменных, книга К. Морриса — подход инфраструктуры как кода. Законы Амдала и Густафсона, метрика Карпа—Флэтта и исследование заполнения промежутков Мюалема и Фейтельсона приведены в исходных статьях.
Спасибо за внимание
Вопросы
phys-dev.github.io/soft-dev-book
Изложение. Лекция завершена. Вопросы по материалу удобно задавать, опираясь на главу «Кластеры и воспроизводимое окружение для расчётов» и на демонстрации: модели очереди, скана и счётчика воспроизводятся на любой машине с Python и NumPy, расчёты MPI — на любой машине с MPI и mpi4py, а пакетные скрипты Slurm, файл определения образа и плейбуки Ansible — на одноузловом кластере, подобном стенду лекции.