← Все статьи

JSONL, шарды и путь данных к GPU: от одной строки до обучения

Как устроены большие наборы для обучения: JSONL против JSON, шарды, сжатие, токены, батчи и эпохи — и почему терабайт данных не требует терабайта RAM.

JSONL, шарды и путь данных к GPU: от одной строки до обучения
Содержание

На склад привезли один ящик на полтонны и сказали: «это ваш набор данных, обучите модель». Подъёмник не берёт. Открыть целиком нельзя — рассыпется. Починить одну битую коробку внутри невозможно, не разобрав весь груз. Так выглядит «датасет 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()

В проде добавятся проверка схемы, манифест хэшей и запрет на «тихое» переписывание уже опубликованного шарда без новой версии.

Типичные ошибки

  1. «JSONL — это просто JSON без скобок». Нет: это поток независимых объектов с границей по строке; парсеры и контракты другие.
  2. «Шард — особый формат». Нет: это часть набора; внутри может быть что угодно из принятых контейнеров.
  3. «Набор 1 TB ⇒ нужно 1 TB RAM». Нет: нужен поток и бюджет под батч плюс буферы.
  4. «Один шард = один батч». Нет: шард обычно содержит много батчей.
  5. «Один шард = один GPU». Не обязательно: зависит от загрузчика и стратегии параллелизма.
  6. «JSONL всегда лучший формат для большого набора». Нет: для колонок и аналитики чаще берут Parquet или доменные контейнеры.
  7. Обучающая и тестовая выборки вперемешку в одной папке без манифеста. Получаете красивые цифры и невоспроизводимую утечку — см. рамку семейств данных в серии.

Что сделать сегодня

  1. Откройте свой текущий «датасет». Если это один огромный JSON или CSV — оцените стоимость нарезки в JSONL-шарды и запишите целевой размер шарда под число воркеров.
  2. Напишите манифест из пяти полей: число записей, число шардов, хэш списка файлов, схема одной записи, дата сборки.
  3. Разнесите обучающую, проверочную и тестовую выборки по каталогам, даже если проверочная пока маленькая: привычка дешевле утечки.
  4. Посчитайте токены на выборке из 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).

Комментарии

Загрузка комментариев…