test 3-point teleop

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Martino Russi
2026-07-15 18:16:46 +02:00
parent f6a845c30c
commit 9c54665a76
17 changed files with 1379 additions and 13 deletions
+103
View File
@@ -0,0 +1,103 @@
# SONIC fidelity audit — Python/ONNX port vs C++ deploy reference
Compares the lerobot Python/ONNX SONIC port against the original C++ deploy stack
(`gear_sonic_deploy/src/g1/g1_deploy_onnx_ref/src/g1_deploy_onnx_ref.cpp` and headers).
**Verdict:** the port is algorithmically faithful in the core control math (the parts
that determine stability and pose tracking). The gaps are concentrated in (a) update
cadence, (b) the safety layer, and (c) two modes/paths present in C++ but never wired up.
No silent math bug was found; divergences are deliberate or missing-feature.
## Genuinely faithful (verified, no action needed)
- **Action production** — both do `q_target = DEFAULT_ANGLES + policy_out[isaaclab→mujoco] * ACTION_SCALE`
(residual-to-default, not residual-to-previous). C++ `g1_deploy_onnx_ref.cpp:3119-3127`,
Python `sonic_pipeline.py:708-724`.
- **Joint-order remap** — `ISAACLAB_TO_MUJOCO` / `MUJOCO_TO_ISAACLAB` identical arrays.
- **Gains** — same `Kp = armature·ω²`, `Kd = 2ζ·armature·ω`, ω = 10·2π, ζ = 2, and the same
2× set `{4,5,10,11,13,14}` (ankles + waist roll/pitch).
- **History normalization** — robot `q` subtracts defaults, velocities raw,
gravity = `quat_rotate(conj(base), [0,0,-1])`, oldest→newest ordering.
- **6D anchor rotation** — verified element-by-element: C++ takes the first two *columns*
of the rotation matrix flattened row-wise (`.cpp:677-683`); Python `quat_to_6d`
(`sonic_pipeline.py:227-240`) produces the identical 6 values. Only the Python docstring
wording ("rows") is misleading — the math is correct.
- **Planner** — replan intervals (RUN 0.1 / CRAWL 0.2 / boxing 1.0 / default 1.0 s),
8-frame slerp crossfade blend, 30→50 Hz linear+slerp resample, `MOTION_LOOK_AHEAD = 2`,
4-frame 30 Hz context. All match.
- **SLERP / heading / FK conventions** — wxyz, shortest-path slerp with 0.9995 linear
fallback, `calc_heading` yaw extraction.
## Divergences that reduce fidelity (ranked)
### 1. Encoder cadence: 10 Hz vs 50 Hz (biggest)
C++ runs the encoder every control tick — `GatherTokenState → Encode()` is unconditional
inside the 50 Hz `Control()` loop (`.cpp:1644-1684`, no N-step gate). The port recomputes
the token only every 5 ticks (`ENCODER_UPDATE_EVERY = 5`, `sonic_pipeline.py:150`), so the
latent is up to 80 ms stale and the decoder consumes a held token for 4 of every 5 ticks.
Likely a perf shortcut. For full faithfulness set `ENCODER_UPDATE_EVERY = 1` (cheap on GPU).
### 2. SMPL root anchor disabled by default
C++ always feeds the reference root orientation into the anchor/heading. The port sets
`smpl_root_quat = None` unless `enable_smpl_root=True` (`sonic_whole_body.py:366`), because
the raw 30 Hz root caused QACC spikes. Faithful fix: slerp-resample the root 30→50 Hz like
the joints, then re-enable by default. Until then, mode-2 heading steering isn't faithful.
See `SONIC_REPLAY_DEBUGGING.md`.
### 3. VR 3-point teleop (`encode_mode = 1`) — now wired (was inert)
Originally the encoder layout for mode 1 existed but nothing set `encode_mode=1` or
filled `vr_3point_local_target` / `vr_3point_local_orn_target`. **Now implemented
end-to-end:**
- Producer `pico_publisher.py` computes the 3 keypoints via `smpl_fk.compute_3point`
(ported from gear_sonic `_process_3pt_pose`) and adds `vr3_pos` (9) / `vr3_orn` (12)
to the `rt/smpl` message.
- `SmplStream` parses them (`has_vr3`); `PicoHeadset(mode="vr3")` emits `vr3_pos.*` /
`vr3_orn.*` action keys.
- `SonicWholeBodyController` extracts them, switches to `encode_mode=1`, fills the
controller targets, and drives locomotion from the joystick/keyboard planner
(`use_joystick=True`); `PlannerController.build_encoder_obs` gained a mode-1 branch
(lower body per-frame step 5 + VR targets + anchor).
Still not ported: `vr_3point_compliance`, and the operator calibration
(`ThreePointPose.apply_calibration`) / physical wrist offsets — the raw tracked joint
poses are used, so hand-tracking may need calibration tuning.
### 4. Safety layer largely absent
- **Joint-velocity kill switch** at >35 rad/s (`.cpp:2829-2832`) — missing.
- **E-stop damping**: C++ e-stop commands `kp=0, kd=8` (active damping, `.cpp:2708-2714`).
The port's Space/e-stop just sets `playing=False` + `LM.IDLE` and stops the cursor — it
does not switch to a damped hold. Less safe.
- **Motor-temperature** monitor (90 °C / 85 °C hysteresis) — missing.
- **Stale-/late-state watchdogs** (500 ms fail, 50 ms warn, 200 ms token timeout) — not in
the SONIC layer (partly covered by `UnitreeG1`, not equivalently).
- **Per-tick delta clamp** — C++ has none, so the reverted `MAX_DELTA_PER_STEP` was correctly
removed; that part is faithful.
### 5. Idle readaptation blend missing
At planner IDLE at a motion end, C++ runs a double-threshold blend (0.98/0.02 toward
robot-current or original target; thresholds 0.10/0.05/0.045 rad; `.cpp:3303-3361`). The
port just holds. Minor; only matters at motion-end idle.
### 6. Input/streaming paths not ported
- **ZMQ Protocol V1 joint streaming** (`encode_mode=0` from a live joint stream via
`StreamedMotionMerger`) — not implemented; the port's streaming path is SMPL-only.
- **External token injection** (tokens over ZMQ/ROS2 bypassing the encoder) — not supported;
the port always encodes locally. Fine for standalone.
- **Gamepad** — C++ has a full gamepad map (EMA smooth 0.3, deadzone 0.05); the port has
joystick byte-parsing + keyboard, no gamepad manager. Functionally close.
### 7. Minor input-feel differences
Delta-heading is continuous (±0.02 rad/tick) in the port vs discrete steps (±π/6, ±π/12) in
C++; speed/height increments differ slightly. Behavioral feel only, not correctness.
## Recommendation (priority order)
1. `ENCODER_UPDATE_EVERY = 1` (or a param defaulting to 1) — closes the biggest gap for
near-zero cost on GPU.
2. Rate-match the SMPL root 30→50 Hz (slerp) and re-enable `enable_smpl_root` by default.
3. Add the safety envelope: joint-velocity kill (35 rad/s) and a proper damped e-stop
(`kp=0, kd≈8`). Real hardware-safety consequences.
4. Wire `encode_mode=1` from the pico headset (3-point targets), or document as out of scope.
5. Fix the `quat_to_6d` docstring wording ("rows" → "first two columns flattened row-wise").
Items 47 are feature-completeness; 13 are what to do for faithful behavior on the robot.
+97
View File
@@ -0,0 +1,97 @@
# SONIC replay instability — root cause & prevention
This documents the multi-day debugging of "SONIC motion replay is unstable / jitters /
lags / dies on the floor", so we don't chase the same ghosts again.
## TL;DR
There were **two independent problems**, and they masked each other:
1. **Wrong conda environment (the "lag"/jitter).** The debugging env `lerobot_sonic`
had a **CUDA-13** stack that the machine's GPU driver cannot run, so ONNX Runtime
silently fell back to CPU and oversubscribed threads. The known-good env
`lerobot312` has a **CUDA-12** stack matching the driver, so the encoder/decoder/
planner run on the GPU (~1220 ms planner inference) and the control loop holds
~4850 Hz.
2. **SMPL root-motion feeding (the NaN/`unstable` crash).** Passing the per-frame SMPL
root quaternion into the mode-2 anchor produced a root-acceleration spike
(`Nan, Inf or huge value in QACC at DOF 0`) mid-episode. Disabling it gives clean
tracking.
Neither is an algorithmic bug in the ported SONIC pipeline. A lot of earlier "fixes"
(ORT thread caps, `MAX_DELTA_PER_STEP` clamp, planner-disable toggle, resampling)
were chasing symptom #1 in the wrong environment and were reverted.
## Environment: what "good" looks like
Run the replay in `lerobot312` (CUDA-12), pointing at the current sonic checkout:
```bash
conda activate lerobot312
PYTHONPATH=/home/yope/Documents/sonic/lerobot/src \
lerobot-replay \
--robot.type=unitree_g1 --robot.controller=SonicWholeBodyController \
--dataset.repo_id=lerobot/SMPL_samples --dataset.episode=12
```
Known-good versions (`lerobot312`):
| package | good (`lerobot312`) | broken (`lerobot_sonic`) |
|-----------------|---------------------|--------------------------|
| GPU driver | CUDA 12.8 (`12080`) | same (unchanged) |
| torch | `2.10.0+cu128` | `2.11.0+cu130` |
| onnxruntime | `onnxruntime-gpu 1.26.0` | CPU `1.27.0` / cu13 mismatch |
| cudnn | cu12 (bundled) | `nvidia-cudnn-cu13 9.19` |
| mujoco | `3.8.1` | `3.10.0` |
### How to verify the GPU path is actually live (do this FIRST)
```bash
python -c "import torch; print('cuda', torch.cuda.is_available())" # must be True
python -c "import onnxruntime as ort; print(ort.get_available_providers())" # must list CUDAExecutionProvider
```
If `torch.cuda.is_available()` is `False` or `CUDAExecutionProvider` is missing, STOP —
you are in the wrong/broken env. Do not "optimize" anything else until this passes.
### Why CUDA 13 was fatal here
The GPU driver supports up to **CUDA 12.8**. A CUDA-13 build of torch/onnxruntime
cannot initialize on it:
- `torch.cuda.is_available()` returns `False` (silent CPU fallback).
- `onnxruntime-gpu` (a CUDA-12 build) can't find a matching cuDNN because only the
CUDA-13 cuDNN is installed → `CUDNN failure 1001: CUDNN_STATUS_NOT_INITIALIZED`.
Installing `onnxruntime` and `onnxruntime-gpu` **together** also breaks: they share the
`onnxruntime` namespace and whichever installs last clobbers the other's shared libs.
Keep only `onnxruntime-gpu` in a GPU env.
## Root motion: the NaN/unstable crash
Symptom:
```
WARNING: Nan, Inf or huge value in QACC at DOF 0. The simulation is unstable. Time = 4.196.
```
`DOF 0` is the floating base/root. Feeding the per-frame SMPL root quaternion
(`root.*` action keys) into `controller.smpl_root_quat` injected a discontinuity in the
reference root trajectory (frame-to-frame jump and/or 30 Hz→50 Hz timing mismatch) that
the tracker converted into an exploding base acceleration.
Current mitigation (in `sonic_whole_body.py`, `run_step`): the per-frame root quaternion
is ignored (`self.controller.smpl_root_quat = None`) so the anchor stays self-driven.
Result: clean tracking, no NaN.
Proper fix (follow-up, not yet done): smooth/slerp-filter the reference root trajectory
(or resample to the control rate) before feeding it to the anchor, then re-enable.
## Prevention checklist
- **Always confirm the env before debugging behavior.** Run the two verification
commands above. Most of the "instability" was environment, not code.
- **Pin the GPU stack** to match the driver (CUDA 12.8): `torch ==2.10.0+cu128`,
`onnxruntime-gpu ==1.26.0`, `mujoco ==3.8.1`. Do not let `lerobot_sonic` drift to a
CUDA-13 stack.
- **Never install `onnxruntime` and `onnxruntime-gpu` side by side.**
- **Don't add band-aid clamps/thread-caps/resampling to hide a CPU-fallback**; fix the
env instead. Those changes were reverted.
- **Root trajectory must be continuous / rate-matched** before it drives the anchor.