첫 번째 모델을 컴파일하세요.
이 튜토리얼에서는 ResNet-50 ONNX 모델을 Model Compiler를 사용하여 사후 학습 양자화(PTQ) 워크플로를 거칩니다. 결과는 Neat 런타임용으로 컴파일된 .tar.gz MPK 아카이브입니다.
이 워크플로는 네 단계로 구성됩니다.
- 모델을 로드합니다.
- 기본적으로 INT8로 양자화하고, 요청 시에는 BF16으로 양자화합니다.
- 정확성을 확인하십시오.
- MLSoC에서 실행할 수 있도록 컴파일하세요.
필수 조건
sima-cli가 설치되었습니다(sima-cli 설정 가이드 참조).- Model Compiler 설치된 위치 Neat SDK 또는 Ubuntu 호스트에 설치합니다. 다음을 입력하세요. 다음 환경에서:
activate-model-compiler
예제를 가져오세요.
Model Compiler가 설치된 Neat SDK 또는 Ubuntu 호스트에서 sima-cli를 사용하여 Model Compiler 예제를 설치합니다.
sima-cli neat install model-compiler/examples
튜토리얼의 나머지 부분에서는 Model Compiler 환경을 활성 상태로 유지하십시오.
양자화 및 컴파일 예제를 실행합니다. 이 스크립트는 ResNet-50 ONNX 모델을 생성하고, 필요한 경우 공개 Open Images 교정 데이터를 다운로드합니다.
cd resnet50-ptq
python3 compile.py
검증 입력이 제공되면 실행 과정에서 골든 리트리버 이미지를 ImageNet 클래스 207으로 분류하고, 컴파일된 아카이브를 생성해야 합니다.
Validation image prediction:
class 207: 'golden retriever' -> 98.82%
Quantization complete.
Compiling model. Output directory: .../compiled_resnet50
Compiled MPK archive written to: .../compiled_resnet50/quantized_resnet50_mpk.tar.gz
결과적으로 생성된 .tar.gz 파일을 사용하여 정확도와 성능을 검증하거나,
또는 이를 사용하여 파이프라인 애플리케이션을 구축할 수 있습니다.
다음 섹션에서는 각 단계를 설명합니다. 전체 스크립트는 마지막에 나타납니다. MLA 테셀레이션은 기본적으로 활성화되어 있으므로 컴파일된 모델이 가속기에 직접 전달됩니다. 컴파일 > 테셀레이션을 참조하십시오.
작동 방식
1. 모델을 불러옵니다.
ONNX ResNet-50 모델을 SDK의 내부 표현 방식으로 로드합니다.
from afe.apis.loaded_net import load_model
from afe.apis.defines import gen1_target, gen2_target
from afe.load.importers.general_importer import onnx_source
from afe.ir.tensor_type import ScalarType
MODEL_PATH = "resnet50.onnx"
TARGET = gen2_target # gen2_target = Modalix, gen1_target = MLSoC
# Model information
input_name, input_shape, input_type = ("input", (1, 3, 224, 224), ScalarType.float32)
input_shapes_dict = {input_name: input_shape}
input_types_dict = {input_name: input_type}
# Load the ONNX model
importer_params = onnx_source(str(MODEL_PATH), input_shapes_dict, input_types_dict)
loaded_net = load_model(importer_params, target=TARGET)
입력 텐서 "input"는 (1, 3, 224, 224) 형태를 가집니다. 즉, 배치 크기는 1, 색상 채널은 3개, 224x224 픽셀이며, 데이터 유형은 float32입니다. onnx_source는 모델을 읽는 방법을 설명합니다(실제 ONNX 파일은 변경되지 않음). load_model은 모델을 양자화를 위해 사용할 수 있는 LoadedNet으로 변환합니다. TARGET은 플랫폼을 선택합니다. MLSoC에는 gen1_target를, Modalix에는 gen2_target를 사용 합니다.
2. 보정 데이터 세트를 준비합니다.
양자화에는 작고 대표적인 보정 데이터 세트가 필요합니다. 이 데이터 세트는 FP32 값을 정수 범위로 매핑하는 스케일링 요소를 설정하여 과도한 클리핑이나 정밀도 손실을 방지합니다.
import cv2
import numpy as np
from sima_utils.data.data_generator import DataGenerator
from afe.core.utils import convert_data_generator_to_iterable
MODEL_INPUT_NAME = "input"
IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
def preprocess(image: np.ndarray, size=(224, 224)) -> np.ndarray:
"""Resize to 224x224, scale to [0, 1], normalize. Returns HWC float32."""
image = cv2.resize(image, size).astype(np.float32) / 255.0
return ((image - IMAGENET_MEAN) / IMAGENET_STD).astype(np.float32)
# Build a DataGenerator from preprocessed NHWC calibration images.
calibration_images = np.stack([preprocess(image) for image in raw_calibration_images])
calib_data = convert_data_generator_to_iterable(
DataGenerator({MODEL_INPUT_NAME: calibration_images}))
DataGenerator에 전달되는 보정 데이터는 NHWC 레이아웃([batch, height, width, channels])으로 제공되어야 합니다. 모델의 입력 텐서가 NCHW([batch, channels, height, width])인 경우에도 마찬가지입니다. 예를 들어, ONNX 입력 형식이 (1, 3, 224, 224)인 경우입니다. 위의 예제는 이미 NHWC 형식을 생 성합니다. 이는 preprocess가 HWC 이미지를 반환하기 때문입니다. 전처리 파이프라인에서 NCHW 배열을 생성하는 경우, 보정 데이터 세트를 생성하기 전에 배열을 전치해야 합니다.
# Convert NCHW -> NHWC
calibration_images = np.transpose(calibration_images, (0, 2, 3, 1))
calib_data = convert_data_generator_to_iterable(
DataGenerator({MODEL_INPUT_NAME: calibration_images}))
배포할 워크로드와 동일한 입력 분포에서 대표적인 이미지를 사용하세요.
3. 양자화
모델을 로드하고 보정 데이터를 준비한 후 양자화를 수행합니다. 패키지 예제는 기본적으로 INT8을 사용하는데, 이는 널리 지원되는 방식이기 때문입니다. 일부 모델은 INT8 양자화 중에 포화 경고를 표시할 수 있습니다. 따라서 컴파일된 출력을 사용하기 전에 양자화된 모델을 검증하십시오.
from afe.apis.defines import QuantizationParams, quantization_scheme, CalibrationMethod
from afe.core.utils import convert_data_generator_to_iterable
quant_configs = QuantizationParams(
calibration_method=CalibrationMethod.from_str('mse'),
activation_quantization_scheme=quantization_scheme(
asymmetric=True, per_channel=False, bits=8),
weight_quantization_scheme=quantization_scheme(
asymmetric=False, per_channel=True, bits=8),
)
sdk_net = loaded_net.quantize(
calib_data,
quant_configs,
model_name="quantized_resnet50",
)
이 예제에서는 활성화 값에 대해 8비트 비대칭 텐서별 양자화를 사용하고, 가중치에는 8비트 대칭 채널별 양자화를 사용합니다. BF16 및 보정 옵션에 대한 자세한 내용은 **Quantization**를 참조하십시오.
4. 정확성 검증
컴파일하기 전에 양자화된 모델을 소프트웨어에서 sdk_net.execute(...)를 사용하여 실행하고, 여전히 올바르게 분류하는지 확인하세요.
import numpy as np
def postprocess_output(output: np.ndarray, labels: list[str]):
probabilities = output[0][0]
idx = int(np.argmax(probabilities))
name = labels[idx] if idx < len(labels) else "?"
return idx, name, probabilities[idx]
# A known image: a Golden Retriever is ImageNet class 207.
with open("data/imagenet_labels.txt") as f:
labels = [line.strip() for line in f]
dog = preprocess(cv2.cvtColor(cv2.imread("data/golden_retriever_207.jpg"), cv2.COLOR_BGR2RGB))
output = sdk_net.execute(inputs={"input": np.expand_dims(dog, axis=0)})
idx, name, score = postprocess_output(output, labels)
print(f"class {idx}: '{name}' -> {100.0 * score:.2f}%")
정확하고 신뢰도가 높은 예측, 예를 들어 207 'golden retriever' -> 98.82%는 전처리 및 양자화가 올바르게 수행되었음을 나타냅니다. 오분류는 일반적으로 전처리 과정의 불일치 또는 재조정이 필요한 양자화 문제를 나타냅니다.
5. 컴파일
검증이 완료되면 모델을 저장하고 컴파일합니다.
sdk_net.save(model_name="quantized_resnet50", output_directory=args.output)
tess = mla_tessellate_params(sdk_net) if args.mla_tessellation else None
sdk_net.compile(output_path=args.output, tessellate_parameters=tess)
결과는 다음과 같습니다. .tar.gz 컴파일된 MLA 프로그램이 포함된 아카이브입니다.
_mpk.json 메타데이터 파일과 실행 통계 파일입니다. 자세한 내용은 다음을 참조하십시오.컴파일** 아카이브 콘텐츠, 배치 크기 및 테셀레이션 옵션에 대해.
전체 대본
완전한 주석이 추가된 프로그램은 아래에 있습니다. 또한, 이 프로그램은 resnet50-ptq/compile.py로, 그리고 ResNet-50 PTQ 예제 소스로 GitHub에서 확인할 수 있습니다.
sima-cli neat install model-compiler/examples
cd resnet50-ptq
이 스크립트는 사용자 지정 ONNX 모델과 보정 이미지 폴더를 대상으로 실행됩니다.
python3 compile.py \
--model resnet50.onnx \
--calib_images ./calib_images \
--output ./compiled_resnet50
# optional accuracy check:
# --validate golden_retriever_207.jpg --labels imagenet_labels.txt
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Compile your first model — ResNet-50 PTQ end-to-end.
Loads an ONNX ResNet-50, calibrates on a folder of images, quantizes to INT8
by default, optionally validates accuracy, and compiles to an MPK ``.tar.gz``.
MLA tessellation is **enabled by default** (inputs HWC, outputs HWC16, driven
directly to/from the MLA, bypassing the EV74 reorder unit). Disable it with
``--no-mla-tessellation`` if your pipeline needs the EV74 reorder path.
Example:
python3 compile.py
"""
import argparse
import logging
import os
import pickle
import subprocess
import sys
from pathlib import Path
import cv2
import numpy as np
from afe.apis.loaded_net import load_model
from afe.apis.defines import (
gen1_target, gen2_target,
QuantizationParams, quantization_scheme, bfloat16_scheme, CalibrationMethod,
TensorTessellateParameters, TensorDRAMLayout,
)
from afe.load.importers.general_importer import onnx_source
from afe.ir.tensor_type import ScalarType
from afe.ir.node import node_is_tuple
from afe.core.utils import convert_data_generator_to_iterable
from sima_utils.data.data_generator import DataGenerator
# ImageNet preprocessing constants (ResNet-50 was trained with these).
IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
INPUT_SHAPE = (1, 3, 224, 224) # NCHW
EXAMPLE_ROOT = Path(__file__).resolve().parent
DEFAULT_MODEL = EXAMPLE_ROOT / "models" / "resnet50_model.onnx"
DEFAULT_CALIBRATION_DATASET = EXAMPLE_ROOT / "data" / "openimages_v7_images_and_labels.pkl"
DEFAULT_VALIDATE_IMAGE = EXAMPLE_ROOT / "data" / "golden_retriever_207.jpg"
DEFAULT_LABELS = EXAMPLE_ROOT / "data" / "imagenet_labels.txt"
PRECISION_CHOICES = ("auto", "bf16", "int8")
logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
log = logging.getLogger("compile")
def preprocess(image: np.ndarray, size=(224, 224)) -> np.ndarray:
"""Resize to 224x224, scale to [0, 1], normalize. Returns HWC float32."""
image = cv2.resize(image, size).astype(np.float32) / 255.0
return ((image - IMAGENET_MEAN) / IMAGENET_STD).astype(np.float32)
def load_calibration_images(folder: str, num_samples: int) -> np.ndarray:
"""Read up to `num_samples` images from `folder` into an NHWC batch."""
exts = (".jpg", ".jpeg", ".png", ".bmp")
paths = [os.path.join(folder, f) for f in sorted(os.listdir(folder))
if f.lower().endswith(exts)][:num_samples]
if not paths:
raise FileNotFoundError(f"No calibration images found in {folder}")
images = [preprocess(cv2.cvtColor(cv2.imread(p), cv2.COLOR_BGR2RGB)) for p in paths]
return np.stack(images) # (N, 224, 224, 3) — the SDK expects NHWC
def load_calibration_dataset(path: Path, num_samples: int) -> np.ndarray:
"""Read calibration images from the generated Open Images pickle."""
with path.open("rb") as file_obj:
dataset = pickle.load(file_obj)
images = dataset.get("data")
if not isinstance(images, list) or not images:
raise ValueError(f"Calibration dataset does not contain image data: {path}")
if len(images) < num_samples:
raise ValueError(
f"Calibration dataset has {len(images)} image(s), but {num_samples} were requested: {path}"
)
return np.stack([preprocess(image) for image in images[:num_samples]])
def run_helper(script: Path, *args: str) -> None:
cmd = [sys.executable, str(script), *args]
log.info("Running: %s", " ".join(cmd))
subprocess.run(cmd, check=True, cwd=str(EXAMPLE_ROOT))
def ensure_default_model(model_path: Path) -> None:
if model_path.is_file():
return
log.info("Model not found at %s; downloading and exporting ResNet-50.", model_path)
run_helper(EXAMPLE_ROOT / "models" / "download_resnet50.py")
if not model_path.is_file():
raise FileNotFoundError(f"Model generation did not create expected file: {model_path}")
def calibration_dataset_size(dataset_path: Path) -> int:
if not dataset_path.is_file():
return 0
with dataset_path.open("rb") as file_obj:
dataset = pickle.load(file_obj)
images = dataset.get("data") if isinstance(dataset, dict) else None
if not isinstance(images, list):
return 0
return len(images)
def ensure_default_calibration_dataset(dataset_path: Path, num_samples: int) -> None:
existing_samples = calibration_dataset_size(dataset_path)
if existing_samples >= num_samples:
return
if existing_samples:
log.info(
"Calibration dataset at %s has %d samples; regenerating with %d samples.",
dataset_path,
existing_samples,
num_samples,
)
else:
log.info("Calibration dataset not found at %s; downloading Open Images samples.", dataset_path)
run_helper(
EXAMPLE_ROOT / "data" / "download_openimages_calibration.py",
"--samples", str(num_samples),
"--output", str(dataset_path),
)
generated_samples = calibration_dataset_size(dataset_path)
if generated_samples < num_samples:
raise RuntimeError(
f"Calibration download created {generated_samples} image(s), "
f"but {num_samples} were requested: {dataset_path}"
)
def mla_tessellate_params(quant_model):
"""Map every MLA input to HWC and every MLA output to HWC16 (direct MLA)."""
mla = quant_model._net.nodes["MLA_0"]
in_tess = TensorTessellateParameters(
tile_shape=(0, 0, 0, 0), enable_mla=True, dram_layout=TensorDRAMLayout.HWC)
out_tess = TensorTessellateParameters(
tile_shape=(0, 0, 0, 0), enable_mla=True, dram_layout=TensorDRAMLayout.HWC16)
params = {name: in_tess for name in mla.input_names}
out_node = mla.ir.nodes[mla.ir.output_node_name]
out_names = out_node.input_node_names if node_is_tuple(out_node) else [out_node.name]
for name in out_names:
params[f"{name}_output"] = out_tess
return params
def validate(sdk_net, image_path: str, labels_path: str, input_name: str) -> None:
"""Run the quantized model on one image and print the top-1 class."""
with open(labels_path) as f:
labels = [line.strip() for line in f]
image = preprocess(cv2.cvtColor(cv2.imread(image_path), cv2.COLOR_BGR2RGB))
output = sdk_net.execute(inputs={input_name: np.expand_dims(image, axis=0)})
probabilities = output[0][0]
idx = int(np.argmax(probabilities))
name = labels[idx] if idx < len(labels) else "?"
print("Validation image prediction:", flush=True)
print(f" class {idx}: '{name}' -> {100.0 * probabilities[idx]:.2f}%", flush=True)
def main() -> int:
ap = argparse.ArgumentParser(description="Compile your first model (ResNet-50 PTQ).")
ap.add_argument(
"--model",
default=None,
help=f"Path to the ResNet-50 ONNX model. Defaults to {DEFAULT_MODEL}.",
)
ap.add_argument(
"--calib_images",
default=None,
help="Folder of calibration images. Defaults to generated Open Images calibration data.",
)
ap.add_argument("--output", default="./compiled_resnet50", help="Output directory.")
ap.add_argument("--device", "--boardtype", default="modalix", choices=["modalix", "mlsoc"],
help="Target hardware (modalix=gen2, mlsoc=gen1).")
ap.add_argument("--input_name", default="input", help="Model input tensor name.")
ap.add_argument("--num_calib_samples", type=int, default=50, help="Calibration sample count.")
ap.add_argument(
"--precision",
choices=PRECISION_CHOICES,
default="auto",
help="Quantization precision. Defaults to int8.",
)
ap.add_argument("--bf16", action="store_true", help="Compatibility alias for --precision bf16.")
ap.add_argument("--validate", metavar="IMAGE",
help="Validate the quantized model on IMAGE (requires --labels).")
ap.add_argument("--labels", help="ImageNet labels file, one class per line.")
ap.add_argument("--no-mla-tessellation", action="store_false", dest="mla_tessellation",
help="Disable direct-MLA tessellation (use the EV74 reorder path).")
ap.set_defaults(mla_tessellation=True)
args = ap.parse_args()
os.makedirs(args.output, exist_ok=True)
target = gen2_target if args.device == "modalix" else gen1_target
precision = "bf16" if args.bf16 else args.precision
if precision == "auto":
precision = "int8"
if precision == "bf16" and args.device != "modalix":
ap.error("BF16 is only supported for Modalix. Use --device modalix or --precision int8.")
model_path = Path(args.model).expanduser().resolve() if args.model else DEFAULT_MODEL
ensure_default_model(model_path)
# 1. Load the ONNX model.
importer = onnx_source(
str(model_path),
{args.input_name: INPUT_SHAPE},
{args.input_name: ScalarType.float32},
)
loaded_net = load_model(importer, target=target)
log.info("Loaded %s for %s", model_path, args.device)
# 2. Prepare the calibration dataset.
if args.calib_images:
calib_images = load_calibration_images(args.calib_images, args.num_calib_samples)
else:
ensure_default_calibration_dataset(DEFAULT_CALIBRATION_DATASET, args.num_calib_samples)
calib_images = load_calibration_dataset(DEFAULT_CALIBRATION_DATASET, args.num_calib_samples)
calib_data = convert_data_generator_to_iterable(
DataGenerator({args.input_name: calib_images}))
log.info("Prepared %d calibration samples", len(calib_images))
# 3. Quantize. INT8 is the default; BF16 remains explicit while compiler support matures.
log.info("Quantizing with %s precision", precision.upper())
if precision == "bf16":
quant_configs = QuantizationParams(
calibration_method=CalibrationMethod.from_str("mse"),
activation_quantization_scheme=bfloat16_scheme(),
weight_quantization_scheme=bfloat16_scheme(),
)
else:
quant_configs = QuantizationParams(
calibration_method=CalibrationMethod.from_str("mse"),
activation_quantization_scheme=quantization_scheme(asymmetric=True, per_channel=False, bits=8),
weight_quantization_scheme=quantization_scheme(asymmetric=False, per_channel=True, bits=8),
)
sdk_net = loaded_net.quantize(calib_data, quant_configs, model_name="quantized_resnet50")
print("Quantization complete.", flush=True)
# 4. (Optional) Validate accuracy.
validate_image = Path(args.validate).expanduser().resolve() if args.validate else DEFAULT_VALIDATE_IMAGE
labels_path = Path(args.labels).expanduser().resolve() if args.labels else DEFAULT_LABELS
if validate_image.is_file() and labels_path.is_file():
validate(sdk_net, str(validate_image), str(labels_path), args.input_name)
elif args.validate or args.labels:
ap.error("--validate and --labels must both point to existing files")
# 5. Compile (MLA tessellation on by default).
sdk_net.save(model_name="quantized_resnet50", output_directory=args.output)
tess = mla_tessellate_params(sdk_net) if args.mla_tessellation else None
if tess:
log.info("MLA tessellation enabled (inputs HWC, outputs HWC16)")
output_dir = Path(args.output).expanduser().resolve()
print(f"Compiling model. Output directory: {output_dir}", flush=True)
sdk_net.compile(output_path=args.output, tessellate_parameters=tess)
compiled_archive = output_dir / "quantized_resnet50_mpk.tar.gz"
if compiled_archive.is_file():
print(f"Compiled MPK archive written to: {compiled_archive}", flush=True)
else:
archives = sorted(output_dir.glob("*_mpk.tar.gz"))
if archives:
print(f"Compiled MPK archive written to: {archives[-1]}", flush=True)
else:
print(f"Compiled model artifacts written to: {output_dir}", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())
다음 단계
컴파일된 .tar.gz 파일을 사용하여 첫 번째 런타임 파이프라인을 구축하거나, 심층적인 양자화 및 컴파일 가이 드로 계속 진행하세요.