adapt gym 5history

This commit is contained in:
cyy_mac
2026-07-28 16:36:33 +08:00
parent 6c4dd6ed14
commit 071c576bfe
14 changed files with 1782 additions and 77 deletions

8
.gitignore vendored
View File

@@ -5,10 +5,18 @@ __pycache__/
deploy_45dim_rl_gym/bpu_quantization/.hb_check/ deploy_45dim_rl_gym/bpu_quantization/.hb_check/
deploy_45dim_rl_gym/bpu_quantization/calibration_data/ deploy_45dim_rl_gym/bpu_quantization/calibration_data/
deploy_45dim_rl_gym/bpu_quantization/calibration_data_fast*/ deploy_45dim_rl_gym/bpu_quantization/calibration_data_fast*/
deploy_45dim_rl_gym/bpu_quantization/calibration_data_*_gym_fast*/
deploy_45dim_rl_gym/bpu_quantization/calibration_data_*_gym_fast*_metadata.json
deploy_45dim_rl_gym/bpu_quantization/mapper_output/ deploy_45dim_rl_gym/bpu_quantization/mapper_output/
deploy_45dim_rl_gym/bpu_quantization/mapper_output_gemm/ deploy_45dim_rl_gym/bpu_quantization/mapper_output_gemm/
deploy_45dim_rl_gym/bpu_quantization/mapper_output_*_gemm/
deploy_45dim_rl_gym/bpu_quantization/mapper_output_6500_gemm/ deploy_45dim_rl_gym/bpu_quantization/mapper_output_6500_gemm/
deploy_45dim_rl_gym/bpu_quantization/hb_mapper_*.log* deploy_45dim_rl_gym/bpu_quantization/hb_mapper_*.log*
deploy_45dim_rl_gym/bpu_quantization/policy_[0-9]*_actions.onnx
deploy_45dim_rl_gym/bpu_quantization/policy_[0-9]*_bpu4d.onnx
deploy_45dim_rl_gym/bpu_quantization/policy_[0-9]*_bpu4d_gemm.onnx
deploy_45dim_rl_gym/bpu_quantization/policy_[0-9]*_int16_gemm.yaml
deploy_45dim_rl_gym/bpu_quantization/policy_[0-9]*_opset11.onnx
deploy_45dim_rl_gym/bpu_quantization/policy_robotlab_15000_bpu4d.onnx deploy_45dim_rl_gym/bpu_quantization/policy_robotlab_15000_bpu4d.onnx
deploy_45dim_rl_gym/bpu_quantization/policy_robotlab_15000_bpu4d_gemm.onnx deploy_45dim_rl_gym/bpu_quantization/policy_robotlab_15000_bpu4d_gemm.onnx
deploy_45dim_rl_gym/bpu_quantization/policy_robotlab_15000_opset11.onnx deploy_45dim_rl_gym/bpu_quantization/policy_robotlab_15000_opset11.onnx

View File

@@ -8,6 +8,7 @@
- `deploy_go1_rlgym_pro_sdk_fastcpp.py`:真机 Go1 PRO 低层部署脚本,独立使用 C++ LowCmd 构包/加密后端 - `deploy_go1_rlgym_pro_sdk_fastcpp.py`:真机 Go1 PRO 低层部署脚本,独立使用 C++ LowCmd 构包/加密后端
- `deploy_go1_rlgym_pro_sdk_lab.py`RobotLab 策略真机部署脚本 - `deploy_go1_rlgym_pro_sdk_lab.py`RobotLab 策略真机部署脚本
- `deploy_go1_rlgym_pro_sdk_lab_fastcpp.py`RobotLab 策略真机部署脚本,独立使用 C++ LowCmd 构包/加密后端 - `deploy_go1_rlgym_pro_sdk_lab_fastcpp.py`RobotLab 策略真机部署脚本,独立使用 C++ LowCmd 构包/加密后端
- `deploy_go1_rlgym_bpu_x5_fastcpp.py`Gym 5 帧 BPU 真机部署脚本,使用 C++ LowCmd/LowState 热路径
该 ONNX 策略使用 45 维单帧观测,以及 5 帧、共 225 维的历史输入,按观测项分组堆叠: 该 ONNX 策略使用 45 维单帧观测,以及 5 帧、共 225 维的历史输入,按观测项分组堆叠:
@@ -52,6 +53,14 @@ cd /root/go1_pro_sdk/fast_lowcmd_cpp
PYTHONPATH=/root/go1_pro_sdk python3 setup.py build_ext --inplace PYTHONPATH=/root/go1_pro_sdk python3 setup.py build_ext --inplace
``` ```
Gym 5 帧 BPU 部署入口默认走 `policy_35k` 对应的 `mapper_output_35k_gemm/policy_35k_int16_gemm.bin`
也可以显式切换:
```bash
PYTHONPATH=/root/go1_pro_deploy python3 deploy_45dim_rl_gym/deploy_go1_rlgym_bpu_x5_fastcpp.py --bpu-round 35k
PYTHONPATH=/root/go1_pro_deploy python3 deploy_45dim_rl_gym/deploy_go1_rlgym_bpu_x5_fastcpp.py --bpu-round 30k
```
## 安全说明 ## 安全说明
直接低层控电机是危险操作。 直接低层控电机是危险操作。

View File

@@ -59,8 +59,6 @@ class BpuInferLibPolicy:
"""Default fast backend: direct C++ DNN API via ctypes.""" """Default fast backend: direct C++ DNN API via ctypes."""
backend_name = "cpp_dnn_api_x5" 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): def __init__(self, model_path, priority=0, bpu_cores=(0,), cpp_lib=DEFAULT_CPP_LIB):
self.model_path = Path(model_path).expanduser().resolve() self.model_path = Path(model_path).expanduser().resolve()
@@ -94,6 +92,10 @@ class BpuInferLibPolicy:
self._lib.rlgym_bpu_infer.restype = 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.argtypes = [ctypes.c_void_p]
self._lib.rlgym_bpu_destroy.restype = None self._lib.rlgym_bpu_destroy.restype = None
self._lib.rlgym_bpu_input_floats.argtypes = [ctypes.c_void_p]
self._lib.rlgym_bpu_input_floats.restype = ctypes.c_int
self._lib.rlgym_bpu_output_floats.argtypes = [ctypes.c_void_p]
self._lib.rlgym_bpu_output_floats.restype = ctypes.c_int
self._lib.rlgym_bpu_version.argtypes = [] self._lib.rlgym_bpu_version.argtypes = []
self._lib.rlgym_bpu_version.restype = ctypes.c_char_p self._lib.rlgym_bpu_version.restype = ctypes.c_char_p
@@ -107,7 +109,13 @@ class BpuInferLibPolicy:
) )
if not self._handle: if not self._handle:
raise RuntimeError(err.value.decode("utf-8", errors="replace")) raise RuntimeError(err.value.decode("utf-8", errors="replace"))
self._output = np.empty(12, dtype=np.float32) self.input_size = int(self._lib.rlgym_bpu_input_floats(self._handle))
self.output_size = int(self._lib.rlgym_bpu_output_floats(self._handle))
if self.input_size <= 0 or self.output_size <= 0:
raise RuntimeError("invalid BPU tensor sizes reported by runtime")
self.input_shape = (1, 1, 1, self.input_size)
self.output_shape = (1, self.output_size, 1, 1)
self._output = np.empty(self.output_size, dtype=np.float32)
self._output_ptr = self._output.ctypes.data_as(ctypes.POINTER(ctypes.c_float)) self._output_ptr = self._output.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
self._err = ctypes.create_string_buffer(1024) self._err = ctypes.create_string_buffer(1024)
@@ -131,8 +139,8 @@ class BpuInferLibPolicy:
def __call__(self, flat_input): def __call__(self, flat_input):
arr = np.asarray(flat_input, dtype=np.float32) arr = np.asarray(flat_input, dtype=np.float32)
if arr.size != 450: if arr.size != self.input_size:
raise ValueError(f"BPU policy input has {arr.size} values, expected 450") raise ValueError(f"BPU policy input has {arr.size} values, expected {self.input_size}")
input_flat = np.ascontiguousarray(arr.reshape(-1), dtype=np.float32) input_flat = np.ascontiguousarray(arr.reshape(-1), dtype=np.float32)
rc = self._lib.rlgym_bpu_infer( rc = self._lib.rlgym_bpu_infer(
self._handle, self._handle,
@@ -172,6 +180,9 @@ class BpuInferLibPythonPolicy:
self.priority = int(priority) self.priority = int(priority)
self.bpu_cores = tuple(int(core) for core in bpu_cores) self.bpu_cores = tuple(int(core) for core in bpu_cores)
self.suppress_runtime_output = bool(suppress_runtime_output) self.suppress_runtime_output = bool(suppress_runtime_output)
self.input_size = 450
self.input_shape = (1, 1, 1, self.input_size)
self.output_shape = (1, 12, 1, 1)
with suppress_c_output(self.suppress_runtime_output): with suppress_c_output(self.suppress_runtime_output):
self.infer = Infer(False) self.infer = Infer(False)
loaded = self.infer.load_model(str(self.model_path)) loaded = self.infer.load_model(str(self.model_path))
@@ -185,8 +196,8 @@ class BpuInferLibPythonPolicy:
def __call__(self, flat_input): def __call__(self, flat_input):
arr = np.asarray(flat_input, dtype=np.float32) arr = np.asarray(flat_input, dtype=np.float32)
if arr.size != 450: if arr.size != self.input_size:
raise ValueError(f"BPU policy input has {arr.size} values, expected 450") raise ValueError(f"BPU policy input has {arr.size} values, expected {self.input_size}")
input_4d = np.ascontiguousarray(arr.reshape(self.input_shape), dtype=np.float32) input_4d = np.ascontiguousarray(arr.reshape(self.input_shape), dtype=np.float32)
with suppress_c_output(self.suppress_runtime_output): with suppress_c_output(self.suppress_runtime_output):
copied = self.infer.read_numpy_arr_float32(input_4d, 0) copied = self.infer.read_numpy_arr_float32(input_4d, 0)

View File

@@ -12,8 +12,10 @@
extern "C" { extern "C" {
void *rlgym_bpu_create(const char *model_path, int bpu_core, int priority, void *rlgym_bpu_create(const char *model_path, int bpu_core, int priority,
char *err, int err_len); char *err, int err_len);
int rlgym_bpu_infer(void *handle, const float *input450, float *output12, char *err, int rlgym_bpu_infer(void *handle, const float *input, float *output, char *err,
int err_len); int err_len);
int rlgym_bpu_input_floats(void *handle);
int rlgym_bpu_output_floats(void *handle);
void rlgym_bpu_destroy(void *handle); void rlgym_bpu_destroy(void *handle);
} }
@@ -54,12 +56,6 @@ int main(int argc, char **argv) {
if (argc > 2) input = argv[2]; if (argc > 2) input = argv[2];
if (argc > 3) repeat = std::atoi(argv[3]); 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] = {}; char err[1024] = {};
void *handle = rlgym_bpu_create(model, 1, 0, err, sizeof(err)); void *handle = rlgym_bpu_create(model, 1, 0, err, sizeof(err));
if (handle == nullptr) { if (handle == nullptr) {
@@ -67,20 +63,37 @@ int main(int argc, char **argv) {
return 3; return 3;
} }
float out[12] = {}; const int input_floats = rlgym_bpu_input_floats(handle);
if (rlgym_bpu_infer(handle, obs.data(), out, err, sizeof(err)) != 0) { const int output_floats = rlgym_bpu_output_floats(handle);
if (input_floats <= 0 || output_floats <= 0) {
std::cerr << "invalid tensor sizes from BPU runtime\n";
rlgym_bpu_destroy(handle);
return 2;
}
std::vector<float> obs;
if (!read_f32_file(input, &obs) || static_cast<int>(obs.size()) != input_floats) {
std::cerr << "failed to read " << input_floats << " float32 input: " << input << "\n";
rlgym_bpu_destroy(handle);
return 2;
}
std::vector<float> out(static_cast<size_t>(output_floats), 0.0f);
if (rlgym_bpu_infer(handle, obs.data(), out.data(), err, sizeof(err)) != 0) {
std::cerr << err << "\n"; std::cerr << err << "\n";
rlgym_bpu_destroy(handle); rlgym_bpu_destroy(handle);
return 4; return 4;
} }
float max_diff = 0.0f; float max_diff = 0.0f;
for (int i = 0; i < 12; ++i) { if (input_floats == 450 && output_floats == 12) {
max_diff = std::max(max_diff, std::fabs(out[i] - kReference[i])); for (int i = 0; i < 12; ++i) {
max_diff = std::max(max_diff, std::fabs(out[static_cast<size_t>(i)] - kReference[i]));
}
} }
const auto t0 = std::chrono::steady_clock::now(); const auto t0 = std::chrono::steady_clock::now();
for (int i = 0; i < repeat; ++i) { for (int i = 0; i < repeat; ++i) {
if (rlgym_bpu_infer(handle, obs.data(), out, err, sizeof(err)) != 0) { if (rlgym_bpu_infer(handle, obs.data(), out.data(), err, sizeof(err)) != 0) {
std::cerr << err << "\n"; std::cerr << err << "\n";
rlgym_bpu_destroy(handle); rlgym_bpu_destroy(handle);
return 5; return 5;
@@ -90,10 +103,13 @@ int main(int argc, char **argv) {
const double elapsed_ms = const double elapsed_ms =
std::chrono::duration<double, std::milli>(t1 - t0).count(); std::chrono::duration<double, std::milli>(t1 - t0).count();
std::cout << "reference_max_abs_diff " << max_diff << "\n"; std::cout << "input_floats=" << input_floats << " output_floats=" << output_floats << "\n";
if (input_floats == 450 && output_floats == 12) {
std::cout << "reference_max_abs_diff " << max_diff << "\n";
}
std::cout << "repeat=" << repeat << " cpp_avg_ms=" << (elapsed_ms / repeat) << "\n"; std::cout << "repeat=" << repeat << " cpp_avg_ms=" << (elapsed_ms / repeat) << "\n";
std::cout << "action0=" << out[0] << " action_max_abs=" std::cout << "action0=" << out[0] << " action_max_abs="
<< *std::max_element(out, out + 12, [](float a, float b) { << *std::max_element(out.begin(), out.end(), [](float a, float b) {
return std::fabs(a) < std::fabs(b); return std::fabs(a) < std::fabs(b);
}) })
<< "\n"; << "\n";

View File

@@ -13,7 +13,6 @@
namespace { namespace {
constexpr int kInputFloats = 450;
constexpr int kOutputFloats = 12; constexpr int kOutputFloats = 12;
void set_error(char *err, int err_len, const std::string &msg) { void set_error(char *err, int err_len, const std::string &msg) {
@@ -88,10 +87,10 @@ class BpuDnnPolicy {
check_dnn(hbDNNGetOutputTensorProperties(&output_tensors_[0].properties, dnn_handle_, 0), check_dnn(hbDNNGetOutputTensorProperties(&output_tensors_[0].properties, dnn_handle_, 0),
"hbDNNGetOutputTensorProperties"); "hbDNNGetOutputTensorProperties");
validate_tensor(input_tensors_[0].properties, kInputFloats, HB_DNN_TENSOR_TYPE_F32, input_floats_ = element_count(input_tensors_[0].properties.validShape);
"input"); output_floats_ = element_count(output_tensors_[0].properties.validShape);
validate_tensor(output_tensors_[0].properties, kOutputFloats, HB_DNN_TENSOR_TYPE_F32, validate_tensor(input_tensors_[0].properties, HB_DNN_TENSOR_TYPE_F32, "input");
"output"); validate_tensor(output_tensors_[0].properties, HB_DNN_TENSOR_TYPE_F32, "output");
alloc_tensor_mem(input_tensors_[0]); alloc_tensor_mem(input_tensors_[0]);
alloc_tensor_mem(output_tensors_[0]); alloc_tensor_mem(output_tensors_[0]);
@@ -112,7 +111,7 @@ class BpuDnnPolicy {
} }
auto &input_mem = input_tensors_[0].sysMem[0]; auto &input_mem = input_tensors_[0].sysMem[0];
std::memcpy(input_mem.virAddr, input, sizeof(float) * kInputFloats); std::memcpy(input_mem.virAddr, input, sizeof(float) * input_floats_);
int code = hbSysFlushMem(&input_mem, HB_SYS_MEM_CACHE_CLEAN); int code = hbSysFlushMem(&input_mem, HB_SYS_MEM_CACHE_CLEAN);
if (code != 0) { if (code != 0) {
throw std::runtime_error(dnn_error("hbSysFlushMem(input)", code)); throw std::runtime_error(dnn_error("hbSysFlushMem(input)", code));
@@ -137,18 +136,15 @@ class BpuDnnPolicy {
if (code != 0) { if (code != 0) {
throw std::runtime_error(dnn_error("hbSysFlushMem(output)", code)); throw std::runtime_error(dnn_error("hbSysFlushMem(output)", code));
} }
std::memcpy(output, output_mem.virAddr, sizeof(float) * kOutputFloats); std::memcpy(output, output_mem.virAddr, sizeof(float) * output_floats_);
} }
int input_floats() const { return input_floats_; }
int output_floats() const { return output_floats_; }
private: private:
static void validate_tensor(const hbDNNTensorProperties &props, int expected_count, static void validate_tensor(const hbDNNTensorProperties &props, int expected_type,
int expected_type, const char *name) { 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) { if (props.tensorType != expected_type) {
std::ostringstream oss; std::ostringstream oss;
oss << name << " tensor type " << props.tensorType << " != " << expected_type; oss << name << " tensor type " << props.tensorType << " != " << expected_type;
@@ -182,6 +178,8 @@ class BpuDnnPolicy {
std::string model_name_; std::string model_name_;
std::vector<hbDNNTensor> input_tensors_; std::vector<hbDNNTensor> input_tensors_;
std::vector<hbDNNTensor> output_tensors_; std::vector<hbDNNTensor> output_tensors_;
int input_floats_{0};
int output_floats_{0};
int bpu_core_{HB_BPU_CORE_ANY}; int bpu_core_{HB_BPU_CORE_ANY};
int priority_{HB_DNN_PRIORITY_LOWEST}; int priority_{HB_DNN_PRIORITY_LOWEST};
}; };
@@ -201,14 +199,14 @@ void *rlgym_bpu_create(const char *model_path, int bpu_core, int priority,
} }
} }
int rlgym_bpu_infer(void *handle, const float *input450, float *output12, char *err, int rlgym_bpu_infer(void *handle, const float *input, float *output, char *err,
int err_len) { int err_len) {
try { try {
set_error(err, err_len, ""); set_error(err, err_len, "");
if (handle == nullptr) { if (handle == nullptr) {
throw std::runtime_error("null policy handle"); throw std::runtime_error("null policy handle");
} }
static_cast<BpuDnnPolicy *>(handle)->infer(input450, output12); static_cast<BpuDnnPolicy *>(handle)->infer(input, output);
return 0; return 0;
} catch (const std::exception &e) { } catch (const std::exception &e) {
set_error(err, err_len, e.what()); set_error(err, err_len, e.what());
@@ -216,6 +214,20 @@ int rlgym_bpu_infer(void *handle, const float *input450, float *output12, char *
} }
} }
int rlgym_bpu_input_floats(void *handle) {
if (handle == nullptr) {
return 0;
}
return static_cast<BpuDnnPolicy *>(handle)->input_floats();
}
int rlgym_bpu_output_floats(void *handle) {
if (handle == nullptr) {
return 0;
}
return static_cast<BpuDnnPolicy *>(handle)->output_floats();
}
void rlgym_bpu_destroy(void *handle) { void rlgym_bpu_destroy(void *handle) {
delete static_cast<BpuDnnPolicy *>(handle); delete static_cast<BpuDnnPolicy *>(handle);
} }

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env python3
"""Compatibility wrapper for the Gym 5-frame BPU deployment entrypoint."""
import runpy
from pathlib import Path
if __name__ == "__main__":
entry = Path(__file__).resolve().parents[1] / "deploy_go1_rlgym_bpu_x5_fastcpp.py"
runpy.run_path(str(entry), run_name="__main__")

View File

@@ -2,8 +2,8 @@
"""Offline BPU policy smoke test for RDK X5. """Offline BPU policy smoke test for RDK X5.
This does not connect to the robot. It loads a BPU .bin and one raw float32 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 input file, runs the C++ DNN API backend repeatedly, and optionally checks the
00000.bin reference output captured from hrt_model_exec. known RobotLab 00000.bin reference output captured from hrt_model_exec.
""" """
import argparse import argparse
@@ -47,15 +47,19 @@ def main():
input_path = Path(args.input_bin).expanduser().resolve() input_path = Path(args.input_bin).expanduser().resolve()
data = np.fromfile(input_path, dtype=np.float32) 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) policy = BpuInferLibPolicy(args.bpu_model)
if data.size != policy.input_size:
raise ValueError(
f"{input_path} has {data.size} float32 values, "
f"but model expects {policy.input_size}"
)
action = policy(data) action = policy(data)
print("action", np.array2string(action, precision=6)) print("action", np.array2string(action, precision=6))
print("action_max_abs", float(np.max(np.abs(action)))) print("action_max_abs", float(np.max(np.abs(action))))
if args.check_reference_00000: if args.check_reference_00000:
if policy.input_size != 450:
raise ValueError("--check-reference-00000 is only valid for the 450-dim RobotLab reference input")
diff = np.abs(action - REFERENCE_00000) diff = np.abs(action - REFERENCE_00000)
print("reference_max_abs_diff", float(diff.max())) print("reference_max_abs_diff", float(diff.max()))
print("reference_mean_abs_diff", float(diff.mean())) print("reference_mean_abs_diff", float(diff.mean()))

View File

@@ -1,8 +1,8 @@
# RDK X5 BPU 量化流程 # RDK X5 BPU 量化流程
这个目录用于把 `../policy_robotlab_15000.onnx` 转成 RDK X5 可运行的 这个目录用于把 Gym/RobotLab 的 ONNX 策略转成 RDK X5 可运行的 Horizon
Horizon runtime `.bin`。量化在 Mac 上用 CPU Docker 完成,板端做离线 runtime `.bin`。量化在 Mac 上用 CPU Docker 完成,板端可以做离线测速和实机
`hrt_model_exec` 验证,暂时不要直接接入实机控制 部署测试
参考资料: 参考资料:
@@ -10,14 +10,103 @@ Horizon runtime `.bin`。量化在 Mac 上用 CPU Docker 完成,板端只做
- D-Robotics 论坛 WTW/Go2/X5 流程:`https://forum.d-robotics.cc/t/topic/28338` - D-Robotics 论坛 WTW/Go2/X5 流程:`https://forum.d-robotics.cc/t/topic/28338`
官方工具链对 ONNX 的关键限制是:`ir_version <= 7``opset10/11`、固定 官方工具链对 ONNX 的关键限制是:`ir_version <= 7``opset10/11`、固定
4 维输入,且 N 维只能为 1。因此这里不能直接拿原始 RobotLab ONNX 编译, 4 维输入,且 N 维只能为 1。因此这里不能直接拿原始 ONNX 编译,需要先裁剪
需要先降级 opset再把 `[1, 450]` 输入包成固定 4D `NCHW` actions-only 输出、降级 opset再把 `[1, D]` 输入包成固定 4D `NCHW`
`[1, 1, 1, 450]` Gym 5 帧是 `[1, 1, 1, 225]`RobotLab 10 帧是 `[1, 1, 1, 450]`
## 当前状态 ## 当前状态
新增 Gym 5 帧策略的一键量化入口,默认目标是:
- 原始模型:`../policy_35k.onnx`
- 原始输入:`obs [1, 225]`
- BPU 编译输入:`obs_4d [1, 1, 1, 225]`
- BPU 输出:`actions [1, 12, 1, 1]`
- 默认输出:`mapper_output_35k_gemm/policy_35k_int16_gemm.bin`
- 默认校准数据:`calibration_data_35k_gym_fast64/`
本机 Docker 已完成一次默认量化:
- 浮点 4D/Gemm 图等价对比64 个真实样本上 `max_abs_diff = 7.15e-7`
- `hb_mapper makertbin` 输出:`actions` cosine `0.998524`L1 `0.014313`L2 `0.005103`Chebyshev `0.040004`
- 编译估计延迟:`463.9 us`
- 产物大小:`2.0M`
- 产物路径:`deploy_45dim_rl_gym/bpu_quantization/mapper_output_35k_gemm/policy_35k_int16_gemm.bin`
一键量化命令:
```bash
cd /Users/chenyouyuan/cyy_ws/deploy_go1_pro/deploy_45dim_rl_gym/bpu_quantization
./quantize_policy_x5.sh
```
切换其他 Gym 轮次时直接指定模型和 round
```bash
./quantize_policy_x5.sh --policy ../policy_30k.onnx --round 30k
./quantize_policy_x5.sh --policy ../policy_25k.onnx --round 25k
./quantize_policy_x5.sh --policy ../policy_15k.onnx --round 15k
```
脚本默认只抽 64 个真实 RL 样本做校准和最多 64 个样本做浮点等价对比,避免
之前 512 样本和 batch 回退导致的量化流程过慢。需要更稳的校准时再手动加大:
```bash
./quantize_policy_x5.sh --samples 128 --compare-limit 128
```
Gym BPU 部署入口支持快速切换轮次:
```bash
cd /root/go1_pro_deploy
PYTHONPATH=/root/go1_pro_deploy python3 deploy_45dim_rl_gym/deploy_go1_rlgym_bpu_x5_fastcpp.py \
--bpu-round 35k \
--kill-sport \
--enable-rl \
--log-dir logs \
--kp 32 --kd 1.0 \
--kp-cal 20 --kd-cal 1.0 \
--power-factor 9 \
--position-protect-limit 0.0 \
--action-clip 6.5 \
--action-trip-limit 8.0 \
--action-hard-trip-limit 16.0 \
--max-target-step 0.0 \
--max-roll-deg 50 \
--max-pitch-deg 50 \
--swap-vy-yaw \
--rc-vx-scale 0.5 \
--rc-vy-scale 0.5 \
--rc-wz-scale 1.0 \
--log-timing
```
也可以直接指定 bin
```bash
PYTHONPATH=/root/go1_pro_deploy python3 deploy_45dim_rl_gym/deploy_go1_rlgym_bpu_x5_fastcpp.py \
--bpu-model deploy_45dim_rl_gym/bpu_quantization/mapper_output_35k_gemm/policy_35k_int16_gemm.bin \
--infer-check --max-steps 1000 --log-timing
```
板端离线测速:
```bash
cd /root/go1_pro_deploy
PYTHONPATH=/root/go1_pro_deploy python3 deploy_45dim_rl_gym/bpu_deploy_x5/test_bpu_policy.py \
--bpu-model deploy_45dim_rl_gym/bpu_quantization/mapper_output_35k_gemm/policy_35k_int16_gemm.bin \
--input-bin deploy_45dim_rl_gym/bpu_quantization/calibration_data_35k_gym_fast64/00000.bin \
--repeat 1000
cd /root/go1_pro_deploy/deploy_45dim_rl_gym/bpu_deploy_x5/cpp
./bpu_dnn_bench \
/root/go1_pro_deploy/deploy_45dim_rl_gym/bpu_quantization/mapper_output_35k_gemm/policy_35k_int16_gemm.bin \
/root/go1_pro_deploy/deploy_45dim_rl_gym/bpu_quantization/calibration_data_35k_gym_fast64/00000.bin \
1000
```
已经完成 `policy_robotlab_15000.onnx``policy_robotlab_6500.onnx` 的 int16 已经完成 `policy_robotlab_15000.onnx``policy_robotlab_6500.onnx` 的 int16
量化。当前 BPU 部署默认使用 6500 版本: 量化。RobotLab BPU 部署默认使用 6500 版本:
- 原始模型:`../policy_robotlab_6500.onnx` - 原始模型:`../policy_robotlab_6500.onnx`
- 原始输入:`obs [1, 450]` - 原始输入:`obs [1, 450]`

View File

@@ -8,11 +8,17 @@ import numpy as np
import onnxruntime as ort import onnxruntime as ort
def load_samples(calibration_dir, limit): def load_samples(calibration_dir, limit, flat_dim):
paths = sorted(Path(calibration_dir).glob("*.bin"))[:limit] paths = sorted(Path(calibration_dir).glob("*.bin"))[:limit]
if not paths: if not paths:
raise FileNotFoundError(f"No calibration .bin files found in {calibration_dir}") raise FileNotFoundError(f"No calibration .bin files found in {calibration_dir}")
return [np.fromfile(path, dtype=np.float32).reshape(1, 450) for path in paths] samples = []
for path in paths:
sample = np.fromfile(path, dtype=np.float32)
if sample.size != flat_dim:
raise ValueError(f"{path} has {sample.size} float32 values, expected {flat_dim}")
samples.append(sample.reshape(1, flat_dim))
return samples
def main(): def main():
@@ -20,6 +26,7 @@ def main():
parser.add_argument("--flat-onnx", type=Path, required=True) parser.add_argument("--flat-onnx", type=Path, required=True)
parser.add_argument("--bpu4d-onnx", type=Path, required=True) parser.add_argument("--bpu4d-onnx", type=Path, required=True)
parser.add_argument("--calibration-dir", type=Path, default=Path("calibration_data")) parser.add_argument("--calibration-dir", type=Path, default=Path("calibration_data"))
parser.add_argument("--flat-dim", type=int, default=450)
parser.add_argument("--limit", type=int, default=64) parser.add_argument("--limit", type=int, default=64)
args = parser.parse_args() args = parser.parse_args()
@@ -30,9 +37,9 @@ def main():
max_abs = 0.0 max_abs = 0.0
max_mean_abs = 0.0 max_mean_abs = 0.0
for sample in load_samples(args.calibration_dir, args.limit): for sample in load_samples(args.calibration_dir, args.limit, args.flat_dim):
out_flat = flat.run(None, {flat_input: sample})[0] out_flat = flat.run(None, {flat_input: sample})[0]
out_wrapped = wrapped.run(None, {wrapped_input: sample.reshape(1, 1, 1, 450)})[0] out_wrapped = wrapped.run(None, {wrapped_input: sample.reshape(1, 1, 1, args.flat_dim)})[0]
diff = np.abs(out_flat - out_wrapped) diff = np.abs(out_flat - out_wrapped)
max_abs = max(max_abs, float(diff.max())) max_abs = max(max_abs, float(diff.max()))
max_mean_abs = max(max_mean_abs, float(diff.mean())) max_mean_abs = max(max_mean_abs, float(diff.mean()))

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Keep only the actions output in a policy ONNX graph."""
import argparse
from pathlib import Path
import onnx
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--output-name", default="actions")
args = parser.parse_args()
model = onnx.load(str(args.input))
outputs = list(model.graph.output)
if not outputs:
raise ValueError("ONNX graph has no outputs")
selected = None
for output in outputs:
if output.name == args.output_name:
selected = output
break
if selected is None:
selected = outputs[0]
print(f"[WARN] output {args.output_name!r} not found; keeping first output {selected.name!r}")
del model.graph.output[:]
model.graph.output.append(selected)
args.output.parent.mkdir(parents=True, exist_ok=True)
onnx.save(model, str(args.output))
print(f"Wrote actions-only ONNX: {args.output}")
if __name__ == "__main__":
main()

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Build float32 BPU calibration inputs from recorded RobotLab deployment logs.""" """Build float32 BPU calibration inputs from recorded deployment logs."""
import argparse import argparse
import json import json
@@ -11,16 +11,14 @@ import numpy as np
NUM_OBS = 45 NUM_OBS = 45
HISTORY_LEN = 10
ONNX_INPUT_DIM = NUM_OBS * HISTORY_LEN
TERM_DIMS = (3, 3, 3, 12, 12, 12) TERM_DIMS = (3, 3, 3, 12, 12, 12)
HERE = Path(__file__).resolve().parent HERE = Path(__file__).resolve().parent
DEFAULT_LOG_ROOT = HERE.parents[1] / "logs" DEFAULT_LOG_ROOT = HERE.parents[1] / "logs"
def build_onnx_input(history): def build_policy_input(history, history_len):
frames = list(history) frames = list(history)
while len(frames) < HISTORY_LEN: while len(frames) < history_len:
frames.insert(0, np.zeros(NUM_OBS, dtype=np.float32)) frames.insert(0, np.zeros(NUM_OBS, dtype=np.float32))
chunks = [] chunks = []
@@ -41,14 +39,14 @@ def reservoir_add(samples, value, seen, max_samples, rng):
samples[replace_index] = value samples[replace_index] = value
def collect_samples(log_paths, max_samples, seed): def collect_samples(log_paths, max_samples, seed, history_len):
rng = random.Random(seed) rng = random.Random(seed)
samples = [] samples = []
seen = 0 seen = 0
usable_runs = [] usable_runs = []
for steps_path in log_paths: for steps_path in log_paths:
history = deque(maxlen=HISTORY_LEN) history = deque(maxlen=history_len)
run_seen = 0 run_seen = 0
was_rl = False was_rl = False
@@ -71,13 +69,13 @@ def collect_samples(log_paths, max_samples, seed):
continue continue
history.append(obs) history.append(obs)
onnx_input = build_onnx_input(history) policy_input = build_policy_input(history, history_len)
if not np.all(np.isfinite(onnx_input)): if not np.all(np.isfinite(policy_input)):
continue continue
seen += 1 seen += 1
run_seen += 1 run_seen += 1
reservoir_add(samples, onnx_input, seen, max_samples, rng) reservoir_add(samples, policy_input, seen, max_samples, rng)
if run_seen: if run_seen:
usable_runs.append({"steps": str(steps_path), "samples": run_seen}) usable_runs.append({"steps": str(steps_path), "samples": run_seen})
@@ -87,14 +85,18 @@ def collect_samples(log_paths, max_samples, seed):
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Create BPU float32 calibration .bin files from RobotLab JSONL logs." description="Create BPU float32 calibration .bin files from JSONL deployment logs."
) )
parser.add_argument( parser.add_argument(
"--logs-root", "--logs-root",
type=Path, type=Path,
default=DEFAULT_LOG_ROOT, default=DEFAULT_LOG_ROOT,
help="Directory containing robotlab_go1_deploy_*/steps.jsonl.", help="Directory containing <log-prefix>_*/steps.jsonl.",
) )
parser.add_argument("--log-prefix", default="robotlab_go1_deploy",
help="Run directory prefix below --logs-root")
parser.add_argument("--history-len", type=int, default=10,
help="Number of 45-dim observations to stack by term")
parser.add_argument( parser.add_argument(
"--output-dir", "--output-dir",
type=Path, type=Path,
@@ -102,23 +104,28 @@ def main():
help="Output directory for raw float32 feature-map .bin files.", help="Output directory for raw float32 feature-map .bin files.",
) )
parser.add_argument("--max-samples", type=int, default=512) parser.add_argument("--max-samples", type=int, default=512)
parser.add_argument("--min-samples", type=int, default=32)
parser.add_argument("--seed", type=int, default=20260727) parser.add_argument("--seed", type=int, default=20260727)
parser.add_argument("--overwrite", action="store_true") parser.add_argument("--overwrite", action="store_true")
args = parser.parse_args() args = parser.parse_args()
if args.max_samples < 32: if args.max_samples < 1:
raise ValueError("--max-samples must be at least 32") raise ValueError("--max-samples must be positive")
if args.history_len < 1:
raise ValueError("--history-len must be positive")
log_paths = sorted(args.logs_root.glob("robotlab_go1_deploy_*/steps.jsonl")) log_paths = sorted(args.logs_root.glob(f"{args.log_prefix}_*/steps.jsonl"))
if not log_paths: if not log_paths:
raise FileNotFoundError(f"No RobotLab step logs found below {args.logs_root}") raise FileNotFoundError(
f"No step logs found for prefix {args.log_prefix!r} below {args.logs_root}"
)
samples, total_seen, usable_runs = collect_samples( samples, total_seen, usable_runs = collect_samples(
log_paths, args.max_samples, args.seed log_paths, args.max_samples, args.seed, args.history_len
) )
if len(samples) < 32: if len(samples) < args.min_samples:
raise RuntimeError( raise RuntimeError(
f"Only {len(samples)} valid RL inputs found; need at least 32 calibration samples." f"Only {len(samples)} valid RL inputs found; need at least {args.min_samples} calibration samples."
) )
output_dir = args.output_dir.resolve() output_dir = args.output_dir.resolve()
@@ -134,13 +141,15 @@ def main():
for index, sample in enumerate(samples): for index, sample in enumerate(samples):
sample.astype(np.float32, copy=False).tofile(output_dir / f"{index:05d}.bin") sample.astype(np.float32, copy=False).tofile(output_dir / f"{index:05d}.bin")
policy_input_dim = NUM_OBS * args.history_len
metadata = { metadata = {
"format": "raw float32 feature-map", "format": "raw float32 feature-map",
"flat_shape": [1, ONNX_INPUT_DIM], "flat_shape": [1, policy_input_dim],
"mapper_shape": [1, 1, 1, ONNX_INPUT_DIM], "mapper_shape": [1, 1, 1, policy_input_dim],
"history_len": HISTORY_LEN, "history_len": args.history_len,
"num_obs": NUM_OBS, "num_obs": NUM_OBS,
"term_dims": list(TERM_DIMS), "term_dims": list(TERM_DIMS),
"log_prefix": args.log_prefix,
"selected_samples": len(samples), "selected_samples": len(samples),
"candidate_rl_inputs": total_seen, "candidate_rl_inputs": total_seen,
"seed": args.seed, "seed": args.seed,

View File

@@ -0,0 +1,199 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
POLICY="../policy_35k.onnx"
ROUND="35k"
NAME=""
HISTORY_LEN=5
FLAT_DIM=""
SAMPLES=64
MIN_SAMPLES=32
LOG_PREFIX="rlgym_go1_deploy"
DOCKER_IMAGE="openexplorer/ai_toolchain_ubuntu_20_x5_cpu:v1.2.8"
COMPARE_LIMIT=64
RUN_CHECKER=1
usage() {
cat <<'EOF'
Usage:
./quantize_policy_x5.sh [options]
Default: quantize Gym policy_35k.onnx as 5-frame/225-dim int16 Gemm BPU model.
Options:
--policy PATH ONNX policy path, relative to this directory or absolute
--round NAME round label used in output paths, e.g. 15k/25k/30k/35k
--name NAME model basename; default is policy filename without .onnx
--history-len N observation history length; Gym=5, RobotLab=10
--flat-dim N flat input dim; default 45 * history-len
--samples N calibration sample count; default 64 for faster mapping
--min-samples N minimum valid samples required; default 32
--log-prefix PREFIX log dir prefix below logs/, default rlgym_go1_deploy
--docker-image IMAGE D-Robotics CPU toolchain image
--compare-limit N float ONNX equivalence sample count, default 64
--skip-checker skip hb_mapper checker before makertbin
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--policy) POLICY="$2"; shift 2 ;;
--round) ROUND="$2"; shift 2 ;;
--name) NAME="$2"; shift 2 ;;
--history-len) HISTORY_LEN="$2"; shift 2 ;;
--flat-dim) FLAT_DIM="$2"; shift 2 ;;
--samples) SAMPLES="$2"; shift 2 ;;
--min-samples) MIN_SAMPLES="$2"; shift 2 ;;
--log-prefix) LOG_PREFIX="$2"; shift 2 ;;
--docker-image) DOCKER_IMAGE="$2"; shift 2 ;;
--compare-limit) COMPARE_LIMIT="$2"; shift 2 ;;
--skip-checker) RUN_CHECKER=0; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
if [[ -z "${FLAT_DIM}" ]]; then
FLAT_DIM=$((45 * HISTORY_LEN))
fi
if [[ "${POLICY}" = /* ]]; then
POLICY_ABS="${POLICY}"
else
POLICY_ABS="${SCRIPT_DIR}/${POLICY}"
fi
POLICY_ABS="$(cd "$(dirname "${POLICY_ABS}")" && pwd)/$(basename "${POLICY_ABS}")"
if [[ ! -f "${POLICY_ABS}" ]]; then
echo "Policy not found: ${POLICY_ABS}" >&2
exit 2
fi
case "${POLICY_ABS}" in
"${REPO_ROOT}"/*) POLICY_REL="${POLICY_ABS#${REPO_ROOT}/}" ;;
*) echo "Policy must be inside repo root ${REPO_ROOT}: ${POLICY_ABS}" >&2; exit 2 ;;
esac
if [[ -z "${NAME}" ]]; then
NAME="$(basename "${POLICY_ABS}" .onnx)"
fi
CAL_DIR="calibration_data_${ROUND}_gym_fast${SAMPLES}"
OUTPUT_DIR="mapper_output_${ROUND}_gemm"
OUTPUT_PREFIX="${NAME}_int16_gemm"
YAML_FILE="${OUTPUT_PREFIX}.yaml"
echo "[INFO] repo : ${REPO_ROOT}"
echo "[INFO] policy : ${POLICY_REL}"
echo "[INFO] name/round : ${NAME} / ${ROUND}"
echo "[INFO] history/shape : ${HISTORY_LEN} / 1x1x1x${FLAT_DIM}"
echo "[INFO] calibration : ${CAL_DIR} (${SAMPLES} samples, prefix ${LOG_PREFIX})"
echo "[INFO] output : ${OUTPUT_DIR}/${OUTPUT_PREFIX}.bin"
docker run --rm --platform linux/amd64 \
-e POLICY_REL="${POLICY_REL}" \
-e NAME="${NAME}" \
-e HISTORY_LEN="${HISTORY_LEN}" \
-e FLAT_DIM="${FLAT_DIM}" \
-e SAMPLES="${SAMPLES}" \
-e MIN_SAMPLES="${MIN_SAMPLES}" \
-e LOG_PREFIX="${LOG_PREFIX}" \
-e CAL_DIR="${CAL_DIR}" \
-e OUTPUT_DIR="${OUTPUT_DIR}" \
-e OUTPUT_PREFIX="${OUTPUT_PREFIX}" \
-e YAML_FILE="${YAML_FILE}" \
-e COMPARE_LIMIT="${COMPARE_LIMIT}" \
-e RUN_CHECKER="${RUN_CHECKER}" \
-v "${REPO_ROOT}:/workspace/deploy_go1_pro" \
"${DOCKER_IMAGE}" \
bash -lc '
set -euo pipefail
cd /workspace/deploy_go1_pro/deploy_45dim_rl_gym/bpu_quantization
POLICY="/workspace/deploy_go1_pro/${POLICY_REL}"
ACTIONS_ONNX="${NAME}_actions.onnx"
OPSET_ONNX="${NAME}_opset11.onnx"
BPU4D_ONNX="${NAME}_bpu4d.onnx"
GEMM_ONNX="${NAME}_bpu4d_gemm.onnx"
python3 make_calibration_data.py \
--logs-root ../../logs \
--log-prefix "${LOG_PREFIX}" \
--history-len "${HISTORY_LEN}" \
--output-dir "${CAL_DIR}" \
--max-samples "${SAMPLES}" \
--min-samples "${MIN_SAMPLES}" \
--overwrite
python3 keep_actions_output.py \
--input "${POLICY}" \
--output "${ACTIONS_ONNX}"
python3 downgrade_policy_to_opset11.py \
--input "${ACTIONS_ONNX}" \
--output "${OPSET_ONNX}"
python3 make_bpu_4d_onnx.py \
--input "${OPSET_ONNX}" \
--output "${BPU4D_ONNX}" \
--flat-dim "${FLAT_DIM}"
python3 replace_group_conv_with_gemm.py \
--input "${BPU4D_ONNX}" \
--output "${GEMM_ONNX}"
python3 compare_4d_onnx.py \
--flat-onnx "${ACTIONS_ONNX}" \
--bpu4d-onnx "${GEMM_ONNX}" \
--calibration-dir "${CAL_DIR}" \
--flat-dim "${FLAT_DIM}" \
--limit "${COMPARE_LIMIT}"
cat > "${YAML_FILE}" <<YAML
model_parameters:
onnx_model: "./${GEMM_ONNX}"
march: "bayes-e"
layer_out_dump: false
working_dir: "${OUTPUT_DIR}"
output_model_file_prefix: "${OUTPUT_PREFIX}"
input_parameters:
input_name: "obs_4d"
input_shape: "1x1x1x${FLAT_DIM}"
input_type_rt: "featuremap"
input_layout_rt: "NCHW"
input_type_train: "featuremap"
input_layout_train: "NCHW"
norm_type: "no_preprocess"
calibration_parameters:
cal_data_dir: "./${CAL_DIR}"
cal_data_type: "float32"
calibration_type: "default"
optimization: "set_all_nodes_int16"
per_channel: true
compiler_parameters:
compile_mode: "latency"
debug: false
optimize_level: "O3"
YAML
if [[ "${RUN_CHECKER}" = "1" ]]; then
hb_mapper checker \
--model "${GEMM_ONNX}" \
--model-type onnx \
--march bayes-e \
--input-shape obs_4d "1x1x1x${FLAT_DIM}"
fi
hb_mapper makertbin \
--config "${YAML_FILE}" \
--model-type onnx
ls -lh "${OUTPUT_DIR}/${OUTPUT_PREFIX}.bin"
'
echo "[INFO] Done: deploy_45dim_rl_gym/bpu_quantization/${OUTPUT_DIR}/${OUTPUT_PREFIX}.bin"

View File

@@ -48,7 +48,17 @@ def replace_node(model):
conv = next((node for node in nodes if node.name == CONV_NAME), None) conv = next((node for node in nodes if node.name == CONV_NAME), None)
if conv is None: if conv is None:
raise ValueError(f"Cannot find node {CONV_NAME!r}") candidates = [
node for node in nodes
if node.op_type == "Conv"
and int(attr_value(node, "group", 1)) > 1
and attr_value(node, "kernel_shape") == [1]
]
if len(candidates) == 1:
conv = candidates[0]
print(f"[WARN] {CONV_NAME!r} not found; using grouped Conv {conv.name!r}")
if conv is None:
raise ValueError(f"Cannot find unique grouped 1x1 Conv node; fixed name {CONV_NAME!r} not found")
if conv.op_type != "Conv": if conv.op_type != "Conv":
raise ValueError(f"{CONV_NAME!r} is {conv.op_type}, expected Conv") raise ValueError(f"{CONV_NAME!r} is {conv.op_type}, expected Conv")

File diff suppressed because it is too large Load Diff