Download convert.py from rinpatch/dinov3-coreml: direct link, hf CLI and curl.
- Browser
- Download file 5.86 kB
-
https://huggingface.co/rinpatch/dinov3-coreml/resolve/main/convert.py
- Command line
-
hf download hf://rinpatch/dinov3-coreml/convert.py
-
curl -L -o convert.py https://huggingface.co/rinpatch/dinov3-coreml/resolve/main/convert.py
5.86 kB
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import shutil | |
| from pathlib import Path | |
| import coremltools as ct | |
| import numpy as np | |
| import timm | |
| import torch | |
| from PIL import Image | |
| DEFAULT_MODEL = "vit_base_patch16_dinov3.lvd1689m" | |
| BASE_MODEL = "facebook/dinov3-vitb16-pretrain-lvd1689m" | |
| IMAGENET_MEAN = (0.485, 0.456, 0.406) | |
| IMAGENET_STD = (0.229, 0.224, 0.225) | |
| class DINOv3Encoder(torch.nn.Module): | |
| def __init__(self, backbone: torch.nn.Module) -> None: | |
| super().__init__() | |
| self.backbone = backbone | |
| self.num_prefix_tokens = backbone.num_prefix_tokens | |
| self.register_buffer("mean", torch.tensor(IMAGENET_MEAN).view(1, 3, 1, 1)) | |
| self.register_buffer("std", torch.tensor(IMAGENET_STD).view(1, 3, 1, 1)) | |
| def forward(self, image: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: | |
| pixels = (image / 255.0 - self.mean) / self.std | |
| features = self.backbone.forward_features(pixels) | |
| cls = features[:, 0] | |
| norm = torch.sqrt(torch.sum(cls * cls, dim=-1, keepdim=True).clamp_min(1e-12)) | |
| # The prefix is one CLS token plus four register tokens. Registers absorb | |
| # high-norm artifacts that would otherwise pollute the patch tokens; they | |
| # are not features and nothing downstream uses them. | |
| patches = features[:, self.num_prefix_tokens:] | |
| # Patches stay unnormalized because dense heads generally want the | |
| # magnitude. Callers doing cosine can normalize per token themselves. | |
| return cls / norm, patches | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser( | |
| description="Convert DINOv3 to a Core ML model returning one CLS embedding." | |
| ) | |
| parser.add_argument("--size", type=int, default=448) | |
| parser.add_argument("--precision", choices=("fp16", "fp32"), default="fp32") | |
| parser.add_argument("--output", type=Path) | |
| parser.add_argument("--force", action="store_true") | |
| return parser.parse_args() | |
| def synthetic_image(size: int) -> Image.Image: | |
| y, x = np.mgrid[0:size, 0:size] | |
| rgb = np.stack( | |
| ((x * 255 // size), (y * 255 // size), ((x + y) * 255 // (size * 2))), | |
| axis=-1, | |
| ).astype(np.uint8) | |
| return Image.fromarray(rgb, mode="RGB") | |
| def main() -> None: | |
| args = parse_args() | |
| if args.size <= 0 or args.size % 16: | |
| raise SystemExit("--size must be a positive multiple of 16") | |
| if args.output is None: | |
| precision_name = args.precision.upper() | |
| args.output = Path(f"models/DINOv3ViTB16-{precision_name}-{args.size}.mlpackage") | |
| if args.output.exists(): | |
| if not args.force: | |
| raise SystemExit(f"{args.output} already exists; pass --force to replace it") | |
| shutil.rmtree(args.output) | |
| example = torch.zeros(1, 3, args.size, args.size, dtype=torch.float32) | |
| backbone = timm.create_model( | |
| DEFAULT_MODEL, | |
| pretrained=True, | |
| img_size=args.size, | |
| num_classes=0, | |
| ).eval() | |
| model = DINOv3Encoder(backbone).eval() | |
| with torch.inference_mode(): | |
| exported = torch.export.export(model, (example,)).run_decompositions({}) | |
| precision = ct.precision.FLOAT16 if args.precision == "fp16" else ct.precision.FLOAT32 | |
| coreml_model = ct.convert( | |
| exported, | |
| convert_to="mlprogram", | |
| inputs=[ | |
| ct.ImageType( | |
| name="image", | |
| shape=example.shape, | |
| color_layout=ct.colorlayout.RGB, | |
| ) | |
| ], | |
| outputs=[ct.TensorType(name="embedding"), ct.TensorType(name="patch_embeddings")], | |
| minimum_deployment_target=ct.target.macOS14, | |
| compute_precision=precision, | |
| ) | |
| coreml_model.author = "dinov3-coreml; base model by Meta" | |
| coreml_model.license = "DINOv3 License" | |
| coreml_model.short_description = "DINOv3 ViT-B/16 CLS embedding and patch tokens" | |
| coreml_model.user_defined_metadata["base_model"] = BASE_MODEL | |
| coreml_model.input_description["image"] = f"RGB image resized to {args.size}x{args.size}" | |
| coreml_model.output_description["embedding"] = "L2-normalized 768-value CLS embedding" | |
| coreml_model.output_description["patch_embeddings"] = "Unnormalized patch tokens, one per 16x16 patch" | |
| args.output.parent.mkdir(parents=True, exist_ok=True) | |
| coreml_model.save(args.output) | |
| image = synthetic_image(args.size) | |
| array = np.asarray(image, dtype=np.float32).transpose(2, 0, 1)[None, ...] | |
| with torch.inference_mode(): | |
| torch_cls, torch_patches = (t.numpy()[0] for t in model(torch.from_numpy(array))) | |
| prediction = coreml_model.predict({"image": image}) | |
| coreml_output = np.asarray(prediction["embedding"])[0] | |
| coreml_patches = np.asarray(prediction["patch_embeddings"])[0] | |
| def cosine_of(a: np.ndarray, b: np.ndarray) -> float: | |
| a, b = a.reshape(-1), b.reshape(-1) | |
| return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) | |
| cosine = cosine_of(torch_cls, coreml_output) | |
| patch_cosine = cosine_of(torch_patches, coreml_patches) | |
| report = { | |
| "model": DEFAULT_MODEL, | |
| "base_model": BASE_MODEL, | |
| "input_size": args.size, | |
| "precision": args.precision, | |
| "output_shape": list(coreml_output.shape), | |
| "patch_output_shape": list(coreml_patches.shape), | |
| "pytorch_coreml_cosine_similarity": cosine, | |
| "pytorch_coreml_patch_cosine_similarity": patch_cosine, | |
| "coreml_output_l2_norm": float(np.linalg.norm(coreml_output)), | |
| } | |
| report_path = args.output.with_suffix(".validation.json") | |
| report_path.write_text(json.dumps(report, indent=2) + "\n") | |
| print(json.dumps({"output": str(args.output), **report}, indent=2)) | |
| if min(cosine, patch_cosine) < 0.999: | |
| raise SystemExit("Core ML parity check failed: cosine similarity is below 0.999") | |
| if __name__ == "__main__": | |
| main() | |