Перейти к содержимому
TensorBase
en

docs / storage / chunked-transfer · API v1 · обновлено 2026-10-12

Чанковая передача файлов .safetensors

На этой странице описано, как TensorBase хранит, принимает и отдаёт многогигабайтные файлы весов, и как обращаться к эндпоинту /media/chunks напрямую, когда CLI использовать нельзя (собственные загрузчики, зеркала в изолированных контурах, скрипты прогрева инференса).

Обзор

Ревизия репозитория — это неизменяемый манифест: список файлов, каждый из которых описан размером, SHA-256 и упорядоченным списком хэшей чанков. На уровне хранилища байты файлов никогда не адресуются по пути — только по хэшу чанка. Это даёт три свойства, важных для чекпойнтов размером 10–500 ГБ:

  • Дедупликация. При дообучении, меняющем только слои со слитым LoRA, повторно загружаются лишь изменившиеся чанки.
  • Докачка. Передача — это набор независимых запросов чанков; обрыв соединения стоит не больше одного чанка.
  • Проверяемость. Клиент сверяет каждый чанк с его хэшем перед записью, а затем весь файл — с манифестом.

Аутентификация

Каждый запрос передаёт токен рабочего пространства в заголовке Authorization. Токены создаются в разделе Настройки → Токены доступа и имеют область действия: токены read позволяют только скачивать, токены write — также создавать загрузки и коммиты. В корпоративных пространствах токены выдаются только пользователям, вошедшим через SSO (SAML 2.0 / OIDC).

shell
export TB_ENDPOINT="https://<your-workspace-host>"   # см. Настройки → API
export TB_TOKEN="tb_live_…"                           # не коммитьте это значение

Модель хранения

Файлы режутся на чанки фиксированного размера 64 МиБ (67 108 864 байта); последний чанк файла обычно короче. Чанк идентифицируется SHA-256 своих байтов в шестнадцатеричной записи в нижнем регистре.

manifest.json (фрагмент)GET /api/v1/repos/{repo}/manifest
{
  "repo": "tb-labs/RuVision-7B-Instruct",
  "revision": "a91f3c0d7e1b4f62",
  "chunk_size": 67108864,
  "files": [
    {
      "path": "model-00001-of-00003.safetensors",
      "size": 4983451184,
      "sha256": "0f6c2d…e91a",
      "chunks": ["5b9e04…71cd", "c013aa…0b42", "…"]      // 75 хэшей
    }
  ]
}

Загрузка

  1. Разделите каждый файл на чанки по 64 МиБ и вычислите SHA-256 каждого чанка.
  2. POST /api/v1/repos/{repo}/uploads с путём файла, размером и упорядоченным списком хэшей. В ответе — upload_id и подмножество хэшей, которых на сервере ещё нет (missing).
  3. Для каждого отсутствующего хэша выполните PUT /media/chunks/{sha256} с сырыми байтами чанка и заголовком X-TB-Upload-Id. Сервер хэширует тело и отклоняет его с 400 chunk_digest_mismatch, если хэш не совпадает с URL. Запросы независимы и могут выполняться параллельно (до 16 на токен).
  4. POST /api/v1/repos/{repo}/uploads/{upload_id}/commit. Сервер проверяет наличие всех чанков и создаёт новую ревизию. Сессия загрузки истекает после 24 часов бездействия; загруженные чанки хранятся 7 дней и переиспользуются следующей сессией.

PUT для уже существующего чанка возвращает 200 OK, не дочитывая тело до конца, поэтому повтор всей загрузки «вслепую» обходится дёшево.

Скачивание

  1. Получите манифест ветки, тега или хэша ревизии: GET /api/v1/repos/{repo}/manifest?revision=main.
  2. Заранее выделите целевой файл размером size байт.
  3. Для каждого чанка i выполните GET /media/chunks/{sha256} и запишите тело по смещению i × chunk_size. Ответы содержат ETag: "<sha256>" и Cache-Control: immutable и поддерживают Range для докачки частично полученного чанка (206 Partial Content).
  4. Проверяйте каждый чанк по хэшу перед записью, а в конце — весь файл по sha256.

Большие ответы могут отдаваться через 307 Temporary Redirect на ближайший узел хранения с подписанным URL, действующим 15 минут. Клиент обязан следовать редиректам и не должен передавать заголовок Authorization на адрес редиректа.

Чтение метаданных тензоров без скачивания весов

Файл .safetensors начинается с 8-байтового целого N в little-endian, за которым следует JSON-заголовок длиной N байт с именами тензоров, типами, формами и смещениями. Поскольку заголовок лежит в первом чанке, для просмотра 30-гигабайтного чекпойнта достаточно двух Range-запросов:

bash
FIRST=$(curl -s -H "Authorization: Bearer $TB_TOKEN" \
  "$TB_ENDPOINT/api/v1/repos/tb-labs/RuVision-7B-Instruct/manifest" | jq -r '.files[0].chunks[0]')

N=$(curl -s -L -r 0-7 -H "Authorization: Bearer $TB_TOKEN" "$TB_ENDPOINT/media/chunks/$FIRST" \
  | od -An -t u8 --endian=little | tr -d ' ')

curl -s -L -r 8-$((8 + N - 1)) -H "Authorization: Bearer $TB_TOKEN" \
  "$TB_ENDPOINT/media/chunks/$FIRST" | jq 'del(."__metadata__") | to_entries[:3]'

Пример на Python

Фрагмент ниже использует только requests и стандартную библиотеку. Это эталонная реализация протокола; в продакшене используйте tensorbase-cli — в нём есть повторы с экспоненциальной задержкой, индикатор прогресса и локальный кэш чанков.

tb_transfer.py
import hashlib
import os
from concurrent.futures import ThreadPoolExecutor

import requests

ENDPOINT = os.environ["TB_ENDPOINT"]
CHUNK_SIZE = 64 * 1024 * 1024

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['TB_TOKEN']}"


def read_chunk(path, index):
    with open(path, "rb") as f:
        f.seek(index * CHUNK_SIZE)
        return f.read(CHUNK_SIZE)


def chunk_digests(path):
    digests = []
    with open(path, "rb") as f:
        while block := f.read(CHUNK_SIZE):
            digests.append(hashlib.sha256(block).hexdigest())
    return digests


def upload(path, repo, target_path):
    digests = chunk_digests(path)
    r = session.post(f"{ENDPOINT}/api/v1/repos/{repo}/uploads", json={
        "path": target_path,
        "size": os.path.getsize(path),
        "chunks": digests,
    })
    r.raise_for_status()
    plan = r.json()
    missing = set(plan["missing"])
    # первый индекс каждого отсутствующего хэша (одинаковые чанки загружаются один раз)
    todo = {d: i for i, d in reversed(list(enumerate(digests))) if d in missing}

    def put(item):
        digest, index = item
        resp = session.put(
            f"{ENDPOINT}/media/chunks/{digest}",
            data=read_chunk(path, index),
            headers={"Content-Type": "application/octet-stream",
                     "X-TB-Upload-Id": plan["upload_id"]},
            timeout=300,
        )
        resp.raise_for_status()

    with ThreadPoolExecutor(max_workers=8) as pool:
        list(pool.map(put, todo.items()))

    commit = session.post(f"{ENDPOINT}/api/v1/repos/{repo}/uploads/{plan['upload_id']}/commit")
    commit.raise_for_status()
    return commit.json()["revision"]


def download(repo, file_path, dest, revision="main"):
    r = session.get(f"{ENDPOINT}/api/v1/repos/{repo}/manifest", params={"revision": revision})
    r.raise_for_status()
    manifest = r.json()
    entry = next(f for f in manifest["files"] if f["path"] == file_path)
    size = manifest["chunk_size"]

    with open(dest, "ab") as f:          # выделяем место, сохраняя уже скачанное
        f.truncate(entry["size"])

    def fetch(item):
        index, digest = item
        with open(dest, "r+b") as f:
            f.seek(index * size)
            if hashlib.sha256(f.read(size)).hexdigest() == digest:
                return                          # уже скачан: докачка
        resp = session.get(f"{ENDPOINT}/media/chunks/{digest}", timeout=300)
        resp.raise_for_status()
        if hashlib.sha256(resp.content).hexdigest() != digest:
            raise IOError(f"chunk {index}: digest mismatch")
        with open(dest, "r+b") as f:
            f.seek(index * size)
            f.write(resp.content)

    with ThreadPoolExecutor(max_workers=8) as pool:
        list(pool.map(fetch, enumerate(entry["chunks"])))

    h = hashlib.sha256()
    with open(dest, "rb") as f:
        while block := f.read(CHUNK_SIZE):
            h.update(block)
    if h.hexdigest() != entry["sha256"]:
        raise IOError(f"{file_path}: file digest mismatch")


if __name__ == "__main__":
    download("tb-labs/RuVision-7B-Instruct",
             "model-00001-of-00003.safetensors",
             "model-00001-of-00003.safetensors")

Пример на curl

Полезно для отладки и в окружениях без Python. $SHA — хэш одного чанка, $UPLOAD_ID берётся из ответа на шаг 2 раздела Загрузка.

bash
# 1. Есть ли этот чанк на сервере? (200 — да, 404 — нужно загрузить)
curl -I -H "Authorization: Bearer $TB_TOKEN" "$TB_ENDPOINT/media/chunks/$SHA"

# 2. Загрузка чанка №0 файла (байты 0 … 64 МиБ − 1)
dd if=model-00001-of-00003.safetensors bs=64M skip=0 count=1 status=none \
  | curl --fail -X PUT --data-binary @- \
      -H "Authorization: Bearer $TB_TOKEN" \
      -H "Content-Type: application/octet-stream" \
      -H "X-TB-Upload-Id: $UPLOAD_ID" \
      "$TB_ENDPOINT/media/chunks/$SHA"

# 3. Скачивание чанка; -C - докачивает прерванную передачу через Range
curl --fail -L -C - -o "chunks/$SHA" \
  -H "Authorization: Bearer $TB_TOKEN" \
  "$TB_ENDPOINT/media/chunks/$SHA"

# 4. Проверка
echo "$SHA  chunks/$SHA" | sha256sum -c -

Лимиты

ПараметрЗначение
Размер чанка64 MiB (фиксированный; последний чанк может быть короче)
Максимальный размер файла5 TiB (81 920 чанков)
Параллельные запросы чанков16 на токен, 64 на пространство (Business: 256)
Время жизни сессии загрузки24 ч бездействия
Срок действия подписанного URL15 мин
Частота запросов к API600 запросов/мин на токен (передача чанков не учитывается)

Ошибки

Ошибки возвращаются в JSON: {"error": "chunk_not_found", "message": "…", "request_id": "…"}. При обращении в поддержку указывайте request_id.

СтатусКодЗначение
400chunk_digest_mismatchХэш тела не совпадает с хэшем в URL. Перечитайте чанк с диска и повторите.
401invalid_tokenТокен отсутствует, истёк или отозван.
403insufficient_scopeТокен read использован для записи или репозиторий вне области действия токена.
404chunk_not_foundЧанк не хранится (или не виден этому пространству).
409upload_expiredСессия загрузки истекла. Начните новую — уже загруженные чанки будут переиспользованы.
413chunk_too_largeТело больше 64 МиБ.
416range_not_satisfiableЗапрошенный диапазон выходит за пределы чанка.
429rate_limitedСлишком много параллельных запросов. Подождите Retry-After секунд.
507quota_exceededДостигнута квота хранилища пространства. См. тарифы и дополнительные услуги.

Что-то непонятно или не хватает информации?

Написать в поддержку