
Статья подготовлена вместе с экспертом
Елизавета Белоконова, менеджер продукта

Хранение данных для машинного обучения деградирует довольно незаметно. Обучение идёт, метрики растут, а через три месяца выясняется, что датасет v3 у двух дата-сайентистов различается на пару тысяч примеров, лучший чекпоинт лежит в /home/user/experiments на ноутбуке, который отдали в ремонт, и модель, ушедшую в продакшен, воспроизвести уже нечем.
Объектное хранилище закрывает эту дыру не за счёт того, что места больше. Количество объектов в бакете VK Object Storage не ограничено, объект грузится обычным запросом до 32 ГБ и составной загрузкой до 320 ТБ, доступ к любому объекту идёт по HTTP параллельно из любого числа обучающих воркеров, а инструменты ML-стека настраиваются на произвольный S3-эндпоинт. Российские команды уже собирают вокруг S3 общий слой хранения: датасеты лежат в бакете, оттуда их читает Spark и туда же пишет Trino.
Одна деталь мешает перенести привычки с ноутбука: в объектном хранилище нет папок. Пока команда этого не учитывает, архитектуру она по сути проектирует вслепую: схема имён определяет и права доступа, и правила удаления, и скорость листинга. Ниже — как разложить артефакты по префиксам, чем версионировать датасеты, где живут признаки и веса и какие правила жизненного цикла режут счёт.

Елизавета Белоконова, менеджер продукта
Данные ML-контура плохо ложатся на диск виртуальной машины сразу по четырём причинам.
Разница между дисками и объектным хранилищем:
| Что требуется ML-контуру | Диск виртуальной машины | Объектное хранилище |
| Ёмкость | выделяется заранее, фиксированным томом | количество объектов в бакете не ограничено |
| Доступ | одна машина, файловая система | запрос по HTTP параллельно из многих воркеров |
| Размер одного объекта | ограничен размером тома | до 32 ГБ обычной загрузкой, до 320 ТБ составной |
| Неактуальные версии | чистятся вручную | удаляются по правилу жизненного цикла |
| Известные границы нагрузки | зависят от типа диска | 1 000 запросов в секунду, листинг 250 в секунду |
Дело не только в объёме. Google в руководстве по MLOps выносит управление данными и моделями (data and model management) в отдельный процесс жизненного цикла — наравне с обучением и развёртыванием. То есть хранение данных для машинного обучения проектируют как отдельную подсистему, а не выбирают, куда положить файлы.
Ёмкость не планируется заранее. Количество объектов в бакете не ограничено, а рамки известны заранее: 100 бакетов на проект, 1 000 запросов в секунду и отдельный лимит на листинг в 250 запросов в секунду. Операции тарифицируются по факту использования.
Параллельный доступ по HTTP. Адресация плоская: объект достаётся по ключу, без обхода дерева каталогов. Несколько GPU-воркеров читают один и тот же префикс одновременно, локальная копия датасета на каждой машине перестаёт быть обязательной.
Экосистема поверх S3 API. DVC, MLflow и Feast работают с S3-совместимым хранилищем как со стандартом де-факто, поэтому переезд между провайдерами сводится к смене эндпоинта и ключей. Обратная сторона — клиенты по умолчанию смотрят в сторону Amazon: для AWS CLI параметр --endpoint-url обязателен именно поэтому.
Ключ объекта состоит из необязательных префиксов и имени, разделитель между ними — символ /. Файловый менеджер рисует по этому разделителю дерево, но физической иерархии за ним нет: photos/myphoto1.jpg выглядит как файл в каталоге photos/, а на деле это один объект с длинным ключом. Отсюда важный вывод: раскладка в S3 — соглашение об именовании, и меняется оно только перезаливкой объектов, а не переименованием каталога.
Рабочая схема для ML-проекта:
text
ml-raw-data/ # неизменяемые исходники, только запись и чтение └── vision/retail-shelves/v3/2026-05-12/ ├── images/000001.jpg └── manifest.jsonl ml-processed/ # готовые выборки под обучение └── retail-shelves/v3/ ├── train/part-0000.parquet ├── val/part-0000.parquet └── test/part-0000.parquet ml-features/ # рассчитанные признаки └── retail/user_purchase_30d/dt=2026-07-22/part-0000.parquet ml-artifacts/ # веса, чекпоинты, метрики └── detector/model-v2/2026-07-22/commit-abc1234/ ├── weights.safetensors └── metrics.json ml-experiments/ # логи и конфиги прогонов └── detector/2026-07-22/run-8f31/ ├── config.yaml └── train.log
Пять верхних имён — это либо пять бакетов, либо пять префиксов одного бакета. Граница проходит по двум признакам. Первый — права доступа: разрешения выдаются на бакет и на объект, и разнести исходники «только чтение» и артефакты «запись из обучения» обычно проще бакетами. Второй — правила жизненного цикла: фильтр правила работает по одному префиксу ключа на правило (плюс по тегам), а самих правил в бакете не больше 50. Против чрезмерного дробления работает лимит в 100 бакетов на проект: бакет на каждый эксперимент упирается в него очень быстро.
Ключ должен отвечать на вопрос «какие ровно это данные» без обращения к чужой памяти. Минимальный набор элементов:
Собранный ключ выглядит так:
text
ml-artifacts/detector/model-v2/2026-07-22/commit-abc1234/weights.safetensors
Три типичные ошибки:
Схема именования связана ещё с одним решением, о котором обычно вспоминают поздно: класс хранения. В Object Storage он указывается при загрузке объекта (STANDARD для класса Hotbox, STANDARD_IA для Icebox) либо наследуется от бакета, если не указан явно. «Горячий» и «холодный» контуры разводятся на этапе проектирования префиксов, а не задним числом.
Git не тянет бинарники в десятки гигабайт, поэтому датасет в репозиторий не кладут. DVC (Data Version Control) решает эту проблему разделением: в Git уезжает небольшой метафайл .dvc с хэшем содержимого, сами данные — в объектное хранилище. Рабочий цикл короткий: dvc add считает хэш и создаёт метафайл, git add и git commit фиксируют его вместе с кодом, dvc push заливает данные в удалённое хранилище.
В результате коммит кода и состояние датасета связаны одним хэшем. Переключились на ветку трёхмесячной давности, выполнили dvc pull и получили ровно те файлы, на которых обучалась модель того периода. Без этого связка «код в Git, данные где-то в бакете» держится на дисциплине и комментариях в чате.
Проект активен: последний релиз на PyPI — 3.67.1 от 31 марта 2026 года. Релизы выходят нерегулярно, интервалы между ними доходят до нескольких месяцев, поэтому пиновать конкретный номер версии в командах не стоит.
DVC работает с любым S3-совместимым хранилищем: адрес задаётся параметром endpointurl, остальные параметры S3 тоже принимаются, а их применимость зависит от платформы хранения. Для Object Storage в московском регионе эндпоинт — https://hb.ru-msk.vkcloud-storage.ru, код региона ru-msk; есть равнозначная короткая форма https://hb.vkcloud-storage.ru.
bash
# удалённое хранилище по умолчанию dvc remote add -d vkcloud s3://ml-raw-data/dvcstore dvc remote modify vkcloud endpointurl https://hb.ru-msk.vkcloud-storage.ru # ключи доступа: флаг --local пишет их в .dvc/config.local, # который игнорируется Git и не уезжает в репозиторий dvc remote modify --local vkcloud access_key_id <ACCESS_KEY> dvc remote modify --local vkcloud secret_access_key <SECRET_KEY>
Флаг --local здесь важен: документация DVC прямо рекомендует его, чтобы чувствительные данные попали в игнорируемый Git файл .dvc/config.local, а не утекли через историю репозитория. Если хранилище отдаёт временные учётные данные, для них есть параметр session_token.
Дальше цикл повторяется на каждой итерации датасета:
bash
dvc add data/retail-shelves/v3 git add data/retail-shelves/v3.dvc data/.gitignore git commit -m "Датасет retail-shelves v3" dvc push # на другой машине или в CI git checkout <commit> dvc pull
Проверять связку проще всего на пустом каталоге: пара dvc push и dvc pull, и если после dvc pull файлы не появились, причина почти всегда в эндпоинте или ключах, а не в самом DVC.
Границу инструмента стоит держать в голове с первого дня. DVC отвечает за соответствие коммита кода состоянию датасета и за файлы в удалённом хранилище. Он не ведёт историю метрик по прогонам, не сравнивает эксперименты в интерфейсе и не хранит реестр моделей со стадиями — это территория MLflow. Попытка вести учёт экспериментов через ветки и теги Git предсказуемо упирается в то, что сравнить двадцать прогонов по трём метрикам через git log нельзя.
По определению самого проекта Feast, feature store — это слой, который решает три задачи. Первая — training-serving skew, то есть расхождение между признаками, на которых модель обучалась, и признаками, которые она получает в продакшене; feature store даёт одинаковый доступ к ним и для обучения, и для низколатентного serving. Вторая — переиспользование признаков через единый слой доступа, который отделяет способ хранения фич от способа их извлечения. Третья — point-in-time correctness, то есть сборка обучающих выборок, корректных на момент времени, без подглядывания в будущее.
Конструктивно это две половины: offline store с исторической выборкой для обучения и online store для низколатентной подачи, плюс реестр, feature server, веб-интерфейс и CLI.
Классическая боль выглядит буднично. Признак user_purchase_30d считает команда рекомендаций в своём пайплайне и команда антифрода в своём. Формулы расходятся на границе окна, обучение идёт на одной версии, продакшен получает другую, а расследование занимает неделю.
Границы проект очерчивает сам. Feast прямым списком пишет, чем feature store не является: не ETL- и не ELT-система, не оркестратор данных, не замена корпоративного хранилища, не инструмент версионирования датасетов и управления экспериментами, не система полного захвата lineage.
Offline store хранит исторические значения признаков файлами в S3, обычно в Parquet, и по ним собирается обучающая выборка с корректностью на момент времени. Конфигурация проекта живёт в feature_store.yaml:
text
project: retail registry: s3://ml-features/registry.db provider: local offline_store: type: file
Значения registry: s3://… и offline_store.type: file совпадают с документацией Feast и её примерами. Путь к самим Parquet-файлам задаётся не здесь, а в источнике FileSource; для S3-совместимого хранилища туда же передаётся s3_endpoint_override. Версии проекта меняются часто: релиз v0.65.0 вышел 20.07.2026, предыдущий, v0.64.0, — 13.06.2026.
Практический вывод для небольшой команды: отдельный слой признаков окупается там, где есть онлайн-инференс и несколько потребителей одних и тех же фич. Если модель переобучается раз в месяц и отдаёт предсказания батчем, роль offline store спокойно играет тот же бакет с Parquet-выборками, а версионирование закрывает DVC.
Хранение данных для машинного обучения выигрывает от соседства с вычислениями: датасеты, признаки и веса — в Object Storage, обучение — на Cloud GPU в том же контуре, без выгрузки терабайтов между площадками. Характеристики сервиса, лимиты и условия подключения собраны на странице Object Storage.
MLflow разделяет хранение на две части: параметры, метрики и метаданные прогонов идут в базу данных tracking-сервера, артефакты (веса, графики, отчёты, конфиги) уезжают в отдельное хранилище. Чтобы артефакты MLflow писались не в Amazon, а в стороннее S3-совместимое хранилище, задаётся переменная окружения MLFLOW_S3_ENDPOINT_URL с адресом эндпоинта. Ключи доступа передаются стандартными переменными.
bash
export MLFLOW_S3_ENDPOINT_URL=https://hb.ru-msk.vkcloud-storage.ru export AWS_ACCESS_KEY_ID=<ACCESS_KEY> export AWS_SECRET_ACCESS_KEY=<SECRET_KEY>
python
import mlflow mlflow.set_tracking_uri("http://mlflow.internal:5000") mlflow.set_experiment("retail-shelves-detector") with mlflow.start_run(run_name="run-8f31"): mlflow.log_param("lr", 3e-4) mlflow.log_metric("map50", 0.71) mlflow.log_artifact("weights.safetensors", artifact_path="model")
Поднимать сервер самостоятельно не обязательно: MLflow есть в ML Platform VK Cloud как отдельный сервис — инстанс создаётся в личном кабинете и работает либо в связке с JupyterHub, либо в режиме Standalone. Для развёртывания моделей в JupyterHub предустановлена python-библиотека MLflow Deployment Client.
Одна ловушка ждёт тех, кто переезжает на третью мажорную версию. В MLflow 3 убраны MLflow Recipes и часть флейворов, появился отдельный объект LoggedModel со своими метриками и параметрами, а артефакты моделей больше не показываются на вкладке Artifacts страницы прогона: они переехали на страницу Logged Models. Жалоба «после обновления пропали веса» в большинстве случаев означает, что интерфейс смотрят не на той странице — объекты в бакете на месте.
Формат весов — вопрос не удобства, а доверия к содержимому бакета. Чекпоинт в pickle-формате, которым PyTorch сохраняет веса по умолчанию, способен исполнить произвольный код при загрузке. С версии PyTorch 2.6 torch.load идёт с weights_only=True по умолчанию и ограничивает распаковку тензорами, примитивами, словарями и явно разрешёнными типами. Дыра остаётся на практике: старые чекпоинты в безопасном режиме часто не читаются, загрузку повторяют с weights_only=False — и чужой файл из общего хранилища выполняет код в вашем окружении.
safetensors закрывает именно эту часть. Формат хранит сырые тензоры и небольшой JSON-заголовок, встроить исполняемый код в файл модели нельзя, отдельные тензоры читаются по имени, без загрузки всего чекпоинта в память, на Hugging Face Hub формат используется по умолчанию. Загрузка быстрее за счёт чтения тензоров по имени и отсутствия распаковки объектов Python. Публичных замеров с воспроизводимой методикой у формата нет, поэтому закладывать конкретный коэффициент ускорения в расчёты не стоит.
8 апреля 2026 года safetensors стал проектом PyTorch Foundation: Hugging Face передала формат фонду, где уже размещены PyTorch, vLLM, DeepSpeed, Ray и Helion. Для команд это сигнал, что безопасный формат весов перестал быть предпочтением отдельного вендора.
Границы у решения тоже есть. safetensors хранит тензоры, а не произвольные объекты Python, поэтому состояние оптимизатора, кастомные классы и часть пайплайнов всё равно сериализуются иначе; такие файлы стоит держать под отдельным префиксом с более узкими правами. И ещё: формат отвечает за отсутствие исполняемого кода, но не за подлинность. За вопрос «кто положил этот чекпоинт в бакет» отвечают права доступа и версионирование, а не расширение файла.
Счёт за хранение растёт чаще не от датасетов, а от мусора вокруг них. Обучение пишет чекпоинт каждые N шагов, препроцессинг оставляет распакованные копии, каждый неудачный прогон оставляет логи и веса, к которым никто не вернётся.
Простая арифметика показывает масштаб: 100 экспериментов, по 10 сохранённых чекпоинтов в каждом, по 1 ГБ на чекпоинт дают 1 ТБ, целиком состоящий из промежуточных состояний. Цифра иллюстративная, но порядок обычно такой же.
Отдельная статья роста — версии объектов. В бакете с включённым версионированием перезапись объекта не удаляет предыдущее состояние, а добавляет новое, и «очищенный» префикс продолжает занимать место старыми версиями.
Жизненный цикл в Object Storage делает ровно одно: автоматизированно удаляет объекты из бакета по заданным правилам. Настраиваются ID, Status, Filter, Expiration (варианты Days, ExpiredObjectDeleteMarker) и NoncurrentVersionExpiration для бакетов с версионированием; фильтр работает по префиксу ключа (один префикс на правило) и по тегам, которые объединяются оператором And. Правила применяются ежедневно и действуют как на объекты, добавленные в бакет с уже настроенным жизненным циклом, так и на объекты бакета, когда правило появилось позже.
Переноса объекта из класса в класс среди этих операций нет. Класс задаётся при загрузке или наследуется от бакета, поэтому «переложить в холодный класс» — это отдельное действие команды: объект копируется на себя с новым классом,
bash
aws s3 cp s3://<БАКЕТ>/<КЛЮЧ> s3://<БАКЕТ>/ --storage-class STANDARD_IA
Смена класса самого бакета на уже загруженные объекты не распространяется. Ждать от правил жизненного цикла автоматического перевода данных в холодный класс не стоит: они именно удаляют.
Ориентир по срокам для типового ML-проекта:
| Тип данных | Срок хранения | Что происходит дальше | Чем реализуется |
| Сырые данные (ml-raw-data/) | бессрочно | остаются источником истины | правил жизненного цикла нет |
| Обработанные выборки (ml-processed/) | 6 месяцев в горячем классе | перенос в холодный класс Icebox | копирование объектов с классом STANDARD_IA |
| Промежуточные чекпоинты (ml-artifacts/*/checkpoints/) | 30 дней | удаление | Expiration с параметром Days |
| Финальные веса продакшен-моделей | бессрочно | остаются вместе с метриками | правил жизненного цикла нет |
| Логи экспериментов (ml-experiments/) | 90 дней | перенос в холодный класс либо удаление | Expiration или ручной перенос |
| Неактуальные версии объектов | 30 дней | удаление старых версий | NoncurrentVersionExpiration |
Сроки в таблице — ориентир, а не настройка сервиса: в регулируемых отраслях срок хранения логов задаётся требованиями, а не удобством. Проверить стоит и лимит: правил жизненного цикла в бакете не больше 50, поэтому «правило на каждый эксперимент» не масштабируется, а «правило на префикс уровня артефактов» работает годами.
С агрессивным TTL (time to live, срок жизни объекта) стоит быть осторожнее: удалённый чекпоинт означает, что дообучить модель с той же точки уже не выйдет. Разумный компромисс: чекпоинты промежуточных эпох чистятся по сроку, финальные веса и метрики живут вместе с кодом, а страховкой от случайного удаления служит версионирование бакета. Если объектное хранилище используется ещё и как цель для резервных копий, правила лучше развести по бакетам, чтобы retention ML-контура не задел бэкапы (про их отдельный контур — в разборе резервного копирования в облако).

Проектирование сводится к четырём решениям, принятым до первой заливки данных. Схему префиксов продумывают первой: переименовать ключ задним числом означает перезалить объекты. Датасеты версионирует DVC, артефакты и веса ведёт MLflow, отдельный слой признаков подключают под онлайн-инференс, а не «на всякий случай». Веса складывают в safetensors. Счёт держат правила жизненного цикла, которые в Object Storage удаляют объекты по сроку, а смену класса хранения команда делает явной операцией. Собранное так хранение данных для машинного обучения перестаёт быть источником сюрпризов и становится обычной инженерной подсистемой с понятными границами.
Хранение данных для машинного обучения на диске виртуальной машины упирается в две вещи сразу. Диск выделяется фиксированного размера и привязан к одной машине, поэтому при нескольких обучающих воркерах датасет начинают копировать, а копии расходятся. В объектном хранилище количество объектов в бакете не ограничено, обычной загрузкой проходит объект до 32 ГБ, составной — до 320 ТБ, а доступ идёт параллельно по HTTP. Плюс инструменты ML-стека умеют писать туда напрямую, без промежуточных копий на локальных дисках.
В формате safetensors, под ключом, который однозначно связывает веса с кодом и датасетом: например, ml-artifacts/detector/model-v2/2026-07-22/commit-abc1234/weights.safetensors. safetensors хранит сырые тензоры и JSON-заголовок, не исполняет код при загрузке и отдаёт отдельные тензоры по имени; в апреле 2026 года формат перешёл под управление PyTorch Foundation. Заливать веса удобнее через MLflow: он пишет артефакты в произвольное S3-совместимое хранилище, адрес которого задаётся переменной MLFLOW_S3_ENDPOINT_URL.
Feature store — слой доступа к признакам, который решает три задачи: расхождение признаков между обучением и продакшеном, переиспользование фич между командами и корректность выборки на момент времени; состоит из offline store и online store. Небольшой команде без онлайн-инференса он чаще не нужен: версионирование датасетов закрывает DVC, эксперименты — MLflow, а роль offline store играют Parquet-выборки в бакете. Проект Feast прямо пишет, что версионированием датасетов и управлением экспериментами он не занимается, поэтому подменять им эти слои не получится.
Наши специалисты свяжутся с вами в ближайшее время и ответят на все вопросы.

Будем держать в курсе новостей и облачных трендов




