Documentação

Integridade do checkpoint

O Laya descarrega pesos de modelos a partir do Hugging Face Hub no momento do carregamento. Por predefinição, aceita o que a revisão predefinida do repositório indicar, o que é conveniente e é o que uma cache offline já contém. Se preferires fixar um commit revisto, ou recusar carregar um checkpoint cujos bytes mudaram, ambos estão disponíveis e ambos são opt-in.

Nada aqui altera o que o Laya carrega até o pedires, por isso acrescentar estas opções a uma implementação existente é seguro. Ambas são ao 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 — vê Fixar uma revisão no servidor.

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

Fixar uma revisão

Passa revision a qualquer carregador. Aceita um SHA de commit, um ramo ou uma tag, e é encaminhado para o Hub.

import laya

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

Prefere os SHAs revistos que acompanham o Laya a um literal teu: são atualizados com os checkpoints, por isso esta forma não pode ficar desatualizada.

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, por isso fixa 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"],
})

Isto importa porque um Router predefinido carrega os três checkpoints do repositório bundle único (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 — o carregamento falha. Se preferires ficar no repositório bundle, fixa-o com um único revision= para os três em vez de revisions= por modelo.

Fixa os três mesmo que só sirvas dois. Um Router oferece todos os checkpoints que conhece, independentemente do que pré-carregas, por isso uma entrada não fixada está a uma decisão de encaminhamento de carregar sem fixação.

Porque é que a fixação não é o padrão

Fixar por predefinição quebraria o carregamento a partir de um snapshot em cache mais antigo, o que importa para implementações no dispositivo e em ambientes isolados: HF_HUB_OFFLINE=1 com uma cache anterior à fixação deixaria de funcionar. Por isso o Laya mantém a predefinição do Hub a menos que passes uma revisão, e disponibiliza os SHAs revistos para quando os quiseres.

Verificar digests de artefactos

Uma revisão fixada diz qual commit buscar. Um digest diz que bytes esperas. Cada ficheiro que listas é hasheado antes de qualquer deles ser analisado e antes de os pesos chegarem ao runtime. O mapa é {path relative to the checkpoint: sha256 hex} — gera-o primeiro, depois passa-o.

Precisas dos dois?

Uma revisão fixada já fixa o conteúdo: o Hub é git, por isso o commit determina a árvore, e os ficheiros grandes são endereçados pelo seu próprio SHA-256. Se fixares e o download tiver êxito, tens os bytes que o commit nomeia. Por isso o digest não está lá para repetir essa verificação — difere em no que confia.

Uma revisão pede um commit ao Hub e acredita na resposta. Um digest é um registo que tu fizeste e tu guardas, comparado em cada carregamento. Isso compra três coisas que a fixação não dá:

  • Cobertura para o caso comum, que é não fixado. A fixação é opt-in e está desligada por predefinição, por isso a maioria das implementações segue um ramo em movimento. O digest é então a única coisa que nota uma mudança.
  • Uma verificação no teu próprio disco. Após o download, o checkpoint são ficheiros normais numa cache que qualquer coisa na máquina pode editar. Nada os volta a verificar no momento do carregamento — exceto um digest.
  • Independência da fonte. Se um espelho, proxy ou o próprio Hub servisse bytes diferentes, o digest é o único controlo que não está a pedir à coisa sob teste que se abone a si mesma.

Essa independência é também a razão pela qual gerar o mapa é um passo manual: uma impressão digital deixa de ser um registo independente no momento em que a coisa que verifica a produz por ti.

Gerar o mapa

Gera-o a partir de um checkpoint que reviste, em vez de copiar digests de qualquer lado, incluindo desta página. Deliberadamente não há nenhum comando que o produza por ti: um mapa calculado a partir da cópia que o Laya acabou de descarregar hasheria esses bytes e depois verificá-los-ia contra si mesmos. A verificação só vale algo porque uma pessoa decidiu que os bytes eram os que queria, por isso gerar o mapa é o passo em que essa decisão fica registada. Um mapa pertence a exatamente um checkpoint: o repositório bundle tem um rl_agent_config.json diferente na sua raiz (o checkpoint inglês) do que em multilingual/, por isso 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)

Um carregamento de Agent da torch analisa cinco ficheiros, e esses são quatro deles. (O ONNXAgent lê um conjunto diferente e aceita adicionalmente as chaves onnx e onnx_path para hashear o próprio grafo.) O quinto, tokenizer/tokenizer_config.json, é deliberadamente deixado de fora: o Laya pode normalizá-lo depois da verificação e reescrevê-lo, caso em que fixá-lo faz falhar o carregamento seguinte. Essa reescrita é condicional — só dispara quando o ficheiro não declara nenhum tokenizer_class, ou declara TokenizersBackend, ou traz extra_special_tokens como lista — por isso em alguns checkpoints nunca acontece e fixar o ficheiro pareceria funcionar. Deixá-lo de fora é a escolha portável, e significa que um ficheiro analisado fica não verificado. Vê 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 incompatibilidade levanta ValueError, e um ficheiro listado que esteja ausente levanta FileNotFoundError. Os ficheiros que não listas não são verificados de todo, por isso o mapa é também a definição do que estás a proteger. Isto funciona num diretório local bem como num download do Hub.

Sem tocar no código

LAYA_SHA256_DIGESTS contém o mesmo mapa como JSON e aplica-se sempre que um carregador é chamado sem um expected_sha256 explícito:

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

Nada gera isto por ti: o valor é o teu próprio mapa, de um checkpoint que reviste. Sob o Docker tem de estar no ambiente antes de o compose arrancar, ou exportado como acima ou no ficheiro .env que o compose lê — o serviço passa ${LAYA_SHA256_DIGESTS:-} adiante, por isso 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á uma variante _FILE desta variável: essa indireção existe para segredos, e um mapa de digests não é um.

Nomeia cada checkpoint quando um processo carrega mais do que um. A variável assume duas formas, e os tipos dos valores 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 própria do verify_digests e aplica os mesmos caminhos a tudo, por isso num router só pode alguma vez corresponder a um checkpoint e recusa o resto. O repositório bundle inclui um model.safetensors e um rl_agent_config.json separados por checkpoint, por isso nomeia-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 é deliberadamente deixado sem fixação em vez de ser um erro, e um nome de modelo que o router não conhece levanta exceção em vez de deixar esse checkpoint não verificado. 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 usares ambas. Um checkpoint que o mapa aninhado da variável de ambiente omite fica fixado a um mapa vazio, por isso um mapa plano não pode escapar-se para dentro dele. Um checkpoint omitido de Router(sha256_digests=...) no código não tem entrada nenhuma, por isso ainda cai para o que a variável de ambiente disser. Nomeia cada checkpoint que pretendes fixar naquele que usares.

Uma variável não definida ou vazia significa nenhuma verificação, por isso é seguro deixá-la de fora de ambientes que não precisam dela. JSON malformado levanta exceção em vez de saltar silenciosamente a verificação, e misturar as duas formas num só objeto é recusado por nome.

Como uma incompatibilidade aparece no servidor

A forma como aparece depende do pré-carregamento. O laya-serve simples pré-carrega por predefinição (LAYA_PRELOAD=1), por isso uma incompatibilidade falha no arranque — alto e determinístico. Os contentores deste repositório definem LAYA_PRELOAD=0 (compose.http.yaml, e o Docker documenta o override), por isso aí o primeiro carregamento acontece num pedido e nada é verificado até então. Uma incompatibilidade é então um 422 no ticket que for encaminhado para esse checkpoint: laya/serve.py mapeia o ValueError para HTTPException(422) e devolve o texto do digest ao autor da chamada. Um ficheiro listado mas ausente levanta FileNotFoundError, que cai num genérico 500 «inference failed» com a razão apenas no log do contentor.

Conta com o 422. Ele classifica-se como erro de cliente em logs, dashboards e regras de alerta, por isso o sítio padrão onde um operador procura uma implementação avariada é o único sítio onde isto não vai aparecer.

Fixar uma revisão no servidor

LAYA_REVISION contém um commit, ramo ou tag aplicado a cada download de checkpoint, ou a palavra reviewed, que procura cada repositório em PINNED_REVISIONS e usa o seu próprio SHA:

LAYA_REVISION=reviewed laya-serve

reviewed para um repositório para o qual a tabela não tem entrada levanta exceção em vez de o carregar sem fixação — uma fixação que silenciosamente resolve para nada é a falha que este controlo existe para prevenir. Um argumento revision= explícito ainda ganha sobre a variável, e não definida ou em branco significa «não pedido», por isso uma cache com HF_HUB_OFFLINE=1 continua a carregar exatamente como antes.

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

Os digests são sempre por modelo — não há um equivalente de revision para todo o router, porque um SHA de commit pode ser partilhado entre checkpoints e um digest não. Vê Docker para as variáveis de implementação e Router para o construtor completo.

Quando um checkpoint é atualizado

Os dois controlos comportam-se de forma diferente, e só um deles precisa de algo de ti.

Uma revisão fixada mantém-te onde estás. Um checkpoint novo não chega a uma implementação fixada até mudares a fixação, que é o objetivo de fixar. PINNED_REVISIONS move-se com a biblioteca, por isso aceitar um commit revisto mais recente significa atualizar o Laya, não editar um SHA.

Os digests param o carregamento, de propósito. O teu mapa foi gerado a partir de bytes que reviste. Bytes diferentes levantam ValueError antes de qualquer coisa ser analisada:

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

Isso é a funcionalidade a funcionar, não um bug para contornar. A ordem importa:

  1. Descobre porque mudaram os bytes — uma publicação intencional, ou algo que não esperavas.
  2. Revê o novo checkpoint.
  3. Regenera o mapa a partir da cópia revista.
  4. Implementa o novo mapa.

Não saltes para o passo 3. Voltar a correr o gerador contra o que acabou de chegar faz passar a verificação e não verifica nada — registra os novos bytes como fidedignos só porque estão presentes, que é precisamente o estado que o digest existia para detetar.

Dois detalhes. Um mapa novo entregue através de LAYA_SHA256_DIGESTS precisa que o processo seja reiniciado, porque um servidor em execução mantém o ambiente com que arrancou. E esta sequência só se aplica a uma implementação que não está fixada por revisão: com ambos os controlos ligados, os bytes novos nunca chegam até moveres a fixação.

Confirmar o que foi de facto carregado

Cada 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 em que o download resolveu, caindo para o que passaste, por isso fixar por ramo ou tag faz eco desse nome em vez de um SHA — fixa por SHA se quiseres que este campo seja um. O carregamento a partir de um diretório local reporta None, e revision é ignorado aí, porque não há nenhum snapshot do Hub a resolver.

O servidor reporta o mesmo, que é a forma mais rápida de confirmar que uma implementação está a correr o checkpoint que pensas que está:

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 de digest — revision, expectedSha256, e ler a revisão de volta. Não tem equivalente a LAYA_SHA256_DIGESTS nem servidor, por isso as duas secções acima não se lhe aplicam:

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 junta-se ao caminho de cache em disco em ~/.cache/laya-ts/, por isso artefactos fixados de forma diferente nunca colidem. No navegador a revisão viaja antes no URL do pedido, 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

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

Não torna seguro um checkpoint não revisto. Um digest só diz que os bytes correspondem ao que registaste; decidir que esses bytes são fidedignos continua a ser teu.

Três limites que vale a pena conhecer antes de confiares nele:

  • Só os ficheiros listados são verificados. Não há um modo «verificar tudo» nem forma de rejeitar um ficheiro que não listaste, por isso um artefacto em falta no teu mapa é carregado sem verificação. O mapa é o limite da garantia.
  • Um ficheiro analisado fica, portanto, fora dele. tokenizer/tokenizer_config.json é analisado, mas o Laya pode normalizá-lo e reescrevê-lo imediatamente após a verificação do digest, por isso fixá-lo pode ter êxito no primeiro carregamento e falhar no seguinte. O mapa recomendado deixa-o de fora por essa razão, o que significa que os seus bytes não são verificados. A reescrita é condicional ao que o ficheiro declara, por isso se acontece depende do checkpoint.
  • A verificação acontece apenas no momento do carregamento. Nada volta a verificar um ficheiro depois, quer seja substituído por um atacante quer pelo próprio processo.