MLOps для дообученных моделей: версии, тесты, откат
После нескольких циклов дообучения, квантования и, может быть, дистилляции на srv-gpu01 и srv-ai01 обычно накапливается десяток папок с чекпоинтами, часть из которых уже никто не помнит, чем отличается от других. Классический разговор через месяц-два: «а какая версия модели сейчас в проде?», «а на каком датасете она обучалась?», «а можно откатиться на предыдущую, если новая версия неожиданно начала отвечать хуже?» — и на все три вопроса нет быстрого ответа, потому что процесс выкладки был ручным и одноразовым. MLOps для дообученных моделей — это не обязательно тяжёлая инфраструктура с десятком сервисов, а прежде всего дисциплина: версионировать не только веса, автоматически проверять качество перед выкладкой и уметь откатиться за секунды, а не за новый цикл обучения. Разберём, как выстроить это на своём железе в сети 192.168.10.0/24 без лишнего инженерного веса.
Что именно нужно версионировать
Слово «MLOps» иногда пугает масштабом ассоциаций — кажется, что речь про Kubernetes-кластеры, CI/CD-пайплайны и команду выделенных инженеров. Для команды с одним-двумя серверами и несколькими дообученными моделями всё это избыточно: нужный набор практик укладывается в дисциплину именования файлов, один bash-скрипт и привычку не удалять предыдущую рабочую версию, пока не убедился, что новая точно лучше. Дальше по шагам разберём именно этот минимально достаточный набор, без лишней инфраструктуры.
Одних весов модели недостаточно для воспроизводимости. Полноценная версия модели — это связка из четырёх вещей: кода и конфигурации обучения (обычный git), версии датасета, на котором она обучалась (датасеты меняются, и без фиксации версии невозможно понять, почему модель ведёт себя иначе после переобучения), самих весов или адаптера, и отчёта об оценке качества (см. статью про метрики после дообучения). Если хранить только веса, через полгода не получится ответить ни на один вопрос из списка выше — только «вот файл, он вроде неплохо отвечает».
Шаг 1. Структура реестра моделей на диске
Не обязательно сразу поднимать MLflow или другой специализированный реестр моделей — для команды с одним-двумя GPU-серверами вполне достаточно простой и дисциплинированной структуры директорий на srv-ai01.
/mnt/srv-ai01/registry/support-qwen/
v1.0/
adapter/ # веса LoRA-адаптера или полный чекпоинт
config.yaml # гиперпараметры обучения
metadata.json # git commit, датасет, базовая модель, дата
eval_report.json # результат прогона golden-набора
v1.1/
...
v2.0/
...
{
"version": "v2.0",
"base_model": "Qwen/Qwen2.5-7B-Instruct",
"git_commit": "a3f9c21",
"dataset_version": "support_clean_v3",
"dataset_sha256": "9e1f7c...",
"trained_at": "2026-08-20T14:32:00",
"trained_by": "support-finetune/scripts/train.py"
}
Такая структура кажется избыточной ровно до первого случая, когда нужно понять, чем текущая прод-модель отличается от версии трёхмесячной давности — с ней это вопрос одного взгляда в metadata.json, без неё — археологические раскопки по логам обучения, если они вообще сохранились.
Отдельно стоит решить, где физически хранить архив старых версий: держать все чекпоинты на быстром NVMe-хранилище srv-ai01 расточительно, если их накопилось два десятка, а часть из них не использовалась месяцами. Рабочая практика — держать на быстром диске только последние 2-3 версии, готовые к мгновенному переключению, а более старые переносить на медленное сетевое хранилище с сохранением всех метаданных, чтобы при необходимости — например, для сравнения с моделью годичной давности — их можно было вернуть обратно, просто это займёт не секунды, а минуты на копирование.
Схему версий стоит держать простой и предсказуемой: мажорная версия меняется при смене базовой модели или архитектурного подхода (например, переход с LoRA на полное дообучение), минорная — при переобучении на обновлённом датасете с теми же гиперпараметрами. Так по одному номеру версии сразу понятно, насколько велики изменения, не читая метаданные — v1.4 к v1.5, скорее всего, просто освежили данные, а v1.x к v2.0 — сменили саму базовую модель, и сравнивать их эволюцию нужно куда внимательнее.
Шаг 2. Версионируй датасеты, а не только код
Самый простой рабочий вариант без внедрения отдельного инструмента — фиксировать хеш файла датасета в метаданных версии модели и хранить сами версии датасета рядом на диске с понятными именами.
sha256sum /mnt/srv-ai01/datasets/support_clean_v3/train.jsonl
Если объёмы данных растут и версий становится много, имеет смысл перейти на DVC — он версионирует большие файлы поверх git, храня сами данные отдельно, а в репозитории только лёгкие указатели на них:
dvc add data/train.jsonl
git add data/train.jsonl.dvc .gitignore
git commit -m 'datasets: support_clean_v3'
dvc push
Неважно, какой из двух вариантов выбрать на старте — важно, чтобы в metadata.json каждой версии модели однозначно фиксировалась ссылка на конкретную версию датасета, а не общее «обучено на актуальных данных».
Шаг 3. Автоматический eval-гейт перед промоушном в прод
Ручной просмотр результатов оценки перед каждой выкладкой рано или поздно приводит к тому, что кто-то забудет это сделать в спешке или под давлением дедлайна. Простой скрипт-гейт, сравнивающий новый отчёт с текущей прод-версией по зафиксированному порогу, снимает эту зависимость от человеческого внимания.
import json, sys
def load_report(path):
return json.load(open(path))
new_report = load_report('/mnt/srv-ai01/registry/support-qwen/v2.0/eval_report.json')
prod_report = load_report('/mnt/srv-ai01/registry/support-qwen/v1.1/eval_report.json')
TOLERANCE = 0.05 # допустимая просадка метрики при промоушне
if new_report['avg_correctness'] < prod_report['avg_correctness'] - TOLERANCE:
print('ОТКАЗ: correctness регрессировал сильнее допустимого порога')
sys.exit(1)
print('гейт пройден, версия готова к промоушну')
Такой скрипт стоит запускать не вручную из головы, а как обязательный шаг сразу после того, как отчёт об оценке новой версии сохранён — например, из того же скрипта обучения или отдельным шагом в cron-джобе, который проверяет появление нового отчёта в реестре. Промоушн в прод должен быть физически невозможен без прохождения гейта, а не опираться на то, что кто-то не забудет его запустить.
Порог допустимой просадки (TOLERANCE) стоит выбирать осознанно, а не наугад. Слишком строгий порог заблокирует полезные версии из-за естественного шума в оценке — LLM-as-judge не идеально стабилен от прогона к прогону, и небольшие колебания в районе одного-двух процентов — это норма, а не сигнал регрессии. Слишком мягкий порог, наоборот, пропустит в прод версию, которая объективно хуже. Разумная отправная точка — 3-5% допустимой просадки по основной метрике при условии, что остальные метрики не деградировали одновременно, а дальше порог можно уточнять по мере накопления опыта с тем, насколько шумной оказывается оценка именно в твоём проекте.
Шаг 4. Продвижение и откат через симлинк
Самый дешёвый способ сделать деплой мгновенным и откат тоже мгновенным — не копировать веса при каждой выкладке, а переключать символическую ссылку, на которую смотрит инференс-сервис.
ln -sfn /mnt/srv-ai01/registry/support-qwen/v2.0 /mnt/srv-ai01/models/support-current
systemctl restart support-inference.service
Сервис инференса всегда читает модель из фиксированного пути /mnt/srv-ai01/models/support-current, не зная и не заботясь о том, какая версия за ним стоит. Если после выкладки v2.0 в проде что-то пошло не так — модель начала отвечать нестабильно, пользователи жалуются, метрики в реальном мониторинге поехали вниз — откат занимает секунды:
ln -sfn /mnt/srv-ai01/registry/support-qwen/v1.1 /mnt/srv-ai01/models/support-current
systemctl restart support-inference.service
Держи на диске srv-ai01 хотя бы две-три последние версии в полной готовности к переключению — старые можно архивировать на более медленное хранилище или удалять по политике хранения, но версия, которая была в проде прямо перед текущей, должна оставаться под рукой всегда, без необходимости что-либо докачивать или пересобирать в момент инцидента.
Шаг 5. Мониторинг в проде
Оценка на golden-наборе перед выкладкой ловит регрессии относительно того, что было известно на момент обучения, но не ловит постепенный дрейф: со временем сотрудники начинают спрашивать про новые темы, которых не было в обучающих данных, и качество ответов на них никак не отражено в старом отчёте. Простое логирование сэмпла реальных запросов и ответов уже сильно помогает.
import json, time
def log_interaction(question, answer, model_version, latency_ms):
record = {
'ts': time.time(),
'model_version': model_version,
'question': question,
'answer': answer,
'latency_ms': latency_ms,
}
with open('/mnt/srv-ai01/logs/support-inference.jsonl', 'a') as f:
f.write(json.dumps(record, ensure_ascii=False) + '\n')
Раз в одну-две недели стоит выгружать случайную выборку из этих логов и прогонять через тот же LLM-as-judge, что используется для оценки после обучения — это даёт непрерывную картину качества в проде, а не разовый снимок на момент выкладки, и часто становится источником новых примеров для следующего датасета дообучения, когда становится видно, на каких реальных вопросах модель заметно слабее.
Полезно также следить за более простыми косвенными сигналами, не требующими прогона через judge на каждый запрос: долю ответов, которые пользователи явно пометили как неудачные (если в интерфейсе есть кнопка «не помогло»), среднюю длину и задержку ответа по времени — резкий скачок задержки часто означает проблему с сервером, а не с моделью, — и долю запросов, на которые модель отвечает отказом или «не знаю». Резкий рост доли отказов иногда означает, что в компании появилась новая тема обращений, которую модель никогда не видела, и это верный сигнал, что пора собирать данные для следующего цикла дообучения, а не просто списывать на шум.
С чего начать, если процесса пока нет вообще
Если сейчас все модели лежат вперемешку в домашней папке на srv-gpu01 без какой-либо системы, не пытайся сразу внедрить всё описанное выше целиком — это займёт неделю на инфраструктуру вместо работы над самими моделями. Минимально полезный первый шаг — просто переименовать существующие чекпоинты по единой схеме версий и завести рядом с каждым текстовый файл с тремя фактами: на каком датасете обучен, какие гиперпараметры использовались, какой результат показал на ручной проверке. Это уже устраняет большую часть боли с «а что это за папка и почему их пять». Симлинк для мгновенного отката — следующий по приоритету шаг, он стоит буквально одной команды и одной привычки, а автоматический eval-гейт и постоянный мониторинг можно добавлять по мере того, как число версий и частота деплоев растут настолько, что ручной контроль перестаёт справляться.
Частые ошибки и как их избежать
- Отсутствие механизма отката. Если деплой умеет двигаться только вперёд, при проблеме в проде единственный выход — срочно переобучать модель заново, что занимает часы вместо секунд на переключение симлинка.
- Версионирование только весов, без датасета. Через несколько месяцев эксплуатации невозможно воспроизвести обучение и понять, почему одна версия модели ведёт себя иначе другой, если неизвестно, на каких именно данных она обучалась.
- Ручной промоушн без автоматического гейта. Рано или поздно кто-то выкатит версию с регрессировавшим качеством просто потому, что забыл или не успел прогнать оценку перед выкладкой в спешке.
- Отсутствие мониторинга после деплоя. Дрейф данных и появление новых тем в вопросах пользователей незаметны без постоянного отслеживания — о деградации качества чаще всего узнают из жалоб, а не из метрик, хотя вполне могли бы узнать раньше.
- Слишком сложный процесс на старте. Попытка сразу внедрить полноценный реестр моделей, CI-пайплайн и дашборды мониторинга для проекта с одной моделью и редкими переобучениями чаще заканчивается тем, что процесс забрасывают из-за избыточной сложности — лучше начать с директорий и симлинка и наращивать сложность по мере реальной необходимости.
Что дальше
Обучение и дообучение моделей — большая область, вот соседние темы серии: