33.3 torch.export és ONNX

33.3.1 Cél, előismeret és induló kérdés

A 33.2. Batch és kis szolgáltatás egység a tanított modellt helyi batch-szolgáltatásként futtatta a teljes PyTorch-keretben. Most azt vizsgáljuk, hogyan adható át a modell olyan környezetnek, ahol a PyTorch-keret nem elérhető, és milyen feltételekkel állítható, hogy az exportált program ugyanazt a függvényt számolja. Előismeret: az nn.Module regisztrációja és state_dict-je (11.2. nn.Module és paraméterregisztráció), a train/eval viselkedéskülönbség (12.1. Tanítás, validáció és kiértékelési mód, 21.3. Dropout, súlyregularizálás és augmentáció), valamint az input-szerződés és batch-kezelés (33.2. Batch és kis szolgáltatás). Induló kérdés: ha egy exportáló eszköz egyetlen példabemeneten azonos kimenetet ad az eredeti modullal, következik-e, hogy minden megengedett bemeneten azonos? A válasz nem; ebben az alfejezetben pontosan megadjuk, milyen típusú állítást lehet és érdemes ellenőrizni.

33.3.2 Definíciók: export, ExportedProgram, dinamikus alak

Legyen M:𝒜→𝒴M:\mathcal A\to\mathcal Y a tanított modul eval módban rögzített viselkedésű függvénye. A fejezet főútján 𝒜\mathcal A a N×1×24×24N\times 1\times 24\times 24 alakú, float32, [0,1][0,1] értékkészletű GlyphLab-képek halmaza, 𝒴\mathcal Y pedig az N×3N\times 3 logit-tér. Az export egy EE eljárás, amely az MM modulból, egy xref∈𝒜x_{\text{ref}}\in\mathcal A referencia-bemenetből és egy DD dinamikusalak-specifikációból egy PP exportált programot készít:

P=E(M;xref,D).P=E(M;\;x_{\text{ref}},D).

A torch.export esetén PP egy ExportedProgram, amely három, elkülönítetten ellenőrizhető részből áll:

Objektum Tartalom Ellenőrzés típusa
gráfmodul a számítási gráf operátorokkal és alakokkal statikus operátor- és alakelemzés
state_dict paraméterek és pufferek konstansként érték-összevetés a forrásmodullal
range_constraints dinamikus tengelyek korlátai guard-teszt a határokon

A dinamikus alak azt jelenti, hogy egy tengely hossza nem egyetlen, az exportáláskor látott értékre rögzül, hanem egy megadott intervallumból vehet fel értéket. A batch tengelyre például

dbatch=Dim("batch",min=2,max=16)d_{\text{batch}}=\mathrm{Dim}(\text{"batch"},\min=2,\max=16)

azt a guardot írja elő, hogy PP csak olyan NN batch-méretre garantált, amelyre N∈{2,…,16}N\in\{2,\dots,16\}. A térbeli 24×2424\times 24 méret a GlyphLab-szerződésben rögzített, ezért statikus maradhat. A guard megsértése nem néma rossz eredményt, hanem explicit constraint-hibát ad. A N=1N=1 batch külön eset lehet a PyTorch alak-specializációja miatt; ha az alkalmazás egyetlen képes inference-t is előír, azt önálló exporttal és teszttel is támogatni kell.

Az ONNX-exporter az ExportedProgramból platformfüggetlen gráfcsere-formátumú modellt készít. A 2.14-es főút az export-alapú exporter, amelyet a dynamo útvonal kapcsol be [D20]. Az ONNX-fájl a súlyokat konstansként tartalmazza, a dinamikus batch tengely pedig névvel ellátott dim_param lesz, amelyet a futtató runtime a tényleges bemeneti alakkal köt le.

33.3.3 Az export ekvivalenciájának numerikus és alakfeltételei

Az alábbi állítás mondja ki, mit ellenőrizhetünk véges kontrollhalmazon. Ez teszt típusú állítás, nem univerzális bizonyítás.

Definíció (ellenőrzött ekvivalencia). Legyen D⊆𝒜D\subseteq\mathcal A véges kontrollhalmaz, ε≥0\varepsilon\ge 0 abszolút és ρ≥0\rho\ge 0 relatív tolerancia. Azt mondjuk, hogy a PP exportált program (ε,ρ,D)(\varepsilon,\rho,D)-ellenőrzötten ekvivalens MM-mel, ha minden x∈Dx\in D esetén

shape(P(x))=shape(M(x)),dtype(P(x))=dtype(M(x)),\mathrm{shape}(P(x))=\mathrm{shape}(M(x)),\qquad \mathrm{dtype}(P(x))=\mathrm{dtype}(M(x)),

és a lapított kimenetekre vett maximumnormában

∥P(x)−M(x)∥∞≤ε+ρ∥M(x)∥∞.\|P(x)-M(x)\|_\infty \;\le\; \varepsilon+\rho\,\|M(x)\|_\infty .

A három feltétel három különböző hibát fog meg: az alakfeltétel a rejtett broadcasting- vagy reduction-hibát, a dtype-feltétel a véletlen float64/float16 váltást, a norma-egyenlőtlenség pedig az értékeltérést. A ρ\rho tag azért kell, mert nagy abszolút értékű logitoknál a lebegőpontos kerekítés arányosan nagyobb lehet; a nullához közeli kimenetnél a ρ∥⋅∥\rho\|\cdot\| tag eltűnik, és az ε\varepsilon abszolút korlát marad érvényben.

Tétel (affin kontrollredukció). Legyenek M(x)=Wx+bM(x)=Wx+b és P(x)=W′x+b′P(x)=W'x+b' affin leképezések ℝd→ℝm\mathbb R^d\to\mathbb R^m. Ha P(x)=M(x)P(x)=M(x) minden x∈{0,e1,…,ed}x\in\{0,e_1,\dots,e_d\} kontrollpontban, ahol eie_i az ii-edik standard bázisvektor, akkor P=MP=M az egész ℝd\mathbb R^d-n.

Bizonyítás. Legyen Δ(x)=P(x)−M(x)=(W′−W)x+(b′−b)\Delta(x)=P(x)-M(x)=(W'-W)x+(b'-b). Mivel Δ(0)=0\Delta(0)=0, ezért b′=bb'=b. Minden i=1,…,di=1,\dots,d esetén Δ(ei)=0\Delta(e_i)=0, tehát a W′−WW'-W mátrix ii-edik oszlopa nullvektor. Mivel ez minden oszlopra igaz, W′=WW'=W, így Δ≡0\Delta\equiv 0. ▫\square

A tétel azt mondja meg, hogy affin modelleknél a d+1d+1 elemű báziskontroll elegendő a teljes bemeneti tér ellenőrzéséhez; a valós CNN-exportnál a modell nemlineáris, ezért a véges kontrollhalmaz csak a kiválasztott pontokra ad bizonyítékot. A gyakorlati protokoll ezért a deklarált dinamikus batch tengely szélsőértékeit (N=2N=2 és N=NmaxN=N_{\max}), egy köztes értéket és a rögzített térbeli méretet fedi le.

33.3.4 Kézi kis példa, majd minimális kód

Legyen d=2d=2, m=2m=2,

W=(1−120,5),b=(0,1−0,2),x=(0,51,0).W=\begin{pmatrix}1 & -1\\ 2 & 0{,}5\end{pmatrix},\qquad b=\begin{pmatrix}0{,}1\\ -0{,}2\end{pmatrix},\qquad x=\begin{pmatrix}0{,}5\\ 1{,}0\end{pmatrix}.

A kötelező Z=XW⊤+bZ=XW^\top+b konvenció szerint egy soros bemenetre:

y=(0,51,0)(12−10,5)+(0,1−0,2)=(−0,41,3).y=\begin{pmatrix}0{,}5 & 1{,}0\end{pmatrix} \begin{pmatrix}1 & 2\\ -1 & 0{,}5\end{pmatrix} +\begin{pmatrix}0{,}1 & -0{,}2\end{pmatrix} =\begin{pmatrix}-0{,}4 & 1{,}3\end{pmatrix}.

A kód ugyanezt adja; a nn.Linear súlyalakja (m,d)=(2,2)(m,d)=(2,2), azaz a fenti WW közvetlenül beírható.

# munkakönyvtár: examples/n131/
import torch
from torch import nn

torch.manual_seed(20261020)
model = nn.Linear(2, 2, dtype=torch.float32)
with torch.no_grad():
    model.weight.copy_(torch.tensor([[1.0, -1.0], [2.0, 0.5]]))
    model.bias.copy_(torch.tensor([0.1, -0.2]))
model.eval()

x = torch.tensor([[0.5, 1.0]], dtype=torch.float32)
with torch.no_grad():
    y = model(x)
print(y)  # tensor([[-0.4000,  1.3000]])

Az export utáni kontrollnak ugyanezt a −0,4;1,3-0{,}4;1{,}3 értéket kell visszaadnia; ha az exportált program mást ad, az értékhiba, nem kerekítési kérdés.

33.3.5 Labor: kis GlyphLab fej exportja és kontrollösszevetés

A labor egyetlen konvolúciós fejet exportál, amely a GlyphLab bemeneti szerződését használja: float32, N×1×24×24N\times 1\times 24\times 24, [0,1][0,1] intenzitás. A modellt az export előtt eval() módba tesszük, mert az export a pillanatnyi viselkedést rögzíti; a dropout és a BatchNorm train-beli véletlenítése/statisztikája így nem kerül a gráfba. A kód futtatható, de az írás során nem futott; a kimeneti sorok jelölt szemléltetések.

# munkakönyvtár: lab/
# Futtatás: python export_check.py
import torch
from torch import nn
from torch.export import Dim, export  # API-ellenőrzés: [D19]

class MiniGlyphHead(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 4, 3, padding=1),
            nn.ReLU(),
            nn.AdaptiveAvgPool2d((1, 1)),
            nn.Flatten(),
            nn.Linear(4, 3),
        )
    def forward(self, x):
        return self.features(x)

torch.manual_seed(20261020)
model = MiniGlyphHead().eval()
x_ref = torch.rand(2, 1, 24, 24, dtype=torch.float32)

with torch.no_grad():
    y_ref = model(x_ref)

batch = Dim("batch", min=2, max=16)
ep = export(model, (x_ref,), dynamic_shapes={"x": {0: batch}})

with torch.no_grad():
    y_exp = ep.module()(x_ref)

print("shape:", y_ref.shape, y_exp.shape)
print("dtype:", y_ref.dtype, y_exp.dtype)
err = (y_ref - y_exp).abs().max().item()
print("max_abs_err:", err)   # jelölt szemléltetés: 0.0 vagy 1e-7 alatt várható

# Opcionális ONNX-export és onnxruntime-összevetés [D20].
# A futás külön környezetet és onnx/onnxruntime telepítést igényel.
# torch.onnx.export(ep, (x_ref,), "glyph_head.onnx", dynamo=True)

A kontrollnak három dolgot kell visszaadnia: azonos shape-ot (2×32\times 3), azonos dtype-ot (torch.float32) és a definíció szerinti norma-egyenlőtlenséget. Az ONNX-runtime-összevetésnél a két futtatókörnyezet eltérő kernelsorrendet használhat, ezért bitazonosságot nem ígérünk; a tolerancia legyen például ε=10−5\varepsilon=10^{-5}, ρ=10−5\rho=10^{-5}.

A runtime fogalma itt kettéválik: az ExportedProgram a PyTorch kereten belül futtatható az ep.module() hívással, míg az ONNX-fájl egy külső runtime-ra, például onnxruntime-ra bízza a kiértékelést. Az előbbi ellenőrzése a PyTorch-környezetben marad, az utóbbié már a céleszköz numerikus viselkedését is méri.

33.3.6 Feladatok

F1 (fogalmi, 1. nehézség). Mondja meg, miben különbözik az ExportedProgram a korábbi torch.jit.trace kimenetétől. Adjon pszeudokód-példát olyan modulra, amelyben egy if x.sum() > 0 elágazás van, és magyarázza el, miért nem szabad a régi TorchScript-trace példákat ellenőrzés nélkül átvenni a jelenlegi export főútjaként.

F2 (levezetési, 2. nehézség). Bizonyítsa be a fenti affin kontrollredukciós tételt, majd mutassa meg egy ellenpéldával, hogy nemlineáris MM-re a {0,e1,e2}\{0,e_1,e_2\} kontrollhalmaz nem elegendő: válassza az M(x1,x2)=x1x2M(x_1,x_2)=x_1x_2 függvényt.

F3 (implementációs, 2. nehézség). Írjon teljes Python-szkriptet, amely egy rögzített súlyú nn.Linear(2,2) modult eval módban exportál, majd a {0,e1,e2,e1+e2}\{0,e_1,e_2,e_1+e_2\} kontrollhalmazon asszertálja, hogy az exportált program kimenete 10−610^{-6} maximumnorma-eltérésen belül egyezik az eredetivel. A seed, a dtype és a futási parancs szerepeljen.

F4 (hibakeresési, 2. nehézség). Az alábbi vázlat hibás: egy dropoutot tartalmazó modellt train módban exportálnak, majd eval módban vetnek össze. A dropout itt szándékosan p=1p=1, ezért a train- és eval-viselkedés biztosan különbözhet, és az eltérés a véletlen modellsúlyoktól is függ.

model = nn.Sequential(nn.Dropout(p=1.0), MiniGlyphHead()).train()
x = torch.rand(2, 1, 24, 24)
ep = export(model, (x,))        # train módban
model.eval()
y1 = model(x)
y2 = ep.module()(x)
print((y1 - y2).abs().max())    # eltérés várható (súlyfüggő)

Lokalizálja a hibát, mondja meg a következményét egy BatchNormos modellnél, és adja meg a javítást.

F5 (kísérleti/transzfer, 3. nehézség). Tervezzen export-ellenőrzési protokollt a GlyphLab MiniGlyphHead modelljéhez. Válassza meg a kontrollhalmazt a test splitből, a dinamikus batch-tartományt, a toleranciát az ONNX-runtime-összevetéshez, és indokolja, miért nem elegendő egyetlen tesztkép. Különböztesse meg a numerikus egyezést a szemantikai ekvivalenciától.

33.3.7 Megoldások és indoklás

F1. A torch.jit.trace a megadott példabemenet által ténylegesen bejárt utat rögzíti. Ha a modulban if x.sum() > 0: return 2*x else: return 3*x szerepel, akkor a pozitív összegű példán a trace csak a 2*x ágat tartalmazza; a másik ág örökre eltűnik, és negatív összegű bemenetre is 2*x-et ad. A torch.export ezzel szemben a teljes modul-kódot elemzi, és a bemenettől függő Python-vezérlést vagy konstans feltételként kezeli, vagy az export folyamatát nem támogatott, adatfüggő vezérlés miatt hibával megszakítja; a torch.export nem a grafikon hallgatólagos megszakításával, és nem is a trace-hez hasonló észrevétlen ágvesztéssel garantálja a teljes gráfot. A régi TorchScript-példa ezért nem aktuális főút; az [D19] státusza miatt a pontos viselkedést a 2.14-es dokumentációban kell megerősíteni.

F2. A tétel bizonyítása megegyezik a főszövegben adott levezetéssel: Δ(0)=0\Delta(0)=0 adja b′=bb'=b-t, Δ(ei)=0\Delta(e_i)=0 adja a W′−WW'-W mátrix ii-edik oszlopának nullvektor voltát, amiből W′=WW'=W. Az ellenpélda: M(x1,x2)=x1x2M(x_1,x_2)=x_1x_2 a {0,e1,e2}\{0,e_1,e_2\} pontokon mindenütt 00-t ad, tehát bármely P≡0P\equiv 0 függvény ezeken egyezik vele; ugyanakkor M(1,1)=1≠0=P(1,1)M(1,1)=1\ne 0=P(1,1), tehát a báziskontroll nem bizonyítja az ekvivalenciát.

F3. Teljes szkript:

# munkakönyvtár: examples/n131/
# Futtatás: python f3_affin_export.py
import torch
from torch import nn
from torch.export import Dim, export  # API-ellenőrzés: [D19]

torch.manual_seed(20261020)
model = nn.Linear(2, 2, dtype=torch.float32)
with torch.no_grad():
    model.weight.copy_(torch.tensor([[1.0, -1.0], [2.0, 0.5]]))
    model.bias.copy_(torch.tensor([0.1, -0.2]))
model.eval()

# A dinamikus batchhez ne egyetlen soros referenciabemenetet adjunk:
# az 1-es méretet a tracer specializálhatja.
x_ref = torch.rand(4, 2, dtype=torch.float32)
batch = Dim("batch", min=2, max=32)
ep = export(model, (x_ref,), dynamic_shapes=({0: batch},))

def check(x):
    with torch.no_grad():
        y_ref = model(x)
        y_exp = ep.module()(x)
    assert y_ref.shape == y_exp.shape, (y_ref.shape, y_exp.shape)
    assert y_ref.dtype == y_exp.dtype
    err = (y_ref - y_exp).abs().max().item()
    assert err < 1e-6, err
    return err

# Az affin kontrollhalmaz négy pontját egyetlen N=4 batchben értékeljük.
controls = torch.tensor([
    [0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]
], dtype=torch.float32)
print("controls max error:", check(controls))
# Külön ellenőrizzük a dinamikus batch másik megengedett méretét is.
print("batch 2 max error:", check(controls[:2]))

A kód a négy kontrollpontot egy N=4N=4 batchben és két kontrollpontot N=2N=2 méreten ellenőrzi; a várható maximális eltérés kerekítési pontosságon belül nulla. Az N=1N=1 referenciabemenetet itt tudatosan kerüljük, mert a dinamikus dimenziót a tracer 1-es méret esetén statikusra specializálhatja; a 00 kontrollpont a bias, az eie_i pontok a súlyoszlopok, az e1+e2e_1+e_2 pedig a teljes összegzés együttes ellenőrzése.

F4. A hiba oka: az export train módban történt, ezért az exportált program a train-beli viselkedést rögzítette. A példában a dropout train módban p=1p=1 mellett nullázza a bemenetet, eval módban viszont változatlanul átengedi; a MiniGlyphHead önmagában nem tartalmaz módérzékeny réteget, ezért az eredeti, csak azt használó ellenpélda hibás lett volna. BatchNormos modulnál train módban a batch-statisztikát használná, eval módban pedig a running statisztikát; a két kimenet így általában eltér. Javítás: model.eval() az exportálás előtt kell meghívni, és utána a kontroll-összevetést is eval módban kell végezni.

F5. Protokollvázlat. Megfigyelési egység: egy teszt-jelenet/nézet pár; a test split 30 jelenetből 60 képet tartalmaz, és a modellválasztásban nem vett részt. Kontrollhalmaz: a 60 tesztképből választott, scene-szinten rétegzett 12 kép, amely a három osztályt egyenlő arányban fedi le. Dinamikus batch: Dim("batch", min=2, max=16), a guard-teszt N=2, N=8, N=16 méreteken; N=1 esetén külön export- és összevetési teszt szükséges. Tolerancia: PyTorch–ExportedProgram összevetésre ε=10−6\varepsilon=10^{-6}, ρ=10−6\rho=10^{-6}; ONNX-runtime-ra ε=10−5\varepsilon=10^{-5}, ρ=10−5\rho=10^{-5}, mert a runtime eltérő kernelsorrendet használhat. Egyetlen tesztkép nem elegendő, mert nem fedi le sem a batch-tengely szélsőértékeit, sem a csatornák közötti broadcast- és reduction-utakat. A numerikus egyezés a kimeneti tenzorok közelsége; a szemantikai ekvivalencia azt állítaná, hogy a program minden megengedett bemenetre ugyanazt a függvényt valósítja meg. A véges kontrollhalmazon mért egyezés az előbbit igazolja a kiválasztott pontokra, az utóbbit nem bizonyítja.

33.3.8 Tipikus tévedés, önellenőrzés, visszahívás

Tipikus tévedés. „A torch.export ugyanaz, mint a régi torch.jit.trace/script, ezért a korábbi TorchScript-példákat változatlanul átvehetjük.” Ez hamis. A trace a példabemenet által bejárt konkrét ágat rögzíti; az inputfüggő elágazás többi ága elveszik. A script nyelvi részhalmazra korlátoz, és a típuskényszer más. Az aktuális export főút a dynamo-alapú trace, amely a nem támogatott Python-vezérlést teljes gráfra kiterjedő export esetén hibával elutasíthatja. A [D19] státusza miatt a pontos API-viselkedést a 2.14-es dokumentációban újra kell olvasni; a régi példa átvétele előtt tehát ellenőrizni kell, nem feltételezni.

Önellenőrzés. A kézi affin példa alakjai: WW (2,2)(2,2), bb (2,)(2,), xx (1,2)(1,2), yy (1,2)(1,2); a számított −0,4-0{,}4 és 1,31{,}3 a kódban ellenőrizhető. A labor seedje 20261020, dtype-ja float32, eszköze CPU; a nem futtatott kimenetek jelölt szemléltetések. A (ε,ρ)(\varepsilon,\rho)-feltétel nullközeli kimenetnél az abszolút, nagy kimenetnél a relatív tagot teszi dominánssá, így nem válik üressé.

Visszahívás. A 21.3. Dropout, súlyregularizálás és augmentáció train/eval megkülönböztetése itt úgy tér vissza, hogy az export a pillanatnyi módot rögzíti; a 29.3. Regularizált latent reprezentáció tanulsága szerint a kis kontrollhalmazon mért nulla hiba nem általános szemantikai garancia; a 32.3. Vision Transformer és hibrid felhasználás modellcsalád-összevetése pedig arra emlékeztet, hogy az export az operátorok szintjén dolgozik: CNN és ViT akkor exportálható ugyanazzal az útvonallal, ha az operátoraik támogatottak.

33.3.9 Rövid összefoglaló

A torch.export eval módban rögzített modulból ExportedProgramot készít, amely a gráfot, a súlyokat és a dinamikus tengelyek guardjait együtt tartalmazza. A bemutatott dinamikus batch a Dim("batch", min=2, max=16) korláttal adható meg; a guard megsértése explicit hiba. Az export ekvivalenciáját alak-, dtype- és (ε,ρ)(\varepsilon,\rho)-normafeltételekkel ellenőrizzük véges kontrollhalmazon; affin modellnél a d+1d+1 bázispont igazolja a teljes egyezést, nemlineáris CNN-nél a kontroll csak teszt-bizonyíték. Az ONNX-exporter az export-alapú útvonalból dolgozik, a külső runtime numerikus egyezését külön toleranciával kell mérni. A régi TorchScript-példa nem aktuális főút.

33.3.10 Következő alkalmazás és források

A következő egység, 33.4. Üzemeltetés, drift és késő címke, az exportált modellt üzemeltetésbe helyezi: ott a bemeneti szerződés, a driftfigyelés és a későn érkező címkék kérdése épül arra, hogy az átadott program viselkedését milyen feltételekkel ismerjük.

Felhasznált források: