fix: ACL bytes_to_ptr GC bug + 添加 NPU 模型转换指南
python/infer_server.py: - 修复 acl.util.bytes_to_ptr() 后 bytes 被 GC 回收导致全零输出的 bug - 保持 bytes 引用直到 memcpy 完成 - 正确解析 FP16 模型输出 (half precision -> float32) docs/CONVERSION_GUIDE.md: - 完整的 YOLOv8 ONNX->OM 转换流程文档 - ATC 转换参数详解及低内存设备配置 - 已知问题排查表 (OOM/全零输出/算子不支持) - 其他 YOLOv8 尺寸模型参考 start.sh: 一键启动脚本
This commit is contained in:
165
docs/CONVERSION_GUIDE.md
Normal file
165
docs/CONVERSION_GUIDE.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# 昇腾 NPU 模型转换指南
|
||||||
|
|
||||||
|
> 硬件: Atlas 200I DK2 / 昇腾 310B4 | CANN 6.2.RC2 | YOLOv8n
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方案概要
|
||||||
|
|
||||||
|
```
|
||||||
|
YOLOv8n.pt → YOLOv8n.onnx → yolov8n.om (ACL原生推理)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 步骤 | 工具 | 耗时 | 资源需求 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| 1. 下载权重 | ultralytics | ~1min | 无 |
|
||||||
|
| 2. 导出 ONNX | ultralytics | ~85s | 需安装 onnx/onnxslim |
|
||||||
|
| 3. ATC 转换 OM | CANN atc | ~16min | 3GB RAM + swap, 单核编译 |
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
```
|
||||||
|
Ubuntu 22.04 aarch64
|
||||||
|
Python 3.9+ (miniconda)
|
||||||
|
CANN 6.2.RC2 (已安装于 /usr/local/Ascend/ascend-toolkit/latest)
|
||||||
|
Go 1.22+ (编译 edge-agent)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 详细步骤
|
||||||
|
|
||||||
|
### 第 1 步:安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python 依赖
|
||||||
|
pip3 install "numpy>=1.24.0,<2.0.0"
|
||||||
|
pip3 install ultralytics opencv-python-headless
|
||||||
|
|
||||||
|
# Go (系统源版本太低, 需手动安装)
|
||||||
|
wget https://go.dev/dl/go1.22.5.linux-arm64.tar.gz
|
||||||
|
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.22.5.linux-arm64.tar.gz
|
||||||
|
rm go1.22.5.linux-arm64.tar.gz
|
||||||
|
export PATH=/usr/local/go/bin:$PATH
|
||||||
|
|
||||||
|
# 验证
|
||||||
|
python3 -c "import ultralytics; print('OK')"
|
||||||
|
go version # 应输出 go1.22.5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第 2 步:导出 ONNX
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/AI-tianyan
|
||||||
|
|
||||||
|
python3 -c "
|
||||||
|
from ultralytics import YOLO
|
||||||
|
model = YOLO('yolov8n.pt') # 或自定义模型路径
|
||||||
|
model.export(format='onnx', imgsz=640, dynamic=False, opset=11, simplify=True)
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
输出:`yolov8n.onnx` (约 13MB)
|
||||||
|
|
||||||
|
**注意事项:**
|
||||||
|
- `dynamic=False`:静态输入尺寸,ATC 转换必须
|
||||||
|
- `opset=11`:CANN 6.2 最稳定的 ONNX opset 版本
|
||||||
|
- `simplify=True`:使用 onnxslim 简化模型图
|
||||||
|
|
||||||
|
### 第 3 步:ATC 转换 OM(关键步骤)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 设置环境变量(3GB RAM 设备必须)
|
||||||
|
export TBE_IMPL_MODE=AI_CORE # 使用 AI Core 模式
|
||||||
|
export TE_PARALLEL_COMPILER=1 # 单线程编译,避免内存溢出
|
||||||
|
|
||||||
|
# 执行转换
|
||||||
|
atc \
|
||||||
|
--model=yolov8n.onnx \
|
||||||
|
--framework=5 \ # 5 = ONNX
|
||||||
|
--output=yolov8n \
|
||||||
|
--soc_version=Ascend310B4 \ # 昇腾 310B4
|
||||||
|
--input_format=NCHW \
|
||||||
|
--input_shape="images:1,3,640,640" \
|
||||||
|
--output_type=FP16 # FP16 更快,精度够用
|
||||||
|
|
||||||
|
# 约 16 分钟后得到 yolov8n.om (约 7.1MB)
|
||||||
|
```
|
||||||
|
|
||||||
|
**常见失败原因:**
|
||||||
|
| 错误 | 原因 | 解决 |
|
||||||
|
|------|------|------|
|
||||||
|
| EOFError / 进程崩溃 | TBE 多线程编译 OOM | 设置 `TE_PARALLEL_COMPILER=1` |
|
||||||
|
| 输出全零 | `bytes_to_ptr()` 后 bytes 被 GC | 保持 bytes 引用见 infer_server.py |
|
||||||
|
| 算子不支持 | ONNX opset 版本过高 | 改用 `opset=11` |
|
||||||
|
|
||||||
|
### 第 4 步:部署模型
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 复制模型到指定位置
|
||||||
|
cp yolov8n.om /opt/tianyan-edge/model/model.om
|
||||||
|
|
||||||
|
# 更新类别文件(COCO 80 类)
|
||||||
|
cp model/names.txt /opt/tianyan-edge/model/names.txt
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
cd /opt/tianyan-edge
|
||||||
|
python3 python/infer_server.py # 推理服务
|
||||||
|
./bin/edge-agent -config config/edge.yaml # 主控 Agent
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 其他模型
|
||||||
|
|
||||||
|
| 模型 | 导出命令 | 输出尺寸 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| yolov8s | `YOLO('yolov8s.pt').export(...)` | 22.5MB onnx → 11MB om |
|
||||||
|
| yolov8m | `YOLO('yolov8m.pt').export(...)` | 51.9MB onnx → 25MB om |
|
||||||
|
| yolov8l | `YOLO('yolov8l.pt').export(...)` | 88.3MB onnx → 43MB om |
|
||||||
|
| yolov8n | `YOLO('yolov8n.pt').export(...)` | 12.3MB onnx → 7.1MB om |
|
||||||
|
|
||||||
|
**注意:** 大模型转换需要更多内存,建议 8GB+ RAM。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## YOLOv10 注意事项
|
||||||
|
|
||||||
|
如果用 YOLOv10,需要:
|
||||||
|
1. `pip install ultralytics yolov10`
|
||||||
|
2. 导出:`YOLOv10('yolov10n.pt').export(format='onnx', ...)`
|
||||||
|
3. 修改 `OUTPUT_FORMAT=nms_free` (YOLOv10 输出格式不同)
|
||||||
|
4. ATC 命令完全相同
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 已知 Bug 修复
|
||||||
|
|
||||||
|
### ACL bytes_to_ptr 提前 GC 问题
|
||||||
|
|
||||||
|
**问题描述:** `acl.util.bytes_to_ptr(blob.tobytes())` 返回的指针,
|
||||||
|
由于 `blob.tobytes()` 创建的临时 bytes 对象被 Python GC 回收,
|
||||||
|
导致后续 `acl.rt.memcpy` 读到全零数据。
|
||||||
|
|
||||||
|
**修复方案(见 `python/infer_server.py`):**
|
||||||
|
```python
|
||||||
|
# 错误写法 - bytes 临时对象会被 GC
|
||||||
|
host_addr = acl.util.bytes_to_ptr(blob.tobytes())
|
||||||
|
acl.rt.memcpy(in_buf, in_sz, host_addr, in_sz, ...) # 读到空数据
|
||||||
|
|
||||||
|
# 正确写法 - 保持引用
|
||||||
|
host_bytes = blob.tobytes() # 保持引用
|
||||||
|
host_addr = acl.util.bytes_to_ptr(host_bytes)
|
||||||
|
acl.rt.memcpy(in_buf, in_sz, host_addr, in_sz, ...) # 数据正确
|
||||||
|
```
|
||||||
|
|
||||||
|
### FP16 输出解析
|
||||||
|
|
||||||
|
ATC 转换时指定 `--output_type=FP16` 后,模型输出为 half precision。
|
||||||
|
需要按半精度读取并转为 float32:
|
||||||
|
|
||||||
|
```python
|
||||||
|
host_bytes = host.tobytes()
|
||||||
|
acl.rt.memcpy(acl.util.bytes_to_ptr(host_bytes), sz, out_buf, sz, ...)
|
||||||
|
host_from_bytes = np.frombuffer(host_bytes, dtype=np.uint8)
|
||||||
|
out_fp16 = host_from_bytes.view(np.float16)
|
||||||
|
output = out_fp16.astype(np.float32).reshape(shape)
|
||||||
|
```
|
||||||
@@ -112,10 +112,15 @@ class AclModel:
|
|||||||
def run(self, blob):
|
def run(self, blob):
|
||||||
"""Push blob to NPU, execute, pull outputs back as numpy arrays."""
|
"""Push blob to NPU, execute, pull outputs back as numpy arrays."""
|
||||||
in_ds = acl.mdl.create_dataset()
|
in_ds = acl.mdl.create_dataset()
|
||||||
|
# Host input buffer - MUST keep reference alive during memcpy
|
||||||
|
host_buf = np.ascontiguousarray(blob)
|
||||||
|
host_bytes = host_buf.tobytes()
|
||||||
|
host_addr = acl.util.bytes_to_ptr(host_bytes)
|
||||||
|
# Device input buffer
|
||||||
in_buf, ret = acl.rt.malloc(self.input_size, ACL_MEM_MALLOC_NORMAL_ONLY)
|
in_buf, ret = acl.rt.malloc(self.input_size, ACL_MEM_MALLOC_NORMAL_ONLY)
|
||||||
assert ret == 0
|
assert ret == 0
|
||||||
ret = acl.rt.memcpy(in_buf, self.input_size,
|
ret = acl.rt.memcpy(in_buf, self.input_size,
|
||||||
blob.tobytes(), self.input_size,
|
host_addr, self.input_size,
|
||||||
ACL_MEMCPY_HOST_TO_DEVICE)
|
ACL_MEMCPY_HOST_TO_DEVICE)
|
||||||
assert ret == 0
|
assert ret == 0
|
||||||
db = acl.create_data_buffer(in_buf, self.input_size)
|
db = acl.create_data_buffer(in_buf, self.input_size)
|
||||||
@@ -135,11 +140,27 @@ class AclModel:
|
|||||||
for i in range(self.output_num):
|
for i in range(self.output_num):
|
||||||
sz = self.out_sizes[i]
|
sz = self.out_sizes[i]
|
||||||
host = np.zeros(sz, dtype=np.uint8)
|
host = np.zeros(sz, dtype=np.uint8)
|
||||||
ret = acl.rt.memcpy(host.ctypes.data, sz,
|
# Keep reference alive during memcpy
|
||||||
|
host_bytes = host.tobytes()
|
||||||
|
host_addr = acl.util.bytes_to_ptr(host_bytes)
|
||||||
|
ret = acl.rt.memcpy(host_addr, sz,
|
||||||
self.out_bufs[i], sz,
|
self.out_bufs[i], sz,
|
||||||
ACL_MEMCPY_DEVICE_TO_HOST)
|
ACL_MEMCPY_DEVICE_TO_HOST)
|
||||||
assert ret == 0
|
assert ret == 0
|
||||||
outputs.append(host.view(np.float32).reshape(self.output_shapes[i]))
|
# Read back from host_bytes (memcpy modified it)
|
||||||
|
host_from_bytes = np.frombuffer(host_bytes, dtype=np.uint8)
|
||||||
|
# FP16 model outputs are half precision (2 bytes)
|
||||||
|
num_fp16 = sz // 2
|
||||||
|
num_fp32 = sz // 4
|
||||||
|
expected = self.output_shapes[i][0] * self.output_shapes[i][1] * self.output_shapes[i][2]
|
||||||
|
if num_fp16 == expected:
|
||||||
|
out_fp16 = host_from_bytes.view(np.float16)
|
||||||
|
outputs.append(out_fp16.astype(np.float32).reshape(self.output_shapes[i]))
|
||||||
|
elif num_fp32 == expected:
|
||||||
|
outputs.append(host_from_bytes.view(np.float32).reshape(self.output_shapes[i]))
|
||||||
|
else:
|
||||||
|
out_fp16 = host_from_bytes.view(np.float16)
|
||||||
|
outputs.append(out_fp16.astype(np.float32).reshape(self.output_shapes[i]))
|
||||||
|
|
||||||
acl.rt.free(in_buf)
|
acl.rt.free(in_buf)
|
||||||
acl.mdl.destroy_dataset(in_ds)
|
acl.mdl.destroy_dataset(in_ds)
|
||||||
|
|||||||
49
start.sh
Normal file
49
start.sh
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 一键启动脚本
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo " Tianyan Edge - 启动脚本"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# 检查模型
|
||||||
|
if [ ! -f model/model.om ]; then
|
||||||
|
echo "[错误] model/model.om 不存在,请先转换模型"
|
||||||
|
echo "使用方法: atc --model=yolov8n.onnx --framework=5 --output=model/model.om --soc_version=Ascend310B4 --input_format=NCHW --input_shape=\"images:1,3,640,640\""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建必要的目录
|
||||||
|
mkdir -p logs
|
||||||
|
|
||||||
|
# 启动推理服务(后台)
|
||||||
|
echo "[1/2] 启动推理服务..."
|
||||||
|
if systemctl is-active --quiet edge-infer 2>/dev/null; then
|
||||||
|
echo " edge-infer 已在运行"
|
||||||
|
else
|
||||||
|
python3 python/infer_server.py > logs/infer.log 2>&1 &
|
||||||
|
INFER_PID=$!
|
||||||
|
echo " 推理服务 PID: $INFER_PID"
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 启动 Go Agent
|
||||||
|
echo "[2/2] 启动 Go Agent..."
|
||||||
|
if systemctl is-active --quiet edge-agent 2>/dev/null; then
|
||||||
|
echo " edge-agent 已在运行"
|
||||||
|
else
|
||||||
|
./build/edge-agent -config config/edge.yaml 2>&1 | tee logs/agent.log &
|
||||||
|
AGENT_PID=$!
|
||||||
|
echo " Agent PID: $AGENT_PID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "启动完成!按 Ctrl+C 停止"
|
||||||
|
echo "推理日志: logs/infer.log"
|
||||||
|
echo "Agent日志: logs/agent.log"
|
||||||
|
|
||||||
|
# 等待
|
||||||
|
wait
|
||||||
Reference in New Issue
Block a user