DUSUNEN-Rota-270M-v1 / training /export_browser_model.py
GoktugD's picture
Release GöktuğTR v1 with audited training, benchmark, and browser ONNX
61af2eb verified
Raw
History Blame Contribute Delete
6.43 kB
#!/usr/bin/env python3
"""Export a q8 ONNX model and validate the browser-demo embeddings."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
import onnx
from onnx import numpy_helper
from optimum.onnxruntime.configuration import AutoQuantizationConfig
from sentence_transformers import SentenceTransformer
from sentence_transformers.backend import export_dynamic_quantized_onnx_model
from goktugtr.text import harrier_query
EXAMPLE_QUERIES = [
"Bir modeli küçük GPU'da nasıl verimli eğitebilirim?",
"Arama sistemlerinde kalite nasıl ölçülür?",
"LiDAR verisinden üç boyutlu harita üretmek",
"Yapay zekâ sonuçları neden yeniden üretilebilir olmalı?",
]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--model", type=Path, default=Path("outputs/goktugtr-270m/final"))
parser.add_argument("--space-dir", type=Path, default=Path("space"))
parser.add_argument(
"--report", type=Path, default=Path("artifacts/onnx-browser-validation.json")
)
return parser.parse_args()
def encode(model: SentenceTransformer, texts: list[str]) -> np.ndarray:
return model.encode(
texts,
batch_size=8,
normalize_embeddings=True,
convert_to_numpy=True,
show_progress_bar=True,
).astype("float32")
def safe_quantization_config() -> object:
"""Quantize the large embedding table while preserving transformer fidelity."""
return AutoQuantizationConfig.avx2(
is_static=False,
operators_to_quantize=["Gather"],
)
def requantize_embedding_per_dimension(
onnx_path: Path, pytorch_model: SentenceTransformer
) -> None:
"""Replace the lossy single-scale embedding quantizer with 640 scales."""
embedding = (
pytorch_model[0]
.auto_model.get_input_embeddings()
.weight.detach()
.float()
.cpu()
.numpy()
)
scales = np.max(np.abs(embedding), axis=0).astype("float32") / 127.0
scales = np.maximum(scales, np.finfo("float32").tiny)
quantized = np.clip(np.rint(embedding / scales[None, :]), -127, 127).astype("int8")
zero_points = np.zeros(scales.shape, dtype="int8")
model = onnx.load(onnx_path)
replacements = {
"embed_tokens.weight_quantized": numpy_helper.from_array(
quantized, name="embed_tokens.weight_quantized"
),
"embed_tokens.weight_scale": numpy_helper.from_array(
scales, name="embed_tokens.weight_scale"
),
"embed_tokens.weight_zero_point": numpy_helper.from_array(
zero_points, name="embed_tokens.weight_zero_point"
),
}
replaced: set[str] = set()
for index, initializer in enumerate(model.graph.initializer):
if initializer.name in replacements:
model.graph.initializer[index].CopyFrom(replacements[initializer.name])
replaced.add(initializer.name)
if replaced != set(replacements):
raise RuntimeError(f"Embedding quantizer tensors not found: {set(replacements) - replaced}")
dequantizers = [
node
for node in model.graph.node
if node.op_type == "DequantizeLinear"
and node.name.endswith("Gather_output_0_DequantizeLinear")
]
if len(dequantizers) != 1:
raise RuntimeError(f"Expected one embedding dequantizer, found {len(dequantizers)}")
del dequantizers[0].attribute[:]
dequantizers[0].attribute.append(onnx.helper.make_attribute("axis", 2))
onnx.checker.check_model(model)
temporary = onnx_path.with_suffix(".tmp.onnx")
onnx.save(model, temporary)
temporary.replace(onnx_path)
def main() -> None:
args = parse_args()
corpus = json.loads((args.space_dir / "corpus.json").read_text(encoding="utf-8"))
documents = [item["text"] for item in corpus]
queries = [harrier_query(query) for query in EXAMPLE_QUERIES]
pytorch_model = SentenceTransformer(str(args.model), device="cpu")
pytorch_model.max_seq_length = 256
reference = encode(pytorch_model, documents + queries)
export_model = SentenceTransformer(str(args.model), backend="onnx", device="cpu")
export_dynamic_quantized_onnx_model(
export_model,
quantization_config=safe_quantization_config(),
model_name_or_path=str(args.model),
file_suffix="quantized",
)
onnx_path = args.model / "onnx/model_quantized.onnx"
requantize_embedding_per_dimension(onnx_path, pytorch_model)
q8_model = SentenceTransformer(
str(args.model),
backend="onnx",
device="cpu",
model_kwargs={"file_name": "onnx/model_quantized.onnx"},
)
q8_model.max_seq_length = 256
quantized = encode(q8_model, documents + queries)
document_count = len(documents)
reference_documents = reference[:document_count]
reference_queries = reference[document_count:]
quantized_documents = quantized[:document_count]
quantized_queries = quantized[document_count:]
embedding_cosines = np.sum(reference * quantized, axis=1)
reference_top = np.argmax(reference_queries @ reference_documents.T, axis=1)
quantized_top = np.argmax(quantized_queries @ quantized_documents.T, axis=1)
report = {
"format": "ONNX mixed int8 embedding / float transformer blocks",
"quantization": "per-dimension int8 embedding; float transformer blocks",
"documents": document_count,
"queries": len(queries),
"embedding_dimension": int(quantized.shape[1]),
"mean_pytorch_to_q8_cosine": float(np.mean(embedding_cosines)),
"minimum_pytorch_to_q8_cosine": float(np.min(embedding_cosines)),
"maximum_absolute_component_difference": float(np.max(np.abs(reference - quantized))),
"example_query_top1_agreement": float(np.mean(reference_top == quantized_top)),
"reference_top1_indices": reference_top.tolist(),
"quantized_top1_indices": quantized_top.tolist(),
}
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
(args.space_dir / "embeddings.json").write_text(
json.dumps(np.round(quantized_documents, 7).tolist(), separators=(",", ":")) + "\n",
encoding="utf-8",
)
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()