Skip to content

Commit 42b7709

Browse files
authored
Add a torchvision classifier exporter
Summary: ## Ulterior Motive Validate Native serialization with realistic CNN packages and full-vector eager references. ## Rationale **What**: Add a torchvision example under `examples/native/vision` that exports MobileNetV2 (`mv2`) or ResNet-50 (`resnet50`) directly to a PTN package. **Why**: Toy graphs do not stress realistic constant sets and argmax-only checks can hide large numerical errors. The exporter emits all artifacts required for complete output comparison. ## Details ```text examples/native/vision/export.py |-- select pretrained model and matching image preprocessing |-- compute eager reference logits |-- torch.export.export -> to_native |-- NativeProgram.save -> <model>.ptn `-- write input, logits, labels, JSON, and text summary ``` The PTG is stored inside the PTN package; no standalone `.ptg` is emitted. Supported model names are `mv2` and `resnet50`. Authored with Codex. Reviewed By: metascroy Differential Revision: D119601864 Pull Request resolved: pytorch#23141
1 parent d874ac3 commit 42b7709

2 files changed

Lines changed: 167 additions & 0 deletions

File tree

‎examples/native/vision/BUCK‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
load("@fbcode_macros//build_defs:build_file_migration.bzl", "fbcode_target")
2+
load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime")
3+
4+
oncall("executorch")
5+
6+
fbcode_target(
7+
_kind = runtime.python_binary,
8+
name = "export",
9+
typing = True,
10+
srcs = ["export.py"],
11+
main_function = "executorch.examples.native.vision.export.main",
12+
deps = [
13+
"fbsource//third-party/pypi/pillow:pillow",
14+
"//caffe2:torch",
15+
"//executorch/examples/models:models",
16+
"//executorch/exir/native:native",
17+
"//pytorch/vision:torchvision", # @manual
18+
],
19+
)

‎examples/native/vision/export.py‎

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# pyre-strict
8+
9+
"""Export a torchvision classifier and its validation artifacts to PTN."""
10+
11+
from __future__ import annotations
12+
13+
import argparse
14+
import json
15+
import logging
16+
from pathlib import Path
17+
from typing import TypedDict
18+
19+
import torch
20+
from executorch.examples.models import MODEL_NAME_TO_MODEL
21+
from executorch.examples.models.model_factory import EagerModelFactory
22+
from executorch.exir.native import to_native
23+
from PIL import Image
24+
from torchvision.models import MobileNet_V2_Weights, ResNet50_Weights # @manual
25+
26+
logger: logging.Logger = logging.getLogger(__name__)
27+
28+
_MODEL_NAMES = ("mv2", "resnet50")
29+
30+
31+
class _Expected(TypedDict):
32+
input_shape: list[int]
33+
output_shape: list[int]
34+
argmax: int
35+
label: str
36+
top5: list[tuple[int, str, float]]
37+
38+
39+
def _default_out_dir() -> Path:
40+
return Path.home() / "scratch/models/native_classification"
41+
42+
43+
def _load_model(name: str) -> torch.nn.Module:
44+
model, _, _, _ = EagerModelFactory.create_model(*MODEL_NAME_TO_MODEL[name])
45+
return model.eval()
46+
47+
48+
def _load_image(name: str, image_path: Path) -> tuple[torch.Tensor, list[str]]:
49+
with Image.open(image_path) as image:
50+
rgb_image = image.convert("RGB")
51+
if name == "mv2":
52+
weights = MobileNet_V2_Weights.DEFAULT
53+
image_tensor = weights.transforms()(rgb_image)
54+
categories = list(weights.meta["categories"])
55+
else:
56+
weights = ResNet50_Weights.IMAGENET1K_V1
57+
image_tensor = weights.transforms()(rgb_image)
58+
categories = list(weights.meta["categories"])
59+
return image_tensor.unsqueeze(0).contiguous(), categories
60+
61+
62+
def _expected(
63+
model: torch.nn.Module, example_input: torch.Tensor, categories: list[str]
64+
) -> tuple[torch.Tensor, _Expected]:
65+
with torch.no_grad():
66+
logits = model(example_input)
67+
flat = logits[0]
68+
probabilities = torch.softmax(flat, dim=0)
69+
top5 = torch.topk(flat, 5)
70+
argmax = int(torch.argmax(flat).item())
71+
return logits.contiguous(), {
72+
"input_shape": list(example_input.shape),
73+
"output_shape": list(logits.shape),
74+
"argmax": argmax,
75+
"label": categories[argmax],
76+
"top5": [
77+
(int(index), categories[int(index)], float(probabilities[int(index)]))
78+
for index in top5.indices.tolist()
79+
],
80+
}
81+
82+
83+
def _write_fp32(path: Path, tensor: torch.Tensor) -> None:
84+
if tensor.dtype is not torch.float32:
85+
raise ValueError(f"expected float32 tensor, got {tensor.dtype}")
86+
array = tensor.detach().to("cpu").contiguous().numpy()
87+
path.write_bytes(array.astype(array.dtype.newbyteorder("<"), copy=False).tobytes())
88+
89+
90+
def _summary(
91+
model_name: str,
92+
paths: dict[str, Path],
93+
methods: set[str],
94+
expected: _Expected,
95+
) -> str:
96+
return "\n".join(
97+
[
98+
f"model: {model_name}",
99+
f"package: {paths['ptn']} ({paths['ptn'].stat().st_size} bytes)",
100+
f"methods: {', '.join(sorted(methods))}",
101+
f"input: {paths['input']} (shape {expected['input_shape']})",
102+
f"expected: {paths['expected_bin']} (shape {expected['output_shape']})",
103+
f" argmax={expected['argmax']} label={expected['label']!r}",
104+
"top5: "
105+
+ ", ".join(
106+
f"{label}={probability:.3f}"
107+
for _, label, probability in expected["top5"]
108+
),
109+
]
110+
)
111+
112+
113+
def main() -> None:
114+
logging.basicConfig(level=logging.INFO)
115+
parser = argparse.ArgumentParser()
116+
parser.add_argument("--model", choices=_MODEL_NAMES, default="mv2")
117+
parser.add_argument("--out-dir", type=Path, default=None)
118+
parser.add_argument("--image", type=Path, required=True)
119+
args = parser.parse_args()
120+
121+
output_dir = (args.out_dir or _default_out_dir()) / args.model
122+
output_dir.mkdir(parents=True, exist_ok=True)
123+
paths = {
124+
"ptn": output_dir / f"{args.model}.ptn",
125+
"input": output_dir / f"{args.model}_input.bin",
126+
"expected_bin": output_dir / f"{args.model}_expected.bin",
127+
"expected_json": output_dir / f"{args.model}_expected.json",
128+
"categories": output_dir / "categories.txt",
129+
"summary": output_dir / f"{args.model}.summary.txt",
130+
}
131+
132+
model = _load_model(args.model)
133+
example_input, categories = _load_image(args.model, args.image)
134+
logits, expected = _expected(model, example_input, categories)
135+
native_program = to_native(torch.export.export(model, (example_input,)))
136+
137+
native_program.save(str(paths["ptn"]))
138+
_write_fp32(paths["input"], example_input)
139+
_write_fp32(paths["expected_bin"], logits)
140+
paths["expected_json"].write_text(json.dumps(expected, indent=2) + "\n")
141+
paths["categories"].write_text("\n".join(categories) + "\n")
142+
summary = _summary(args.model, paths, native_program.methods, expected)
143+
paths["summary"].write_text(summary + "\n")
144+
logger.info("wrote %s\n%s", output_dir, summary)
145+
146+
147+
if __name__ == "__main__":
148+
main()

0 commit comments

Comments
 (0)