Plaiglab / scripts /export_desklib_onnx.py
SanidhyaDhangar's picture
PlaigLab — Hugging Face Space (Docker) clean deploy
ebebfe8
Raw
History Blame Contribute Delete
5.42 kB
"""Export the desklib DeBERTa-v3 AI-detector to a small int8 ONNX so it runs on
onnxruntime (no torch at inference) and frees ~1.3GB of disk.
Disk-safe ordering on a near-full disk: copy the tokenizer out, load the torch
model into RAM, DELETE the 1.7GB HF cache (model is already in RAM), THEN write
the ONNX — so the disk never holds the torch cache and the fp32 ONNX at once.
Finally verify the int8 ONNX matches torch on sample texts before we rely on it.
Output: models/desklib_onnx/{model.onnx, tokenizer files, config.json, meta.json}
Run: python scripts/export_desklib_onnx.py
"""
import glob, json, os, shutil, sys
import numpy as np
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DST = os.path.join(ROOT, "models", "desklib_onnx")
NAME = "desklib/ai-text-detector-v1.01"
TOK_FILES = ["spm.model", "tokenizer.json", "tokenizer_config.json",
"special_tokens_map.json", "added_tokens.json", "config.json"]
def main():
import torch, torch.nn as nn
from transformers import AutoConfig, AutoModel, AutoTokenizer, PreTrainedModel
os.makedirs(DST, exist_ok=True)
class Desklib(PreTrainedModel):
config_class = AutoConfig
def __init__(self, config):
super().__init__(config)
self.model = AutoModel.from_config(config)
self.classifier = nn.Linear(config.hidden_size, 1)
self.init_weights()
def forward(self, input_ids, attention_mask=None, **kw):
out = self.model(input_ids, attention_mask=attention_mask)[0]
mask = attention_mask.unsqueeze(-1).expand(out.size()).float()
pooled = (out * mask).sum(1) / mask.sum(1).clamp(min=1e-9)
return self.classifier(pooled)
tok = AutoTokenizer.from_pretrained(NAME)
model = Desklib.from_pretrained(NAME).eval()
print("torch model loaded into RAM")
# copy tokenizer/config out of the (now-populated) cache before we delete it
snap = glob.glob(os.path.join(
os.path.expanduser("~"), ".cache", "huggingface", "hub",
"models--desklib--ai-text-detector-v1.01", "snapshots", "*"))
if snap:
for f in TOK_FILES:
src = os.path.join(snap[0], f)
if os.path.exists(src):
shutil.copy(src, os.path.join(DST, f))
print("copied tokenizer/config to", DST)
# sample texts for verification (computed BEFORE we delete anything)
samples = ["The proposed framework leverages a comprehensive and multifaceted "
"approach to optimize performance across diverse benchmarks, underscoring "
"its pivotal role in advancing the field.",
"we ran the test three times and honestly the numbers were all over the "
"place, not sure why, maybe the sensor was loose or we messed up the wiring"]
@torch.no_grad()
def torch_p(t):
e = tok(t, truncation=True, max_length=512, return_tensors="pt")
return float(torch.sigmoid(model(**{k: e[k] for k in
("input_ids", "attention_mask")})[0]))
torch_ref = [torch_p(t) for t in samples]
# free the 1.7GB disk cache now that the model is in RAM
cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "huggingface",
"hub", "models--desklib--ai-text-detector-v1.01")
shutil.rmtree(cache_dir, ignore_errors=True)
print("freed HF cache (model stays in RAM)")
# export fp32 ONNX
fp32 = os.path.join(DST, "model_fp32.onnx")
dummy = tok(samples[0], return_tensors="pt", truncation=True, max_length=64)
torch.onnx.export(
model, (dummy["input_ids"], dummy["attention_mask"]), fp32,
input_names=["input_ids", "attention_mask"], output_names=["logits"],
dynamic_axes={"input_ids": {0: "b", 1: "s"},
"attention_mask": {0: "b", 1: "s"}, "logits": {0: "b"}},
opset_version=14, do_constant_folding=True, dynamo=False)
print(f"exported fp32 ONNX ({os.path.getsize(fp32)//1024//1024} MB)")
# dynamic int8 quantization
from onnxruntime.quantization import quantize_dynamic, QuantType
out = os.path.join(DST, "model.onnx")
quantize_dynamic(fp32, out, weight_type=QuantType.QInt8)
os.remove(fp32)
print(f"quantized int8 ONNX ({os.path.getsize(out)//1024//1024} MB); removed fp32")
# verify ONNX matches torch
import onnxruntime as ort
sess = ort.InferenceSession(out, providers=["CPUExecutionProvider"])
inn = [i.name for i in sess.get_inputs()]
def onnx_p(t):
e = tok(t, truncation=True, max_length=512)
feed = {"input_ids": np.array([e["input_ids"]], np.int64),
"attention_mask": np.array([e["attention_mask"]], np.int64)}
logit = sess.run(None, {k: v for k, v in feed.items() if k in inn})[0]
return float(1 / (1 + np.exp(-logit.reshape(-1)[0])))
print("\nVERIFY (torch vs int8 ONNX):")
ok = True
for t, ref in zip(samples, torch_ref):
o = onnx_p(t)
d = abs(o - ref)
ok &= d < 0.06
print(f" torch={ref:.3f} onnx={o:.3f} |Δ|={d:.3f}")
json.dump({"model": NAME, "format": "onnx-int8-dynamic", "max_len": 512,
"verified": ok}, open(os.path.join(DST, "meta.json"), "w"), indent=1)
print("\nVERIFIED OK" if ok else "\nWARNING: ONNX deviates from torch — check")
if __name__ == "__main__":
main()