← Aung Kaung Myat/Walkthroughsプロジェクト解説
CPU-only robot-learning track · project 3 · repoCPUのみのrobot learningトラック · プロジェクト3 · リポジトリ microduck-rl

Microduck Balance and Push Recovery

A 25 cm, 737 g open-source biped with fourteen servos, trained to stay up under shoves with PPO on a laptop CPU — no GPU, no hardware. Measured against a PD controller, tested on physics it never saw, exported to ONNX.

全高25 cm・重さ737 g、14サーボのオープンソース二足ロボットに、押されても立ち続ける動作をノートPCのCPUだけでPPO学習させました(GPUも実機もなし)。PD controllerと比較し、見たことのない物理モデルで検証し、ONNXへexportしています。

MuJoCo 3.12PyTorch CPUPPO multiprocessing14-DOF bipedsim-to-sim transfer ONNX Runtimeno CUDA anywhere8 of 14 threads
Status進捗
Steps 1–5 all measuredStep 1–5 すべて計測済み
ObservationObservation
48 dims, all real sensors次元、すべて実機センサー
PD baseline, reward v2PD baseline(reward v2)
175.2 ± 3.1 of 500/500点
PPO after 50M steps50M step後のPPO
345 vs 176 of 500/500点
Training rate, measured in the loop学習ループ内で計測した速度
≈3 300 env-steps/s
Testsテスト
27 contract + reward, PPO, DR, export suites契約テスト27件 + reward・PPO・DR・exportの各スイート
00

At a glance要点

The result, the one negative finding, and what was built — before any detail.結果と、1つのネガティブな発見と、作ったもの。詳細はその後で。

Return of 500 — 100 episodes × 5 seedsreturn(500点満点)— 100 episodes × 5 seeds
PPO · 50M stepsPPO · 50M stepPPO v2, 50M steps: 344.8 ± 2.9 of 500344.8PPO · 25M stepsPPO · 25M stepPPO v2, 25M steps (chunk 1): 332.6 ± 5.1332.6PD controllerPD controllerPD hold-pose, reward v2: 175.8 ± 0.2175.8
Share of steps upright直立していたstepの割合
PPO · 50M stepsPPO · 50M stepupright 73% of steps73%PPO · 25M stepsPPO · 25M stepupright 74% of steps74%PD controllerPD controllerupright 47% of steps47%
Domain randomisation on held-out physics — upright shareheld-out physicsでのdomain randomisation — 直立割合
Trained on groundcontact; backlash and rollers are the authors' own variants the policies never saw.学習はgroundcontactモデル。backlashとrollersは作者が公開する別モデルで、policyは一度も見ていません。
groundcontactgroundcontactnominal policy, training model: upright 47%47%randomised policy: upright 28%28%+ backlash+ backlashnominal policy on held-out backlash model: 38%38%randomised policy: 24%24%rollersrollersnominal policy on held-out rollers model: 73%73%randomised policy: 13%13%
nominal policy通常のpolicyrandomised policy — worseランダム化したpolicy — 悪化
Numbers as a table数値を表で見る
PolicyPolicyreturnreturnupright直立
PPO · 50M344.8 ± 2.90.73
PPO · 25M332.6 ± 5.10.74
PD175.8 ± 0.20.47
nominal / randomised on groundcontact通常 / ランダム化(groundcontact)332.6 / 312.20.47 / 0.28
… on backlash… backlash317.4 / 315.40.38 / 0.24
… on rollers… rollers373.6 / 147.30.73 / 0.13
  • 01PPO beats the PD controller 344.8 vs 175.8 of 500 and stays upright 73% vs 47% of steps, on a 14-servo biped under three seeded shoves per episode.PPOがPD controllerを上回る:500点中344.8 vs 175.8。直立割合は73% vs 47%。14サーボの二足ロボットに、1 episodeあたり3回の押しを与えた条件。
  • 02Only real sensors. 48-dim observation (joints, gyro, gravity, last action); the reward was rebuilt after three of four penalties measured under 0.25% of the return.入力は実機センサーの値のみ。48次元のobservation(関節・ジャイロ・重力・直前のaction)。rewardは、4つのpenaltyのうち3つがreturnの0.25%未満と計測されたため再設計。
  • 03Training was repaired mid-run. Chunk 1's step size ran away (clip fraction 0.73, KL spikes above 50); chunk 2 resumed with a KL stop and every seed improved.学習は途中で修正。chunk 1でstep sizeが暴走(clip fraction 0.73、KLが50超)。chunk 2はKL stopを入れて再開し、全seedで改善。
  • 04Domain randomisation made it worse. On two held-out physics models the randomised policy braces in a crouch: upright 28% vs 47%, and 13% on rollers. Reported as a negative result.Domain randomisationは悪化。2つのheld-out物理モデルで、ランダム化したpolicyはしゃがんで踏ん張る挙動に:直立28% vs 47%、rollersでは13%。ネガティブな結果としてそのまま報告。
  • 05Runs on CPU end to end. ≈3,300 env-steps/s in the training loop, 50M steps in 2.4 h; ONNX export 0.033 ms p99 on one thread. Getting up after a fall is still unshown.すべてCPUで完結。学習ループで約3,300 env-steps/s、50M stepを2.4時間。ONNX exportは1スレッドでp99 0.033 ms。転倒後の起き上がりは未達成。
01

In plain words概要

Teaching a small biped to stay up and get back up, on a laptop CPU, after checking that it is possible at all.

小型二足ロボットに「倒れずに立ち続け、倒れたら起き上がる」ことをノートPCのCPUで学習させます。まず、それがそもそも可能かを確かめてから始めます。

The Microduck is a small open-source walking robot. Its authors publish the physics model, the meshes, and a training setup — which requires a CUDA GPU, because that is how modern robot-learning frameworks run thousands of simulated robots in parallel.

Microduckは小型のオープンソース歩行ロボットです。作者は物理モデル、メッシュ、学習環境を公開していますが、 その学習環境にはCUDA GPUが必要です。最近のrobot learningフレームワークは、GPU上で数千台のシミュレーションロボットを並列に動かす前提だからです。

There is no GPU here and no hardware to buy. So this project runs the same physics model in plain CPU MuJoCo, parallel across processes instead of GPU threads, inside a budget of eight of the machine's fourteen cores, on a laptop that has other work to do.

ここにはGPUもなく、実機を買う予定もありません。そこで本プロジェクトでは、同じ物理モデルを通常のCPU版MuJoCoで動かし、 GPUスレッドの代わりにプロセスで並列化します。使うのは14コア中8コアまで。他の作業も抱えたノートPC上での実行です。

The task is not walking. It is standing still under three seeded shoves per five-second episode and getting back up when one of them wins. Every episode runs its full 250 steps, so a policy that lies down is scored for lying down instead of being cut off early.

タスクは歩行ではありません。5秒間のepisodeごとにseedで決まる3回の押し(push)を受けながら立ち続け、 押しに負けて倒れたら起き上がることです。どのepisodeも必ず250 step最後まで走らせるため、寝転んだpolicyは途中で打ち切られず、寝転んだまま採点されます。

The first two steps train nothing, on purpose. The third trains.

最初の2ステップでは、意図的に何も学習させません。学習は3つ目からです。

What each step is for各ステップの目的

Step 1 is a feasibility gate: is this machine fast enough to train a policy at all, and what is the honest number to budget against? Step 2 is the environment contract: what does the policy see, what does it emit, when does an episode end, and what number does it have to beat? Both were written before any learning code, because a training run that fails is uninterpretable if you cannot rule out the machine and the environment first. Step 3 re-decides the reward on the numbers step 2 produced, then runs PPO against the PD controller. Step 4 randomises the physics and measures what that is worth on held-out models; step 5 exports the policy. Step 3 has run its full 50M steps and step 4 the first half of its own; steps 4 and 5 are measured on step 3's policy at the same halfway point.

Step 1は実現可能性の判定(feasibility gate)です。このマシンでそもそもpolicyを学習できる速度が出るのか、 計画の前提にすべき正直な数値はいくつか、を確かめます。Step 2は環境の契約(environment contract)です。 policyが何を見て何を出力するか、episodeがいつ終わるか、超えるべき数値は何かを定めます。 どちらも学習コードより先に書きました。マシンと環境の問題を先に排除できなければ、学習が失敗しても原因を解釈できないからです。 Step 3ではstep 2で得た数値をもとにrewardを決め直し、PD controllerを相手にPPOを走らせます。 Step 4ではphysicsをランダム化し、held-outモデルでその効果を計測します。Step 5ではpolicyをexportします。 Step 3は50M stepをすべて完走し、step 4は自身の前半を終えています。Step 4と5は、step 3のpolicyの同じ中間時点のものを使って計測しています。

Why "sim-to-sim" is the real target本当の目標が「sim-to-sim」である理由

The robot's authors ship four physics variants of the same robot — including one with a passive backlash joint inserted in series with every actuated joint, which is a much more realistic model of a cheap hobby servo's slop. All four expose the same fourteen actuators, in the same order, under the same names.

ロボットの作者は、同じロボットの物理モデルを4種類公開しています。その中には、駆動関節すべてに受動的なbacklash関節を直列に挟んだモデルもあり、 安価なホビーサーボのガタをはるかに現実的に再現しています。4種類とも、同じ14個のアクチュエータを同じ順番・同じ名前で持っています。

That means a policy trained on one variant can be run on another with no remapping at all. The held-out physics is supplied by the robot's own authors rather than invented here — a far stronger position than randomising some parameters and then testing on the same randomisation, which is the standard and much weaker version of this experiment.

つまり、あるモデルで学習したpolicyを、対応付けを一切変えずに別のモデルで動かせます。held-outのphysicsはここで作ったものではなく、 ロボットの作者自身が提供したものです。いくつかのパラメータをランダム化し、同じランダム化の上でテストするという一般的な(そしてずっと弱い)実験より、 はるかに強い立場で評価できます。

Terms used on this pageこのページの用語

policyThe function mapping what the robot senses to what it commands. This is the thing being learned.ロボットが感知した情報を指令値に変換する関数。学習の対象そのものです。
observationThe vector the policy reads each control step — here 48 numbers: joint positions and velocities, the previous action, the gyro, and gravity in body frame.policyが制御stepごとに読むベクトル。ここでは48個の数値で、関節の位置と速度、直前のaction、ジャイロ、胴体座標系での重力方向です。
action scaleHow far one policy output is allowed to move a joint away from its neutral position. Too large and the robot thrashes.policyの1回の出力で、関節を基準姿勢からどこまで動かせるか。大きすぎるとロボットが暴れます。
PD controllerA fixed control law: command proportional to position error plus a term proportional to how fast that error is changing. No learning.固定の制御則。位置誤差に比例する項と、その誤差の変化速度に比例する項の和を指令します。学習はしません。
env-steps/sSimulation throughput. It is the number that decides whether training takes an hour or a week.シミュレーションのthroughput。学習が1時間で終わるか1週間かかるかを決める数値です。
domain randomisationVarying physics and appearance during training so the policy is not tuned to one exact model of the robot.学習中にphysicsや見た目を変化させ、policyがロボットの特定の1モデルだけに合わせ込まれないようにする手法。
system IDMeasuring the real actuator to find out how it actually behaves, which then sets an honest range to randomise over.実際のアクチュエータを計測して実際の挙動を調べること。その結果が、ランダム化の妥当な範囲を決めます。
sim-to-realThe gap between a policy that works in simulation and one that works on the hardware. The whole point of the exercise.シミュレーションで動くpolicyと、実機で動くpolicyとの差。この取り組み全体の目的です。
PPOProximal Policy Optimization. The policy is nudged toward actions that scored above expectation, with each update clipped so one batch cannot move it far.Proximal Policy Optimization。期待より高く評価されたactionの方向へpolicyを少しずつ寄せます。各更新はclipされ、1つのbatchでpolicyが大きく動かないようにします。
GAEGeneralised advantage estimation: how much better than expected each step went, blended over future steps by a discount γ and a mixing factor λ.Generalised advantage estimation。各stepが期待よりどれだけ良かったかを、割引率γと混合係数λで将来のstepにわたって合成したものです。
running normaliserA running mean and variance of every observation seen, used to put all 48 channels on the same scale before the network sees them. It is saved with the policy.これまでに見たすべてのobservationの移動平均と分散。ネットワークに入る前に48チャネルすべてのスケールを揃えるために使い、policyと一緒に保存します。
truncation vs terminationAn episode that ends because time ran out (truncation) still has a future worth something; one that ends because the task failed (termination) does not. Every episode here is a truncation.時間切れで終わったepisode(truncation)には、その先にもまだ価値があります。タスク失敗で終わったepisode(termination)にはありません。ここではすべてのepisodeがtruncationです。
clip fraction / KLTwo per-update health readings: how many samples hit PPO's clip, and how far the new policy moved from the old one. Both high means the step size is aggressive.更新ごとの2つの健全性指標。PPOのclipに当たったサンプルの割合と、新しいpolicyが古いpolicyからどれだけ動いたか。両方が高ければstep sizeが攻めすぎです。
held-out physicsRobot models the policy is evaluated on but never trained on. Here they come from the robot's own authors.評価には使うが学習には一度も使わないロボットモデル。ここではロボットの作者自身が提供したものです。
ONNXA portable file format for a trained network. The exported graph runs in ONNX Runtime with no PyTorch on the robot.学習済みネットワークの可搬なファイル形式。exportしたグラフはONNX Runtimeで動き、ロボット側にPyTorchは不要です。
02

The six steps6つのステップ

Where each step stands: five measured, step 3 at its full 50M steps, step 4 on the first half of its training, one waiting for hardware.

各ステップの現状。5つは計測済みで、step 3は50M stepを完走、step 4は学習の前半まで、残る1つは実機待ちでした。

step 1 · donestep 1 · 完了Feasibility gate実現可能性の判定 Asset fetch, model inspection, CPU throughput scaling, sustained-load decay, drop tests. アセット取得、モデル調査、CPU throughputのスケーリング、連続負荷での低下、落下テスト。 PASS · physics rate ≈13 300 steps/s合格 · 物理演算速度 ≈13 300 steps/s
step 2 · donestep 2 · 完了Environment contract環境の契約 48-dim observation, 14-dim action, 50 Hz, fixed-length episodes with seeded pushes, multiprocess vector env. 48次元のobservation、14次元のaction、50 Hz、seed付きpushのある固定長episode、マルチプロセスのvector env。 27 tests · PD baseline 108.7 (v1)テスト27件 · PD baseline 108.7(v1)
step 3 · measuredstep 3 · 計測済みReward v2 and PPOReward v2とPPO Reward re-decided on step 2's measurements; PD baseline re-measured at 175.2; PPO with a running normaliser and truncation bootstrap, 50M steps in two chunks, the second with a KL stop. Step 2の計測結果でrewardを決め直し、PD baselineを再計測して175.2。running normaliserとtruncation bootstrap付きのPPOで50M stepを2つのchunkに分けて学習し、2つ目はKL stop付き。 PD 175.2 to beat · results in section 13超えるべきPD 175.2 · 結果は13節
step 4 · chunk 1 measuredstep 4 · chunk 1計測済みDomain randomisationDomain randomisation Servo ranges from the four measured fits, mass, friction, latency, noise. Evaluated on two held-out models from upstream. 4つの実測フィットから取ったサーボの範囲、質量、摩擦、latency、ノイズ。本家の2つのheld-outモデルで評価。 did not help · results in section 15効果なし · 結果は15節
step 5 · measuredstep 5 · 計測済みONNX exportONNX export Normaliser folded into the graph, verified on rollout observations, one-thread latency. Normaliserをグラフに組み込み、rolloutのobservationで検証し、1スレッドでのlatencyを計測。 p99 0.033 ms on the chunk-1 policychunk 1のpolicyでp99 0.033 ms
step 6 · droppedstep 6 · 中止The physical robot実機ロボット Run the exported policy on a real Microduck. Dropped: the hardware will not be bought, so step 4's held-out-physics table is the only sim-to-real evidence this project carries. exportしたpolicyを実機のMicroduckで動かす予定でした。実機は購入しないため中止です。したがって、本プロジェクトのsim-to-realの根拠は、step 4のheld-out physicsの表だけです。 not planned予定なし
03

Every file, and what it is for全ファイルとその役割

Every source file, how big it is, and the single job it owns.

すべてのソースファイルについて、その規模と、担当する唯一の役割をまとめます。

FileファイルLines行数What it does役割
common.py211 The shared contract. The variant filenames including the two held-out ones; the fourteen actuator names written out so a test can catch an upstream reordering; the 500 Hz / 50 Hz timing; the four measured actuator classes; and actuated_qpos_index(), which computes the strided joint index instead of assuming a contiguous slice.共有の契約。2つのheld-outを含むモデルのファイル名、本家側の並び替えをテストで検出できるよう明記した14個のアクチュエータ名、 500 Hz / 50 Hzのタイミング、実測された4種類のアクチュエータクラス、そして連続したスライスを前提とせず、飛び飛びの関節インデックスを計算するactuated_qpos_index()。
env.py406 The environment. The 48-dim observation layout and its per-group scaling, the residual action mapping, both reward versions behind reward="v1" and "v2", the push schedule, the action-latency buffer, the sensor noise the environment applies itself, and the mj_forward that keeps every observation channel on one timestamp.環境本体。48次元observationの配置とグループごとのスケーリング、残差(residual)方式のactionの写像、 reward="v1"と"v2"で切り替える2つのrewardバージョン、pushのスケジュール、actionのlatencyバッファ、環境が自前で加えるセンサーノイズ、 そしてobservationの全チャネルを同じ時刻に揃えるmj_forward。
vec_env.py190 A hand-written multiprocess vector environment. Forks N workers over pipes, one environment each, capped at the eight-thread budget. Carries truncated, terminated and the terminal observation across so the training loop can bootstrap correctly.自作のマルチプロセスvector環境。パイプ経由でN個のworkerをforkし、各workerが環境を1つ持ちます。上限は8スレッドの予算内です。 学習ループが正しくbootstrapできるよう、truncated、terminated、終端のobservationを受け渡します。
ppo.py442 Step 3. The PPO loop from the earlier cart-pole project, plus vectorised rollouts, a Welford running normaliser that is part of the policy, the truncation bootstrap, chunked checkpoints with exact resume, a throughput monitor that says when the rate falls 20% below the first five updates, and a box check that refuses to start on a busy machine.Step 3。以前のcart-poleプロジェクトのPPOループに、ベクトル化したrollout、policyの一部としてのWelford方式running normaliser、 truncation bootstrap、正確に再開できるchunk単位のcheckpoint、速度が最初の5回の更新より20%下がったら知らせるthroughputモニタ、 マシンが混んでいると開始を拒否するチェックを加えたものです。
metrics.py90 Return, survival, recovery and time-to-recover, defined once. Training-time evaluation and eval_policy.py both import it, so the curve and the final table cannot disagree.Return、survival、recovery、復帰時間を一か所で定義します。学習中の評価とeval_policy.pyの両方がこれをimportするため、 学習曲線と最終的な表が食い違うことはありません。
eval_policy.py91 100 episodes × 5 seeds, deterministic mean actions, the PD controller on the same seeds, on any model variant. Episode j of seed s is reset with s·100000 + j, so any row can be replayed.100 episodes × 5 seeds、決定論的な平均action、同じseedでのPD controller、任意のモデルで評価します。 seed sのepisode jはs·100000 + jでresetするため、どの行も再現できます。
dr.py156 Step 4. The randomisation ranges, taken from the model rather than chosen: each of five servo parameters between the min and max of the four fits, mass ±15%, floor friction 0.6–1.2×, latency 0–2 steps, initial noise up to 0.3 rad, sensor noise at upstream's declared magnitudes. Nominal values captured once so nothing compounds; restore() is exact.Step 4。ランダム化の範囲は、恣意的に選ばずモデルから取ります。5つのサーボパラメータはそれぞれ4つのフィットの最小値〜最大値、 質量±15%、床の摩擦0.6–1.2×、latency 0–2 step、初期ノイズ最大0.3 rad、センサーノイズは本家が宣言した大きさ。 公称値は一度だけ記録するため変化が累積せず、restore()で正確に元に戻ります。
eval_gap.py62 Runs a nominal-trained and a DR-trained policy on the training model and both held-out models and writes the gap table.公称モデルで学習したpolicyとDRで学習したpolicyを、学習用モデルと2つのheld-outモデルで動かし、差分の表を出力します。
export_onnx.py140 Step 5. One graph from the raw 48-dim observation to the 14-dim mean action with the normaliser folded in; 1,000 rollout observations through both paths first, then one-thread batch-1 latency.Step 5。normaliserを組み込み、生の48次元observationから14次元の平均actionまでを1つのグラフにします。 まず1,000個のrolloutのobservationを両方の経路に通して照合し、そのあと1スレッド・batch 1のlatencyを計測します。
baseline.py193 The PD controller commanded to hold the standing pose over twenty seeds under either reward, plus the per-term shares, the fallen-region reward statistics and the observation-group scales. It prints the factor that would give joint_vel a 5% share, which is how the v2 weight was set.立ち姿勢を保持するよう指令したPD controllerを、どちらのrewardでも20 seedで評価します。項ごとの割合、倒れた領域でのreward統計、 observationグループごとのスケールも出力します。joint_velの割合を5%にする係数も表示し、v2の重みはこれで決めました。
bench.py466 The CPU benchmark. Brackets every configuration with a single-process reference window and refuses to certify a table if that reference drifts more than 10% or trends; samples /proc/<pid>/stat twice to measure what else is on the box; refuses more than eight processes without an explicit flag.CPUベンチマーク。各構成を単一プロセスの基準計測で前後から挟み、その基準が10%以上ずれるか傾向を持つ場合は表を認定しません。 /proc/<pid>/statを2回サンプリングして他に何が動いているかを測り、明示的なフラグなしに8プロセスを超える実行は拒否します。
bench_wrapper.py97 Measures the Python environment wrapper's cost against a bare mj_step loop doing the same substeps, back to back in one process.同じsubstepを回す素のmj_stepループと比べて、Python環境wrapperのコストを1プロセス内で続けて計測します。
boxcheck.py34 Reads the one-minute load average and refuses to start a run above 4 unless forced.1分間のload averageを読み、4を超えていれば強制指定がない限り実行を拒否します。
drop_test.py159 Two physics sanity tests, a limp drop and a commanded pose hold, with a settle detector and a rendered filmstrip.物理の妥当性を見る2つのテスト(脱力状態での落下と、指令した姿勢の保持)。静止判定と、レンダリングしたコマ送り画像付き。
inspect_model.py92 Dumps what each MJCF variant contains: degree-of-freedom counts, joints, actuators, sensors, contact masks.各MJCFモデルの中身(自由度の数、関節、アクチュエータ、センサー、接触マスク)を出力します。
test_model.py · test_env.py161 · 263 The MJCF contract and the twenty-seven environment checks, including the one that rebuilds the observation from its five sources and asserts a bit-for-bit match.MJCFの契約テストと、環境の27項目のチェック。observationを5つのソースから組み直し、ビット単位で一致することを確認するテストも含みます。
test_reward.py133 Pins two v1 episode returns to the last decimal so the 108.7 baseline stays reproducible; checks that v2 has a height gradient everywhere below standing and v1 has none near the floor; checks the v2 weight set from measurement; checks that sensor noise touches only the gyro.v1の2つのepisode returnを最後の桁まで固定し、108.7のbaselineが再現できる状態を保ちます。v2には立位より下のあらゆる高さで勾配があり、 v1には床付近で勾配がないこと、計測から決めたv2の重み、センサーノイズがジャイロにしか影響しないことを確認します。
test_ppo.py129 GAE on a three-step case by hand; the truncation bootstrap arriving through the reward and what zeroing it would cost; the normaliser against numpy; checkpoint resume landing on the same step with the same weights.3 stepの例を手計算したGAE、rewardを通じて入るtruncation bootstrapと、それをゼロにした場合の損失、numpyと照合したnormaliser、 checkpointから再開すると同じstep・同じ重みになること。
test_dr.py123 Ranges are the min/max of the four fits with friction loss the widest; 300 samples stay inside; apply and restore are exact and nothing compounds; both held-out models rest on the floor and keep the actuator order; walk_backlash is refused as a held-out model.範囲が4つのフィットの最小・最大で、friction lossが最も広いこと。300サンプルが範囲内に収まること。適用と復元が正確で累積しないこと。 2つのheld-outモデルがどちらも床の上に静止し、アクチュエータの順序を保つこと。walk_backlashがheld-outモデルとして拒否されること。
test_export.py68 The exported graph agrees with the training path and disagrees with the un-normalised actor, so the normaliser is provably inside it; clipping survives export.exportしたグラフが学習時の経路と一致し、正規化なしのactorとは一致しないこと。これでnormaliserが確かに中に入っていると証明できます。clipもexport後に残ること。
fetch_assets.sh · verify.sh53 · 135 The pinned upstream fetch (nothing under assets/ is committed; the 3D files are CC BY-SA-NC), and the tiered check script: cheap structural tiers first, the benchmark last.固定したコミットから本家のファイルを取得するスクリプト(assets/以下は何もコミットしていません。3DファイルはCC BY-SA-NCです)と、 段階的なチェックスクリプト。軽い構造チェックを先に、ベンチマークを最後に実行します。
Dockerfile · .github/workflows/ci.yml24 · 48 A CPU-only image that runs the asset-free tests, and continuous integration that fetches the assets and runs every suite on each push.アセット不要のテストを実行するCPU専用イメージと、push毎にアセットを取得して全テストスイートを実行するCI。
runs/ · out/ · docs/ISSUES.md— Benchmark JSON, both baseline files, training logs and checkpoints, drop-test filmstrips, and the seven open questions written as issues.ベンチマークのJSON、2つのbaselineファイル、学習ログとcheckpoint、落下テストのコマ送り画像、そしてissueとして書いた7つの未解決の問い。
04

Step 1 — the feasibility gate, and four wrong answersStep 1 — 実現可能性の判定と、4つの誤った答え

The throughput measurement that decides whether the project is feasible, and the four wrong numbers before the right one.

プロジェクトが実現可能かを決めるthroughputの計測と、正しい数値にたどり着くまでの4つの誤った数値。

The unit, and the gate単位と判定基準

One environment step is one 50 Hz control decision, which is ten physics steps at the model's 500 Hz timestep. Reinforcement-learning budgets are quoted in environment steps, so the gate is written in them, and it was set before measuring:

1 environment stepは50 Hzの制御判断1回分で、モデルの500 Hzのtimestepでは物理step 10回にあたります。 強化学習の予算はenvironment stepで表すのが通例なので、判定基準もその単位で書き、計測の前に決めました。

The gate, fixed in advance事前に固定した判定基準

Below 5 000 environment-steps per second at eight processes, walking leaves the scope and the project becomes stand-only.

8プロセスで毎秒5 000 environment step未満なら、歩行はスコープ外とし、立位のみのプロジェクトにする。

MuJoCo's step for a sixteen-body model is single-threaded and small enough to stay in cache, so parallelism has to come from processes. Every worker sets OMP_NUM_THREADS=1 before MuJoCo loads, or the workers each spawn a thread pool and fight each other.

16ボディのモデルに対するMuJoCoのstepはシングルスレッドで、キャッシュに収まるほど小さいため、並列化はプロセスで行うしかありません。 各workerはMuJoCoを読み込む前にOMP_NUM_THREADS=1を設定します。そうしないと、各workerがそれぞれスレッドプールを立ち上げて奪い合います。

Scaling, 20 s per configurationスケーリング(各構成20 s)

Processesプロセス数env-steps/sPer process1プロセスあたりSpeed-up高速化率Efficiency効率
16 5216 5211.00×100 %
212 5556 2781.93×96 %
421 4245 3563.29×82 %
828 7493 5944.41×55 %

Gate: PASS, 5.7× over. The single-process reference window between configurations held to within 6 % across the whole sweep, so the four rows were measured on the same machine as each other. Run twice, seven minutes apart: the eight-process row came back 28 960 and 28 749 — 0.7 % apart.

判定:合格(基準の5.7倍)。構成の合間に挟んだ単一プロセスの基準計測は、スイープ全体を通じて6 %以内に収まりました。 つまり4行とも、同じ状態のマシンで計測できています。7分空けて2回実行したところ、8プロセスの行は28 960と28 749で、差は0.7 %でした。

The first set of numbers, and why they are the interesting ones最初の数値と、それが興味深い理由

Step 1 originally certified a different table: 6 666 / 9 801 / 14 275 / 20 474 — same code, same variant, same thread budget, same laptop on mains power. The difference is a host state the guest cannot read.

Step 1は当初、別の表を認定していました。6 666 / 9 801 / 14 275 / 20 474です。コードもモデルもスレッド予算も同じで、同じノートPCをAC電源につないだ状態でした。 違いは、ゲストOSからは読み取れないホスト側の状態です。

Read the shape rather than the totals, because the shape is the diagnosis. Single-process is flat within run-to-run spread; every multi-process row is roughly a third lower. Thermal throttling decays with time under load and would have moved the reference windows within the sweep. Another process competing for CPU depresses every row, the single-process one included. A sustained all-core power limit does exactly this and nothing else — single-core turbo never depended on the all-core budget, every additional core did.

合計値ではなく形を読んでください。形がそのまま診断になります。 単一プロセスの値は実行ごとのばらつきの範囲内で変わらず、マルチプロセスの行はどれも約3分の1低くなっています。 熱によるスロットリングなら負荷をかけ続けるほど低下するので、スイープの途中で基準計測の値が動いたはずです。 他のプロセスがCPUを奪っているなら、単一プロセスの行も含めてすべての行が下がります。 全コアの持続電力制限なら、まさにこの形になり、それ以外の形にはなりません。シングルコアのターボは全コアの電力予算に依存しませんが、コアを追加するたびにその予算に依存するからです。

The tell that should have caught it at the timeその時点で気づくべきだった兆候

Two processes on a fourteen-core machine returned 74 % efficiency. There is no core-count explanation for that — two workers cannot contend for cores when twelve are idle. It was read as a property of the chip, and a scaling story was built on top of it. It was a property of the machine's power state that afternoon.

14コアのマシンで2プロセスの効率が74 %でした。これはコア数では説明できません。12コアが空いているのに、2つのworkerがコアを奪い合うことはないからです。 当時はこれをチップの特性と読み、その上にスケーリングの説明を組み立ててしまいました。実際には、その日の午後のマシンの電源状態によるものでした。

There was a deeper excursion in the same window: the single-process rate fell to 3 069 env-steps/s — 46 % of certified — with the box idle. The benchmark's reference bracket called that run stable, and it was right to: the tool detects a reference that moves during a sweep, and this was a machine that was uniformly slow for the whole sweep. That is a real limitation of the method — a drift guard cannot catch a bias that is already in place when the first window opens. The only defence is an absolute expectation, which is what the certified table now provides.

同じ期間に、さらに大きな落ち込みもありました。マシンがアイドル状態なのに、単一プロセスの速度が3 069 env-steps/s(認定値の46 %)まで落ちたのです。 ベンチマークの基準計測はこの実行を安定と判定しましたが、それは正しい判定でした。このツールが検出するのはスイープ中に動く基準であり、 このときのマシンはスイープ全体を通じて一様に遅かったからです。これはこの手法の本当の限界です。 ドリフト検出では、最初の計測が始まる時点ですでに存在するバイアスは捉えられません。 唯一の防御策は絶対値としての期待値を持つことで、今は認定済みの表がその役割を果たしています。

Both recovered after a check on the Windows side. Which setting changed is not recorded, and that is the finding rather than a hole in it: a WSL2 guest cannot read power mode, charger wattage, package power or core frequency, so host state is unrecoverable after the fact and has to be written down at measurement time or lost.

どちらもWindows側を確認したあとで回復しました。どの設定が変わったのかは記録されていませんが、それ自体が発見であり、記録の欠落ではありません。 WSL2のゲストからは電源モード、充電器のワット数、パッケージ電力、コア周波数を読み取れません。したがってホストの状態は後から復元できず、計測時に書き留めておかなければ失われます。

What a training run actually gets — three reasons it is not 28 749学習で実際に出る速度 — 28 749にならない3つの理由

First, the sweep pauses and a training run does not. The sweep brackets every configuration with a reference window, which lets the processor package cool. Holding eight processes flat out for four minutes:

1つ目は、スイープには休みがあるが学習にはないことです。スイープでは各構成を基準計測で挟むため、その間にプロセッサが冷えます。 8プロセスを4分間全力で回し続けた結果は次のとおりです。

Elapsed経過時間env-steps/svs peakピーク比
20 s33 349—
60 s≈30 900−7 %
120 s27 313−18 %
180 s≈24 600−26 %
240 s19 256−42 %

Monotonic in all twelve windows, and the final step is −1.0 %, so the curve has just about flattened. Run in the reduced-power state, the same test read 27 905 in its first window and then eleven windows at 20 152 ± 287 — a 5 % spread with no trend. The two runs are worth more together than separately: their floors agree to 3.9 %. The host power state changed how much burst the machine had to spend, not where it ended up.

12区間すべてで単調に減少し、最後の変化は−1.0 %なので、曲線はほぼ横ばいになっています。 電力が制限された状態で同じテストを行うと、最初の区間は27 905、その後の11区間は20 152 ± 287でした。ばらつきは5 %で、傾向はありません。 2つの実行は、組み合わせることで個別よりも多くを語ります。底の値が3.9 %以内で一致しているのです。 ホストの電源状態が変えたのは、マシンが使える瞬間的な余力の量であって、最終的に落ち着く値ではありませんでした。

Second, the benchmark does not measure the thing that gets trained. Its worker is a bare mj_step loop on the walk variant. The task runs the Python environment wrapper on groundcontact, and both of those cost:

2つ目は、ベンチマークが学習対象そのものを計測していないことです。ベンチマークのworkerはwalkモデル上の素のmj_stepループです。 実際のタスクではgroundcontact上でPython環境wrapperを動かしており、そのどちらにもコストがかかります。

Single process, 10 s windows単一プロセス、10 s区間Bare素の mj_stepThrough the wrapperwrapper経由Wrapper costwrapperのコスト
walk7 5055 69232 %
groundcontact5 2234 17525 %

About seven points of that 25 % is a correctness fix rather than overhead — see the defect list below — and it is explicitly not an optimisation target.

この25 %のうち約7ポイントはオーバーヘッドではなく正しさのための修正です(後述の不具合リスト参照)。明確に最適化の対象外としています。

Third, groundcontact does not scale like walk. The variant with five times the ground-collidable geometry loses efficiency earlier and ends thirteen points lower at eight processes: 42 % against 55 %. That is the signature of a shared resource outside the core — cache or memory bandwidth — and against a performance-core/efficiency-core split, which would not care how much contact solving each process does.

3つ目は、groundcontactがwalkのようにはスケールしないことです。床と衝突しうる形状が5倍あるこのモデルは、 より早く効率が落ち、8プロセスでは13ポイント低い42 %(walkは55 %)で終わります。これはコアの外側にある共有資源、 つまりキャッシュかメモリ帯域が原因であることを示しています。Pコア/Eコアの違いが原因なら、各プロセスの接触計算の量には左右されないはずなので、その説とは合いません。

The number a training run should be budgeted on学習計画の前提にすべき数値

Measured directly, on the variant that gets trained, under the load it gets trained at, over eighteen windows. The shape is not the walk shape: groundcontact undershoots at window 11 and then climbs back for six windows, ending 11 % above its minimum and still rising +0.6 % at the last window. So this is not a steady state either, and eighteen windows found the dip rather than the floor.

学習に使うモデルで、学習時と同じ負荷をかけ、18区間にわたって直接計測しました。形はwalkとは異なります。 groundcontactは11区間目で落ち込んだあと6区間かけて戻り、最小値より11 %高い値で終わりました。最後の区間でもまだ+0.6 %上昇していました。 したがってこれも定常状態ではなく、18区間で見つかったのは底ではなく一時的な落ち込みです。

Window 11, the global minimum, is contaminated — a ten-second two-process benchmark of the author's own overlapped it — and is excluded. Excluding it, the last nine windows run 15 806 to 17 266, a 9 % band, mean 16 590. That band is the honest uncertainty, and the budget uses its middle rather than its top.

全体の最小値である11区間目は汚染されています。作者自身が走らせた10秒間・2プロセスのベンチマークと重なったため、除外しました。 それを除くと、最後の9区間は15 806〜17 266で、幅は9 %、平均は16 590です。この幅が正直な不確かさであり、計画には上端ではなく中央の値を使います。

16 590 × 0.799 (wrapper) = ~13 300 env-steps/s
range 12 600 – 13 800 across the band

Read on before budgeting on it. That is the physics with random actions. When step 3 ran the training loop itself it measured about 3 300 env-steps/s; the reasons are in the step 3 section below.

この数値で計画を立てる前に、続きを読んでください。これはランダムなactionでの物理演算の速度です。 Step 3で学習ループそのものを動かしたときの計測値は約3 300 env-steps/sでした。理由は後述のstep 3の節にあります。

The correction that went the other way — and it is the most reusable finding here逆方向への修正 — ここで最も応用の利く発見

The tally for this one number, at the end of step 1, was 28 749 → 18 400 → 8 000 → 13 300. Three corrections downward from unmeasured optimism, and then one upward, because the 8 000 figure had been built by multiplying an already-depressed measurement by a sustained derate borrowed from a different variant — the derate was counted twice. Step 3 added a fifth revision, downward again, when the loop itself ran.

Step 1の終了時点で、この1つの数値の変遷は28 749 → 18 400 → 8 000 → 13 300でした。 計測していない楽観に対して3回下方修正し、そのあと1回上方修正しています。8 000という値は、すでに低下していた計測値に、 別のモデルから借りた持続時の低下率を掛けて作ったもので、低下分を二重に数えていたからです。 Step 3で学習ループそのものを動かした際に、5回目の修正(再び下方)が加わりました。

Being conservative is not free and it is not automatically honest. An unmeasured pessimistic assumption is the same error as an unmeasured optimistic one, and it costs real scope. The fix in both directions was identical: measure the thing itself instead of composing estimates of its parts.

保守的であることはタダではなく、それだけで誠実になるわけでもありません。計測していない悲観的な仮定は、計測していない楽観的な仮定と同じ誤りであり、 実際のスコープを削ってしまいます。どちらの方向でも対処は同じでした。部分の見積もりを組み合わせるのではなく、対象そのものを計測することです。

Training budget学習予算Planned at ≈13 300 steps/s計画(≈13 300 steps/s)Actual at ≈3 300 steps/s実際(≈3 300 steps/s)
10 M — hyperparameter probe10 M — ハイパーパラメータの試行13 min50 min
50 M — stand + push recovery50 M — 立位 + push からの復帰1.0 h4.2 h → 2 chunks2 chunk
100 M — stand, generous100 M — 立位、余裕を持たせた場合2.1 h → 2 chunks2 chunk8.4 h → 5 chunks5 chunk
400 M — walking gait, upstream-scale400 M — 歩行、本家と同規模8.4 h → 5 chunks5 chunk34 h, out of reach again、再び手の届かない規模

Scope conclusion: stand-and-recover is one comfortable sitting; walking is five chunks. That was the question the gate had to answer, and it survived all four revisions of the number — which is the only reason the revisions were tolerable.

スコープの結論:立位と復帰なら1回の作業で無理なく終わり、歩行は5 chunk必要。 これが判定で答えるべき問いでした。この結論は数値の4回の修正すべてを経ても変わりませんでした。修正が許容できたのは、それが唯一の理由です。

Why efficiency falls to 55 %, and what could not be determined効率が55 %まで落ちる理由と、特定できなかったこと

The chip is an Intel Core Ultra 5 225H: fourteen cores, no hyperthreading, heterogeneous — performance cores alongside efficiency cores. The obvious hypothesis is that workers 5–8 land on the slower ones. Pinning four workers to CPUs 0–3 and then to CPUs 10–13 gave a 15 % difference under load and 6 % single-threaded — far too small for a performance/efficiency split, which would show roughly 2×.

チップはIntel Core Ultra 5 225Hです。14コアでハイパースレッディングはなく、Pコア(高性能)とEコア(高効率)が混在する構成です。 すぐに思いつく仮説は、workerの5〜8番目が遅いコアに割り当てられるというものです。4つのworkerをCPU 0–3に固定した場合とCPU 10–13に固定した場合を比べると、 負荷時で15 %、シングルスレッドで6 %の差でした。P/Eコアの差なら約2倍になるはずなので、小さすぎます。

The explanation is that CPU affinity inside WSL2 does not pin to a physical core. sched_setaffinity binds the process to a virtual CPU, and the hypervisor schedules virtual CPUs onto physical cores on its own. So that experiment cannot answer the question, and neither can any other experiment run from inside the guest.

理由は、WSL2内でのCPU affinityは物理コアへの固定にならないことです。 sched_setaffinityはプロセスを仮想CPUに結びつけるだけで、仮想CPUをどの物理コアに載せるかはハイパーバイザーが独自に決めます。 したがってこの実験ではこの問いに答えられず、ゲスト内から行うほかのどの実験でも答えられません。

Pinning was the wrong instrument. Changing the workload worked. The groundcontact sweep loses efficiency earlier and ends thirteen points lower on the same cores in the same session, and a core-type split cannot produce that — which physical core a process lands on does not depend on how much contact solving it does. That points at cache and memory bandwidth over the other candidates. It is one comparison between two variants, not a proof, and it is recorded as unresolved rather than resolved with the plausible-sounding answer. What matters operationally is settled anyway: eight processes is the right choice, because it delivers the most total throughput even at 55 % efficiency.

コアの固定は道具として不適切でした。有効だったのはワークロードを変えることです。 同じセッション・同じコアで、groundcontactのスイープはより早く効率が落ち、13ポイント低く終わります。 コアの種類の違いではこうはなりません。プロセスがどの物理コアに載るかは、接触計算の量とは無関係だからです。 このことから、他の候補よりもキャッシュとメモリ帯域が疑わしいと言えます。ただし2つのモデルを1回比べただけで証明ではないため、もっともらしい答えで決着させず、未解決として記録しています。 運用上重要な点はいずれにせよ決着しています。効率が55 %でも合計throughputが最も高いので、8プロセスが正しい選択です。

05

Three things about the model that changed the plan計画を変えたモデルの3つの性質

Three properties of the robot model that forced the plan to change.

ロボットモデルの3つの性質が、計画の変更を余儀なくさせました。

01

On the walk model, only the feet can touch the groundwalkモデルでは、地面に触れられるのは足だけ

MuJoCo lets two geometries collide only if one's contype shares a bit with the other's conaffinity. In the walking model the floor is 1/1 and the two foot geometries are 1/1 — but every other body and limb geometry is 2/2. Two and one share no bits, so the trunk, head and legs cannot touch the floor at all.

MuJoCoでは、一方のcontypeともう一方のconaffinityに共通のビットがある場合にだけ2つの形状が衝突します。 歩行用モデルでは床が1/1、両足の形状も1/1ですが、それ以外の胴体や四肢の形状はすべて2/2です。2と1には共通のビットがないため、胴体・頭・脚は床にまったく触れられません。

VariantモデルGeometries that can touch the floor床に触れられる形状Trunk at rest静止時の胴体
walk2 (the feet)(両足)−10.5 cm — through the floor— 床を突き抜ける
groundcontact10+3.2 cm — resting on it— 床の上に静止

This is deliberate upstream: a walking task ends the episode the instant the robot falls, so what happens afterwards never has to be physical, and dropping the collision geometry makes the simulation faster. It makes walk the wrong model for anything involving lying on the ground or getting back up. The failure is silent — nothing raises, no contact is reported, the robot just sinks, and a reward function reading trunk height would have been quietly rewarded for falling through the world. Decision: the task uses groundcontact.

これは本家の意図的な設計です。歩行タスクではロボットが倒れた瞬間にepisodeが終わるため、その後の挙動が物理的に正しい必要はなく、衝突形状を減らせばシミュレーションも速くなります。 しかしそのせいで、walkは地面に横たわる・起き上がるといった動作を扱うには不適切なモデルになります。しかもこの失敗は表に出ません。 例外も出ず、接触も報告されず、ロボットはただ沈んでいきます。胴体の高さを読むreward関数なら、世界の底へ落ちていくことに対して黙って報酬を与えていたはずです。 決定:タスクにはgroundcontactを使う。

02

The backlash variant renumbers the state vector — and it is the evaluation modelbacklashモデルは状態ベクトルの番号を変える — しかもそれが評価用モデル

VariantモデルnqnvPassive joints受動関節Action spaceAction空間
walk21200identical同一
groundcontact21200identical同一
rollers25244identical同一
walk_backlash353414identical同一

That the action space is identical across all four is what makes the sim-to-sim step possible. But walk_backlash inserts a passive joint in series with every actuated joint, and those passive joints interleave: the actuated joint angles sit at state indices 7, 9, 11 … 33, not the contiguous 7…20 they occupy elsewhere.

4つのモデルでaction空間が同一であることが、sim-to-simのステップを可能にしています。しかしwalk_backlashはすべての駆動関節に受動関節を直列に挟み、 それらが交互に並びます。そのため駆動関節の角度は状態インデックスの7, 9, 11 … 33に位置し、他のモデルのような連続した7…20にはなりません。

Any observation builder that hard-codes qpos[7:21] reads a mixture of joint angles and backlash deflections on the evaluation model — plausible numbers, wrong meaning, no error. Measured, the shortcut is wrong on 13 of 14 joints, by up to 0.938 rad. Everything here goes through common.actuated_qpos_index() instead, and a test asserts the stride.

qpos[7:21]を決め打ちするobservationの組み立て処理は、評価用モデル上では関節角度とbacklashのたわみが混ざった値を読んでしまいます。 数値はもっともらしいのに意味が違い、エラーも出ません。計測すると、この手抜きは14関節中13関節で誤っており、誤差は最大0.938 radでした。 ここではすべてcommon.actuated_qpos_index()を経由し、インデックスの間隔はテストで確認しています。

03

Joint friction is the parameter the hardware people disagree about by 6.7×関節の摩擦は、ハードウェア担当者の間で6.7倍も食い違うパラメータ

The upstream properties file ships four fitted actuator classes for the same servo — different people, different benches, all left in the file.

本家のプロパティファイルには、同じサーボに対してフィットしたアクチュエータクラスが4つ入っています。人も計測台も異なり、そのすべてがファイルに残されています。

ClassクラスDampingFriction lossArmaturekpForce limit
chosen_actuator0.0530.00480.00180.5500.96
chosen_actuator_old0.0480.00600.00200.5200.91
chosen_actuator_new0.0410.03200.00200.3860.67
chosen_actuator_antoine0.0440.01300.00170.4300.75
spread (max/min)ばらつき(最大/最小)1.29×6.67×1.18×1.42×1.43×

Damping, armature, gain and torque limit agree to within about 40 %. Friction loss disagrees by a factor of seven. This is a measured parameter uncertainty, not a guess, and it is the honest place to get a domain-randomisation range from — rather than the usual ±20 % around a nominal, chosen because it sounds reasonable. It also says where to spend the randomisation budget: wide on friction, narrow on everything else. A test parses the XML and fails if upstream refits a servo.

Damping、armature、ゲイン、トルク上限は約40 %以内で一致していますが、friction lossは7倍も食い違います。 これは推測ではなく計測されたパラメータの不確かさであり、domain randomisationの範囲を決める根拠として誠実なものです。 それっぽいという理由で選ばれがちな「公称値±20 %」よりも妥当です。ランダム化の予算をどこに使うべきかも示しています。摩擦は広く、それ以外は狭くです。 XMLを解析するテストがあり、本家がサーボを再フィットするとテストが失敗します。

06

Do the physics behave? Two drop tests物理は正しく動くか — 2つの落下テスト

Two physics checks confirming the model falls, settles and collides the way it should.

モデルが正しく落下し、静止し、衝突することを確かめる2つの物理チェック。

Limp — a ragdoll from 25 cm脱力 — 25 cmからのラグドール落下

Actuator gains and biases zeroed so the robot has no muscle at all. Note that commanding ctrl = 0 does not do this: a MuJoCo position actuator produces gain·ctrl + bias₁·qpos + bias₂·qvel, so a zero command is a stiff hold at the zero pose. All three coefficients have to go.

アクチュエータのゲインとバイアスをゼロにして、ロボットから筋力を完全に取り除きます。ctrl = 0を指令してもこうはならない点に注意してください。 MuJoCoの位置アクチュエータはgain·ctrl + bias₁·qpos + bias₂·qvelを出力するため、ゼロ指令はゼロ姿勢での固い保持になります。3つの係数すべてを消す必要があります。

It falls, lands, and comes to rest at trunk height 3.0 cm with 14 contacts and a maximum joint speed of 0.09 rad/s. No sinking, no jitter, no explosion.

ロボットは落下・着地し、胴体の高さ3.0 cm、接触点14、最大関節速度0.09 rad/sで静止しました。沈み込みも振動も発散もありません。

Hold — commanded to the standing pose保持 — 立ち姿勢を指令

The actuators are commanded to the STAND keyframe, which is where every training episode will begin. It holds for 0.79 s, then topples, and settles face-down at 4.30 s.

アクチュエータにSTANDのキーフレームを指令します。すべての学習episodeの開始姿勢です。 0.79 s保持したあと倒れ、4.30 sでうつ伏せのまま静止しました。

Why that number mattersこの数値が重要な理由

The shipped PD controller at kp = 0.55 does not hold the pose. So STAND is an unstable equilibrium, standing is a real balancing problem rather than a pose hold, and the baseline the learned policy has to beat is easy to state: falls over in 0.79 s.

付属のPD controller(kp = 0.55)では姿勢を保持できません。つまりSTANDは不安定な平衡点であり、 立つことは姿勢の保持ではなく本物のバランス問題です。学習したpolicyが超えるべきbaselineも簡単に言えます。0.79 sで倒れるです。

The same test on walk topples at the identical 0.79 s — the dynamics match until the body reaches the floor — and then sinks to −10.5 cm, which is the contact-mask finding again, visible as a number.

walkで同じテストを行うと、まったく同じ0.79 sで倒れます(胴体が床に届くまでは力学が一致しています)。その後−10.5 cmまで沈みます。 先ほどの接触マスクの発見が、数値として改めて現れたものです。

07

Step 2 — the environment contractStep 2 — 環境の契約

The 48-dimensional observation, the action scaling, and the six reward terms.

48次元のobservation、actionのスケーリング、6つのreward項。

The observation is 48-dim. Step 1 said 61, and 61 was wrongObservationは48次元。Step 1では61としたが、61は誤り

61 is everything the simulator knows about the robot. 48 is everything the robot knows about itself. The difference is thirteen numbers that are free in MuJoCo and do not exist on hardware.

61はシミュレータがロボットについて知っているすべてです。48はロボットが自分自身について知っているすべてです。 その差の13個は、MuJoCoではタダで手に入っても、実機には存在しない数値です。

14joint position関節位置servo present-positionサーボの現在位置
14joint velocity関節速度servo present-velocity — real, and noisy. Included because the servos report it, not because it is cleanサーボの現在速度。実在するがノイズが多い。きれいだからではなく、サーボが出力するから含めている
14previous action直前のactionthe policy's own last output; it is in RAMpolicy自身の直前の出力。RAM上にある
3gyroジャイロIMU rate gyroIMUのレートジャイロ
3projected gravity投影した重力world −Z in the trunk frame — the observable part of attitude胴体座標系で見たワールドの−Z方向。姿勢のうち観測可能な部分
3IMU linear velocityIMUの並進速度a velocimeter. There is no state estimator on the robot, so base linear velocity is not measurable速度計。ロボットに状態推定器がないため、ベースの並進速度は計測できない
3trunk position胴体の位置world-frame xyz — same problem, and no external tracking rig in the loopワールド座標系のxyz。同じ問題があり、外部のトラッキング装置もループに入っていない
4full orientation quaternion姿勢クォータニオン全体yaw is not observable from a gyro and an accelerometer; the model declares no magnetometer, so heading can only be integrated, and driftsジャイロと加速度計からyawは観測できない。モデルに磁気センサーがないため、方位は積分でしか得られず、ドリフトする
3root angular momentumルートの角運動量a MuJoCo computation over the body tree, not a sensorボディツリー全体に対するMuJoCoの計算値で、センサーではない
Why cut them before training rather than after学習後ではなく学習前に削る理由

A policy that reads those learns to depend on them and then has nothing to run on. Since the only claim this project can honestly make is about transfer, the observation is cut down to sensors the robot carries before any training happens. It costs nothing now and cannot be retrofitted later.

これらを読むpolicyはそれに依存するように学習し、実機では動かすための入力がなくなります。本プロジェクトが誠実に主張できるのは転移についてだけなので、 学習を始める前に、observationをロボットが実際に積んでいるセンサーだけに絞りました。今なら何のコストもかからず、後から付け足すことはできません。

One further detail: the gyro is read from the angular-velocity sensor rather than its twin imu_ang_vel. Both sit on the same site and read identically today, because MuJoCo applies a sensor's declared noise only when a flag is set and it is not set here. But angular-velocity is the one upstream put noise="0.005" on — so step 4 flips the flag and gets the noise magnitude the robot's authors chose, instead of one invented to look reasonable.

もう1つ細かい点があります。ジャイロは、同じものが2つあるうちのimu_ang_velではなくangular-velocityセンサーから読みます。 どちらも同じsiteにあり、現状では同じ値を返します。MuJoCoはフラグが立っているときだけセンサーの宣言ノイズを加えますが、ここではそのフラグを立てていないからです。 しかし本家がnoise="0.005"を付けたのはangular-velocityの方です。そのためstep 4でフラグを立てれば、それらしく見えるよう作った値ではなく、ロボットの作者が選んだノイズの大きさが得られます。

A test rebuilds the observation from those five sources and asserts the environment's output matches bit for bit. That is the only way to prove nothing simulator-only leaked in — and it separately checks that the excluded velocimeter is reading a live non-zero signal, so the exclusion is a real one rather than a channel that happens to be zero.

あるテストでは、observationをこの5つのソースから組み直し、環境の出力とビット単位で一致することを確認します。 シミュレータにしかない情報が紛れ込んでいないことを証明する唯一の方法です。さらに別途、除外した速度計が実際にゼロでない値を読んでいることも確認しています。 たまたまゼロのチャネルではなく、本当に除外していることを示すためです。

Actions, and a trap in the model fileActionと、モデルファイルの落とし穴

Fourteen outputs in [−1, 1], applied as residuals around the standing pose:

[−1, 1]の範囲の14個の出力を、立ち姿勢からの残差(residual)として適用します。

target = STAND_pose + 0.35 * action        then clipped to the joint limits

A saturated action moves a joint 0.35 rad — about 22 % of the median joint range — in one 20 ms decision. That number is a hyperparameter, not a derived quantity, and step 3 is set up to report what happens at 0.2 and 0.5.

飽和したactionは、1回20 msの判断で関節を0.35 rad(関節可動域の中央値の約22 %)動かします。この値は導出された量ではなくハイパーパラメータで、 step 3では0.2と0.5の場合の結果を報告できるようにしてあります。

The clip is not decorationclipは飾りではない

ctrlrange on all fourteen actuators is [−10, 10] rad, while the tightest joint limit — hip roll — is ±0.384 rad. Handing a position actuator a 10 rad target raises nothing: it saturates against the joint stop and spends the entire force range holding there. Nothing in the model prevents this and nothing reports it.

14個すべてのアクチュエータのctrlrangeは[−10, 10] radですが、最も狭い関節制限(股関節ロール)は±0.384 radです。 位置アクチュエータに10 radの目標を与えてもエラーは出ません。関節のストッパーに当たって飽和し、出せる力のすべてをそこで押し付けることに使ってしまいます。 モデルにはこれを防ぐ仕組みも、報告する仕組みもありません。

Episodes and pushesEpisodeとpush

250 steps at 50 Hz — five seconds — fixed length with no early termination. The usual locomotion setup ends the episode the moment the robot falls. That is right for walking and wrong here: recovery is half the task, and terminating on a fall makes falling unrecoverable by construction. It also keeps returns comparable — every episode is the same length, so a baseline that topples early gets a low return rather than a short episode that looks cheap.

50 Hzで250 step、つまり5秒間で、途中終了のない固定長です。一般的な移動ロボットの設定では、ロボットが倒れた瞬間にepisodeを終えます。 歩行ならそれで正しいのですが、ここでは誤りです。復帰はタスクの半分であり、転倒で終了させると構造上、転倒から復帰できなくなります。 また固定長にすることでreturnを比較可能に保てます。すべてのepisodeが同じ長さなので、早く倒れるbaselineは「短くて安上がりに見えるepisode」ではなく、低いreturnとして現れます。

Three pushes per episode, drawn from the episode seed: magnitude uniform in 0.15–0.45 m/s applied to the trunk's linear velocity, direction uniform in azimuth, timing uniform but held half a second clear of both ends. A push at t = 0 is an initial condition rather than a disturbance, and one at the buzzer is never recovered from inside the episode.

pushはepisodeごとに3回で、episodeのseedから生成します。大きさは0.15–0.45 m/sの一様分布で胴体の並進速度に加え、方向は方位角について一様、 タイミングも一様ですが、両端から0.5秒は空けています。t = 0のpushは外乱ではなく初期条件になり、終了間際のpushはepisode内で復帰しようがないからです。

The reward — six terms, all logged separatelyReward — 6つの項、すべて個別に記録

Term項Weight重みShape形
upright+1.0trunk z-axis against world up, floored at 0胴体z軸とワールド上向きの比較、下限0
height+1.0Gaussian on trunk height about 0.12 m, σ = 3 cm胴体の高さに対する0.12 m中心のガウス関数、σ = 3 cm
posture−0.10mean squared joint deviation from STANDSTANDからの関節偏差の二乗平均
action rate−0.05mean squared change in actionactionの変化量の二乗平均
joint velocity−2e−4mean squared joint velocity関節速度の二乗平均
effort−0.02mean squared actuator forceアクチュエータ出力の二乗平均

A perfectly held stand scores 2.0 per step, so 500 is the episode ceiling. Each term is logged separately in the step info so step 3 can show which one is doing the work, rather than reporting a single scalar and calling it tuned.

完璧に立ち続ければ1 stepあたり2.0点なので、episodeの上限は500です。 各項はstep infoに個別に記録しています。単一のスカラー値を報告して「調整済み」とするのではなく、step 3でどの項が効いているかを示せるようにするためです。

Observation scaling, and the vector environmentObservationのスケーリングとvector環境

Each group is divided down so no input dominates by unit choice alone: joint positions and projected gravity are already order 1, joint velocity reaches ~20 rad/s in a fall and is scaled by 0.05, and the gyro by 0.25. Whether those scales are right is one of the three open risks below — they are not.

単位の選び方だけで特定の入力が支配的にならないよう、グループごとに割って縮めます。関節位置と投影した重力はもともと1程度、 関節速度は転倒時に~20 rad/sに達するので0.05倍、ジャイロは0.25倍です。このスケールが正しいかどうかは後述の3つの未解決リスクの1つで、結論から言えば正しくありません。

vec_env.py forks N workers over pipes, one environment each, capped at eight. Environment i is seeded seed + i, and a test asserts that N workers reproduce N sequential environments bit for bit — so a run is reproducible at any worker count, and a result cannot quietly depend on how it was parallelised.

vec_env.pyはパイプ経由でN個のworkerをforkし、各workerが環境を1つ持ちます。上限は8です。環境iのseedはseed + iで、 N個のworkerがN個の環境を順番に実行した場合とビット単位で一致することをテストで確認しています。そのため実行はworker数によらず再現でき、 結果が並列化の仕方に黙って依存することはありません。

08

The number a learned policy has to beat学習したpolicyが超えるべき数値

The hand-tuned PD controller that any learned policy has to beat.

学習したpolicyが必ず超えなければならない、手調整のPD controller。

The shipped PD controller commanded to hold STAND — which is exactly what a zero action emits — over twenty seeds of the full task, pushes and initial-state noise included.

付属のPD controllerにSTANDの保持を指令し(ゼロのactionが出す指令とまったく同じです)、pushと初期状態のノイズを含むタスク全体を20 seedで評価しました。

Measurement計測項目Value値
return108.7 ± 2.9 of a 500 ceiling(上限500)
fraction of the episode on the floorepisode中に床に倒れていた割合54 %
starts to tilt (upright cos < 0.9)傾き始め(upright cos < 0.9)0.65 s
trunk reaches the floor (height < 4 cm)胴体が床に到達(高さ < 4 cm)2.28 s

The last two reconcile the drop test with this one: 0.79 s was the toppling threshold, 2.28 s is ground contact. Same event, measured at two points on the way down.

最後の2行で、落下テストとこの計測の整合が取れます。0.79 sは倒れ始めのしきい値、2.28 sは床への接触です。同じ出来事を、倒れていく途中の2つの時点で計測したものです。

Why this file exists at allこのファイルが存在する理由

The first version of these numbers — 108.9 ± 2.8, with a ground-contact time of 2.54 s against a threshold that was never written down — came from an ad-hoc script that was not kept. The return survived re-measurement to within the noise it already reported. The contact time did not, and there is now no way to tell whether that is the correctness fix or a different threshold. That is the argument for a tracked script and a JSON file rather than a number in a notebook.

これらの数値の最初の版(108.9 ± 2.8、床への接触時刻2.54 s、しきい値は記録なし)は、保存しなかったその場しのぎのスクリプトから出たものでした。 returnは再計測しても、もともと報告していたノイズの範囲内に収まりました。接触時刻は収まらず、それが正しさのための修正によるものか、しきい値の違いによるものか、今では判別できません。 ノートに書いた数値ではなく、管理されたスクリプトとJSONファイルを残すべき理由はここにあります。

09

Five silent defects the tests did not catchテストが見逃した5つの潜在バグ

Defects the test suite passed over, and what each would have cost during training.

テストスイートをすり抜けた不具合と、それぞれが学習中にもたらしたはずの損失。

Step 2 closed by reviewing the environment against the thing that is about to consume it — a PPO loop — rather than against its own tests. Twenty-seven contract tests had passed. Five defects came out of that review, all silent, all now fixed with a test that fails without the fix.

Step 2の締めくくりに、環境を自身のテストではなく、これからそれを使うもの、つまりPPOループの視点でレビューしました。 27件の契約テストはすべて通っていました。それでもレビューで5つの不具合が見つかりました。いずれも表に出ないもので、現在はすべて修正済みです。修正がなければ失敗するテストも追加しています。

01

The observation mixed two instantsObservationに2つの時刻が混在していた

mj_step integrates position and velocity to t+1 but leaves sensor data and body positions at t — so the gyro was 2 ms older than the joint angles beside it. Every number was plausible. Measured, it was 0.054 rad/s on the gyro and 1.4 mm on trunk height against a 30 mm reward sigma. It attacks the one claim this project makes — that every observation channel is one real hardware could produce — and no IMU disagrees with its own encoders by a timestep. Fixed by calling mj_forward before reading anything, which costs 7 % of throughput and is a correctness cost, not overhead.

mj_stepは位置と速度をt+1まで積分しますが、センサーの値とボディの位置はtのままです。そのため、ジャイロは隣の関節角度より2 ms古い値でした。 どの数値ももっともらしく見えました。計測すると、ジャイロで0.054 rad/s、胴体の高さで1.4 mmのずれがありました(rewardのσは30 mm)。 これは本プロジェクトの唯一の主張、「observationのすべてのチャネルが実機で得られる値である」を損ないます。自分のエンコーダと1 timestepずれるIMUは存在しません。 何かを読む前にmj_forwardを呼ぶことで修正しました。throughputの7 %を消費しますが、これはオーバーヘッドではなく正しさのためのコストです。

02

The reset clamp never executedReset時のクランプが一度も実行されていなかった

np.clip(qpos[idx], lo, hi, out=qpos[idx]) with an integer index array writes into a temporary copy — fancy indexing does not produce a view. Latent at the default initialisation noise, because the tightest joint sits 0.297 rad clear of its limit. At the larger noise step 4 will use, two joints of fourteen start 0.086 rad outside their range and MuJoCo applies a limit impulse at t = 0.

整数のインデックス配列を使ったnp.clip(qpos[idx], lo, hi, out=qpos[idx])は、一時的なコピーに書き込みます。fancy indexingはviewを返さないからです。 デフォルトの初期化ノイズでは、最も余裕のない関節でも制限まで0.297 radあるため、問題は表面化しませんでした。 step 4で使う大きなノイズでは、14関節中2関節が可動域から0.086 radはみ出した状態で始まり、t = 0でMuJoCoが制限のインパルスを加えます。

03

The done flag carried no truncation informationdoneフラグにtruncationの情報がなかった

Every episode ends on the step limit and none on a terminal state. A stock GAE loop zeroes the bootstrap at done, which at γ = 0.99 corrupts the value target about a hundred steps back into a 250-step episode. The training curve still goes up. This is the same bug the previous project in this track designed its environment specifically to make impossible.

すべてのepisodeはstep数の上限で終わり、終端状態で終わるものはありません。一般的なGAEループはdoneでbootstrapをゼロにするため、 γ = 0.99では250 stepのepisodeの約100 step前までvalueの目標値が壊れます。それでも学習曲線は上がります。 このトラックの前のプロジェクトで、環境の設計段階から起こり得ないようにしたのとまったく同じバグです。

04

A dead worker reported nothing useful落ちたworkerが役に立つ情報を何も出さなかった

A worker that died gave the parent a bare EOFError naming neither the worker nor the cause, and bad environment arguments failed the same way out of the constructor. The child's traceback does reach stderr — but in a redirected training log it is nowhere near the failure.

落ちたworkerは親プロセスに素のEOFErrorを返すだけで、どのworkerか、原因は何かを示しませんでした。不正な環境引数もコンストラクタで同じように失敗していました。 子プロセスのtracebackはstderrには出ますが、リダイレクトした学習ログの中では、失敗箇所から遠く離れた場所に埋もれてしまいます。

05

Stepping before resetting produced a valid-looking, push-free episodeResetせずにstepすると、正常に見えるpushなしのepisodeができていた

And the vector environment's constructor raising a validation error emitted an unrelated AttributeError from its destructor on top of it. One test in this repository was doing exactly the first thing and discarding the result.

また、vector環境のコンストラクタが検証エラーを出すと、デストラクタから無関係なAttributeErrorがさらに重ねて出ていました。 このリポジトリのテストの1つが、まさに前者を行い、その結果を捨てていました。

The point of listing theseこれらを列挙する意味

Twenty-seven contract tests say the environment does what it claims. A review against its consumer found five things the tests did not. Neither can say the task is learnable; step 3 answers that.

27件の契約テストは、環境が仕様どおりに動くことを示します。使う側の視点でのレビューは、テストが見逃した5つを見つけました。 しかしどちらも、タスクが学習可能かどうかは示せません。それに答えるのがstep 3です。

10

Three open risks at the end of step 2Step 2終了時点の3つの未解決リスク

The three measured facts step 2 handed to step 3, and how step 3 answered each.

Step 2からstep 3へ引き継いだ3つの計測事実と、step 3がそれぞれにどう答えたか。

1 — Three of the four reward penalties are numerically dead1 — 4つのreward penaltyのうち3つが数値的に効いていない

Term項Weight重みPD baselineRandom actionsランダムaction
upright+1.070.067.9
height+1.038.840.8
action_rate−0.050.00−8.33
posture−0.10−0.15−0.22
effort−0.02−0.01−0.07
joint_vel−2e−4−0.00−0.04

action_rate is zero for the PD baseline only because a constant action has no rate — it is live. The other three are under 0.25 % of the return under both policies. joint_vel's weight was chosen for the ~20 rad/s of a fall, and measured joint speed is under 1 rad/s — 400× smaller once squared. So the plan to report which penalty is doing the work would report three zeros, and effort and velocity regularisation, which is usually what governs whether a policy transfers, is absent in practice. Reweighting changes the baseline, so it was left to step 3, which dropped two of the three and set the third from measurement (next section).

PD baselineでaction_rateがゼロなのは、一定のactionには変化がないからにすぎず、この項は機能しています。 残り3つは、どちらのpolicyでもreturnの0.25 %未満です。joint_velの重みは転倒時の~20 rad/sを想定して選んだものですが、計測した関節速度は1 rad/s未満で、二乗すると400分の1以下になります。 つまり「どのpenaltyが効いているかを報告する」という計画では3つのゼロを報告することになり、policyが転移できるかを通常左右する出力と速度の正則化が、実質的に存在しないことになります。 重みを変えるとbaselineも変わるため、これはstep 3に持ち越しました。step 3では3つのうち2つを削除し、残り1つを計測にもとづいて設定しました(次節)。

2 — The reward is informative about standing and nearly flat about getting up2 — Rewardは立つことには敏感だが、起き上がることにはほぼ平坦

Per-step reward while upright (cos > 0.9) is 1.92 ± 0.05; while down (cos < 0.3) it is 0.12 ± 0.04. The level gap is 16×, so the return clearly prefers standing and the task is not degenerate. But upright is floored at 0 past 90°, and height is a 3 cm Gaussian worth about 1e−3 at floor level — so inside the fallen region the reward barely says which way is up. Expect exploration, not reward shape, to decide whether recovery is learned, and expect fall slowly and stay tilted as the competing local optimum. Step 3 replaced the Gaussian with a ramp for this reason.

1 stepあたりのrewardは、直立時(cos > 0.9)で1.92 ± 0.05、倒れているとき(cos < 0.3)で0.12 ± 0.04です。水準の差は16倍あるため、 returnは明らかに立つことを好み、タスクとして破綻してはいません。しかしuprightは90°を超えると0で頭打ちになり、heightは幅3 cmのガウス関数で床の高さでは約1e−3にしかなりません。 そのため倒れた領域の中では、rewardはどちらが上かをほとんど教えてくれません。復帰を学習できるかどうかはrewardの形ではなく探索で決まり、 対抗する局所最適解としてゆっくり倒れて傾いたままでいる挙動が現れると予想されます。この理由から、step 3ではガウス関数を線形のランプに置き換えました。

3 — The observation scaling is a guess, and the measurement says it guessed wrong3 — Observationのスケーリングは推測で、計測によるとその推測は外れている

Group RMS under random actions: projected gravity 0.577, previous action 0.575, gyro 0.211, joint position 0.094, joint velocity 0.044. A 13× spread — with the 28 dimensions that describe the body's configuration carrying the least variance of all, so an unnormalised first layer attends mostly to the policy's own previous output. Step 3 carries a running observation normaliser rather than a better fixed guess.

ランダムaction時のグループ別RMSは、投影した重力0.577、直前のaction 0.575、ジャイロ0.211、関節位置0.094、関節速度0.044でした。 13倍のばらつきがあり、しかも体の形を表す28次元の分散が最も小さいため、正規化しない第1層は主にpolicy自身の直前の出力に注目してしまいます。 Step 3では、より良い固定値を推測し直すのではなく、running observation normaliserを導入しました。

Scope, now that the gate has been measured判定を計測し終えた時点でのスコープ

Item項目
In対象Stand and recover from pushes, on groundcontact, 50 M environment steps. Planned as 1.0 h; at the rate the loop actually runs it is two chunks of about 2 h.groundcontact上での立位とpushからの復帰、50 M environment step。計画では1.0 hでしたが、学習ループの実際の速度では約2 hのchunkが2つになります。
In対象Domain randomisation over the four measured actuator classes, evaluated on groundcontact_backlash and rollers as held-out physics (step 4 explains why not walk_backlash).実測された4種類のアクチュエータクラスにわたるdomain randomisation。held-out physicsとしてgroundcontact_backlashとrollersで評価(walk_backlashを使わない理由はstep 4で説明)。
Out対象外 (was in, then out again)(一度対象に入り、再び対象外に)A walking gait, 400 M steps. 8.4 h at the planned rate; 34 h at the measured one, which is out of reach on this budget.歩行、400 M step。計画の速度なら8.4 h、計測した速度では34 hで、この予算では手が届きません。
Out対象外Anything requiring mjlab, MuJoCo Warp, or a GPU.mjlab、MuJoCo Warp、GPUを必要とするものすべて。

What steps 1 and 2 did not prove, and what step 3 did about itStep 1と2で証明できなかったことと、step 3での対応

  • Steps 1 and 2 trained nothing. Throughput was measured with random actions around STAND. The training loop adds policy forward passes, advantage computation and optimiser steps on top, and the section on the fifth revision below says what that cost.
  • The 13 300 figure had one inherited factor (a wrapper cost measured single-core) and was missing two more: the eight pipe round trips per step and the policy forward pass.
  • Neither sustained run reached a steady state. One was still falling 1.0 % per window after twelve; the other bottomed out at window 11 and was still climbing 0.6 % after eighteen. The 9 % plateau band is the honest uncertainty on the budget rate.
  • The throughput figure has been wrong five times. Treat any number here as provisional until a script in the repository reproduces it on the workload it is quoted for.
  • The 0.79 s fall time is one deterministic rollout from one keyframe. It is a baseline to beat, not a distribution — the 108.7 ± 2.9 return over twenty seeds is the distributional version.
  • At the end of step 2 the reward had never had anything optimise against it. Three of its four penalties were measurably inert and the fallen region nearly flat; both were written up rather than quietly retuned. Step 3 then changed the reward in the open, as version 2, and re-measured the baseline under it.
  • No observation noise, no domain randomisation, no actuator variation. One nominal physics model. Step 4 adds the spread.
  • Run-to-run spread is about 1 % from two runs seven minutes apart — not enough samples to call it a distribution, and it says nothing about spread across days, where the host power state is the dominant term and has already moved these numbers by 40 %. Two significant figures is all any of this supports.
  • Step 1と2では何も学習していません。throughputはSTAND周辺のランダムactionで計測しました。学習ループではさらにpolicyの順伝播、advantageの計算、 optimizerの更新が加わります。そのコストは後述の「5回目の修正」の節で示します。
  • 13 300という数値には、引き継いだ係数が1つ(シングルコアで計測したwrapperのコスト)含まれ、さらに2つが抜けていました。 1 stepあたり8回のパイプ往復と、policyの順伝播です。
  • どちらの連続負荷実行も定常状態に達していません。一方は12区間後もまだ区間ごとに1.0 %下がり続け、もう一方は11区間目で底を打ったあと18区間後もまだ0.6 %上昇していました。 9 %の横ばい帯が、計画に使う速度の正直な不確かさです。
  • throughputの数値は5回間違えています。リポジトリのスクリプトが、その数値の対象となるワークロードで再現するまでは、ここにあるどの数値も暫定値として扱ってください。
  • 0.79 sという転倒時間は、1つのキーフレームからの決定論的なrollout 1回分です。超えるべきbaselineであって分布ではありません。 分布として見るなら、20 seedでのreturn 108.7 ± 2.9がそれにあたります。
  • Step 2の終了時点では、rewardに対して最適化をかけたことが一度もありませんでした。4つのpenaltyのうち3つは計測上効いておらず、倒れた領域はほぼ平坦でした。 どちらも黙って調整し直すのではなく、文書に残しました。その後step 3でrewardをバージョン2として公開の形で変更し、それを使ってbaselineを再計測しました。
  • Observationのノイズも、domain randomisationも、アクチュエータのばらつきもありません。公称の物理モデル1つだけです。ばらつきはstep 4で加えます。
  • 実行ごとのばらつきは約1 %で、7分空けた2回の実行から得たものです。分布と呼ぶにはサンプルが足りず、日をまたいだばらつきについては何も言えません。 日をまたぐ場合はホストの電源状態が支配的で、すでにこれらの数値を40 %動かしています。ここにあるどの数値も、有効数字2桁までしか裏付けられません。
11

Step 3 — the reward decisionStep 3 — rewardの決定

Four measured facts, one rejected fix, and a second reward version that keeps the first reproducible.

4つの計測事実、却下した1つの修正案、そして1つ目の再現性を保ったままの2つ目のrewardバージョン。

A reward is a specification, and this one had never had anything optimise against it. Step 2 closed with three measured facts about it and a note that changing it was a step-3 decision, because a new reward invalidates the 108.7 baseline. Here is the decision, with one more measurement taken while making it.

rewardは仕様そのものですが、このrewardに対して最適化をかけたことはまだ一度もありませんでした。Step 2は、rewardに関する3つの計測事実と、 「変更はstep 3で判断する」という注記で締めくくりました。新しいrewardにすると108.7のbaselineが無効になるからです。以下がその判断で、判断の過程でもう1つ計測を追加しています。

  1. Three of four penalties are dead. posture, effort and joint_vel are each under 0.25% of the return under both the PD controller and random actions.
  2. The fallen region is nearly flat. 0.12 ± 0.04 reward per step while down against 1.92 ± 0.05 upright. The height term is a 3 cm Gaussian about 0.12 m, worth about 1e−3 at floor level.
  3. The observation scaling guessed wrong, a 13× rms spread across groups. This one is answered in the training loop, not the reward.
  4. Measured for this decision: over 1,248 fallen steps in six episodes, the fallen duck lies with trunk cosine between 0.0 and 0.3 and trunk height between 3.1 and 6 cm. The cosine never went below −0.06. It lands on its front or side, trunk near horizontal, and does not roll past 90°.
  1. 4つのpenaltyのうち3つが効いていない。posture、effort、joint_velはいずれも、PD controllerでもランダムactionでもreturnの0.25%未満です。
  2. 倒れた領域がほぼ平坦。倒れているときの1 stepあたりのrewardは0.12 ± 0.04、直立時は1.92 ± 0.05です。heightの項は0.12 m中心・幅3 cmのガウス関数で、床の高さでは約1e−3にしかなりません。
  3. Observationのスケーリングの推測が外れている。グループ間でrmsに13倍の差があります。これはrewardではなく学習ループ側で対処します。
  4. 今回の判断のために計測したこと:6 episode・1,248 stepの転倒状態を調べると、倒れたロボットは胴体のcosineが0.0〜0.3、胴体の高さが3.1〜6 cmの範囲に横たわっていました。 cosineが−0.06を下回ったことは一度もありません。うつ伏せか横向きに着地し、胴体はほぼ水平で、90°を超えて転がることはありません。
The fix that was rejected on the measurement計測によって却下した修正案

The obvious repair for an upright term floored at zero is (1 + cos)/2, so tilting past 90° still costs something. Fact 4 says the robot never gets there. In the band it actually occupies, 0.0 to 0.3, that change halves the slope of the one gradient the fallen region already has, to buy a gradient in a band it never enters. Rejected.

0で頭打ちになるupright項の定番の修正は(1 + cos)/2にすることで、90°を超えて傾いてもペナルティが残るようになります。 しかし事実4によれば、ロボットはそこまで傾きません。実際にいる0.0〜0.3の範囲では、この変更は倒れた領域にある唯一の勾配の傾きを半分にしてしまい、 その代わりに得られるのは、ロボットが決して入らない範囲での勾配です。却下しました。

Reward v2, next to v1Reward v2とv1の比較

Term項v1v2Why理由
upright+1.0 · max(0, cos)unchanged変更なしslope 1 per unit cosine where the duck actually lies; the floor never engagesロボットが実際に横たわる範囲でcosine 1あたり傾き1。下限0には一度も当たらない
height+1.0 · exp(−((h−0.12)/0.03)²)+1.0 · clip(h / 0.12, 0, 1)the Gaussian is 1e−4 to 0.02 across the whole fallen height range; the ramp is 8.3 per metre everywhere below standing. This is the term that makes lifting the trunk while tilted worth anythingガウス関数は倒れた高さの範囲全体で1e−4〜0.02にしかならない。ランプなら立位より下のどこでも1 mあたり8.3。傾いたまま胴体を持ち上げることに価値を与えるのはこの項
posture−0.10dropped削除under 0.25% of the returnreturnの0.25%未満
effort−0.02dropped削除under 0.25%, and actuator force is not something the real servos report0.25%未満で、しかもアクチュエータの出力は実機のサーボが報告する値ではない
action rate−0.05unchanged変更なしthe one penalty measured live計測上で唯一効いているpenalty
joint velocity−2e−4−4.2e−2set by measurement. A placeholder of −2e−3 left the term at 0.24% of the random-action return; baseline.py --reward v2 asked for 21× to reach the 5% target, and at −4.2e−2 it is 5.24% under random actions and 0.17% under the PD controller, which does not move計測で決定。仮の値−2e−3ではランダムaction時のreturnの0.24%にとどまった。baseline.py --reward v2が目標の5%に届くには21倍が必要と示し、−4.2e−2ではランダムaction時に5.24%、ほとんど動かないPD controllerでは0.17%

The ceiling stays 2.0 per step, 500 per episode. On the same trajectories the reward's slope against trunk height inside the fallen region is −1.3 per metre under v1 and +6.6 under v2. v1 stays byte-identical behind reward="v1", and test_reward.py pins two v1 episode returns to the last decimal so the 108.7 baseline remains reproducible.

上限は変わらず1 stepあたり2.0、1 episodeあたり500です。同じ軌道で比べると、倒れた領域での胴体の高さに対するrewardの傾きはv1で1 mあたり−1.3、v2で+6.6です。 v1はreward="v1"でバイト単位まで同一のまま残してあり、test_reward.pyがv1の2つのepisode returnを最後の桁まで固定しているため、108.7のbaselineは再現可能なままです。

The PD baseline, re-measured under v2v2で再計測したPD baseline

reward v1reward v2
return, 20 seedsreturn(20 seed)108.7 ± 2.9175.2 ± 3.1 of 500/500
fraction of the episode on the floorepisode中に床に倒れていた割合54%54%
starts to tilt / reaches the floor傾き始め / 床に到達0.65 s / 2.28 s0.65 s / 2.28 s
reward per step, upright / down1 stepあたりのreward(直立 / 転倒)1.92 / 0.121.90 / 0.42
term shares under PD (upright / height / action rate / joint vel)PDでの項の割合(upright / height / action rate / joint vel)—40 / 60 / 0 / 0.17%
term shares under random actionsランダムactionでの項の割合—43 / 67 / −5.3 / −5.2%

Same trajectories, same controller, a different number: the ramp pays out on the way down, so the level gap between upright and fallen is 5× instead of 16×. The return still clearly prefers standing. 175.2 ± 3.1 is the number the policy has to beat.

同じ軌道、同じcontrollerでも数値は変わります。ランプは倒れていく途中でも点を与えるため、直立と転倒の水準の差は16倍ではなく5倍になります。 それでもreturnは明らかに立つことを好みます。policyが超えるべき数値は175.2 ± 3.1です。

Found while wiring this up実装中に見つけたこと

MuJoCo 3.12 no longer has the mjENBL_SENSORNOISE flag step 2 planned to flip. The model still declares the magnitudes (0.005 rad/s on the gyro, 0.001 on the orientation quaternion), so the environment applies them itself when sensor_noise=True. A test checks the gyro moves and nothing else does.

MuJoCo 3.12には、step 2で立てる予定だったmjENBL_SENSORNOISEフラグがもうありません。モデルにはノイズの大きさ(ジャイロ0.005 rad/s、姿勢クォータニオン0.001)が宣言されたままなので、 sensor_noise=Trueのときは環境が自前でそれを加えます。ジャイロだけが変化し、他は変化しないことをテストで確認しています。

12

Step 3 — the training loopStep 3 — 学習ループ

A textbook PPO with four additions this environment demands, and what each one prevents.

教科書どおりのPPOに、この環境が必要とする4つの追加を加えたもの。それぞれが何を防ぐか。

The loop is the one from the earlier cart-pole project: generalised advantage estimation, orthogonal initialisation, the clipped surrogate objective, several epochs of minibatch updates per batch, and a learning rate that anneals to zero. Gaussian policy, separate actor and critic, each two hidden layers of 256 tanh units. Entropy bonus zero, because a Gaussian's differential entropy is unbounded below and a bonus would push the action noise up without a floor.

ループは以前のcart-poleプロジェクトのものです。GAE、直交初期化、clipしたsurrogate目的関数、batchごとに数epochのminibatch更新、ゼロまで減衰させる学習率。 policyはガウス分布で、actorとcriticは別々のネットワークで、それぞれ256ユニットのtanh隠れ層を2層持ちます。entropyボーナスはゼロです。 ガウス分布の微分エントロピーには下限がなく、ボーナスを入れるとactionのノイズを際限なく押し上げてしまうからです。

Addition追加要素What it is内容What goes wrong without itなかった場合の問題
vectorised rolloutsベクトル化したrollouteight forked environments, one observation batch of 8 × 48 per step, 256 steps per environment per update, batch 2,048forkした8つの環境、1 stepあたり8 × 48のobservation batch 1つ、更新ごとに環境あたり256 step、batch 2,048one environment at a time would run at an eighth of the rate環境を1つずつ動かすと速度は8分の1になる
running normaliserWelford mean and variance over every observation seen, applied before the network, saved in the checkpoint, folded into the ONNX graph at exportこれまでのすべてのobservationに対するWelford方式の平均と分散。ネットワークの前で適用し、checkpointに保存し、export時にONNXグラフへ組み込むstep 2 measured a 13× rms spread across observation groups with the 28 body-configuration dims carrying the least variance; an unnormalised first layer attends mostly to the policy's own last actionstep 2でobservationグループ間に13倍のrmsの差があり、体の形を表す28次元の分散が最も小さいと計測された。正規化しない第1層は主にpolicy自身の直前のactionに注目してしまう
truncation bootstrapevery episode ends on the step limit and none on a failure, so the value target at the last step is r + γ·V(terminal_obs), with the terminal observation carried across by the vector environmentすべてのepisodeはstep数の上限で終わり、失敗で終わるものはない。そのため最終stepのvalueの目標値はr + γ·V(terminal_obs)とし、終端のobservationはvector環境が受け渡すa stock loop zeroes the bootstrap on done; at γ = 0.99 that corrupts the value target for roughly 100 of the 250 steps. test_ppo.py shows the corruption on a hand-computed three-step case一般的なループはdoneでbootstrapをゼロにする。γ = 0.99では250 step中およそ100 stepでvalueの目標値が壊れる。test_ppo.pyが手計算した3 stepの例でこの破損を示している
chunked resumechunk単位の再開--chunk-steps per invocation, a checkpoint every ten updates, --resume restores model, optimiser, normaliser, RNG and step counter1回の起動ごとに--chunk-steps分を実行し、10回の更新ごとにcheckpointを保存。--resumeでモデル、optimizer、normaliser、乱数状態、stepカウンタを復元the machine is shared and never left running unattended, so a 50M-step run has to survive being split. A killed run loses at most one checkpoint intervalマシンは共用で、無人で動かしっぱなしにはしないため、50M stepの実行は分割に耐える必要がある。強制終了しても失うのは最大でcheckpoint 1回分

What resume promises and what it does not. Two resumes from the same checkpoint are bit-identical; a test asserts it. A split run is not bit-identical to an uninterrupted one, because the workers' physics state is not checkpointed and the resumed chunk starts fresh episodes. Stated rather than papered over.

再開機能が保証すること、しないこと。同じcheckpointから2回再開した結果はビット単位で一致し、テストで確認しています。 一方、分割した実行は中断なしの実行とビット単位では一致しません。workerの物理状態はcheckpointに含まれず、再開したchunkは新しいepisodeから始まるからです。 取り繕わずに明記しておきます。

Throughput is logged every update, and the log says so when the rolling rate falls more than 20% below the mean of the first five updates. On this machine that is the only visible sign of a thermal or power limit, and it looks identical to contention from another process, so the warning names both. The box check refuses to start above a one-minute load average of 4.

throughputは更新ごとに記録し、移動平均の速度が最初の5回の更新の平均より20%以上下がると、ログにその旨を出します。 このマシンでは、それが熱や電力の制限を示す唯一の目に見える兆候です。他のプロセスとの競合とも見分けがつかないため、警告では両方の可能性を挙げます。 起動前のチェックは、1分間のload averageが4を超えていれば開始を拒否します。

How the run is scored実行の採点方法

Metrics are defined once in metrics.py and imported by both the in-training evaluation and eval_policy.py, so the learning curve and the final table cannot disagree.

指標はmetrics.pyで一度だけ定義し、学習中の評価とeval_policy.pyの両方がそれをimportします。そのため学習曲線と最終的な表が食い違うことはありません。

Metric指標Definition定義
returnsum of per-step reward; 500 ceiling under either reward, the two versions not comparable to each otherstepごとのrewardの合計。どちらのrewardでも上限は500だが、2つのバージョン同士は比較できない
survivalfraction of steps with the trunk above 4 cm胴体が4 cmより上にあるstepの割合
recoveryper push: upright (cos > 0.9) within 2 s of the push. A push that lands on an already-fallen robot cannot be recovered from by definition; it is excluded from the denominator and counted separately, so a policy that lives on the floor cannot hide behind a small denominatorpushごとに、pushから2 s以内に直立(cos > 0.9)しているか。すでに倒れているロボットへのpushは定義上復帰しようがないため、分母から除いて別に数える。床にいる時間の長いpolicyが小さな分母に隠れられないようにするため
time to recover復帰時間push step to first upright step, p50 and p90 over recovered pushespushのstepから最初に直立したstepまで。復帰したpushについてのp50とp90
13

Step 3 — results, 25M and 50M stepsStep 3 — 結果(25M stepと50M step)

PPO against the PD controller on the same seeds, after each half of the 50M budget.

50Mの予算の前半・後半それぞれの終了時点で、同じseedを使ってPPOとPD controllerを比較。

OMP_NUM_THREADS=1 nice -n 10 python ppo.py --total-steps 50000000 --chunk-steps 25000000 --tag v2 --reward v2
OMP_NUM_THREADS=1 nice -n 10 python ppo.py --resume runs/ppo_v2.ckpt.pt --tag v2 --target-kl 0.02 --chunk-steps 12500000   # chunk 2, until 50M
python eval_policy.py runs/ppo_v2.pt --variant groundcontact      # -> runs/eval_v2_groundcontact.json

Configuration: 8 workers, batch 2,048, 5 epochs × 8 minibatches, learning rate 3e−4 annealed, γ 0.99, λ 0.95, clip 0.2, entropy 0, initial log-std −0.5. Evaluation: 100 episodes × 5 seeds, deterministic mean actions, the PD controller on the same seeds.

設定:8 worker、batch 2,048、5 epochs × 8 minibatches、学習率3e−4(減衰あり)、γ 0.99、λ 0.95、clip 0.2、entropy 0、初期log-std −0.5。 評価:100 episodes × 5 seeds、決定論的な平均action、同じseedでのPD controller。

Policyreturn (± over seeds)(± はseed間)survivalrecovery (± over seeds)(± はseed間)time to recover p50 / p90 s復帰時間 p50 / p90 spushes on a fallen robot転倒中のロボットへのpush
PPO v2, 50M steps (chunk 2,PPO v2、50M step(chunk 2、 --target-kl 0.02))344.8 ± 2.90.730.99 ± 0.000.00 / 0.00629
PPO v2, 25M steps (chunk 1)PPO v2、25M step(chunk 1)332.6 ± 5.10.741.00 ± 0.000.00 / 0.00699
PD hold-pose, reward v2PD 姿勢保持、reward v2175.8 ± 0.20.470.88 ± 0.200.00 / 0.001443

Read the recovery column with its time to recover. A push counts as recovered if the trunk is back inside the upright band within 2 s, and 0.00 s means it never left the band: no push that landed on the standing PPO robot knocked it over, while 12% of the pushes on the standing PD robot did. The column measures push resistance, not standing back up. The last column is the same fact from the other side: 699 pushes landed on an already-fallen PPO robot against 1,443 for PD, so the learned policy was down less than half as often. Neither policy has demonstrated getting up after a fall; pushes strong enough to knock the learned policy over are a follow-up run.

recoveryの列は、復帰時間の列と合わせて読んでください。pushのあと2 s以内に胴体が直立の範囲に戻れば復帰とみなしますが、0.00 sは一度もその範囲を出なかったことを意味します。 立っているPPOのロボットに当たったpushで倒れたものは1つもなく、立っているPDのロボットでは12%のpushで倒れました。この列が測っているのは押しへの耐性であって、起き上がる能力ではありません。 最後の列は同じ事実を反対側から見たものです。すでに倒れていたロボットへのpushはPPOで699回、PDで1,443回で、学習したpolicyが倒れていた頻度は半分未満でした。 転倒後に起き上がる能力は、どちらのpolicyでもまだ示されていません。学習したpolicyを倒せるほど強いpushでの実行は、今後の課題です。

After the first 25M-step chunk, PPO scored 332.6 ± 5.1 of 500 against the PD controller's 175.8 ± 0.2 under the same reward and kept the trunk up for 74% of steps (PD 47%), 100 episodes × 5 seeds. The push column reads 100% against 88%, but with a 0.00 s time to recover it measures push resistance, not standing back up: 797 of the 801 pushes that landed on the standing policy never moved the trunk out of the upright band at all, and the 100% is 0.995 rounded. After the second chunk the policy scores 344.8 ± 2.9, and all five evaluation seeds went up, by 8.6 to 16.0. Survival did not move (0.73 against 0.74, about 1.3 standard errors). Pushes on a fallen robot fell from 699 to 629, and the upright fraction rose from 0.47 to 0.50. That is a little more time standing, not a new behaviour, and getting up after a fall is still not shown.

最初の25M stepのchunkを終えた時点で、PPOは同じrewardで500点中332.6 ± 5.1を記録し、PD controllerの175.8 ± 0.2を上回りました。胴体を立てていたstepの割合は74%(PDは47%)で、100 episodes × 5 seedsでの結果です。 pushの列は100%対88%ですが、復帰時間が0.00 sなので、測っているのは押しへの耐性であって起き上がる能力ではありません。立っているpolicyに当たった801回のpushのうち797回は、胴体を直立の範囲から一度も外に出しておらず、100%は0.995を丸めた値です。 2つ目のchunkのあと、policyは344.8 ± 2.9となり、5つの評価seedすべてで8.6〜16.0上昇しました。survivalは変わっていません(0.73対0.74、標準誤差の約1.3倍)。 倒れたロボットへのpushは699回から629回に減り、直立していた割合は0.47から0.50に上がりました。立っている時間が少し延びただけで、新しい挙動ではありません。転倒後に起き上がる能力は依然として示されていません。

What the log showed on the way学習途中のログが示したこと

The rolling 50-episode return rose from 155 at the first update to about 350 by 2.5M steps and peaked at 374 near 7.8M, against the PD controller's 175.2 (its 20-seed baseline; the 175.8 above is the same controller under the 100-episode × 5-seed evaluation). Then it stopped: the five 5M-step blocks averaged 337, 342, 332, 332 and 328, and the in-loop 20-episode evaluation averaged 354 with 0.83 survival over the first 5M steps against 330 with 0.69 over the last 5M. Those are block means on purpose: one evaluation is 20 episodes, and the last seven of them span 346 down to 313, so no pair of single rows would be evidence of anything.

直近50 episodeの移動平均returnは、最初の更新時の155から2.5M stepまでに約350へ上がり、7.8M付近で374のピークに達しました。 PD controllerは175.2です(20 seedでのbaseline。上の175.8は同じcontrollerを100 episodes × 5 seedsで評価した値)。そこで伸びが止まりました。 5M stepごとの5つのブロックの平均は337、342、332、332、328で、学習ループ内の20 episode評価の平均は、最初の5M stepで354(survival 0.83)、最後の5Mで330(survival 0.69)でした。 あえてブロック平均を使っています。1回の評価は20 episodeで、最後の7回は346から313まで幅があるため、単独の行を2つ比べても何の根拠にもならないからです。

The optimiser statistics say why. The clip fraction, the share of samples whose ratio hit the 0.2 clip, had a median of 0.48 over the first 5M steps and 0.73 over the last 5M. The approximate KL between consecutive policies went from a median of 0.07 to 0.76 with spikes above 50, and the policy's standard deviation shrank from 0.61 at the first update to 0.08 at the last. After roughly 5M steps every update moved the policy further than the clipped objective is meant to allow, and the extra distance bought nothing. That is the finding of this chunk: the step size, not the budget, is what to fix. So the second chunk resumed from runs/ppo_v2.ckpt.pt with an approximate-KL stop on the epoch loop (--target-kl 0.02), because two more hours at the same settings would repeat the same mistake.

理由はoptimizerの統計に表れています。clip fraction(比率が0.2のclipに当たったサンプルの割合)の中央値は、最初の5M stepで0.48、最後の5Mで0.73でした。 連続するpolicy間の近似KLの中央値は0.07から0.76になり、50を超えるスパイクもありました。policyの標準偏差は最初の更新の0.61から最後には0.08まで縮みました。 およそ5M stepを過ぎてからは、どの更新もclipした目的関数が本来許す以上にpolicyを動かしており、その余分な移動は何の成果も生んでいません。 これがこのchunkでの発見です。直すべきは予算ではなくstep sizeです。そこで2つ目のchunkはruns/ppo_v2.ckpt.ptから再開し、 epochループに近似KLによる停止(--target-kl 0.02)を入れました。同じ設定でさらに2時間回しても、同じ失敗を繰り返すだけだからです。

What the KL stop changedKL stopで何が変わったか

Over chunk 1 the mean approximate KL per update was 0.53 and the mean clip fraction 0.66. Over chunk 2 they were 0.032 and 0.28, and the largest KL was 0.24 against 173. The clip fraction's median fell from 0.40 over the first 5M steps of chunk 2 to 0.16 over the last 5M, and the policy's standard deviation held at 0.08 to 0.09. The rolling return's five block means went 331, 332, 337, 344, 347, against chunk 1's flat-to-falling 337, 342, 332, 332, 328. This run has two confounds and cannot separate them. The learning rate anneals to zero over the same span, and there is no chunk 2 without the stop to compare against. So the claim is that chunk 2 improved with the stop in place, not that the stop caused all of the gain.

chunk 1では、更新あたりの近似KLの平均が0.53、clip fractionの平均が0.66でした。chunk 2ではそれぞれ0.032と0.28で、KLの最大値は173に対して0.24でした。 clip fractionの中央値はchunk 2の最初の5M stepで0.40、最後の5Mで0.16まで下がり、policyの標準偏差は0.08〜0.09で安定していました。 移動平均returnの5つのブロック平均は331、332、337、344、347で、chunk 1の横ばいから下降の337、342、332、332、328とは対照的です。 ただしこの実行には2つの交絡要因があり、切り分けられません。同じ期間に学習率がゼロまで減衰しており、またstopなしのchunk 2という比較対象もありません。 したがって主張できるのは「stopを入れた状態でchunk 2は改善した」ということであり、「改善のすべてがstopによる」ということではありません。

Chunk 2 ran as three invocations of under an hour each: 58 min, then a 4-minute run stopped by hand right after a checkpoint, then 60 min. The runner rested the machine 30 minutes after every 2.5 hours of work. The throughput warning fired on 86 of 12,207 updates, against 4,296 of 12,208 in chunk 1's single 2.4-hour run, which is more evidence that chunk 1's slowdown was heat or power. The chunk-1 policy is kept byte-identical as runs/ppo_v2_chunk1.pt; the step 4 gap table and the step 5 export were measured on it. The action-scale question step 2 left open, 0.2 and 0.5 against the 0.35 default, is a --action-scale flag and two more runs; whether it is answered or dropped is recorded in the repository's issues.

chunk 2は1時間未満の起動3回に分けて実行しました。58 min、checkpoint直後に手動で止めた4分間、そして60 minです。実行担当は2.5時間の作業ごとにマシンを30分休ませました。 throughputの警告は12,207回の更新中86回しか出ず、2.4時間通しで回したchunk 1の4,296/12,208回とは大きく異なります。chunk 1の速度低下が熱か電力によるものだったという、さらなる根拠です。 chunk 1のpolicyはruns/ppo_v2_chunk1.ptとしてバイト単位で同一のまま保存しており、step 4の差分の表とstep 5のexportはこれで計測しています。 Step 2で残したaction scaleの問い(デフォルト0.35に対して0.2と0.5)は、--action-scaleフラグと追加の2回の実行で答えられます。答えたか取りやめたかはリポジトリのissueに記録しています。

14

The fifth revision of the throughput figurethroughputの数値の5回目の修正

Five numbers for the same quantity, what each one left out, and the one that finally measured the workload itself.

同じ量に対する5つの数値、それぞれが見落としていたもの、そして最終的にワークロードそのものを計測した数値。

env-steps/sHow it was obtained求め方What it left out見落としていたもの
28 74920 s burst, 8 processes, bare physics on the walk model20 sの短時間計測、8プロセス、walkモデルでの素の物理演算the package cools between rows; a burst is not a sustained rate行の合間にプロセッサが冷える。瞬間的な速度は持続的な速度ではない
18 400sustained 8 processes on walkwalkで8プロセスを連続実行the model actually trained on has five times the floor-collidable geometry実際に学習に使うモデルは、床と衝突しうる形状が5倍ある
8 000a burst on groundcontact, derated by the walk sustained factorgroundcontactでの短時間計測に、walkの持続時の低下率を掛けたものthe burst had been taken on a hot box, so the derate was counted twice; too low短時間計測自体がすでに熱を持ったマシンで行われていたため、低下分を二重に数えていた。低すぎる
13 300sustained 8 processes on groundcontact × a wrapper factor measured on one coregroundcontactで8プロセスを連続実行 × 1コアで計測したwrapperの係数eight pipe round trips per step, and the policy forward pass1 stepあたり8回のパイプ往復と、policyの順伝播
≈3 300ppo.py itself, chunk 1, printed every updateppo.pyそのもの、chunk 1、更新ごとに出力nothing; this is the rate the budget has to useなし。計画にはこの速度を使う必要がある

The first chunk ran at 3,000 to 3,750 env-steps/s with the policy in the loop. The log's own throughput warning, which fires when the last three updates average under 80 % of the first five, fired on 4,296 of the 12,208 updates: 9 % of the first two thousand, 84 % of the last four thousand, as the mean rate fell from 3,407 to 3,024 env-steps/s. The box check at the start recorded a 1-minute load of 0.64 and nothing else ran for the whole chunk. The process table said where the time went: the single Python main process at 76% of one core, each of the eight workers near 20%. The workers spend most of their time waiting for the main loop to generate the next action batch and read the last one back. Physics is not the bottleneck; the interpreter between the physics and the network is.

最初のchunkは、policyをループに入れた状態で3,000〜3,750 env-steps/sで動きました。直近3回の更新の平均が最初の5回の80 %を下回ると出るログのthroughput警告は、 12,208回の更新中4,296回出ました。最初の2千回では9 %、最後の4千回では84 %で、その間に平均速度は3,407から3,024 env-steps/sに下がっています。 開始時のチェックでは1分間のloadが0.64で、chunkの間ほかには何も動いていませんでした。時間の行方はプロセス一覧が示していました。 Pythonのメインプロセス1つが1コアの76%を使い、8つのworkerはそれぞれ20%前後でした。workerは時間の大半を、メインループが次のaction batchを生成し、前回の結果を読み戻すのを待つことに費やしています。 ボトルネックは物理演算ではなく、物理演算とネットワークの間にあるインタプリタです。

The C++ backend project measured the same thing from the other side a few hours earlier: this Python vector environment delivers about 6,400 env-steps/s at eight processes under random actions, half of the 13,300 that had been budgeted, before any network is involved. Its own threads reach 1.9 to 2.0× that. Neither number was in the composition that produced 13,300, because the composition multiplied a physics rate by a wrapper factor and never ran the thing it was estimating.

C++バックエンドのプロジェクトでは、その数時間前に同じことを反対側から計測していました。 このPythonのvector環境は、ネットワークが関わる前のランダムactionの段階で、8プロセスで約6,400 env-steps/sしか出ません。計画していた13,300の半分です。 C++版のスレッド実装はその1.9〜2.0倍に達します。どちらの数値も13,300を導いた計算には入っていませんでした。その計算は物理演算の速度にwrapperの係数を掛けただけで、見積もろうとしている対象そのものを一度も動かしていなかったからです。

The lesson, restated教訓、改めて

Every one of the five figures was honest, and four of them were wrong, because each unmeasured factor had been given a value. The fix was the same each time: run the workload that the number is quoted for. Here that meant the training loop, and it cost a factor of four against the plan. The 50M budget is now two chunks of about two hours instead of one hour, and the walking gait is out of reach again at 34 h.

5つの数値はどれも誠実に出したものでしたが、そのうち4つは誤りでした。計測していない要素それぞれに、仮の値を与えていたからです。 対処は毎回同じでした。その数値が対象としているワークロードそのものを動かすことです。ここではそれが学習ループであり、計画に対して4倍の差が出ました。 50Mの予算は1時間ではなく約2時間のchunk 2つになり、歩行は34 hで再び手の届かないものになりました。

15

Step 4 — domain randomisation, and which physics is held outStep 4 — domain randomisationと、held-outにするphysics

Ranges taken from the model rather than chosen, two held-out models from upstream, and why the one step 2 named cannot be used. Chunk 1 measured: randomisation bought robustness to backlash and cost the robot its stance.

恣意的に選ばずモデルから取った範囲、本家の2つのheld-outモデル、そしてstep 2で挙げたモデルが使えない理由。chunk 1の計測結果:ランダム化でbacklashへの頑健性は得たが、ロボットは立ち姿勢を失った。

Domain randomisation varies the physics at every reset so the policy cannot tune itself to one exact robot. The question step 4 asks is whether that buys anything measurable: a policy trained on one nominal model and a policy trained under randomisation are both evaluated on models neither has seen, and the difference in how much each loses is what randomisation is worth on this robot.

Domain randomisationはresetのたびにphysicsを変え、policyが特定の1台のロボットに合わせ込まれないようにします。Step 4の問いは、それで計測可能な何かが得られるかどうかです。 公称モデル1つで学習したpolicyと、ランダム化のもとで学習したpolicyを、どちらも見たことのないモデルで評価します。それぞれがどれだけ性能を落とすかの差が、このロボットにおけるランダム化の価値です。

The plan changed, for the reason found in step 1Step 1で見つけた理由により計画を変更

Step 2 named walk_backlash as the held-out model. It cannot be. It is the walk collision set with backlash joints added, so only its two feet can touch the floor and a fallen robot sinks through the world, exactly as on walk. Upstream also ships robot_groundcontact_backlash.xml, the ground-contact model with the same passive backlash joint in series with every actuator (10 floor-collidable geoms, 35 position coordinates), and rollers, which adds passive roller geometry at the feet and was always ground-contact based. Those two are the held-out set. walk_backlash stays in the repository for the strided-index test, which is what it was good for, and test_dr.py refuses it as a held-out model.

Step 2ではwalk_backlashをheld-outモデルとしていましたが、それは使えません。walkの衝突設定にbacklash関節を加えたものなので、 床に触れられるのは両足だけで、倒れたロボットはwalkとまったく同じように世界の底へ沈んでいきます。本家にはrobot_groundcontact_backlash.xmlもあります。 これはground-contactモデルの全アクチュエータに同じ受動backlash関節を直列に入れたもの(床と衝突しうるgeom 10個、位置座標35個)です。 さらに足に受動ローラーの形状を加えたrollersもあり、こちらはもともとground-contactベースです。この2つをheld-outのセットとします。 walk_backlashは、本来の使い道である飛び飛びのインデックスのテスト用にリポジトリに残し、test_dr.pyはこれをheld-outモデルとしては拒否します。

Factor要素Range範囲Source根拠
servo damping, friction loss, armature, gain, force limitサーボのdamping、friction loss、armature、ゲイン、力の上限uniform between the min and max of the four fitted actuator classes, all 14 servos together4つのフィット済みアクチュエータクラスの最小値〜最大値で一様、14サーボすべてに同じ値joints_properties.xml: four fits of the same XL330 servo by different people; friction loss spans 6.7×, everything else under 1.5×joints_properties.xml:同じXL330サーボを別々の人がフィットした4つの結果。friction lossは6.7倍の幅、他はすべて1.5倍未満
body mass and inertiaボディの質量と慣性× 0.85 – 1.15, every body、全ボディ—
floor sliding friction床の滑り摩擦× 0.6 – 1.2—
action latencyactionのlatency0 – 2 control steps (0 – 40 ms)0 – 2制御step(0 – 40 ms)applied in step(); the previous-action observation stays the action just emitted, because that is what the policy actually knowsstep()内で適用。直前のactionのobservationは出力したばかりのactionのまま。policyが実際に知っているのはそれだから
initial-state noise初期状態のノイズ0.02 – 0.3 radthis is what makes step 2's reset-clamp fix livestep 2のreset時クランプの修正が実際に効くのはこのため
sensor noiseセンサーノイズgyro 0.005 rad/s, quaternion 0.001ジャイロ0.005 rad/s、クォータニオン0.001upstream's declared magnitudes, applied by the environment本家が宣言した大きさを、環境側で適用

The spread is wide exactly where the hardware people disagree and narrow where they agree, and it is applied to all fourteen servos together, the way a batch of the same servo would vary. Nominal values are captured once at construction, so every apply is relative to the vendored model and nothing compounds; restore() puts every array back bit for bit, and a test checks that.

幅はハードウェア担当者の意見が分かれる箇所でちょうど広く、一致する箇所では狭くなっています。また、同じサーボのロットがばらつくのと同じように、14サーボすべてに同じ値を適用します。 公称値は構築時に一度だけ記録するため、どのapplyも同梱のモデルを基準にしており、変化が累積しません。restore()はすべての配列をビット単位で元に戻し、それをテストで確認しています。

OMP_NUM_THREADS=1 nice -n 10 python ppo.py --total-steps 50000000 --chunk-steps 25000000 --tag v2dr --reward v2 --dr
python eval_gap.py --nominal runs/ppo_v2_chunk1.pt --dr runs/ppo_v2dr.pt   # -> runs/gap.json

Both policies are the first 25M-step chunk of a 50M run (the nominal one is runs/ppo_v2_chunk1.pt; its second chunk is in section 13, but this table compares like with like): one training seed each, and the same configuration apart from randomisation. The randomised run is set up as 50M in two chunks, not as a single 25M run, so that its learning-rate anneal matches the nominal policy's. It took 2.4 hours at about 3 300 environment steps per second. Each cell is 100 episodes × 5 seeds, the same seeds everywhere, and ± is the spread of the five per-seed means.

どちらのpolicyも、50Mの実行の最初の25M stepのchunkです(公称のものはruns/ppo_v2_chunk1.pt。その2つ目のchunkは13節にありますが、この表では条件を揃えて比較しています)。 学習seedはそれぞれ1つで、ランダム化以外の設定は同じです。ランダム化した実行は、学習率の減衰を公称のpolicyと揃えるため、単独の25M実行ではなく2 chunkの50Mとして設定しています。 約3 300 environment steps/sで2.4時間かかりました。各セルは100 episodes × 5 seedsで、seedはすべて共通、±は5つのseed別平均のばらつきです。

Trained学習条件Evaluated on評価モデルreturnuprightsurvivaltrunk height p50胴体の高さ p50return gapreturnの差upright gapuprightの差
nominal公称groundcontact332.6 ± 5.10.47 ± 0.020.740.058 m——
nominal公称groundcontact_backlash317.4 ± 4.30.38 ± 0.020.560.039 m−15.2−0.09
nominal公称rollers373.6 ± 11.60.73 ± 0.040.900.138 m+41.1+0.26
randomisedランダム化groundcontact312.2 ± 5.70.28 ± 0.011.000.091 m——
randomisedランダム化groundcontact_backlash315.4 ± 4.50.24 ± 0.010.990.092 m+3.2−0.04
randomisedランダム化rollers147.3 ± 2.30.13 ± 0.000.840.042 m−164.9−0.15

Upright is the share of steps with the trunk within about 26° of vertical. Survival is the share with the trunk above the 4 cm fall height. Trunk height is the median of per-episode medians, and 12 cm is standing. Push recovery is left out because it reads 0.93–1.00 in every cell, the degenerate metric from section 13.

Uprightは胴体が鉛直から約26°以内にあるstepの割合、survivalは胴体が転倒判定の高さ4 cmより上にあるstepの割合です。 胴体の高さはepisodeごとの中央値のさらに中央値で、立っている状態は12 cmです。pushからのrecoveryは、どのセルでも0.93–1.00となり13節で述べた退化した指標なので、表から外しています。

The metric that would have got this wrong結論を誤らせるところだった指標

Read by survival, the randomised policy is a clear win: 1.00 against 0.74 on the training model and 0.99 against 0.56 on backlash, and the first version of the evaluation script led with that column. But survival only asks whether the trunk is above 4 cm. The randomised policy is upright for 28% of steps against 47%, its trunk sits at a median of 9 cm, and its return splits 134 from the upright term against the nominal policy's 183. It learned to brace in a low, tilted crouch that never counts as a fall, not to stand. Upright share and trunk height went into the metrics module after that; the rerun reproduced every return to the decimal.

survivalで見れば、ランダム化したpolicyの明らかな勝ちです。学習用モデルで1.00対0.74、backlashで0.99対0.56で、評価スクリプトの最初の版はこの列を先頭に出していました。 しかしsurvivalが問うのは、胴体が4 cmより上にあるかどうかだけです。ランダム化したpolicyが直立していたのはstepの28%(公称は47%)で、胴体の高さの中央値は9 cm、 returnのうちupright項からの分は134(公称のpolicyは183)でした。このpolicyが学んだのは立つことではなく、転倒とは判定されない低く傾いたしゃがみ姿勢で踏ん張ることでした。 その後、直立の割合と胴体の高さを指標モジュールに加えました。再実行では、すべてのreturnが小数点以下まで再現しました。

Backlash: the randomised policy barely notices it (+3.2 return, −0.04 upright), while the nominal one loses 15 and 0.09 and falls more often. That is the robustness randomisation is meant to buy, and it bought it, but around a worse posture. The nominal policy on backlash is still upright more often than the randomised one on the model it trained on. Rollers: the randomised policy collapses, losing 165 of return, with its median trunk 2 mm above the fall line. The nominal policy does better on rollers than on its own model, so for a policy that actually stands this is not a harder physics model at all. A crouch held up by randomised foot friction has nothing to push against on free-rolling contacts. That is a hypothesis, not a measurement.

Backlash:ランダム化したpolicyはほとんど影響を受けません(return +3.2、upright −0.04)。一方、公称のpolicyは15と0.09を失い、倒れる頻度も増えます。 これはランダム化で得ようとしていた頑健性そのもので、実際に得られましたが、より悪い姿勢を軸にしたものでした。backlash上の公称policyでさえ、学習に使ったモデル上のランダム化policyより直立している時間が長いのです。 Rollers:ランダム化したpolicyは崩壊し、returnを165失い、胴体の高さの中央値は転倒ラインのわずか2 mm上でした。公称のpolicyはrollersの方が自分のモデルより良い結果なので、 実際に立てるpolicyにとっては、これはまったく難しい物理モデルではありません。ランダム化した足の摩擦に支えられたしゃがみ姿勢は、自由に転がる接触では踏ん張る先がない、と考えられます。これは仮説であり、計測ではありません。

Net: on this robot, under this reward, at half the schedule and one seed, domain randomisation made the policy worse. That is the question step 4 was built to answer, and it is reported as it came out. What it does not settle: both runs share step 3's step-size problem, milder here (approximate KL rising to 0.32 against 0.76), and neither used the KL stop added afterwards. The likely cause is the reward. Its height term is linear and blind to tilt, so a crouch at 9 cm earns about 1.2 per step every step, against 2.0 for a stand that sometimes ends on the floor near 0.5. Randomised physics makes standing harder to learn and bracing safe under every sample. Testing that needs a new reward version with its own baseline, not a tweak to this one.

結論:このロボット、このreward、予定の半分の学習量、seed 1つという条件では、domain randomisationはpolicyを悪化させました。 これがstep 4で答えるべき問いであり、出た結果をそのまま報告しています。未解決の点もあります。2つの実行はどちらもstep 3のstep sizeの問題を抱えており(ここでは近似KLが0.76に対して0.32まで上昇と軽め)、 後から加えたKL stopはどちらにも使っていません。原因として最も考えられるのはrewardです。height項は線形で傾きを見ないため、9 cmのしゃがみ姿勢は毎step約1.2を稼ぎ続けます。 これに対し、立つ姿勢は2.0を得られるものの、ときどき床に倒れて0.5付近になります。ランダム化したphysicsのもとでは立つことの学習が難しくなり、踏ん張る姿勢はどのサンプルでも安全になります。 これを検証するには、このrewardを微調整するのではなく、独自のbaselineを持つ新しいrewardバージョンが必要です。

16

Step 5 — one graph, normaliser includedStep 5 — normaliserを含む1つのグラフ

The export design, the two checks that run before any timing, and the one-thread latency table for the chunk-1 policy.

exportの設計、時間計測の前に行う2つのチェック、chunk 1のpolicyの1スレッドlatencyの表。

export_onnx.py writes one ONNX graph from the raw 48-dim observation to the 14-dim mean action, with the normaliser folded in as (obs − mean) / std, clipped. A policy shipped without its normaliser is a different policy, and a controller on the robot should not have to know the normaliser existed.

export_onnx.pyは、生の48次元observationから14次元の平均actionまでを1つのONNXグラフとして出力します。normaliserは(obs − mean) / std(clip付き)として組み込みます。 normaliserなしで出荷したpolicyは別のpolicyです。ロボット側のcontrollerが、normaliserの存在を意識しなくて済むようにすべきです。

Verification comes before timing. 1,000 observations from the policy's own rollouts, not white noise, whose range the normaliser has never seen, go through both paths and the maximum difference must be under 1e−5. test_export.py proves the fold with a random actor: the graph disagrees with the un-normalised actor and agrees with the training-time path, and a huge observation gives the same finite answer both ways, so the clip survived export. Then ONNX Runtime latency on one intra-op thread, batch 1, p50 and p99 over 2,000 calls, which is the shape an on-robot controller runs at 50 Hz.

時間計測より先に検証を行います。normaliserが一度も見たことのない範囲のホワイトノイズではなく、policy自身のrolloutから取った1,000個のobservationを両方の経路に通し、 最大差が1e−5未満であることを求めます。test_export.pyはランダムなactorで組み込みを証明します。グラフは正規化なしのactorとは一致せず、学習時の経路とは一致します。 また極端に大きなobservationを入れても両方で同じ有限の値が返るので、clipがexport後も残っていることがわかります。そのうえで、ONNX Runtimeのlatencyを1 intra-opスレッド・batch 1で2,000回呼び出し、p50とp99を計測します。 これはロボット上のcontrollerが50 Hzで動くときの形です。

python export_onnx.py runs/ppo_v2_chunk1.pt   # -> runs/policy_v2.onnx, runs/onnx_v2.json

Measured on the chunk-1 policy:

chunk 1のpolicyでの計測結果:

value値
max |torch − onnx| over 1,000 rollout observationsrolloutのobservation 1,000個での max |torch − onnx|2.4e-06
ONNX Runtime, 1 intra-op thread, batch 1, p50 / p99ONNX Runtime、1 intra-opスレッド、batch 1、p50 / p990.010 / 0.033 ms
torch eager, 1 thread, batch 1, p50 / p99torch eager、1スレッド、batch 1、p50 / p990.026 / 0.128 ms
share of the 20 ms control period used at p99p99時点で20 msの制御周期に占める割合0.2%
17

The same environment in C++C++で書いた同じ環境

A sibling repository that reproduces this environment to the bit and what it found out about this one.

この環境をビット単位で再現する姉妹リポジトリと、それがこの環境について明らかにしたこと。

A separate project, a threaded C++17 backend, ports env.py and vec_env.py to one process with eight threads and a pybind11 binding, and runs ppo.py on it unchanged. Its first claim is equality, not speed: 6,275 of 6,275 observations across 25 episodes are bit-identical to this environment's, which took matching numpy's summation order, Python's pow and a float32 constant. Its second claim is the one that matters here: alternating the two backends in one run, its threads deliver 1.9 to 2.0× this vector environment at eight workers. Its third is this page's step 3, run again on that backend with the same configuration and training seed: 50M steps in 3.1 hours instead of 4.4, and a policy that scores 341.9 ± 4.1 of 500 on this environment against 344.8 ± 2.9 for the one trained here, inside the evaluation noise. The runaway step size of chunk 1 reproduced there too, and the KL stop fixed it the same way. Its survival was 0.95 against this policy's 0.73, so both backends were trained on two more seeds. This step run again on seeds 1 and 2 scores 357.3 and 342.2 with survival 0.999 and 0.895: the 0.73 here is the low draw of three, and across three seeds per backend neither return nor survival differs (339.4 ± 2.3 against 348.1 ± 8.1, survival 0.94 ± 0.06 against 0.87 ± 0.14).

別プロジェクトのスレッド化したC++17バックエンドは、env.pyとvec_env.pyを8スレッドの単一プロセスとpybind11のバインディングに移植し、 その上でppo.pyを無変更で動かします。1つ目の主張は速度ではなく同一性です。25 episodeにわたる6,275個中6,275個のobservationがこの環境とビット単位で一致しており、 そのためにnumpyの加算順序、Pythonのpow、float32の定数まで合わせました。2つ目の主張がここで重要なものです。1回の実行の中で2つのバックエンドを交互に使って比べると、 8 workerでC++版のスレッドはこのvector環境の1.9〜2.0倍の速度を出します。3つ目は、このページのstep 3を同じ設定・同じ学習seedでそのバックエンド上で再実行したものです。 50 M stepが4.4時間ではなく3.1時間で終わり、得られたpolicyはこの環境で500点中341.9 ± 4.1を記録しました。ここで学習したpolicyの344.8 ± 2.9とは評価ノイズの範囲内です。 chunk 1でのstep sizeの暴走はC++版でも再現し、KL stopで同じように解消しました。survivalはC++版が0.95、このpolicyが0.73だったため、両バックエンドでさらに2つのseedで学習しました。 このステップをseed 1と2で再実行すると、scoreは357.3と342.2、survivalは0.999と0.895でした。ここでの0.73は3回中の低い引きであり、バックエンドごとに3 seedで比べると、 returnにもsurvivalにも差はありません(339.4 ± 2.3対348.1 ± 8.1、survival 0.94 ± 0.06対0.87 ± 0.14)。

18

Running it実行方法

Setup, the commands in order, and the runtimes to expect on a CPU.

セットアップ、実行するコマンドの順序、CPUでの想定実行時間。

git clone https://github.com/AungKaung1928/microduck-rl.git
cd microduck-rl
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt

./fetch_assets.sh        # pinned upstream commit, ~24 MB, gitignored
./verify.sh              # tiers 1-5 are cheap; tier 6 loads the box

The cheap checks, individually軽いチェックを個別に実行

python3 test_model.py     # the MJCF contract: variants, actuator order, classes
python3 test_env.py       # 27 environment contract checks
python3 inspect_model.py --all
python3 drop_test.py --mode limp --z0 0.25 --seconds 5
python3 baseline.py --seeds 20               # reward v1, the 108.7 baseline
python3 baseline.py --seeds 20 --reward v2   # reward v2, the 175.2 baseline
python3 test_reward.py && python3 test_ppo.py && python3 test_dr.py && python3 test_export.py

Training, evaluation, export学習、評価、export

OMP_NUM_THREADS=1 nice -n 10 python3 ppo.py --total-steps 50000000 --chunk-steps 25000000 --tag v2 --reward v2
OMP_NUM_THREADS=1 nice -n 10 python3 ppo.py --resume runs/ppo_v2.ckpt.pt --tag v2 --target-kl 0.02   # chunk 2
python3 eval_policy.py runs/ppo_v2.pt --variant groundcontact
python3 export_onnx.py runs/ppo_v2_chunk1.pt   # step 5 was measured on the chunk-1 policy

Each chunk is about two hours at the measured 3,300 env-steps/s. The loop refuses to start on a machine whose one-minute load average is above 4, and prints the rate every update so a throttled or contended run is visible while it happens.

計測した3,300 env-steps/sで、各chunkは約2時間です。学習ループは1分間のload averageが4を超えるマシンでは開始を拒否し、 更新ごとに速度を出力するため、スロットリングや競合が起きている実行はその場で見てわかります。

The benchmark — needs the machine to itselfベンチマーク — マシンを占有して実行

nice -n 10 python3 bench.py --seconds 20 --ref-seconds 10 --tag main
nice -n 10 python3 bench.py --sustained 8 --seconds 20 --windows 12 --tag sustained
nice -n 10 python3 bench_wrapper.py --seconds 12        # 1 core, no load

Steps 1 and 2 need only mujoco and numpy; steps 3 to 5 add CPU-only torch, onnx and onnxruntime. Rendering is needed only by the drop-test filmstrips and goes through whatever MUJOCO_GL names.

Step 1と2に必要なのはmujocoとnumpyだけです。step 3〜5ではCPU版のtorch、onnx、onnxruntimeが加わります。 レンダリングが必要なのは落下テストのコマ送り画像だけで、MUJOCO_GLで指定したバックエンドを使います。

bench.py refuses to run more than eight processes without an explicit flag, and refuses to certify a table if its single-process reference drifts more than 10 % during the sweep.

bench.pyは明示的なフラグなしに8プロセスを超える実行を拒否し、スイープ中に単一プロセスの基準が10 %以上ずれた場合は表を認定しません。

Licenceライセンス

Upstream code is Apache-2.0; the 3D model files are Creative Commons BY-SA-NC. assets/ is therefore gitignored and reproduced by fetch_assets.sh from a pinned commit. Code in the repository is the author's own.

本家のコードはApache-2.0、3DモデルファイルはCreative Commons BY-SA-NCです。そのためassets/はgitignoreに入れ、fetch_assets.shで固定コミットから再現します。 リポジトリ内のコードは作者自身のものです。

Measured environment計測環境

Intel Core Ultra 5 225H, 14 cores, no hyperthreading, 21 GB available to WSL2. Windows 11 + WSL2, Ubuntu 22.04, Python 3.10, MuJoCo 3.12, torch 2.14 CPU, onnxruntime 1.23, software rendering through WSLg. No CUDA anywhere.

Intel Core Ultra 5 225H、14コア、ハイパースレッディングなし、WSL2で使えるメモリ21 GB。Windows 11 + WSL2、Ubuntu 22.04、Python 3.10、MuJoCo 3.12、 torch 2.14 CPU、onnxruntime 1.23、WSLg経由のソフトウェアレンダリング。CUDAは一切使っていません。

19

The short version要約

The whole project compressed into one paragraph.

プロジェクト全体を1段落にまとめたもの。

A balance-and-recover task for a 14-servo open-source biped, built to run entirely on a laptop CPU because the upstream training stack requires CUDA. Step 1 is a feasibility gate whose answer arrived four times, 28 749, then 18 400, then 8 000, then 13 300 environment steps per second, and the write-up keeps every revision, including the one that was wrong in the conservative direction. Along the way it found that the walking model's contact mask lets the robot fall through the floor, that the backlash variant renumbers the state vector, and that the robot's own authors disagree about joint friction by 6.7×, which is where the randomisation range comes from. Step 2 defines the environment: a 48-dimensional observation cut down from 61 so every channel is one real hardware could produce, 14 residual actions with a clip that exists because the control range is 26× wider than the tightest joint limit, fixed-length episodes with seeded pushes, and a reward with a measured PD baseline of 108.7 ± 2.9 out of 500. A review found five silent defects that twenty-seven passing tests had not. Step 3 rebuilt the reward on the numbers, dropping two dead penalties, replacing a flat height Gaussian with a ramp, setting the joint-velocity weight from its measured share and rejecting the obvious upright fix because the fallen duck never tilts past 90°; the PD baseline under the new reward is 175.2 ± 3.1. PPO with a running normaliser and a truncation bootstrap then ran its first 25-million-step chunk, and running it added a fifth revision to the throughput figure: about 3 300 environment steps per second with the policy in the loop, a quarter of the plan, because the eight pipe round trips and the network forward pass had never been in the estimate. The first chunk's step size ran away, with a clip fraction of 0.73 and KL spikes above 50. The second chunk resumed with a KL stop and finished the 50M steps at 344.8 ± 2.9 against the PD controller's 175.8, with every seed up on chunk 1's 332.6 (section 13). Step 4 trained a second policy under randomisation over the measured servo range and tested both on two held-out models from upstream. The randomised policy never counted as fallen, but it had learned to brace in a crouch instead of standing. It shrugged off backlash and collapsed on rollers, a model the nominal policy found easier than its own, so on this reward and this schedule randomisation made things worse. The ONNX export, with the normaliser folded in, runs at 0.033 ms p99 on one thread.

14サーボのオープンソース二足ロボットに、バランス維持と転倒復帰のタスクを課しました。本家の学習環境はCUDAが必要なため、すべてノートPCのCPUで動くように作っています。 Step 1は実現可能性の判定で、その答えは28 749、18 400、8 000、13 300 environment steps/sと4回変わりました。この記録では、保守的な方向に誤っていたものも含め、すべての修正を残しています。 その過程で、歩行用モデルの接触マスクのせいでロボットが床を突き抜けること、backlashモデルでは状態ベクトルの番号が変わること、ロボットの作者自身の間で関節摩擦の値が6.7倍食い違っていることを見つけました。 最後の点がランダム化の範囲の根拠になっています。Step 2では環境を定義しました。すべてのチャネルを実機で得られる値にするため61次元から削った48次元のobservation、 制御範囲が最も狭い関節制限の26倍も広いために必要なclip付きの14個の残差action、seed付きpushのある固定長episode、そして計測したPD baselineが500点中108.7 ± 2.9のrewardです。 レビューでは、通過していた27件のテストが見逃した5つの潜在バグが見つかりました。Step 3では数値にもとづいてrewardを作り直しました。効いていない2つのpenaltyを削除し、平坦な高さのガウス関数をランプに置き換え、 関節速度の重みを計測した割合から設定しました。倒れたロボットは90°を超えて傾かないため、定番のupright修正は却下しました。新しいrewardでのPD baselineは175.2 ± 3.1です。 その後、running normaliserとtruncation bootstrap付きのPPOで最初の25M stepのchunkを実行し、throughputの数値に5回目の修正が加わりました。policyをループに入れると約3 300 environment steps/sで、計画の4分の1です。 8回のパイプ往復とネットワークの順伝播が、一度も見積もりに入っていなかったからです。最初のchunkではstep sizeが暴走し、clip fractionは0.73、KLには50を超えるスパイクがありました。 2つ目のchunkはKL stopを入れて再開し、50M stepを344.8 ± 2.9で終えました。PD controllerは175.8で、すべてのseedでchunk 1の332.6から上昇しています(13節)。 Step 4では、計測したサーボの範囲でランダム化して2つ目のpolicyを学習し、本家の2つのheld-outモデルで両方をテストしました。ランダム化したpolicyは転倒とは一度も判定されませんでしたが、 学んでいたのは立つことではなく、しゃがんで踏ん張ることでした。backlashには動じず、rollersでは崩壊しました。rollersは公称のpolicyにとっては自分のモデルより易しいモデルです。 つまりこのrewardとこの学習量では、ランダム化は事態を悪化させました。normaliserを組み込んだONNX exportは、1スレッドでp99 0.033 msで動きます。