来源:notes/documents/09_dg板卡X5_BPU语音转写_直调SDK实现与实测.md

dg 板卡 X5:走 BPU 做语音转文字(已跑通,但为什么不划算)

一句话结论

在 dg(RDK X5)上可以绕过 ALSA 麦克风阵列,直接调用官方闭源 SDK 的 C 接口,把音频文件喂进 BPU 做转写——我把它跑通了。但实测下来没有实用优势:管道本身只能跑到约 2.6 倍实时(CPU 方案 5.5 倍),原因是 BPU 只承担了 encoder 一段,整条链路的耗时在 CPU 侧的串行前端处理和解码上。批量转写继续用 CPU;这条路的价值在于「实时拾音 + ASR 一体化」或「把 CPU 让出来」。

背景:之前的判断和这次的突破

之前的结论是「BPU 语音方案喂不了文件」,依据是官方 ROS 节点把输入硬编码成 8 麦阵列 ALSA 设备(micphone_chn=8),裸板报 Channels count (8) not available,而 SDK 是闭源库。

这次的突破点:官方 ROS 节点 D-Robotics/hobot_audio 是开源的,仓库里带着 SDK 头文件 include/x5/horizon_speech_sdk/include/horizonspeechsdk/hrsc_sdk.h。头文件显示 SDK 提供的是「喂 PCM 帧」的接口,跟 ALSA 没有绑定——那个 8 麦限制只是 ROS 节点的实现方式,不是 SDK 的限制。

实现路径

  1. 从开源仓库拿到 hrsc_sdk.h,确认关键接口:HrscInit / HrscStart / HrscProcess / HrscRelease,回调 HrscAsrCallback 直接给最终文本
  2. 自己构造 HrscAudioBuffer,按帧喂 PCM,完全绕过 ALSA
  3. asr_output_mode = 2(0=不输出,1=唤醒后输出,2=全部输出)→ 不需要唤醒词,连续转写
  4. 用 g++ 链接板上的 /opt/tros/humble/lib/libhrsc.soLD_LIBRARY_PATH 指过去即可

模型确实跑在 BPU 上,日志证据:[BPU_PLAT] BPU Platform Version(1.3.6) soc info(x5)[DNN] Runtime version = 1.24.5,加载 encoder.bin(175MB);ldd libhrsc.so 显示依赖 libdnn.so / libcnn_intf.so.1 / libhbmem.so.1

踩过的三个坑

  1. BPU 内存整块独占:ION 的 cma_reserved 堆只有 320MB,bpu_service 占 312MB,175MB 的 encoder 必然分配失败(hbrtErrorBPUMemAllocFail)。跑之前必须 systemctl stop bpu-service,跑完再启回来 → ASR 与视觉推理服务互斥
  2. 声道填充方式决定成败:6 个声道灌同样内容时,AEC/波束成形会把信号当回声消掉,只出碎片(4 段);改成只喂第 0 通道、其余静音后,输出变成与 CPU 基线基本一致的 11 段。2 通道和 8 通道配置直接无输出
  3. 必须接近实时喂数(或喂完后等管道排空):1x/2x 喂数出 11 段,4x 掉到 9 段,8x 剩 6 段,全速只出 1 段——但这不是数据丢失,HrscProcess 只是入队(30 秒音频仅耗时 0.03 秒),等足够久结果仍会全部返回

实测数据

管道真实耗时(30 秒音频,仅 ch0 填充,全速喂 + 排空)

指标 数值
HrscProcess 调用总耗时 0.03 秒(≈1000x,喂数不是瓶颈)
首段结果返回 +1.2 秒
末段结果返回 +11.6 秒 → 管道本身约 2.6x 实时
端到端(含约 3.5 秒模型加载) 16.8 秒 → 1.79x 实时
CPU 时间 user+sys 25.8 秒 / wall 16.8 秒 → 平均 1.54 核在忙

与 CPU 方案对照

  BPU(HRSC 直调) CPU(sherpa-onnx paraformer-large int8,4 线程)
吞吐 约 2.6x 实时(含加载 1.8x) 5.5x 实时(RTF 0.182)
并行度 1.5 核 + BPU 4 核吃满
30 秒音频 16.8 秒 decode 10.9 秒(+加载 8.8 秒)
全文 2h34m54s 约 60 分钟(外推,且要停 bpu_service) 约 29 分钟
CPU 工作量 约 0.74 CPU-秒/音频秒 约 0.73 CPU-秒/音频秒

质量对照(同一段 30 秒)

  • CPU:电脑时代 p c 时代差不多是七五年七六年成立了两家公司一个叫苹果一个叫微软…还有一家公司叫 intel…开启了全人类的 p c 时代…在手机的计算平台 arm 高通…google 的安卓
  • BPU:能电脑时代pc时代 / 差不多多是七五年七六年 / 成立了两家公司 / 一个叫苹果 / 一个叫微软一 / 还有一家公司叫因子 / 开启了全人类的时代 / …在手机的计算屏 / um高通 / 包括google的安卓
  • 差异:intel因子计算平台计算屏,少量丢字。整体可用,但弱于 CPU 方案

为什么 BPU 没有优势(定量)

  • BPU 只接走 encoder 一段:AEC、AGC、波束成形、DOA、降噪、VAD、CTC 解码、FST 语言模型重打分、ITN 全在 CPU 侧(libhrsc.so 里的 AecProcessBlockCrgruVad* 等)
  • 总 CPU 工作量几乎没变:BPU 管道 0.74 CPU-秒/音频秒 vs 纯 CPU 方案 0.73 —— offload 后 CPU 的活没减少,并行度却从 4 核降到 1.5 核,所以墙钟更慢
  • 流式小 chunk 让 BPU 发挥不出来model.flag 固定 chunk_size,batch=1、序列短,BPU 擅长的「大张量并行吞吐」用不上,大部分时间空闲
  • CPU 方案本身太高效:离线整段推理 + 4 线程 + ONNX Runtime 的 NEON 优化,还省掉了整套交互式前端
  • 结论:加速了链条上一小段,却牺牲了 4 核并行,净效果为负

什么情况下 BPU 才值得用

  1. 把 encoder+CTC 从交互式管道里单独抽出来(需要逆向闭源 SDK,或自己用量化工具链编译模型)
  2. 做多路并发批处理(当前 SDK 是单流)
  3. 目标不是吞吐而是释放 CPU / 低功耗——此时 BPU 有意义,CPU 可以去做别的事

复现步骤

# 1. 取头文件(从开源仓库)
curl -sL https://raw.githubusercontent.com/D-Robotics/hobot_audio/main/include/x5/horizon_speech_sdk/include/horizonspeechsdk/hrsc_sdk.h -o /tmp/hrsc_sdk.h

# 2. 编译(源码见附录 A)
g++ -O2 -o /tmp/asr/hrsc_asr /tmp/asr/hrsc_asr.cpp -I/tmp \
    -L/opt/tros/humble/lib -lhrsc -Wl,-rpath,/opt/tros/humble/lib -lpthread

# 3. 腾出 BPU 内存
systemctl stop bpu-service

# 4. 运行:<wav> <out.txt> <chn> <ref_ch> <asr_mode> <pace> <fill> <wait_s>
export LD_LIBRARY_PATH=/opt/tros/humble/lib
./hrsc_asr test30.wav out.txt 6 4 2 0 1 3

# 5. 恢复服务
systemctl start bpu-service

参数含义:chn=喂给 SDK 的通道数(6);ref_ch=参考通道(4);asr_mode=2 表示连续输出;pace=喂数限速(1.0 实时,0 全速);fill=1 表示只填第 0 通道、其余静音(关键);wait_s=喂完后等待异步结果的时间。

附录 A:完整代码(hrsc_asr.cpp)

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <fstream>
#include <iostream>
#include <unistd.h>
#include <ctime>
#include <sys/resource.h>
#include "hrsc_sdk.h"

static std::vector<std::string> g_asr;
static struct timespec g_t0;
static double g_last_ms = 0;
static int g_n = 0;
static double now_ms() { struct timespec n; clock_gettime(CLOCK_MONOTONIC,&n); return (n.tv_sec-g_t0.tv_sec)*1000.0+(n.tv_nsec-g_t0.tv_nsec)/1e6; }

static void AsrCallback(const void *, const char *asr) {
  if (asr && *asr) { double t=now_ms(); g_last_ms=t; g_n++; printf("\n[ASR +%.0fms] %s\n", t, asr); fflush(stdout); g_asr.push_back(asr); }
}
static void AsrDataCallback(const void *, const HrscCallbackData *d) {
  if (d && d->result && *d->result) std::cout << "\n[DATA] " << d->result << std::endl;
}
static void EventCallback(const void *, HrscEventType e) { std::cout << "\n[EVENT] " << (int)e << std::endl; }
static void CmdCallback(const void *, const char *c) { if (c && *c) std::cout << "\n[CMD] " << c << std::endl; }
static void DoaCallback(const void *, int) {}
static void WkpCallback(const void *, const HrscCallbackData *, const int) {}
static void VoipCallback(const void *, const HrscCallbackData *) {}
static void ProcCallback(const void *, const HrscCallbackData *) {}
static void AuthCallback(const void *, const int code) { std::cout << "\n[AUTH] " << code << std::endl; }

static std::string ReadFile(const char *p) {
  std::ifstream s(p, std::ios::in | std::ios::binary);
  if (!s.is_open()) return "";
  std::string d((std::istreambuf_iterator<char>(s)), std::istreambuf_iterator<char>());
  return d;
}

int main(int argc, char **argv) {
  if (argc < 3) { printf("usage: %s in16k_mono.wav out.txt [channels] [ref_ch] [asr_mode]\n", argv[0]); return 2; }
  const char *wav_path = argv[1];
  const char *out_path = argv[2];
  int chn = argc > 3 ? atoi(argv[3]) : 6;
  int ref_ch = argc > 4 ? atoi(argv[4]) : 6;
  int asr_mode = argc > 5 ? atoi(argv[5]) : 2;
  double pace = argc > 6 ? atof(argv[6]) : 0.0;  // 1.0 = realtime, 0 = as fast as possible
  int fill = argc > 7 ? atoi(argv[7]) : 0;      // 0=all ch audio, 1=only ch0, 2=all but ref
  printf("fill=%d\n", fill);
  int wait_s = argc > 8 ? atoi(argv[8]) : 3;

  FILE *f = fopen(wav_path, "rb");
  if (!f) { printf("cannot open %s\n", wav_path); return 2; }
  fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 44, SEEK_SET);
  if (sz <= 44) { printf("bad wav\n"); return 2; }
  std::vector<short> pcm((sz - 44) / 2);
  size_t got = fread(pcm.data(), 2, pcm.size(), f);
  pcm.resize(got);
  fclose(f);

  int sr = 16000;
  double dur = (double)pcm.size() / sr;
  printf("input=%s frames=%zu dur=%.1fs chn=%d ref_ch=%d asr_mode=%d pace=%.2f\n", wav_path, pcm.size(), dur, chn, ref_ch, asr_mode, pace);
  printf("sdk version: %s\n", HrscGetVersion());

  HrscEffectConfig cfg; memset(&cfg, 0, sizeof(cfg));
  static HrscAudioConfig in_cfg, out_cfg;
  in_cfg.sample_rate = sr; in_cfg.audio_channels = chn; in_cfg.audio_format = kHrscAudioFormatPcm16Bit;
  out_cfg.sample_rate = sr; out_cfg.audio_channels = 1; out_cfg.audio_format = kHrscAudioFormatPcm16Bit;
  cfg.input_cfg = in_cfg;
  cfg.output_cfg = out_cfg;
  cfg.ref_ch_index = (unsigned char)ref_ch;
  cfg.wakeup_prefix = 200; cfg.wakeup_suffix = 200;
  cfg.asr_timeout = 5000; cfg.vad_timeout = 5000;
  cfg.target_score = 0;
  cfg.cfg_file_path = "/opt/tros/humble/lib/hobot_audio/hrsc";
  cfg.support_command_word = 0;
  cfg.is_use_linear_mic_flag = 0;
  cfg.asr_output_mode = asr_mode;
  cfg.asr_output_channel = 0;
  std::string wake = ReadFile("/opt/tros/humble/lib/hobot_audio/hrsc/cmd_word.json");
  cfg.custom_wakeup_word = wake.empty() ? nullptr : wake.c_str();
  cfg.priv = &cfg;
  cfg.HrscAsrCallback = AsrCallback;
  cfg.HrscAsrDataCallback = AsrDataCallback;
  cfg.HrscEventCallback = EventCallback;
  cfg.HrscCmdCallback = CmdCallback;
  cfg.HrscDoaCallbadk = DoaCallback;
  cfg.HrscWakeupDataCallback = WkpCallback;
  cfg.HrscVoipDataCallback = VoipCallback;
  cfg.HrscProcessedDataCallback = ProcCallback;
  cfg.HrscAuthCallback = AuthCallback;

  void *handle = HrscInit(&cfg);
  if (!handle) { printf("HrscInit FAILED\n"); return 3; }
  if (HRSC_CODE_SUCCESS != HrscStart(handle)) { printf("HrscStart FAILED\n"); return 3; }
  printf("hrsc started ok\n"); fflush(stdout);

  clock_gettime(CLOCK_MONOTONIC, &g_t0);
  struct timespec t_start = g_t0;
  const int FRAME = 512;
  double feed_secs = 0;
  std::vector<short> frame(FRAME * chn);
  size_t pos = 0; long long t_ms = 0;
  while (pos < pcm.size()) {
    size_t n = pcm.size() - pos; if (n > (size_t)FRAME) n = FRAME;
    for (size_t i = 0; i < n; i++) {
      for (int c = 0; c < chn; c++) {
        bool use = (fill == 0) ? true : (fill == 1 ? (c == 0) : (c != ref_ch));
        frame[i * chn + c] = use ? pcm[pos + i] : 0;
      }
    }
    if (n < (size_t)FRAME) memset(&frame[n * chn], 0, (FRAME - n) * chn * sizeof(short));
    HrscAudioBuffer buf; buf.audio_data = frame.data(); buf.size = FRAME * chn * sizeof(short);
    buf.start = t_ms; buf.end = t_ms + 32;
    { struct timespec a,b; clock_gettime(CLOCK_MONOTONIC,&a); HrscProcess(handle, &buf); clock_gettime(CLOCK_MONOTONIC,&b); feed_secs += (b.tv_sec-a.tv_sec)+(b.tv_nsec-a.tv_nsec)/1e9; }
    pos += n; t_ms += (FRAME * 1000) / sr;
    if (pace > 0) {
      long want_us = (long)((double)t_ms * 1000.0 / pace);
      struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now);
      long now_us = (now.tv_sec - t_start.tv_sec) * 1000000L + (now.tv_nsec - t_start.tv_nsec) / 1000L;
      if (want_us > now_us) usleep(want_us - now_us);
    }
  }
  printf("\nfeed done, %.1fs audio, time_in_HrscProcess=%.2fs (=> %.2fx realtime), tail silence...\n", dur, feed_secs, dur/feed_secs); fflush(stdout);
  for (int k = 0; k < 100; k++) {
    memset(frame.data(), 0, frame.size() * sizeof(short));
    HrscAudioBuffer buf; buf.audio_data = frame.data(); buf.size = frame.size() * sizeof(short);
    buf.start = t_ms; buf.end = t_ms + 32;
    HrscProcess(handle, &buf); t_ms += (FRAME * 1000) / sr;
    usleep(1000);
  }
  printf("feed loop finished at +%.0fms, now draining async pipeline...\n", now_ms());
  double drain_start = now_ms(), last_change = now_ms();
  int last_n = g_n;
  while (now_ms() - drain_start < 300000) {
    usleep(200000);
    if (g_n != last_n) { last_n = g_n; last_change = now_ms(); }
    if (g_n > 0 && now_ms() - last_change > 5000) break;
  }
  { struct rusage ru; getrusage(RUSAGE_SELF,&ru); double cpu=ru.ru_utime.tv_sec+ru.ru_utime.tv_usec/1e6+ru.ru_stime.tv_sec+ru.ru_stime.tv_usec/1e6; printf("\n[CPU] user+sys=%.2fs / wall=%.2fs => 平均 %.2f 核在忙\n", cpu, now_ms()/1000.0, cpu/(now_ms()/1000.0)); }
  printf("\n=== 全部 %d 段结果到达,最后一段在 +%.0fms,端到端 %.2fs (音频 %.1fs => %.2fx 实时) ===\n", g_n, g_last_ms, now_ms()/1000.0, dur, dur/(now_ms()/1000.0));

  std::ofstream o(out_path);
  for (auto &s : g_asr) o << s << "\n";
  o.close();
  printf("\nsegments=%zu written=%s\n", g_asr.size(), out_path);
  HrscStop(handle);
  HrscRelease(&handle);
  return 0;
}

附录 B:原始文件位置

  • 本仓库:notes/sources/dg_bpu_asr/hrsc_asr.cppnotes/sources/dg_bpu_asr/measurements.md
  • dg(重启会丢):/tmp/asr/hrsc_asr.cpp/tmp/asr/test30.wav/tmp/asr/bpu_*.txt
  • 头文件来源:https://github.com/D-Robotics/hobot_audio (include/x5/horizon_speech_sdk/include/horizonspeechsdk/hrsc_sdk.h
  • 相关笔记:方案总览 note_a4def66c886144e0;全文转写 note_796c8125fe43479a