Intégrité des checkpoints
Laya télécharge les poids du modèle depuis le Hugging Face Hub au chargement. Par défaut, il prend ce que pointe la révision par défaut du dépôt, ce qui est pratique et correspond à ce que contient déjà un cache hors ligne. Si tu préfères épingler un commit revu, ou refuser de charger un checkpoint dont les octets ont changé, les deux sont disponibles et les deux sont opt-in.
Rien ici ne change ce que Laya charge tant que tu ne le demandes pas, donc ajouter ces options à un
déploiement existant est sûr. Les deux sont au niveau de la bibliothèque : les digests atteignent le
serveur HTTP via une variable d’environnement, et l’épinglage de révision via LAYA_REVISION — voir
Épingler une révision dans le serveur.
Liés : Docker pour les variables de déploiement, laya.load et Agent,
et Router.
Épingler une révision
Passe revision à n’importe quel chargeur. Il accepte un SHA de commit, une branche, ou un tag, et est
transmis au Hub.
import laya
agent = laya.load("convaiinnovations/laya", revision="<commit sha, branch, or tag>")
print(agent.revision) # what the download resolved to
Préfère les SHA revus qui sont livrés avec Laya à un littéral de ton cru : ils sont mis à jour avec les checkpoints, donc cette forme ne peut pas devenir obsolète.
from laya import PINNED_REVISIONS
agent = laya.load(
"convaiinnovations/laya",
revision=PINNED_REVISIONS["convaiinnovations/laya"],
)
Router prend le même revision, et revisions pour épingler chaque checkpoint séparément. Les clés de
PINNED_REVISIONS sont les trois dépôts standalone, donc épingle un Router avec
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"],
})
Cela compte parce qu’un Router par défaut charge les trois checkpoints depuis le dépôt bundle unique
(convaiinnovations/laya, avec multilingual/ et typed-decisions/ comme sous-dossiers), et un SHA de
commit de laya-multilingual n’existe pas dans le dépôt bundle. Sans standalone_repos, l’épinglage n’est
pas simplement ignoré — le chargement échoue. Si tu préfères rester sur le dépôt bundle, épingle-le avec un
seul revision= pour les trois au lieu de revisions= par modèle.
Épingle les trois même si tu n’en sers que deux. Un Router offre chaque checkpoint qu’il connaît,
indépendamment de ce que tu précharges, donc une entrée non épinglée est à une décision de routage d’un
chargement non épinglé.
Pourquoi l’épinglage n’est pas le défaut
Épingler par défaut casserait le chargement depuis un ancien instantané en cache, ce qui compte pour les
déploiements sur appareil et isolés : HF_HUB_OFFLINE=1 avec un cache antérieur à l’épinglage cesserait de
fonctionner. Laya garde donc le défaut du Hub sauf si tu passes une révision, et met les SHA revus à
disposition pour quand tu en veux.
Vérifier les digests d’artefacts
Une révision épinglée dit quel commit récupérer. Un digest dit quels octets tu attends. Chaque fichier
que tu listes est haché avant qu’aucun d’eux ne soit parsé et avant que les poids n’atteignent le runtime.
La carte est {path relative to the checkpoint: sha256 hex} — génère-la d’abord, puis passe-la.
As-tu besoin des deux ?
Une révision épinglée fixe déjà le contenu : le Hub est git, donc le commit détermine l’arbre, et les gros fichiers sont adressés par leur propre SHA-256. Si tu épingles et que le téléchargement réussit, tu as les octets que nomme ce commit. Donc le digest n’est pas là pour répéter cette vérification — il diffère en ce en quoi il a confiance.
Une révision demande un commit au Hub et croit la réponse. Un digest est un enregistrement que toi tu as fait et que toi tu gardes, comparé à chaque chargement. Cela achète trois choses que l’épinglage n’achète pas :
- Couverture du cas courant, qui est non épinglé. L’épinglage est opt-in et désactivé par défaut, donc la plupart des déploiements suivent une branche mouvante. Un digest est alors la seule chose qui remarque un changement.
- Un contrôle sur ton propre disque. Après le téléchargement, le checkpoint est de simples fichiers dans un cache que n’importe quoi sur la machine peut modifier. Rien ne les revérifie au chargement — sauf un digest.
- Indépendance vis-à-vis de la source. Si un miroir, un proxy ou le Hub lui-même servait des octets différents, le digest est le seul contrôle qui ne demande pas à la chose testée de se porter garante elle-même.
Cette indépendance est aussi pourquoi générer la carte est une étape manuelle : une empreinte cesse d’être un enregistrement indépendant au moment où la chose qu’elle vérifie la produit pour toi.
Générer la carte
Génère-la depuis un checkpoint que tu as revu plutôt que de copier des digests de quelque part, y compris
de cette page. Il n’y a délibérément aucune commande qui produit cela pour toi : une carte calculée depuis
la copie que Laya vient de télécharger hacherait ces octets puis les vérifierait contre eux-mêmes. Le
contrôle ne vaut quelque chose que parce qu’une personne a décidé que les octets étaient ceux qu’elle
voulait, donc générer la carte est l’étape où cette décision est enregistrée. Une carte appartient à
exactement un checkpoint : le dépôt bundle contient un rl_agent_config.json différent à sa racine (le
checkpoint anglais) qu’en multilingual/, donc une carte générée depuis l’un échouera contre l’autre.
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)
Un chargement torch Agent parse cinq fichiers, et ceux-ci en sont quatre. (ONNXAgent lit un ensemble
différent et accepte en plus les clés onnx et onnx_path pour hacher le graphe lui-même.) Le cinquième,
tokenizer/tokenizer_config.json, est délibérément laissé de côté : Laya peut le normaliser après la
vérification et le réécrire, auquel cas l’épingler fait échouer le prochain chargement. Cette réécriture
est conditionnelle — elle se déclenche seulement quand le fichier ne déclare pas de tokenizer_class, ou
déclare TokenizersBackend, ou porte extra_special_tokens comme liste — donc sur certains checkpoints
elle n’a jamais lieu et épingler le fichier semblerait fonctionner. Le laisser de côté est le choix
portable, et cela signifie qu’un fichier parsé n’est pas vérifié. Voir
Ce que ça protège et ne protège pas.
Utiliser la carte
import json
import laya
with open("digests.json") as f:
agent = laya.load("convaiinnovations/laya", expected_sha256=json.load(f))
Les clés sont des chemins relatifs au répertoire du checkpoint. Une non-concordance lève ValueError, et
un fichier listé qui est absent lève FileNotFoundError. Les fichiers que tu ne listes pas ne sont pas
vérifiés du tout, donc la carte est aussi la définition de ce que tu protèges. Cela fonctionne sur un
répertoire local aussi bien que sur un téléchargement Hub.
Sans toucher au code
LAYA_SHA256_DIGESTS contient la même carte en JSON et s’applique chaque fois qu’un chargeur est appelé
sans expected_sha256 explicite :
export LAYA_SHA256_DIGESTS="$(cat digests.json)"
laya-serve
Rien ne génère cela pour toi : la valeur est ta propre carte, issue d’un checkpoint que tu as revu. Sous
Docker, elle doit être dans l’environnement avant que compose ne démarre, soit exportée comme ci-dessus,
soit dans le fichier .env que lit compose — le service transmet ${LAYA_SHA256_DIGESTS:-}, donc une
variable non définie signifie silencieusement aucune vérification :
echo "LAYA_SHA256_DIGESTS=$(cat digests.json)" >> .env
docker compose -f compose.yaml -f compose.http.yaml up laya-serve
Il n’y a pas de variante _FILE de cette variable : cette indirection existe pour les secrets, et une
carte de digests n’en est pas un.
Nomme chaque checkpoint quand un processus en charge plusieurs. La variable prend deux formes, et les types de valeurs disent laquelle :
# 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>"}}'
La forme plate est la propre lecture de verify_digests et applique les mêmes chemins à tout, donc sur un
routeur elle ne peut jamais correspondre qu’à un seul checkpoint et refuse les autres. Le dépôt bundle
livre un model.safetensors et un rl_agent_config.json séparés par checkpoint, donc nomme-les :
flat map generated from the english checkpoint
load english ok
load multilingual ValueError: laya: SHA-256 mismatch for rl_agent_config.json
Un checkpoint que la carte imbriquée ne nomme pas est délibérément non épinglé plutôt qu’une erreur, et un
nom de modèle que le routeur ne connaît pas lève une erreur plutôt que de laisser ce checkpoint non
vérifié. en se résout en english, la même normalisation que celle qu’applique
Router(sha256_digests=...).
Les deux routes diffèrent sur ce dernier point, ce qui est facile à trébucher si tu utilises les deux. Un
checkpoint que la carte imbriquée de l’environnement omet est épinglé à une carte vide, donc une carte
plate ne peut pas y fuiter. Un checkpoint omis de Router(sha256_digests=...) dans le code n’a aucune
entrée du tout, donc il retombe encore sur ce que dit l’environnement. Nomme chaque checkpoint que tu
comptes épingler dans celle que tu utilises.
Une variable non définie ou vide signifie aucune vérification, donc c’est sûr de l’omettre des environnements qui n’en ont pas besoin. Un JSON malformé lève une erreur plutôt que de sauter silencieusement le contrôle, et mélanger les deux formes dans un seul objet est refusé par son nom.
À quoi ressemble une non-concordance dans le serveur
La façon dont elle remonte dépend du préchargement. Le laya-serve nu précharge par défaut
(LAYA_PRELOAD=1), donc une non-concordance échoue au démarrage — bruyamment et de façon
déterministe. Les conteneurs de ce dépôt mettent LAYA_PRELOAD=0 (compose.http.yaml, et
Docker documente l’override), donc là le premier chargement a lieu sur une requête et rien
n’est vérifié avant. Une non-concordance est alors un 422 sur le ticket qui route vers ce checkpoint :
laya/serve.py mappe la ValueError sur HTTPException(422) et renvoie le texte du digest à l’appelant.
Un fichier listé mais absent lève plutôt FileNotFoundError, qui retombe sur un 500 « inference
failed » générique avec la raison seulement dans le log du conteneur.
Prévois le 422. Il se classe comme une erreur client dans les logs, les tableaux de bord et les règles d’alerte, donc l’endroit par défaut où un opérateur cherche un déploiement cassé est le seul endroit où celui-ci n’apparaîtra pas.
Épingler une révision dans le serveur
LAYA_REVISION contient un commit, une branche ou un tag appliqué à chaque téléchargement de checkpoint,
ou le mot reviewed, qui cherche chaque dépôt dans PINNED_REVISIONS et utilise son propre SHA :
LAYA_REVISION=reviewed laya-serve
reviewed pour un dépôt absent de la table lève une erreur plutôt que de le charger non épinglé — un
épinglage qui se résout silencieusement à rien est la défaillance que ce contrôle existe pour prévenir. Un
argument revision= explicite l’emporte quand même sur la variable, et non défini ou vide signifie « pas
demandé », donc un cache HF_HUB_OFFLINE=1 continue de charger exactement comme avant.
Dans le code, Router prend les deux par modèle :
router = laya.Router(
revisions={"english": PINNED_REVISIONS["convaiinnovations/laya"]},
sha256_digests={"english": {"rl_agent_config.json": "<sha256>"}},
)
Les digests sont toujours par modèle — il n’y a pas d’équivalent de revision à l’échelle du routeur,
parce qu’un SHA de commit peut être partagé entre checkpoints et qu’un digest ne peut pas l’être. Voir
Docker pour les variables de déploiement et Router pour le constructeur
complet.
Quand un checkpoint est mis à jour
Les deux contrôles se comportent différemment, et un seul d’entre eux attend quelque chose de toi.
Une révision épinglée te garde où tu es. Un nouveau checkpoint n’atteint pas un déploiement épinglé
avant que tu ne changes l’épinglage, ce qui est le but de l’épinglage. PINNED_REVISIONS bouge avec la
bibliothèque, donc prendre un commit revu plus récent signifie mettre à jour Laya, pas éditer un SHA.
Les digests arrêtent le chargement, exprès. Ta carte a été générée depuis des octets que tu as revus.
Des octets différents lèvent ValueError avant que quoi que ce soit ne soit parsé :
ValueError: laya: SHA-256 mismatch for rl_agent_config.json: expected ae287b56…, got 25061739…
C’est la fonctionnalité qui marche, pas un bug à contourner. L’ordre compte :
- Découvre pourquoi les octets ont changé — une release intentionnelle, ou quelque chose que tu n’attendais pas.
- Révise le nouveau checkpoint.
- Régénère la carte depuis la copie revue.
- Déploie la nouvelle carte.
Ne saute pas à l’étape 3. Relancer le générateur contre ce qui vient d’arriver fait passer le contrôle et ne vérifie rien — il enregistre les nouveaux octets comme dignes de confiance parce qu’ils sont présents, ce qui est précisément l’état que le digest existait pour détecter.
Deux détails. Une nouvelle carte livrée via LAYA_SHA256_DIGESTS nécessite que le processus soit redémarré,
parce qu’un serveur en cours garde l’environnement avec lequel il a démarré. Et cette séquence ne s’applique
qu’à un déploiement qui n’est pas épinglé à une révision : avec les deux contrôles actifs, les nouveaux
octets n’arrivent jamais avant que tu ne déplaces l’épinglage.
Confirmer ce qui a réellement été chargé
Chaque agent enregistre le commit d’où il vient, None pour un répertoire local :
agent.revision # Agent and ONNXAgent
router.loaded_revisions # {"english": "55cf4c4e…", …} for each resident agent
agent.revision rapporte l’instantané vers lequel le téléchargement a résolu, en retombant sur ce que tu
as passé, donc épingler par branche ou tag renvoie ce nom plutôt qu’un SHA — épingle par SHA si tu veux que
ce champ en soit un. Charger depuis un répertoire local rapporte None, et revision y est ignoré, parce
qu’il n’y a pas d’instantané Hub à résoudre.
Le serveur rapporte la même chose, ce qui est le moyen le plus rapide de confirmer qu’un déploiement exécute le checkpoint que tu crois :
curl -s localhost:8000/health
# {"status":"ok","loaded":["english"],"revisions":{"english":"55cf4c4e…"},"device":"auto"}
laya-ts
Le paquet TypeScript reflète les parties épinglage et digest — revision, expectedSha256, et la relecture
de la révision. Il n’a pas d’équivalent de LAYA_SHA256_DIGESTS ni de serveur, donc les deux sections
ci-dessus ne s’y appliquent pas :
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>" },
});
Une révision explicite rejoint le chemin de cache sur disque sous ~/.cache/laya-ts/, donc des artefacts
épinglés différemment n’entrent jamais en collision. Dans le navigateur, la révision voyage dans l’URL de
requête à la place, ce qui indexe CacheStorage de la même façon. createNodeProvider accepte
expectedSha256 pour les graphes ONNX qu’il charge.
Ce que ça protège et ne protège pas
Cela détecte un checkpoint dont le contenu a changé par rapport à ce que tu as revu — une modification de dépôt en amont, un miroir compromis, un téléchargement corrompu, ou une copie locale modifiée.
Cela ne rend pas un checkpoint non revu sûr. Un digest dit seulement que les octets correspondent à ce que tu as enregistré ; décider que ces octets sont dignes de confiance reste ton affaire.
Trois limites à connaître avant de t’y fier :
- Seuls les fichiers listés sont vérifiés. Il n’y a pas de mode « tout vérifier » ni de moyen de refuser un fichier que tu n’as pas listé, donc un artefact absent de ta carte est chargé sans vérification. La carte est la frontière de la garantie.
- Un fichier parsé est donc en dehors.
tokenizer/tokenizer_config.jsonest parsé, mais Laya peut le normaliser et le réécrire immédiatement après le contrôle de digest, donc l’épingler peut réussir au premier chargement et échouer au suivant. La carte recommandée le laisse de côté pour cette raison, ce qui signifie que ses octets ne sont pas vérifiés. La réécriture dépend de ce que déclare le fichier, donc qu’elle ait lieu dépend du checkpoint. - La vérification n’a lieu qu’au chargement. Rien ne revérifie un fichier ensuite, qu’il soit remplacé par un attaquant ou par le processus lui-même.