A 34.1. Mért késleltetés, throughput és
memória egységben a késleltetést, az átbocsátóképességet és a
memóriahasználatot mértük. Ott a tárolási méret csak az egyik erőforrás
volt. Most a kvantálást vizsgáljuk: egy már betanított,
float32 paraméterekkel és aktivációkkal számoló modellt
kisebb bitszélességű, jellemzően int8 számábrázolásra
képezünk le. A közvetlen motiváció a memória- és sávszélesség-igény
csökkentése, de a leképzés numerikus hibát visz be. A kérdés az, hogy
ezt a hibát hogyan tudjuk a skála és a kerekítés szintjén pontosan
korlátozni, és hol szűnik meg a korlát.
Előismeret. A 33.2. Batch és kis
szolgáltatás inference-szerződése rögzíti, hogy a bemeneti kép
float32,
intenzitású és NCHW alakú. A kvantálás ezen a bejárati szerződésen nem
változtat: a modell továbbra is float32 képet fogad, a
kisebb bitszélesség a belső reprezentációban jelenik meg. A 30.2. Kontrasztív cél és temperature
softmax-hőmérséklete azt mutatta, hogy ugyanakkora logitváltozás más
predikciós érzékenységet okozhat; a 22.2.
Címkemegőrző transzformációk figyelmeztetett, hogy a tanító- és a
kiszolgálási eloszlás eltérése önmagában is hibát termel. A kvantálás
kalibrációja ugyanezt a domaineltérést tesszi fel nagyító alá.
Induló kérdés. Ha egy float32 értéket
egy közös
skálával int8 egészre kerekítünk, majd visszaállítunk,
legfeljebb mekkora lehet a visszaállítás abszolút hibája, amíg nem érjük
el a int8 tartomány szélét?
Legyen az eredeti valós érték . Egy affin kvantáló egy pár, ahol
A kódolt egész szám
a visszaállított, dekvantált érték pedig
Itt
egészek, int8 esetén például
,
.
A
a legközelebbi egészre kerekítés, az
a vágás (clipping). Ha
nem teljesül, az érték telítődik: a kódolt érték a
tartomány szélére esik, és a dekvantált érték nem követi tovább
-et.
A definícióból közvetlenül látszik a két szabadsági fok szerepe. A skála határozza meg, hogy egy egész lépés mekkora valós lépésnek felel meg. A zero-point azt mondja meg, melyik egész kódolja a -t: ha , akkor a kerekítés előtt áll, tehát -hoz általában tartozik. Aszimmetrikus tartomány, például , esetén szükség van nemnulla zero-pointra.
Két elterjedt granularitás van. Per-tensor kvantálásnál egy teljes tenzor minden eleme ugyanazt az párt használja. Per-channel kvantálásnál a csatorna mentén skálavektort és zero-point vektort használunk; például egy aktivációban az elemhez az tartozik. E két fogalmat nem szabad felcserélni, mert a per-channel finomabb felbontás kisebb kerekítési hibát adhat, de több kalibrációs adatot és más kernel-támogatást igényelhet.
A kalibráció (calibration) a skála és zero-point megválasztása reprezentatív adaton. Egy egyszerű, gyakori szabály a min-max kalibráció:
ahol a kalibrációs tenzor megfigyelt minimuma és maximuma. A kvantálásnál a reprezentálható valós tartomány tartalmazza a nullát is: a kalibrált szélsőértékeket ezért és szerint bővítjük; ezután a tartományra vágható. Ha a kibővített , a konvenció használható. Egy nemnulla konstans tenzor esetén viszont a általában nem biztosítja az érték torzításmentes reprezentálását. A kalibráció a tanítástól függetlenül, a legjobb modell kiválasztása után történik, és nem használhatja a tesztet; a teszt a végső, már kvantált modell független értékelésére szolgál.
A tanulási célhoz tartozó kötelező állítás:
Állítás (abszolút kerekítési hibakorlát). Legyen valós, , egészek. Legyen , a vágás előtti kerekített egész, és
Ha a telítetlenségi feltétel
teljesül, akkor
Bizonyítás. Vezessük be a kerekítés előtti értéket: . A legközelebbi egészre kerekítés definíciójából minden esetén
A telítetlenségi feltétel miatt a vágás hatástalan, tehát . Jelölje ; ekkor . A dekvantált érték:
Így
Ezzel az állítást bebizonyítottuk. A bizonyítás semmilyen eloszlási feltevést nem használ; a korlát tetszőleges telítetlen -re érvényes, tetszőleges -vel. Ha a kerekítés döntetlennél nem a páros egész felé kerekít, hanem mindig felfelé, a tulajdonság akkor is fennáll, tehát a korlát érvényes marad; a döntetlen-szabály csak az konkrét értékét befolyásolja.
Miért nem érvényes a korlát telítéskor? Ha annyira nagy, hogy , akkor a vágás -ra cseréli a kerekített értéket, és a konstanssá válik. Ekkor további növelésével nő, viszont már nem, így tetszőlegesen nagy lehet. Ugyanez igaz az alsó telítésre. A telítési hiba tehát nem a kerekítés, hanem a reprezentálható tartomány korlátosságának következménye.
Legyen , , , . Négy értékre:
| telített? | |||
|---|---|---|---|
| nem | |||
| nem | |||
| nem | |||
| igen |
A harmadik sor azt mutatja, hogy a zero-point itt éppen -ra kódolja a valós nullát. A negyedik sorban , a vágás előtti kerekítés , ami meghaladja a felső határt; a vágás után . A hiba , mert a telítetlenségi feltétel sérült.
A labor célja egy önálló, Pythonban ellenőrizhető kvantáló és
kalibráló függvény, amely nem backend-gyorsítást ígér, hanem a numerikus
hibát méri. A kód tiszta NumPy, CPU, float32
bemenettel.
Munkahely: lab/q134_quant/.
# quant_example.py
import numpy as np
def affine_quantize(x, scale, zero_point, qmin=-128, qmax=127):
# x: float32 tömb; scale: pozitív float32; zero_point: int
if not float(scale) > 0.0:
raise ValueError("scale > 0 szükséges")
y = x / np.float64(scale) + float(zero_point)
q_unclamped = np.rint(y) # legközelebbi egész, feles döntetlennél páros
saturated = (q_unclamped < qmin) | (q_unclamped > qmax)
q = np.clip(q_unclamped, qmin, qmax).astype(np.int64)
x_hat = (np.float32(scale) * (q - zero_point)).astype(np.float32)
return q, x_hat, saturated
def per_tensor_minmax(x, qmin=-128, qmax=127):
# A nulla is legyen reprezentálható; ez a konstans, nemnulla inputot is kezeli.
rmin = min(0.0, float(np.min(x)))
rmax = max(0.0, float(np.max(x)))
if rmax == rmin: # pontosan minden érték nulla
return np.float32(1.0), 0
scale = np.float32((rmax - rmin) / (qmax - qmin))
zero_point = qmin - int(np.rint(rmin / np.float64(scale)))
zero_point = int(np.clip(zero_point, qmin, qmax))
return scale, zero_point
if __name__ == "__main__":
rng = np.random.default_rng(20261020)
x = rng.normal(0.2, 0.5, size=(4, 8)).astype(np.float32)
scale, z = per_tensor_minmax(x)
q, x_hat, sat = affine_quantize(x, scale, z)
err = np.max(np.abs(x_hat - x))
print("scale=", scale)
print("zero_point=", z)
print("max_abs_err=", err)
print("saturated_cells=", int(sat.sum()))
# Ellenőrzés: telítetlen esetben a levezetett korlátnak teljesülnie kell.
if int(sat.sum()) == 0:
assert err <= np.float64(scale) / 2.0 + 1e-6, "hibás kerekítési korlát"
# Determinisztikus kézi kontroll a 4. szakasz táblázatára.
x_ref = np.array([1.37, -0.12, 0.0, 32.0], dtype=np.float32)
q_ref, x_hat_ref, sat_ref = affine_quantize(
x_ref, np.float32(0.25), 0, qmin=-128, qmax=127
)
print("control_q=", q_ref)
print("control_x_hat=", x_hat_ref)
print("control_saturated=", sat_ref)
assert np.allclose(x_hat_ref[:3], np.array([1.25, 0.0, 0.0], dtype=np.float32),
atol=1e-6)
assert q_ref[3] == 127 and sat_ref[3]
print("control_ok")Futtatás:
cd lab/q134_quant
python quant_example.pyA determinisztikus kontroll várható kimenete
control_q=[5 0 0 127],
control_x_hat=[1.25 0. 0. 31.75],
control_saturated=[False False False True]. A véletlen
tenzorra a script kiírja a skálát, zero-pointot, maximális abszolút
hibát és a telített elemek számát; ezt a hallgató a laborban futtatja.
Az itteni szöveg nem állítja, hogy ezt a véletlen futtatást most
végrehajtottuk; a kézi kontroll értékei a fenti képletekből
közvetlenül számíthatók. Valódi backend-gyorsulást kizárólag tényleges
mérés alapján szabad közölni, a 34.1. Mért
késleltetés, throughput és memória mérési protokolljával.
Input: két pár: és ; egy rögzített egész.
Előfeltétel: ismeri az affin kvantálás definícióját.
Átadandó eredmény: magyarázza el a skála és a zero-point különbségét, és adja meg mindkét párhoz a -hoz tartozó dekvantált értéket. Indokolja, miért nem elegendő a egész bitminta önmagában az eredeti valós érték rekonstruálásához.
Input: , , , , .
Előfeltétel: ismeri a 3. szakasz állítását és bizonyítását.
Átadandó eredmény: lépésenként számolja ki értékét, a kerekített -t, a vágott -t és a dekvantált -et. Mutassa meg, hogy a hiba , ami nagyobb -nél, és nevezze meg pontosan, hogy a tétel mely feltétele nem teljesül.
Input:
,
per-channel skálák
,
zero-pointok
;
a pontos numerikus értékeket a hallgató választja, de rögzítse seeddel:
np.random.default_rng(20261020).
Előfeltétel: a labor kódjának működése;
float32 dtype.
Átadandó eredmény: futtatható
quantize_per_channel(x, scales, zero_points, qmin, qmax)
függvény, amely minden
oszlophoz külön
párt használ. Ellenőrizze, hogy minden olyan oszlopra, ahol nincs
telített elem, a maximális abszolút hiba legfeljebb
legyen. A megoldásnak tartalmaznia kell a teljes kódot és a kontroll
kimenetét.
Input: az alábbi kalibrációs kódrészlet:
scale = (rmax - rmin) / 255.0
z = qmin - int(rmin / scale) # hibaforrásElőfeltétel: ismeri a min-max kalibráció képletét és
a Python int szemantikáját.
Átadandó eredmény: mutasson egy konkrét
rmin értéket, ahol a int(rmin / scale) nem a
kívánt legközelebbi egész kerekítést adja. Magyarázza el a hiba okát,
következményét a reprezentálható tartományra, adja meg a javítást, és
írja le azt a numerikus ellenőrzést, amellyel a javítás helyességét
visszaigazolja.
Input: egy GlyphLabon tanított kis CNN
float32 súllyal; a metadata.csv scene-szintű
splitje; korlátozott CPU-erőforrás.
Előfeltétel: a 33.2. Batch és kis szolgáltatás inference-szerződése, a 34.1. Mért késleltetés, throughput és memória mérési módszertana és a 34.2. Kvantálás és hibatovábbterjedés kvantálási hibamértéke.
Átadandó eredmény: egy írásbeli kísérleti terv
post-training quantization (PTQ) és float32 baseline
összehasonlítására. Azonosítsa a kalibrációs halmazt, a rögzített
seedet, a mért numerikus hibát, a validációs metrikát és a kísérlet
érvényességi korlátját. Tüntesse fel, mely állításokat nem szabad
levonni valódi backend-futás nélkül.
A dekvantálási képlet .
Ugyanaz a bitminta tehát az egyik kvantálóban -t, a másikban -t jelent. A skála adja egy egész lépés valós méretét, a zero-point pedig a nulla valós érték kódját; mindkettő ismerete szükséges, mert a tárolt önmagában csak egy egész, nem hordozza a mértékegységet és az origóeltolást.
A 3. szakasz állítása felteszi, hogy . Itt , tehát a feltétel nem teljesül; a kapott hiba ezért nem mond ellent a tételnek. Alakellenőrzés: , és a hiba is dimenziótlan valós; egész.
# per_channel_quant.py
import numpy as np
def quantize_per_channel(x, scales, zero_points, qmin=-128, qmax=127):
x = np.asarray(x, dtype=np.float32)
scales = np.asarray(scales, dtype=np.float32)
zero_points = np.asarray(zero_points, dtype=np.int64)
y = x / scales.astype(np.float64) + zero_points.astype(np.float64)
q_unclamped = np.rint(y)
saturated = (q_unclamped < qmin) | (q_unclamped > qmax)
q = np.clip(q_unclamped, qmin, qmax).astype(np.int64)
x_hat = (scales * (q - zero_points)).astype(np.float32)
return q, x_hat, saturated
if __name__ == "__main__":
rng = np.random.default_rng(20261020)
x = rng.normal(0.0, 1.0, size=(4, 6)).astype(np.float32)
scales = rng.uniform(0.05, 0.15, size=(6,)).astype(np.float32)
zero_points = rng.integers(-20, 21, size=(6,))
q, x_hat, sat = quantize_per_channel(x, scales, zero_points)
for c in range(6):
col_sat = sat[:, c]
err_c = np.max(np.abs(x_hat[:, c] - x[:, c]))
if not np.any(col_sat):
assert err_c <= np.float64(scales[c]) / 2.0 + 1e-6, (c, err_c)
print(f"channel={c} scale={scales[c]:.6f} max_abs_err={err_c:.6f} "
f"saturated={int(np.sum(col_sat))}")
print("per_channel_control_ok")A vektorizáció a scales és zero_points
broadcastingjára épül: a
alakú x oszlopaihoz a
hosszú vektorok rendelődnek. A tolerancia 1e-6, mert a
kerekítési korlát felső határa analitikus, a lebegőpontos műveletek
viszont
-nál
kisebb numerikus eltérést adhatnak float32-ben. Szélsőeset:
ha egy oszlop telített elemet tartalmaz, a korlátot nem követeljük meg,
a konzol kimutatja a telített cellák számát.
A Python int(x) függvény nulla felé csonkít, nem a
legközelebbi egészre kerekít. Példa:
,
.
Ekkor
.
A int(-13.7) értéke
,
a legközelebbi egész viszont
.
A rossz zero-point ezért eggyel nagyobb lesz a helyesnél, és a
reprezentálható legkisebb érték nem éri el a tényleges minimumot; a
legkisebb bemenetek telítődnek, szisztematikus pozitív torzítást
okozva.
Javítás:
z = qmin - int(np.rint(rmin / scale))Ellenőrzés: a javított -vel számoljuk ki a kalibrációs minimum kvantálását. Azt várjuk, hogy esetén (vagy a kerekítés miatt legfeljebb egy egész eltéréssel), és a hibatűrésen belül közelébe esik. Konkrétan , mellett a helyes ; ezzel , kerekítve , nem -t csonkító érték. A fenti F3/F4 mintához hasonló, kis unit tesztben összehasonlítható a két változat maximális hibája telítetlen tartományon.
Egy lehetséges terv:
float32 baseline-hoz képest, és mekkora a maximális
logiteltérés?float32
súllyal; a val_loss alapján már kiválasztott, a tesztet a protokoll
végéig nem használjuk.affine_quantize függvényével; aktivációkra nem
állítunk backend-int8 futtatást.int8 feltételezéssel.Gyakori tévedés. „Az int8 reprezentáció
automatikusan kisebb és gyorsabb modellt ad.” Az int8 tárolás
valóban csökkentheti a paraméterek bájtjait, de a futás csak
akkor gyorsul, ha az adott háttérben (CPU-utasításkészlet, GPU-kernel,
runtime) az összes érintett operátornak létezik támogatott int8
implementációja. Ellenpélda: ha egy háló egyik operátorához nincs int8
kernel, a futtatórendszer dekvantálja a bemenetet,
float32-ben számol, majd újra kvantál. Ekkor a memória
csökkenhet, de a számítási idő akár nőhet is a többlet
dequantize/quantize lépések miatt; a fenti labor tiszta NumPy
szimulációja ugyancsak nem gyors kernel. Az int8 tehát egy
reprezentáció, nem egy sebességígéretekre jogosító
operátorszerződés.
Önellenőrzés. (1) Ha egy telítetlen tartományban a skálát megduplázzuk, mi történik az abszolút hibakorláttal? (2) Miért nem lehet a zero-point tetszőleges valós szám? (3) A kalibráció használhatja-e a validációt?
Válaszok: (1) A alakból a korlát -ről -re nő; a nagyobb lépésköz nagyobb maximális hibát enged. (2) Mert és különbsége lép be a képletbe; ha nem egész, a kerekített egész -val az értékhez való közelség nem fordítható le a kívánt -hez való közelséggé, és a kódolt értékek rácsát eltoló, nem egész origó nem őrizné meg az egész kódolás szerkezetét. A definíció emiatt -t köt ki. (3) Nem; a kalibráció a modell belső paramétereinek választása, ezért ugyanaz a logika tiltja, mint a hiperparaméter-választást a validáción: a kiértékelő halmazt érintetlenül kell hagyni.
Korábbi fogalom visszahívása. A 33.2. Batch és kis szolgáltatás szerint az
inference bemenet float32 és
-beli;
a kvantálás ezt a bemeneti szerződést nem írja felül, hanem a modell
belsejében történik. A 30.2. Kontrasztív cél és
temperature arról tanított, hogy a softmax hőmérséklete
felnagyíthatja a logitok közötti kis különbségeket: egy
logithiba predikciós hatása ezért nem pusztán a
-tól,
hanem az érvényes hőmérséklettől és a szomszédos osztálypontszámoktól is
függ. A 22.2. Címkemegőrző transzformációk
címkemegőrző transzformációs érvelése analóg módon figyelmeztet: a
kalibrációs eloszlásnak a kiszolgálási eloszlást kell reprezentálnia,
különben a kvantálási hiba egy megjósolható domaineltérésre rakódik.
Az affin kvantálás egy valós értéket a egészre képez, a dekvantálás pedig . A skála az egész lépés valós hossza, a zero-point a nulla kódja. Telítetlen tartományban a kerekítésből következik, mert a legközelebbi egész hibája legfeljebb , és a skálával szorzás ezt -re skálázza. Telítéskor a vágás miatt a hiba tetszőlegesen nagy lehet. A kalibráció reprezentatív adaton választja meg az párt, de a tesztet nem érintheti. Az int8 tárolás önmagában nem bizonyít backend-támogatást vagy gyorsulást.
A 34.3. Pruning és struktúra egység a pruning és a strukturált ritkítás kérdését vizsgálja: ott azt elemezzük, hogyan távolíthatók el paraméterek vagy szerkezeti egységek a hibakorlát és a mért hatékonyság megőrzése mellett.
A hibakorlát levezetése és a kód önálló példa. A kapcsolódó mérési módszereket a 34.1. Mért késleltetés, throughput és memória, a modell exportját a 33.3. torch.export és ONNX, a desztillációt a 34.4. Teacher–student desztilláció tárgyalja.