← Усі статті

JSONL, шарди і шлях даних до GPU: від одного рядка до навчання

Як влаштовані великі набори для навчання: JSON проти JSONL, шарди, стиснення, токени, батчі й епохи — і чому терабайт даних не потребує терабайта 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     ← потік незалежних записів

Для машинного навчання це збігається з інтуїцією «один приклад — один запис»: діалог для 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).

Коментарі

Завантаження коментарів…