baberu-ocr-webgpu / export_encoder_fp32.py
ameraino11's picture
Add complete 121 MB and 242 MB WebGPU variants, port source, and benchmarks
520f98d verified
Raw
History Blame Contribute Delete
3.55 kB
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import onnxruntime as ort
import torch
from export_decoder_fp32 import (
MODEL_DIR,
OPSET,
OUTPUT_DIR,
REPORT_DIR,
inspect_graph,
load_model,
)
ROOT = Path(__file__).resolve().parent
OFFICIAL_FP16_PATH = MODEL_DIR / "onnx" / "vision_fp16.onnx"
class VisionEncoderProjector(torch.nn.Module):
"""Exact Baberu DINOv2 encoder plus its trained 768 -> 512 projector."""
def __init__(self, ocr_model):
super().__init__()
self.vision_encoder = ocr_model.vision_encoder
self.projector = ocr_model.projector
def forward(self, pixel_values: torch.Tensor) -> torch.Tensor:
vision_states = self.vision_encoder(
pixel_values=pixel_values,
return_dict=False,
)[0]
# DINOv2 position zero is CLS. Baberu uses only the 16 x 16 patch grid.
return self.projector(vision_states[:, 1:, :])
def comparison(expected: np.ndarray, actual: np.ndarray) -> dict[str, float]:
difference = np.abs(expected - actual)
expected_flat = expected.reshape(-1).astype(np.float64)
actual_flat = actual.reshape(-1).astype(np.float64)
cosine = float(
np.dot(expected_flat, actual_flat)
/ (np.linalg.norm(expected_flat) * np.linalg.norm(actual_flat))
)
return {
"max_abs": float(difference.max()),
"mean_abs": float(difference.mean()),
"cosine_similarity": cosine,
}
def run_cpu(path: Path, pixels: np.ndarray) -> np.ndarray:
session = ort.InferenceSession(str(path), providers=["CPUExecutionProvider"])
return session.run(["vision_embeds"], {"pixel_values": pixels})[0]
def main() -> None:
if not (MODEL_DIR / "model.safetensors").exists():
raise SystemExit("Model is missing. Run download_model.py first.")
if not OFFICIAL_FP16_PATH.exists():
raise SystemExit("Official vision_fp16.onnx is missing. Run download_model.py.")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
REPORT_DIR.mkdir(parents=True, exist_ok=True)
torch.manual_seed(23)
torch.set_grad_enabled(False)
model = load_model()
wrapper = VisionEncoderProjector(model).float().eval()
pixels = torch.randn(1, 3, 224, 224, dtype=torch.float32) * 0.25
fp32_path = OUTPUT_DIR / "vision_fp32.onnx"
print(f"Exporting {fp32_path}")
torch.onnx.export(
wrapper,
(pixels,),
fp32_path,
input_names=["pixel_values"],
output_names=["vision_embeds"],
opset_version=OPSET,
do_constant_folding=True,
dynamo=False,
)
with torch.inference_mode():
expected = wrapper(pixels).cpu().numpy()
pixels_numpy = pixels.cpu().numpy()
fp32_actual = run_cpu(fp32_path, pixels_numpy)
fp16_actual = run_cpu(OFFICIAL_FP16_PATH, pixels_numpy)
report = {
"fp32": {
"graph": inspect_graph(fp32_path),
"parity": comparison(expected, fp32_actual),
},
"official_fp16": {
"graph": inspect_graph(OFFICIAL_FP16_PATH),
"parity_to_pytorch_fp32": comparison(expected, fp16_actual),
},
}
report_path = REPORT_DIR / "encoder-report.json"
report_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(f"Wrote {report_path}")
print(json.dumps({key: value.get("parity", value.get("parity_to_pytorch_fp32")) for key, value in report.items()}, indent=2))
if __name__ == "__main__":
main()