视觉画布设计
canvas-design
基于设计原则创作静态视觉作品,用于海报与品牌图形。
onnx-to-cvimodel【文档与办公】Expert guide for converting ONNX models to CVIMODEL format for Sophgo CV181x TPU. Supports YOLO11/YOLO26 (detect, pose, seg, cls), BiSeNetv2 (semantic segmentation), and PP-LiteSeg (semantic segmentation). Includes tested conversion scripts, quantization tables (qtables), ION memory optimization (--quant_output), validation workflow, and complete doc…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
This skill provides scripts, configurations, and guidance for converting models from ONNX to CVIMODEL format for Sophgo CV181x TPU (reCamera, SG200x).
Before suggesting or running conversion commands, require:
sophgo/tpuc_dev:v3.1 imageIf a required dependency or input is missing:
If conversion completes but ONNX vs CVIMODEL validation was not run, do not describe the model as verified.
Supported models:
The conversion process uses TPU-MLIR Docker environment and requires:
# Export YOLO11n to ONNX and convert to CVIMODEL in one go
python3 export_and_convert.py --model yolo11n --task detect
# Or use existing ONNX
./convert_yolo11_detect.sh yolo11n.onnx dataset/
./convert_to_cvimodel.sh <onnx_file> <dataset_dir>
./convert_yolo11_detect.sh <onnx> <dataset> # YOLO11 detection
./convert_yolo11_pose.sh <onnx> <dataset> # YOLO11 pose (needs qtable)
./convert_yolo11_seg.sh <onnx> <dataset> # YOLO11 segmentation (needs qtable)
./convert_yolo11_cls.sh <onnx> <dataset> # YOLO11 classification
./convert_yolo26_detect.sh <onnx> <dataset> # YOLO26 detection
./convert_yolo26_cls.sh <onnx> <dataset> # YOLO26 classification
./convert_bisenetv2.sh <onnx> <dataset> # BiSeNetv2 (INT8 + BF16 + INT8_quant_output)
./convert_ppliteseg.sh <onnx> <dataset> # PP-LiteSeg (INT8 + INT8_quant_output)
PP-LiteSeg requires ONNX graph surgery before conversion (handled automatically by the script):
count_include_pad (0 → 1, cv181x requirement)./batch_convert_all.sh # Convert all ONNX files in current directory
Problem: CV181x ION memory is shared between TPU, VPSS, VENC. Total ~60MB. INT8 models with float32 output dequantize on-chip, consuming massive ION memory.
Solution: Use --quant_output flag to keep int8 output, avoiding dequantization.
# Without --quant_output (INT8 model, float32 output) → 62.89MB ION ❌
model_deploy.py ... --quantize INT8 --model model_int8.cvimodel
# With --quant_output (INT8 model, int8 output) → 34.27MB ION ✅
model_deploy.py ... --quantize INT8 --quant_output --model model_int8_qout.cvimodel
When to use --quant_output:
When NOT to use --quant_output:
| BiSeNetv2 Variant | Model Size | ION Memory | Output Type |
|---|---|---|---|
| BF16 | 14 MB | 87.59 MB | float32 |
| INT8 (no --quant_output) | 6.0 MB | 62.89 MB | float32 |
| INT8 (--quant_output) | 5.9 MB | 34.27 MB | int8 |
| PP-LiteSeg Variant | Model Size | ION Memory | Output Type |
|---|---|---|---|
| INT8 (no --quant_output) | 10.3 MB | 59.00 MB | float32 |
| INT8 (--quant_output) | 9.8 MB | 26.73 MB | int8 |
The Docker image sophgo/tpuc_dev:v3.1 ships with /workspace/tpu-mlir empty.
You MUST mount your local tpu-mlir installation:
# Correct: mount local tpu-mlir
docker run --rm \
-v /path/to/local/tpu-mlir:/workspace/tpu-mlir \
-v /path/to/work:/work \
sophgo/tpuc_dev:v3.1 bash -c \
'source /workspace/tpu-mlir/envsetup.sh && ...'
# Wrong: using the empty /workspace/tpu-mlir inside Docker
Models converted with --fuse_preprocess expect uint8 RGB NHWC input, NOT float32 NCHW:
# CORRECT: feed raw uint8 RGB data
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
img_resized = cv2.resize(img_rgb, (MODEL_W, MODEL_H))
input_data = np.ascontiguousarray(img_resized, dtype=np.uint8)
# Flatten to (1, 1, 1, H*W*3) for input_image_raw
model.inputs[0].data[:] = input_data.reshape(model.inputs[0].data.shape)
# WRONG: feeding float32 NCHW normalized data
After conversion, always validate ONNX vs CVIMODEL output consistency:
# 1. ONNX inference (standard float32 pipeline)
from ultralytics import YOLO
import onnxruntime as ort, numpy as np, cv2
sess = ort.InferenceSession("model.onnx")
img = cv2.imread("test.jpg")
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_float = cv2.resize(img_rgb, (W, H)).astype(np.float32) / 255.0
img_norm = (img_float - mean) / std
img_nchw = np.transpose(img_norm, (2, 0, 1))[np.newaxis].astype(np.float32)
onnx_out = sess.run(None, {input_name: img_nchw})[0]
label_onnx = np.argmax(onnx_out[0], axis=0).astype(np.uint8)
# 2. CVIMODEL inference (uint8 RGB, fuse_preprocess handles normalization)
import pyruntime_cvi as cvi
model = cvi.Model("model_cv181x_int8_qout.cvimodel", output_all_tensors=True)
img_rgb2 = cv2.cvtColor(cv2.imread("test.jpg"), cv2.COLOR_BGR2RGB)
input_data = np.ascontiguousarray(cv2.resize(img_rgb2, (W, H)), dtype=np.uint8)
model.inputs[0].data[:] = input_data.reshape(model.inputs[0].data.shape)
model.forward()
# Find int8 output (name contains "preds" and dtype is int8)
for out in model.outputs:
if "preds" in out.name:
cvi_out = out.data
label_cvi = np.argmax(cvi_out[0].astype(np.int32), axis=0).astype(np.uint8)
# 3. Compare
agreement = np.sum(label_onnx == label_cvi) / label_onnx.size * 100
print(f"Pixel agreement: {agreement:.2f}%")
| Metric | Value |
|---|---|
| ONNX → CVIMODEL pixel agreement | 98.76% (image 1), 99.00% (image 2) |
| road IoU | 0.9982 |
| sidewalk IoU | 0.9896 |
| building IoU | 0.9821 |
| car IoU | 0.9161 / 0.9565 |
| vegetation IoU | 0.9509 / 0.9440 |
| Device inference (CV181x TPU) | pre: 2ms, infer: 276ms, post: 158ms = 436ms total |
| ION memory (int8_qout) | 34.27 MB |
Quantization errors are concentrated at:
| Metric | Value |
|---|---|
| ONNX → CVIMODEL pixel agreement | 92.40% (image 1), 96.24% (image 2) |
| Avg agreement | 94.32% |
| road IoU | 0.9905 / 0.9520 |
| building IoU | 0.8785 / 0.9654 |
| car IoU | 0.7909 / 0.9328 |
| vegetation IoU | 0.9083 / 0.4895 |
| sidewalk IoU | 0.9458 |
| ION memory (int8) | 59.00 MB |
| ION memory (int8_qout) | 26.73 MB |
PP-LiteSeg has lower pixel agreement than BiSeNetV2 (94% vs 98%) due to:
Recommendation: Use --quant_output variant (26.73 MB ION) — standard INT8 (59 MB) is near the cv181x ~60MB ION limit.
| Model | ONNX Size | CVIMODEL Size | Command |
|---|---|---|---|
| YOLO11n detect | 10.7 MB | 3.0 MB | ./convert_yolo11_detect.sh yolo11n.onnx dataset/ |
| YOLO11n pose | 11.3 MB | 3.6 MB | ./convert_yolo11_pose.sh yolo11n-pose.onnx dataset/ |
| YOLO11n seg | 11.7 MB | 4.0 MB | ./convert_yolo11_seg.sh yolo11n-seg.onnx dataset/ |
| YOLO11n cls | 10.8 MB | 3.0 MB | ./convert_yolo11_cls.sh yolo11n-cls.onnx dataset/ |
| YOLO26n detect | 9.4 MB | 2.9 MB | ./convert_yolo26_detect.sh yolo26n.onnx dataset/ |
| YOLO26n cls | 11.3 MB | 3.0 MB | ./convert_yolo26_cls.sh yolo26n-cls.onnx dataset/ |
| BiSeNetv2 seg | 13 MB | 5.9 MB (INT8_qout) | ./convert_bisenetv2.sh bisenetv2.onnx dataset/ |
| PP-LiteSeg seg | 32 MB | 9.8 MB (INT8_qout) | ./convert_ppliteseg.sh pp_liteseg.onnx dataset/ |
--output_names /model.23/cv2.0/cv2.0.2/Conv_output_0,/model.23/cv3.0/cv3.0.2/Conv_output_0,/model.23/cv2.1/cv2.1.2/Conv_output_0,/model.23/cv3.1/cv3.1.2/Conv_output_0,/model.23/cv2.2/cv2.2.2/Conv_output_0,/model.23/cv3.2/cv3.2.2/Conv_output_0
--output_names /model.23/one2one_cv2.0/one2one_cv2.0.2/Conv_output_0,/model.23/one2one_cv3.0/one2one_cv3.0.2/Conv_output_0,/model.23/one2one_cv2.1/one2one_cv2.1.2/Conv_output_0,/model.23/one2one_cv3.1/one2one_cv3.1.2/Conv_output_0,/model.23/one2one_cv2.2/one2one_cv2.2.2/Conv_output_0,/model.23/one2one_cv3.2/one2one_cv3.2.2/Conv_output_0
--output_names output0,output1
--output_names output0
--quantize_table yolo11n_pose_qtable
--output_names preds
--input_shapes [[1,3,512,1024]]
--mean 123.675,116.28,103.53 --scale 0.01712475,0.01750700,0.01742919
--output_names p2o.pd_op.bilinear_interp.6.0
--input_shapes [[1,3,512,1024]]
--mean 123.675,116.28,103.53 --scale 0.01712475,0.01750700,0.01742919
PP-LiteSeg ONNX graph surgery required (see PP-LiteSeg Conversion Details below).
BiSeNetv2 is a lightweight semantic segmentation model. Key differences from YOLO:
[1,3,512,1024] (H, W configurable via env vars)123.675,116.28,103.53 / 0.01712,0.01751,0.01743)--keep_aspect_ratio: Fixed resolution input# Basic conversion (produces 3 variants: int8, bf16, int8_qout)
./scripts/convert_bisenetv2.sh bisenetv2.onnx ./dataset
# Custom resolution
INPUT_H=256 INPUT_W=512 ./scripts/convert_bisenetv2.sh bisenetv2.onnx ./dataset
# Custom model name
MODEL_NAME=bisenetv2_custom ./scripts/convert_bisenetv2.sh bisenetv2.onnx ./dataset
| Variable | Default | Description |
|---|---|---|
MODEL_NAME | bisenetv2_cityscapes | Model name used in output filenames |
CHIP | cv181x | Target chip |
INPUT_H | 512 | Input height |
INPUT_W | 1024 | Input width |
CALIBRATION_EPOCHS | 20 | Number of calibration images |
OUTPUT_NAME | preds | ONNX output tensor name |
MEAN | 123.675,116.28,103.53 | ImageNet mean |
SCALE | 0.01712475,0.01750700,0.01742919 | ImageNet scale |
QUANT_OUTPUT | true | Whether to generate --quant_output variant |
PP-LiteSeg is a PaddlePaddle lightweight semantic segmentation model. It requires ONNX graph surgery before TPU-MLIR conversion.
PP-LiteSeg ONNX has these issues that must be fixed:
Remove ArgMax + Cast nodes: Original output is [1, H, W] int32 (post-argmax label map). TPU-MLIR needs [1, 19, H, W] float32 pre-argmax logits. Remove the last ArgMax and Cast nodes, expose the preceding Resize (bilinear_interp) output.
Fix AveragePool count_include_pad: PP-LiteSeg has 5 AveragePool ops with count_include_pad=0, but cv181x hardware requires count_include_pad=1. Change all 5 to =1. Impact: minimal (< 0.01% pixel difference).
Pre-simplify with local onnxsim: Docker's onnxsim (inside sophgo/tpuc_dev:v3.1) introduces broken Squeeze ops. Run onnxsim locally (pip install onnxsim) before feeding to Docker. Do NOT run onnxsim inside Docker.
# Graph surgery (automated by convert_ppliteseg.sh)
import onnx, onnxsim
from onnx import TensorProto, helper
model = onnx.load("pp_liteseg.onnx")
# 1. Find last Resize output name
resize_out = None
for node in model.graph.node:
if node.op_type == 'Resize':
resize_out = node.output[0]
# 2. Add as new graph output
new_out = helper.make_tensor_value_info(resize_out, TensorProto.FLOAT, [1, 19, 512, 1024])
model.graph.output.insert(0, new_out)
# 3. Simplify LOCALLY (not in Docker)
model, check = onnxsim.simplify(model, test_input_shapes={'x': [1, 3, 512, 1024]})
# 4. Fix AveragePool count_include_pad
for node in model.graph.node:
if node.op_type == 'AveragePool':
for attr in node.attribute:
if attr.name == 'count_include_pad':
attr.i = 1
onnx.save(model, "pp_liteseg_prepared.onnx")
# Basic conversion (produces int8 + int8_qout variants)
./scripts/convert_ppliteseg.sh pp_liteseg.onnx ./dataset
# Custom resolution
INPUT_H=256 INPUT_W=512 ./scripts/convert_ppliteseg.sh pp_liteseg.onnx ./dataset
| Variable | Default | Description |
|---|---|---|
MODEL_NAME | pp_liteseg | Model name |
CHIP | cv181x | Target chip |
INPUT_H | 512 | Input height |
INPUT_W | 1024 | Input width |
CALIBRATION_EPOCHS | 20 | Calibration images |
MEAN | 123.675,116.28,103.53 | ImageNet mean |
SCALE | 0.01712475,0.01750700,0.01742919 | ImageNet scale |
QUANT_OUTPUT | true | Generate int8 output variant |
PP-LiteSeg cvimodel is detected as MA_MODEL_TYPE_BISENETV2 (type 17) by sscma-model. The BiSeNetV2 postprocessor handles both int8 and float32 outputs via argmax, so PP-LiteSeg works directly with sscma-model:
# On device (RGB_PACKED format, sscma-model feeds HWC packed data)
sscma-model pp_liteseg_int8_qout.cvimodel input.jpg result.jpg
Important: Use RGB_PACKED (not RGB_PLANAR) format. sscma-model always feeds HWC packed data via rgb888_to_rgb888().
docker run --privileged --rm \
-v /path/to/local/tpu-mlir:/workspace/tpu-mlir \
-v $PWD:/work \
-w /workspace sophgo/tpuc_dev:v3.1 bash -c "
source /workspace/tpu-mlir/envsetup.sh
mkdir -p workspace && cd workspace
cp ../model.onnx .
model_transform.py --model_name MODEL --model_def model.onnx \
--input_shapes '[[1,3,640,640]]' --mean 0.0,0.0,0.0 --scale 0.0039216,0.0039216,0.0039216 \
--keep_aspect_ratio --pixel_format rgb --output_names 'OUTPUT_NAMES' \
--test_input test.jpg --test_result top.npz --mlir model.mlir
run_calibration.py model.mlir --dataset ../dataset --input_num 100 -o calib_table
model_deploy.py --mlir model.mlir --quantize INT8 --quant_input \
--processor cv181x --calibration_table calib_table \
--test_input test.jpg --test_reference top.npz \
--customization_format RGB_PACKED --fuse_preprocess --aligned_input \
--model model_cv181x_int8.cvimodel
"
from ultralytics import YOLO
YOLO('yolo11n.pt').export(format='onnx', imgsz=640, simplify=False, opset=12) # Detection
YOLO('yolo11n-cls.pt').export(format='onnx', imgsz=224, simplify=False, opset=12) # Classification
YOLO('yolo11n-pose.pt').export(format='onnx', imgsz=640, simplify=False, opset=12) # Pose
YOLO('yolo11n-seg.pt').export(format='onnx', imgsz=640, simplify=False, opset=12) # Segmentation
import onnx
from onnx import shape_inference
model = onnx.load('model.onnx')
model = shape_inference.infer_shapes(model)
for output in model.graph.output:
shape = [str(d.dim_value) if d.dim_value else '?'
for d in output.type.tensor_type.shape.dim]
print(f"{output.name}: [{', '.join(shape)}]")
For pose/segmentation, use qtable for better accuracy:
# Copy qtables from skill assets
cp assets/yolo11n_pose_qtable ./
cp assets/yolo11n_seg ./
# Use in deployment
model_deploy.py ... --quantize_table yolo11n_pose_qtable \
--model yolo11n-pose_cv181x_mix.cvimodel
| Model | Reason | Alternative |
|---|---|---|
| YOLO26n pose | Mod operation not supported | Use YOLO11n-pose |
| YOLO26n seg | Mod operation not supported | Use YOLO11n-seg |
| ONNX TopK/Argmax ops | Not supported in TPU-MLIR | Post-process on CPU instead |
Error: operand xxx not found during model_transform
Cause: Docker's onnxsim introduces broken Squeeze ops when simplifying PP-LiteSeg.
Solution: Pre-simplify ONNX with local onnxsim (pip install onnxsim) before Docker conversion.
Error: AvgPooling2d: assertion count_include_pad=true during model_deploy
Cause: PP-LiteSeg has AveragePool with count_include_pad=0, but cv181x requires =1.
Solution: Fix all AveragePool nodes in ONNX before conversion (automated by convert_ppliteseg.sh).
Error: Op not support: Mod
Cause: YOLO26 pose or segmentation uses the Mod operator, which is not supported here.
Solution: Use YOLO11 for pose or segmentation instead.
Error: model_transform: command not found
Cause: TPU-MLIR environment was not initialized.
Solution: Run source /workspace/tpu-mlir/envsetup.sh inside the container.
Error: Wrong output order or unusable detection results
Cause: Detection outputs are not provided in the expected alternating order.
Solution: For detection models, ensure outputs are alternating Box, Class, Box, Class, Box, Class.
Error: ION memory exceeds budget
Cause: INT8 output was dequantized to float32 or the chosen variant uses too much memory.
Solution: Add --quant_output when downstream logic only needs relative ordering.
Error: pymlir import fails
Cause: Host Python environment does not match the expected TPU-MLIR runtime.
Solution: Run the workflow inside the Docker container instead of on the host.
Error: /workspace/tpu-mlir is empty inside Docker
Cause: Local TPU-MLIR was not mounted into the container.
Solution: Mount it explicitly with -v /path/to/tpu-mlir:/workspace/tpu-mlir.
Error: CVIMODEL results are completely wrong
Cause: Input preprocessing is mismatched.
Solution: Check that fuse_preprocess is receiving uint8 RGB NHWC input, not float32 NCHW.
Error: Device output is bbox-style instead of segmentation
Cause: cvimodel uses RGB_PLANAR format but sscma-model feeds HWC packed data.
Solution: Re-convert with --customization_format RGB_PACKED. sscma-model always feeds HWC data via rgb888_to_rgb888().
--quant_output for ION-constrained devices - PP-LiteSeg: 59MB→27MB, BiSeNetV2: 63MB→34MBRGB_PACKED for sscma-model compatibility - sscma-model feeds HWC packed data