Почему ваш датасет тормозит обучение: проблема мелких файлов на HDD
Типичный сценарий: вы арендовали облачный инстанс с GPU, загрузили датасет из десятков тысяч аудиофайлов, изображений или видеофрагментов и запустили обучение. Ожидаете, что GPU будет загружен на 90-100%, но фактическая утилизация держится на уровне 20-30%. Причина не в коде модели и не в версии PyTorch. Узкое место - дисковая подсистема.
Жесткие диски (HDD) плохо справляются со случайным доступом к множеству мелких файлов. Стандартный DataLoader при каждом шаге запрашивает батч из нескольких десятков примеров, и если каждый пример - отдельный файл, диск вынужден постоянно перемещать головку. Результат: IOPS падает до 50-100 операций в секунду, задержки растут до десятков миллисекунд, а GPU простаивает в ожидании данных.
В нашем тестовом пайплайне на сервере с HDD и GPU A4000 скорость обучения составляла 36 шагов в минуту. CPU при этом был загружен минимально - он просто ждал, пока диск отдаст очередную порцию файлов. Проблема усугубляется на бюджетных серверах и облачных инстансах, где HDD остается стандартом из-за низкой стоимости гигабайта.
Решение - устранить случайный доступ, объединив тысячи файлов в один или несколько крупных блобов. Переход к последовательному чтению меняет картину радикально.
Решение: упаковка датасета в сжатые блобы для последовательного чтения
Концепция проста: вместо хранения каждого изображения или аудиоклипа отдельным файлом на диске, вы упаковываете весь датасет в один крупный контейнер. DataLoader читает этот контейнер последовательно, блоками по несколько мегабайт, и извлекает нужные примеры уже в памяти. HDD на последовательном чтении выдает 150-200 МБ/с - в сотни раз быстрее, чем при случайном доступе к мелким файлам.
Механизм ускорения включает три фактора. Первый: последовательное чтение использует всю пропускную способность диска. Второй: число системных вызовов (open, read, close) сокращается с тысяч до единиц на батч. Третий: сжатие блоба снижает объем данных, передаваемых с диска, и разгружает CPU - распаковка на лету часто быстрее, чем чтение несжатого файла с медленного носителя.
Результат нашего кейса: скорость обучения выросла с 36 до 47 шагов в минуту - прирост 30%. GPU перестал простаивать, утилизация поднялась до 70-80%. Эпоха на датасете из 50 тысяч примеров сократилась с 23 до 17 часов.
Форматы упаковки: TAR, Parquet и WebDataset - что выбрать
Выбор формата зависит от типа данных и фреймворка. Разберем три основных варианта.
TAR - универсальный архиватор, доступный в любой Unix-системе. Плюсы: простота создания (tar -cf dataset.tar /data), не требует дополнительных библиотек, поддерживает потоковое чтение. Минусы: не сжимает данные по умолчанию, для сжатия нужен дополнительный проход (tar -czf). Метаданные хранятся отдельно от данных - для ML-пайплайна это означает, что разбор архива требует последовательного прохода по всем записям. Подходит для прототипирования и случаев, когда датасет уже упакован в tar.gz.
Parquet - колоночный формат, оптимизированный для аналитических запросов. Плюсы: эффективное сжатие (до 5-10x на табличных данных), встроенная поддержка схемы, возможность читать только нужные колонки. Идеален для табличных датасетов и метаданных. Минусы: не предназначен для бинарных данных (изображения, аудио), требует конвертации в байтовые строки. Хороший выбор, когда датасет состоит из числовых признаков и текстовых меток.
WebDataset - формат, разработанный специально для ML-пайплайнов. Плюсы: нативная интеграция с PyTorch и TensorFlow, поддержка потокового чтения и перемешивания на уровне чанков, эффективная работа с бинарными данными. Минусы: требует установки отдельной библиотеки, менее распространен за пределами ML-сообщества. Рекомендуемый выбор для датасетов из изображений, аудио и видео.
Для нашего кейса с аудиофайлами мы выбрали WebDataset - он обеспечил наилучший баланс между скоростью чтения и удобством интеграции с PyTorch DataLoader.
Как упаковка влияет на производительность: цифры и метрики
Эксперимент проводился на сервере с Intel Xeon E5-2678 v3, 64 ГБ RAM, HDD Seagate Barracuda 4 ТБ (7200 RPM) и GPU NVIDIA A4000. Датасет: 50 тысяч аудиофайлов в формате WAV, средний размер файла - 120 КБ, общий объем - 5.7 ГБ. Модель: сверточный классификатор на базе ResNet-18, адаптированный под спектрограммы.
До упаковки: файлы хранились в директории с 50 подпапками по 1000 файлов. DataLoader с num_workers=4. Скорость: 36 шагов/мин, утилизация GPU - 25-30%, загрузка CPU - 15-20%. Время эпохи: 23 часа.
После упаковки в WebDataset: 10 шардов по 5000 примеров, сжатие zstd. DataLoader с тем же num_workers=4, но с webdataset.WebDataset в качестве источника. Скорость: 47 шагов/мин (+30.5%), утилизация GPU - 70-80%, загрузка CPU - 40-50% (из-за распаковки zstd). Время эпохи: 17 часов.
Прирост не достиг теоретического максимума по двум причинам. Первая: HDD на последовательном чтении все еще ограничивает пропускную способность - 150 МБ/с хватает для батча из 32 спектрограмм, но не более. Вторая: распаковка zstd создает нагрузку на CPU, которая при num_workers=4 начинает конкурировать с обучением. Переход на LZ4-сжатие снизил загрузку CPU до 25-30% и поднял скорость до 49 шагов/мин, но увеличил размер блоба на 15%.
Похожий подход использовала команда Hugging Face при оптимизации библиотеки datasets - их улучшения потоковой загрузки дали 100-кратное снижение числа запросов и ускорение до 10 раз для формата Parquet. Принцип тот же: устранить случайный доступ и накладные расходы на открытие файлов.
Внедрение в пайплайн: примеры кода и лучшие практики
Создание WebDataset из папки с файлами выполняется одной командой:
pip install webdataset
python -m webdataset.tar /path/to/dataset /output/dataset-%06d.tar --maxsize 1gКлючевой параметр - maxsize. Для HDD оптимальный размер шарда - 500 МБ – 2 ГБ. Меньшие шарды увеличивают накладные расходы на открытие файлов, большие - затрудняют перемешивание.
Типовая ошибка: упаковка всего датасета в один гигантский TAR-файл. Это лишает возможности эффективно перемешивать данные между эпохами. Правильный подход - разбить на шарды и использовать перемешивание на уровне шардов.
Интеграция с PyTorch и TensorFlow
Для PyTorch минимальный рабочий пример выглядит так:
import webdataset as wds
import torch
dataset = wds.WebDataset("dataset-{000000..000009}.tar")\
.shuffle(1000)\
.decode()\
.to_tuple("input.pth", "target.pth")\
.batched(32)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=None, num_workers=4)
for batch in dataloader:
inputs, targets = batch
# обучениеМетод .shuffle(1000) задает размер буфера перемешивания - 1000 примеров. Это компромисс между качеством перемешивания и потреблением памяти. Для датасетов с сильной корреляцией соседних примеров (например, видео по кадрам) увеличьте буфер до 5000-10000.
Для TensorFlow используйте генератор, читающий TAR-архивы:
import tensorflow as tf
import tarfile
import io
def tar_generator(tar_path):
with tarfile.open(tar_path, 'r|*') as tar:
for member in tar:
if member.isfile():
f = tar.extractfile(member)
yield tf.io.decode_raw(f.read(), tf.float32)
dataset = tf.data.Dataset.from_generator(
tar_generator,
args=['dataset.tar'],
output_signature=tf.TensorSpec(shape=(None,), dtype=tf.float32)
).batch(32).prefetch(tf.data.AUTOTUNE)Производительность TensorFlow-варианта на 10-15% ниже, чем у WebDataset с PyTorch, из-за накладных расходов на Python-генератор. Для production-пайплайнов на TensorFlow рассмотрите формат TFRecord - он оптимизирован под tf.data.Dataset и дает сравнимую с WebDataset скорость.
При работе с большими моделями и датасетами, которые не помещаются на локальный диск, стоит обратить внимание на архитектурные изменения в huggingface_hub v1.0 - HTTP/Xet протокол позволяет загружать только нужные части больших файлов без полного скачивания.
Альтернативы для крупных датасетов: постоянное сетевое хранилище в облаке
Упаковка решает проблему случайного доступа, но не устраняет другую боль облачных ML-пайплайнов: повторную загрузку данных при каждом запуске GPU-инстанса. Spot-инстансы дешевы, но эфемерны - после прерывания локальный диск очищается, и датасет приходится скачивать заново. На 100 ГБ данных это занимает 20-40 минут даже при хорошем канале.
Решение: постоянное сетевое хранилище (Network Volume), которое монтируется к GPU-инстансу и сохраняет данные между запусками. Runpod предоставляет такую возможность из коробки.
Настройка Network Volume в Runpod для ML-пайплайна
Шаги для настройки:
- Создайте Network Volume в консоли Runpod - выберите регион (желательно тот же, где запускаются GPU-поды) и размер (минимум в 1.5 раза больше датасета).
- Загрузите упакованный датасет на том через SCP или используя встроенный uploader.
- При создании пода укажите монтирование тома: вкладка Storage → Add Network Volume → выберите созданный том и путь монтирования, например /workspace/data.
- В коде обучения указывайте путь /workspace/data/dataset-{000000..000009}.tar.
Пример конфигурации пода в Runpod: GPU A4000, 16 vCPU, 64 ГБ RAM, Network Volume 250 ГБ SSD. Стоимость хранения - $0.07/ГБ/месяц, то есть том на 250 ГБ обходится в $17.5/мес. При ежедневных тренировках это окупается за счет экономии времени на загрузку данных.
Ограничения: скорость чтения с Network Volume зависит от сети внутри датацентра. В Runpod она составляет 500-800 МБ/с для SSD-томов, что сопоставимо с локальным NVMe. Однако при пиковых нагрузках в датацентре скорость может падать до 200 МБ/с. Для страховки используйте кэширование на локальном диске пода - скопируйте шарды текущей эпохи перед началом обучения.
Альтернативы: AWS EFS (дороже, но эластичнее), GCP Filestore (интегрирован с Vertex AI), или собственное решение на базе NFS-сервера. Runpod выигрывает по соотношению цена/простота для небольших команд и индивидуальных ML-инженеров.
Когда упаковка не нужна: ограничения и подводные камни
Метод дает максимальный эффект на HDD с множеством мелких файлов. В остальных случаях выигрыш может быть незначительным или отсутствовать.
SSD и NVMe-диски имеют IOPS на уровне 100-500 тысяч - случайный доступ к мелким файлам для них не проблема. На NVMe разница между чтением отдельных файлов и блоба составляет 5-10%, что сопоставимо с погрешностью измерений. Если ваш датасет уже на SSD, упаковка не окупает затраченных усилий.
Очень большие файлы - видео высокого разрешения, медицинские снимки в DICOM, облака точек LiDAR - не выигрывают от объединения. Один файл размером 500 МБ читается так же быстро, как фрагмент блоба в 500 МБ. Более того, упаковка таких файлов создает проблемы: для извлечения одного примера приходится распаковывать весь блоб или реализовывать сложную индексацию.
Часто обновляемые датасеты требуют переупаковки при каждом изменении. Если вы ежедневно добавляете новые примеры, накладные расходы на пересоздание блобов съедят весь выигрыш от ускорения чтения. В этом сценарии рассмотрите объектное хранилище (S3) с последовательным чтением диапазонов байтов.
Альтернативы для специфических случаев: RAM-диск для небольших датасетов (до 50 ГБ), распределенные файловые системы (Lustre, BeeGFS) для кластеров, или формат Lance - современная альтернатива Parquet с поддержкой произвольного доступа и векторного поиска.
Интересный пример оптимизации на уровне данных - перенос подготовки табличных данных на GPU. Там узкое место не ввод-вывод, а трансформации на CPU, и ускорение достигается за счет cuDF и Polars GPU Engine.
Выводы: стоит ли упаковывать ваш датасет
Упаковка мелких файлов в сжатые блобы дает прирост скорости обучения на 30% при работе с HDD. Ключевой механизм - замена случайного доступа последовательным чтением, что устраняет главное узкое место в пайплайне.
Чек-лист для принятия решения:
- У вас тысячи или десятки тысяч мелких файлов (менее 1 МБ каждый) и HDD - упаковывайте в WebDataset, это даст максимальный прирост.
- Вы работаете в облаке с эфемерными инстансами - добавьте Network Volume (Runpod или аналог), чтобы не скачивать датасет заново при каждом запуске.
- Ваш датасет уже на SSD или NVMe - выигрыш будет минимальным, усилия не оправданы.
- Файлы большие (видео, DICOM) и их немного - упаковка не нужна, используйте прямое чтение.
- Датасет часто обновляется - оцените накладные расходы на переупаковку, возможно, S3-хранилище будет удобнее.
Финальная рекомендация: проведите A/B-тест на своем датасете. Упакуйте 10% данных в WebDataset, замерьте скорость обучения и сравните с базовым вариантом. Это займет час-два и даст точный ответ для вашего конкретного случая, а не усредненную оценку из статьи.
Системная оптимизация пайплайна - от хранения данных до выбора модели - дает кумулятивный эффект. Опыт Databricks показывает, что выбор правильной обвязки (harness) для кодинга влияет на итоговую стоимость не меньше, чем выбор модели. Тот же принцип применим к хранению данных: правильный формат и способ доступа экономят часы обучения и десятки долларов на облачных ресурсах.