Dokumentation

Checkpoint-Integrität

Laya lädt die Modellgewichte beim Laden vom Hugging Face Hub herunter. Standardmäßig nimmt es, worauf die Standardrevision des Repositorys zeigt, was bequem ist und was ein Offline-Cache bereits enthält. Wenn du lieber einen geprüften Commit pinnen oder das Laden eines Checkpoints verweigern möchtest, dessen Bytes sich geändert haben, sind beide verfügbar, und beide sind opt-in.

Nichts hier ändert, was Laya lädt, bis du es verlangst, das Hinzufügen dieser Optionen zu einem bestehenden Deployment ist also sicher. Beide sind auf Bibliotheksebene: Digests erreichen den HTTP-Server über eine Umgebungsvariable und das Revision-Pinning über LAYA_REVISION — siehe Eine Revision im Server pinnen.

Verwandt: Docker für Deployment-Variablen, laya.load und Agent und Router.

Eine Revision pinnen

Übergib revision an jeden Loader. Es akzeptiert einen Commit-SHA, einen Branch oder ein Tag und wird an den Hub weitergereicht.

import laya

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

Bevorzuge die geprüften SHAs, die mit Laya ausgeliefert werden, gegenüber einem eigenen Literal: sie werden mit den Checkpoints aktualisiert, diese Form kann also nicht veralten.

from laya import PINNED_REVISIONS

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

Router nimmt dieselbe revision, und revisions, um jeden Checkpoint separat zu pinnen. Die Schlüssel von PINNED_REVISIONS sind die drei eigenständigen Repositories, pinne einen Router also mit 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"],
})

Das ist wichtig, weil ein Standard-Router alle drei Checkpoints aus dem einen Bundle-Repository lädt (convaiinnovations/laya, mit multilingual/ und typed-decisions/ als Unterordner), und ein Commit-SHA aus laya-multilingual existiert nicht im Bundle-Repository. Ohne standalone_repos wird der Pin nicht etwa nur ignoriert — das Laden schlägt fehl. Wenn du lieber im Bundle-Repository bleiben willst, pinne es mit einem einzigen revision= für alle drei statt mit revisions= pro Modell.

Pinne alle drei, auch wenn du nur zwei auslieferst. Ein Router bietet jeden Checkpoint an, den er kennt, unabhängig davon, was du vorlädst, ein nicht gepinnter Eintrag ist also eine Routing-Entscheidung davon entfernt, ungepinnt geladen zu werden.

Warum Pinning nicht der Standard ist

Pinning als Standard würde das Laden aus einem älteren gecachten Snapshot brechen, was für On-Device- und Air-Gapped-Deployments wichtig ist: HF_HUB_OFFLINE=1 mit einem Cache, der älter ist als der Pin, würde aufhören zu funktionieren. Laya behält daher den Hub-Standard bei, sofern du keine Revision übergibst, und hält die geprüften SHAs bereit, wenn du sie willst.

Artefakt-Digests verifizieren

Eine gepinnte Revision sagt, welchen Commit du holst. Ein Digest sagt, welche Bytes du erwartest. Jede Datei, die du auflistest, wird gehasht, bevor eine von ihnen geparst wird und bevor Gewichte die Laufzeit erreichen. Die Map ist {path relative to the checkpoint: sha256 hex} — generiere sie zuerst und übergib sie dann.

Brauchst du beides?

Eine gepinnte Revision legt den Inhalt bereits fest: der Hub ist git, der Commit bestimmt also den Baum, und große Dateien werden über ihr eigenes SHA-256 adressiert. Wenn du pinnst und der Download erfolgreich ist, hast du die Bytes, die dieser Commit benennt. Der Digest ist also nicht dazu da, diese Prüfung zu wiederholen — er unterscheidet sich darin, worauf er vertraut.

Eine Revision fragt den Hub nach einem Commit und glaubt der Antwort. Ein Digest ist eine Aufzeichnung, die du erstellt und du behältst, bei jedem Laden verglichen. Das erkauft drei Dinge, die das Pinning nicht bietet:

  • Abdeckung für den häufigen Fall, der ungepinnt ist. Pinning ist opt-in und standardmäßig aus, die meisten Deployments folgen also einem beweglichen Branch. Ein Digest ist dann das Einzige, das eine Änderung bemerkt.
  • Eine Prüfung auf deiner eigenen Festplatte. Nach dem Download ist der Checkpoint gewöhnliche Dateien in einem Cache, den alles auf der Maschine bearbeiten kann. Nichts verifiziert sie zur Ladezeit erneut — außer einem Digest.
  • Unabhängigkeit von der Quelle. Wenn ein Mirror, ein Proxy oder der Hub selbst andere Bytes ausliefern würde, ist der Digest das einzige Kontrollmittel, das nicht die Sache unter Prüfung bittet, für sich selbst zu bürgen.

Diese Unabhängigkeit ist auch der Grund, warum das Generieren der Map ein manueller Schritt ist: ein Fingerabdruck hört in dem Moment auf, eine unabhängige Aufzeichnung zu sein, in dem die Sache, die er prüft, ihn für dich erzeugt.

Die Map generieren

Generiere sie aus einem Checkpoint, den du geprüft hast, statt Digests von irgendwo zu kopieren, auch nicht von dieser Seite. Es gibt bewusst keinen Befehl, der das für dich erzeugt: eine Map, die aus der Kopie berechnet wird, die Laya gerade heruntergeladen hat, würde diese Bytes hashen und sie dann gegen sich selbst verifizieren. Die Prüfung ist nur etwas wert, weil ein Mensch entschieden hat, dass dies die gewünschten Bytes waren, das Generieren der Map ist also der Schritt, in dem diese Entscheidung aufgezeichnet wird. Eine Map gehört zu genau einem Checkpoint: das Bundle-Repository enthält in seinem Wurzelverzeichnis (dem englischen Checkpoint) ein anderes rl_agent_config.json als in multilingual/, eine aus dem einen generierte Map schlägt also gegen den anderen fehl.

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)

Ein torch-Agent-Laden parst fünf Dateien, und das sind vier davon. (ONNXAgent liest eine andere Menge und akzeptiert zusätzlich die Schlüssel onnx und onnx_path, um den Graphen selbst zu hashen.) Die fünfte, tokenizer/tokenizer_config.json, wird bewusst ausgelassen: Laya kann sie nach der Verifikation normalisieren und zurückschreiben, in diesem Fall lässt das Pinnen sie das nächste Laden scheitern. Diese Neuschreibung ist bedingt — sie greift nur, wenn die Datei keinen tokenizer_class deklariert oder TokenizersBackend deklariert oder extra_special_tokens als Liste trägt —, auf manchen Checkpoints passiert sie also nie und das Pinnen der Datei würde zu funktionieren scheinen. Sie auszulassen ist die portable Wahl, und es bedeutet, dass eine geparste Datei unverifiziert ist. Siehe Was das schützt und was nicht.

Die Map verwenden

import json

import laya

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

Die Schlüssel sind Pfade relativ zum Checkpoint-Verzeichnis. Ein Mismatch löst ValueError aus, und eine aufgelistete Datei, die fehlt, löst FileNotFoundError aus. Dateien, die du nicht auflistest, werden gar nicht geprüft, die Map ist also auch die Definition dessen, was du schützt. Das funktioniert sowohl auf einem lokalen Verzeichnis als auch bei einem Hub-Download.

Ohne den Code anzufassen

LAYA_SHA256_DIGESTS enthält dieselbe Map als JSON und wird angewendet, wann immer ein Loader ohne explizites expected_sha256 aufgerufen wird:

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

Nichts erzeugt das für dich: der Wert ist deine eigene Map, aus einem Checkpoint, den du geprüft hast. Unter Docker muss er in der Umgebung sein, bevor compose startet, entweder exportiert wie oben oder in der .env-Datei, die compose liest — der Dienst reicht ${LAYA_SHA256_DIGESTS:-} durch, eine nicht gesetzte Variable bedeutet also still, dass keine Verifikation stattfindet:

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

Es gibt keine _FILE-Variante dieser Variable: diese Indirektion gibt es für Secrets, und eine Digest-Map ist kein Secret.

Benenne jeden Checkpoint, wenn ein Prozess mehr als einen lädt. Die Variable nimmt zwei Formen an, und die Werttypen sagen, welche:

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

Die flache Form ist die eigene Lesart von verify_digests und wendet dieselben Pfade auf alles an, auf einem Router kann sie also nur je einen Checkpoint treffen und lehnt die übrigen ab. Das gebündelte Repository liefert pro Checkpoint eine eigene model.safetensors und rl_agent_config.json, benenne sie also:

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

Ein Checkpoint, den die verschachtelte Map nicht nennt, bleibt bewusst ungepinnt statt ein Fehler zu sein, und ein Modellname, den der Router nicht kennt, löst eine Ausnahme aus, statt diesen Checkpoint unverifiziert zu lassen. en wird zu english aufgelöst, dieselbe Normalisierung, die Router(sha256_digests=...) anwendet.

Die beiden Routen unterscheiden sich in genau diesem letzten Punkt, worüber man leicht stolpert, wenn man beide nutzt. Ein Checkpoint, den die verschachtelte Map der Umgebung auslässt, wird auf eine leere Map gepinnt, eine flache Map kann also nicht in ihn durchsickern. Ein Checkpoint, der in Router(sha256_digests=...) im Code ausgelassen wird, hat gar keinen Eintrag, er fällt also weiterhin auf das zurück, was die Umgebung sagt. Benenne jeden Checkpoint, den du pinnen willst, in der Variante, die du verwendest.

Eine nicht gesetzte oder leere Variable bedeutet keine Verifikation, es ist also sicher, sie aus Umgebungen wegzulassen, die sie nicht brauchen. Fehlerhaftes JSON löst eine Ausnahme aus, statt die Prüfung still zu überspringen, und das Mischen der zwei Formen in einem Objekt wird namentlich abgelehnt.

Wie ein Mismatch im Server aussieht

Wie er auftaucht, hängt vom Preloading ab. Ein blankes laya-serve lädt standardmäßig vor (LAYA_PRELOAD=1), ein Mismatch scheitert also beim Start — laut und deterministisch. Die Container in diesem Repository setzen LAYA_PRELOAD=0 (compose.http.yaml, und Docker dokumentiert die Überschreibung), dort findet das erste Laden also bei einer Anfrage statt und nichts wird bis dahin verifiziert. Ein Mismatch ist dann ein 422 bei dem Ticket, das zu diesem Checkpoint routet: laya/serve.py bildet den ValueError auf HTTPException(422) ab und gibt den Digest-Text an den Aufrufer zurück. Eine aufgelistete, aber fehlende Datei löst stattdessen FileNotFoundError aus, was auf einen generischen 500 „inference failed” durchfällt, mit dem Grund nur im Container-Log.

Plane den 422 ein. Er sortiert sich in Logs, Dashboards und Alert-Regeln als Client-Fehler ein, der Standardort, an dem ein Operator nach einem kaputten Deployment sucht, ist also genau der Ort, an dem dieser nicht auftaucht.

Eine Revision im Server pinnen

LAYA_REVISION enthält einen Commit, einen Branch oder ein Tag, der auf jeden Checkpoint-Download angewendet wird, oder das Wort reviewed, das jedes Repository in PINNED_REVISIONS nachschlägt und sein eigenes SHA verwendet:

LAYA_REVISION=reviewed laya-serve

reviewed für ein Repository, für das die Tabelle keinen Eintrag hat, löst eine Ausnahme aus, statt es ungepinnt zu laden — ein Pin, der still zu nichts auflöst, ist genau der Fehler, den dieses Kontrollmittel verhindern soll. Ein explizites revision=-Argument gewinnt weiterhin gegen die Variable, und nicht gesetzt oder leer bedeutet „nicht verlangt”, ein HF_HUB_OFFLINE=1-Cache lädt also weiterhin genau wie zuvor.

Im Code nimmt Router beides pro Modell:

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

Digests sind immer pro Modell — es gibt kein routerweites Gegenstück zu revision, weil ein Commit-SHA über Checkpoints hinweg geteilt werden kann und ein Digest nicht. Siehe Docker für die Deployment-Variablen und Router für den vollständigen Konstruktor.

Wenn ein Checkpoint aktualisiert wird

Die beiden Kontrollmittel verhalten sich unterschiedlich, und nur eines von ihnen braucht etwas von dir.

Eine gepinnte Revision hält dich, wo du bist. Ein neuer Checkpoint erreicht ein gepinntes Deployment erst, wenn du den Pin änderst, was der Sinn des Pinnens ist. PINNED_REVISIONS bewegt sich mit der Bibliothek, einen neueren geprüften Commit zu übernehmen bedeutet also, Laya zu aktualisieren, nicht, einen SHA zu bearbeiten.

Digests stoppen das Laden, absichtlich. Deine Map wurde aus Bytes generiert, die du geprüft hast. Andere Bytes lösen ValueError aus, bevor irgendetwas geparst wird:

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

Das ist die Funktion, die arbeitet, kein Bug, den man umgehen muss. Die Reihenfolge ist wichtig:

  1. Finde heraus, warum sich die Bytes geändert haben — ein beabsichtigtes Release oder etwas, das du nicht erwartet hast.
  2. Prüfe den neuen Checkpoint.
  3. Generiere die Map aus der geprüften Kopie neu.
  4. Deploye die neue Map.

Überspringe Schritt 3 nicht. Den Generator erneut gegen das laufen zu lassen, was gerade angekommen ist, lässt die Prüfung durchgehen und verifiziert nichts — es zeichnet die neuen Bytes als vertrauenswürdig auf, weil sie vorhanden sind, was genau der Zustand ist, den der Digest erkennen sollte.

Zwei Details. Eine neue Map, die über LAYA_SHA256_DIGESTS geliefert wird, braucht einen Neustart des Prozesses, weil ein laufender Server die Umgebung behält, mit der er gestartet ist. Und diese Sequenz gilt nur für ein Deployment, das nicht revisionsgepinnt ist: mit beiden Kontrollmitteln an kommen die neuen Bytes nie an, bis du den Pin verschiebst.

Bestätigen, was tatsächlich geladen wurde

Jeder Agent zeichnet den Commit auf, aus dem er stammt, None für ein lokales Verzeichnis:

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

agent.revision meldet den Snapshot, zu dem der Download aufgelöst wurde, mit Rückfall auf das, was du übergeben hast, ein Pin per Branch oder Tag gibt also diesen Namen statt eines SHA zurück — pinne per SHA, wenn dieses Feld eines sein soll. Das Laden aus einem lokalen Verzeichnis meldet None, und revision wird dort ignoriert, weil es keinen Hub-Snapshot aufzulösen gibt.

Der Server meldet dasselbe, was der schnellste Weg ist zu bestätigen, dass ein Deployment den Checkpoint ausführt, den du denkst:

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

laya-ts

Das TypeScript-Paket spiegelt die Teile zu Pinning und Digests — revision, expectedSha256 und das Zurücklesen der Revision. Es hat kein Gegenstück zu LAYA_SHA256_DIGESTS und keinen Server, die beiden Abschnitte oben gelten also nicht für es:

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

Eine explizite Revision tritt dem On-Disk-Cache-Pfad unter ~/.cache/laya-ts/ bei, unterschiedlich gepinnte Artefakte kollidieren also nie. Im Browser reist die Revision stattdessen in der Anfrage-URL, was CacheStorage auf dieselbe Weise schlüsselt. createNodeProvider akzeptiert expectedSha256 für die ONNX-Graphen, die es lädt.

Was das schützt und was nicht

Es erkennt einen Checkpoint, dessen Inhalt sich gegenüber dem geändert hat, was du geprüft hast — eine Bearbeitung im Upstream-Repository, ein kompromittierter Mirror, ein beschädigter Download oder eine veränderte lokale Kopie.

Es macht einen ungeprüften Checkpoint nicht sicher. Ein Digest sagt nur, dass die Bytes mit dem übereinstimmen, was du aufgezeichnet hast; zu entscheiden, dass diese Bytes vertrauenswürdig sind, bleibt weiterhin deine Sache.

Drei Grenzen, die du kennen solltest, bevor du dich darauf verlässt:

  • Nur aufgelistete Dateien werden geprüft. Es gibt keinen „alles verifizieren”-Modus und keine Möglichkeit, eine nicht aufgelistete Datei abzulehnen, ein Artefakt, das in deiner Map fehlt, wird also unverifiziert geladen. Die Map ist die Grenze der Garantie.
  • Eine geparste Datei liegt daher außerhalb davon. tokenizer/tokenizer_config.json wird geparst, aber Laya kann sie normalisieren und unmittelbar nach der Digest-Prüfung zurückschreiben, das Pinnen der Datei kann also beim ersten Laden gelingen und beim nächsten fehlschlagen. Die empfohlene Map lässt sie aus diesem Grund weg, was bedeutet, dass ihre Bytes nicht verifiziert werden. Die Neuschreibung hängt davon ab, was die Datei deklariert, ob sie passiert, hängt also vom Checkpoint ab.
  • Die Verifikation findet nur zur Ladezeit statt. Nichts prüft eine Datei danach erneut, ob sie nun von einem Angreifer oder vom Prozess selbst ersetzt wurde.