Содержание
На склад привезли один ящик на полтонны и сказали: «это ваш набор данных, обучите модель». Подъёмник не берёт. Открыть целиком нельзя — рассыпется. Починить одну битую коробку внутри невозможно, не разобрав весь груз. Так выглядит «датасет 400 GB в одном JSON»: ноутбук падает по памяти, обучение «на всём сразу» кажется нереальным, а вопрос «сколько нужно RAM» звучит как «сколько нужно склада под весь контейнерный порт».
Настоящие конвейеры машинного обучения работают иначе. Данные режут на паллеты — шарды; внутри паллеты лежат коробки — по одной записи на строку в формате JSON Lines (JSONL); на конвейер к видеоускорителю (GPU) уезжают уже не паллеты целиком, а небольшие партии примеров — батчи. Размер всего набора и объём, который одновременно живёт в оперативной памяти или видеопамяти, — разные величины. Ниже — как устроен этот путь от одной строки до шага обучения, и где его обычно путают.
Материал продолжает серию об инженерии датасетов с акцентом на физическое устройство хранения и загрузку в обучение. Дизайн корпуса инструкций — в лонгриде про дообучение с учителем (SFT); подготовка корпуса для поиска — в главе про данные RAG. Здесь — склады, паллеты и конвейер.
Ключевые выводы
JSON — один документ. JSONL — поток независимых объектов, по одному на строку. Для больших наборов почти всегда нужен второй.
Шард — часть набора, а не «особый формат файла». Шард может быть .jsonl, .jsonl.gz, Parquet или другим контейнером.
Размер набора ≠ RAM. Терабайт можно обучать, читая шарды потоком и держа в памяти только текущий батч.
Модель не «читает JSON». Строка превращается в токены, токены собираются в батч, батч уходит на GPU. JSONL — способ хранения и обмена, не язык модели.
Для бюджета обучения важнее токены, чем гигабайты файла. Один гигабайт JSONL даёт разный объём токенов в зависимости от языка, полей и токенизатора.
JSONL удобен как формат обмена и первичной загрузки. Для колоночной аналитики и выборочного чтения полей чаще выигрывает Parquet или специализированные контейнеры вроде WebDataset.
Обучающая, проверочная и тестовая выборки — разные каталоги с собственными шардами. Одна «каша» из файлов без манифеста ломает сравнение прогонов — об этом в опорной статье серии.
Один большой JSON — как ящик, который нельзя увезти
JSON (JavaScript Object Notation) удобен: его читает человек, его понимают почти все языки, им отдают ответы API и конфигурации. Небольшой документ с объектами, массивами, строками и числами — нормальный выбор.
Проблема начинается, когда «небольшой документ» становится единственным контейнером на сотни гигабайт. Тогда это уже не файл конфигурации, а монолит:
- парсер часто хочет увидеть документ целиком или держит в памяти огромный массив;
- потоковая обработка неудобна: нельзя просто «взять следующую запись»;
- параллельная обработка упирается в один файл и блокировки;
- повреждение середины файла может сделать нечитаемым всё остальное.
На складе это тот самый ящик на полтонны: формат знакомый, логистика невозможная. Для обучения и для ETL-конвейеров нужна другая договорённость о том, как лежат примеры.
JSONL: одна строка — одна коробка
JSON Lines (JSONL; также NDJSON, Newline Delimited JSON) — соглашение: каждая строка файла — отдельный, самодостаточный JSON-объект. Между объектами нет общей обёртки-массива.
{"id":1,"text":"Привет"}
{"id":2,"text":"Как дела?"}
{"id":3,"text":"До свидания"}
Сравните с классическим JSON-массивом:
JSON: [ {...}, {...}, {...} ] ← один документ
JSONL: {...}\n{...}\n{...}\n ← поток независимых записей
Для ML это совпадает с интуицией «один пример — одна запись»: диалог для SFT, метка класса, пара «запрос — ответ». Типичная запись для набора инструкций может выглядеть так (схему полей разбираем в статье про корпус SFT):
{"messages":[{"role":"user","content":"Что такое Python?"},{"role":"assistant","content":"Python — язык программирования..."}]}
Читать такой файл можно потоком, не загружая весь набор:
import json
with open("dataset.jsonl", encoding="utf-8") as f:
for line in f:
item = json.loads(line)
process(item)
Почему это удобно на практике:
- низкий пик памяти при последовательном чтении;
- простое дописывание новых примеров в конец файла;
- фильтрация и выборка без разбора гигантского массива;
- естественная нарезка на файлы для распределённой обработки.
Это не «JSON без квадратных скобок». Это другой контракт: границы записи = границы строки, объекты независимы.
Датасет — не файл, а набор примеров
Датасет в обучении — это согласованный набор примеров под задачу, а не «файл, который скачали». Одна запись может быть текстом, парой вопрос–ответ, изображением со ссылкой, разметкой класса или списком сообщений. Объём легко уходит в миллионы записей и сотни гигабайт или терабайты.
Пока команда думает «у нас есть data.json», она думает о контейнере. Как только появляется контракт «что считается одним примером» и «как проверить качество среза», появляется продукт данных — в духе опорного лонгрида серии. Физическое устройство на диске (JSONL и шарды) обслуживает этот продукт: его можно версионировать, копировать по частям и подавать в загрузчик.
Шард — паллета, а не «формат»
Шард (shard) — часть большого набора. Когда один dataset.jsonl вырастает до сотен гигабайт, его режут:
dataset.jsonl → dataset/
(500 GB) ├── shard-00000.jsonl
├── shard-00001.jsonl
├── shard-00002.jsonl
└── ...
Все шарды вместе — это датасет. Каждая строка внутри шарда — по-прежнему один пример. Порядок записей и способ раскладки по файлам могут влиять на обучение, если вы не перемешиваете данные осознанно.
Критичное различие: JSONL — формат записи; шард — способ нарезки. Один шард может быть .jsonl, .jsonl.gz, Parquet, TFRecord или архивом WebDataset. Путать «у нас шарды» с «у нас JSONL» — всё равно что путать «паллета» с «картонная коробка».
Универсального «правильного» размера шарда нет. Встречаются десятки мегабайт, сотни мегабайт, 1–10 GB. Компромисс зависит от числа воркеров, скорости диска и сети, лимитов объектного хранилища на число объектов, удобства повторной обработки и стоимости листинга миллионов мелких файлов. Размер выбирают под конкретный конвейер, а не по магическому числу из чужого репозитория.
Зачем большие наборы режут на шарды
Паллеты на складе нужны не из любви к нумерации файлов.
Память. Не нужно держать весь набор в RAM: читаете текущий шард или даже окно внутри него, обрабатываете, освобождаете буфер.
Параллелизм. Разные процессы или ускорители могут читать разные шарды одновременно:
воркер 1 → shard-00000
воркер 2 → shard-00001
воркер 3 → shard-00002
воркер 4 → shard-00003
Хранение и перенос. Копировать, бэкапить и перезаливать удобнее по частям. Повторная очистка касается только «грязных» шардов, а не всего терабайта.
Отказоустойчивость. Повреждённый шард можно пересобрать; остальной набор остаётся рабочим.
Распределённое обучение. В схемах с параллелизмом по данным разные воркеры часто видят разные срезы потока. Конкретная раскладка зависит от фреймворка: «один шард = один GPU» — не правило, а частный случай.
Нарезка данных не равна нарезке модели. Разделение набора по шардам и параллелизм модели (слои на разных устройствах) — разные оси масштабирования.
Сжатие: когда паллету обматывают плёнкой
Текст JSONL хорошо жмётся: повторяются ключи полей, похожие конструкции, служебные слова. Часто шарды хранят как shard-00000.jsonl.gz: меньше место на диске и меньше трафик при выгрузке из объектного хранилища. Плата — CPU на распаковку и хуже случайный доступ по сравнению с некоторыми бинарными колонночными форматами.
Типичный компромисс продакшена: хранить сжатыми шарды на холодном/тёплом слое, а в горячем загрузчике решать, держать ли кэш распакованных кусков. Если узкое место — диск и сеть, сжатие почти всегда окупается. Если узкое место — уже насыщенные ядра CPU на распаковке при крошечных шардах, имеет смысл укрупнить файлы или сменить формат.
От строки на полке до шага на GPU
JSONL — исходное представление. Модель на обучении работает с числами. Упрощённый путь:
flowchart TB
raw[Сырые данные] --> clean[Очистка и фильтры]
clean --> jsonl[JSONL]
jsonl --> shards[Шарды]
shards --> loader[Data loader]
loader --> shuffle[Перемешивание]
shuffle --> tok[Токенизатор]
tok --> batch[Батчи]
batch --> gpu[GPU: forward / loss / backward]
Токен — фрагмент текста (целое слово, кусок слова, знак), которому токенизатор сопоставил номер. Модель не «читает» фразу «Привет, мир!» как человек: она видит последовательность идентификаторов. Поэтому гигабайт JSONL ≠ фиксированное число токенов: язык, служебные поля JSON, длина ответов и словарь токенизатора меняют бюджет. Для оценки стоимости и длительности прогона считают токены и шаги, а не только размер папки.
Батч — группа примеров (или усечённых последовательностей), которые обрабатываются за один шаг. Датасет — все примеры; батч — то, что сейчас на конвейере у GPU. Размер батча и длина последовательности вместе давят на видеопамять: грубо говоря, растёт произведение «сколько последовательностей × какая длина». Один шард при этом может содержать тысячи записей и много батчей — шард ≠ батч.
Эпоха — один полный проход по обучающему набору (по всем шардам train в согласованном порядке или с перемешиванием). Прочитали условно все 100 шардов — формально прошли эпоху; детали порядка зависят от загрузчика.
Сценарий «1 TB на машине с ограниченной RAM» тогда выглядит так:
1 TB набора
→ 1000 шардов × ~1 GB
→ читаем кусок шарда
→ примеры → токены → батч на GPU
→ освобождаем буфер → следующий батч
Склад по-прежнему полный. На конвейере в каждый момент — одна партия коробок, не весь порт.
Перемешивание: чтобы конвейер не кормили одной полкой подряд
Если в наборе сначала идут сто тысяч текстов про автомобили, потом про медицину, потом про код, модель за эпоху видит неестественные «сезоны». Градиенты смещаются пачками домена. Поэтому данные перемешивают: на уровне порядка шардов, буферизованного перемешивания записей или более дорогих схем полного перемешивания.
Полное перемешивание огромного набора дорого по памяти и вводу-выводу. На практике часто комбинируют: случайный порядок шардов + буфер перемешивания внутри потока. Качество перемешивания — компромисс с ресурсами, а не галочка «всегда идеально».
JSONL или Parquet: что на какой полке
| Характеристика | JSONL | Parquet |
|---|---|---|
| Читаемость человеком | высокая | низкая |
| Простота отладки | высокая | средняя |
| Потоковая обработка по записям | удобная | возможна |
| Сжатие | хорошее (особенно gzip) | обычно очень сильное |
| Колонночное чтение полей | нет | да |
| Типичная роль | обмен, первичная загрузка, выгрузки SFT | аналитика, озёра данных, выборочные колонки |
JSONL часто остаётся языком обмена: его легко открыть, починить одну строку, прогнать скриптом, отдать разметчику. Parquet выигрывает, когда нужно читать три колонки из широкой таблицы на терабайтах или плотно упаковать типизированные поля. В одном конвейере оба формата соседствуют: сырой и размеченный обмен в JSONL, витрины и признаки — в Parquet. Утверждение «JSONL оптимален для любого большого набора» ложно так же, как «Parquet всегда удобнее для ручной правки одной записи».
Как сложить каталог под свой проект
Минимальная дисциплина склада — разнести сырьё, очистку и разрезы обучения:
dataset/
├── raw/
├── cleaned/
├── deduplicated/
├── train/
│ ├── shard-00000.jsonl.gz
│ ├── shard-00001.jsonl.gz
│ └── ...
├── validation/
│ └── shard-00000.jsonl.gz
└── test/
└── ...
Каталог raw не трогают обучением. cleaned / deduplicated — воспроизводимые стадии. Каталоги train / validation / test — разные семейства с точки зрения утечек: оценочные шарды не должны «случайно» попасть в обучение. Манифест версии (хэш файлов, схема, дата) привязывают к каждому прогону — иначе сравнение метрик бессмысленно; рамка — в инженерии датасетов.
Типичный контур в эксплуатации:
сырьё → очистка → дедуп → фильтры → JSONL → шарды → сжатие
→ объектное хранилище → загрузчик → перемешивание → токенизатор → батчи → GPU
Для LLM путь тот же: интернет и документы становятся сырьём, затем JSONL и шарды, затем токены. JSONL не «формат мысли модели»; шарды не слой архитектуры сети. Это инфраструктура объёма. Когда объём и задача вообще оправдывают дообучение, а не только промпт и поиск — см. когда нужно дообучение.
Пример на миллион записей
Условный набор: 1 000 000 записей, в среднем ~5 KB на запись → порядка 5 GB до накладных расходов. Раскладка:
100 шардов × ~10 000 записей
dataset/
├── shard-00000.jsonl
├── ...
└── shard-00099.jsonl
Дальше каждый шард кормит загрузчик: записи → токены → батчи → GPU. Если батч — 32 последовательности, один шард на 10 000 записей даст сотни шагов, а не «один шаг на шард». Именно здесь ломается интуиция «файл = порция обучения».
Минимальный инструментарий на Python: потоковое чтение, запись, подсчёт строк, нарезка на N файлов, gzip, пропуск битых строк с логом. Даже маленький скрипт нарезки уже превращает монолит в паллеты:
import json
from pathlib import Path
def shard_jsonl(src: Path, out_dir: Path, rows_per_shard: int = 10_000) -> None:
out_dir.mkdir(parents=True, exist_ok=True)
shard_idx, n_in_shard = 0, 0
out = None
try:
with src.open(encoding="utf-8") as f:
for line in f:
if n_in_shard == 0:
if out:
out.close()
out = (out_dir / f"shard-{shard_idx:05d}.jsonl").open(
"w", encoding="utf-8"
)
shard_idx += 1
out.write(line if line.endswith("\n") else line + "\n")
n_in_shard = (n_in_shard + 1) % rows_per_shard
finally:
if out:
out.close()
В проде добавятся проверка схемы, манифест хэшей и запрет на «тихое» переписывание уже опубликованного шарда без новой версии.
Типичные ошибки
- «JSONL — это просто JSON без скобок». Нет: это поток независимых объектов с границей по строке; парсеры и контракты другие.
- «Шард — особый формат». Нет: это часть набора; внутри может быть что угодно из принятых контейнеров.
- «Набор 1 TB ⇒ нужно 1 TB RAM». Нет: нужен поток и бюджет под батч плюс буферы.
- «Один шард = один батч». Нет: шард обычно содержит много батчей.
- «Один шард = один GPU». Не обязательно: зависит от загрузчика и стратегии параллелизма.
- «JSONL всегда лучший формат для большого набора». Нет: для колонок и аналитики чаще берут Parquet или доменные контейнеры.
- Обучающая и тестовая выборки вперемешку в одной папке без манифеста. Получаете красивые цифры и невоспроизводимую утечку — см. рамку семейств данных в серии.
Что сделать сегодня
- Откройте свой текущий «датасет». Если это один огромный JSON или CSV — оцените стоимость нарезки в JSONL-шарды и запишите целевой размер шарда под число воркеров.
- Напишите манифест из пяти полей: число записей, число шардов, хэш списка файлов, схема одной записи, дата сборки.
- Разнесите обучающую, проверочную и тестовую выборки по каталогам, даже если проверочная пока маленькая: привычка дешевле утечки.
- Посчитайте токены на выборке из 1000 строк вашим токенизатором — сравните с гигабайтами и перестаньте планировать обучение только по размеру папки.
Часто задаваемые вопросы (FAQ)
Нужно ли всегда использовать JSONL для обучения LLM?
Нет. JSONL удобен для обмена и многих SFT-выгрузок. В претрейне и крупных корпусах встречаются собственные бинарные форматы, WebDataset, Parquet и смеси. Выбирайте по узкому месту: отладка записи, колонки, сеть, CPU распаковки.
Какой размер шарда выбрать?
Такой, чтобы число файлов не взрывало листинг хранилища, один шард обрабатывался разумное время, а воркеры не простаивали в ожидании гигантского файла. Часто начинают с сотен МБ – нескольких GB и измеряют throughput загрузчика.
Шардирование — это то же самое, что параллелизм по данным?
Нет. Шардирование — про хранение и чтение частей набора. Параллелизм по данным (data parallel) — про то, как несколько устройств считают градиенты по своим долям батча и синхронизируются. Они часто встречаются вместе, но это разные слои.
Почему нельзя судить о объёме обучения только по размеру JSONL на диске?
Потому что служебные поля, язык, шаблоны диалогов и токенизатор меняют число токенов. Два файла по 10 GB могут дать кратно разный бюджет обучения.
Куда смотреть дальше в серии?
К опорному обзору инженерии датасетов и дизайну корпуса SFT. Следующие логичные сателлиты — контракты полей записи и схемы метаданных, а также разрезы обучения/оценки и утечки.
Дальше в серии
Физическое устройство без контракта полей легко превращается в «каждый шард со своей схемой». Следующий узел очереди — схема записи, метаданные и контракты (dataset-schema-contracts-2026), затем — явные разрезы и борьба с утечками (dataset-splits-leakage-2026).



Комментарии