TL;DR: Домашний сервер, две RTX 3090 без NVLink, 64 ГБ RAM и Flux. Каждая «холодная» генерация намертво забивала 8-гигабайтный swap на системном NVMe, щедро выкатывая ~4,5 ГБ случайной записи на QLC-диск за раз. Предыдущий системный SSD уже ушел в мир иной, показав 17 тысяч ошибок SMART всего на 2% износа, поэтому второй диск я решил поберечь.
Вместе с LLM выяснили, что ядро Linux не умирало от OOM, а просто «заботливо» парковало память по умолчанию. Перенесли своп в сжатую RAM через
zramи открутилиvm.swappinessдо 10. Запись упала до 0,10 ГБ на генерацию (в 44 раза!), пайплайны целы, диск больше не "стойко переносит тяготы и лишения". Ниже — замеры, грабли и готовый рецепт.
Дисклеймер («Кто пустил аналитика в консоль?!»)
Сразу без обиняков: я бизнес-аналитик, а не Linux-админ и уж тем тем более неDevOps- инженер. Моя стихия — это требования, диаграммы, метрики и здравый смысл.
Поэтому всю техническую рутину — от гипотезы и плана профилирования до написания скрипта-сэмплера на Python — за меня писала модель Qwen 3.8 27B. В роли техлида-зануды выступал агент-ревьюер DeepSeek V4 Flash, который "бил Qwen-джуниора по рукам" и перепроверял системные команды перед тем, как я нажму Enter.
Я же выступал в роли продакт-оунера этой спасательной операции, валидировал логи и оплачивал электричество. Так что если в тексте мелькает аналитический подход — вы знаете, откуда растут ноги.
Исходный стек (as-is)
Вся драма разворачивалась на домашнем сервере, собранном собственноручно и поднятом в марте этого года, с конфигом:
Железо: 64 ГБ DDR4 RAM, две штуки Nvidia RTX 3090 (без NVLink).
Текущий SSD: 1 ТБ NVMe ADATA SX8200PNP (бюджетная QLC-память, без DRAM-буфера), занят примерно на 80%. Это уже «наследник» — о судьбе предшественника ниже.
-
Flux-сервис: контейнер с
black-forest-labs/FLUX.2-dev(bf16) под оберткойuvicorn, локальный порт). Загрузка модели ленивая — прогревается только при первой входящей генерации. В пайплайне включен последовательный оффлоад весов:low_cpu_mem_usage=True enable_sequential_cpu_offload() # веса гоняются между GPU и оперативной памятью Swap: старый добрый
/swap.imgна 8 ГБ, прямо в корне SSD с дефолтным приоритетом -2 иvm.swappiness = 60.Мониторинг: Netdata в отдельном Docker-контейнере.
Почему все это заставляло системный SSD страдать
Коротко: мелкие случайные блоки по 4 КБ вперемешку с интенсивным чтением быстро заставляют контроллер "загрустить" - поправьте, если ошибаюсь.
Когда каждая генерация картинки "с ноги" забивает в swap гигабайты данных, диск "берет микрокредит на свой ресурс TBW под грабительский процент".
Запахло жареным / призрак мертвечины
С самого начала наблюдал регулярные истерики Netdata: «Swap usage 100%» при каждом старте Flux.
Казалось бы — ну забивается swap и забивается, RAM же хватает. Но психологическая травма не заставила долго ждать: уже в сентябре скоропостижно скончался системный SSD — WD BLACK SN7100 на 2 ТБ (PCIe 4.0).
После очередной перезагрузки с обновлением ядра файловая система упала в read-only (тут не уверен, корректно ли выражаюсь). Утилита fsck собрала ее из осколков, но отчет smartctl уронил мою чельсть на клавиатуру: 17 220 ошибок Media and Data Integrity Errors всего при 5 096 часах работы и смешных 2% расчетного износа. Диск не просто "износился" — он деградировал аппаратно задолго до гарантийного срока. WD поехал на барахолку, а его место занял ADATA SX8200PNP.
Не буду утверждать, что исключительно swap-файл прикончил брендовый WD. Но факт остается фактом: постоянная долбежка случайной записью на накопитель, который держит ОС, docker-контейнеры и пр. — это лучший способ добить память с дефектом ячеек. На бюджетном QLC повторять этот аттракцион совсем не хотелось, а цены на pro-варианты нынче ну совсем несъедобные.
Задачу на уровне не-технаря сформулировал следующим образом: устранить паразитные записи свопа на физический накопитель, не сломав при этом зоопарк моделей (в фоне на машине постоянно крутятся llama.cpp с Qwen-27B, Ollama под vision-задачи и Docling).
Замер: что вообще происходит под капотом?
Поскольку предположения - враг аналитика, решил дать Qwen-у под присмотром DeepSeek набросать легковесный Python-сэмплер. Скрипт опрашивает только интерфейсы статистики /proc и /sys с шагом 0,5 секунды и делает следующее:
Стучится в
/healthсервиса Flux, ожидая статусloaded:false.Фиксирует базовый уровень в покое (5 секунд).
Дергает
POST /generate(тестовый прогон: 512×512, 8 шагов Euler).Еще 60 секунд после генерации собирает хвосты активности.
Пишет CSV-файл с графиком метрик.
Отслеживали: VmHWM процесса контейнера (пиковый объем резидентной памяти по данным ядра), MemAvailable, остаток свопа (SwapFree) и реальные записанные секторы контроллера NVMe через /sys/block/nvme0n1/stat (1 сектор = 512 байт).
Что показала «холодная» генерация в исходном состоянии:
Метрика |
Значение (До) |
|---|---|
Пиковый рабочий набор RAM (VmHWM) |
52,11 ГБ |
Реальная запись на NVMe за прогон |
4,57 ГБ (4 903 632 896 байт) |
Заполнение Swap |
8,00 ГБ (100%), |
Минимум свободной памяти (MemAvailable) |
40,2 ГБ |
Пиковый Memory PSI (some/full) |
~13% |
Время холодной генерации |
~350 с |
Главный инсайт
Своп на 100% не означал, что машине не хватает оперативной памяти! На протяжении всего инференса в системе оставалось не менее 40 ГБ свободной RAM.
Ядро Linux видело дефолтный флаг vm.swappiness = 60 («сбрасывай анонимные страницы, освобождай page cache под дисковый кэш, не стесняйся»). Модель во время инициализации и оффлоада раздувает кратковременный буфер (до 52 ГБ), и ядро проактивно выталкивало «малоиспользуемые» куски на диск.
Вывод: не нужно докупать оперативку до 128 ГБ и уж тем более нельзя просто отчикать swap. Нужно поменять политику ядра по вытеснению памяти и спрятать сам swap внутрь RAM с компрессией.
Текущее здоровье подопытного
Снимаем показатели здоровья текущего накопителя через sudo smartctl -a /dev/nvme0n1:
SMART overall-health self-assessment test result: PASSED Critical Warning: 0x00 Available Spare: 100% Percentage Used: 10% Data Units Read: 245,510,562 [125 TB] Data Units Written: 49,424,487 [25,3 TB] Media and Data Integrity Errors: 0 Power On Hours: 21 202 Unsafe Shutdowns: 16
Считаем экономику: записано 25,3 ТБ при 10% счетчика износа. Значит, контроллер "надеется" на примерно на 250–260 TBW (типично для дешевого терабайтника). В запасе еще около 228 ТБ ресурса.
При среднем расходе ~0,03 DWPD диск живет в тепличных условиях, но ежедневные прогоны тяжелых диффузионных сеток способны быстро превратить теплицу в сцену перестрелки из первой "Матрицы" (обожаю ее). Зачем тратить циклы ячеек на то, что можно сжать в оперативной памяти, верно?
Решение (to-be): сажаем ядро на диету
План, собранный нейросетями и утвержденный моим внутренним занудой:
Создаем блочный девайс в оперативной памяти с прозрачным сжатием
zram0на 8 ГБ с высоким приоритетом (100).-
Настраиваем генератор через
/etc/systemd/zram-generator.conf:[zram0] zram-size = 8192 compression-algorithm = zstd swap-priority = 100 -
Усмиряем потуги ядра сбрасывать данные на диск через
/etc/sysctl.d/99-swappiness.conf:vm.swappiness = 10 Деактивируем и физически удаляем
/swap.imgиз корня, вычищая строку из/etc/fstab.
Почему выделил именно 8 ГБ, а не 16? Замеры показали, что даже при нахальном swappiness = 60 пиковое потребление свопа упиралось ровно в 8 ГБ. При значении 10 потребность падала до сотен мегабайт. При этом сжатие алгоритмом zstd упаковывает эти 8 ГБ виртуальных данных примерно в 3,2–3,3 ГБ реальной физической памяти. Раздувать zram до 16 ГБ было бы тратой RAM "по-богатому".
Грабли и нюансы, на которые "мы" наступили
Здесь нейросети пару раз попытались сачкануть, но вовремя сами поймали ошибки в логах:
Капризный синтаксис
zram-size: Если написать по привычкеzram-size = 8g, сервис упадет с паникойUnparsedTokensRemaining("g"). Утилите нужны имеено мегабайты: число8192.-
Фантомный zstd: Если в ядре модуль не подгружен заранее, система тихо и без предупреждений откатится на старый добрый
lzo-rle. Перед запуском обязательно делаемmodprobe zram && modprobe zstd, а затем проверяем активный компрессор:cat /sys/block/zram0/comp_algorithm # Должно быть строго со скобками: ... [zstd] ... Хитрые (во всяком случае, для меня) юниты systemd:
zram-generatorформирует динамические юниты с типомstatic. Писатьsystemctl enable dev-zram0.swapбесполезно — генератор сам собирает топологию на раннем этапе загрузки ОС."Правило сапера" при переключении: Не делаем
swapoff -a! Если новый zram не поднялся, оставим систему без страховки прямо под нагрузкой. Сначала активируемzram0, смотрим черезswapon --show, что работают оба (у zram приоритет выше), затем гасим старый своп:swapoff /swap.img, и только после этого удаляем файл.Потайные скрипты с
swapoff -a: У меня в утилите для временной выгрузки контейнеров ради ComfyUI болтался костыльsudo swapoff -a && sudo swapon -a. В новых реалиях он бы просто снес zram-диск. Скрипт пришлось оперативно отредактировать.
Валидация: проверяем результат на цифрах
Берем тот же тестовый сценарий: рестарт контейнера Flux, статус loaded:false, холодный запрос 512×512 в 8 шагов.
Метрика |
До (Swap-файл на NVMe) |
После (zram + swappiness=10) |
Разница |
|---|---|---|---|
Запись на NVMe за прогон |
4,57 ГБ |
0,10 ГБ (110 МБ) |
Снижение в 44(!) раза |
Пик свопа |
8 ГБ на QLC-диске |
8 ГБ в zram (~3,3 ГБ RAM) |
0 байт на диск |
Минимум доступной RAM |
40,2 ГБ |
35,3 ГБ |
В пределах нормы |
OOM-киллер |
Спит |
Спит |
Стабильно |
Время холодной генерации |
~350 с |
~325 с |
Легкий буст из-за RAM |
Запись на системный диск за холодную генерацию упала с 4,57 гигабайт до 110 мегабайт! Оставшиеся 100 МБ — стандартный "шум" системных логов и прочего.
Для полноты картины снял телеметрию боевой тёплой генерации (1344×768, 20 шагов, около 800 секунд инференса) через sar:
Окно мониторинга |
Утилизация NVMe (%util) |
Скорость чтения |
Скорость записи |
|---|---|---|---|
10:10 – 10:20 |
36% |
~840 МБ/с |
~0,9 МБ/с |
10:20 – 10:30 |
60% |
~1,37 ГБ/с |
~0,4 МБ/с |
Диск периодически нагружен, но теперь это чтение — как понимаю, стриминг слоев модели между RAM и видеопамятью карт. Чтение для флеш-памяти почти "бесплатно" с точки зрения износа, а запись -- уже на уровне погрешности.
Что решил НЕ делать (и почему)
Зажимать память через Docker
mem_limit: Рабочий набор процесса в пике прыгает до 52 ГБ. Если бы выставил лимит, скажем, в 48 ГБ, контейнер бы просто падал по OOM на старте. Лечить проблему падением сервиса — такая себе оптимизация.Полностью выключать swap (
swapoffнавсегда): Опасная практика (как минимум в моем случае). Когда рядом крутятся другие прожорливые модели и сервисы OCR, внезапный скачок потребления без страховки в виде свопа приведет к "отстрелу" процессов ядром.zramже дает безопасность свопа без физического вреда диску.Переносить своп на старый WD или HDD: Смысла гонять изношенный дефектный SSD нет никакого, а HDD похоронит производительность инференса.
Резюме для тех, кто держит нейросети дома
Алерт «Swap 100%» — не всегда катастрофа и нехватка RAM. Проверяйте показатель
MemAvailable. Если свободной памяти полно, ваше ядро просто слишком проактивно паркует страницы в файл из-за высокогоswappiness.Своп на системном NVMe быстро сжирает ресурс SSD. Проверьте счетчики секторов в
/sys/block/<диск>/statдо и после тяжелых нагрузок.Тандем
zram+vm.swappiness = 10творит чудеса. В нашем случае сократил запись на SSD на 97%.LLM-агенты вполне пригодны для сисадминских задач. Даже если вы не технарь, связка из "джун"-модели (Qwen 3.8 27B) и ревьюера (DeepSeek V4 Flash) позволяет вполне себе безопасно диагностировать ядро, писать тулинг и оптимизировать Linux-сервер — главное, не отключать собственное критическое мышление.
Г