8(800) 222 32 56
Панель управления
AI Инфраструктура LLM

I/O-изоляция LLM-узла: cgroup v2 и systemd против штормов NVMe

I/O-изоляция LLM-узла: cgroup v2 и systemd против штормов NVMe
Подберите идеальное решение для ваших задач:
в России, США и Нидерландах обеспечат максимальную скорость. Воспользуйтесь всеми преимуществами надежного оборудования. Базовая помощь и техническое обслуживание входят в пакет услуг.
Model sync начинается по расписанию, и TTFT у работающего LLM внезапно растёт, хотя GPU свободна. Причина часто прячется в общей очереди NVMe: online inference и фоновая загрузка весов конкурируют на block layer. Разберём, как отделить эти потоки через cgroup v2 и systemd slices, проверить результат двумя gate и откатить лимит без простоя сервиса.

Готовы перейти на современную серверную инфраструктуру?

В King Servers мы предлагаем серверы как на AMD EPYC, так и на Intel Xeon, с гибкими конфигурациями под любые задачи — от виртуализации и веб-хостинга до S3-хранилищ и кластеров хранения данных.

  • S3-совместимое хранилище для резервных копий
  • Панель управления, API, масштабируемость
  • Поддержку 24/7 и помощь в выборе конфигурации

Создайте аккаунт

Быстрая регистрация для доступа к инфраструктуре

Как выглядит I/O-инцидент на LLM-узле

Утром online inference держит привычный SLO, а во время синхронизации новой модели time to first token внезапно растёт. GPU при этом не перегрет, VRAM свободна, сеть не забита. Знакомо? Часто очередь уже образовалась ниже приложения: большой последовательный read весов и мелкие обращения работающего сервиса делят один NVMe, а Linux честно обслуживает обоих без знания о бизнес-приоритете.

Эта статья решает узкую задачу: отделяет I/O онлайн-сервиса от загрузок моделей, checkpoint и batch-job на одном выделенном сервере с помощью cgroup v2 и systemd slices. Это не продолжение настройки memory pressure. В материале про cgroup v2, PSI и systemd-oomd для LLM граница проходит по RAM и OOM; здесь мы работаем с block I/O, io.max, io.stat и io.pressure.

Цель не в том, чтобы выжать из NVMe абстрактный максимум. Нужен проверяемый контракт: фоновая работа может идти медленнее, зато запросы пользователей не получают хвост latency из-за чужого pull. Мы сначала измерим baseline, потом введём ограничение только для batch-группы, докажем его на infrastructure gate и отдельно проверим приложение. Любое число в примерах — стартовая лабораторная величина, а не готовая рекомендация для вашего диска.

Загрузка весов LLM конкурирует с online inference за очередь NVMe

Проверяем cgroup v2, device и реальную принадлежность процесса

Первый риск — ограничить не тот слой. Каталог моделей может лежать на LVM, mdraid, dm-crypt или сетевой файловой системе, а имя /dev/nvme0n1 в инструкции ничего не доказывает. Документация ядра cgroup v2 задаёт io.max по паре major:minor block device; значит, этот идентификатор нужно получить на конкретном узле.

Сначала подтвердите unified hierarchy и наличие контроллера io. Затем найдите источник mount point модели, разверните device-mapper до физических устройств и проверьте ControlGroup каждой службы. Если downloader запущен из интерактивной shell, Kubernetes init container или cron вне ожидаемой slice, красивый unit-файл ничего не изменит.

bashcheck-io-controller.sh
stat -fc %T /sys/fs/cgroup
cat /sys/fs/cgroup/cgroup.controllers
findmnt -no SOURCE,TARGET,FSTYPE /var/lib/models
lsblk -o NAME,MAJ:MIN,TYPE,FSTYPE,MOUNTPOINTS
systemctl show vllm.service model-sync.service -p Slice -p ControlGroup

Infrastructure gate здесь простой: во время пробной загрузки должны расти rbytes или rios именно в cgroup фоновой службы. Отрицательная проверка не менее важна: счётчики inference-группы не должны внезапно отражать весь bulk read. Если отражают, остановитесь и исправьте placement процессов до настройки лимитов.

С чего начать изоляцию

Архитектура: sibling slices вместо одного общего лимита

Удобная схема состоит из двух соседних ветвей: llm-inference.slice и llm-batch.slice. В первой живёт vLLM или другой runtime, во второй — model sync, конвертация, checkpoint upload и вспомогательные job. Ограничение ставится на batch, а inference остаётся без искусственного потолка. Такой дизайн локализует изменение и делает rollback одной операцией.

Почему не поставить высокий IOWeight inference и забыть? Вес — относительный механизм: он проявляется при конкуренции sibling-групп и зависит от планировщика и модели устройства. io.max задаёт понятный верхний предел BPS/IOPS для шумного соседа. В проде часто начинают с жёсткого ceiling для bulk workload, а веса тестируют позже. Ядро допускает временные bursts, поэтому короткий пик всё равно возможен.

Не смешивайте эту задачу с доставкой модели. OCI-артефакты и node-local cache сокращают объём повторных скачиваний; отдельная статья про кэш весов LLM на NVMe разбирает именно этот слой. I/O isolation отвечает на другой вопрос: что произойдёт, когда загрузка всё-таки началась рядом с работающим inference.

Схема systemd slices и cgroup v2 io.max для разделения inference и фоновой загрузки весов

Снимаем baseline без выдуманных бенчмарков

Не начинайте с числа вроде «500 MB/s достаточно всем». Один NVMe может устойчиво держать последовательное чтение и резко проседать на смешанной очереди; другой упирается в thermal throttling, RAID или шифрование. Baseline должен объединять три слоя: характеристики block I/O, pressure внутри cgroup и пользовательскую latency приложения.

Для лабораторного профиля подходит fio, но только на отдельном test file и с согласованным размером. Актуальная документация fio 3.42 позволяет фиксировать rate, IOPS и latency percentiles. Не запускайте destructive job на device с рабочими весами. Сначала воспроизведите профиль downloader: крупный sequential read, затем добавьте небольшой random-read поток, похожий на обращения runtime к файлам модели.

inillm-io-baseline.fio
[global]
directory=/var/lib/models/io-probe
size=8G
direct=1
ioengine=io_uring
runtime=120
time_based=1
group_reporting=1
percentile_list=95:99:99.9

[bulk-model-read]
rw=read
bs=1M
iodepth=16

[inference-side-read]
rw=randread
bs=128k
iodepth=4
rate_iops=200

Значения в job-файле — форма эксперимента, а не performance claim. Замените size, queue depth и rate профилем своего приложения. Параллельно соберите TTFT и queue time на фиксированном наборе запросов. В текущей документации vLLM per-request metrics помечены как доступная функция, но включение может давать заметный CPU overhead при высокой конкуренции, поэтому его тоже нужно проверить на своей версии и нагрузке.

Какой регулятор выбрать

Жёсткий потолок. Подходит для предсказуемого ограничения batch-download по BPS или IOPS на конкретном block device.
Относительная доля. Полезна между sibling-группами, но эффект зависит от scheduler и текущей конкуренции.
Сигнал, не лимит. Показывает время stall и помогает связать I/O contention с деградацией приложения.
Стенд для проверки NVMe latency под одновременной нагрузкой inference и bulk read

Настраиваем лимит через systemd, а не ручной echo

Ручная запись в /sys/fs/cgroup/.../io.max годится для диагностики, но не для устойчивой конфигурации. systemd создаёт иерархию, переносит процессы и восстанавливает свойства после рестарта. Сначала закрепите version lock: зафиксируйте kernel, systemd, filesystem, I/O scheduler и unit revision в change ticket. При обновлении любого из этих слоёв gates повторяются.

Ниже пример drop-in для фоновой службы. Пределы 600M read и 150M write намеренно показаны как лабораторная отправная точка. На вашем сервере они должны следовать из baseline и оставлять запас под online workload.

inimodel-sync.service.d/io-control.conf
[Service]
Slice=llm-batch.slice
IOAccounting=yes
IOReadBandwidthMax=/dev/nvme0n1 600M
IOWriteBandwidthMax=/dev/nvme0n1 150M

После systemctl daemon-reload и рестарта службы проверьте эффективные значения через systemctl show и соответствующий io.max. Не переносите в batch.slice сам inference, journald или kubelet: тогда потолок начнёт ограничивать критическую инфраструктуру. Для контейнеров граница должна проходить на уровне unit/scope, который действительно владеет workload.

Для buffered writes есть существенное ограничение. Ядро атрибутирует writeback корректно только на поддерживаемых файловых системах, среди которых ext4, btrfs, f2fs и XFS; на неподдерживаемых writeback может уйти в root cgroup. Ещё хуже общий inode, который одновременно dirty несколько групп: ownership может переключаться, и учёт становится неточным. Поэтому checkpoint каждого job лучше писать в отдельный файл и каталог.

Раздельные I/O-каналы GPU inference и фоновой синхронизации моделей внутри AI-сервера

Два gate: сначала инфраструктура, потом приложение

Infrastructure gate доказывает механику. Запустите контролируемый bulk read в llm-batch.slice, убедитесь, что его средняя скорость приблизилась к ceiling, а счётчики io.stat растут на нужном major:minor. Затем выполните отрицательный тест: процесс вне batch.slice не должен получить тот же потолок. Это защищает от самой неприятной ошибки — случайно ограничить весь сервер и принять деградацию за «работающий QoS».

Application gate отвечает на пользовательский вопрос. Под тем же фоновым потоком отправьте заранее сохранённый набор inference-запросов. Сравните TTFT, queue time, error rate, tokens per second и readiness с baseline. Никаких универсальных процентов: gate проходит только по вашему SLO и заранее установленному error budget. Отдельно проверьте холодный старт новой model revision, иначе слишком жёсткий rbps превратит rollout в бесконечный NotReady.

В платформе Kubernetes тест не заменяет проверку scheduling. Статья про KServe, GPU Operator, Kueue и autoscaling рассматривает cluster-level orchestration; здесь unit-level gate остаётся обязательным на каждом GPU-узле, где online и background I/O делят накопитель.

Infrastructure gate перед canary

Canary rollout и rollback без героизма

Начинайте с одного узла, одной model revision и одного типа фоновой операции. Зафиксируйте окно наблюдения, затем включите предел на canary. Если downloader стал медленнее, а application gate проходит, расширьте rollout на небольшую долю узлов. Если TTFT ухудшился, не «дотягивайте» эксперимент до конца: снимите свойство, перезапустите только фоновую службу и верните workload к baseline.

Rollback должен существовать до canary. Для transient-настройки это сброс property к infinity; для drop-in — удаление конкретного override, daemon-reload и restart batch unit. Не перезапускайте vLLM без необходимости: изменение принадлежит фоновой ветви. После отката снова снимите io.stat, PSI и application metrics, чтобы доказать возврат, а не просто отсутствие алерта.

Есть и организационная ловушка. Команда моделей может ускорить sync увеличением параллелизма, не меняя формальный BPS. Тогда IOPS и queue depth меняются, а прежний ceiling перестаёт защищать latency. Поэтому rollout contract включает владельца unit, допустимые downloader flags и повторный baseline при смене формата весов, filesystem или storage topology.

Production rollout I/O-лимита: baseline, infrastructure gate, application gate, canary и rollback

Наблюдаемость: связываем io.pressure с TTFT

io.pressure показывает долю времени, когда задачи stalled из-за I/O. В PSI строка some означает, что остановилась хотя бы часть задач, full — что одновременно простаивают все non-idle tasks группы. Метрики доступны на уровне системы и каждой cgroup; именно per-cgroup сигнал помогает не обвинять NVMe во всех проблемах узла.

Собирайте три линии на одной временной шкале: delta rbytes/wbytes/rios/wios из batch.slice, some/full из её io.pressure и application latency. Kernel PSI docs также описывают poll-trigger: userspace может получить событие, когда накопленный stall превысил порог в окне. Порог выводят из нормального профиля, а не копируют из статьи.

bashobserve-llm-io.sh
CG=/sys/fs/cgroup/llm-batch.slice
cat "$CG/io.stat"
cat "$CG/io.pressure"
systemctl show model-sync.service   -p ControlGroup -p IOReadBandwidthMax -p IOWriteBandwidthMax
curl -fsS http://127.0.0.1:8000/metrics |   grep -E 'time_to_first_token|request_queue_time'

Корреляция не равна причине. Если TTFT вырос, а batch rbytes не изменился, ищите CPU throttling, memory reclaim, network storage или GPU fault. Хороший dashboard не скрывает отрицательные данные: нулевая активность batch при деградации — сильный аргумент снять лишний I/O-лимит и исследовать другой слой.

Как читать сигнал деградации

Смотрите прирост rbytes/rios в batch.slice и some/full в io.pressure. Один высокий avg10 без контекста ещё не доказывает причину.
Сопоставляйте окно pressure с TTFT, queue time, readiness и ошибками загрузки модели. Application metric подтверждает влияние на пользователя.
Лимит оставляют только когда фон замедлился, а SLO inference сохранился. Иначе ослабляют предел или выполняют rollback.
Телеметрия NVMe, cgroup I/O pressure и GPU inference на выделенном сервере

Типовые ошибки и разбор неудачного лимита

Лимит не действует. Чаще всего указан partition вместо фактического device-mapper target, процесс живёт не в той cgroup, I/O идёт через другой mount или buffered writeback не атрибутируется filesystem. Сравните major:minor в io.stat с lsblk и findmnt; только потом меняйте rate.

Сервис загружается слишком долго. Ceiling защитил online traffic, но нарушил startup deadline. Решение — не отключать readiness. Ослабьте предел для controlled rollout, прогрейте модель заранее или разделите sync и load на разные этапы. Readiness должна оставаться failure-visible, иначе запросы попадут в runtime, который ещё читает веса.

TTFT всё равно растёт. Возможно, причина выше block layer: page cache reclaim, CPU contention, NUMA placement, remote filesystem или saturation PCIe. Проведите отрицательный контроль: полностью остановите batch unit, оставив тот же набор inference-запросов. Если TTFT не вернулся к baseline, I/O-лимит не лечит текущий инцидент. Снимите его и переходите к CPU, memory, network и GPU telemetry. Такая проверка полезнее последовательного ужесточения rbps, потому что отделяет совпадение по времени от причинной связи.

Узкое место переместилось. После ограничения чтения downloader может начать дольше удерживать CPU, network socket или temporary space. Смотрите на весь путь: registry или NFS → page cache → block device → runtime → GPU. Если очередь переехала в сеть, NVMe PSI снизится, но rollout не станет безопаснее. Зафиксируйте duration синхронизации, свободное место, retry rate и readiness нового экземпляра. При превышении maintenance window возвращайте исходную конфигурацию, а не маскируйте проблему увеличением startup deadline.

Вес ухудшает ситуацию. IOWeight не является гарантией bandwidth и может требовать io.cost/controller support. Начните с проверяемого io.max, а эксперимент с weight проводите отдельно. Не включайте io.latency вслепую: kernel docs прямо советуют подобрать target по устройству и иерархии sibling-групп.

Что делать при неудачном rollout

Дерево решений при росте TTFT из-за I/O pressure на LLM-узле

Практический итог: защищаем SLO, а не цифру NVMe

I/O-изоляция полезна, когда один GPU-узел одновременно обслуживает запросы и выполняет тяжёлую доставку моделей, checkpoint или batch read. Рабочая схема проста по форме, но требовательна к доказательствам: отдельные sibling slices, ceiling только на фоновой ветви, корректный major:minor, per-cgroup учёт и два независимых gate.

Не начинайте с «правильных» мегабайт в секунду. Снимите baseline на своём kernel, systemd, filesystem и NVMe; зафиксируйте application SLO; затем проведите canary с готовым rollback. Если фон замедлился, а TTFT и readiness удержались, лимит выполняет работу. Если нет — откатите изменение и исследуйте другой слой, не расширяя привилегии и ограничения наугад.

Сохраните результаты canary рядом с unit revision: backing device, лимиты, профиль фоновой операции, PSI и application metrics. Этот короткий operational record пригодится при следующей смене модели или диска. Без него команда неизбежно начнёт спорить с памятью прошлого инцидента, а не с измерениями.

Следующий практический шаг — выбрать один GPU-сервер, воспроизвести model-sync в контролируемом окне и пройти checklist из статьи. Для нового контура лучше сразу разнести model cache и online runtime по понятным units: так каждое изменение остаётся локальным, наблюдаемым и обратимым.

Memory pressure в LLM-инференсе: cgroup v2, PSI и systemd-oomd
AI

Memory pressure в LLM-инференсе: cgroup v2, PSI и systemd-oomd

Практический runbook для защиты LLM-инференса от дефицита host RAM: PSI, cgroup v2, systemd-oomd, лимиты vLLM, алерты и аварийное восстановление.

NVIDIA CDI для rootless GPU-контейнеров: безопасный запуск LLM inference
AI

NVIDIA CDI для rootless GPU-контейнеров: безопасный запуск LLM inference

Практический runbook: NVIDIA Container Toolkit CDI, rootless Podman и Docker, выбор GPU/MIG, запуск vLLM, hardening, обновления и rollback.

Proxmox VE на выделенном сервере: как собрать частное облако для бизнеса в 2026 году
Proxmox

Proxmox VE на выделенном сервере: как собрать частное облако для бизнеса в 2026 году

Практический гид по Proxmox VE на выделенном сервере: выбор железа, ZFS и Ceph, сеть, кластер, HA, backup и безопасность.