来源:notes/documents/05_dg板卡X5_BPU推理服务_VLM接入与运维加固.md

dg 板卡 X5 BPU 推理服务:接入 VLM 图像理解与运维加固

日期:2026-09-20 设备:RDK X5(dg,10.71.48.154) 服务:http://10.71.48.154:8080

一句话结论

把板上的 SmolVLM 接进已有的 BPU HTTP 推理服务:图片 POST 进去、一句话描述出来POST /v1/vlm),VLM 由独立的 vlm-service systemd unit 托管,HTTP 侧只做共享内存发图和 token 汇聚。端到端实测 7117 ms / 22 tokens,第二次请求同样成功。为了让这套东西能长期跑,顺手修掉三个只有真部署才会暴露的坑:关停要 90 秒健康检查会打死正在启动的服务rsync --delete 删掉了不在仓库里的脚本;另外排掉一个让内置展示页「只能看概览、其他 tab 点不动」的 Python 转义坑。

1. 这次做了什么

  1. dg 上现场复核官方 SmolVLM 链路,定位「模型加载失败」的真实原因(相对路径按工作目录解析)。
  2. 把 VLM 封装成 HTTP 能力:bpu_service/vlm.py 桥接层 + POST /v1/vlm + GET /v1/vlm/status
  3. 新增 vlm-service unit 和精简版 launch,只起共享内存环境 + hobot_llamacpp,不再连带 ASR/TTS/websocket。
  4. 把「全功能展示页」做成一页 UI(概览 / 检测分类 / VLM / 压测 / 指标),并修掉它的 JS 全量失效问题。
  5. 运维加固:停止流程、健康检查启动宽限、部署脚本排除项,以及 tools/page_check.js 这个页面自检工具。

2. 接入方式:VLM 是独立 unit,HTTP 侧只做桥接

HTTP 请求(jpg/png)
    -> bpu_service/server.py  /v1/vlm
    -> bpu_service/vlm.py     桥接层:OpenCV 打包 NV12 + 汇聚 token
        /hbmem_img (HbmMsg1080P, packed NV12, BEST_EFFORT)
            -> hobot_llamacpp(SmolVLM2-256M)
        /prompt_text (std_msgs/String, RELIABLE) ->
        /tts_text    (std_msgs/String, 逐 token)
    -> JSON {"text": ..., "infer_ms": ..., "tokens": ...}
组件 作用
bpu_service/vlm.py 桥接层:发图、发 prompt、按静默收 token、串行化请求(同一时刻只跑一次推理)
deploy/vlm-service.service VLM 的 ROS 2 栈 unit,和 bpu-service 解耦:ROS 挂了 HTTP 服务照常提供检测/分类
deploy/launch/vlm_stack.launch.py 精简 launch:共享内存环境 + hobot_llamacpp,模型路径写成绝对路径
deploy/run-vlm.sh source TROS 环境后 exec ros2 launch
tests/vlm_test.py 端到端测试:状态查询 + 发图 + 第二次请求复用

为什么不直接复用官方 llama_vlm.launch.py:它连同 ASR/TTS/websocket 一起起,8 核 A55 会被跑满(实测 load avg 约 21),视觉链路被饿死,模型加载从 1 分钟拖到 11 分钟。

3. 打通链路的四个坑

3.1 官方 launch 的模型名是相对路径(本次的主坑)

官方默认参数是相对文件名:

llm_model_name: SmolVLM2-256M-Video-Instruct-Q8_0.gguf
model_file_name: vit_model_int16_v2.bin
model_type(0:internvl, 1:smolvlm): 0

节点按进程工作目录解析这些名字,日志里能看到 pwd_path is /opt/tros/humble/lib/hobot_llamacpp,而那里只有 config/ 和二进制本身,没有任何模型,于是加载直接失败:

[dnn]: Load model: vit_model_int16_v2.bin fail, ret: -6000006

/opt/vlm-models/ 里那个 vit_model_int16_v2.bin -> SigLip_..._X5.bin 软链,就是当初为迁就「相对文件名」留下的痕迹——只在工作目录恰好是 /opt/vlm-models 时有效,换个目录启动立刻失效。

修法是显式传绝对路径,并把 model_type 设成 SmolVLM:

ros2 launch <launch.py> \
  llamacpp_model_type:=1 \
  llamacpp_vit_model_file_name:=/opt/vlm-models/SigLip_int16_SmolVLM2_256M_Instruct_MLP_C1_UP_X5.bin \
  llamacpp_gguf_model_file_name:=/opt/vlm-models/SmolVLM2-256M-Video-Instruct-Q8_0.gguf

这两个默认值已经固化在 deploy/launch/vlm_stack.launch.py 里,所以 systemd 用 /opt/bpu-service 当工作目录启动也不会再踩。

3.2 板载硬件 JPEG 解码器吃不下这张图

用官方链路里的 hobot_codec 做 jpeg → NV12 时会报:

[HobotVdec]: Dequeue input buffer failed, ret = 0xf0000001

原因是 image2.jpgprogressive JPEG,硬件解码器不支持。改成在 HTTP 侧用 OpenCV 解码并打包 NV12 直接发布,既绕开了解码器限制,也把 VPU 留给 VLM。

3.3 QoS 不一致会静默丢消息

  • /hbmem_imgBEST_EFFORT 订阅且 depth=1:只发一帧很可能在发现(discovery)完成前就被丢弃,所以桥接层一次连发 3 帧、间隔 0.6 s。
  • /prompt_text 在节点侧是 RELIABLE:发布端也必须 RELIABLE,否则 prompt 发出去没人收到,表现为「图片到了、模型不回答」。

3.4 /tts_text 是逐 token 流,还有填充词

  • 节点把回答一个 token 一条消息地发到 /tts_text,桥接层按「静默 1.5 s」判定收尾。
  • 官方默认的 cute_words好的,让我看看;没问题,我想想;...)会在推理前先说一句填充词,实测会污染结果,已在 launch 里置空。
  • token 流里有空字符串消息(推理开始的标记),聚合时要过滤。
  • 自己写测试脚本时必须真的 spin 节点:我第一版没收 token,是因为脚本发完图就退出,消息还没到。

4. 实测结果

项目 结果
端到端(bus 图) infer 7117 ms / tokens 22,round-trip 7526 ms
输出示例 An outdoor event with a man in a blue shirt and a woman in a white dress in a public square.
第二次请求 成功(桥接层可复用、串行化)
另一张图 The image shows a modern, multi-story building with a prominent glass facade and a large central window.
BPU 侧回归 tests/smoke_test.pyFAILURES: 0,yolov8 并发 32 请求 0.74 s(43 req/s)
模型冷启动 热缓存约 60 s;首次从 SD 卡读权重可达 11 分钟(当时被 ASR/TTS 抢 CPU)

归档证据文件 source_materials/vlm-http-e2e-20260920.txt 保存的是同一天的另一次运行:infer 6469 ms / tokens 21,第二次请求同样成功(两次运行的差异来自图片内容和板子负载)。

5. 运维加固:三个只有真部署才会暴露的坑

5.1 systemctl restart 要 90 秒才被杀

现象:State 'stop-sigterm' timed out. Killing.Failed with result 'timeout'。原因是 SIGTERM 只唤醒了 rclpy 的线程,主线程还卡在 serve_forever() 里继续服务,systemd 等到 TimeoutStopSec 直接 SIGKILL。

修法两层:

  • server.py 里注册 SIGTERM/SIGINT 处理器,用一个线程调 httpd.shutdown() 让主循环退出,收尾时 vlm.close()
  • 收尾后直接 os._exit(0):rclpy 和 BPU runtime 自己持有 C++ 线程,走解释器正常析构会 terminate called without an active exception → SIGABRT,systemd 记成失败;
  • unit 增加 TimeoutStopSec=20 作为兜底。

结果:重启耗时从 90 s(超时被 SIGKILL)= > 0.5 sResult=successNRestarts=0

5.2 健康检查会打死正在启动的服务

BPU 要加载模型、VLM 要建 ROS 节点,启动期约 15 s 内端口不响应;而健康检查原本只重试 3 次、每次间隔 1 s,于是把「还在启动」判成「已经卡死」,systemctl restart 下去,反而变成启动风暴(实测出现过一秒内连环重启、以及启动中被 SIGTERM 打断导致 RCLError: publisher's context is invalid 退出)。

修法:deploy/bpu-healthcheck.shActiveEnterTimestamp 判断,服务启动后 45 s 内只记一条日志、不探测;45 s 之后再按 /healthz 判定。这个脚本同时兼顾了 vlm-service 的存活检查。

5.3 rsync --delete 删掉了板子上不在仓库里的文件

部署用的是 rsync -a --delete,而 /opt/bpu-service/deploy/bpu-healthcheck.sh 当时只存在于板子上、仓库里没有 → 一次部署就把它删了,unit 还 enabled,下次触发必然失败。

修法:把脚本补进仓库(顺带增强),并加了 deploy/sync.sh 固化同步命令与排除项(.git / node_modules / __pycache__),避免把本地的 tools/node_modules(28 MB)推到板子上。

另外把「VLM 建节点失败」从抛异常崩服务改成返回 VlmUnavailable/v1/vlm 返回 503,检测/分类等其它接口不受影响。

6. 展示页:「只能看概览,其他 tab 都点不了」

症状:打开 http://10.71.48.154:8080/ 只有默认高亮的「概览」可见,其它 5 个 tab 点了没反应。

根因:页面 HTML/JS 是 server.py 里的 INDEX_HTML 字符串,用的是普通三引号字符串;JS 里 ].join('\n')\nPython 当成转义符处理,写进响应时变成真实换行,把一个 JS 字符串字面量劈成两行 → 整段内联脚本语法错误 → 所有事件绑定都没执行,只剩默认带 on 类的那一节可见。

].join('            // 实际发出的内容:字符串在这里断开了
');

修法:把 INDEX_HTML 改成原始字符串(r"""),让源码里的 JS 逐字节等于浏览器收到的 JS,顺手消掉这一整类坑。

防回归:新增 tools/page_check.js。它做两件事:

  1. vm.Script 编译内联 JS(不依赖浏览器,能精确报出行号和出错位置);
  2. 有 jsdom 时真实点击每个 nav 按钮,断言对应 section 被激活。
node tools/page_check.js --http http://10.71.48.154:8080   # PASS:5/5 tab
node tools/page_check.js --file broken-page.html           # FAIL:page.js line 63 around: "].join('"

对修复前抓下来的页面,这个工具会精确复现症状(只有 overview 可见、其余 4 个 tab FAIL),所以它能真的拦住回归。

7. 复现清单

  1. 确认 ZeroTier / SSH 可达,systemctl is-active bpu-service vlm-service 都是 active。
  2. sh deploy/sync.sh 把仓库同步到 dg:/opt/bpu-service(排除 .git / node_modules / __pycache__)。
  3. python3 tests/vlm_test.py --http http://127.0.0.1:8080 跑端到端。
  4. python3 tests/smoke_test.py 跑 BPU 侧回归(确认没被 VLM 拖慢)。
  5. node tools/page_check.js --http http://10.71.48.154:8080 检查展示页(本地跑,需要 node/jsdom)。
  6. systemctl start bpu-healthcheck.service 手动跑一次体检,确认不会误判重启。

8. 相关文件

位置 说明
dg-bpu-service/bpu_service/vlm.py VLM 桥接层
dg-bpu-service/bpu_service/server.py /v1/vlm/v1/vlm/status、内置展示页、SIGTERM 处理
dg-bpu-service/deploy/launch/vlm_stack.launch.py 精简 VLM launch,模型绝对路径
dg-bpu-service/deploy/vlm-service.servicerun-vlm.sh VLM 的 systemd unit
dg-bpu-service/deploy/bpu-healthcheck.sh 健康检查 + 启动宽限
dg-bpu-service/deploy/sync.sh 部署同步脚本
dg-bpu-service/tests/vlm_test.pysmoke_test.py 端到端 / 回归测试
dg-bpu-service/tools/page_check.js 展示页自检(JS 编译 + tab 点击)
notes/sources/projects/dg板卡X5_BPU推理服务/source_materials/bpu-service/ 本仓库源码归档
notes/sources/projects/dg板卡X5_BPU推理服务/source_materials/vlm-http-e2e-20260920.txt 端到端实测输出
notes/sources/projects/dg板卡X5_BPU推理服务/source_materials/showcase-page-check-20260920.txt 展示页自检输出(修复前/后对照)

板子上的对应路径:服务 /opt/bpu-service,模型 /opt/vlm-models/,VLM 栈 unit vlm-service

9. 与既有笔记的关系

  • notes/documents/01_dg板卡X5_BPU推理服务_原理_问答_代码.md 讲的是 BPU 推理服务本身(调用链、解码器、性能);
  • notes/documents/dg_RDK_X5_官方_SmolVLM_多模态部署与验证.md 讲的是官方 VLM 部署方案(依赖、模型、历史验证);
  • 本篇是把两者接起来的那一层:服务化接入 + 运维加固 + 展示页排障,并修正了前一篇里「dg 已关机、无法复核」的过期状态。