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 секунды и делает следующее:

  1. Стучится в /health сервиса Flux, ожидая статус loaded:false.

  2. Фиксирует базовый уровень в покое (5 секунд).

  3. Дергает POST /generate (тестовый прогон: 512×512, 8 шагов Euler).

  4. Еще 60 секунд после генерации собирает хвосты активности.

  5. Пишет 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%), SwapFree упал в 0

Минимум свободной памяти (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): сажаем ядро на диету

План, собранный нейросетями и утвержденный моим внутренним занудой:

  1. Создаем блочный девайс в оперативной памяти с прозрачным сжатием zram0 на 8 ГБ с высоким приоритетом (100).

  2. Настраиваем генератор через /etc/systemd/zram-generator.conf:

    [zram0]
    zram-size = 8192
    compression-algorithm = zstd
    swap-priority = 100
    
  3. Усмиряем потуги ядра сбрасывать данные на диск через /etc/sysctl.d/99-swappiness.conf:

    vm.swappiness = 10
    
  4. Деактивируем и физически удаляем /swap.img из корня, вычищая строку из /etc/fstab.

Почему выделил именно 8 ГБ, а не 16? Замеры показали, что даже при нахальном swappiness = 60 пиковое потребление свопа упиралось ровно в 8 ГБ. При значении 10 потребность падала до сотен мегабайт. При этом сжатие алгоритмом zstd упаковывает эти 8 ГБ виртуальных данных примерно в 3,2–3,3 ГБ реальной физической памяти. Раздувать zram до 16 ГБ было бы тратой RAM "по-богатому".

Грабли и нюансы, на которые "мы" наступили

Здесь нейросети пару раз попытались сачкануть, но вовремя сами поймали ошибки в логах:

  1. Капризный синтаксис zram-size: Если написать по привычке zram-size = 8g, сервис упадет с паникой UnparsedTokensRemaining("g"). Утилите нужны имеено мегабайты: число 8192.

  2. Фантомный zstd: Если в ядре модуль не подгружен заранее, система тихо и без предупреждений откатится на старый добрый lzo-rle. Перед запуском обязательно делаем modprobe zram && modprobe zstd, а затем проверяем активный компрессор:

    cat /sys/block/zram0/comp_algorithm
    # Должно быть строго со скобками: ... [zstd] ...
    
  3. Хитрые (во всяком случае, для меня) юниты systemd: zram-generator формирует динамические юниты с типом static. Писать systemctl enable dev-zram0.swap бесполезно — генератор сам собирает топологию на раннем этапе загрузки ОС.

  4. "Правило сапера" при переключении: Не делаем swapoff -a! Если новый zram не поднялся, оставим систему без страховки прямо под нагрузкой. Сначала активируем zram0, смотрим через swapon --show, что работают оба (у zram приоритет выше), затем гасим старый своп: swapoff /swap.img, и только после этого удаляем файл.

  5. Потайные скрипты с 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 похоронит производительность инференса.

Резюме для тех, кто держит нейросети дома

  1. Алерт «Swap 100%» — не всегда катастрофа и нехватка RAM. Проверяйте показатель MemAvailable. Если свободной памяти полно, ваше ядро просто слишком проактивно паркует страницы в файл из-за высокого swappiness.

  2. Своп на системном NVMe быстро сжирает ресурс SSD. Проверьте счетчики секторов в /sys/block/<диск>/stat до и после тяжелых нагрузок.

  3. Тандем zram + vm.swappiness = 10 творит чудеса. В нашем случае сократил запись на SSD на 97%.

  4. LLM-агенты вполне пригодны для сисадминских задач. Даже если вы не технарь, связка из "джун"-модели (Qwen 3.8 27B) и ревьюера (DeepSeek V4 Flash) позволяет вполне себе безопасно диагностировать ядро, писать тулинг и оптимизировать Linux-сервер — главное, не отключать собственное критическое мышление.

Г

Комментарии (0)