| 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] |
| |
| 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() |
|
|