TokForge API

v0.1.0 HTTP :8787 проверка…

1Обзор

TokForge — config-driven corpus tokenization and shard distribution framework. Сервер скачивает source-файлы датасетов из HuggingFace, токенизирует их токенизатором HuggingFaceTB/SmolLM2-1.7B, пишет raw-шарды uint16 без NPY-заголовка, заливает в HF bucket domofon/datasets, ведёт реестр (корпуса, версии, шарды, source-диапазоны) и выдаёт Modal-аккаунтам персональные наборы шардов (allocation) с lease/TTL, отчётами о consumption и возвратом неиспользованного в пул.

Публичная документация: Swagger UI · ReDoc · OpenAPI JSON: /openapi.json. Все страницы доступны по HTTP без авторизации; все API-запросы, кроме /healthz, требуют заголовок Authorization: Bearer <TOKFORGE_API_KEY>.

ПараметрЗначение
Base URL (внешний)http://193.148.56.44:8787
Base URL (локальный)http://127.0.0.1:8787
API key по умолчаниюtokforge-local-7f3a9c21d4e6
Bucketdomofon/datasets (публичный)
Прямые ссылки на файлы buckethttps://huggingface.co/buckets/domofon/datasets/resolve/<path> (GET 200, Range 206)
ТокенизаторHuggingFaceTB/SmolLM2-1.7B, vocab 49152, tokenizer_sha e6d44ded9564218b, max id 49151 < 65536
Шард по умолчанию125 000 000 токенов (~250MB), raw uint16, size == tokens*2

2Быстрый старт

# 1. health (без auth)
curl -s http://193.148.56.44:8787/healthz
# → {"ok":true,"db":true,"bucket":true}

# 2. авторизованный запрос
curl -s -H 'Authorization: Bearer tokforge-local-7f3a9c21d4e6' \
  http://193.148.56.44:8787/api/v1/jobs

# 3. создать run
curl -s -H 'Authorization: Bearer tokforge-local-7f3a9c21d4e6' -H 'Content-Type: application/json' \
  -X POST http://193.148.56.44:8787/api/v1/runs -d '{"config":{...run-конфиг...}}'

# 4. создать job (тело оборачивается в {"config": ...})
curl -s -H 'Authorization: Bearer tokforge-local-7f3a9c21d4e6' -H 'Content-Type: application/json' \
  -X POST http://193.148.56.44:8787/api/v1/jobs -d '{"config":{...job-конфиг...}}'

3Формат ошибок

Все ошибки — JSON вида:

{"error": {"code": "not_enough_shards", "message": "недостаточно свободных шардов для allocation", "details": {"available_tokens": 200031}}}
HTTPcodeСитуация
400invalid_job_config, invalid_run_config, requested_tokens_too_small/largeсхема не прошла Pydantic-валидацию; диапазоны, prefix, лимиты
401unauthorizedнет/неверный Bearer-токен
403account_mismatchaccount в запросе не совпадает с владельцем allocation
404job_not_found, run_not_found, allocation_not_found, corpus_not_foundобъект не найден
409job_exists, run_exists, job_status_conflict, not_enough_shards, allocation_expired, allocation_status_conflict, consumption_conflict, allocation_renew_failedконфликт состояния/ресурсов
422ошибка валидации тела запроса FastAPI (стандартный detail)
502hf_unavailableHF Hub недоступен (live-справочник)
500internal_errorнеобработанное исключение

4Jobs — токенизация датасетов

Жизненный цикл job: validating (POST) → префлайт worker'ом (токенизатор, source-range, overlap, prefix) → queuedrunning (CAS) → done / failed / cancelled. Один активный job; остальные — FIFO по created_at. Heartbeat каждые 10 с; если старше 10 минут — API помечает failed (code worker_heartbeat_lost).

POST /api/v1/jobs
Быстрая схема-валидация (Pydantic), job кладётся в validating. Тяжёлый префлайт делает worker. Ответ 202. Поддержан idempotency_key.
// запрос: тело = {"config": {...}, "idempotency_key": "opt"}
{"config": {
  "schema": "tokforge.job/v1", "job_id": "cosmo2-v5", "corpus": "cosmo2",
  "labels": ["pretrain", "synthetic", "en"],
  "source": {"type": "hf_dataset", "dataset": "HuggingFaceTB/smollm-corpus",
             "config": "cosmopedia-v2", "split": "train", "text_field": "text",
             "files": {"from": 75, "to": 90}},
  "tokenizer": {"repo_id": "HuggingFaceTB/SmolLM2-1.7B", "revision": null},
  "output": {"format": "npy-raw", "dtype": "uint16", "shard_tokens": 125000000,
             "bucket": "domofon/datasets", "prefix": "cosmo2-v5", "version": "v5",
             "tags": {"license": "Apache-2.0"}},
  "limits": {"target_tokens": 6000000000, "max_source_files": 20},
  "options": {"workers": 10, "batch_size": 2048, "chunk_size": 512}
}}

// ответ 202
{"job_id":"cosmo2-v5","status":"validating","queued_at":"2026-08-15T22:00:00Z"}
GET /api/v1/jobs · GET /api/v1/jobs/{job_id}
Список / детальный статус с прогрессом (phase, files_done, shards_sealed, tokens_written, worker_heartbeat_at) и полным конфигом.
POST /api/v1/jobs/{job_id}/cancel
Разрешён из validating/queued/running (CAS). Worker останавливается между файлами/шардами. Временные файлы НЕ удаляются.
POST /api/v1/jobs/{job_id}/retry
Только из failed/cancelled. attempt += 1, статус queued, прогресс сохраняется (resume идемпотентен: чекпоинт после каждого файла и шарда, загруженные шарды не дублируются).
Тонкости префлайта: файлы фильтруются по config (§5.3: /config/, -config-, prefix config/; если config-фильтр пуст — берутся все parquet) и по split (basename split-…); диапазон [from, to) не должен пересекаться с завершёнными job'ами того же (dataset, config, split); prefix должен быть свободен в bucket и в БД; vocab токенизатора обязан помещаться в uint16 (< 65536); tokenizer_sha фиксируется в progress.

5Runs — политика аллокаций

POST /api/v1/runs
Создать run (тело {"config": {…}}). Ответ {"run_id":"domofon-v5","status":"active"}.
GET /api/v1/runs · GET /api/v1/runs/{run_id}
Список / детали + статистика: allocations_total, tokens_granted, tokens_consumed.
PATCH /api/v1/runs/{run_id}
Тело {"patch": {…}}; можно менять default_requested_tokens, lease_ttl_s, allow_partial_reissue. Применяется к НОВЫМ allocation.
// run-конфиг (examples/run_domofon_v5.json)
{"schema": "tokforge.run/v1", "run_id": "domofon-v5", "description": "Domofon-1B v5 rotation",
 "allocation": {
   "default_requested_tokens": 1500000000, "min_requested_tokens": 125000000,
   "max_requested_tokens": 5000000000, "strategy": "balanced",
   "groups": ["cosmo2-v5", "fineweb-edu-v5", "finemath-v5", "kaggle-v3", "chatml-v2"],
   "lease_ttl_s": 21600, "renew_before_s": 3600, "allow_partial_reissue": false}}

6Allocations — персональные наборы шардов

Жизненный цикл: reserved (создана, TTL пошёл) → active (activate при старте train, TTL продлевается) → отчёты fully_consumed/partialcompleted (complete) / released (release) / expired (TTL истёк, фоновый sweeper раз в 60 с). Один шард не может быть в двух активных allocation. Неиспользованные шарды возвращаются в пул (available), частично использованные остаются partial и автоматически не выдаются.

POST /api/v1/allocations
Выбор шардов: strategy balanced — round-robin по группам из groups, внутри группы по возрастанию idx, перебор на один шард допускается; выбор и резервирование — в одной SQLite-транзакции (BEGIN IMMEDIATE). requested_tokens = override или default_requested_tokens, проверка min/max. При нехватке — 409 not_enough_shards с available_tokens.
// запрос
{"run_id":"domofon-v5","client":"modal","account":"hexecaci65",
 "attempt_id":"run5-att141","requested_tokens_override":1500000000,
 "idempotency_key":"run5-att141-prepare-187800"}

// ответ 201
{"allocation_id":"alloc_9f2c11ab","status":"reserved","expires_at":"2026-08-16T01:00:00Z",
 "granted_tokens":1500000000,
 "shards":[{"path":"cosmo2-v5/data-00000-of-00008.npy",
   "url":"https://huggingface.co/buckets/domofon/datasets/resolve/cosmo2-v5/data-00000-of-00008.npy",
   "tokens":125000000,"bytes":250000000,"sha256":"abc…"}],
 "dataset_selection":{"corpora":[{"group":"cosmo2-v5","shards":{"list":[0,1,2,3]}}],
   "order":"interleave_default"},
 "fingerprint":"ds1:1a2b3c4d5e6f7081"}
dataset_selection всегда использует shards: {"list":[…]} (явный персональный список, не range/all), совместим с kd/dataset_resolve.py. fingerprint = ds1: + sha256(canon), canon = строки path\tbytes по всем шардам, отсортированным по path.
POST /api/v1/allocations/{id}/activate
Тело {"account":…,"attempt_id":…}. reserved→active, expires_at = now + lease_ttl_s. Чужой account → 403.
POST /api/v1/allocations/{id}/renew
Продлевает TTL от текущего момента. Если уже expired — 409 allocation_expired.
POST /api/v1/allocations/{id}/consumption
1..N событий: fully_consumed → шард consumed навсегда; partial → шард partial с partial_tokens, в пул автоматически не выдаётся. Для каждого события пишется shard_events. Шард обязан принадлежать allocation, иначе 409.
{"account":"hexecaci65","attempt_id":"run5-att141","reported_at":"2026-08-15T22:00:00Z",
 "events":[{"path":"cosmo2-v5/data-00000-of-00008.npy","event":"fully_consumed",
   "tokens_consumed":125000000,"step_from":187800,"step_to":188300,
   "meta":{"source":"source_mix_callback"}},
  {"path":"cosmo2-v5/data-00001-of-00008.npy","event":"partial",
   "tokens_consumed":30000000,"step_from":188300,"step_to":188500}]}
POST /api/v1/allocations/{id}/complete · POST /api/v1/allocations/{id}/release
complete: не тронутые reserved/active шарды → available, allocation → completed, история в shard_events. release — то же, но статус released (для «упал до старта», steps_completed=0).
GET /api/v1/allocations/{id}
Полное состояние: шарды с url/state/tokens_consumed, dataset_selection, fingerprint, статусы и таймстампы.
Idempotency: поле idempotency_key в теле (jobs/allocations) — повторный запрос возвращает сохранённый ответ (24 часа, таблица idempotency).

7Inventory — реестр

GET /api/v1/corpora · GET /api/v1/corpora/{corpus}
Корпуса с labels и versions; детализация: prefix, status (complete/in_progress/error), shards, tokens, available_tokens.
GET /api/v1/shards?prefix=&status=
Шарды с path/url/tokens/docs/bytes/sha256/status/job_id/idx/total/partial_tokens. Фильтры необязательны.
GET /api/v1/inventory?dataset=&config=
Live-справочник source-датасета: total_source_files, consumed_ranges, free_files, cached/cached_at (кэш 5 минут).
POST /api/v1/admin/reconcile
Сверка БД и bucket: .npy без записи в БД (с sidecar'ом) регистрируются; шарды БД без файла в bucket → error. Ничего не удаляет и не перезаписывает.
GET /api/v1/admin/stats
Счётчики jobs/shards по статусам, tokens_total/available/consumed, runs, allocations.

8Форматы данных

8.1 .npy-raw (шард)

dtype=uint16, БЕЗ NPY-заголовка: первый байт файла — первый токен. Инвариант после seal: size == tokens * 2. Читается np.memmap(path, dtype=np.uint16). Каждый документ кодируется целиком (add_special_tokens=False, BOS не добавляется), после каждого документа — trailing EOS id 0. Максимальный id < 65536. Финальные имена data-00000-of-NNNNN.npy присваиваются только после завершения токенизации всего job (total всегда фактический).

8.2 Sidecar <prefix>/_meta/<shard>.json

{"engine":"tokforge","engine_version":"0.1.0","shard":"data-00000-of-00008.npy",
 "path":"cosmo2-v5/data-00000-of-00008.npy","format":"npy-raw","dtype":"uint16",
 "num_docs":173000,"tokens":125000000,"size":250000000,"sha256":"abc…",
 "eos_token_id":0,"max_token_id":49151,"tokenizer":"HuggingFaceTB/SmolLM2-1.7B",
 "tokenizer_revision":null,"tokenizer_sha":"e6d44ded9564218b",
 "corpus":"cosmo2","version":"v5","labels":["pretrain","synthetic","en"],
 "source_range":[75,90],"license":"Apache-2.0"}

8.3 Агрегаты

<prefix>/_aggregate.json: engine, prefix, corpus, version, shards, docs, tokens, bytes, files[], source_range, source_dataset, source_config, created_at. <prefix>/_tags.json: corpus, version, labels, status=complete, license, created_by. tokforge/registry.json в корне bucket — read-only снимок SQLite (корпуса, shards_total, tokens_total, updated_at). Источник истины — SQLite; bucket-файлы — артефакты.

9CLI

tokforge submit examples/job_cosmo2_v5.json    # отправить job
 tokforge jobs | job <job_id> | cancel <job_id> | retry <job_id>
 tokforge inventory [--dataset D] [--config C]
 tokforge shards [--prefix X] [--status S]
 tokforge runs [--create run.json]
 tokforge allocate --run domofon-v5 --account acct1 --attempt-id att1 [--requested-tokens N]
 tokforge worker | reconcile | stats

Берёт TOKFORGE_BASE_URL/TOKFORGE_API_KEY из окружения или из configs/server.json. Установка: ./venv/bin/pip install -e /srv/tokforge.

10Деплой и доступ

Сервисы запущены в tmux (systemd в контейнере degraded; unit-файлы готовы в deploy/):

tmux new-session -d -s tokforge-api 'cd /srv/tokforge && ./venv/bin/uvicorn tokforge.api.app:app --host 0.0.0.0 --port 8787'
tmux new-session -d -s tokforge-worker 'cd /srv/tokforge && ./venv/bin/python -m tokforge.worker'

Слушает 0.0.0.0:8787; фаервол не блокирует (ufw inactive, iptables ACCEPT). Проверено через публичный IP: http://193.148.56.44:8787/healthz → 200. Без TLS.

11Правила безопасности и инварианты

12Статус сборки