33.2 Batch és kis szolgáltatás

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

A 33.1. Inference-szerződés és modellcsomag alfejezetben felépített modellcsomag akkor válik használható eszközzé, ha a súlyokat a tanítási ciklustól elválasztva, kiszolgálási úton is alkalmazni tudjuk. Itt egy helyi batch-inference parancsot építünk, amely a csomag state_dict-jét tölti be, validálja a bemenetet, és CSV-formátumú predikciót ír ki. Előismeret: a modellcsomag tartalma, az eval mód és az inference-szerződés 33.1. Inference-szerződés és modellcsomag alfejezetből.

Induló kérdés: egy kolléga uint8, 0–255 skálájú .npy tömböt ad a modellnek, „hiszen a kép ugyanaz”. A modell numerikusan lefuthat, a predikció mégis érvénytelen; ennek okát az elfogadott bemeneti szerződés formális rögzítésével látjuk be.

33.2.2 1. Request, batch CLI, model id és local service

Definíció (request). A request a szolgáltatás felé küldött üzenet, amely legalább a modellazonosítót és a bemenetet tartalmazza. Egyetlen kép esetén a bemenet alakja X∈ℝ1×C×H×WX\in\mathbb R^{1\times C\times H\times W}; batch esetén X∈ℝN×C×H×WX\in\mathbb R^{N\times C\times H\times W}, ahol N≥1N\ge 1 a feldolgozandó minták száma. A request nem azonos a modell forwardjával: a request érvényességét a séma, az eredményét a modell határozza meg.

Definíció (model id). A modellazonosító a csomag verziójához és a súlyok lenyomatához kötött string. A GlyphLab-osztályozó esetében ez glyph-orientation-v1. Az azonosító a szerződés része: a kliens ezzel jelzi, mely csomagot hívja; a szolgáltatás ismeretlen azonosítóra explicit hibát ad, nem „valamilyen” modellt futtat.

Definíció (batch CLI). Batch parancssori eszköznek nevezzük azt a nem interaktív programot, amely egy bemeneti batch-fájlt olvas, egyetlen előrefelé menetet végez, és a predikciókat fájlba írja. Nincs tréninglépés, nincs paraméterfrissítés, nincs validációs ciklus.

Definíció (local service). Helyi szolgáltatásnak nevezzük azt a folyamatot, amely loopback vagy HTTP végponton keresztül fogad requesteket, és válaszüzenetet ad. Ebben az egységben a szolgáltatást csak specifikáció szintjén adjuk meg; a futtatható főút a batch CLI.

Definíció (latency). Egy request késleltetése Tlat=tresp−treqT_{lat}=t_{resp}-t_{req}, ahol treqt_{req} a kérés beérkezésének, trespt_{resp} a válasz elküldésének időpontja. Batch esetén TbatchT_{batch} a teljes köteg késleltetése; az amortizált minta-késleltetés Tbatch/NT_{batch}/N. A kettő nem felcserélhető: a második nem egyetlen kérés válaszideje.

33.2.3 2. A bemeneti séma: alak, csatorna és értéktartomány

A modell nem „bármilyen képet” fogad, hanem csak a tanítási adat szerződésének megfelelő tenzort. A szerződést formálisan rögzítjük.

Definíció (elfogadott GlyphLab képbatch). Legyen a glyph-orientation-v1 csomag bemeneti halmaza 𝒳v1\mathcal X_{v1}. A tenzor XX pontosan akkor eleme 𝒳v1\mathcal X_{v1}-nek, ha teljesül mindhárom feltétel:

  1. Dtype. XX lebegőpontos típusa float32.
  2. Alak. X∈ℝN×C×H×WX\in\mathbb R^{N\times C\times H\times W} valamely N≥1N\ge 1 egészre, továbbá C=1C=1, H=24H=24, W=24W=24; azaz a csatorna-térbeli alak 1×24×241\times24\times24.
  3. Értéktartomány. Minden n∈{0,…,N−1}n\in\{0,\ldots,N-1\}, i,j∈{0,…,23}i,j\in\{0,\ldots,23\} indexre a pixelértékre Xn,0,i,j∈[0,1],X_{n,0,i,j}\in[0,1], és az érték nem NaN.

A három feltétel más-más hibát véd. Az alakfeltétel a konvolúciós és affin rétegek bemeneti dimenzióját rögzíti; a csatornafeltétel a grayscale képet írja elő, mivel a modell első konvolúciójának súlya ℝCout×1×kh×kw\mathbb R^{C_{out}\times1\times k_h\times k_w} alakú; az értéktartomány a GlyphLab generátor normalizálási szerződését tükrözi, amelyben az intenzitás float32 és [0,1][0,1]-beli. A 0–255 skálájú uint8 kép tehát két feltételt is sért: a dtype-ot és az értéktartományt.

Állítás (a validátor pontosan a szerződést ellenőrzi). A validate_batch függvény pontosan akkor tér vissza hiba nélkül, ha a bemenete 𝒳v1\mathcal X_{v1}-ben van.

Bizonyítás. A függvény ágai a definíció konjunkcióinak közvetlen kódolása: ellenőrzi a dtype-ot, az ndim értéket, az N,C,H,WN,C,H,W alakot, a nemnegatív batchméretet, a végességet, végül a [0,1][0,1] tartományt. Minden feltétel megsértése kivételt vált ki, ezért a hiba nélküli visszatérés minden feltétel teljesülését jelenti. A feltételek definíció szerint pontosan 𝒳v1\mathcal X_{v1} tagságát adják, tehát a függvény a halmaz karakterisztikus ellenőrzése. A NaN-t a harmadik feltételben a [0,1][0,1] intervallumtól való eltérés zárja ki; a kódban a pontos hibaüzenet érdekében külön végességi ellenőrzést is végzünk. ◼\blacksquare

A megfelelőség nem numerikus heurisztika, hanem a definícióból következő egyezés. Az viszont külön eldöntendő, hogy egy adott .npy fájl ténylegesen eleget tesz-e a szerződésnek; ezt a batch CLI a modellhívás előtt hajtja végre.

33.2.4 3. Kézi kis példa a validátoron

Tekintsünk négy bemenetet:

A definíció alapján XA∈𝒳v1X^A\in\mathcal X_{v1}, tehát a validátor elfogadja. XBX^B az 1. feltételt sérti, XCX^C a 2. feltételt; a validátor mindkettőt elutasítja. XDX^D a 3. feltételt sérti, mert a NaN nem eleme a [0,1][0,1] intervallumnak.

Ennek oktatási szerepe kettős. Egyrészt mutatja, hogy a helyes predikcióhoz a bemeneti szerződés betartása szükséges; másrészt megmutatja, hogy a „majdnem jó” bemenet is elutasítandó, mert a modell hallgatólagosan más eloszláson tanult.

33.2.5 4. Labor: helyi batch-inference parancs

A labor a 33.1. Inference-szerződés és modellcsomag alfejezetben készített csomagot használja: package/model.py tartalmazza a GlyphCNN architektúrát, ckpt/glyph-orientation-v1.pt a state_dict-et. A GlyphCNN bemeneti csatornája 1, bemeneti térbeli mérete 24×2424\times24, kimenete 3 logit. A futtatás CPU-n történik.

Bemeneti fájl: data/images.npy, alak (360,1,24,24)(360,1,24,24), float32, [0,1][0,1]. A parancs:

python lab/batch_infer.py \
  --checkpoint ckpt/glyph-orientation-v1.pt \
  --images data/images.npy \
  --output results/predictions.csv \
  --model-id glyph-orientation-v1

A fő kód:

import argparse, csv, sys, time
from pathlib import Path
import numpy as np
import torch

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from package.model import GlyphCNN

MODEL_ID = "glyph-orientation-v1"
C_EXP, H_EXP, W_EXP = 1, 24, 24

def validate_batch(x: np.ndarray) -> None:
    if not isinstance(x, np.ndarray):
        raise TypeError("a bemenetnek np.ndarray-nek kell lennie")
    if x.dtype != np.float32:
        raise TypeError(f"dtype hiba: {x.dtype}; a várt float32")
    if x.ndim != 4:
        raise ValueError(f"alak hiba: 4D NCHW várt, kapott: {x.ndim}D")
    N, C, H, W = x.shape
    if N == 0:
        raise ValueError("üres batch nem fogadható el")
    if (C, H, W) != (C_EXP, H_EXP, W_EXP):
        raise ValueError(f"alak hiba: 1x24x24 várt, kapott: {C}x{H}x{W}")
    if not np.all(np.isfinite(x)):
        raise ValueError("nem véges pixelérték (NaN vagy inf)")
    if np.any(x < 0.0) or np.any(x > 1.0):
        raise ValueError("pixelértékeknek a [0,1] tartományban kell lenniük")

def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--checkpoint", required=True)
    ap.add_argument("--images", required=True)
    ap.add_argument("--output", required=True)
    ap.add_argument("--model-id", default=MODEL_ID)
    args = ap.parse_args()

    if args.model_id != MODEL_ID:
        raise ValueError(f"ismeretlen modellazonosító: {args.model_id}")

    x = np.load(args.images)
    validate_batch(x)

    model = GlyphCNN()
    state = torch.load(args.checkpoint, map_location="cpu", weights_only=True)
    if isinstance(state, dict) and "model_state_dict" in state:
        state = state["model_state_dict"]
    model.load_state_dict(state)
    model.eval()

    t0 = time.perf_counter()
    with torch.inference_mode():
        logits = model(torch.from_numpy(x))
    probs = torch.softmax(logits, dim=1).numpy()
    elapsed = time.perf_counter() - t0

    with open(args.output, "w", newline="") as f:
        w = csv.writer(f)
        w.writerow(["image_idx", "predicted", "p0", "p1", "p2"])
        for i, (pred, p) in enumerate(zip(probs.argmax(axis=1), probs)):
            w.writerow([i, int(pred), float(p[0]), float(p[1]), float(p[2])])
    print(f"model_id={args.model_id} n={logits.shape[0]} "
          f"batch_latency_s={elapsed:.6f} output={args.output}")

if __name__ == "__main__":
    main()

A kód három ponton tükrözi a 33.1. Inference-szerződés és modellcsomag szerződést: a modell eval() állapotba kerül, a paraméterek nem kapnak gradienst, és a betöltés weights_only=True módon, csak state_dict-et fogad el. A torch.inference_mode() azt is rögzíti, hogy itt nem autograd-számítás folyik; ez különböző feladat, mint az eval 33.1. Inference-szerződés és modellcsomag szerinti állapotváltása. A --model-id ellenőrzés a csomag azonosítóját védi.

Ellenőrzés. A fenti futás kimenete results/predictions.csv. Az ellenőrző szkript:

import numpy as np, sys
rows = np.loadtxt(sys.argv[1], delimiter=",", skiprows=1, ndmin=2)
assert rows.shape[0] == 360
assert rows.shape[1] == 5
assert np.all((rows[:, 1] >= 0) & (rows[:, 1] <= 2))
assert np.allclose(rows[:, 2:5].sum(axis=1), 1.0, atol=1e-6)
print("batch inference ellenőrzés sikeres: 360 sor, "
      "predikciók 0..2, valószínűségsorösszegek 1 ± 1e-6")

A kimenet a csomag n129-ben rögzített ellenőrzési feltételeit ellenőrzi. A fenti eredmény jelzett szemléltetés, nem elvégzett mérés: az értékeket nem helyettesíthetik a laborban mért eredmények. A futást a laborban külön kell elvégezni.

33.2.6 5. Latency és a fejlesztői siker határa

A batch CLI késleltetésének méréséhez rögzíteni kell a bemeneti batch méretét, a hardvert, a melegítő hívások számát és az ismétlésszámot. Egy ismételhető kis mérés például: 10 melegítő hívás, majd 100 mért hívás, mindig ugyanazzal a 360 képes batch-csel; a kimenet p50 és p90 késleltetés. A p50 a medián, a p90 az az érték, amely alá a mért késleltetések legalább 90%-a esik; mindkettő tapasztalati kvantilis, nem modellből származtatott garancia.

A gyakori félreértés az, hogy egy fejlesztői gépen 20 kérésből 20 sikeres válasz a rendelkezésre állást bizonyítja. Nem bizonyítja: 0 hiba 20 próbából a binomiális arány 95%-os felső konfidenciahatárára a „rule of three” szerint körülbelül 3/20=0,153/20=0{,}15; azaz a mérés nem zárja ki, hogy a tényleges hibaarány 15% legyen. A termelési állításhoz terheléses, párhuzamos klienssel végzett, tartós mérés, erőforrás-korlát és hibatípusok szerinti bontás szükséges.

33.2.7 6. Loopback API specifikáció

A helyi szolgáltatás szerződése, amelyet a batch mellett utólag ismételt, egyedi kérésekre használhatunk:

POST /v1/predict
Content-Type: application/json

{
  "model_id": "glyph-orientation-v1",
  "image": {
    "shape": [1, 24, 24],
    "dtype": "float32",
    "values": [0.0, 0.5, ..., 1.0]
  }
}

A values mező pontosan 1⋅24⋅24=5761\cdot24\cdot24=576 elemet tartalmaz, mindegyik [0,1][0,1]-beli. Sikeres válasz:

200 OK
{
  "model_id": "glyph-orientation-v1",
  "predicted": 0,
  "probs": [0.92, 0.05, 0.03]
}

Sémahiba esetén 422, ismeretlen modellazonosítóra 404, belső hibára 500 a tervezett státuszkód; a 422 üzenetnek meg kell neveznie a sértett feltételt, nem elegendő a „hibás kérés” szöveg. A specifikáció loopback tesztje a szolgáltatásfüggvény közvetlen, HTTP nélküli hívása, így a hálózat nem része az ellenőrzésnek.

33.2.8 Feladatok

F1 (fogalmi, nehézség 1). Magyarázza el a különbséget a batch CLI és a local service között. Miért nem bizonyítja a batch CLI sikeres futása azt, hogy a loopback API is rendelkezésre áll?

F2 (levezetési/bizonyítási, nehézség 2). Bizonyítsa be, hogy rögzített NN mellett az 𝒳N={X∈ℝN×1×24×24:Xn,0,i,j∈[0,1]∀n,i,j}\mathcal X_N=\{X\in\mathbb R^{N\times1\times24\times24}: X_{n,0,i,j}\in[0,1]\ \forall n,i,j\} halmaz konvex. Ellenőrizze a bizonyítást a λ=0\lambda=0 és λ=1\lambda=1 szélsőesetben.

F3 (implementációs, nehézség 2). Írjon validate_image(x) függvényt, amely egyetlen 1×1×24×241\times1\times24\times24 képet ellenőriz, és minden hibát külön típusú kivétellel jelez. Tesztelje négy esetre: helyes kép, rossz dtype, rossz alak, −0,1-0{,}1 érték.

F4 (hibakeresési, nehézség 2). Az alábbi kód miért fogadja el a NaN-t tartalmazó bemenetet?

def validate(x):
    if not (np.any(x < 0.0) or np.any(x > 1.0)):
        return "elfogadva"
    return "elutasítva"

Adja meg a javítást és egy olyan bemenetet, amelyen a hibás változat „elfogadva”, a javított változat „elutasítva” eredményt ad.

F5 (kísérleti/transzfer, nehézség 3). Tervezze meg a batch CLI késleltetésmérését úgy, hogy a mérésből p50 és p90 értéket lehessen jelenteni. Írja le a melegítés, ismétlés, batchméret, hardver és kiugró mérések kezelését, valamint azt, mely állítás nem vonható le a mérésből.

33.2.9 Megoldások és indoklás

F1. A batch CLI egyszeri parancs: bemeneti fájlt olvas és kimeneti fájlt ír, nem vár új kéréseket. A local service folyamatban marad, és request–response párokat szolgál ki. A batch CLI sikere a séma, a modellbetöltés és egy batch feldolgozásának helyességét mutatja; az API rendelkezésre állása ezen kívül a szolgáltatásfolyamat elindulásától, a hívási protokolltól és a folyamatos üzemtől függ. Előfordulhat, hogy a CLI fut, de a szerver nem figyel, vagy a request nem éri el a végpontot.

F2. Legyen X,Y∈𝒳NX,Y\in\mathcal X_N és λ∈[0,1]\lambda\in[0,1]. Ekkor (λX+(1−λ)Y)n,0,i,j=λXn,0,i,j+(1−λ)Yn,0,i,j.(\lambda X+(1-\lambda)Y)_{n,0,i,j} =\lambda X_{n,0,i,j}+(1-\lambda)Y_{n,0,i,j}. Mivel Xn,0,i,j∈[0,1]X_{n,0,i,j}\in[0,1], Yn,0,i,j∈[0,1]Y_{n,0,i,j}\in[0,1] és λ,1−λ∈[0,1]\lambda,1-\lambda\in[0,1], mindkét tag nemnegatív, és az összeg felső korlátja λ⋅1+(1−λ)⋅1=1.\lambda\cdot1+(1-\lambda)\cdot1=1. Tehát a konvex kombináció minden eleme [0,1][0,1]-beli, az alak és a dtype változatlan, így a kombináció 𝒳N\mathcal X_N-ben van. A λ=0\lambda=0 eset YY-t, a λ=1\lambda=1 eset XX-et adja vissza, mindkettő a feltétel szerint eleme a halmaznak. A konvexitás a pixelérték-intervallum konvexitásának komponensenkénti öröklődése; az alak- és dtypefeltétel rögzített NN mellett nem változik a kombinációban.

F3. Egy lehetséges megoldás:

import numpy as np

def validate_image(x: np.ndarray) -> None:
    if not isinstance(x, np.ndarray):
        raise TypeError("x nem np.ndarray")
    if x.dtype != np.float32:
        raise TypeError(f"dtype hiba: {x.dtype}")
    if x.shape != (1, 1, 24, 24):
        raise ValueError(f"alak hiba: {x.shape}")
    if not np.all(np.isfinite(x)):
        raise ValueError("nem véges érték")
    if np.any(x < 0.0) or np.any(x > 1.0):
        raise ValueError("érték a [0,1] tartományon kívül")

Tesztek: np.full((1,1,24,24), 0.5, dtype=np.float32) elfogadva; np.full((1,1,24,24), 128, dtype=np.uint8) dtype hibát ad; np.full((1,1,23,24), 0.5, dtype=np.float32) alak hibát ad; egy tömb, amelyben az egyik elem −0,1-0{,}1, értéktartomány-hibát ad. Alakellenőrzés: a (1,1,24,24) pontosan a szerződés, az N=1N=1 batch egyes eleme.

F4. A hiba oka: NaN esetén mind az x < 0.0, mind az x > 1.0 összehasonlítás hamis, ezért a not (hamis or hamis) kifejezés igaz, és a függvény elfogadja a bemenetet. A tartományvizsgálat nem zárja ki a nem véges értékeket. Javítás: először végességi ellenőrzést kell végezni:

def ellenoriz_bemenet(x):
    if not np.all(np.isfinite(x)):
        return "elutasítva"
    if np.any(x < 0.0) or np.any(x > 1.0):
        return "elutasítva"
    return "elfogadva"

Ellenőrző bemenet: x = np.array([[[[np.nan]]]], dtype=np.float32); a hibás változat „elfogadva”, a javított „elutasítva” eredményt ad.

F5. A protokoll: rögzítsük a CPU-t, a kötegméretet N=360N=360, a checkpointot és az inputfájlt. Hajtsunk végre 10 melegítő hívást, amelyek nem szerepelnek a statisztikában, majd 100 mért hívást; mindegyik előtt ne töltsük újra a modellt. Minden hívásnál mérjük TbatchT_{batch}-et time.perf_counter()-rel a validált batch modellhívásának kezdetétől a softmax-kimenet elkészültéig. Számítsuk ki a p50 és p90 empirikus kvantilist. A kiugró értékeket ne töröljük, hanem külön jelentsük. Az eredmény csak erre a gépre, erre a PyTorch/NumPy verzióra és erre a batchméretre érvényes; nem vonható le általános GPU- vagy szerverállítás, és nem bizonyít termelési rendelkezésre állást, mert egyetlen folyamat, nincs párhuzamos terhelés és nincs hálózati réteg a mérésben.

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

A fejezet kiemelt tévedése: egy fejlesztői végpont sikere nem termelési rendelkezésre állási bizonyíték. Ellenpélda: a fejlesztői gépen 20 kérésből 20 sikerül; a termelési környezetben 100 párhuzamos kliensből 5 timeoutot kap. A 20/20-as minta nem cáfolja a 15%-os hibaarányt, és nem tartalmaz párhuzamos terhelést, hálózati késleltetést vagy erőforrás-versenyt. A helyes állítás feltétele: nagyobb minta, párhuzamos kliens, rögzített környezet és hiba szerinti bontás.

Önellenőrzés a labor után: a batch CLI a modellhívás előtt hívta-e a validátort? A betöltés weights_only=True volt-e? A modell eval() állapotban volt-e? A valószínűségsorok összege 1 volt-e a megadott 10−610^{-6} toleranciával? A kimeneti fájl tartalmazta-e a model_id-t? Ha bármelyik válasz nem, a futtatás nem felel meg a szerződésnek.

Visszahívás: 32.2. Scaled dot-product attention alfejezetben a batch dimenzió az attention-mátrixok első indexe volt; itt is a NN dimenzió az első, és a séma a csatorna–térbeli részt rögzíti. 29.2. Denoising és korrupciós modell alfejezetben a zajos–tiszta párok szerződése a [0,1][0,1] intenzitástartományt használta; a mostani validátor ugyanezt a tartományt ellenőrzi a betanított osztályozó bemenetén. 21.2. Learning rate és ütemezés alfejezetben a checkpoint a model, optimizer és scheduler állapotát együtt rögzítette; a batch inference csak a model state_dict-et használja, és semmilyen scheduler-állapotot nem futtat.

33.2.11 Rövid összefoglaló

A batch inference a tanítástól elválasztott kiszolgálási út: a modellcsomagból betölti a state_dict-et, a séma szerint validálja a bemenetet, és predikciót ír fájlba. Az elfogadott GlyphLab tenzor float32, NCHW alakú, N≥1N\ge1, csatorna-térbeli alak 1×24×241\times24\times24, minden pixel [0,1][0,1]-beli és véges. A validátor pontosan ezt a halmazt ellenőrzi; a helyes input a formális szerződésből következik. A model id a csomag verziójához kötött, a local service ehhez rendel request–response szerződést. A késleltetés méréséhez melegítés, ismétlés és környezeti rögzítés kell; fejlesztői siker nem termelési rendelkezésre állási bizonyíték.

A következő 33.3. torch.export és ONNX egység a torch.export és ONNX exportálást tárgyalja: ott azt vizsgáljuk, hogyan alakítható ez a betöltött Python-modell portálható számítási gráffá. Forrás: [D09] PyTorch contributors (2.14). Saving and Loading Models. Hivatalos oktatóoldal olvasva; a state_dict, eval és weights_only szerződésére használtuk. Ebben az egységben export API-t nem tárgyaltunk; a [D20] export-alapú ONNX-szakasz a következő egységhez tartozik, az ottani ellenőrzési igénnyel együtt.