ONNX → Cubie NPU 컴파일 가이드
본 문서는 ONNX 모델을 Cubie A7A 보드의 NPU에서 실행할 수 있는 .saigeedge 번들로
컴파일하는 절차를 안내합니다. detection, mlc, reid 세 가지 task 모두 동일한 진입점인
scripts/compile.py를 사용합니다.
1. 개요
컴파일의 입력과 출력, 실행 조건은 다음과 같습니다.
- 입력 — ONNX 모델 1개와 calibration dataset(
val.json) 1개. - 출력 —
.saigeedge번들. NBG(.nb)와 runtime config(yaml)를 하나로 묶은 단일 파일입니다. - 실행 환경 — docker가 동작하는 호스트. ACUITY 툴체인이 docker 이미지 내부에만 존재합니다.
- 양자화 — INT8 PTQ(uint8 asymmetric_affine). calibration 이미지로 양자화 scale을 추정합니다.
docker와 이미지가 준비된 호스트(준비 절차는 3장)에서의 전체 실행 절차는 다음과 같습니다.
# 저장소 이동 및 Python 환경 활성화 (pyyaml·onnx가 있으면 어떤 환경이든 가능)
cd <repo>/SaigeSafetyEdge
conda activate saige_safety_edge # 환경 이름은 예시. 별도 env가 없으면 생략 가능
# matplotlib 캐시를 host-local로 지정 (NFS 쓰기 회피, 선택 사항)
export MPLCONFIGDIR=/tmp/mpl
# manifest의 (task, onnx, calib, output, num_classes)를 모델에 맞게 채운 뒤 실행
python scripts/compile.py config/compile_models.yml --only detection
# 산출물: <output>.saigeedge (예: runs/det.cubie.saigeedge)
2. 구성 요소
컴파일 과정에 등장하는 주요 구성 요소는 다음과 같습니다.
| 구성 요소 | 설명 |
|---|---|
| Cubie A7A | Radxa의 SoC 보드. 칩은 Allwinner A733, NPU는 Vivante VIP9000 NANODI입니다. |
| AWNN / VIPLite | 보드에서 NBG를 로드하고 추론을 수행하는 런타임(libawnn.so)입니다. |
| ACUITY toolkit / pegasus | Allwinner와 Verisilicon이 배포하는 비공개 컴파일러입니다. pegasus CLI로 ONNX를 양자화하고 컴파일하며, 비공개 배포 특성상 Radxa 공식 docker 이미지 내부에서만 동작합니다. |
NBG (.nb) |
pegasus가 생성하는 보드 실행 바이너리(Network Binary Graph)입니다. |
.saigeedge |
NBG와 runtime config(yaml)를 묶은 SaigeEdge 배포 번들입니다. 호스트와 보드의 추론 API가 이 파일을 읽습니다. |
컴파일러(SaigeEdge.convert.compiler.cubie.CubieCompiler)는 호스트에서 실행되며,
docker exec로 컨테이너 안의 pegasus를 호출합니다. 따라서 호스트에는 docker만 설치되어
있으면 되고, ACUITY는 따로 설치하지 않아도 됩니다.
3. 사전 준비
이하 절차는 docker가 설치되지 않은 환경을 기준으로 합니다. 이미 docker와 이미지가 준비된 호스트라면 3.1절의 확인만으로 이후 설치 단계를 건너뛸 수 있습니다.
3.1 준비 상태 확인
다음 두 명령이 모두 OK를 출력하면 3.2절과 3.3절을 건너뛰고 3.4절로 이동합니다.
docker ps >/dev/null 2>&1 && echo "docker OK" || echo "docker 설치 또는 권한 필요 (3.2절)"
docker image inspect ubuntu-npu:v2.0.10.1 >/dev/null 2>&1 \
&& echo "image OK" || echo "이미지 로드 필요 (3.3절)"
docker가 동작하고 ACUITY 이미지가 존재하는 호스트라면 GPU 없이도 컴파일할 수 있습니다.
다만 GPU 워크스테이션처럼 docker 기본 런타임이 nvidia로 설정된 호스트에서는 컨테이너
기동이 실패할 수 있습니다. 이 경우 8장의 nvidia-container-runtime 항목을 참고하십시오.
사내에는 docker와 이미지가 미리 구성된 컴파일 호스트로 lnx2가 준비되어 있으므로,
별도 환경을 직접 구성하기 어렵다면 lnx2를 사용하면 됩니다.
3.2 Docker 설치 및 권한 설정
(a) Docker 엔진 설치 (Ubuntu 기준)
# 공식 설치 스크립트 (Ubuntu/Debian 계열)
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# 또는 배포판 패키지 사용
sudo apt-get update && sudo apt-get install -y docker.io
docker --version
(b) docker 그룹에 사용자 추가
컨테이너가 산출물을 root 권한으로 생성하지 않도록, 그리고 매 명령에 sudo를 사용하지
않도록 현재 사용자를 docker 그룹에 추가합니다.
sudo usermod -aG docker "$USER"
newgrp docker # 또는 로그아웃 후 재로그인하여 그룹을 반영합니다.
docker ps # 권한 오류 없이 동작해야 합니다.
계정이 docker 그룹에 속하지 않으면 docker ps가 권한 오류를 반환합니다. sudo 권한이
없는 호스트에서는 그룹 추가가 어려우므로, 이미 docker와 이미지가 구성된 lnx2를
사용하면 됩니다.
3.3 ACUITY 이미지 설치 — ubuntu-npu:v2.0.10.1 (약 7.8GB)
ACUITY 툴체인은 Docker Hub에 공개되어 있지 않습니다. 아래 두 방법 중 하나로 이미지를
확보한 뒤 docker load로 등록합니다.
방법 A — 사내 NFS 캐시본 (권장)
사내 호스트라면 미리 받아 둔 tarball을 그대로 로드하는 것이 가장 빠릅니다. 외부 다운로드 없이 NFS read 한 번으로 끝나며, 로컬 docker 스토리지에만 기록되므로 NFS 쓰기도 없습니다.
docker load -i /NFS/database_personal/NPU/cubie/resources/ubuntu-npu_v2.0.10.1.tar
docker image inspect ubuntu-npu:v2.0.10.1 >/dev/null && echo "image OK"
캐시본의 정식 보관 경로와 접근 권한은 확정 중입니다. 위 경로에 접근할 수 없으면 방법 B를 사용하십시오.
방법 B — 외부 다운로드 (캐시본이 없을 때)
Allwinner가 tarball로만 배포하므로 직접 내려받아 등록합니다.
# (1) Allwinner netstorage에서 docker_images 묶음을 내려받습니다.
# https://netstorage.allwinnertech.com:5001/sharing/Mh23BhPHq
# 브라우저 로그인이 필요할 수 있으며, 내려받은 zip을 호스트로 전송합니다.
# 참고 문서(Radxa): cubie/a7a/app-dev/npu-dev/cubie-acuity-env
# (2) 압축을 해제합니다. 이미지가 zip 내부에 다시 zip으로 포함되어 있습니다.
unzip docker_images_v2.0.x.zip
cd docker_images_v2.0.x
unzip ubuntu-npu_v2.0.10.1.tar.zip
# (3) 이미지를 등록합니다. 약 7.8GB로 수 분이 소요됩니다.
docker load -i ubuntu-npu_v2.0.10.1.tar
# docker 그룹에 속하지 않은 경우: sudo docker load -i ...
# (4) 태그가 정확히 일치하는지 확인합니다. 컴파일러가 이 태그를 고정 참조합니다.
docker image inspect ubuntu-npu:v2.0.10.1 >/dev/null && echo "image OK"
태그가 ubuntu-npu:v2.0.10.1과 다르면 컴파일러가 이미지를 찾지 못합니다. 다른 태그로
등록된 경우 docker tag <loaded> ubuntu-npu:v2.0.10.1로 맞춰 주십시오. 컨테이너는
컴파일러가 saige-cubie라는 이름으로 자동 생성하고 재사용하므로, 직접 docker run을
실행할 필요는 없습니다.
3.4 Python 환경
본 저장소는 ONNX만을 입력으로 사용하므로 torch 의존성이 없습니다. 호스트 측 컴파일에는 다음 두 패키지가 필요합니다.
pyyaml— inputmeta yml 패치에 사용합니다.onnx— 출력 shape 추출에 사용합니다(SaigeEdgerequirements/common.txt에 포함).
저장소 표준 설치로 충분합니다.
컴파일 단계 자체는 pyyaml과 onnx만 있으면 동작합니다. 본 문서의 saige_safety_edge는
환경 이름의 예시일 뿐이며, 두 패키지만 설치되어 있으면 base conda를 비롯해 어떤 Python
환경에서도 컴파일할 수 있습니다. 저장소와 config를 공유 스토리지에 두면 docker가 동작하는
어느 호스트에서나 동일한 경로로 컴파일할 수 있습니다.
3.5 입력 준비
다음 두 입력을 준비합니다. 각 요건은 4장과 5장에서 설명합니다.
- ONNX 모델 — 4장의 요건을 충족해야 합니다.
- Calibration dataset (
val.json) — 5장의 요건을 충족해야 합니다.
4. 입력 ONNX 요건
- 고정된 shape가 필요합니다. 출력 텐서에 symbolic dim(
dim_param)이 남아 있으면 컴파일이 실패합니다. AWNN 런타임에는 출력 개수와 shape를 조회하는 API가 없어, 컴파일 시점의 ONNX graph가 유일한 기준입니다. export 시 batch 등 동적 축을 고정하십시오. - Detection(YOLOX) 은
decode_forward로 export한 ONNX를 그대로 사용합니다. - Cubie 백엔드는
/head/Concat_output_0,/head/Concat_1_output_0,/head/Concat_2_output_0(stride 8/16/32)을 새로운 출력으로 노출하는 3-tensor output-cut surgery를 자동으로 적용합니다. INT8 PTQ에서 단일 출력 YOLOX의 sigmoid 채널이 zero-collapse하는 현상을 막기 위한 처리입니다. - airockchip export는 사용하지 마십시오. p3/p4/p5를 사전에 wrap하여 surgery 대상인
/head/Concat노드를 제거하므로output_names not found in graph오류가 발생합니다. 표준 export를 사용하십시오. - 권장 export 경로는 SafetyTraining의
scripts/export_onnx_npu.py(decode_forward)입니다. - head 노드 이름이 다른 경우, manifest entry에
surgery: {output_names: [...]}를 지정하여 재정의할 수 있습니다.
5. Calibration dataset
INT8 PTQ는 activation 분포를 바탕으로 양자화 scale을 결정하므로 대표성 있는 이미지가 필요합니다.
val.json형식이며,n_classes,validationsplit, 이미지 절대경로 등의 필드를 포함합니다.- 이미지 경로는 절대경로여야 합니다. symlink나 상대경로는 컨테이너 내부에서 참조가 끊겨 실패합니다. 컴파일러가 calibration 이미지를 work_dir로 복사하므로 이 문제는 자동으로 해소됩니다.
n_samples는 기본값 100을 권장합니다. 빠른 검증에는 16까지 줄일 수 있으며, 배포본은 값을 높이는 것이 좋습니다.- detection처럼 클래스 분포가 불균형한 데이터셋에서는 Cubie가 object-count 기준이 아닌
image-level class-balanced sampler(
CubieImageBalancedCalibration)를 사용합니다. object-count로 균등화하면 다수 클래스가 과소 수집되어 mAP가 하락할 수 있으며(2026-05-26, person AP −8.7%p), 이를 막기 위한 방식입니다. 이미지 한 장이 클래스 하나에 대응하는 classification에는 영향이 없습니다.
6. 컴파일 실행
6.1 Manifest 작성 (config/compile_models.yml)
저장소에는 예시 manifest로 config/compile_models.yml 이 포함되어 있으며, detection /
mlc / reid 세 모델의 entry가 모두 정의되어 있습니다. 이 파일을 복사해 각 entry의 경로만
본인 모델에 맞게 수정하면 됩니다.
entry마다 (task, onnx, calib, output, num_classes) 다섯 항목만 지정하면, 나머지 옵션
(normalize, mean, std, postprocess, input_size)은 task 프로파일(scripts/compile_profiles.py)이
자동으로 채웁니다.
# config/compile_models.yml
target: cubie # 모든 entry의 기본 백엔드
calibration_method: ema # ema | minmax | kl | normal | auto
n_samples: 100
seed: 1337
models:
- task: detection # detection | mlc | reid
onnx: /path/best.onnx
calib: /path/val.json
output: runs/det.cubie.saigeedge
num_classes: 10
# 선택적 재정의:
# input_size: [544, 960] # (H, W). 생략 시 ONNX에서 도출합니다.
# runtime_overrides: {postprocess: {conf_threshold: 0.3}}
# surgery: {output_names: [...]} # 사용자 정의 head 노드
# password: "..." # 번들 AES 암호화
각 모델의 onnx·calib·output 절대경로는 config/compile_models.yml에 정의되어 있습니다.
경로는 환경에 따라 바뀌므로, 정확한 값은 항상 이 파일에서 확인하고 본인 모델·데이터셋
경로로 교체하십시오. 현재 정의된 세 모델의 구성 요약은 다음과 같습니다.
| 모델(task) | 모델 | num_classes |
input_size |
비고 |
|---|---|---|---|---|
detection |
YOLOX-S | 10 | ONNX에서 도출(640×640) | decode_forward ONNX 사용 |
mlc |
swsl-resnet50 | 7 | 128×128 (config에서 override) | 클래스: helmet, head, person, fire, smoke, fall_down, harness |
reid |
MobileNetV2 (torchreid) | 751 | 256×128 | 예시 onnx는 저장소에 포함되지 않으므로 본인 가중치로 교체해야 합니다 |
위 구성은 변경될 수 있습니다. 표와
config/compile_models.yml이 다르면 항상 파일을 기준으로 삼으십시오. 표의input_size는 detection을 제외하고 config의input_size항목으로 직접 지정한 값이며, 6.1절 하단의 task 프로파일 기본값보다 우선합니다.
task별 자동 프로파일은 다음과 같습니다. 기본 input_size는 프로파일 기본값이며, entry에
input_size를 지정하면 그 값이 우선합니다(예: 현재 mlc entry는 128×128로 override되어
아래 224×224 기본값이 적용되지 않습니다).
| task | 기본 input_size | normalize | mean / std | postprocess |
|---|---|---|---|---|
detection |
ONNX에서 도출 | div255 | [0,0,0] / [1,1,1] |
conf 0.25 / iou 0.45 |
mlc |
224×224 | div255 | [0,0,0] / [1,1,1] |
per-class threshold 50.0 |
reid |
256×128 | ImageNet | [.485,.456,.406] / [.229,.224,.225] |
없음(임베딩 출력) |
6.2 실행
manifest 경로를 인자로 전달하며, --only로 컴파일할 모델(task)을 선택합니다.
# 전체 entry 컴파일
python scripts/compile.py config/compile_models.yml
# 모델별 컴파일 (해당 task만 선택)
python scripts/compile.py config/compile_models.yml --only detection # detection 모델
python scripts/compile.py config/compile_models.yml --only mlc # mlc 모델
python scripts/compile.py config/compile_models.yml --only reid # reid 모델
# 백엔드 호출 없이 프로파일과 경로만 검증
python scripts/compile.py config/compile_models.yml --dry-run
컴파일에 성공하면 output 경로에 .saigeedge 파일이 생성됩니다. 실패 처리는 단계에 따라
다릅니다.
- 경로·필수 키 검증 실패 (onnx/calib 파일 부재, 필수 항목 누락) — 해당 entry에서 실행이
즉시 전체 중단됩니다(
SystemExit). 뒤따르는 entry는 시도되지 않고 요약 줄도 출력되지 않습니다. 여러 모델을 한 번에 컴파일하기 전에--dry-run으로 경로를 먼저 검증하십시오. - 백엔드 컴파일 실패 (컨테이너·pegasus 단계) — 해당 entry만 실패로 수집하고 나머지 entry를 계속 진행하며, 하나라도 실패하면 종료 코드 1을 반환합니다.
공유 스토리지 주의 — 컴파일 로그는 host-local 경로(
/tmp,~/runs)에 먼저 기록한 뒤 완료 후mv로 옮기십시오. 네트워크 마운트(NFS/CIFS)에 직접 기록하면 부하나 hang을 유발할 수 있습니다.
7. 내부 동작 (pegasus 파이프라인)
실행 흐름은 scripts/compile.py → build_compiler(_target_="cubie") →
CubieCompiler.compile() 순서입니다. 각 단계는 Radxa의 pegasus_*.sh 셸 스크립트와 1:1로
대응합니다.
ONNX
└─ (detection) YOLOX 3-tensor output-cut surgery → <stem>_surgery.onnx
└─ docker exec saige-cubie 내부:
1. pegasus import onnx → <stem>.json (graph) + <stem>.data (weights)
2. pegasus generate inputmeta → <stem>_inputmeta.yml
3. pegasus generate postprocess-file → <stem>_postprocess_file.yml
4. inputmeta.yml 패치 → dataset.txt 경로 + mean/scale 주입
5. pegasus quantize (INT8 PTQ) → <stem>_uint8.quantize
6. pegasus export ovxlib
--pack-nbg-unify --optimize <PID> → network_binary.nb
└─ CheckpointHandler.save: .nb + runtime config(yaml) → .saigeedge
mean/scale은 _derive_mean_scale이 자동으로 환산합니다. 보드 호스트는 raw uint8 [0,255]
값(nhwc_uint8)을 NBG에 그대로 전달하므로, NBG 내부에서 (x_uint8 - mean) * scale 형태로
정규화를 융합합니다.
normalize=false→mean=[0],scale=[1/255]normalize=true→mean = host_mean·255,scale = 1/(host_std·255)- 예: ResNet50(ImageNet) →
mean=[123.675, 116.28, 103.53],scale ≈ [0.01712, 0.01751, 0.01743]
컴파일이 끝나면 runtime config에 input_layout: nhwc_uint8, normalize: false,
output_shapes(ONNX 출력 shape)가 기록되며, detection의 경우 task: detection_yolox가
추가로 기록됩니다.
8. 문제 해결
| 증상 | 원인 및 해결 |
|---|---|
'docker' CLI not found 또는 권한 오류 |
docker 미설치 또는 그룹 미포함. 3.2절을 참고하십시오. |
컨테이너 기동 시 nvidia-container-runtime ... not found |
호스트 docker 기본 런타임이 nvidia로 설정됨(GPU 워크스테이션에 흔함). 아래 명령으로 컨테이너를 runc로 미리 띄우면 컴파일러가 그대로 재사용합니다. |
Docker image 'ubuntu-npu:v2.0.10.1' not found |
사내 NFS 캐시본을 docker load하거나(3.3절 방법 A), 캐시본이 없으면 netstorage에서 받습니다(방법 B). |
ONNX output '...' has symbolic dim |
동적 축이 남아 있습니다. export 시 shape를 고정합니다(4장). |
output_names not found in graph (detection) |
airockchip export로 /head/Concat 노드가 제거됨. 표준 export를 사용하거나 surgery로 재정의합니다. |
Data File ... could not be found / FileNotFoundError: best.onnx |
calibration 또는 ONNX 경로가 컨테이너에 마운트되지 않은 위치를 가리킵니다. 절대경로를 사용하십시오. |
pegasus quantize did not produce ...quantize (종료 코드 0이지만 산출물 없음) |
quantizer와 qtype 조합이 비호환하여 발생하는 silent fail입니다. SAIGE_CUBIE_KEEP_WORK=1로 work_dir를 보존한 뒤 로그 말미를 확인합니다. |
보드에서 vip_create_network status=-4 |
NBG의 PID와 칩 cid가 불일치합니다. A733의 기본값은 VIP9000NANODI_PLUS_PID0X1000003B이며, 다른 보드는 10장을 참고합니다. |
Fatal model generation error: 65280 (hybrid 적용 시) |
출력 텐서를 float16으로 승격할 수 없습니다. 컴파일러가 @output*을 자동 제외하므로 직접 승격하지 마십시오. |
| 런타임 detection 디코드 실패 | task가 detection으로 지정됨. Cubie detection은 detection_yolox여야 하며, 컴파일러가 자동으로 설정합니다. |
nvidia-container-runtime 오류는 컨테이너를 runc로 직접 기동하여 우회할 수 있습니다.
마운트 경로(-v)에는 컴파일 output 디렉터리가 포함되어야 합니다. 아래처럼 컨테이너를
미리 띄워 두면 컴파일러가 같은 이름(saige-cubie)의 컨테이너를 그대로 재사용합니다.
docker run -d --name saige-cubie --runtime=runc --ipc=host \
-v /tmp/cubie_out:/tmp/cubie_out ubuntu-npu:v2.0.10.1 tail -f /dev/null
SAIGE_CUBIE_KEEP_WORK=1을 지정하면 중간 산출물(json, data, inputmeta, quantize, nbg)이
<output_dir>/cubie_work_<stem>/에 보존되어 컨테이너 내부에서 단계별 재현이 가능합니다.
detection은 output-cut surgery 이후 stem이 <stem>_surgery로 바뀌므로, 작업 디렉터리와 최종
NBG도 각각 cubie_work_<stem>_surgery/, <stem>_surgery.nb 이름을 사용합니다(예: best.onnx
→ best_surgery.nb).
9. 환경 변수 및 튜닝 옵션
다음은 정식 인터페이스로 노출되기 전, sweep 목적으로 환경 변수로 제공되는 옵션입니다.
| 환경 변수 | 기본값 | 용도 |
|---|---|---|
SAIGE_CUBIE_KEEP_WORK |
off | 중간 산출물 보존(디버깅). |
SAIGE_CUBIE_QUANTIZER |
asymmetric_affine |
quantizer 변경 실험. |
SAIGE_CUBIE_QTYPE |
uint8 |
양자화 dtype 변경 실험. |
SAIGE_CUBIE_HYBRID_TOPN |
off | entropy가 가장 낮은 N개 텐서를 float16으로 승격(양자화 손실 회복). |
SAIGE_CUBIE_HYBRID_DTYPE |
float16 |
hybrid 승격 dtype. |
manifest의 calibration_method는 pegasus --algorithm으로 다음과 같이 매핑됩니다.
ema → moving_average, minmax / normal → normal, kl → kl_divergence, auto → auto
MobileNetV3와 같이 SE-block을 포함한 모델의 outlier 처리에는 kl이 normal보다 정확도
회복에 유리합니다.
10. 다른 보드로의 이식
CubieCompiler._TARGET_PLATFORM과 _VSIM_CONFIG를 대상 칩의 PID로 변경하면 됩니다.
이 값은 pegasus export --optimize 인자이자 NBG 헤더에 기록되는 칩 PID로, 보드 커널이
ioctl로 보고하는 VIP cid와 일치해야 합니다.
| 보드 / 칩 | PID 문자열 |
|---|---|
| T527 (Cubie A7S) | VIP9000PICO_PID0XEE |
| A527 | VIP9000NANOSI_PLUS_PID0X10000016 |
| A733 (Cubie A7A, 현재 기본값) | VIP9000NANODI_PLUS_PID0X1000003B |
11. 산출물 검증
- 번들 구조 확인 —
.saigeedge는checkpoint.yaml과<stem>.nb를 담은 zip입니다 (schema_version: 2). runtime config는 다음 명령으로 확인합니다.
detection의 경우 task: detection_yolox, output_shapes(3-tensor),
input_layout: nhwc_uint8가 포함되어 있어야 정상입니다.
2. 보드 추론 — .saigeedge를 Cubie 보드로 복사하고 로드한 뒤 E2E 추론 또는 벤치마크로
동작을 확인합니다. 보드의 출력은 로컬 디스크에 기록한 뒤 작업 종료 후 옮기십시오. 네트워크
마운트(CIFS/NFS)에 직접 기록하면 D-state hang이 발생할 수 있습니다.
3. 정확도 및 지연 비교 — GPU 대비 비교가 필요하면 별도의 E2E 벤치마크 결과와 대조합니다.