Documentação

Integridade de checkpoint

O Laya baixa os pesos do modelo do Hugging Face Hub no momento da carga. Por padrão ele pega o que a revisão padrão do repositório aponta, o que é conveniente e é o que um cache offline já contém. Se você preferir fixar um commit revisado, ou recusar carregar um checkpoint cujos bytes mudaram, ambos estão disponíveis e ambos são opt-in.

Nada aqui muda o que o Laya carrega até você pedir, então adicionar essas opções a uma implantação existente é seguro. Ambos são no nível da biblioteca: os digests chegam ao servidor HTTP através de uma variável de ambiente, e a fixação de revisão através de LAYA_REVISION — veja Fixar uma revisão no servidor.

Relacionado: Docker para as variáveis de implantação, laya.load e Agent, e Router.

Fixar uma revisão

Passe revision para qualquer loader. Ele aceita um SHA de commit, um branch ou uma tag, e é repassado ao Hub.

import laya

agent = laya.load("convaiinnovations/laya", revision="<commit sha, branch, or tag>")
print(agent.revision)   # what the download resolved to

Prefira os SHAs revisados que vêm com o Laya a um literal seu: eles são atualizados junto com os checkpoints, então desta forma não dá para ficarem desatualizados.

from laya import PINNED_REVISIONS

agent = laya.load(
    "convaiinnovations/laya",
    revision=PINNED_REVISIONS["convaiinnovations/laya"],
)

O Router aceita o mesmo revision, e revisions para fixar cada checkpoint separadamente. As chaves de PINNED_REVISIONS são os três repositórios autônomos, então fixe um Router com 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"],
})

Isso importa porque um Router padrão carrega os três checkpoints do único repositório bundle (convaiinnovations/laya, com multilingual/ e typed-decisions/ como subpastas), e um SHA de commit de laya-multilingual não existe no repositório bundle. Sem standalone_repos a fixação não é apenas ignorada — a carga falha. Se você preferir ficar no repositório bundle, fixe-o com um único revision= para os três em vez de revisions= por modelo.

Fixe os três mesmo que você só sirva dois. Um Router oferece todo checkpoint que conhece, independentemente do que você pré-carrega, então uma entrada sem fixação está a uma decisão de roteamento de carregar sem fixação.

Por que fixar não é o padrão

Fixar por padrão quebraria a carga de um snapshot em cache mais antigo, o que importa para implantações em dispositivo e em ambiente isolado: HF_HUB_OFFLINE=1 com um cache anterior à fixação pararia de funcionar. O Laya, portanto, mantém o padrão do Hub a menos que você passe uma revisão, e deixa os SHAs revisados disponíveis para quando você os quiser.

Verificar digests de artefato

Uma revisão fixada diz qual commit buscar. Um digest diz quais bytes você espera. Todo arquivo que você listar é transformado em hash antes de qualquer um deles ser analisado e antes de os pesos chegarem ao runtime. O mapa é {path relative to the checkpoint: sha256 hex} — gere-o primeiro, depois passe-o.

Você precisa dos dois?

Uma revisão fixada já fixa o conteúdo: o Hub é git, então o commit determina a árvore, e arquivos grandes são endereçados pelo próprio SHA-256. Se você fixa e o download tem sucesso, você tem os bytes que aquele commit nomeia. Então o digest não está ali para repetir essa checagem — ele difere em no que confia.

Uma revisão pede um commit ao Hub e acredita na resposta. Um digest é um registro que você fez e você guarda, comparado a cada carga. Isso compra três coisas que a fixação não dá:

  • Cobertura para o caso comum, que é o sem fixação. A fixação é opt-in e desligada por padrão, então a maioria das implantações segue um branch móvel. Um digest é então a única coisa que nota uma mudança.
  • Uma checagem no seu próprio disco. Depois do download, o checkpoint são arquivos comuns em um cache que qualquer coisa na máquina pode editar. Nada os reverifica no momento da carga — exceto um digest.
  • Independência da fonte. Se um espelho, proxy ou o próprio Hub servisse bytes diferentes, o digest é o único controle que não pede à coisa sob teste que ateste por si mesma.

Essa independência é também por que gerar o mapa é um passo manual: uma impressão digital deixa de ser um registro independente no momento em que a coisa que ela verifica a produz para você.

Gerar o mapa

Gere-o a partir de um checkpoint que você revisou, em vez de copiar digests de qualquer lugar, incluindo desta página. Não existe de propósito nenhum comando que produza isto para você: um mapa calculado a partir da cópia que o Laya acabou de baixar transformaria esses bytes em hash e depois os verificaria contra eles mesmos. A checagem vale algo só porque uma pessoa decidiu que aqueles bytes eram os que queria, então gerar o mapa é o passo em que essa decisão é registrada. Um mapa pertence a exatamente um checkpoint: o repositório bundle guarda um rl_agent_config.json diferente na sua raiz (o checkpoint em inglês) do que em multilingual/, então um mapa gerado a partir de um vai falhar contra o outro.

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)

Uma carga de Agent do torch analisa cinco arquivos, e esses são quatro deles. (ONNXAgent lê um conjunto diferente e adicionalmente aceita as chaves onnx e onnx_path para transformar o próprio grafo em digest.) O quinto, tokenizer/tokenizer_config.json, é deixado de fora de propósito: o Laya pode normalizá-lo depois da verificação e reescrevê-lo, e nesse caso fixá-lo faz a próxima carga falhar. Essa reescrita é condicional — ela dispara apenas quando o arquivo não declara tokenizer_class, ou declara TokenizersBackend, ou carrega extra_special_tokens como uma lista — então em alguns checkpoints ela nunca acontece e fixar o arquivo pareceria funcionar. Deixá-lo de fora é a escolha portátil, e significa que um arquivo analisado fica sem verificação. Veja O que isto protege e o que não protege.

Usar o mapa

import json

import laya

with open("digests.json") as f:
    agent = laya.load("convaiinnovations/laya", expected_sha256=json.load(f))

As chaves são caminhos relativos ao diretório do checkpoint. Uma divergência lança ValueError, e um arquivo listado que está ausente lança FileNotFoundError. Arquivos que você não lista não são verificados de forma alguma, então o mapa também é a definição do que você está protegendo. Isso funciona tanto em um diretório local quanto em um download do Hub.

Sem tocar no código

LAYA_SHA256_DIGESTS guarda o mesmo mapa como JSON e se aplica sempre que um loader é chamado sem um expected_sha256 explícito:

export LAYA_SHA256_DIGESTS="$(cat digests.json)"
laya-serve

Nada gera isto para você: o valor é o seu próprio mapa, de um checkpoint que você revisou. Sob Docker ele precisa estar no ambiente antes de o compose iniciar, seja exportado como acima ou no arquivo .env que o compose lê — o serviço repassa ${LAYA_SHA256_DIGESTS:-}, então uma variável não definida significa silenciosamente nenhuma verificação:

echo "LAYA_SHA256_DIGESTS=$(cat digests.json)" >> .env
docker compose -f compose.yaml -f compose.http.yaml up laya-serve

Não há variante _FILE desta variável: essa indireção existe para segredos, e um mapa de digests não é um.

Nomeie cada checkpoint quando um processo carrega mais de um. A variável aceita dois formatos, e os tipos de valor dizem qual:

# 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>"}}'

A forma plana é a leitura do próprio verify_digests e aplica os mesmos caminhos a tudo, então em um router ela só consegue casar com um checkpoint e recusa o resto. O repositório bundle distribui um model.safetensors e um rl_agent_config.json separados por checkpoint, então nomeie-os:

flat map generated from the english checkpoint
  load english        ok
  load multilingual   ValueError: laya: SHA-256 mismatch for rl_agent_config.json

Um checkpoint que o mapa aninhado não nomeia fica de propósito sem fixação em vez de virar erro, e um nome de modelo que o router não conhece lança em vez de deixar aquele checkpoint sem verificação. en resolve para english, a mesma normalização que Router(sha256_digests=...) aplica.

As duas rotas diferem nesse último ponto, o que é fácil de tropeçar se você usa as duas. Um checkpoint que o mapa aninhado do ambiente omite é fixado a um mapa vazio, então um mapa plano não consegue vazar para dentro dele. Um checkpoint omitido de Router(sha256_digests=...) no código não tem entrada alguma, então ele ainda cai para o que o ambiente disser. Nomeie todo checkpoint que você pretende fixar, em qualquer uma das formas que usar.

Uma variável não definida ou vazia significa nenhuma verificação, então é seguro deixá-la de fora de ambientes que não precisam dela. JSON malformado lança em vez de pular a checagem em silêncio, e misturar os dois formatos em um mesmo objeto é recusado por nome.

Como uma divergência aparece no servidor

Como ela se manifesta depende da pré-carga. O laya-serve puro pré-carrega por padrão (LAYA_PRELOAD=1), então uma divergência falha na inicialização — alto e determinístico. Os contêineres neste repositório definem LAYA_PRELOAD=0 (compose.http.yaml, e o Docker documenta o override), então lá a primeira carga acontece em uma solicitação e nada é verificado até então. Uma divergência é então um 422 em qualquer ticket que roteie para aquele checkpoint: laya/serve.py mapeia o ValueError para HTTPException(422) e retorna o texto do digest ao chamador. Um arquivo listado mas ausente lança FileNotFoundError, que cai em um 500 “inference failed” genérico, com o motivo apenas no log do contêiner.

Planeje para o 422. Ele se ordena como erro de cliente em logs, dashboards e regras de alerta, então o lugar padrão onde um operador procura uma implantação quebrada é o único lugar onde isto não vai aparecer.

Fixar uma revisão no servidor

LAYA_REVISION guarda um commit, branch ou tag aplicado a todo download de checkpoint, ou a palavra reviewed, que procura cada repositório em PINNED_REVISIONS e usa o próprio SHA dele:

LAYA_REVISION=reviewed laya-serve

reviewed para um repositório que a tabela não tem é lançado em vez de carregá-lo sem fixação — uma fixação que resolve silenciosamente para nada é a falha que este controle existe para prevenir. Um argumento revision= explícito ainda vence a variável, e não definido ou em branco significa “não pedido”, então um cache com HF_HUB_OFFLINE=1 continua carregando exatamente como antes.

No código, o Router aceita ambos por modelo:

router = laya.Router(
    revisions={"english": PINNED_REVISIONS["convaiinnovations/laya"]},
    sha256_digests={"english": {"rl_agent_config.json": "<sha256>"}},
)

Digests são sempre por modelo — não há equivalente de revision para o router inteiro, porque um SHA de commit pode ser compartilhado entre checkpoints e um digest não. Veja Docker para as variáveis de implantação e Router para o construtor completo.

Quando um checkpoint é atualizado

Os dois controles se comportam de forma diferente, e apenas um deles precisa de algo de você.

Uma revisão fixada mantém você onde está. Um checkpoint novo não chega a uma implantação fixada até você mudar a fixação, que é o ponto de fixar. PINNED_REVISIONS se move com a biblioteca, então pegar um commit revisado mais novo significa atualizar o Laya, não editar um SHA.

Os digests param a carga, de propósito. Seu mapa foi gerado a partir de bytes que você revisou. Bytes diferentes lançam ValueError antes de qualquer coisa ser analisada:

ValueError: laya: SHA-256 mismatch for rl_agent_config.json: expected ae287b56…, got 25061739…

Isso é o recurso funcionando, não um bug para contornar. A ordem importa:

  1. Descubra por que os bytes mudaram — uma versão intencional, ou algo que você não esperava.
  2. Revise o novo checkpoint.
  3. Regenere o mapa a partir da cópia revisada.
  4. Implante o novo mapa.

Não pule para o passo 3. Rodar o gerador de novo contra o que acabou de chegar faz a checagem passar e não verifica nada — ele registra os novos bytes como confiáveis porque eles estão presentes, que é precisamente o estado que o digest existia para detectar.

Dois detalhes. Um novo mapa entregue via LAYA_SHA256_DIGESTS precisa do processo reiniciado, porque um servidor em execução mantém o ambiente com que iniciou. E essa sequência só se aplica a uma implantação que não tem revisão fixada: com os dois controles ligados, os novos bytes nunca chegam até você mover a fixação.

Confirmar o que de fato foi carregado

Todo agente registra o commit de onde veio, None para um diretório local:

agent.revision                 # Agent and ONNXAgent
router.loaded_revisions        # {"english": "55cf4c4e…", …} for each resident agent

agent.revision reporta o snapshot que o download resolveu, caindo para o que você passou, então fixar por branch ou tag ecoa aquele nome em vez de um SHA — fixe por SHA se você quer que este campo seja um. A carga a partir de um diretório local reporta None, e revision é ignorado ali, porque não há snapshot do Hub a resolver.

O servidor reporta a mesma coisa, que é a forma mais rápida de confirmar que uma implantação está rodando o checkpoint que você pensa que é:

curl -s localhost:8000/health
# {"status":"ok","loaded":["english"],"revisions":{"english":"55cf4c4e…"},"device":"auto"}

laya-ts

O pacote TypeScript espelha as partes de fixação e digest — revision, expectedSha256, e a leitura da revisão de volta. Ele não tem equivalente a LAYA_SHA256_DIGESTS nem servidor, então as duas seções acima não se aplicam a ele:

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>" },
});

Uma revisão explícita entra no caminho de cache em disco sob ~/.cache/laya-ts/, então artefatos com fixações diferentes nunca colidem. No navegador a revisão viaja na URL da solicitação, o que indexa o CacheStorage da mesma forma. createNodeProvider aceita expectedSha256 para os grafos ONNX que carrega.

O que isto protege e o que não protege

Ele detecta um checkpoint cujo conteúdo mudou em relação ao que você revisou — uma edição em repositório a montante, um espelho comprometido, um download corrompido ou uma cópia local modificada.

Ele não torna seguro um checkpoint não revisado. Um digest só diz que os bytes correspondem ao que você registrou; decidir que aqueles bytes são confiáveis ainda é sua responsabilidade.

Três limites que vale conhecer antes de confiar nele:

  • Apenas arquivos listados são verificados. Não há modo “verificar tudo” nem forma de recusar um arquivo que você não listou, então um artefato ausente do seu mapa é carregado sem verificação. O mapa é a fronteira da garantia.
  • Um arquivo analisado, portanto, fica fora dela. tokenizer/tokenizer_config.json é analisado, mas o Laya pode normalizá-lo e reescrevê-lo imediatamente após a checagem de digest, então fixá-lo pode ter sucesso na primeira carga e falhar na seguinte. O mapa recomendado o deixa de fora por esse motivo, o que significa que seus bytes não são verificados. A reescrita é condicional ao que o arquivo declara, então se ela acontece depende do checkpoint.
  • A verificação acontece apenas no momento da carga. Nada reverifica um arquivo depois, seja ele substituído por um atacante ou pelo próprio processo.