File size: 3,545 Bytes
520f98d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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()