来源:notes/documents/02_dg板卡X5_BPU推理服务_可视化HTML.md

dg 板卡 X5 BPU 推理服务:可视化 HTML 报告

日期:2026-09-19

这是一篇原生 Markdown 内嵌 HTML 的笔记:正文本身是一段自包含的 HTML(纯 <style> + <div>,无 JavaScript、无外部资源),嵌在 Markdown 里由博客渲染。它用图表的方式把整套 BPU 推理服务讲了什么、原理是什么、实测到什么,一次性呈现出来。

说明:飞书文档导入会丢弃 <div> / <style> 这类原生 HTML 标签(已实测确认),所以这篇只在博客侧渲染;文字版原理与代码见 notes/documents/01_dg板卡X5_BPU推理服务_原理_问答_代码.md

dg 板卡 X5 · BPU 推理服务

一台没有摄像头、麦克风、屏幕的地瓜机器人 RDK X5,被我改造成了一台纯算力的 AI 推理服务器:图片从网络进来,检测结果从网络出去。

RDK X5 / X5M BPU 1 GHz 34 个官方模型 HTTP 推理服务 零第三方依赖
9.8 msyolov8 纯 BPU 单帧
52 ms板上端到端单请求
37.9 req/s板上 4 并发吞吐
1731全部 Python 代码

1我做了什么

把 BPU 变成网络服务

纯标准库 ThreadingHTTPServer,零额外依赖。图片 POST 进来,检测框 / 分类结果 JSON 回去,浏览器还能直接拖图上传。

直接吃板载模型

板子 /opt/hobot/model/x5/basic 下 34 个官方 .bin 模型开箱即用,不需要做任何模型转换。

三种后处理解码器

YOLOv5 锚框解码、YOLOv8 / v10 / v11 / v12 的 DFL 解码、ImageNet 分类 top-k;不认识的模型退回原始张量统计。

为摄像头预留了接口

除了编码图片,还做了 /v1/infer/nv12,直接收原始 NV12 帧。以后接上摄像头或 RTSP 解码器,这层不用改。

2从我的 Python 到 BPU 硬件:完整调用链

我的 Python 代码
全部入口只有一行 from hbm_runtime import HB_HBMRuntime,没有任何路径 hack。
HB_HBMRuntime.so
pybind11 编出来的 aarch64 原生扩展,把 C++ 类暴露成 Python 类;只 NEEDED libdnn.so,没有 RPATH。
libdnn.so
地平线 DNN 运行时,提供 hbDNN* C API;往下挂 libcnn_intf / libhbmem(BPU 共享内存)/ libhbrt_bayes(BPU Runtime 3.15.55)。
Linux 内核模块
bpu_frameworkbpu_coresbpu_hw_io_x5,通过字符设备 /dev/bpu_core0 (10,84)、/dev/bpu (10,85)、/dev/ion (10,127) 下发任务。
BPU 硬件
地平线自研神经网络加速器,实测运行在 996 MHz,跑满后单帧 yolov8 约 9.8 ms。

3一张 JPEG 的完整数据流

收到 JPEG 字节HTTP body · 137 KB
cv2.imdecode 解码成 BGR810 × 1080 × 3
letterbox 等比缩放 + 填 114640 × 640 × 3
BGR → YUV_I420 → 交错 UVpacked NV12 · 614400 B
包成运行时要求的嵌套字典模型名 → 输入名 → buffer
BPU 前向推理HB_HBMRuntime.run()
按 output_quants 反量化int32 / int8 → float32
head 解码 + sigmoid + NMSDFL / 锚框
逆映射回原图 + JSON 返回像素坐标 bbox

4五个契约点:为什么这样写才能精准命中 BPU

① 模型形态

必须是 OpenExplorer 量化编译后的 .bin,BPU 不认识 .pt / ONNX。

② 张量名

input/output 名字必须运行时从 runtime 实例读,不能硬编码。

③ 输入排布

input_shape[1,3,640,640],实际要喂 614400 字节的 packed NV12。

④ 量化还原

定点输出必须按逐通道 scale / zero_point 反量化,直接转 float 是错的。

⑤ 并发模型

同一 .bin 重复加载会被 runtime 去重,只能单实例 + 信号量 + 多线程 run。

5实测性能

yolov8 纯 BPU(1 线程)100 FPS
yolov5s_v6 纯 BPU49.8 FPS
yolo11m 纯 BPU25.0 FPS
板上端到端 4 并发37.9 req/s
跨 ZeroTier 4 并发14.2 req/s

BPU 是单核的:线程数 1 → 4 时吞吐几乎不变(100 → 99.4 FPS),但单帧延迟从 9.8 ms 涨到 30 ms,多出来的线程全在排队。跨 ZeroTier 的差距几乎全部来自网络传输(137 KB 走公网虚拟局域网),不是算力问题。

6两张图的实测结果

bus.jpg · 810×1080 · 官方 COCO 测试图
模型主要结果耗时
yolo11mbus 0.93 · person 0.90/0.90/0.89/0.7843.2 ms
yolov12nbus 0.86 · person 0.82/0.78/0.7525.4 ms
yolov8bus 0.82 · person 0.74/0.69/0.6710.0 ms
yolov5s_v6person 0.78/0.77/0.72 · bus 0.7420.2 ms
mobilenetv2分类 minibus 0.482.6 ms
扣篮照 · 1279×1920 竖图
设置主要结果耗时
yolo11m + stretch扣篮者 0.86 · 篮下球员 0.77 · 场边 4 人 0.68/0.56/0.54/0.4538.5 ms
yolo11m + letterbox最高只有 0.65(竖图被压小)38.1 ms
mobilenetv2分类 scoreboard 0.542.4 ms
googlenetscoreboard 0.12 · ballplayer 0.083.1 ms

关键教训:竖图必须用 resize=stretch,letterbox 会把它缩成 640×426、上下填黑边,目标置信度从 0.86 掉到 0.65。

7你问过的问题,一次说清

X5 的 BPU 可以拿来做什么?

四类事:目标检测(YOLO 全家族、SSD、FCOS、CenterNet)、图像分类(MobileNet / ResNet / GoogLeNet / EfficientNet)、语义分割(DeepLabv3+、STDC)、多模态大模型(CLIP 文本编码、LLaMA.cpp)。核心优势是比 CPU 高一个量级的算力、只有几瓦功耗。

没有摄像头、麦克风、扩音器,还能做什么?

实测确认 /dev/video* 不存在、采集到的音频是饱和噪声、也没有显示设备,所以"感知输入"这条腿断了。但可以当纯算力服务器:网络推理服务、离线批量处理、边缘计算节点,以及 GPIO/I2C/SPI/UART 的传感器控制类项目。

代码是怎么精准调用到 BPU 的?

不是猜路径,是分层调用 + 五个契约点:Python 只 import 官方 hbm_runtime,它用 pybind11 包住 libdnn.so,再往下是 hbrt、内核模块、/dev/bpu_core0。真正难的不是调用,是让输入排布、张量名、量化参数都符合运行时约定。

是地平线封装了官方 Python 库吗?

是。hobot-dnn 提供底层 libdnn / libhbrthobot-spdev 提供 Python wheel(hbm_runtime-3.0.9-py3-none-any.whl),安装时由 postinst 自动 pip3 install 到 dist-packages。不是我写的,也不是第三方。

代码怎么找到官方封装的库的?

没有任何路径 hack。Python 层靠 dist-packages 本来就在 sys.path 里;原生库层/etc/ld.so.conf.d/ 下地平线装的配置 + ldconfig 缓存,而 HB_HBMRuntime.so 自己不写 RPATH。我代码里只有一行懒加载 import。

代码在哪里?量大吗?

开发副本在 dg-bpu-service/,部署在 dg:/opt/bpu-service/,systemd 托管。服务本体 1430 行,加测试与工具共 1731 行;没有 Web 框架、没有数据库、没有前端构建链。

8踩过的坑

01input_shape 写 [1,3,640,640],实际要喂 packed NV12。
02同一 .bin 重复加载被 runtime 去重,拿不到第二个句柄。
03YOLOv5 head 是 255 = 3×(5+80),类别数要除以 3。
04板载分类模型输出已过 softmax,再算一次置信度塌到 0.002。
05竖图 letterbox 掉点,改 stretch 后 0.65 → 0.86。
06全量 sigmoid 后处理 35 ms,用单调性预筛降到 5 ms。
07YOLOv8 box 分支是 int32 + 逐通道 scale,不反量化框全乱。
08板端 hbrt 3.15.55 与部分模型 build 版本不一致,只告警不影响结果。

结论

X5 的 BPU 在没有摄像头、麦克风、屏幕的情况下,依然可以作为一个高性价比的边缘 AI 算力节点:图片进、结果出。整套服务 1430 行纯标准库 Python,把 CPU(预处理 + 后处理)和 BPU(前向推理)的分工划清楚,就能稳定跑出 yolov8 单帧 9.8 ms、端到端 52 ms 的成绩。真正的门槛不在"调用 BPU",而在理解它的五个数据契约。

附:原生 Markdown 嵌入 HTML 的实测结论

这次实验的目标是搞清楚「怎么把 HTML 放进 Markdown 里,并真的让别人看到」。

做法:把一整段自包含 HTML(<style> + <div>,无 JavaScript、无外部图片/字体/CDN)直接写在 Markdown 正文里。块级 HTML 标签必须顶格起行,中间不要用 Markdown 语法包裹它。

结论一:博客侧可以渲染(可用)。 博客是 Jekyll,_config.ymlmarkdown: kramdown + kramdown.input: GFM。kramdown 在 GFM 模式下会把块级原生 HTML 原样透传,不做转义。导入脚本 import_personal_assistant_notes.py 也只是把笔记原文照搬进 _posts/,没有任何 HTML 转义逻辑,所以内嵌 HTML 能一路走到最终页面。

结论二:飞书文档侧不能渲染(会丢)。 lark-cli docs +create --doc-format markdown 走的是飞书的 Markdown → 文档块转换,实测结果是:

写法 飞书导入后
<div style="...">...</div> 标签被丢弃,只剩里面的纯文本
<b> / <p> 被当作飞书 XML 富文本节点处理
<style>.x{...}</style> 样式被当成普通文字原样保留(很难看)
<table><tr><td> 被识别并转成 Markdown 表格

所以同一篇笔记里混大量原生 HTML,会让飞书那边的文档变得很乱。这次的做法是拆成两篇:文字版走飞书 + 博客,HTML 可视化版只在博客。

结论三:Jekyll 的 Liquid 是隐藏地雷。 Jekyll 在 Markdown 渲染之前会先跑一遍 Liquid 模板引擎,所以正文里任何成对的双花括号(Liquid 的输出语法)或「花括号 + 百分号」(Liquid 的标签语法)都会在渲染前被当成模板求值,轻则内容消失、重则整站构建失败。写代码示例(尤其是嵌套字典字面量)或 CSS 时特别容易踩到。规避方式有两种:一是内容里干脆不出现这两个序列;二是用 Liquid 自带的 raw / endraw 标签把整段包起来,标签只对博客侧生效,飞书侧会看到标签本身。

结论四:必须无 JavaScript。 kramdown 对 <script> 的处理不稳定,而且博客正文里塞脚本既没必要也不安全。这次的可视化全部用纯 CSS 实现:分层图用带色边框的卡片 + <div>▼</div>,条形图用百分比宽度的 <i>,响应式用 @media。这样任何 Markdown 渲染器都不会把它拆坏。

可复用的模板骨架

<style>
.viz-wrap{ /* 所有样式必须自带命名空间前缀,避免和博客主题冲突 */ }
</style>

<div class="viz-wrap" markdown="0">
  <h3>标题</h3>
  <p>正文……</p>
</div>

三个要点:样式类名加统一前缀(这次用 bpuv-)、markdown="0" 提示渲染器不要解析内部内容、整段 HTML 里不要出现成对的双花括号或 Liquid 标签语法。