bpu
This commit is contained in:
188
deploy_45dim_rl_gym/bpu_deploy_x5/README.md
Normal file
188
deploy_45dim_rl_gym/bpu_deploy_x5/README.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# RDK X5 BPU 部署测试
|
||||
|
||||
这个目录是 RobotLab BPU `.bin` 的隔离部署路径,不覆盖现有 ONNX/fastcpp
|
||||
部署脚本。
|
||||
|
||||
当前包含:
|
||||
|
||||
- `bpu_policy.py`:BPU policy wrapper,默认使用 `cpp_dnn_api_x5` C++ DNN API 后端。
|
||||
- `cpp/`:C++ DNN API 推理库源码、板端编译脚本和纯 C++ benchmark。
|
||||
- `deploy_go1_robotlab_bpu_x5_fastcpp.py`:基于现有 `lab_fastcpp` 状态机的 BPU 版本。
|
||||
|
||||
## 重要限制
|
||||
|
||||
不要用 `hobot_dnn.pyeasy_dnn` 直接跑这个模型。这个 BPU 模型的输入是
|
||||
`featuremap`,板端实测 `pyeasy_dnn.forward()` 会产生明显错误的 action,
|
||||
和 `hrt_model_exec infer` 不一致。
|
||||
|
||||
当前默认不再使用 `bpu_infer_lib` 做实机推理,而是用 C++ 直接调用 DNN API。
|
||||
部署脚本使用的 C++ 调用顺序是:
|
||||
|
||||
```bash
|
||||
hbDNNInitializeFromFiles -> hbDNNGetModelHandle -> hbDNNInfer -> hbDNNWaitTaskDone -> hbDNNReleaseTask
|
||||
```
|
||||
|
||||
这个路径已经用 `00000.bin` 验证,输出和 `hrt_model_exec infer` 对齐。
|
||||
|
||||
`bpu_infer_lib.forward(False)` 在当前板端会从 C runtime 向 stdout 每帧打印
|
||||
`duplicate model infer is not supported in one batch` / `invalid model`。
|
||||
虽然输出和 `hrt_model_exec infer` 一致,但这说明 task 提交方式不干净。旧 Python
|
||||
fallback 保留为 `BpuInferLibPythonPolicy`,只用于对照,不作为默认部署路径。
|
||||
|
||||
这个判断参考 D-Robotics RDK X5 runtime sample:
|
||||
|
||||
- `03_misc/run_resnet50_feature.sh` 是官方 featuremap 输入模型示例,流程是
|
||||
prepare feature tensor -> infer success -> task done。
|
||||
- 性能测量示例在循环中每帧执行 `hbDNNInfer` 后,立刻
|
||||
`hbDNNWaitTaskDone(task_handle, 0)`,再 `hbDNNReleaseTask(task_handle)` 并把
|
||||
task handle 置空。
|
||||
|
||||
`bpu_infer_lib` 的 Python API 没有直接暴露 `hbDNNReleaseTask`,所以这里改成
|
||||
C++ shared library,并由 Python 通过 `ctypes` 调用。
|
||||
|
||||
板端编译:
|
||||
|
||||
```bash
|
||||
cd /root/go1_pro_deploy/deploy_45dim_rl_gym/bpu_deploy_x5/cpp
|
||||
bash build_board.sh
|
||||
```
|
||||
|
||||
## 模型路径
|
||||
|
||||
默认模型:
|
||||
|
||||
```text
|
||||
deploy_45dim_rl_gym/bpu_quantization/mapper_output_6500_gemm/policy_robotlab_6500_int16_gemm.bin
|
||||
```
|
||||
|
||||
输入输出:
|
||||
|
||||
- 输入:`obs_4d [1, 1, 1, 450]`,float32 featuremap
|
||||
- 输出:`actions [1, 12, 1, 1]`,float32,脚本会 flatten 成 12 维 action
|
||||
|
||||
## 板端准备
|
||||
|
||||
同步仓库和模型后,在板端确认:
|
||||
|
||||
```bash
|
||||
cd /root/go1_pro_deploy
|
||||
ls deploy_45dim_rl_gym/bpu_quantization/mapper_output_6500_gemm/policy_robotlab_6500_int16_gemm.bin
|
||||
ls deploy_45dim_rl_gym/bpu_quantization/calibration_data/00000.bin
|
||||
ls deploy_45dim_rl_gym/bpu_deploy_x5/cpp/libbpu_dnn_policy.so
|
||||
```
|
||||
|
||||
如果 `mapper_output_6500_gemm/` 没有同步到 git,需要手动把 `.bin` 放到对应路径,
|
||||
或用 `--bpu-model` 指定绝对路径。
|
||||
|
||||
## 纯离线 BPU 自检
|
||||
|
||||
先不连接机器人,确认 C++ BPU 后端输出和 `hrt_model_exec infer` 的参考值一致:
|
||||
|
||||
```bash
|
||||
cd /root/go1_pro_deploy
|
||||
PYTHONPATH=/root/go1_pro_deploy \
|
||||
python3 deploy_45dim_rl_gym/bpu_deploy_x5/test_bpu_policy.py \
|
||||
--check-reference-00000 \
|
||||
--repeat 1000
|
||||
```
|
||||
|
||||
通过时应看到:
|
||||
|
||||
```text
|
||||
reference_max_abs_diff 0.0
|
||||
```
|
||||
|
||||
当前板端 `root@192.168.150.167` 已验证:
|
||||
|
||||
```text
|
||||
reference_max_abs_diff 4.76837158203125e-07
|
||||
reference_mean_abs_diff 3.071812386679085e-07
|
||||
repeat=1000 avg_ms=1.019592 # Python ctypes -> C++ DNN API
|
||||
duplicate_errors=0
|
||||
```
|
||||
|
||||
纯 C++ benchmark:
|
||||
|
||||
```bash
|
||||
cd /root/go1_pro_deploy/deploy_45dim_rl_gym/bpu_deploy_x5/cpp
|
||||
./bpu_dnn_bench
|
||||
```
|
||||
|
||||
当前板端结果:
|
||||
|
||||
```text
|
||||
reference_max_abs_diff 0
|
||||
repeat=1000 cpp_avg_ms=0.711932
|
||||
duplicate_errors=0
|
||||
```
|
||||
|
||||
## 离线推理检查
|
||||
|
||||
先不要接电机控制,只运行 BPU inference:
|
||||
|
||||
```bash
|
||||
cd /root/go1_pro_deploy
|
||||
PYTHONPATH=/root/go1_pro_sdk:/root/go1_pro_deploy \
|
||||
python3 deploy_45dim_rl_gym/bpu_deploy_x5/deploy_go1_robotlab_bpu_x5_fastcpp.py \
|
||||
--infer-check \
|
||||
--log-dir logs \
|
||||
--print-every 50 \
|
||||
--max-steps 500
|
||||
```
|
||||
|
||||
这一步仍会连接 MCU 读取状态,但不会发送电机指令。
|
||||
|
||||
## 悬空状态机测试
|
||||
|
||||
只有 `--infer-check` 日志确认 action 正常后,再悬空测试状态机。先不加
|
||||
`--enable-rl`,R2 只能走到 `INFER_TEST`,不会进入 RL 控制:
|
||||
|
||||
```bash
|
||||
cd /root/go1_pro_deploy
|
||||
PYTHONPATH=/root/go1_pro_sdk:/root/go1_pro_deploy \
|
||||
python3 deploy_45dim_rl_gym/bpu_deploy_x5/deploy_go1_robotlab_bpu_x5_fastcpp.py \
|
||||
--kill-sport \
|
||||
--log-dir logs \
|
||||
--kp 28 --kd 0.7 \
|
||||
--kp-cal 20 --kd-cal 1.0 \
|
||||
--power-factor 7 \
|
||||
--position-protect-limit 0.0 \
|
||||
--action-clip 5.0 \
|
||||
--action-trip-limit 8.0 \
|
||||
--action-hard-trip-limit 16.0 \
|
||||
--max-target-step 0.025 \
|
||||
--max-roll-deg 35 \
|
||||
--max-pitch-deg 35 \
|
||||
--swap-vy-yaw \
|
||||
--rc-vx-scale 0.5 \
|
||||
--rc-vy-scale 0.5 \
|
||||
--rc-wz-scale 1.0
|
||||
```
|
||||
|
||||
确认 `OBS_TEST` 和 `INFER_TEST` 正常后,才允许加 `--enable-rl` 做悬空 RL:
|
||||
|
||||
```bash
|
||||
cd /root/go1_pro_deploy
|
||||
PYTHONPATH=/root/go1_pro_sdk:/root/go1_pro_deploy \
|
||||
python3 deploy_45dim_rl_gym/bpu_deploy_x5/deploy_go1_robotlab_bpu_x5_fastcpp.py \
|
||||
--kill-sport \
|
||||
--enable-rl \
|
||||
--log-dir logs \
|
||||
--kp 28 --kd 0.7 \
|
||||
--kp-cal 20 --kd-cal 1.0 \
|
||||
--power-factor 7 \
|
||||
--position-protect-limit 0.0 \
|
||||
--action-clip 5.0 \
|
||||
--action-trip-limit 8.0 \
|
||||
--action-hard-trip-limit 16.0 \
|
||||
--max-target-step 0.025 \
|
||||
--max-roll-deg 35 \
|
||||
--max-pitch-deg 35 \
|
||||
--swap-vy-yaw \
|
||||
--rc-vx-scale 0.5 \
|
||||
--rc-vy-scale 0.5 \
|
||||
--rc-wz-scale 1.0
|
||||
```
|
||||
|
||||
地面测试不要直接从 BPU 版本开始;先用同参数 ONNX 版本确认行为,再做 BPU/ONNX
|
||||
对照。
|
||||
207
deploy_45dim_rl_gym/bpu_deploy_x5/bpu_policy.py
Normal file
207
deploy_45dim_rl_gym/bpu_deploy_x5/bpu_policy.py
Normal file
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
"""RDK X5 BPU policy runtime wrappers."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
HERE = Path(__file__).parent.resolve()
|
||||
DEFAULT_CPP_LIB = HERE / "cpp" / "libbpu_dnn_policy.so"
|
||||
|
||||
|
||||
def flush_c_stdio():
|
||||
try:
|
||||
ctypes.CDLL(None).fflush(None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@contextmanager
|
||||
def suppress_c_output(enabled=True):
|
||||
if not enabled:
|
||||
yield
|
||||
return
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
flush_c_stdio()
|
||||
saved_fds = {fd: os.dup(fd) for fd in (1, 2)}
|
||||
devnull_fd = os.open(os.devnull, os.O_WRONLY)
|
||||
try:
|
||||
os.dup2(devnull_fd, 1)
|
||||
os.dup2(devnull_fd, 2)
|
||||
yield
|
||||
finally:
|
||||
flush_c_stdio()
|
||||
for fd, saved_fd in saved_fds.items():
|
||||
os.dup2(saved_fd, fd)
|
||||
os.close(saved_fd)
|
||||
os.close(devnull_fd)
|
||||
|
||||
|
||||
def _first_bpu_core(bpu_cores):
|
||||
cores = tuple(int(core) for core in bpu_cores)
|
||||
if not cores:
|
||||
return 0
|
||||
core = cores[0]
|
||||
if core == 0:
|
||||
return 1
|
||||
if core == 1:
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
class BpuInferLibPolicy:
|
||||
"""Default fast backend: direct C++ DNN API via ctypes."""
|
||||
|
||||
backend_name = "cpp_dnn_api_x5"
|
||||
input_shape = (1, 1, 1, 450)
|
||||
output_shape = (1, 12, 1, 1)
|
||||
|
||||
def __init__(self, model_path, priority=0, bpu_cores=(0,), cpp_lib=DEFAULT_CPP_LIB):
|
||||
self.model_path = Path(model_path).expanduser().resolve()
|
||||
self.cpp_lib = Path(cpp_lib).expanduser().resolve()
|
||||
if not self.model_path.exists():
|
||||
raise FileNotFoundError(f"BPU model not found: {self.model_path}")
|
||||
if not self.cpp_lib.exists():
|
||||
raise FileNotFoundError(
|
||||
f"C++ BPU runtime library not found: {self.cpp_lib}. "
|
||||
f"Build it on the RDK X5 board with: cd {self.cpp_lib.parent} && bash build_board.sh"
|
||||
)
|
||||
|
||||
self.priority = int(priority)
|
||||
self.bpu_cores = tuple(int(core) for core in bpu_cores)
|
||||
self._lib = ctypes.CDLL(str(self.cpp_lib))
|
||||
self._lib.rlgym_bpu_create.argtypes = [
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_int,
|
||||
ctypes.c_int,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_int,
|
||||
]
|
||||
self._lib.rlgym_bpu_create.restype = ctypes.c_void_p
|
||||
self._lib.rlgym_bpu_infer.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.POINTER(ctypes.c_float),
|
||||
ctypes.POINTER(ctypes.c_float),
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_int,
|
||||
]
|
||||
self._lib.rlgym_bpu_infer.restype = ctypes.c_int
|
||||
self._lib.rlgym_bpu_destroy.argtypes = [ctypes.c_void_p]
|
||||
self._lib.rlgym_bpu_destroy.restype = None
|
||||
self._lib.rlgym_bpu_version.argtypes = []
|
||||
self._lib.rlgym_bpu_version.restype = ctypes.c_char_p
|
||||
|
||||
err = ctypes.create_string_buffer(1024)
|
||||
self._handle = self._lib.rlgym_bpu_create(
|
||||
str(self.model_path).encode("utf-8"),
|
||||
_first_bpu_core(self.bpu_cores),
|
||||
self.priority,
|
||||
err,
|
||||
len(err),
|
||||
)
|
||||
if not self._handle:
|
||||
raise RuntimeError(err.value.decode("utf-8", errors="replace"))
|
||||
self._output = np.empty(12, dtype=np.float32)
|
||||
self._output_ptr = self._output.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
|
||||
self._err = ctypes.create_string_buffer(1024)
|
||||
|
||||
print(f"[INFO] BPU model: {self.model_path}")
|
||||
print(f"[INFO] Backend: {self.backend_name}")
|
||||
print(f"[INFO] Runtime: {self.cpp_lib}")
|
||||
print(f"[INFO] Input : obs_4d {self.input_shape}")
|
||||
print(f"[INFO] Output : actions {self.output_shape}")
|
||||
|
||||
def close(self):
|
||||
handle = getattr(self, "_handle", None)
|
||||
if handle:
|
||||
self._lib.rlgym_bpu_destroy(handle)
|
||||
self._handle = None
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __call__(self, flat_input):
|
||||
arr = np.asarray(flat_input, dtype=np.float32)
|
||||
if arr.size != 450:
|
||||
raise ValueError(f"BPU policy input has {arr.size} values, expected 450")
|
||||
input_flat = np.ascontiguousarray(arr.reshape(-1), dtype=np.float32)
|
||||
rc = self._lib.rlgym_bpu_infer(
|
||||
self._handle,
|
||||
input_flat.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
|
||||
self._output_ptr,
|
||||
self._err,
|
||||
len(self._err),
|
||||
)
|
||||
if rc != 0:
|
||||
raise RuntimeError(self._err.value.decode("utf-8", errors="replace"))
|
||||
if not np.all(np.isfinite(self._output)):
|
||||
raise RuntimeError(f"BPU policy output is not finite: {self._output}")
|
||||
return self._output.copy()
|
||||
|
||||
|
||||
class BpuInferLibPythonPolicy:
|
||||
"""Slower Python package backend kept only for comparison."""
|
||||
|
||||
backend_name = "bpu_infer_lib_x5_python"
|
||||
input_shape = (1, 1, 1, 450)
|
||||
output_shape = (1, 12, 1, 1)
|
||||
|
||||
def __init__(self, model_path, priority=0, bpu_cores=(0,), suppress_runtime_output=False):
|
||||
self.model_path = Path(model_path).expanduser().resolve()
|
||||
if not self.model_path.exists():
|
||||
raise FileNotFoundError(f"BPU model not found: {self.model_path}")
|
||||
|
||||
try:
|
||||
from bpu_infer_lib import Infer
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"bpu_infer_lib is required for the fallback Python BPU backend. "
|
||||
"Do not use hobot_dnn.pyeasy_dnn for this policy: it produced "
|
||||
"wrong actions in local board tests."
|
||||
) from exc
|
||||
|
||||
self.priority = int(priority)
|
||||
self.bpu_cores = tuple(int(core) for core in bpu_cores)
|
||||
self.suppress_runtime_output = bool(suppress_runtime_output)
|
||||
with suppress_c_output(self.suppress_runtime_output):
|
||||
self.infer = Infer(False)
|
||||
loaded = self.infer.load_model(str(self.model_path))
|
||||
if not loaded:
|
||||
raise RuntimeError(f"Failed to load BPU model: {self.model_path}")
|
||||
|
||||
print(f"[INFO] BPU model: {self.model_path}")
|
||||
print(f"[INFO] Backend: {self.backend_name}")
|
||||
print(f"[INFO] Input : obs_4d {self.input_shape}")
|
||||
print(f"[INFO] Output : actions {self.output_shape}")
|
||||
|
||||
def __call__(self, flat_input):
|
||||
arr = np.asarray(flat_input, dtype=np.float32)
|
||||
if arr.size != 450:
|
||||
raise ValueError(f"BPU policy input has {arr.size} values, expected 450")
|
||||
input_4d = np.ascontiguousarray(arr.reshape(self.input_shape), dtype=np.float32)
|
||||
with suppress_c_output(self.suppress_runtime_output):
|
||||
copied = self.infer.read_numpy_arr_float32(input_4d, 0)
|
||||
if copied:
|
||||
self.infer.forward(True)
|
||||
got_output = self.infer.get_output()
|
||||
if not got_output:
|
||||
raise RuntimeError("bpu_infer_lib get_output() failed")
|
||||
output = self.infer.get_infer_res_np_float32(0)
|
||||
if not copied:
|
||||
raise RuntimeError("Failed to copy float32 input into bpu_infer_lib")
|
||||
action = np.asarray(output, dtype=np.float32).reshape(-1)
|
||||
if action.size < 12:
|
||||
raise RuntimeError(f"BPU policy output has {action.size} values, expected at least 12")
|
||||
action = action[:12].astype(np.float32, copy=False)
|
||||
if not np.all(np.isfinite(action)):
|
||||
raise RuntimeError(f"BPU policy output is not finite: {action}")
|
||||
return action
|
||||
103
deploy_45dim_rl_gym/bpu_deploy_x5/cpp/bpu_dnn_bench.cpp
Normal file
103
deploy_45dim_rl_gym/bpu_deploy_x5/cpp/bpu_dnn_bench.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
void *rlgym_bpu_create(const char *model_path, int bpu_core, int priority,
|
||||
char *err, int err_len);
|
||||
int rlgym_bpu_infer(void *handle, const float *input450, float *output12, char *err,
|
||||
int err_len);
|
||||
void rlgym_bpu_destroy(void *handle);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kReference[12] = {
|
||||
0.537439f, 1.032869f, 0.042010f, -0.463015f,
|
||||
0.515008f, 1.349627f, 0.014652f, -1.518314f,
|
||||
1.012123f, -0.346840f, 0.086353f, -0.755527f,
|
||||
};
|
||||
|
||||
bool read_f32_file(const std::string &path, std::vector<float> *data) {
|
||||
std::ifstream ifs(path, std::ios::binary);
|
||||
if (!ifs) {
|
||||
return false;
|
||||
}
|
||||
ifs.seekg(0, std::ios::end);
|
||||
const auto size = ifs.tellg();
|
||||
ifs.seekg(0, std::ios::beg);
|
||||
if (size <= 0 || size % static_cast<std::streamoff>(sizeof(float)) != 0) {
|
||||
return false;
|
||||
}
|
||||
data->resize(static_cast<size_t>(size) / sizeof(float));
|
||||
ifs.read(reinterpret_cast<char *>(data->data()), size);
|
||||
return ifs.good();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *model =
|
||||
"/root/go1_pro_deploy/deploy_45dim_rl_gym/bpu_quantization/mapper_output_6500_gemm/"
|
||||
"policy_robotlab_6500_int16_gemm.bin";
|
||||
const char *input =
|
||||
"/root/go1_pro_deploy/deploy_45dim_rl_gym/bpu_quantization/calibration_data/00000.bin";
|
||||
int repeat = 1000;
|
||||
if (argc > 1) model = argv[1];
|
||||
if (argc > 2) input = argv[2];
|
||||
if (argc > 3) repeat = std::atoi(argv[3]);
|
||||
|
||||
std::vector<float> obs;
|
||||
if (!read_f32_file(input, &obs) || obs.size() != 450) {
|
||||
std::cerr << "failed to read 450 float32 input: " << input << "\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
char err[1024] = {};
|
||||
void *handle = rlgym_bpu_create(model, 1, 0, err, sizeof(err));
|
||||
if (handle == nullptr) {
|
||||
std::cerr << err << "\n";
|
||||
return 3;
|
||||
}
|
||||
|
||||
float out[12] = {};
|
||||
if (rlgym_bpu_infer(handle, obs.data(), out, err, sizeof(err)) != 0) {
|
||||
std::cerr << err << "\n";
|
||||
rlgym_bpu_destroy(handle);
|
||||
return 4;
|
||||
}
|
||||
float max_diff = 0.0f;
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
max_diff = std::max(max_diff, std::fabs(out[i] - kReference[i]));
|
||||
}
|
||||
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < repeat; ++i) {
|
||||
if (rlgym_bpu_infer(handle, obs.data(), out, err, sizeof(err)) != 0) {
|
||||
std::cerr << err << "\n";
|
||||
rlgym_bpu_destroy(handle);
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
const auto t1 = std::chrono::steady_clock::now();
|
||||
const double elapsed_ms =
|
||||
std::chrono::duration<double, std::milli>(t1 - t0).count();
|
||||
|
||||
std::cout << "reference_max_abs_diff " << max_diff << "\n";
|
||||
std::cout << "repeat=" << repeat << " cpp_avg_ms=" << (elapsed_ms / repeat) << "\n";
|
||||
std::cout << "action0=" << out[0] << " action_max_abs="
|
||||
<< *std::max_element(out, out + 12, [](float a, float b) {
|
||||
return std::fabs(a) < std::fabs(b);
|
||||
})
|
||||
<< "\n";
|
||||
|
||||
rlgym_bpu_destroy(handle);
|
||||
return 0;
|
||||
}
|
||||
227
deploy_45dim_rl_gym/bpu_deploy_x5/cpp/bpu_dnn_policy.cpp
Normal file
227
deploy_45dim_rl_gym/bpu_deploy_x5/cpp/bpu_dnn_policy.cpp
Normal file
@@ -0,0 +1,227 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <dnn/hb_dnn.h>
|
||||
#include <dnn/hb_dnn_ext.h>
|
||||
#include <dnn/hb_dnn_status.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kInputFloats = 450;
|
||||
constexpr int kOutputFloats = 12;
|
||||
|
||||
void set_error(char *err, int err_len, const std::string &msg) {
|
||||
if (err == nullptr || err_len <= 0) {
|
||||
return;
|
||||
}
|
||||
std::snprintf(err, static_cast<size_t>(err_len), "%s", msg.c_str());
|
||||
}
|
||||
|
||||
std::string dnn_error(const std::string &where, int code) {
|
||||
std::ostringstream oss;
|
||||
oss << where << " failed: " << code;
|
||||
const char *desc = hbDNNGetErrorDesc(code);
|
||||
if (desc != nullptr) {
|
||||
oss << " (" << desc << ")";
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
int element_count(const hbDNNTensorShape &shape) {
|
||||
int count = 1;
|
||||
for (int i = 0; i < shape.numDimensions; ++i) {
|
||||
count *= shape.dimensionSize[i];
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void check_dnn(int code, const std::string &where) {
|
||||
if (code != 0) {
|
||||
throw std::runtime_error(dnn_error(where, code));
|
||||
}
|
||||
}
|
||||
|
||||
class BpuDnnPolicy {
|
||||
public:
|
||||
BpuDnnPolicy(const char *model_path, int bpu_core, int priority)
|
||||
: bpu_core_(bpu_core), priority_(priority) {
|
||||
if (model_path == nullptr || model_path[0] == '\0') {
|
||||
throw std::runtime_error("empty model path");
|
||||
}
|
||||
|
||||
const char *model_files[] = {model_path};
|
||||
check_dnn(hbDNNInitializeFromFiles(&packed_handle_, model_files, 1),
|
||||
"hbDNNInitializeFromFiles");
|
||||
|
||||
const char **model_names = nullptr;
|
||||
int32_t model_count = 0;
|
||||
check_dnn(hbDNNGetModelNameList(&model_names, &model_count, packed_handle_),
|
||||
"hbDNNGetModelNameList");
|
||||
if (model_count <= 0 || model_names == nullptr || model_names[0] == nullptr) {
|
||||
throw std::runtime_error("model has no names");
|
||||
}
|
||||
model_name_ = model_names[0];
|
||||
check_dnn(hbDNNGetModelHandle(&dnn_handle_, packed_handle_, model_names[0]),
|
||||
"hbDNNGetModelHandle");
|
||||
|
||||
int32_t input_count = 0;
|
||||
int32_t output_count = 0;
|
||||
check_dnn(hbDNNGetInputCount(&input_count, dnn_handle_), "hbDNNGetInputCount");
|
||||
check_dnn(hbDNNGetOutputCount(&output_count, dnn_handle_), "hbDNNGetOutputCount");
|
||||
if (input_count != 1 || output_count != 1) {
|
||||
std::ostringstream oss;
|
||||
oss << "expected 1 input and 1 output, got " << input_count << " inputs and "
|
||||
<< output_count << " outputs";
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
|
||||
input_tensors_.resize(1);
|
||||
output_tensors_.resize(1);
|
||||
check_dnn(hbDNNGetInputTensorProperties(&input_tensors_[0].properties, dnn_handle_, 0),
|
||||
"hbDNNGetInputTensorProperties");
|
||||
check_dnn(hbDNNGetOutputTensorProperties(&output_tensors_[0].properties, dnn_handle_, 0),
|
||||
"hbDNNGetOutputTensorProperties");
|
||||
|
||||
validate_tensor(input_tensors_[0].properties, kInputFloats, HB_DNN_TENSOR_TYPE_F32,
|
||||
"input");
|
||||
validate_tensor(output_tensors_[0].properties, kOutputFloats, HB_DNN_TENSOR_TYPE_F32,
|
||||
"output");
|
||||
|
||||
alloc_tensor_mem(input_tensors_[0]);
|
||||
alloc_tensor_mem(output_tensors_[0]);
|
||||
}
|
||||
|
||||
~BpuDnnPolicy() {
|
||||
release_tensor_mem(input_tensors_);
|
||||
release_tensor_mem(output_tensors_);
|
||||
if (packed_handle_ != nullptr) {
|
||||
hbDNNRelease(packed_handle_);
|
||||
packed_handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void infer(const float *input, float *output) {
|
||||
if (input == nullptr || output == nullptr) {
|
||||
throw std::runtime_error("null input/output pointer");
|
||||
}
|
||||
|
||||
auto &input_mem = input_tensors_[0].sysMem[0];
|
||||
std::memcpy(input_mem.virAddr, input, sizeof(float) * kInputFloats);
|
||||
int code = hbSysFlushMem(&input_mem, HB_SYS_MEM_CACHE_CLEAN);
|
||||
if (code != 0) {
|
||||
throw std::runtime_error(dnn_error("hbSysFlushMem(input)", code));
|
||||
}
|
||||
|
||||
hbDNNInferCtrlParam ctrl;
|
||||
HB_DNN_INITIALIZE_INFER_CTRL_PARAM(&ctrl);
|
||||
ctrl.bpuCoreId = bpu_core_;
|
||||
ctrl.priority = priority_;
|
||||
ctrl.more = false;
|
||||
|
||||
hbDNNTaskHandle_t task_handle = nullptr;
|
||||
hbDNNTensor *output_ptr = output_tensors_.data();
|
||||
check_dnn(hbDNNInfer(&task_handle, &output_ptr, input_tensors_.data(), dnn_handle_, &ctrl),
|
||||
"hbDNNInfer");
|
||||
check_dnn(hbDNNWaitTaskDone(task_handle, 0), "hbDNNWaitTaskDone");
|
||||
check_dnn(hbDNNReleaseTask(task_handle), "hbDNNReleaseTask");
|
||||
task_handle = nullptr;
|
||||
|
||||
auto &output_mem = output_tensors_[0].sysMem[0];
|
||||
code = hbSysFlushMem(&output_mem, HB_SYS_MEM_CACHE_INVALIDATE);
|
||||
if (code != 0) {
|
||||
throw std::runtime_error(dnn_error("hbSysFlushMem(output)", code));
|
||||
}
|
||||
std::memcpy(output, output_mem.virAddr, sizeof(float) * kOutputFloats);
|
||||
}
|
||||
|
||||
private:
|
||||
static void validate_tensor(const hbDNNTensorProperties &props, int expected_count,
|
||||
int expected_type, const char *name) {
|
||||
const int count = element_count(props.validShape);
|
||||
if (count != expected_count) {
|
||||
std::ostringstream oss;
|
||||
oss << name << " valid element count " << count << " != " << expected_count;
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
if (props.tensorType != expected_type) {
|
||||
std::ostringstream oss;
|
||||
oss << name << " tensor type " << props.tensorType << " != " << expected_type;
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
if (props.alignedByteSize <= 0) {
|
||||
throw std::runtime_error(std::string(name) + " alignedByteSize <= 0");
|
||||
}
|
||||
}
|
||||
|
||||
static void alloc_tensor_mem(hbDNNTensor &tensor) {
|
||||
std::memset(tensor.sysMem, 0, sizeof(tensor.sysMem));
|
||||
const uint32_t bytes = static_cast<uint32_t>(tensor.properties.alignedByteSize);
|
||||
const int code = hbSysAllocCachedMem(&tensor.sysMem[0], bytes);
|
||||
if (code != 0) {
|
||||
throw std::runtime_error(dnn_error("hbSysAllocCachedMem", code));
|
||||
}
|
||||
}
|
||||
|
||||
static void release_tensor_mem(std::vector<hbDNNTensor> &tensors) {
|
||||
for (auto &tensor : tensors) {
|
||||
if (tensor.sysMem[0].virAddr != nullptr || tensor.sysMem[0].phyAddr != 0) {
|
||||
hbSysFreeMem(&tensor.sysMem[0]);
|
||||
std::memset(tensor.sysMem, 0, sizeof(tensor.sysMem));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hbPackedDNNHandle_t packed_handle_{nullptr};
|
||||
hbDNNHandle_t dnn_handle_{nullptr};
|
||||
std::string model_name_;
|
||||
std::vector<hbDNNTensor> input_tensors_;
|
||||
std::vector<hbDNNTensor> output_tensors_;
|
||||
int bpu_core_{HB_BPU_CORE_ANY};
|
||||
int priority_{HB_DNN_PRIORITY_LOWEST};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
void *rlgym_bpu_create(const char *model_path, int bpu_core, int priority,
|
||||
char *err, int err_len) {
|
||||
try {
|
||||
set_error(err, err_len, "");
|
||||
return new BpuDnnPolicy(model_path, bpu_core, priority);
|
||||
} catch (const std::exception &e) {
|
||||
set_error(err, err_len, e.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int rlgym_bpu_infer(void *handle, const float *input450, float *output12, char *err,
|
||||
int err_len) {
|
||||
try {
|
||||
set_error(err, err_len, "");
|
||||
if (handle == nullptr) {
|
||||
throw std::runtime_error("null policy handle");
|
||||
}
|
||||
static_cast<BpuDnnPolicy *>(handle)->infer(input450, output12);
|
||||
return 0;
|
||||
} catch (const std::exception &e) {
|
||||
set_error(err, err_len, e.what());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void rlgym_bpu_destroy(void *handle) {
|
||||
delete static_cast<BpuDnnPolicy *>(handle);
|
||||
}
|
||||
|
||||
const char *rlgym_bpu_version() {
|
||||
return "cpp_dnn_api_x5";
|
||||
}
|
||||
|
||||
}
|
||||
20
deploy_45dim_rl_gym/bpu_deploy_x5/cpp/build_board.sh
Normal file
20
deploy_45dim_rl_gym/bpu_deploy_x5/cpp/build_board.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
g++ -O3 -DNDEBUG -std=c++17 -fPIC -shared \
|
||||
bpu_dnn_policy.cpp \
|
||||
-I/usr/include \
|
||||
-ldnn \
|
||||
-o libbpu_dnn_policy.so
|
||||
|
||||
g++ -O3 -DNDEBUG -std=c++17 \
|
||||
bpu_dnn_bench.cpp \
|
||||
-I/usr/include \
|
||||
-L. -lbpu_dnn_policy \
|
||||
-Wl,-rpath,'$ORIGIN' \
|
||||
-o bpu_dnn_bench
|
||||
|
||||
ls -lh libbpu_dnn_policy.so
|
||||
ls -lh bpu_dnn_bench
|
||||
File diff suppressed because it is too large
Load Diff
74
deploy_45dim_rl_gym/bpu_deploy_x5/test_bpu_policy.py
Normal file
74
deploy_45dim_rl_gym/bpu_deploy_x5/test_bpu_policy.py
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline BPU policy smoke test for RDK X5.
|
||||
|
||||
This does not connect to the robot. It loads a BPU .bin and one raw float32
|
||||
input file, runs bpu_infer_lib repeatedly, and optionally checks the known
|
||||
00000.bin reference output captured from hrt_model_exec.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from bpu_policy import BpuInferLibPolicy
|
||||
|
||||
|
||||
HERE = Path(__file__).parent.resolve()
|
||||
DEFAULT_MODEL = (
|
||||
HERE.parent / "bpu_quantization" / "mapper_output_6500_gemm" /
|
||||
"policy_robotlab_6500_int16_gemm.bin"
|
||||
)
|
||||
DEFAULT_INPUT = HERE.parent / "bpu_quantization" / "calibration_data" / "00000.bin"
|
||||
REFERENCE_00000 = np.array([
|
||||
0.537439,
|
||||
1.032869,
|
||||
0.042010,
|
||||
-0.463015,
|
||||
0.515008,
|
||||
1.349627,
|
||||
0.014652,
|
||||
-1.518314,
|
||||
1.012123,
|
||||
-0.346840,
|
||||
0.086353,
|
||||
-0.755527,
|
||||
], dtype=np.float32)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Offline BPU policy smoke test")
|
||||
parser.add_argument("--bpu-model", default=str(DEFAULT_MODEL))
|
||||
parser.add_argument("--input-bin", default=str(DEFAULT_INPUT))
|
||||
parser.add_argument("--repeat", type=int, default=1000)
|
||||
parser.add_argument("--check-reference-00000", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
input_path = Path(args.input_bin).expanduser().resolve()
|
||||
data = np.fromfile(input_path, dtype=np.float32)
|
||||
if data.size != 450:
|
||||
raise ValueError(f"{input_path} has {data.size} float32 values, expected 450")
|
||||
|
||||
policy = BpuInferLibPolicy(args.bpu_model)
|
||||
action = policy(data)
|
||||
print("action", np.array2string(action, precision=6))
|
||||
print("action_max_abs", float(np.max(np.abs(action))))
|
||||
|
||||
if args.check_reference_00000:
|
||||
diff = np.abs(action - REFERENCE_00000)
|
||||
print("reference_max_abs_diff", float(diff.max()))
|
||||
print("reference_mean_abs_diff", float(diff.mean()))
|
||||
if diff.max() > 1e-5:
|
||||
raise RuntimeError("BPU output does not match the known 00000.bin reference")
|
||||
|
||||
repeats = max(1, int(args.repeat))
|
||||
t0 = time.perf_counter()
|
||||
for _ in range(repeats):
|
||||
policy(data)
|
||||
elapsed_ms = (time.perf_counter() - t0) * 1000.0
|
||||
print(f"repeat={repeats} avg_ms={elapsed_ms / repeats:.6f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user