7.2.1. Example: timm Model Inference

An application example that retrieves a model from timm and performs inference on the Image (beignets-task-guide.png)

beignets-task-guide.png

Fig. 7.2 beignets-task-guide.png

Execution Method (resnet50.a1h_in1k)

$ cd /opt/pfn/pfcomp/codegen/MLSDK/examples/
$ ./run_timm.sh --model_name resnet50.a1h_in1k --batch_size 16

Expected Output (resnet50.a1h_in1k)

MNCore2 top-5 classes:
- espresso (967)
- cup (968)
- chocolate sauce, chocolate syrup (960)
- consomme (925)
- eggnog (969)
Torch top-5 classes:
- espresso (967)
- cup (968)
- chocolate sauce, chocolate syrup (960)
- eggnog (969)
- consomme (925)

Execution Method (mobilenetv3_small_050.lamb_in1k)

$ cd /opt/pfn/pfcomp/codegen/MLSDK/examples/
$ ./run_timm.sh --model_name mobilenetv3_small_050.lamb_in1k --batch_size 16

Expected Output (mobilenetv3_small_050.lamb_in1k)

MNCore2 top-5 classes:
- cup (968)
- trifle (927)
- face powder (551)
- ice cream, icecream (928)
- coffee mug (504)
Torch top-5 classes:
- cup (968)
- trifle (927)
- ice cream, icecream (928)
- face powder (551)
- coffee mug (504)

Scripts

Listing 7.11 /opt/pfn/pfcomp/codegen/MLSDK/examples/run_timm.sh
 1#! /bin/bash
 2
 3set -eux -o pipefail
 4
 5EXAMPLE_NAME=run_timm
 6VENVDIR=/tmp/${EXAMPLE_NAME}_venv
 7
 8CURRENT_DIR=$(realpath $(dirname $0))
 9CODEGEN_DIR=$(realpath ${CURRENT_DIR}/../../)
10BUILD_DIR=${BUILD_DIR:-${CODEGEN_DIR}/build}
11
12if [[ ! -d ${VENVDIR} ]]; then
13    python3 -m venv --system-site-packages ${VENVDIR}
14    source ${VENVDIR}/bin/activate
19    pip3 install timm==1.0.14 huggingface-hub==0.28.1
20else
21    source ${VENVDIR}/bin/activate
22fi
23
24source "${BUILD_DIR}/codegen_pythonpath.sh"
25
26exec python3 ${CURRENT_DIR}/${EXAMPLE_NAME}.py "$@"
Listing 7.12 /opt/pfn/pfcomp/codegen/MLSDK/examples/run_timm.py
  1import argparse
  2import os
  3from pathlib import Path
  4from typing import Any, Optional, Union
  5
  6import mncore  # noqa: F401
  7import timm
  8import torch
  9from mlsdk import (
 10    Context,
 11    MNCoreSGD,
 12    MNDevice,
 13    set_buffer_name_in_optimizer,
 14    set_tensor_name_in_module,
 15    storage,
 16)
 17from PIL import Image
 18
 19SAMPLE_IMAGE_PATH = os.path.join(
 20    os.path.dirname(__file__), "./datasets/mncore2_chip.png"
 21)
 22
 23
 24def escape_path(path: str) -> str:
 25    escaped = ""
 26    for c in path:
 27        if c.isalnum() or c in "_-":
 28            escaped += c
 29        else:
 30            escaped += "_"
 31    return escaped
 32
 33
 34def create_model_with_cache(
 35    model_name: str, model_cache_dir: Optional[str] = None, **kwargs: Any
 36) -> Any:
 37    if not model_cache_dir:
 38        return timm.create_model(model_name, **kwargs)
 39    else:
 40        timm_version = "timm_version" + timm.__version__
 41        torch_version = "torch_version" + torch.__version__
 42        cache_dir = os.path.join(
 43            model_cache_dir,
 44            escape_path(f"{torch_version}_{timm_version}_{model_name}"),
 45        )
 46        # Load the model always from the cache to return the same model object always.
 47        # This should also create the cache if it does not exist.
 48        return timm.create_model(model_name, **kwargs, cache_dir=cache_dir)
 49
 50
 51def imagenet_classes() -> list[str]:
 52    script_dir = os.path.dirname(__file__)
 53    imagenet_classes_path = os.path.join(script_dir, "imagenet_classes.txt")
 54    with open(imagenet_classes_path) as f:
 55        return [line.strip() for line in f]
 56
 57
 58def run_inference(
 59    model_name: str,
 60    batch_size: int,
 61    outdir: str,
 62    option_json_path: Optional[Path],
 63    device_str: str,
 64    model_cache_dir: Optional[str],
 65) -> None:
 66    img = Image.open(SAMPLE_IMAGE_PATH)
 67    model = create_model_with_cache(
 68        model_name,
 69        pretrained=True,
 70        model_cache_dir=model_cache_dir,
 71    )
 72    model = model.eval()
 73
 74    def infer(input: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
 75        with torch.no_grad():
 76            x = input["images"]
 77            return {"out": model(x)}
 78
 79    data_config = timm.data.resolve_model_data_config(model)
 80    transforms = timm.data.create_transform(**data_config, is_training=False)
 81    images = transforms(img).unsqueeze(0).expand(batch_size, -1, -1, -1)
 82    sample = {"images": images}
 83
 84    device = MNDevice(device_str)
 85    context = Context(device)
 86    Context.switch_context(context)
 87    context.registry.register("model", model)
 88
 89    compile_options: dict[str, str] = {}
 90    if option_json_path is not None:
 91        compile_options = {"option_json": str(option_json_path)}
 92
 93    compiled_infer = context.compile(
 94        infer,
 95        sample,
 96        storage.path(outdir) / "infer",
 97        options=compile_options,
 98    )
 99    result_as_proxy = compiled_infer(sample)
100    result_on_torch = infer(sample)
101
102    # Tensors obtained via ".cpu()" from TensorProxy exist on GPU in CUDA environments,
103    # so they need to be moved to CPU before the comparison.
104    result = result_as_proxy["out"].cpu()
105    if result.is_cuda:
106        result = result.cpu()
107
108    torch.allclose(result, result_on_torch["out"], atol=1e-5)
109
110    if "in1k" in model_name:
111        classes = imagenet_classes()
112        mncore_top5_classes = torch.topk(result[0], 5).indices.cpu()
113        print("MNCore2 top-5 classes:")
114        for i in mncore_top5_classes:
115            print(f"- {classes[i]} ({i.item()})")
116        torch_top5_classes = torch.topk(result_on_torch["out"][0], 5).indices
117        print("Torch top-5 classes:")
118        for i in torch_top5_classes:
119            print(f"- {classes[i]} ({i.item()})")
120
121
122def run_training_torch_onnx(
123    model_name: str,
124    batch_size: int,
125    outdir: str,
126    option_json_path: Optional[Path],
127    device: str,
128    model_cache_dir: Optional[str],
129) -> None:
130    device = MNDevice(device)
131    context = Context(device)
132    Context.switch_context(context)
133
134    img = Image.open(SAMPLE_IMAGE_PATH)
135
136    model = create_model_with_cache(
137        model_name,
138        pretrained=True,
139        num_classes=1000,
140        model_cache_dir=model_cache_dir,
141    )
142    data_config = timm.data.resolve_model_data_config(model)
143    transforms = timm.data.create_transform(**data_config, is_training=False)
144    images = transforms(img).unsqueeze(0).expand(batch_size, -1, -1, -1)
145    labels = torch.randint(0, 1000, (batch_size,))
146    sample = {"images": images, "labels": labels}
147
148    model = model.train()
149    context.registry.register("model", model)
150    optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
151    context.registry.register("optimizer", optimizer)
152    loss_fn = torch.nn.CrossEntropyLoss()
153
154    def f(inputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
155        return {"loss": loss_fn(model(inputs["images"]), inputs["labels"])}
156
157    compile_options: dict[str, Union[str, bool]] = {}
158    if option_json_path is not None:
159        compile_options = {"option_json": str(option_json_path)}
160    compile_options["backprop"] = True
161    compiled_f = context.compile(
162        f,
163        sample,
164        storage.path(outdir) / "train_step_torch_onnx",
165        optimizers=[optimizer],
166        options=compile_options,
167    )
168
169    first_loss = compiled_f(sample)["loss"].cpu()
170    for _ in range(10):
171        compiled_f(sample)
172    context.synchronize()
173    last_loss = compiled_f(sample)["loss"].cpu()
174
175    assert last_loss < first_loss
176
177
178def run_training_fx2onnx(
179    model_name: str,
180    batch_size: int,
181    outdir: str,
182    option_json_path: Optional[Path],
183    device_str: str,
184    model_cache_dir: Optional[str],
185) -> None:
186    device = MNDevice(device_str)
187    context = Context(device)
188    Context.switch_context(context)
189
190    img = Image.open(SAMPLE_IMAGE_PATH)
191
192    model = create_model_with_cache(
193        model_name,
194        pretrained=True,
195        num_classes=1000,
196        model_cache_dir=model_cache_dir,
197    )
198    model = model.train()
199    set_tensor_name_in_module(model, "model0")
200    for p in model.parameters():
201        context.register_param(p)
202
203    optimizer = MNCoreSGD(model.parameters(), 0.1, 0.9, 0.0)
204    set_buffer_name_in_optimizer(optimizer, "optimizer0")
205    context.register_optimizer_buffers(optimizer)
206    loss_fn = torch.nn.CrossEntropyLoss()
207
208    def train_step(input: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
209        x = input["images"]
210        t = input["labels"]
211        optimizer.zero_grad()
212        y = model(x)
213        loss = loss_fn(y, t)
214        loss.backward()
215        optimizer.step()
216        return {"loss": loss}
217
218    data_config = timm.data.resolve_model_data_config(model)
219    transforms = timm.data.create_transform(**data_config, is_training=False)
220    images = transforms(img).unsqueeze(0).expand(batch_size, -1, -1, -1)
221    labels = torch.randint(0, 1000, (batch_size,))
222    sample = {"images": images, "labels": labels}
223
224    compile_options: dict[str, str] = {}
225    if option_json_path is not None:
226        compile_options = {"option_json": str(option_json_path)}
227
228    compiled_train_step = context.compile(
229        train_step,
230        sample,
231        storage.path(outdir) / "train_step_fx2onnx",
232        options=compile_options,
233        export_kwargs={"use_fx2onnx": True},
234    )
235
236    first_loss = compiled_train_step(sample)["loss"].cpu()
237    for _ in range(10):
238        compiled_train_step(sample)
239    context.synchronize()
240    last_loss = compiled_train_step(sample)["loss"].cpu()
241
242    assert last_loss < first_loss
243
244
245if __name__ == "__main__":
246    parser = argparse.ArgumentParser()
247    parser.add_argument("--batch_size", type=int, default=1, required=True)
248    parser.add_argument("--model_name", type=str)
249    parser.add_argument("--outdir", type=str, default="/tmp/mlsdk_timm")
250    parser.add_argument("--option_json", type=Path, default=None)
251    parser.add_argument("--is_training", action="store_true")
252    parser.add_argument(
253        "--device",
254        type=str,
255        default="mncore2:auto",
256        choices=["mncore2:auto", "pfvm:cpu", "pfvm:cuda"],
257    )
258    parser.add_argument(
259        "--model_cache_dir",
260        type=str,
261        default=None,
262        help="Directory to cache the model weights. "
263        "If not set, weights are always downloaded from the hub. default: None",
264    )
265    args = parser.parse_args()
266
267    outdir = args.outdir
268    if outdir is None:
269        outdir = f"/tmp/MLSDK_codegen_dir_{args.model_name}"
270        if args.is_training:
271            outdir += "_training"
272        else:
273            outdir += "_inference"
274
275    # TODO (akirakawata): Should we make this argument?
276    use_fx2onnx = not bool(
277        int(os.environ.get("MNCORE_USE_LEGACY_ONNX_EXPORTER", False))
278    )
279    if args.is_training:
280        if use_fx2onnx:
281            run_training_fx2onnx(
282                args.model_name,
283                args.batch_size,
284                outdir,
285                args.option_json,
286                args.device,
287                args.model_cache_dir,
288            )
289        else:
290            run_training_torch_onnx(
291                args.model_name,
292                args.batch_size,
293                args.outdir,
294                args.option_json,
295                args.device,
296                args.model_cache_dir,
297            )
298    else:
299        run_inference(
300            args.model_name,
301            args.batch_size,
302            args.outdir,
303            args.option_json,
304            args.device,
305            args.model_cache_dir,
306        )