From 2745fcf45ae30d197256fa8fbcf33652f7d8e1c0 Mon Sep 17 00:00:00 2001
From: gongbaojiding-1123
<310524940+gongbaojiding-1123@users.noreply.github.com>
Date: Thu, 22 Oct 2026 21:16:49 +0800
Subject: [PATCH 1/8] feat(rma): add locomotion pipeline and reproducible
experiment portfolio
---
.gitignore | 10 +
README.md | 4 +
README_zh.md | 3 +
deploy_cpp/CMakeLists.txt | 41 +
deploy_cpp/include/DoubleBuffer.hpp | 54 +
deploy_cpp/include/TensorRTPolicy.hpp | 80 +
deploy_cpp/src/main_control_loop.cpp | 192 ++
deploy_cpp/src/policy_worker.cpp | 52 +
deploy_cpp/tests/test_double_buffer.cpp | 39 +
docs/rma/README.md | 115 +
docs/rma/REPRODUCING.md | 60 +
docs/rma/THIRD_PARTY_NOTICES.md | 14 +
docs/rma/VALIDATION.md | 41 +
.../rma/EXP-000_teacher_full_baseline.md | 54 +
experiments/rma/EXP-001_forward_v1.md | 49 +
experiments/rma/EXP-002_diagnosis_plan.md | 40 +
experiments/rma/TEMPLATE.md | 15 +
legged_gym/go2_config.py | 10 +
requirements-rma-export.txt | 4 +
results/rma/baseline/seed123.json | 61 +
results/rma/baseline/seed456.json | 61 +
results/rma/baseline/seed789.json | 61 +
results/rma/evaluation_summary.csv | 7 +
results/rma/figures/forward_v1_training.png | Bin 0 -> 291350 bytes
results/rma/figures/velocity_comparison.png | Bin 0 -> 76934 bytes
.../rma/forward_v1/checkpoint_metadata.json | 20 +
.../rma/forward_v1/config_snapshot/base.yaml | 239 ++
.../config_snapshot/composed_unresolved.yaml | 438 +++
.../forward_v1/config_snapshot/mujoco.yaml | 29 +
.../rma/forward_v1/config_snapshot/rma.yaml | 60 +
.../config_snapshot/rma_forward.yaml | 20 +
results/rma/forward_v1/run_configuration.json | 35 +
results/rma/forward_v1/seed123.json | 62 +
results/rma/forward_v1/seed456.json | 62 +
results/rma/forward_v1/seed789.json | 62 +
results/rma/forward_v1/training_metrics.csv | 3001 +++++++++++++++++
results/rma/forward_v1/training_summary.json | 37 +
results/rma/provenance.json | 91 +
rsl_rl/unilab_rma/__init__.py | 4 +
rsl_rl/unilab_rma/actor_critic_rma.py | 81 +
rsl_rl/unilab_rma/adaptation_module.py | 77 +
rsl_rl/unilab_rma/deployment.py | 16 +
rsl_rl/unilab_rma/env_adapter.py | 57 +
rsl_rl/unilab_rma/evaluation_diagnostics.py | 83 +
rsl_rl/unilab_rma/experiment_report.py | 114 +
rsl_rl/unilab_rma/ppo.py | 47 +
rsl_rl/unilab_rma/rendering.py | 110 +
rsl_rl/unilab_rma/runner.py | 146 +
rsl_rl/unilab_rma/scene_contract.py | 46 +
rsl_rl/unilab_rma/tensorrt_runtime.py | 129 +
rsl_rl/unilab_rma/training_log.py | 115 +
scripts/evaluate_rma.py | 212 ++
scripts/export_onnx.py | 99 +
scripts/report_rma.py | 22 +
scripts/train_rma.py | 104 +
.../conf/ppo/task/go2_joystick_flat/rma.yaml | 60 +
.../task/go2_joystick_flat/rma_forward.yaml | 20 +
src/unilab/envs/mdp/rma.py | 54 +
tests/rma/test_core.py | 191 ++
tests/rma/test_evaluation_diagnostics.py | 94 +
tests/rma/test_experiment_report.py | 24 +
tests/rma/test_rendering.py | 315 ++
tests/rma/test_tensorrt_runtime.py | 120 +
tests/rma/test_training_log.py | 98 +
64 files changed, 7661 insertions(+)
create mode 100644 deploy_cpp/CMakeLists.txt
create mode 100644 deploy_cpp/include/DoubleBuffer.hpp
create mode 100644 deploy_cpp/include/TensorRTPolicy.hpp
create mode 100644 deploy_cpp/src/main_control_loop.cpp
create mode 100644 deploy_cpp/src/policy_worker.cpp
create mode 100644 deploy_cpp/tests/test_double_buffer.cpp
create mode 100644 docs/rma/README.md
create mode 100644 docs/rma/REPRODUCING.md
create mode 100644 docs/rma/THIRD_PARTY_NOTICES.md
create mode 100644 docs/rma/VALIDATION.md
create mode 100644 experiments/rma/EXP-000_teacher_full_baseline.md
create mode 100644 experiments/rma/EXP-001_forward_v1.md
create mode 100644 experiments/rma/EXP-002_diagnosis_plan.md
create mode 100644 experiments/rma/TEMPLATE.md
create mode 100644 legged_gym/go2_config.py
create mode 100644 requirements-rma-export.txt
create mode 100644 results/rma/baseline/seed123.json
create mode 100644 results/rma/baseline/seed456.json
create mode 100644 results/rma/baseline/seed789.json
create mode 100644 results/rma/evaluation_summary.csv
create mode 100644 results/rma/figures/forward_v1_training.png
create mode 100644 results/rma/figures/velocity_comparison.png
create mode 100644 results/rma/forward_v1/checkpoint_metadata.json
create mode 100644 results/rma/forward_v1/config_snapshot/base.yaml
create mode 100644 results/rma/forward_v1/config_snapshot/composed_unresolved.yaml
create mode 100644 results/rma/forward_v1/config_snapshot/mujoco.yaml
create mode 100644 results/rma/forward_v1/config_snapshot/rma.yaml
create mode 100644 results/rma/forward_v1/config_snapshot/rma_forward.yaml
create mode 100644 results/rma/forward_v1/run_configuration.json
create mode 100644 results/rma/forward_v1/seed123.json
create mode 100644 results/rma/forward_v1/seed456.json
create mode 100644 results/rma/forward_v1/seed789.json
create mode 100644 results/rma/forward_v1/training_metrics.csv
create mode 100644 results/rma/forward_v1/training_summary.json
create mode 100644 results/rma/provenance.json
create mode 100644 rsl_rl/unilab_rma/__init__.py
create mode 100644 rsl_rl/unilab_rma/actor_critic_rma.py
create mode 100644 rsl_rl/unilab_rma/adaptation_module.py
create mode 100644 rsl_rl/unilab_rma/deployment.py
create mode 100644 rsl_rl/unilab_rma/env_adapter.py
create mode 100644 rsl_rl/unilab_rma/evaluation_diagnostics.py
create mode 100644 rsl_rl/unilab_rma/experiment_report.py
create mode 100644 rsl_rl/unilab_rma/ppo.py
create mode 100644 rsl_rl/unilab_rma/rendering.py
create mode 100644 rsl_rl/unilab_rma/runner.py
create mode 100644 rsl_rl/unilab_rma/scene_contract.py
create mode 100644 rsl_rl/unilab_rma/tensorrt_runtime.py
create mode 100644 rsl_rl/unilab_rma/training_log.py
create mode 100644 scripts/evaluate_rma.py
create mode 100644 scripts/export_onnx.py
create mode 100644 scripts/report_rma.py
create mode 100644 scripts/train_rma.py
create mode 100644 src/unilab/conf/ppo/task/go2_joystick_flat/rma.yaml
create mode 100644 src/unilab/conf/ppo/task/go2_joystick_flat/rma_forward.yaml
create mode 100644 src/unilab/envs/mdp/rma.py
create mode 100644 tests/rma/test_core.py
create mode 100644 tests/rma/test_evaluation_diagnostics.py
create mode 100644 tests/rma/test_experiment_report.py
create mode 100644 tests/rma/test_rendering.py
create mode 100644 tests/rma/test_tensorrt_runtime.py
create mode 100644 tests/rma/test_training_log.py
diff --git a/.gitignore b/.gitignore
index 36ea87ed9..f63c2ba4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,15 @@
# Python
__pycache__/
+
+# RMA experiment runtime/build artifacts. Curated small reports live in results/rma/.
+build/
+build-*/
+*.pt
+*.pth
+*.onnx
+*.engine
+*.plan
+events.out.tfevents.*
dist/
.eggs/
*.egg-info/
diff --git a/README.md b/README.md
index 2fbfad7e6..7d608d776 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,10 @@ Contract-driven infrastructure for robot learning across physics backends and ha
Languages: English | 简体中文
+> **Fork experiment: Go2 RMA locomotion.** See the [experiment portfolio](docs/rma/README.md)
+> for teacher/student code, reproducible evaluation data, learning curves and C++ mock deployment.
+> Current forward-speed tracking has not reached the target; failures and reproduction limits are documented.
+
diff --git a/README_zh.md b/README_zh.md
index 4654bd779..158c83643 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -6,6 +6,9 @@
语言:简体中文 | English
+> **本 fork 的 Go2 RMA 运动控制实验**:[查看项目展示与实验记录](docs/rma/README.md)。
+> 包含教师/学生代码、真实评估数据、训练曲线和 C++ mock 部署;当前前进速度跟踪尚未达标。
+
diff --git a/deploy_cpp/CMakeLists.txt b/deploy_cpp/CMakeLists.txt
new file mode 100644
index 000000000..aff515c28
--- /dev/null
+++ b/deploy_cpp/CMakeLists.txt
@@ -0,0 +1,41 @@
+cmake_minimum_required(VERSION 3.20)
+project(unilab_rma_deploy LANGUAGES CXX)
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+option(WITH_TENSORRT "Build TensorRT 10 policy inference" OFF)
+option(ENABLE_TSAN "Enable thread sanitizer on supported Unix compilers" OFF)
+find_package(Threads REQUIRED)
+add_executable(control_loop src/main_control_loop.cpp)
+add_executable(test_double_buffer tests/test_double_buffer.cpp)
+foreach(target control_loop test_double_buffer)
+ target_include_directories(${target} PRIVATE include)
+ target_link_libraries(${target} PRIVATE Threads::Threads)
+ if(MSVC)
+ target_compile_options(${target} PRIVATE /W4 /EHsc)
+ else()
+ target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic)
+ if(ENABLE_TSAN)
+ target_compile_options(${target} PRIVATE -fsanitize=thread -g)
+ target_link_options(${target} PRIVATE -fsanitize=thread)
+ endif()
+ endif()
+endforeach()
+if(WITH_TENSORRT)
+ find_package(CUDAToolkit REQUIRED)
+ find_path(TENSORRT_INCLUDE NvInfer.h HINTS ${TENSORRT_ROOT}/include REQUIRED)
+ find_library(TENSORRT_LIBRARY nvinfer HINTS ${TENSORRT_ROOT}/lib ${TENSORRT_ROOT}/lib64 REQUIRED)
+ target_compile_definitions(control_loop PRIVATE WITH_TENSORRT)
+ target_include_directories(control_loop PRIVATE ${TENSORRT_INCLUDE})
+ target_link_libraries(control_loop PRIVATE ${TENSORRT_LIBRARY} CUDA::cudart)
+ add_executable(policy_worker src/policy_worker.cpp)
+ target_include_directories(policy_worker PRIVATE include ${TENSORRT_INCLUDE})
+ target_link_libraries(policy_worker PRIVATE ${TENSORRT_LIBRARY} CUDA::cudart)
+ if(MSVC)
+ target_compile_options(policy_worker PRIVATE /W4 /EHsc)
+ else()
+ target_compile_options(policy_worker PRIVATE -Wall -Wextra -Wpedantic)
+ endif()
+endif()
+enable_testing()
+add_test(NAME double_buffer_consistency COMMAND test_double_buffer)
+add_test(NAME mock_control_loop COMMAND control_loop --seconds 1)
diff --git a/deploy_cpp/include/DoubleBuffer.hpp b/deploy_cpp/include/DoubleBuffer.hpp
new file mode 100644
index 000000000..9d475c208
--- /dev/null
+++ b/deploy_cpp/include/DoubleBuffer.hpp
@@ -0,0 +1,54 @@
+#pragma once
+#include
+#include
+#include
+
+// Single producer / single consumer, latest-value mailbox, exactly two payload slots.
+// Publication alone is insufficient: slot ownership prevents payload data races.
+// Nonblocking try operations: caller retains last valid value on read failure.
+template class DoubleBuffer {
+ static_assert(std::is_trivially_copyable_v, "Payload must be trivially copyable");
+ static_assert(std::atomic::is_always_lock_free, "Requires lock-free atomic");
+ enum { Free=0, Writing=1, Ready=2, Reading=3 };
+ struct alignas(64) Slot {
+ std::atomic state{Free};
+ T value{};
+ };
+ std::array slots_{};
+ std::atomic published_{-1};
+public:
+ bool tryWrite(const T& value) noexcept {
+ const int previous=published_.load(std::memory_order_acquire);
+ const int first=previous<0?0:1-previous;
+ for(int attempt=0;attempt<2;++attempt){
+ const int index=attempt==0?first:1-first;
+ auto& slot=slots_[index];
+ int state=slot.state.load(std::memory_order_acquire);
+ if(state!=Free && state!=Ready) continue;
+ if(!slot.state.compare_exchange_strong(state,Writing,
+ std::memory_order_acq_rel,std::memory_order_relaxed)) continue;
+ slot.value=value;
+ // Announce while Writing, then release the payload. A reader of an
+ // old index cannot consume a recycled slot before its publication;
+ // Ready acquire also orders the publication before its next read.
+ published_.store(index,std::memory_order_release);
+ slot.state.store(Ready,std::memory_order_release);
+ return true;
+ }
+ return false;
+ }
+ bool tryRead(T& value) noexcept {
+ const int index=published_.load(std::memory_order_acquire);
+ if(index<0) return false;
+ auto& slot=slots_[index];
+ int state=Ready;
+ if(!slot.state.compare_exchange_strong(state,Reading,
+ std::memory_order_acq_rel,std::memory_order_relaxed)) return false;
+ value=slot.value;
+ slot.state.store(Free,std::memory_order_release);
+ return true;
+ }
+ DoubleBuffer()=default;
+ DoubleBuffer(const DoubleBuffer&)=delete;
+ DoubleBuffer& operator=(const DoubleBuffer&)=delete;
+};
diff --git a/deploy_cpp/include/TensorRTPolicy.hpp b/deploy_cpp/include/TensorRTPolicy.hpp
new file mode 100644
index 000000000..a6adfdf0b
--- /dev/null
+++ b/deploy_cpp/include/TensorRTPolicy.hpp
@@ -0,0 +1,80 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class TensorRTPolicy {
+ struct Logger : nvinfer1::ILogger {
+ void log(Severity severity,const char* msg) noexcept override {
+ if(severity<=Severity::kERROR) std::fprintf(stderr,"TensorRT: %s\n",msg);
+ }
+ } logger_;
+ std::unique_ptr runtime_;
+ std::unique_ptr engine_;
+ std::unique_ptr context_;
+ cudaStream_t stream_{};
+ void *obs_{},*history_{},*action_{};
+ static void check(cudaError_t result){
+ if(result!=cudaSuccess) throw std::runtime_error(cudaGetErrorString(result));
+ }
+ void cleanup() noexcept {
+ if(stream_) cudaStreamSynchronize(stream_);
+ if(obs_) cudaFree(obs_);
+ if(history_) cudaFree(history_);
+ if(action_) cudaFree(action_);
+ if(stream_) cudaStreamDestroy(stream_);
+ }
+ void require(const char* name,nvinfer1::TensorIOMode mode,std::initializer_list shape){
+ if(engine_->getTensorIOMode(name)!=mode || engine_->getTensorDataType(name)!=nvinfer1::DataType::kFLOAT ||
+ engine_->getTensorLocation(name)!=nvinfer1::TensorLocation::kDEVICE)
+ throw std::runtime_error(std::string("Engine IO type/mode mismatch: ")+name);
+ const auto dims=engine_->getTensorShape(name);
+ if(dims.nbDims!=static_cast(shape.size())) throw std::runtime_error("Engine rank mismatch");
+ int i=0;
+ for(int d:shape) if(dims.d[i++]!=d) throw std::runtime_error("Engine shape mismatch");
+ }
+public:
+ explicit TensorRTPolicy(const std::string& path){
+ try {
+ std::ifstream file(path,std::ios::binary);
+ if(!file) throw std::runtime_error("Cannot open TensorRT engine");
+ std::vector bytes((std::istreambuf_iterator(file)),{});
+ runtime_.reset(nvinfer1::createInferRuntime(logger_));
+ if(!runtime_) throw std::runtime_error("Cannot create TensorRT runtime");
+ engine_.reset(runtime_->deserializeCudaEngine(bytes.data(),bytes.size()));
+ if(!engine_) throw std::runtime_error("Cannot deserialize engine");
+ if(engine_->getNbIOTensors()!=3) throw std::runtime_error("Expected exactly 3 IO tensors");
+ require("observation",nvinfer1::TensorIOMode::kINPUT,{1,49});
+ require("history",nvinfer1::TensorIOMode::kINPUT,{1,50,49});
+ require("action",nvinfer1::TensorIOMode::kOUTPUT,{1,12});
+ context_.reset(engine_->createExecutionContext());
+ if(!context_) throw std::runtime_error("Cannot create execution context");
+ check(cudaStreamCreate(&stream_));
+ check(cudaMalloc(&obs_,49*sizeof(float)));
+ check(cudaMalloc(&history_,2450*sizeof(float)));
+ check(cudaMalloc(&action_,12*sizeof(float)));
+ if(!context_->setTensorAddress("observation",obs_) ||
+ !context_->setTensorAddress("history",history_) ||
+ !context_->setTensorAddress("action",action_)) throw std::runtime_error("Cannot bind IO");
+ } catch(...) { cleanup(); throw; }
+ }
+ ~TensorRTPolicy(){ cleanup(); }
+ void infer(const std::array& obs,const std::array& history,
+ std::array& action){
+ check(cudaMemcpyAsync(obs_,obs.data(),sizeof(obs),cudaMemcpyHostToDevice,stream_));
+ check(cudaMemcpyAsync(history_,history.data(),sizeof(history),cudaMemcpyHostToDevice,stream_));
+ if(!context_->enqueueV3(stream_)) throw std::runtime_error("enqueueV3 failed");
+ check(cudaMemcpyAsync(action.data(),action_,sizeof(action),cudaMemcpyDeviceToHost,stream_));
+ check(cudaStreamSynchronize(stream_));
+ }
+ TensorRTPolicy(const TensorRTPolicy&)=delete;
+ TensorRTPolicy& operator=(const TensorRTPolicy&)=delete;
+};
diff --git a/deploy_cpp/src/main_control_loop.cpp b/deploy_cpp/src/main_control_loop.cpp
new file mode 100644
index 000000000..d69873e57
--- /dev/null
+++ b/deploy_cpp/src/main_control_loop.cpp
@@ -0,0 +1,192 @@
+// Timing/IO framework. MOCK ROBOT ONLY: no Unitree SDK commands are sent.
+// C++17, TensorRT 10 optional, static FP32 IO with optional internal FP16 engine.
+#include "DoubleBuffer.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#ifdef WITH_TENSORRT
+#include "TensorRTPolicy.hpp"
+#endif
+
+using Clock=std::chrono::steady_clock;
+std::int64_t nowNs(){return std::chrono::duration_cast(Clock::now().time_since_epoch()).count();}
+struct Sensor {
+ // q,dq in joint order FL FR RL RR; q0 is supplied in actuator order FR FL RR RL.
+ std::array q{},dq{};
+ std::array gyro{},gravity{0,0,-1},command{};
+ std::int64_t stamp{};
+};
+struct Command {
+ std::array target{};
+ std::int64_t stamp{};
+ std::uint64_t sequence{};
+};
+struct ControlConfig {
+ std::array q0{},kp{},kd{},limit{};
+ ControlConfig(){kp.fill(35);kd.fill(.5);limit.fill(10);}
+ void load(const std::string& path){
+ std::ifstream f(path);
+ if(!f) throw std::runtime_error("Cannot read control config");
+ for(int i=0;i<12;++i){
+ if(!(f>>q0[i]>>kp[i]>>kd[i]>>limit[i]) || !std::isfinite(q0[i]) ||
+ !std::isfinite(kp[i]) || !std::isfinite(kd[i]) || !std::isfinite(limit[i]) ||
+ kp[i]<0 || kd[i]<0 || limit[i]<=0) throw std::runtime_error("Invalid control config row");
+ }
+ std::string extra;
+ if(f>>extra) throw std::runtime_error("Control config must contain exactly 12 rows of q0 kp kd torque_limit");
+ }
+};
+// Joint -> actuator index, based on copied YAML; verify against materialized scene before real use.
+constexpr std::array jointToAct{3,4,5,0,1,2,9,10,11,6,7,8};
+
+int main(int argc,char** argv){
+ try {
+ int seconds=5;
+ std::string enginePath, configPath;
+ for(int i=1;i3600) throw std::runtime_error("seconds must be 1..3600");
+ ControlConfig cfg;
+ if(!configPath.empty()) cfg.load(configPath);
+ if(!enginePath.empty() && configPath.empty()) throw std::runtime_error("Engine requires matching home/PD config");
+#ifdef WITH_TENSORRT
+ std::unique_ptr policy;
+ if(!enginePath.empty()){
+ policy=std::make_unique(enginePath);
+ // Trigger lazy CUDA/kernel initialization before timed control starts.
+ std::array warmObs{};
+ std::array warmHistory{};
+ std::array warmAction{};
+ policy->infer(warmObs,warmHistory,warmAction);
+ for(float a:warmAction) if(!std::isfinite(a)) throw std::runtime_error("Non-finite warm-up action");
+ std::cout<<"TensorRT engine loaded; IO checked; warm-up complete\n";
+ }
+#else
+ if(!enginePath.empty()) throw std::runtime_error("Rebuild with WITH_TENSORRT=ON");
+#endif
+ std::cout<<"MOCK ROBOT ONLY; "<<(enginePath.empty()?"zero-action mock policy":"TensorRT policy")
+ <<"; 50 Hz policy / 1000 Hz PD requested\n";
+ DoubleBuffer sensors;
+ DoubleBuffer commands;
+ std::atomic stop{false};
+ std::atomic fastTicks{0},slowTicks{0},stale{0},fastMiss{0},slowMiss{0};
+ // Thread-owned statistics; main reads them only after both joins.
+ std::uint64_t commandsPublished=0,commandsConsumed=0;
+ float maxActionAbs=0.f,maxTargetOffset=0.f,maxTorqueAbs=0.f;
+#ifdef WITH_TENSORRT
+ std::uint64_t inferenceCalls=0;
+ double inferenceTotalMs=0.,inferenceMaxMs=0.;
+#endif
+ std::string failure; // Written only by slow thread; read after join.
+ const auto end=Clock::now()+std::chrono::seconds(seconds);
+ std::thread fast([&]{
+ Sensor s;
+ for(int j=0;j<12;++j) s.q[j]=cfg.q0[jointToAct[j]];
+ Command active;
+ auto deadline=Clock::now();
+ while(!stop.load(std::memory_order_acquire) && Clock::now()active.sequence){
+ active=candidate;
+ ++commandsConsumed;
+ }
+ const bool fresh=active.sequence>0 && s.stamp>=active.stamp && s.stamp-active.stamp<100000000;
+ if(!fresh) ++stale;
+ for(int j=0;j<12;++j){
+ const int a=jointToAct[j];
+ float torque=fresh?cfg.kp[a]*(active.target[a]-s.q[j])-cfg.kd[a]*s.dq[j]:0.f;
+ torque=std::clamp(torque,-cfg.limit[a],cfg.limit[a]);
+ maxTorqueAbs=std::max(maxTorqueAbs,std::abs(torque));
+ // Mock plant, no gravity/contact. Replace with timestamped SDK sensor/command IO.
+ s.dq[j]+=(torque-.2f*s.dq[j])/.2f*.001f;
+ s.q[j]+=s.dq[j]*.001f;
+ }
+ ++fastTicks;
+ deadline+=std::chrono::milliseconds(1);
+ if(Clock::now()>deadline){++fastMiss;deadline=Clock::now();}
+ std::this_thread::sleep_until(deadline);
+ }
+ stop.store(true,std::memory_order_release);
+ });
+ std::thread slow([&]{
+ try {
+ std::array history{};
+ std::array previous{};
+ auto deadline=Clock::now();
+ std::uint64_t sequence=0;
+ float phase=0.f;
+ while(!stop.load(std::memory_order_acquire)){
+ Sensor s;
+ if(sensors.tryRead(s) && nowNs()-s.stamp<20000000){
+ std::array obs{};
+ for(int i=0;i<3;++i){obs[i]=s.gyro[i];obs[3+i]=s.gravity[i];obs[42+i]=s.command[i];}
+ for(int j=0;j<12;++j){obs[6+j]=s.q[j]-cfg.q0[jointToAct[j]];obs[18+j]=s.dq[j];obs[30+j]=previous[j];}
+ // Reset phase on process start. Real runtime must match environment phase resets.
+ constexpr float offsets[4]={0,.5f,.5f,0};
+ for(int i=0;i<4;++i) obs[45+i]=std::fmod(phase+offsets[i],1.f);
+ // Shift left explicitly; no overlapping std::copy ranges.
+ for(int i=0;i<2450-49;++i) history[i]=history[i+49];
+ std::copy(obs.begin(),obs.end(),history.end()-49);
+ std::array action{};
+#ifdef WITH_TENSORRT
+ if(policy){
+ const auto started=Clock::now();
+ policy->infer(obs,history,action);
+ const double elapsed=std::chrono::duration(Clock::now()-started).count();
+ ++inferenceCalls;
+ inferenceTotalMs+=elapsed;
+ inferenceMaxMs=std::max(inferenceMaxMs,elapsed);
+ }
+#endif
+ Command cmd;
+ cmd.stamp=s.stamp; // Sensor age includes inference latency.
+ cmd.sequence=++sequence;
+ for(int i=0;i<12;++i){
+ if(!std::isfinite(action[i])) throw std::runtime_error("Non-finite model action");
+ action[i]=std::clamp(action[i],-1.f,1.f);
+ cmd.target[i]=cfg.q0[i]+.25f*action[i];
+ maxActionAbs=std::max(maxActionAbs,std::abs(action[i]));
+ maxTargetOffset=std::max(maxTargetOffset,std::abs(cmd.target[i]-cfg.q0[i]));
+ }
+ if(commands.tryWrite(cmd)){
+ previous=action;
+ ++commandsPublished;
+ }
+ phase=std::fmod(phase+.04f,1.f); // Same float32 0.02s * 2Hz update as training.
+ ++slowTicks;
+ }
+ deadline+=std::chrono::milliseconds(20);
+ if(Clock::now()>deadline){++slowMiss;deadline=Clock::now();}
+ std::this_thread::sleep_until(deadline);
+ }
+ } catch(const std::exception& e){failure=e.what();stop.store(true,std::memory_order_release);}
+ });
+ fast.join();slow.join();
+ std::cout<<"PD ticks="<0) std::cout<<"TensorRT calls="<
+#include
+#include
+#include
+
+int main(int argc,char** argv){
+ static_assert(sizeof(float)==4 && std::numeric_limits::is_iec559,
+ "The worker protocol requires IEEE754 float32");
+ try {
+ if(argc!=3 || std::string(argv[1])!="--engine")
+ throw std::runtime_error("Usage: policy_worker --engine file.engine");
+ const std::uint32_t endian=1;
+ if(*reinterpret_cast(&endian)!=1)
+ throw std::runtime_error("The worker protocol requires little endian");
+ TensorRTPolicy policy(argv[2]);
+ std::array obs{};
+ std::array history{};
+ std::array action{};
+ policy.infer(obs,history,action); // Finish CUDA initialization before handshake.
+ for(float value:action) if(!std::isfinite(value))
+ throw std::runtime_error("Non-finite warm-up output");
+ const std::array dimensions{49,50,12};
+ std::cout.write("RMA1",4);
+ std::cout.write(reinterpret_cast(dimensions.data()),sizeof(dimensions));
+ std::cout.flush();
+ while(true){
+ std::cin.read(reinterpret_cast(obs.data()),sizeof(obs));
+ if(std::cin.gcount()==0 && std::cin.eof()) break;
+ if(!std::cin) throw std::runtime_error("Incomplete observation frame");
+ std::cin.read(reinterpret_cast(history.data()),sizeof(history));
+ if(!std::cin) throw std::runtime_error("Incomplete history frame");
+ for(float value:obs) if(!std::isfinite(value))
+ throw std::runtime_error("Non-finite observation input");
+ for(float value:history) if(!std::isfinite(value))
+ throw std::runtime_error("Non-finite history input");
+ policy.infer(obs,history,action);
+ for(float value:action) if(!std::isfinite(value))
+ throw std::runtime_error("Non-finite action output");
+ std::cout.write(reinterpret_cast(action.data()),sizeof(action));
+ std::cout.flush();
+ if(!std::cout) throw std::runtime_error("Cannot write action frame");
+ }
+ return 0;
+ } catch(const std::exception& error){
+ std::cerr<<"TensorRT worker: "<
+#include
+#include
+#include
+#include
+struct Payload {std::uint64_t sequence{};std::array values{};};
+int main(){
+ DoubleBuffer buffer;
+ Payload first;
+ if(buffer.tryRead(first)) return 1;
+ std::atomic done{false};
+ std::atomic errors{0};
+ std::uint64_t count=0;
+ std::thread writer([&]{
+ for(std::uint64_t i=1;i<=200000;++i){
+ Payload p;p.sequence=i;p.values.fill(i);
+ while(!buffer.tryWrite(p)) std::this_thread::yield();
+ }
+ done.store(true,std::memory_order_release);
+ });
+ std::thread reader([&]{
+ std::uint64_t last=0;
+ auto verify=[&](const Payload& p){
+ if(p.sequence<=last) ++errors;
+ for(auto v:p.values) if(v!=p.sequence) ++errors;
+ last=p.sequence;++count;
+ };
+ while(!done.load(std::memory_order_acquire)){
+ Payload p;
+ if(buffer.tryRead(p)) verify(p);
+ }
+ Payload p;
+ if(buffer.tryRead(p)) verify(p);
+ });
+ writer.join();reader.join();
+ std::cout<<"snapshots="<0?0:1;
+}
diff --git a/docs/rma/README.md b/docs/rma/README.md
new file mode 100644
index 000000000..bc1e4b421
--- /dev/null
+++ b/docs/rma/README.md
@@ -0,0 +1,115 @@
+# Go2 RMA Locomotion Lab
+
+基于 UniLab 的 Go2 强化学习运动控制实验:教师 PPO、历史观测适应、模型导出与 C++ 模拟部署。
+
+> 当前状态:工程组件已有测试与运行记录;教师尚未学会稳定跟踪 0.5 m/s 前进指令。
+> 本仓库保留失败结果和可复现的诊断,不声称完成鲁棒行走、真机部署或跨物理引擎泛化。
+
+## 这个项目解决什么问题
+
+目标是研究如何让四足机器人按速度指令运动,以及如何通过历史观测估计摩擦、负载相关的隐变量。
+当前第一项任务是定位“策略原地踏步但没有向前移动”的原因。
+
+```mermaid
+flowchart LR
+ P["训练特权参数:摩擦、负载"] --> E["教师 Encoder"]
+ E --> Z["隐变量 z:8 维"]
+ H["过去 50 步观测:50 × 49"] --> A["学生 Adaptation MLP"]
+ A --> ZH["估计隐变量 z_hat:8 维"]
+ O["当前观测:49 维"] --> ACT["共享 Actor"]
+ Z --> ACT
+ ZH --> ACT
+ ACT --> U["12 维动作"]
+```
+
+教师评估使用真实特权参数,学生使用历史估计隐变量;两条路径是分别选择的。
+部署图只保留 Adaptation + Actor,教师 Encoder 与 Critic 不进入部署图。
+
+本分支在 UniLab fork 中加入增量实验层,保留 `unilab` 导入,不修改上游项目的归属。
+训练脚本使用项目内的独立 PPO 实现,不是官方 RSL-RL 的训练入口。
+
+## 已观察到的结果
+
+所有评估固定机体系指令 `(0.5, 0, 0)`,每个种子使用 8 个环境、每环境 1000 个控制步。
+
+| 模型 | 评估种子 | 平均前向速度 (m/s) | 前向 RMSE (m/s) | 提前终止次数 |
+|---|---:|---:|---:|---:|
+| 原始教师 | 123 | -0.006058 | 0.510957 | 0 |
+| 原始教师 | 456 | -0.011667 | 0.517436 | 1 |
+| 原始教师 | 789 | -0.004922 | 0.511072 | 1 |
+| 前进配置教师 v1 | 123 | 0.015686 | 0.489772 | 2 |
+| 前进配置教师 v1 | 456 | 0.013730 | 0.490668 | 0 |
+| 前进配置教师 v1 | 789 | 0.012254 | 0.491959 | 0 |
+
+前进配置 v1 已完成 3000 次更新、4,608,000 次环境转移,初始化种子为 42。
+三个评估种子不是三个独立训练种子,表格不构成跨训练种子的显著性结论。
+不同配置使用不同奖励权重,因此不能直接用总奖励比较任务效果。
+提前终止次数不是经独立检测标注的跌倒率。
+
+
+
+
+
+原始数值、训练记录及分析见 [results](../../results/rma/) 和 [实验记录](../../experiments/rma/)。
+曲线展示奖励而非实际速度;当前训练器尚未逐步记录前向速度曲线。
+
+## 代码结构
+
+```text
+rsl_rl/unilab_rma/ 教师/学生网络、PPO、GAE、历史缓冲与 UniLab 适配
+scripts/ 训练、评估、导出与结果汇总
+legged_gym/ Go2 接口常量,并非重新实现 legged_gym
+src/unilab/... RMA 事件与任务配置;原有 UniLab 框架保持上游归属
+deploy_cpp/ TensorRT 接口、双缓冲与 50/1000Hz mock 控制框架
+tests/rma/ 算法与接口测试
+experiments/ 问题、假设、对照设计与结论
+results/ 精选 JSON、CSV、配置记录与图表
+docs/ 使用说明、接口契约与复现边界
+```
+
+## 复现与依赖
+
+完整说明见 [复现指南](REPRODUCING.md) 和 [验证记录](VALIDATION.md)。
+
+已发布的实测数据基于本地 UniLab 1.3.3 工作副本、MuJoCo CPU 物理环境和 PyTorch。
+本 fork 基线为 UniLab 1.3.4,基线 commit 与关键文件差异见 [provenance.json](../../results/rma/provenance.json)。
+原始运行副本未携带 Git 历史,且部分配置/运行时文件与 fork 不同;因此不能把旧实测冒充为当前分支端到端验证。
+不建议为了运行本仓库直接升级已经验证过的仿真环境。
+
+核心测试与结果图表可以独立运行;真实 Go2 训练仍需要完整 UniLab、依赖与机器人资产。
+在匹配的环境中执行:
+
+```bash
+cd /path/to/this/UniLab
+uv run --no-sync python -m unittest discover -s tests/rma -p 'test_*.py'
+```
+
+评估已存在的前进配置教师:
+
+```bash
+uv run --no-sync python scripts/evaluate_rma.py \
+ --policy teacher --checkpoint runs/rma/teacher_forward_v1.pt \
+ --task go2_joystick_flat/rma_forward \
+ --num-envs 8 --steps 1000 --seed 123 \
+ --command 0.5 0 0 --diagnostics
+```
+
+在上述命令追加 `--render` 可看画面,追加 `--render-speed 0.5` 可慢放。
+训练默认不打开可视化。实验模型权重和已安装运行环境不包含在本仓库中。
+以上是已有完整 1.3.3 运行环境的评估入口;1.3.4 环境接入必须单独完成兼容性验证。
+
+## 接下来如何改进
+
+1. 保留当前基线,补齐逐步速度、动作饱和和 PPO 更新诊断。
+2. 在一致训练预算下分别检验指令范围、摆腿奖励和速度跟踪权重。
+3. 教师速度跟踪通过后,比较学生自身 rollout 与教师表现。
+4. 将 ONNX / TensorRT 的数值一致性与任务表现分别验收。
+
+本仓库当前不是自动课程调度器;现有 `rma_forward` 是静态前进任务配置。
+详细设计见 [下一项实验](../../experiments/rma/EXP-002_diagnosis_plan.md)。
+
+## 来源与贡献范围
+
+UniLab 是上游仿真与任务框架;本项目贡献范围为 RMA 增量实现、实验配置、诊断、导出/模拟部署及实验分析。
+上游来源、许可证与本地源码快照的限制见 [来源说明](THIRD_PARTY_NOTICES.md)。
+AI 工具参与了代码和文档辅助;已验证范围和未验证项以测试记录及实验数据为准。
diff --git a/docs/rma/REPRODUCING.md b/docs/rma/REPRODUCING.md
new file mode 100644
index 000000000..c227d9b5b
--- /dev/null
+++ b/docs/rma/REPRODUCING.md
@@ -0,0 +1,60 @@
+# RMA 实验复现与验证范围
+
+## 先区分两个代码版本
+
+旧实测来自没有 Git 历史的本地 UniLab 1.3.3 工作副本。
+本分支从用户 fork 的 `c19b8810059e3c495dc5394a5652d3c82c00f70f`(1.3.4)开始,导入 RMA 增量代码。
+`results/rma/provenance.json` 保存了运行依赖、来源文件哈希和关键框架文件差异。
+存在差异时,不应把旧模型成绩标成当前 Git commit 上重跑的结果。
+
+## 复现表格和图表
+
+在有 PyTorch 和 matplotlib 的 Python 环境中,从项目根目录执行:
+
+```bash
+uv run --no-sync python scripts/report_rma.py
+```
+
+输入是 `results/rma/baseline/seed*.json`、`results/rma/forward_v1/seed*.json`
+和 `results/rma/forward_v1/training_metrics.csv`。
+输出是 `results/rma/evaluation_summary.csv` 与 `results/rma/figures/*.png`。
+训练图使用真实的 3000 行 CSV,平滑线为包含启动区间的尾随 100 次更新均值。
+没有填补不存在的模型速度曲线,也没有生成虚构视频。
+
+## 核心算法/接口测试与 C++ mock
+
+```bash
+uv run --no-sync python -m unittest discover -s tests/rma -p 'test_*.py'
+cmake -S deploy_cpp -B build/rma -DCMAKE_BUILD_TYPE=Release
+cmake --build build/rma -j2
+ctest --test-dir build/rma --output-on-failure
+```
+
+这些测试验证算法接口、历史缓冲、概率/终止处理、导出接口和并发 mock。
+它们不能证明 Go2 速度跟踪、真机安全、硬实时或 TensorRT 在目标设备上的精度与延迟。
+
+## 真实评估协议
+
+保留原 checkpoint,固定 `(0.5, 0, 0)`,每个种子运行 8 环境 × 1000 个控制步,
+种子为 123、456、789,模型权重不变。完整参数见 EXP-001。
+加入 `--diagnostics` 才输出各轴速度、各轴 RMSE 与奖励分项。
+统计包含启动和重置过程;下一阶段应提前约定稳态统计窗口,并同时保留全程指标。
+日志若使用 `tee` 保存,会混有启动文字,不是纯 JSON 文件。
+
+当前静态配置继承的上游基础奖励定义在 1.3.3 与 1.3.4 有变化。
+历史配置快照应与原运行环境一起用于追溯,不能单独覆盖新版本的全部框架文件。
+重新运行当前分支得到的结果,应另起实验编号,并保存当前 commit 和完整配置。
+
+## 训练与 checkpoint 语义
+
+3000 次更新 × 64 环境 × 24 rollout 长度 = 4,608,000 次环境转移。
+策略/更新 CUDA 不等于物理仿真也在 GPU;本次物理环境为 CPU。
+当前 `--checkpoint` 只恢复网络权重,未恢复 optimizer/RNG/环境状态,属于 warm-start。
+当前保存行为每 20 次覆盖同一路径;旧运行没有保存全部历史 checkpoint,无法补算其历史速度曲线。
+
+## 环境和资产
+
+机器人资源通过上游资产流程获取,路径与 home keyframe 必须匹配。
+不要将 `.venv`、mesh、完整运行目录或大模型文件直接加入 Git。
+本分支没有发布训练权重;checkpoint SHA256 用于之后核对同一模型。
+环境安装方式以 fork 上游说明为准;复现旧数据需匹配 provenance 中记录的版本及配置。
diff --git a/docs/rma/THIRD_PARTY_NOTICES.md b/docs/rma/THIRD_PARTY_NOTICES.md
new file mode 100644
index 000000000..93021e5ba
--- /dev/null
+++ b/docs/rma/THIRD_PARTY_NOTICES.md
@@ -0,0 +1,14 @@
+# 来源与贡献范围
+
+- 上游框架:UniLab,仓库 `https://github.com/unilabsim/UniLab`。
+- 用户 fork:`https://github.com/gongbaojiding-1123/UniLab`。
+- 原有 UniLab 源码、许可证和归属保持不变;许可证见仓库根目录 `LICENSE`。
+- RMA 增量:`rsl_rl/unilab_rma`、RMA 脚本与配置、`deploy_cpp`、相关测试与本组实验文档。
+- `legged_gym/go2_config.py` 只声明接口常量,不是完整 legged_gym 的移植。
+- 当前训练器是本项目独立 PPO;不能将其结果归属于官方 RSL-RL 的默认实现。
+- RMA 的教师隐变量/历史适应思路属于已有研究,本项目不宣称发明该方法。
+- 机器人资源、TensorRT/CUDA、PyTorch 等依赖仍遵循其各自许可,未随实验结果重新分发。
+- 代码和文档使用了 AI 辅助,结果数据来自已保存的训练 CSV 与用户提供的评估输出。
+
+原运行目录未携带 Git 历史,无法证明它与某个上游 commit 完全一致。
+本分支保留关键文件差异记录,不通过猜测补填上游 commit。
diff --git a/docs/rma/VALIDATION.md b/docs/rma/VALIDATION.md
new file mode 100644
index 000000000..488266e04
--- /dev/null
+++ b/docs/rma/VALIDATION.md
@@ -0,0 +1,41 @@
+# RMA 分支验证记录
+
+日期:2026-10-22。
+
+## 已通过
+
+- 导入本地 RMA 增量层及精选实验数据;数据汇总严格匹配 6 份 JSON 和 3000 行训练 CSV。
+- `uv run --no-sync python scripts/report_rma.py`:生成汇总 CSV 与两张静态图;图表已人工查看。
+- `uv run --no-sync python -m unittest discover -s tests/rma -p 'test_*.py'`:33 项通过,格式化后重跑仍通过。
+- `cmake -S deploy_cpp -B build/rma -DCMAKE_BUILD_TYPE=Release`:配置成功。
+- `cmake --build build/rma -j2`:构建成功。
+- `ctest --test-dir build/rma --output-on-failure`:2/2 通过(并发一致性和 mock 控制)。
+- `make check` 中的 ruff format/check、`mypy src/unilab`:通过。
+
+核心测试与报告脚本使用现有 WSL Python 环境。C++ 为 GNU 11.4.0、CMake 生成的 Release 构建。
+没有追加 Go2 长训,没有更改用户运行目录的策略参数或仿真配置。
+
+## 完整门禁尚未通过
+
+在本 fork 1.3.4 源码上,复用了已有 1.3.3 的 WSL 环境运行完整门禁:
+
+```bash
+UV_PROJECT_ENVIRONMENT=/home/abc/my_unilab/.venv UV_NO_SYNC=1 make check
+UV_PROJECT_ENVIRONMENT=/home/abc/my_unilab/.venv UV_NO_SYNC=1 make test-all
+```
+
+Pyright 报告上游现有框架文件的 10 项错误和 3 项警告,包括缺失的 packed-reset/capability API、
+未解析导入与可选值检查。报错位置没有被本次改动修改。
+依赖/源码版本不一致是已观察到的限制,尚未证明这是所有错误的唯一原因。
+`make test-all` 因前置类型检查未通过而未到达后续全仓测试阶段,不能报告成全仓测试通过。
+未为通过门禁而升级正在使用的训练环境或更改上游框架代码。
+
+若创建 PR,应保持 Draft;本地核心验证不替代完整门禁和当前 head 的远端 CI。
+1.3.4 分支下的真实 Go2 训练、旧 checkpoint 行为兼容、TensorRT/真机与跨引擎测试未在本次执行。
+
+## 历史数据边界
+
+发布的运动指标是已有本地 1.3.3 运行副本产生的结果,不是当前 1.3.4 分支重跑的结果。
+`results/rma/forward_v1/config_snapshot` 是训练完成后从仍在使用的工作副本采集的配置,
+并非训练开始时自动保存的不可变快照;不能据此补造完整历史溯源。
+原运行配置、CSV、checkpoint 与来源 SHA256 记录见 `results/rma/provenance.json`。
diff --git a/experiments/rma/EXP-000_teacher_full_baseline.md b/experiments/rma/EXP-000_teacher_full_baseline.md
new file mode 100644
index 000000000..1572cffe3
--- /dev/null
+++ b/experiments/rma/EXP-000_teacher_full_baseline.md
@@ -0,0 +1,54 @@
+# EXP-000:旧教师固定前进指令评估
+
+记录整理:2026-10-22。原始评估目录日期:2026-10-21。
+本记录由已有 JSON 整理,本次没有重新训练或重新评估。
+
+## 问题与设置
+
+- 模型:旧 `teacher_full.pt`,教师策略;不是新前进配置模型。
+- 目标指令:机体系 `(0.5 m/s, 0 m/s, 0 rad/s)`。
+- 每次 8 环境 × 1000 策略步,共 8000 次环境转移。
+- 三个评估种子:123、456、789。它们不是三个训练种子。
+- 原始 JSON 中未保存完整训练配置、训练 commit 或 checkpoint 校验和;本记录不补造这些信息。
+
+## 原始数据
+
+实际 WSL 目录:
+`/home/abc/my_unilab/runs/rma/circling_diagnosis/verified_20261021_225308/`
+
+仓库中发布的对应数据:
+
+- `results/rma/baseline/seed123.json`
+- `results/rma/baseline/seed456.json`
+- `results/rma/baseline/seed789.json`
+
+| 评估种子 | 平均 vx (m/s) | 前向 RMSE (m/s) | 平面速度 RMSE (m/s) | 提前终止次数 |
+|---|---:|---:|---:|---:|
+| 123 | -0.006058 | 0.510957 | 0.514638 | 0 |
+| 456 | -0.011667 | 0.517436 | 0.522385 | 1 |
+| 789 | -0.004922 | 0.511072 | 0.515623 | 1 |
+
+表格数值仅为展示做了四舍五入,原始 JSON 保留精度。
+统计包含启动与重置后的过渡过程;不是单独截取的稳态数据。
+提前终止次数不等于经独立接触检测标注的跌倒次数。
+
+种子 123 的乘权重、尚未乘控制 dt 的奖励分项:
+
+- `tracking_lin_vel`:0.360382。
+- `swing_feet_z`:1.272840。
+- `similar_to_default`:-0.183316。
+
+## 结论与局限
+
+事实:该模型在这三组测试中,平均前向速度接近零,未实现目标速度跟踪。
+辅助摆腿奖励的贡献较高,值得检查是否存在“原地踏步也获得收益”的路径。
+奖励分项大小本身不证明这是唯一原因,也不直接给出哪个奖励权重应设为多少。
+没有由此证明 PPO 公式错误、需要更换仿真器或需要修改部署线程。
+
+## 下一步
+
+1. 核对后续新模型实际使用的任务与配置。
+2. 用相同固定指令、种子、环境数与步数评估新模型;保留新日志和本基线。
+3. 若仍不前进,按 `EXP-002_diagnosis_plan.md` 设计单因素对照及控制链路诊断。
+
+后续前进配置结果现已收集,见 `EXP-001_forward_v1.md`。
diff --git a/experiments/rma/EXP-001_forward_v1.md b/experiments/rma/EXP-001_forward_v1.md
new file mode 100644
index 000000000..15b62d76d
--- /dev/null
+++ b/experiments/rma/EXP-001_forward_v1.md
@@ -0,0 +1,49 @@
+# EXP-001:前进配置教师仍未跟踪目标速度
+
+日期:2026-10-22。状态:训练与三个种子的评估已完成,运动目标未达成。
+
+## 假设与改动
+
+原始教师在固定前进指令下几乎不前进。初始假设是辅助摆腿奖励与任务范围可能影响学习。
+本次将 vx 指令收窄到 `[0.3, 0.6]`、vy/wz 置零,速度跟踪权重由 1 改为 2,摆腿奖励权重由 4 改为 0.25。
+这是组合实验,不能将结果归因于单独某一项改动。
+
+## 训练证据
+
+运行配置记录确认:
+
+- 教师 PPO,从头训练,初始化种子 42。
+- 环境 64,采样长度 24,更新 3000 次,环境转移 4,608,000 次。
+- 策略/更新设备 CUDA,物理环境设备 CPU,后端 MuJoCo。
+- 模型 `teacher_forward_v1.pt`;训练运行配置、CSV 与 checkpoint 元数据另存于 `results/rma/forward_v1/`。
+
+3000 是优化迭代次数,不是环境交互总步数。已完成这一预算,但这不等于证明预算足以收敛。
+每 500 次更新的平均每步奖励约为 0.02093、0.02190、0.02167、0.02149、0.02197、0.02147。
+曲线在早期提高后长期处于平台,没有支持继续无条件加训必然成功的趋势。
+
+## 测试协议
+
+教师策略、固定机体系指令 `(0.5, 0, 0)`,每种子 8 环境 × 1000 控制步。
+三个评估种子为 123、456、789,模型权重相同。统计包含启动和回合重置后的过渡过程。
+
+| 种子 | 平均 vx (m/s) | 前向 RMSE (m/s) | 平面速度 RMSE (m/s) | 提前终止次数 |
+|---|---:|---:|---:|---:|
+| 123 | 0.015686 | 0.489772 | 0.493154 | 2 |
+| 456 | 0.013730 | 0.490668 | 0.493155 | 0 |
+| 789 | 0.012254 | 0.491959 | 0.494027 | 0 |
+
+## 结论
+
+- 目标 0.5 m/s 下,三个条件的平均前向速度都只有约 0.014 m/s,尚未学会任务要求的前进。
+- 相比旧模型有小幅变化,但不能把小幅改善描述为已经学会走路。
+- 仅凭最终 checkpoint 不能区分“预算不够”和“局部最优/探索或控制问题”。
+- 降低摆腿奖励并增加跟踪奖励后问题仍存在;不能继续将唯一原因认定为摆腿奖励过强。
+- 原始、新配置的奖励权重不同,总 reward 不可直接横向比较。
+- 三个评估种子不能替代多个训练种子,也不支持统计显著性结论。
+
+## 下一步
+
+先看已有训练 CSV 的趋势,并设计逐步速度、策略输出范围和控制链路诊断。
+在采样预算相同的前提下做单因素对照,或采用预先约定的延长预算对照。
+当前 `--checkpoint` 只加载权重,没有恢复 optimizer/RNG,不能把它描述为精确续训。
+不自动追加长训、不覆盖原模型,不把失败结果隐藏掉。
diff --git a/experiments/rma/EXP-002_diagnosis_plan.md b/experiments/rma/EXP-002_diagnosis_plan.md
new file mode 100644
index 000000000..2d529eada
--- /dev/null
+++ b/experiments/rma/EXP-002_diagnosis_plan.md
@@ -0,0 +1,40 @@
+# EXP-002:先诊断平台,再选择对照实验
+
+状态:计划,尚未训练,不含预测值冒充实测。
+
+## 证据
+
+EXP-001 已完成 3000 更新、4.608M 环境转移,前向速度约 0.014 m/s。
+训练 CSV 的每 500 更新均值约为 0.02093、0.02190、0.02167、0.02149、0.02197、0.02147。
+约 500 次以后奖励长期平台,现有结果不能支持“继续挂机一定会学会”。
+也不能据此反向断言 4.608M 对所有配置都足够。
+
+## 第一项可审查的交付:诊断
+
+不先改变奖励/控制/优化参数,计划增加仅在评估时启用的记录:
+
+- 每步、每环境 vx/vy/wz、目标值、done 与回合步数。
+- 每关节归一化动作分布与接近边界的比例;若要定位 PD,另记录目标/实测角和扭矩。
+- 同时报告全程误差与预先定义的启动窗口外误差。
+
+验收:不启用记录时动作与原评估一致;记录字段、单位和坐标系明确;重置不跨回合串联。
+当前指标没有提供扭矩饱和证据,不得把“疑似饱和”写成已确认问题。
+
+## 后续对照候选
+
+| 候选 | 保持不变 | 唯一变化 | 目的 |
+|---|---|---|---|
+| 预算对照 | 配置、初始化种子、环境数、优化设置 | 总训练预算,并保存中间模型 | 检查延长训练是否改善任务指标 |
+| 指令消融 | 原奖励、控制器与 PPO | 指令收窄为前进 | 检查任务范围影响 |
+| 辅助奖励消融 | 前进指令和其他项 | 摆腿奖励权重 | 检查辅助奖励影响 |
+| 跟踪奖励消融 | 其余项 | 速度跟踪权重 | 检查任务激励影响 |
+
+不同时执行所有候选;先根据诊断选择最有信息量的一项。
+训练预算对照需要连续轨迹或明确标记 warm-start;当前入口不是完整恢复。
+每次保存配置快照、commit、种子、训练总转移数、checkpoint 哈希和相同协议下的评估。
+
+## 判断标准
+
+主要指标是固定指令下的实际速度与前向 RMSE;侧向/转向误差和提前终止作为辅助指标。
+多个独立训练种子用于检验训练稳定性,不能用三个评估种子替代。
+当前还没有通过指标;任何新增阈值需在实验前约定,并明确是本项目标准。
diff --git a/experiments/rma/TEMPLATE.md b/experiments/rma/TEMPLATE.md
new file mode 100644
index 000000000..b78b99c89
--- /dev/null
+++ b/experiments/rma/TEMPLATE.md
@@ -0,0 +1,15 @@
+# 实验编号:
+
+- 日期 / 状态:
+- 问题与已有证据:
+- 可被推翻的假设:
+- 相对哪个基线,只改变哪一项:
+- 代码 commit / 未提交补丁:
+- 完整配置快照 / 依赖信息:
+- 训练命令 / 初始化种子 / 总环境转移数 / 设备:
+- 日志目录 / checkpoint / SHA256:
+- 评估指令 / 种子 / 环境数 / 步数 / 统计窗口:
+- 实验前约定的主要指标:
+- 逐种子结果 / 原始数据 / 图表 / 视频:
+- 结论支持什么、不支持什么:
+- 下一项最小实验:
diff --git a/legged_gym/go2_config.py b/legged_gym/go2_config.py
new file mode 100644
index 000000000..e17cc1ccb
--- /dev/null
+++ b/legged_gym/go2_config.py
@@ -0,0 +1,10 @@
+"""RMA contract only; canonical simulation config remains in unilab/conf YAML."""
+
+OBS_DIM = 49
+CRITIC_DIM = 52
+PRIVILEGED_DIM = 2
+HISTORY_LENGTH = 50
+LATENT_DIM = 8
+NUM_ACTIONS = 12
+POLICY_PERIOD_S = 0.02
+ACTION_SCALE = 0.25
diff --git a/requirements-rma-export.txt b/requirements-rma-export.txt
new file mode 100644
index 000000000..baf3d05bc
--- /dev/null
+++ b/requirements-rma-export.txt
@@ -0,0 +1,4 @@
+# Install inside the original, working UniLab environment.
+onnx>=1.16,<2
+onnxruntime<1.20; python_version < '3.11'
+onnxruntime>=1.20; python_version >= '3.11'
diff --git a/results/rma/baseline/seed123.json b/results/rma/baseline/seed123.json
new file mode 100644
index 000000000..446ec94a6
--- /dev/null
+++ b/results/rma/baseline/seed123.json
@@ -0,0 +1,61 @@
+{
+ "policy": "teacher",
+ "seed": 123,
+ "completed_episodes": 8,
+ "mean_completed_episode_return": 22.94372197985649,
+ "mean_completed_episode_length_steps": 636.25,
+ "mean_step_reward": 0.03588731563591864,
+ "planar_velocity_rmse_m_per_s": 0.5146384602254557,
+ "unfinished_episodes": 8,
+ "mean_unfinished_episode_return": 12.943596839904785,
+ "student_latent_mse_vs_teacher": null,
+ "student_action_rmse_vs_teacher": null,
+ "terminations": 0,
+ "total_transitions": 8000,
+ "command_override": [
+ 0.5,
+ 0.0,
+ 0.0
+ ],
+ "diagnostics": {
+ "velocity_axes": [
+ "vx_m_per_s",
+ "vy_m_per_s",
+ "yaw_rate_rad_per_s"
+ ],
+ "velocity_frame": "robot body; clean critic local_linvel xy and gyro z",
+ "initial_command_env0": [
+ 0.5,
+ 0.0,
+ 0.0
+ ],
+ "mean_command": [
+ 0.5,
+ 0.0,
+ 0.0
+ ],
+ "mean_measured_velocity": [
+ -0.006058229419679051,
+ 0.005980759545039106,
+ 0.014841998879314587
+ ],
+ "tracking_rmse_by_axis": [
+ 0.5109570487642697,
+ 0.061446229894282584,
+ 0.11541315228345941
+ ],
+ "mean_absolute_yaw_rate_rad_per_s": 0.06477700540811179,
+ "weighted_reward_terms_before_dt": {
+ "tracking_lin_vel": 0.3603822733908892,
+ "tracking_ang_vel": 0.1920580652654171,
+ "lin_vel_z": -0.011202149931981695,
+ "ang_vel_xy": -0.023651930230640574,
+ "base_height": -0.004054067645105533,
+ "action_rate": -0.008745710445419946,
+ "similar_to_default": -0.18331554384529591,
+ "contact": 0.2000549937337637,
+ "swing_feet_z": 1.2728398969173431
+ },
+ "reward_terms_note": "weighted terms BEFORE control-dt scaling; not per-step rewards"
+ }
+}
diff --git a/results/rma/baseline/seed456.json b/results/rma/baseline/seed456.json
new file mode 100644
index 000000000..3b134fb90
--- /dev/null
+++ b/results/rma/baseline/seed456.json
@@ -0,0 +1,61 @@
+{
+ "policy": "teacher",
+ "seed": 456,
+ "completed_episodes": 9,
+ "mean_completed_episode_return": 17.958606203397114,
+ "mean_completed_episode_length_steps": 509.6666666666667,
+ "mean_step_reward": 0.035673840930336154,
+ "planar_velocity_rmse_m_per_s": 0.5223854549035621,
+ "unfinished_episodes": 8,
+ "mean_unfinished_episode_return": 15.470402717590332,
+ "student_latent_mse_vs_teacher": null,
+ "student_action_rmse_vs_teacher": null,
+ "terminations": 1,
+ "total_transitions": 8000,
+ "command_override": [
+ 0.5,
+ 0.0,
+ 0.0
+ ],
+ "diagnostics": {
+ "velocity_axes": [
+ "vx_m_per_s",
+ "vy_m_per_s",
+ "yaw_rate_rad_per_s"
+ ],
+ "velocity_frame": "robot body; clean critic local_linvel xy and gyro z",
+ "initial_command_env0": [
+ 0.5,
+ 0.0,
+ 0.0
+ ],
+ "mean_command": [
+ 0.5,
+ 0.0,
+ 0.0
+ ],
+ "mean_measured_velocity": [
+ -0.011666606367917325,
+ 0.014149421059863528,
+ 0.018934832205886893
+ ],
+ "tracking_rmse_by_axis": [
+ 0.5174359250308406,
+ 0.07174000046007187,
+ 0.143143680448307
+ ],
+ "mean_absolute_yaw_rate_rad_per_s": 0.06684632264653795,
+ "weighted_reward_terms_before_dt": {
+ "tracking_lin_vel": 0.3547401953712106,
+ "tracking_ang_vel": 0.19153448170423507,
+ "lin_vel_z": -0.014624936374981188,
+ "ang_vel_xy": -0.025369600676742265,
+ "base_height": -0.006269701974932105,
+ "action_rate": -0.008777687560716459,
+ "similar_to_default": -0.18115082198753954,
+ "contact": 0.20040749409794809,
+ "swing_feet_z": 1.2732026720046996
+ },
+ "reward_terms_note": "weighted terms BEFORE control-dt scaling; not per-step rewards"
+ }
+}
diff --git a/results/rma/baseline/seed789.json b/results/rma/baseline/seed789.json
new file mode 100644
index 000000000..6706f4473
--- /dev/null
+++ b/results/rma/baseline/seed789.json
@@ -0,0 +1,61 @@
+{
+ "policy": "teacher",
+ "seed": 789,
+ "completed_episodes": 8,
+ "mean_completed_episode_return": 17.56580749154091,
+ "mean_completed_episode_length_steps": 496.875,
+ "mean_step_reward": 0.03569934609872871,
+ "planar_velocity_rmse_m_per_s": 0.5156226711943084,
+ "unfinished_episodes": 8,
+ "mean_unfinished_episode_return": 18.13353729248047,
+ "student_latent_mse_vs_teacher": null,
+ "student_action_rmse_vs_teacher": null,
+ "terminations": 1,
+ "total_transitions": 8000,
+ "command_override": [
+ 0.5,
+ 0.0,
+ 0.0
+ ],
+ "diagnostics": {
+ "velocity_axes": [
+ "vx_m_per_s",
+ "vy_m_per_s",
+ "yaw_rate_rad_per_s"
+ ],
+ "velocity_frame": "robot body; clean critic local_linvel xy and gyro z",
+ "initial_command_env0": [
+ 0.5,
+ 0.0,
+ 0.0
+ ],
+ "mean_command": [
+ 0.5,
+ 0.0,
+ 0.0
+ ],
+ "mean_measured_velocity": [
+ -0.004921973013823731,
+ 0.005795657981586174,
+ -0.0007342302818937725
+ ],
+ "tracking_rmse_by_axis": [
+ 0.5110722221933418,
+ 0.06835146357993736,
+ 0.15672107160305368
+ ],
+ "mean_absolute_yaw_rate_rad_per_s": 0.07771462473009702,
+ "weighted_reward_terms_before_dt": {
+ "tracking_lin_vel": 0.36330923438072205,
+ "tracking_ang_vel": 0.1901205448284745,
+ "lin_vel_z": -0.013487572537051164,
+ "ang_vel_xy": -0.028084615291387307,
+ "base_height": -0.004377204851363786,
+ "action_rate": -0.008941495961428927,
+ "similar_to_default": -0.18495080276578665,
+ "contact": 0.19976249381899833,
+ "swing_feet_z": 1.2716167651414871
+ },
+ "reward_terms_note": "weighted terms BEFORE control-dt scaling; not per-step rewards"
+ }
+}
diff --git a/results/rma/evaluation_summary.csv b/results/rma/evaluation_summary.csv
new file mode 100644
index 000000000..94c3ebbc7
--- /dev/null
+++ b/results/rma/evaluation_summary.csv
@@ -0,0 +1,7 @@
+model,seed,target_vx_m_s,mean_vx_m_s,vx_rmse_m_s,planar_rmse_m_s,terminations,total_transitions
+baseline,123,0.5,-0.006058229419679051,0.5109570487642697,0.5146384602254557,0,8000
+baseline,456,0.5,-0.011666606367917325,0.5174359250308406,0.5223854549035621,1,8000
+baseline,789,0.5,-0.004921973013823731,0.5110722221933418,0.5156226711943084,1,8000
+forward_v1,123,0.5,0.015685936529645686,0.4897715884761797,0.49315400810945587,2,8000
+forward_v1,456,0.5,0.013729549131713991,0.49066846115678453,0.49315459737476025,0,8000
+forward_v1,789,0.5,0.012253768285125432,0.49195942077463545,0.49402653605180963,0,8000
diff --git a/results/rma/figures/forward_v1_training.png b/results/rma/figures/forward_v1_training.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3729661ef46ab1e495caad2435efdb0c02644da
GIT binary patch
literal 291350
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