2.3 Izolált környezet, notebook és verzió

2.3.1 Cél, előismeret és indítókérdés

Ebben az egységben nem a modellt, hanem a modellt futtató környezetet vizsgáljuk. A 2.2. Lebegőpont és stabil elemi számítás egységben láttuk, hogy egy lebegőpontos eredmény csak toleranciával értelmezhető; most azt a kérdést tesszük fel, hogy egyáltalán melyik Python hajtotta végre a számítást, milyen csomagverziókkal és milyen kernelben. Induló kérdés: egy notebook cellája 0.91 pontosságot mutat, de a cella tegnap futott. Milyen további adat kell ahhoz, hogy ezt a jelenlegi kód eredményének tekinthessük?

Előismeret: 2.2. Lebegőpont és stabil elemi számítás fogalmai (float32/float64, allclose, tolerancia), valamint alapvető parancssori és fájlkezelési ismeret. Nem feltételezünk csomagkezelői vagy notebook-szerver üzemeltetői gyakorlatot.

2.3.2 Interpreter, kernel és csomagprofil

A három fogalom különböző rétegre vonatkozik, ezért pontosan definiáljuk őket.

Definíció (Python interpreter). A Python interpreter az a folyamat, amely a Python bytecode-ot végrehajtja. Egy futó Python program számára az interpreter elérési útja a sys.executable karakterlánc. Ugyanazon a gépen több interpreter is létezhet, például a rendszer /usr/bin/python3 és egy projekt .venv/bin/python fájlja; a kettő különböző csomagkörnyezetet láthat.

Definíció (Jupyter kernel). A Jupyter kernel egy háttérfolyamat, amely egy adott interpreterrel hajtja végre a notebook celláinak kódját, és a notebook szerverrel kommunikál. A notebook dokumentum (.ipynb) csak cellákat, kimeneteket és metaadatokat tárol; a kernel nem a notebook része.

Definíció (csomagprofil). A csomagprofil a futtatáshoz specifikált csomagok és azok verzióinak zárt halmaza. Egy lock fájl, például uv.lock, a közvetlen függőségek mellett a tranzitív függőségeket is rögzíti, ezért két uv sync futtatás elvileg ugyanazt a profilt állítja elő.

A három réteget a következő példa választja el. Tegyük fel, hogy a notebook kernelt python3 néven indítottuk, de a projekt interpretere .venv/bin/python. A notebook cellája ekkor a kernel interpreterével fut, nem feltétlenül azzal, amelyet a parancssorban uv run python hív. A pontos állapotot a notebookban is a sys.executable mutatja; ha a cella más interpreterrel fut, a numpy verzió is eltérhet.

2.3.3 Reprodukciós feltételek levezetése

A fejezet levezetési kötelezettsége: milyen feltételek mellett várjuk, hogy azonos input azonos ellenőrző számítást adjon? Ehhez formalizáljuk a futás fogalmát.

Legyen PP egy determinisztikus program, azaz olyan program, amelynek minden állapotátmenete csak a megelőző állapottól és a bemenettől függ; nincs benne rejtett véletlen forrás, időbélyeg vagy külső hálózati válasz. Legyen II a bemenet, E=(e,V,B)E=(e, V, B) a környezeti leíró, ahol

Jelölje PE(I)P_E(I) a program kimenetét az EE környezetben. A reprodukálhatóság alapesete a következő.

Lemma (azonos környezet, azonos kimenet). Ha PP determinisztikus, akkor rögzített EE és II mellett PE(I)P_E(I) minden futásban ugyanaz az érték.

Bizonyításvázlat. A futás állapotsorozatát s0,s1,…,sTs_0, s_1, \ldots, s_T jelöli, ahol s0s_0 a kezdőállapot az II bemenettel. A determinisztikusság miatt minden tt lépéshez van olyan FtF_t átmenetfüggvény, hogy st+1=Ft(st)s_{t+1} = F_t(s_t), és FtF_t csak a programkódtól és EE-től függ. Mivel EE és II rögzített, s0s_0 rögzített; teljes indukcióval minden sts_t rögzített, így PE(I)=sTP_E(I)=s_T is. A bizonyítás erőssége, hogy a környezeti leíró minden befolyásoló komponensét rögzítettük; ha bármelyik rejtve változik, a lemma feltétele nem teljesül. ▫\square

A lemma azonban nem ígér bitazonosságot, ha BB változik. Ugyanaz a Python- és könyvtárverzió két különböző CPU-n más lebegőpontos utasítást, más reduction-sorrendet vagy más gyorsított könyvtárat használhat. A [D02] dokumentáció kifejezetten rögzíti, hogy azonos seed sem garantál platformok és verziók közötti bitazonos eredményt. Ezért a gyakorlati reprodukciós szerződés nem pontos egyenlőséget, hanem a 2.2. Lebegőpont és stabil elemi számítás egységből ismert toleranciás egyezést követel meg:

|x̃−x|≤ϵabs+ϵrel|x|, \left|\tilde x - x\right| \le \epsilon_{\mathrm{abs}} + \epsilon_{\mathrm{rel}} \left|x\right|,

ahol xx a referenciaként elfogadott érték, x̃\tilde x az új környezetben kapott érték, ϵabs\epsilon_{\mathrm{abs}} és ϵrel\epsilon_{\mathrm{rel}} pedig a számítás skálájához és dtype-jához indokolt korlát. float64 gradienseket ellenőrző kis konvolúciós példán ϵabs=10−11\epsilon_{\mathrm{abs}}=10^{-11}, ϵrel=10−9\epsilon_{\mathrm{rel}}=10^{-9} észszerű középső differenciás ellenőrzéshez; a pontos értéket a konkrét számítás kondíciója és az alkalmazott differencia lépése határozza meg. Ha a program float32 redukciót tartalmaz, a tolerancia nem húzható be tetszőlegesen.

2.3.4 Izolált környezet: uv és venv

A Pythonhoz több környezetkezelő létezik; a főút a [D01] dokumentáció alapján az uv. A uv egy parancssori eszköz, amely a pyproject.toml és a uv.lock alapján szinkronizálja a projekt csomagprofilját. A labor könyvtára az 1.1. Modell, tanulás és felhasználási szerződés laborcsomagjának (cnn-labor-1.0.0) lab/ könyvtára; ha a uv még nincs telepítve, a [D01] szerinti telepítő vagy a python -m pip install uv parancs adja. A labor könyvtárában a következő parancsok hozzák létre a tanulási profilt:

cd lab
uv venv --python 3.12
uv sync
uv run python --version

A uv sync a lock fájlban rögzített csomagokat telepíti a projekt .venv könyvtárába; önmagában is létrehozza a környezetet, a uv venv lépés csak az interpreter választását teszi explicitté. A --python 3.12 a tanulási interpreter verzióját adja meg; a megengedett tartományt (>=3.12,<3.13) a labor pyproject.toml-ja rögzíti, a csomagverziókat (NumPy 2.3.5, PyTorch 2.14.1 CPU-build, Matplotlib 3.10.8, pandas 2.3.3, SciPy 1.18.1) a uv.lock. A uv run előtaggal futtatott parancsok a projekt környezetét használják anélkül, hogy a felhasználónak kézzel aktiválnia kellene a virtuális környezetet. Ez a megkülönböztetés fontos: a python parancs a shell aktuális környezetét hívja, a uv run python a projekt profilját.

A CPU/GPU build kérdése szintén a csomagprofil része. A PyTorch-nak külön wheel-változatai vannak CPU-s és CUDA-s builddel. A labor főútja kis CPU-s buildet használ. Ha valaki CUDA-s buildet telepít, a torch.cuda.is_available() értéke változhat; a modellkódnak ezt explicit módon kell kezelnie, például device = "cuda" if torch.cuda.is_available() else "cpu" formában. A build azért is része a reprodukciós feltételnek, mert ugyanaz a Python-csomagnév más natív kódot takarhat.

2.3.5 Jupyter notebook és a kernel állapota

A Jupyter notebook egy JSON dokumentum, amely cellákból, azok kimeneteiből és metaadatokból áll. A kernel a cella végrehajtásakor interpretert indít vagy használ; a notebook kimenete a végrehajtás múltbeli rögzítése. A sys.executable a kernelben is elérhető, és a következő cellával ellenőrizhető:

import sys
print(sys.executable)

A kimenet például .../lab/.venv/bin/python lehet, de csak akkor, ha a kernel valóban ezt az interpretert használja. A jupyter kernelspec list parancs a rendelkezésre álló kernelek nevét és indítási útvonalát mutatja; ugyanaz a python3 név több környezetre is mutathat.

A notebook régi kimenete nem bizonyítja a jelenlegi kód futását. Ennek oka, hogy a notebook cellái megosztott kernelállapotot használnak: ha az 1. cellában a = 1 szerepelt, majd a cellát töröltük, a 2. cella print(a) akkor is 1-et írhat ki, ha az aktuális notebookban az 1. cella már nincs jelen. A megbízható ellenőrzés a Restart & Run All, amely újraindítja a kernelt, és minden cellát a dokumentumbeli sorrendben futtat. Ezt követően a mentett notebook kimenete már a jelenlegi kód és a friss kernelállapot együttes eredménye.

2.3.6 Labor: verziójelentés és konvolúciós kontroll új környezetben

A labor két részből áll, és a kiinduló lab könyvtárban végezhető el. Az első rész verziójelentést készít notebookból és parancssorból, a második rész új környezetben reprodukálja a konvolúciós ellenőrző számítást.

Első lépés: verziójelentés parancssorból. Hozzuk létre a lab/version_report.py fájlt:

import json
import platform
import sys

import numpy as np

try:
    import torch
except ImportError:
    torch = None

report = {
    "py_executable": sys.executable,
    "python_version": platform.python_version(),
    "python_implementation": platform.python_implementation(),
    "platform": platform.platform(),
    "numpy_version": np.__version__,
    "torch_version": torch.__version__ if torch is not None else None,
    "torch_cuda_available": torch.cuda.is_available() if torch is not None else False,
    "device_contract": "cpu",
}
print(json.dumps(report, indent=2, sort_keys=True))

Futtatás és elmentés:

cd lab
uv run python version_report.py | tee reports/version_report_cli.json

A reports könyvtárat előzetesen létre kell hozni (mkdir -p reports). A lockolt profilban mért kimenet; a py_executable és a platform mező a saját gép útvonalát és kernelét mutatja, itt rövidítve:

{
  "device_contract": "cpu",
  "numpy_version": "2.3.5",
  "platform": "Linux-…-x86_64-with-glibc2.39",
  "py_executable": "…/cnn-labor-1.0.0/lab/.venv/bin/python",
  "python_implementation": "CPython",
  "python_version": "3.12.3",
  "torch_cuda_available": false,
  "torch_version": "2.14.1+cpu"
}

A NumPy- és PyTorch-verziónak a lock fájllal kell egyeznie; a Python javítóverziója (3.12.x) és a platformsor gépenként eltérhet.

Második lépés: ugyanaz a jelentés notebookból. Indítsuk a Jupyter szervert a projekt környezetéből:

cd lab
uv run jupyter notebook

Egy új notebook első cellájában futtassuk a fenti version_report.py tartalmát, majd válasszuk a Restart & Run All műveletet, és mentsük reports/version_report.ipynb néven. A notebook kernelét úgy válasszuk, hogy a cella sys.executable értéke megegyezzen a CLI-jelentés py_executable mezőjével. Ha a két érték eltér, a notebook nem a projekt interpreterét használja; ilyenkor a kernelkonfigurációt kell javítani, nem a kódot.

Harmadik lépés: konvolúciós kontroll új környezetben. A laborcsomag konvolúciós referenciája, a lab/conv_reference.py, a NumPy-referenciát véges differenciával, --torch kapcsolóval PyTorch autograddal is összeveti (float64, CPU, rögzített bemenetek), és az eredményt JSON-ként a standard kimenetre írja. A csomag lab/conv_check_report.json jelentése ennek a szkriptnek egy tényleges futását rögzíti. Az új környezetben a cél annak ellenőrzése, hogy a számítás a jelentésben rögzített értékekkel toleranciásan egyezik:

cd lab
uv run python conv_reference.py --torch > reports/conv_check_local.json

Az összehasonlítást a version_report.py mintájára egy kis ellenőrző kóddal végezzük: beolvassuk a lab/conv_check_report.json és a friss reports/conv_check_local.json mezőit (a beágyazott szótárakat is), és minden numerikus mezőre np.allclose-t alkalmazunk rtol=1e-9, atol=1e-11 paraméterrel; a numpy_version és torch_version szövegmezőket csak kiírjuk. A lockolt profilban minden numerikus mező a tűrésen belül egyezett. Az átadandó eredmény a három fájl: version_report_cli.json, version_report.ipynb, conv_check_local.json, valamint az összehasonlítás rövid összegzése, amely megnevezi az eltérő mezőket, ha vannak.

A labor célja nem annak bizonyítása, hogy a konvolúció matematikailag helyes; azt a későbbi 15.1.–15.4. egységekben önállóan levezetjük. Itt a környezet reprodukálhatóságát ellenőrizzük: ha a friss környezetben ugyanaz a seed, dtype és device ugyanazt az ellenőrző számítást adja, akkor a csomagprofil és a futási szerződés konzisztens.

2.3.7 Gyakori tévedés és ellenpélda

A leggyakoribb tévedés, hogy a notebook régi kimenetét az aktuális kód futási bizonyítékának tekintjük. Ellenpélda: indítsunk egy kernelt, futtassuk az x = 0.1 + 0.2 cellát, majd töröljük a cellát, és írjunk egy új cellát, amely csak print(x)-et tartalmaz. A notebook kimenete 0.30000000000000004 lesz, pedig a jelenlegi dokumentumban az x definíciója már nem szerepel. A kimenet tehát nem a dokumentumban látható kódból, hanem a kernel rejtett állapotából származik. A Restart & Run All ezt a rejtett állapotot megszünteti; ha ekkor a print(x) cella NameError-t ad, akkor a korábbi kimenet valóban csak régi kernelállapot volt.

2.3.8 Feladatok

F1 (fogalmi, nehézség 1). Magyarázza el, mi a különbség a Python interpreter, a Jupyter kernel és a csomagprofil között. Adjon egy konkrét forgatókönyvet, amelyben ugyanaz a notebook két különböző sys.executable értékkel futhat. Átadandó: három definíció és a forgatókönyv.

F2 (levezetési, nehézség 2). A szövegben kimondott lemma szerint determinisztikus program azonos környezeti leíróval azonos kimenetet ad. Bontsa ki a bizonyítást arra az esetre, amikor a program három lépésből áll: s1=F1(s0)s_1 = F_1(s_0), s2=F2(s1)s_2 = F_2(s_1), s3=F3(s2)s_3 = F_3(s_2). Mutassa meg, hogy ha minden FtF_t determinisztikus, akkor s3s_3 egyértelmű. Ezután indokolja meg, miért nem következik ebből a bitazonosság két különböző CPU-n. Átadandó: a háromlépéses levezetés, a feltételek és egy konkrét ellenpélda a lebegőpontos eltérésre.

F3 (implementációs, nehézség 1). Készítsen environment_check.py CLI-szkriptet, amely kiírja a sys.executable, a Python-verzió, a NumPy verzió, a PyTorch verzió, a CUDA-elérhetőség és a device_contract mezőket JSON formában. A szkript fogadjon egy --device argumentumot, amelynek értéke cpu vagy cuda. Ha cuda kérése esetén a CUDA nem elérhető, a szkript írjon error mezőt, és 1-es kóddal lépjen ki. Átadandó: a teljes kód, egy futtatási parancs, a kimenet és a hibaellenőrzés.

F4 (hibakeresési, nehézség 2). Egy hallgató a notebookban a sys.executable cellát futtatja, és a kimenet /usr/bin/python3. A parancssori uv run python -c 'import sys; print(sys.executable)' viszont .../lab/.venv/bin/python-t ad. A hallgató arra következtet, hogy a uv elromlott. Mi a valódi hiba? Mi a következménye annak, ha a notebookot a rendszerinterpretálóval futtatja? Hogyan ellenőrizhető a javítás? Átadandó: ok, következmény, javítás és a visszaellenőrzés módja.

F5 (kísérlettervezési, nehézség 3). Tervezzen kis kísérletet annak eldöntésére, hogy a konvolúciós ellenőrző számítás két izolált környezetben toleranciásan reprodukálható-e. Az egyik környezet a labor CPU-profilja, a másik egy frissen létrehozott uv környezet. Adja meg a kérdést, a rögzített seedet, dtype-ot, devicet, baseline-t, metrikát, szükséges erőforrást és a következtetési korlátot. Átadandó: a protokoll és annak magyarázata, hogy miért nem állítható ebből általános platformfüggetlen bitazonosság.

2.3.9 Megoldások és indoklás

F1 megoldás. Az interpreter a Python bytecode-ot végrehajtó folyamat; azonosítója a sys.executable útvonal. A Jupyter kernel egy szerverhez kapcsolódó folyamat, amely egy interpreterrel futtatja a notebook celláit; a kernel és a notebook dokumentum külön objektum. A csomagprofil a specifikált függőségek és verziók zárt halmaza, például uv.lock. Forgatókönyv: a notebookot a rendszer python3 kerneljével indítjuk, miközben a projekt .venv-je már létezik. Ekkor a notebook sys.executable értéke /usr/bin/python3, míg ugyanaz a kód uv run python alatt a .venv/bin/python értéket adja; a NumPy verziók is eltérhetnek.

F2 megoldás. A három lépésre: s0s_0 rögzített, mert II és EE adott. Ekkor s1=F1(s0)s_1 = F_1(s_0) egyértelmű, mert F1F_1 függvény. Ha s1s_1 egyértelmű, akkor s2=F2(s1)s_2 = F_2(s_1) is egyértelmű, végül s3=F3(s2)s_3 = F_3(s_2) is. A bizonyítás a függvény fogalmából és a teljes indukció specializálásából áll. A bitazonosság hiánya: két CPU eltérhet az utasításkészletben és a lebegőpontos reduction sorrendjében. Például egy nyolcelemű összeget az egyik gép (a1+a2)+(a3+a4)+⋯(a_1+a_2)+(a_3+a_4)+\cdots módon, a másik páronkénti vagy szekvenciális sorrendben számol; a kerekítési hibák miatt a két eredmény utolsó bitje eltérhet. Ezért az azonos Python- és PyTorch-verzió sem garantál bitazonos kimenetet különböző platformon.

F3 megoldás. A kód:

import argparse
import json
import platform
import sys

import numpy as np

try:
    import torch
except ImportError:
    torch = None

def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--device", choices=["cpu", "cuda"], default="cpu")
    args = parser.parse_args()

    cuda_available = bool(torch is not None and torch.cuda.is_available())
    report = {
        "py_executable": sys.executable,
        "python_version": platform.python_version(),
        "python_implementation": platform.python_implementation(),
        "platform": platform.platform(),
        "numpy_version": np.__version__,
        "torch_version": torch.__version__ if torch is not None else None,
        "cuda_available": cuda_available,
        "device_contract": args.device,
    }
    if args.device == "cuda" and not cuda_available:
        report["error"] = "CUDA requested but not available"
        print(json.dumps(report, indent=2, sort_keys=True))
        return 1

    print(json.dumps(report, indent=2, sort_keys=True))
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

Futtatás: uv run python environment_check.py --device cpu. A CPU-s kimenet cuda_available: false mellett device_contract: "cpu"; a --device cuda változat CPU-s gépen error mezővel és 1-es visszatérési kóddal zárul. Az ellenőrzés a kilépési kódból és a JSON error mezőjének jelenlétéből áll.

F4 megoldás. Az ok nem a uv hibája, hanem az, hogy a notebook kernelét a rendszer-interpreterhez kötötték, nem a projekt környezetéhez. Következmény: a notebook a rendszer csomagprofilját látja, így a verziójelentés nem a labor profilját írja le, és a reprodukálhatósági állítás sérül. A javítás a Jupyter kernelspec átállítása vagy új kernel létrehozása a .venv interpreterével, például uv run python -m ipykernel install --user --name glyphlab formában, majd a notebookban a glyphlab kernel kiválasztása. Visszaellenőrzés: Restart & Run All után a notebook sys.executable értékének meg kell egyeznie a uv run python -c 'import sys; print(sys.executable)' kimenettel.

F5 megoldás. Kérdés: ugyanazt az ellenőrző számítást adja-e a laborprofil és egy frissen létrehozott izolált környezet? Protokoll: a második környezetet uv venv --python 3.12 és uv sync hozza létre ugyanabból a lock fájlból. Mindkét környezetben a conv_reference.py --torch fut (float64, CPU, a szkriptben rögzített bemenetekkel). Baseline: a kezdőlabor lab/conv_check_report.json numerikus értékei. Metrika: minden számszerű mezőre np.allclose(..., rtol=1e-9, atol=1e-11); ezen kívül a jelentés metaadatainak egyezését is ellenőrizzük. Erőforrás: kis CPU-s futás, néhány másodperc. Következtetési korlát: a toleranciás egyezés a két adott környezetre vonatkozik; nem bizonyít platformfüggetlen bitazonosságot, és ha a másik gép buildje eltér, az egyezés meghiúsulhat anélkül, hogy a kód hibás lenne. A [D02] dokumentáció szerint a seed szükséges, de nem elégséges feltétel a bitazonos reprodukcióhoz.

2.3.10 Önellenőrzés és visszahívás

Önellenőrző kérdések: meg tudom-e nevezni a sys.executable értéket abban a környezetben, ahol a modellemet futtattam? Rögzítettem-e a csomagprofilt lock fájlban? A notebook kimenetét Restart & Run All után mentettem-e? Ha bármelyik válasz nem, a kísérleti naplóból hiányzik a reprodukciós feltétel.

Visszahívás az 1.3. Adatfelosztás és a megfigyelési egység egységből: ahogyan a split érvényessége attól függ, hogy a megfigyelési egység szerint csoportosítottunk-e, úgy a futási állítás érvényessége attól függ, hogy a környezeti leíró minden befolyásoló komponensét rögzítettük-e. Visszahívás a 2.2. Lebegőpont és stabil elemi számítás egységből: a numerikus egyenlőséget allclose-zal és indokolt toleranciával ellenőrizzük, nem == művelettel.

2.3.11 Rövid összefoglaló

A Python interpreter, a Jupyter kernel és a csomagprofil három különböző réteg. A reprodukálhatóság akkor értelmes, ha a determinisztikus program bemenete mellett az interpretert, a lockolt függőségeket és a buildkörnyezetet is rögzítjük; ebből azonos környezetben azonos kimenet következik. Platformok között bitazonosság nem ígérhető, ezért a toleranciás allclose a helyes ellenőrzés. A notebook csak Restart & Run All után tekinthető az aktuális kód futási bizonyítékának. A labor verziójelentést készít CLI-ből és notebookból, majd új környezetben reprodukálja a konvolúciós kontrollt a kezdőlabor jelentésével összehasonlítva.

2.3.12 Következő alkalmazás

A 2.4. Kis tudományos szoftverprojekt egységben kis tudományos szoftverprojektet szervezünk: a most rögzített környezeti jelentés lesz a projekt README-jének és futtatási szerződésének első szakasza. A verziójelentés adatai akkor válnak hasznossá, amikor egy hiba vagy eltérő eredmény okát kell lokalizálni.

2.3.13 Források