Целостность чекпойнта
Laya загружает веса модели из Hugging Face Hub во время загрузки. По умолчанию берётся то, на что указывает ревизия репозитория по умолчанию, — это удобно и совпадает с тем, что уже содержит офлайн-кэш. Если вы предпочитаете закрепить проверенный коммит или отказаться загружать чекпойнт, байты которого изменились, доступны оба варианта, и оба включаются по желанию.
Ничто здесь не меняет то, что загружает Laya, пока вы об этом не попросите, так что добавлять эти
параметры в существующее развёртывание безопасно. Оба варианта действуют на уровне библиотеки:
дайджесты попадают на HTTP-сервер через переменную окружения, а закрепление ревизии — через
LAYA_REVISION. См. Закрепление ревизии на сервере.
См. также: Docker о переменных развёртывания, laya.load и Agent
и Router.
Закрепить ревизию
Передайте revision в любой загрузчик. Он принимает SHA коммита, ветку или тег и пересылается в Hub.
import laya
agent = laya.load("convaiinnovations/laya", revision="<commit sha, branch, or tag>")
print(agent.revision) # what the download resolved to
Отдавайте предпочтение проверенным SHA, которые поставляются с Laya, а не собственному литералу: они обновляются вместе с чекпойнтами, поэтому такая форма не может устареть.
from laya import PINNED_REVISIONS
agent = laya.load(
"convaiinnovations/laya",
revision=PINNED_REVISIONS["convaiinnovations/laya"],
)
Router принимает тот же revision, а revisions — чтобы закрепить каждый чекпойнт отдельно. Ключи
PINNED_REVISIONS — это три standalone-репозитория, поэтому закрепляйте Router с
standalone_repos=True:
router = laya.Router(standalone_repos=True, revisions={
"english": PINNED_REVISIONS["convaiinnovations/laya"],
"multilingual": PINNED_REVISIONS["convaiinnovations/laya-multilingual"],
"typed-decisions": PINNED_REVISIONS["convaiinnovations/laya-typed-decisions"],
})
Это важно, потому что Router по умолчанию загружает все три чекпойнта из одного bundle-репозитория
(convaiinnovations/laya, где multilingual/ и typed-decisions/ — подпапки), а SHA коммита из
laya-multilingual в bundle-репозитории не существует. Без standalone_repos закрепление не просто
игнорируется — загрузка падает. Если вы предпочитаете остаться на bundle-репозитории, закрепите его
одним revision= для всех трёх, а не revisions= для каждой модели.
Закрепляйте все три, даже если обслуживаете только два. Router предлагает каждый чекпойнт, о котором
знает, независимо от того, что вы предзагружаете, поэтому незакреплённая запись находится в одном
решении маршрутизации от загрузки без закрепления.
Почему закрепление не по умолчанию
Закрепление по умолчанию сломало бы загрузку из более старого снимка в кэше, что важно для развёртываний
на устройстве и в изолированных сетях: HF_HUB_OFFLINE=1 с кэшем, который старше закрепления, перестал
бы работать. Поэтому Laya сохраняет значение по умолчанию Hub, если вы не передадите ревизию, и делает
проверенные SHA доступными на случай, когда они вам понадобятся.
Проверить дайджесты артефактов
Закреплённая ревизия говорит, какой коммит получить. Дайджест говорит, какие байты вы ожидаете.
Каждый перечисленный вами файл хэшируется до того, как будет разобран любой из них, и до того, как веса
достигнут среды выполнения. Карта имеет вид {path relative to the checkpoint: sha256 hex} — сначала
сгенерируйте её, затем передайте.
Нужны ли оба?
Закреплённая ревизия уже фиксирует содержимое: Hub — это git, поэтому коммит определяет дерево, а большие файлы адресуются по собственному SHA-256. Если вы закрепите и загрузка пройдёт успешно, у вас есть байты, которые называет этот коммит. Значит, дайджест здесь не для того, чтобы повторить эту проверку, — он отличается тем, чему доверяет.
Ревизия запрашивает у Hub коммит и верит ответу. Дайджест — это запись, которую сделали вы и которую храните вы, сверяемая при каждой загрузке. Это даёт три вещи, которых не даёт закрепление:
- Покрытие для обычного случая, когда закрепления нет. Закрепление включается по желанию и по умолчанию выключено, поэтому большинство развёртываний следуют за движущейся веткой. Тогда дайджест — единственное, что замечает изменение.
- Проверка на вашем собственном диске. После загрузки чекпойнт — это обычные файлы в кэше, которые может изменить что угодно на машине. Ничто не перепроверяет их во время загрузки — кроме дайджеста.
- Независимость от источника. Если зеркало, прокси или сам Hub отдали другие байты, дайджест — единственный контроль, который не просит проверяемую сущность поручиться за саму себя.
Эта независимость — также причина, по которой генерация карты является ручным шагом: отпечаток перестаёт быть независимой записью в тот момент, когда проверяемая сущность создаёт его за вас.
Сгенерировать карту
Сгенерируйте её из чекпойнта, который вы проверили, а не копируйте дайджесты откуда-либо, включая эту
страницу. Намеренно не существует команды, которая сделает это за вас: карта, вычисленная по копии,
которую Laya только что загрузила, захэшировала бы эти байты и затем сверила бы их сами с собой. Проверка
чего-то стоит только потому, что человек решил, что эти байты — те, что ему нужны, поэтому генерация
карты — шаг, на котором это решение фиксируется. Карта принадлежит ровно одному чекпойнту:
bundle-репозиторий содержит другой rl_agent_config.json в своём корне (чекпойнт english), нежели в
multilingual/, поэтому карта, сгенерированная из одного, не пройдёт проверку против другого.
import hashlib, json, os
CHECKPOINT = "/path/to/checkpoint" # the directory a load actually reads
FILES = [
"rl_agent_config.json",
"tokenizer/tokenizer.json",
"encoder/config.json",
"model.safetensors",
]
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
digests = {rel: sha256(os.path.join(CHECKPOINT, rel)) for rel in FILES}
with open("digests.json", "w") as f:
json.dump(digests, f, indent=2)
Загрузка Agent на torch разбирает пять файлов, и это четыре из них. (ONNXAgent читает другой набор и
вдобавок принимает ключи onnx и onnx_path, чтобы хэшировать сам граф.) Пятый,
tokenizer/tokenizer_config.json, намеренно оставлен вне списка: Laya может нормализовать его после
проверки и записать обратно, и в этом случае его закрепление заставляет следующую загрузку упасть. Эта
перезапись условна — она срабатывает только когда файл не объявляет tokenizer_class, или объявляет
TokenizersBackend, или несёт extra_special_tokens в виде списка, — поэтому на некоторых чекпойнтах она
никогда не происходит, и закрепление файла будет выглядеть работающим. Оставить его вне списка —
переносимый выбор, и он означает, что один разобранный файл остаётся непроверенным. См.
Что это защищает, а что нет.
Использовать карту
import json
import laya
with open("digests.json") as f:
agent = laya.load("convaiinnovations/laya", expected_sha256=json.load(f))
Ключи — это пути относительно каталога чекпойнта. Несовпадение вызывает ValueError, а перечисленный
файл, которого нет, — FileNotFoundError. Файлы, которые вы не перечислили, не проверяются вовсе,
поэтому карта — это ещё и определение того, что вы защищаете. Это работает как с локальным каталогом, так
и с загрузкой из Hub.
Не трогая код
LAYA_SHA256_DIGESTS содержит ту же карту в виде JSON и применяется всякий раз, когда загрузчик
вызывается без явного expected_sha256:
export LAYA_SHA256_DIGESTS="$(cat digests.json)"
laya-serve
Ничто не генерирует это за вас: значение — ваша собственная карта, из проверенного вами чекпойнта. Под
Docker оно должно быть в окружении до запуска compose, либо экспортировано, как выше, либо в файле
.env, который читает compose, — сервис пропускает ${LAYA_SHA256_DIGESTS:-}, поэтому не заданная
переменная молча означает отсутствие проверки:
echo "LAYA_SHA256_DIGESTS=$(cat digests.json)" >> .env
docker compose -f compose.yaml -f compose.http.yaml up laya-serve
У этой переменной нет варианта _FILE: эта косвенность существует для секретов, а карта дайджестов — не
секрет.
Называйте каждый чекпойнт, когда процесс загружает больше одного. Переменная принимает две формы, и типы значений говорят, какую:
# one set of files, checked on every checkpoint the process loads
LAYA_SHA256_DIGESTS='{"rl_agent_config.json": "<sha256>"}'
# a map per checkpoint, which is what a router serving several needs
LAYA_SHA256_DIGESTS='{"english": {"rl_agent_config.json": "<sha256>"},
"multilingual": {"rl_agent_config.json": "<sha256>"}}'
Плоская форма — это собственное чтение verify_digests, и она применяет одни и те же пути ко всему,
поэтому на router она может совпасть только с одним чекпойнтом и отвергает остальные. Упакованный
репозиторий поставляет отдельные model.safetensors и rl_agent_config.json для каждого чекпойнта,
поэтому назовите их:
flat map generated from the english checkpoint
load english ok
load multilingual ValueError: laya: SHA-256 mismatch for rl_agent_config.json
Чекпойнт, который вложенная карта не называет, намеренно остаётся незакреплённым, а не считается ошибкой,
а имя модели, которого router не знает, вызывает исключение, а не оставляет этот чекпойнт непроверенным.
en разрешается в english — та же нормализация, которую применяет Router(sha256_digests=...).
Эти два пути различаются в последнем пункте, и на этом легко споткнуться, если использовать оба. Чекпойнт,
опущенный во вложенной карте окружения, закрепляется за пустой картой, поэтому плоская карта не может в
него просочиться. Чекпойнт, опущенный в Router(sha256_digests=...) в коде, не имеет записи вовсе,
поэтому он всё ещё откатывается к тому, что говорит окружение. Назовите каждый чекпойнт, который хотите
закрепить, в том из них, который используете.
Не заданная или пустая переменная означает отсутствие проверки, поэтому её безопасно не указывать в окружениях, где она не нужна. Некорректный JSON вызывает исключение, а не молча пропускает проверку, а смешивание двух форм в одном объекте отклоняется по имени.
Как несовпадение выглядит на сервере
То, как это проявляется, зависит от предзагрузки. Голый laya-serve предзагружает по умолчанию
(LAYA_PRELOAD=1), поэтому несовпадение приводит к отказу при запуске — громко и детерминированно.
Контейнеры в этом репозитории задают LAYA_PRELOAD=0 (compose.http.yaml, и Docker
документирует переопределение), поэтому там первая загрузка происходит по запросу, и до этого момента
ничего не проверяется. Тогда несовпадение — это 422 на том запросе, который маршрутизируется к этому
чекпойнту: laya/serve.py преобразует ValueError в HTTPException(422) и возвращает вызывающему текст
дайджеста. Перечисленный, но отсутствующий файл вместо этого вызывает FileNotFoundError, который
проваливается в общую 500 «inference failed» с причиной только в логе контейнера.
Рассчитывайте на 422. В логах, дашбордах и правилах оповещений он классифицируется как клиентская ошибка, поэтому место, где оператор по умолчанию ищет сломанное развёртывание, — единственное, где это не появится.
Закрепление ревизии на сервере
LAYA_REVISION содержит коммит, ветку или тег, применяемые к каждой загрузке чекпойнта, либо слово
reviewed, которое ищет каждый репозиторий в PINNED_REVISIONS и использует собственный SHA:
LAYA_REVISION=reviewed laya-serve
reviewed для репозитория, записи о котором нет в таблице, вызывает исключение, а не загружает его
без закрепления, — закрепление, которое тихо разрешается в ничто, и есть тот сбой, ради предотвращения
которого существует этот контроль. Явный аргумент revision= всё ещё побеждает переменную, а не заданное
или пустое значение означает «не запрошено», поэтому кэш HF_HUB_OFFLINE=1 продолжает загружаться точно
как прежде.
В коде Router принимает оба параметра для каждой модели:
router = laya.Router(
revisions={"english": PINNED_REVISIONS["convaiinnovations/laya"]},
sha256_digests={"english": {"rl_agent_config.json": "<sha256>"}},
)
Дайджесты всегда задаются для каждой модели — нет эквивалента revision для всего router, потому что SHA
коммита может быть общим для нескольких чекпойнтов, а дайджест — нет. См. Docker о переменных
развёртывания и Router о полном конструкторе.
Когда чекпойнт обновляется
Эти два контроля ведут себя по-разному, и только один из них требует чего-то от вас.
Закреплённая ревизия удерживает вас на месте. Новый чекпойнт не доходит до закреплённого
развёртывания, пока вы не измените закрепление, — в этом и смысл закрепления. PINNED_REVISIONS движется
вместе с библиотекой, поэтому взять более новый проверенный коммит означает обновить Laya, а не
отредактировать SHA.
Дайджесты останавливают загрузку, намеренно. Ваша карта была сгенерирована из байтов, которые вы
проверили. Другие байты вызывают ValueError до того, как что-либо будет разобрано:
ValueError: laya: SHA-256 mismatch for rl_agent_config.json: expected ae287b56…, got 25061739…
Это работающая функция, а не баг, который нужно обойти. Порядок важен:
- Выясните, почему изменились байты — намеренный релиз или что-то, чего вы не ожидали.
- Проверьте новый чекпойнт.
- Перегенерируйте карту из проверенной копии.
- Разверните новую карту.
Не перескакивайте к шагу 3. Повторный запуск генератора против того, что только что пришло, заставит проверку пройти и ничего не проверит — он записывает новые байты как доверенные, потому что они присутствуют, а это ровно то состояние, которое дайджест и существовал, чтобы обнаружить.
Две детали. Новая карта, доставленная через LAYA_SHA256_DIGESTS, требует перезапуска процесса, потому
что работающий сервер сохраняет окружение, с которым стартовал. И эта последовательность применима только
к развёртыванию, которое не закреплено по ревизии: при обоих включённых контролях новые байты никогда
не придут, пока вы не сдвинете закрепление.
Подтвердить, что действительно загрузилось
Каждый агент записывает коммит, из которого он пришёл, — None для локального каталога:
agent.revision # Agent and ONNXAgent
router.loaded_revisions # {"english": "55cf4c4e…", …} for each resident agent
agent.revision сообщает снимок, к которому разрешилась загрузка, откатываясь к тому, что вы передали,
поэтому закрепление по ветке или тегу повторяет это имя, а не SHA, — закрепляйте по SHA, если хотите,
чтобы это поле было именно им. Загрузка из локального каталога сообщает None, и revision там
игнорируется, потому что нет снимка Hub, к которому можно разрешиться.
Сервер сообщает то же самое, и это самый быстрый способ подтвердить, что развёртывание работает на том чекпойнте, о котором вы думаете:
curl -s localhost:8000/health
# {"status":"ok","loaded":["english"],"revisions":{"english":"55cf4c4e…"},"device":"auto"}
laya-ts
Пакет TypeScript повторяет части с закреплением и дайджестами — revision, expectedSha256 и чтение
ревизии обратно. У него нет эквивалента LAYA_SHA256_DIGESTS и нет сервера, поэтому две предыдущие
секции к нему не относятся:
import { loadNodeBundle, PINNED_REVISIONS } from "laya-ts";
const bundle = await loadNodeBundle("convaiinnovations/laya", {
revision: PINNED_REVISIONS["convaiinnovations/laya"],
expectedSha256: { "rl_agent_config.json": "<sha256 of that file>" },
});
Явная ревизия присоединяется к пути кэша на диске под ~/.cache/laya-ts/, поэтому по-разному закреплённые
артефакты никогда не конфликтуют. В браузере ревизия вместо этого путешествует в URL запроса, что
индексирует CacheStorage тем же образом. createNodeProvider принимает expectedSha256 для загружаемых
им графов ONNX.
Что это защищает, а что нет
Он обнаруживает чекпойнт, содержимое которого изменилось относительно того, что вы проверяли, — правка в upstream-репозитории, скомпрометированное зеркало, повреждённая загрузка или изменённая локальная копия.
Он не делает непроверенный чекпойнт безопасным. Дайджест лишь говорит, что байты совпадают с тем, что вы записали; решать, что этим байтам можно доверять, по-прежнему вам.
Три ограничения, о которых стоит знать, прежде чем полагаться на это:
- Проверяются только перечисленные файлы. Нет режима «проверить всё» и нет способа отвергнуть файл, который вы не перечислили, поэтому артефакт, отсутствующий в вашей карте, загружается непроверенным. Карта — это граница гарантии.
- Один разобранный файл поэтому остаётся вне её.
tokenizer/tokenizer_config.jsonразбирается, но Laya может нормализовать его и записать обратно сразу после проверки дайджеста, поэтому его закрепление может пройти при первой загрузке и упасть при следующей. Рекомендуемая карта оставляет его вне списка по этой причине, а значит, его байты не проверяются. Перезапись зависит от того, что объявляет файл, поэтому происходит ли она — зависит от чекпойнта. - Проверка происходит только во время загрузки. Ничто не перепроверяет файл впоследствии — ни когда его заменяет злоумышленник, ни когда это делает сам процесс.