Download code/quantize_eval.py from sraivante/TinyFaceNet-1M: direct link, hf CLI and curl.
- Browser
- Download file 6.01 kB
-
https://huggingface.co/sraivante/TinyFaceNet-1M/resolve/main/code/quantize_eval.py
- Command line
-
hf download hf://sraivante/TinyFaceNet-1M/code/quantize_eval.py
-
curl -L -o quantize_eval.py https://huggingface.co/sraivante/TinyFaceNet-1M/resolve/main/code/quantize_eval.py
6.01 kB
| """Quantize a TinyFaceNet checkpoint and measure accuracy drop. | |
| Variants: fp32 | int8_ort (ONNX Runtime static, QOperator, per-channel -- the real deployable path) | |
| | int8_w (weight-only fake-quant, per-channel symmetric) | int4_w (weight-only fake-quant, per-channel) | int4_g32 (per-group of 32) | |
| Metrics (date-held-out test set): family acc (centroid argmax), open-set acc with THR, mean cosine agreement with fp32 embeddings over the FULL dataset, | |
| and argmax agreement with fp32 over the full dataset (statistically more stable than the 59-face test set). | |
| usage: python quantize_eval.py <ckpt> <tag> -> analysis/quant_<tag>.json, dataset/<tag>_fp32.onnx, dataset/<tag>_int8.onnx""" | |
| import sys, os, json, time, copy, numpy as np, torch, torch.nn.functional as F, onnx, onnxruntime as ort | |
| from onnxruntime.quantization import quantize_static, CalibrationDataReader, QuantType, QuantFormat | |
| from model import TinyFaceNet | |
| torch.set_num_threads(4); CK,TAG=sys.argv[1],sys.argv[2]; THR=0.55 | |
| ck=torch.load(CK,weights_only=False); names=list(map(str,ck["names"])); C=ck["centroids"].numpy(); oi=names.index("other") | |
| d=np.load("dataset/faces_v2.npz"); X=d["imgs"]; L=np.array(list(map(str,d["labels"]))); DT=d["date"]; S=d["src"] | |
| Y=np.array([names.index(l) for l in L]); test=np.array([(x[:6] in ("202511","202605","202606")) for x in DT]) | |
| full=np.linspace(0,len(X)-1,1500).astype(int) # agreement subset (1500 faces, spread over sources) | |
| def prep(x): return ((x.transpose(0,3,1,2).astype(np.float32)/255.)-0.5)/0.5 | |
| def base(): m=TinyFaceNet(n_cls=len(names)); m.load_state_dict(ck["model"]); m.eval(); return m | |
| def run_torch(m,ix): | |
| out=[] | |
| with torch.no_grad(): | |
| for i in range(0,len(ix),256): out.append(F.normalize(m(torch.from_numpy(prep(X[ix[i:i+256]]))),dim=1).numpy()) | |
| return np.concatenate(out) | |
| def run_ort(path,ix): | |
| s=ort.InferenceSession(path,providers=["CPUExecutionProvider"]); n=s.get_inputs()[0].name; out=[] | |
| for i in range(0,len(ix),64): e=s.run(None,{n:prep(X[ix[i:i+64]])})[0]; out.append(e/np.linalg.norm(e,axis=1,keepdims=True)) | |
| return np.concatenate(out) | |
| def metrics(E,Eref,ix): | |
| sim=E[np.isin(ix,np.nonzero(test)[0])]@C.T if False else None | |
| te=np.nonzero(test)[0]; Et=E[:len(te)]; yt=Y[te]; sim=Et@C.T; pred=sim.argmax(1); best=sim.max(1) | |
| fam=yt!=oi; acc=float((pred[fam]==yt[fam]).mean()) | |
| pred_os=np.where(best>=THR,pred,oi); acc_os=float((pred_os==yt).mean()) # open-set: unknown->other | |
| per={names[c]:round(float((pred[yt==c]==c).mean()),3) for c in range(len(names)) if (yt==c).any()} | |
| Ef=E[len(te):]; Rf=Eref[len(te):]; cos=float((Ef*Rf).sum(1).mean()); agree=float(((Ef@C.T).argmax(1)==(Rf@C.T).argmax(1)).mean()) | |
| return dict(fam_acc=acc,openset_acc=acc_os,per_class=per,cos_vs_fp32=cos,argmax_agree_vs_fp32=agree) | |
| def fake_quant(m,bits,group=None): | |
| m=copy.deepcopy(m); qmax=2**(bits-1)-1 | |
| for name,p in m.named_parameters(): | |
| if p.dim()<2 or name.startswith(("distill","cls")): continue | |
| w=p.data; o=w.shape[0]; wf=w.reshape(o,-1) | |
| if group and wf.shape[1]%group==0: wg=wf.reshape(o,-1,group); sc=wg.abs().amax(2,keepdim=True).clamp_min(1e-8)/qmax; wf=((wg/sc).round().clamp(-qmax,qmax)*sc).reshape(o,-1) | |
| else: sc=wf.abs().amax(1,keepdim=True).clamp_min(1e-8)/qmax; wf=(wf/sc).round().clamp(-qmax,qmax)*sc | |
| p.data=wf.reshape(w.shape) | |
| return m | |
| def lat(fn,n=50): | |
| x=np.zeros((1,112,112,3),np.uint8); fn(np.array([0])); t=time.perf_counter() | |
| for _ in range(n): fn(np.array([0])) | |
| return (time.perf_counter()-t)/n*1000 | |
| ix=np.concatenate([np.nonzero(test)[0],full]); res={}; m=base() | |
| # ---- fp32 torch ---- | |
| E0=run_torch(m,ix); res["fp32"]=dict(**metrics(E0,E0,ix),size_kb=round(sum(p.numel() for n,p in m.named_parameters() if not n.startswith(("distill","cls")))*4/1024),lat_ms=round(lat(lambda i:run_torch(m,i)),2),note="PyTorch fp32 (deploy params only)") | |
| # ---- ONNX export + ORT fp32 ---- | |
| fp=f"dataset/{TAG}_fp32.onnx"; q8=f"dataset/{TAG}_int8.onnx" | |
| torch.onnx.export(m,torch.zeros(1,3,112,112),fp,input_names=["x"],output_names=["emb"],dynamic_axes={"x":{0:"b"},"emb":{0:"b"}},opset_version=17,dynamo=False) | |
| E=run_ort(fp,ix); res["fp32_onnx"]=dict(**metrics(E,E0,ix),size_kb=round(os.path.getsize(fp)/1024),lat_ms=round(lat(lambda i:run_ort(fp,i)),2),note="ONNX Runtime fp32") | |
| # ---- ORT static int8 ---- | |
| class CR(CalibrationDataReader): | |
| def __init__(s): tr=np.nonzero(~test)[0]; s.ix=np.linspace(0,len(tr)-1,200).astype(int); s.tr=tr; s.i=0 | |
| def get_next(s): | |
| if s.i>=len(s.ix): return None | |
| b=s.tr[s.ix[s.i:s.i+8]]; s.i+=8; return {"x":prep(X[b])} | |
| quantize_static(fp,q8,CR(),quant_format=QuantFormat.QOperator,per_channel=True,activation_type=QuantType.QUInt8,weight_type=QuantType.QInt8) | |
| E=run_ort(q8,ix); res["int8_ort"]=dict(**metrics(E,E0,ix),size_kb=round(os.path.getsize(q8)/1024),lat_ms=round(lat(lambda i:run_ort(q8,i)),2),note="ONNX Runtime static int8, per-channel, 200 calib imgs (deployable)") | |
| # ---- weight-only fake quant ---- | |
| for tag,bits,g,note in [("int8_w",8,None,"simulated: int8 weights per-channel, fp32 activations"), | |
| ("int4_w",4,None,"simulated: int4 weights per-channel (coarse)"), | |
| ("int4_g32",4,32,"simulated: int4 weights, group=32 scales (llama.cpp-style)")]: | |
| mq=fake_quant(m,bits,g); E=run_torch(mq,ix); core=sum(p.numel() for n,p in m.named_parameters() if not n.startswith(("distill","cls"))) | |
| kb=core*bits/8/1024*(1+ (2/g if g else 0.002)) | |
| res[tag]=dict(**metrics(E,E0,ix),size_kb=round(kb),lat_ms=None,note=note) | |
| json.dump(dict(ckpt=CK,tag=TAG,names=names,n_test=int(test.sum()),n_test_family=int((test&(Y!=oi)).sum()),n_agree=len(full),thr=THR,results=res),open(f"analysis/quant_{TAG}.json","w"),indent=1) | |
| for k,v in res.items(): print(f"{k:10s} fam_acc={v['fam_acc']:.3f} openset={v['openset_acc']:.3f} cos={v['cos_vs_fp32']:.4f} agree={v['argmax_agree_vs_fp32']:.3f} size={v['size_kb']}KB lat={v['lat_ms']}ms") | |