Skip to main content
Glama
Alonbbar6

robot-runtime

by Alonbbar6

リモートロボットポリシーのための制御ランタイム

シミュレートされたFranka Pandaがピックアンドプレースを行い、その動作を駆動するポリシーはHTTP境界の背後に存在する——そして、その境界が誤動作しても動作し続ける。

サーバー停止を乗り越えて保持し、再開するランタイム

興味深いのはアームではない。アームとモデルの間にあるすべてのものだ。アクションチャンクのスケジューリング、陳腐化の拒否、バックオフ付きリトライ、サーキットブレーカー、自己解除する保護ホールド、そしてエージェントがセルを操作できても損傷させられないようにするMCPツールサーフェス。

すべてノートパソコン上で動作する。GPUなし、ROSインストールなし、ハードウェアなし。


ネットワークが難しい部分である理由

操作ポリシーはGPUを必要とする。ロボットはリアルタイム制御ループを必要とする。それらが同じマシンであることはまれなので、実際にはモデルはネットワークホップの背後に置かれる——だからこそポリシーは単一ステップではなくアクションチャンクを出力するのだ。50 Hzで推論サーバーと往復することはできないが、一度に400 ms分のアクションを要求し、次のチャンクが送信中である間に実行を継続することはできる。

このリポジトリのすべての難しい問題は、その1つのホップから生じる。

  • チャンクは、観測が取得された時点に存在した世界を記述する。それが届く頃にはすでに古くなっている。古すぎるとはどの程度か?

  • 制御ループは、サーバーが応答したかどうかに関係なく、20 msごとにアームに指令を出さなければならない。実行できる有効なものが何もないとき、それは何をするのか?

  • リクエストは失敗し、リトライされ、順序が乱れて到着する。古い応答が新しい応答を上書きするのを防ぐものは何か?

  • モデルはNaNを出力することがある。バージョンがずれたサーバーは、別のロボット用のターゲットを送ることがある。それをアクチュエータに渡すことを拒否するものは何か?

Related MCP server: VELMA

結果

条件ごとに25シード、実HTTP、シード付きRNGから注入された障害。 python experiments/latency_sweep.py --seeds 25 --ablations

条件

タスク成功率

安全に終了

中央値時間

保持時間

回復回数

p50レイテンシ

陳腐化拒否

リトライ

clean

100%

100%

6.4 s

0.0 s

0

20 ms

0

0

lan — 20 ms ± 5

100%

100%

6.7 s

0.0 s

0

40 ms

0

0

wan — 150 ms ± 40

100%

100%

11.1 s

0.0 s

0

160 ms

0

0

congested — 250 ms ± 150, 5%損失

100%

100%

12.6 s

0.36 s

68

280 ms

222

64

lossy — 20%損失

100%

100%

9.8 s

0.26 s

50

60 ms

197

222

flaky_server — 20% 5xx

100%

100%

8.1 s

0.0 s

0

60 ms

0

248

outage — サーバー3秒停止

100%

100%

13.9 s

6.2 s

25

60 ms

0

24

タスク成功率はキューブがターゲット上にあること。安全に終了は意図的に別の列にしている。実行がタスクに失敗しても正しいことがあり得る。なぜなら停止することが正しい答えである場合があるからだ。この2つをまとめると、ネットワークが悪かったこととロボットがすべきでないことをしたことの違いが隠れてしまう。

表全体にわたるパターンが設計目標である。リンクが劣化するにつれて、ロボットは遅くなるのであって、間違うのではない。250 msの輻輳リンクはサイクル時間を2倍にし、222個の陳腐化チャンクを拒否する。キューブを落としたり、行くべきでない場所に到達したりはしない。

アブレーション——各緩和策を除去

失敗するのを見たことがない安全チェックは、機能すると主張できない安全チェックである。

除去したもの

タスク成功率

中央値時間

備考

(なし — ベースライン congested)

100%

12.6 s

ホールドからの回復 (outage)

0%

0.5 s

ラッチ式停止、再開しない

陳腐化チェック (congested)

88%

23.7 s

動いた世界に対する計画を実行する

リトライ (congested)

96%

17.5 s

適応リード (congested)

100%

12.4 s

ただし保持時間1.10 s vs 0.36 s

リトライ (lossy)

100%

7.9 s

なしの方が速い — 下記参照

障害スイープが実際に発見したこと

これらは両方とも実際の欠陥だった。どちらも健全なlocalhostサーバーに対しては見えず、スイープが初めて実行されたときに両方とも現れた。

1. 保護停止に復帰経路がなかった。 outageプロファイルでは、ランタイムは死んだサーバーを正しく検出し、位置を保持し、e-stopをラッチした——その後、サーバーが3秒後に復帰してもそこに座り続けた。正しい、しかし役に立たない。ネットワークの瞬断のたびに人間が歩いてきて再アームしなければならないロボットは、2週間目でプラグを抜かれる。

この修正は1つの概念を2つに分割する。有効なチャンクが到着した瞬間に自己解除する保護ホールドと、それが決して到着しない場合に8秒後に作動するラッチ式e-stopである。outageは0% → 100%になり、同じ変更でcongestedも修正された。冒頭の図はその修正が機能しているところである。

2. リクエストのリードタイムがレイテンシより短かった。 ランタイムは120 ms分のアクションが残ったときに次のチャンクを要求した。輻輳リンクでは往復に280 msかかった。すべてのリクエストは有用であるには140 ms遅すぎて発行され、アームはほぼすべてのチャンク境界で枯渇した。遅すぎて送信されたリクエストは、いくらリトライしても修正できない——もっと早く要求する必要がある。

ランタイムは現在、自身のp95レイテンシを測定し、リードをそれに合わせてスケーリングする。congestedでの保持時間は1.10 sから0.36 sに低下した。

3. 費用に見合わない緩和策。 lossyプロファイルでは、リトライをオフにすると成功率を失うことなく速くなり(7.9 s vs 9.8 s)、197件の陳腐化拒否もなくなった。低レイテンシのリンクでは、チャンキングがすでに冗長性を提供している。リトライが届く頃には、新しいリクエストの方がより有用だっただろう。リトライはcongestedではその価値を発揮するが(96% → 100%)、lossyでは発揮しない。それが表に含まれている理由だ。機能した緩和策だけを報告することが、機能しないものを出荷することにつながるからだ。

仕組み

        robot side                          │            policy side
                                            │
  ┌──────────────────────────────┐          │       ┌────────────────────┐
  │ runtime.py  50 Hz loop       │          │       │ server.py          │
  │   1 collect ── poll ─────────┼── HTTP ──┼──────▶│  POST /predict     │
  │   2 request ── submit        │          │       │  obs → 20 actions  │
  │   3 act                      │◀─────────┼───────│                    │
  │   4 check                    │          │       └────────────────────┘
  └──┬────────┬────────┬─────────┘          │        stateless; knows
     │        │        │                    │        nothing about episodes
     ▼        ▼        ▼                    │        or scheduling
  client   scheduler  safety                │
  retries  staleness  NaN/limits/workspace  │
  backoff  ordering   rate limit            │
  breaker  discards   e-stop                │

モジュール

1つの役割

contracts.py

配線を越えるすべての型を一度だけ定義

clock.py

時間、注入可能——実時間または仮想時間

sim.py

6つのメソッドの背後にあるMuJoCo。ここでハードウェアに交換する

policies/scripted.py

VLAの代役: ステートレス、チャンク化、リアクティブ

server.py

HTTPの背後にあるポリシー

client.py

submit/poll、デッドライン、リトライ、バックオフ、サーキットブレーカー

scheduler.py

どのチャンクを信頼し、どのアクションを実行するか

safety.py

ポリシーが間違っていると仮定する

runtime.py

50 Hzループ

recording.py

MCAPロギング

mcp_server.py

セルをMCPツールとして公開

特筆すべき3つの決定:

ループはネットワーク上で決してブロックしない。 ステップ2は送信し、ステップ1はポーリングし、何も待たない。遅いサーバーによって停止させられる制御ループは、制御ループではない。

陳腐化は到着時刻ではなくobserved_atから測定される。 戻ってくるのに300 msかかったチャンクは、着地した瞬間に300 ms古い。

安全チェックは2つの異なるレートで実行される。 チャンク検証は高コスト(すべてのアクションに対する順運動学)であり、信頼境界でチャンクごとに1回実行される。レート制限は安価で、毎ティック実行される。悪い計画を丸ごと拒否することは、それを微妙に間違ったものにクランプするよりも優れている。

クロックのトリック

仮想時間は実時間の約100倍の速さで進むため、400 msのロボット時間は4 msのウォールクロックで経過する——localhostへのHTTP往復よりも速い。注意しないと、すべての応答が遅れて見え、実験はランタイムではなくハーネスを測定することになる。

そこでSimClock.settle()は仮想時間を進めずに実秒でブロックする。ランタイムが観測する唯一の遅延は、障害プロファイルが要求した遅延だけである。同じクライアントコード、同じリトライパス、同じ陳腐化ロジック——ハードウェア上のWallClockでは、settle()はno-opである。これが上記のすべての数値をビット単位で再現可能にしている理由である。

エージェントからの操作 (MCP)

python -m robot_runtime.mcp_server

12個のツール。6個は読み取り専用(状態、カメラ、障害プロファイル、録画、監査ログ)、6個はロボットを動かす。ゲーティングはプロンプト内のリクエストではなく、サーバー側の状態である:

run_pick_and_place  → {"ok": false, "error": "cell is not armed",
                       "hint": "call arm_cell with a reason before commanding motion"}
arm_cell("  ")      → {"ok": false, "error": "a reason is required"}
arm_cell("demo")    → {"ok": true, "armed": true, "expires_in_s": 120.0}
emergency_stop()    → {"ok": true, "estopped": true}
run_pick_and_place  → {"ok": false, "error": "cell is e-stopped"}
clear_estop()       → {"ok": false, "error": "confirmation required"}
  • 動作はゲートされる。読み取りはされない。停止ボタンは決してされない。 認証しないと到達できない安全制御は、安全制御ではない。

  • アーミングには理由が必要で、期限が切れる、そして理由はログに記録される。

  • エラーはhintを持つ構造化された結果であり、例外ではない。 hint: start the policy serverを読むエージェントは問題を修正できる。スタックトレースは推測させる。

  • すべての呼び出しは監査ログに追加され、同じインターフェースを通じて読み取り可能なので、「正確に何を呼んだのか?」には常に答えがある。

可観測性とリプレイ

すべてのエピソードはMCAP——ROS 2がログを取るコンテナ——に4つのトピックで記録される: /observation、/action_chunk、/command、/event。重要なのはイベントトピックである。データだけでなく決定を記録するからだ:

3.28s  request_failed:     unreachable
3.52s  breaker_rejected:   circuit open, request not sent
3.80s  protective_hold:    no valid action for 0.50s
6.66s  hold_released:      resumed on chunk 136

最初のバージョンが書いた128行ではなく9行——繰り返されるイベントは圧縮される。ブレーカーは開いている間、毎秒50ティックのすべてでリクエストを拒否し、50個すべてを書いても最初の1個が言わなかったことは何も言わない。

録画のリプレイは、正確なコマンドを新しくシードされたシミュレータに再実行する:

$ python experiments/replay.py recordings/outage-seed0.mcap
  commands_replayed: 533
  placement_error_m: 0.007850735794278705   # live run: 0.007850735794278705
  time drift: 0.000 ms

ビット単位で正確。最初はそうではなかった。/commandは小数点以下6桁に丸めてログされ、実行とそのリプレイの間に1ミクロンのずれが生じた。小さなことだ——しかしそれで「正確に再現する」が偽になり、それが録画の存在意義そのものだった。

これは現場の障害が修正される方法でもある。サイトからMCAPを送り返し、リプレイし、アームが再び間違ったことをするのを自分のノートパソコンで見る。

実行方法

python3 -m venv ~/.venvs/robotarm && ~/.venvs/robotarm/bin/pip install -r requirements.txt

venvは意図的に内蔵ディスクに置かれる。このリポジトリはexFATボリューム上にあり、macOSがAppleDoubleの._ファイルを散らし、MuJoCoのプラグインローダーがそれをdlopenしようとして死に、gitがパックインデックスを維持できないからだ。

python experiments/latency_sweep.py --seeds 25 --ablations   # the results table
mjpython demos/run_with_viewer.py --profile outage           # watch it hold and recover
python experiments/replay.py recordings/outage-seed0.mcap    # replay a recording
python -m robot_runtime.mcp_server                           # agent-facing tools
mjpython demos/pick_and_place.py                             # the original scripted demo
pytest -q                                                    # 38 tests, ~5 s

ビューアを伴うものにはpythonではなくmjpythonを使う——macOSではウィンドウがメインスレッドを所有しなければならない。

テスト

38テスト、約5秒、テスト対象のモックなし。クライアントテストは偽のトランスポートを使うが実際の障害インジェクタを使い、ランタイムテストはスレッド内の実際のサーバーへの実HTTPを介して行われる。

そのうちの3つは上記の欠陥であり、回帰として保持されている: test_server_outage_holds_then_recovers、 test_adaptive_lead_reduces_time_spent_holding、そして test_the_stage_machine_does_not_oscillate——初期のポリシーは高さを位置より先にチェックしたため、lift→carry→lift→carryを切り替えていた。

これは何ではないか

率直に言えば、ロボティクスエンジニアに対して過大な主張をすることは、スクリーニングではなく面接で失敗することになるからです。

  • シミュレーションのみ。 ハードウェアなし、sim-to-real転送なし、実機のPandaに対する接触モデルのキャリブレーションなし。

  • ポリシーは手書きであり、学習されたものではありません。 ステートレス、チャンク化、言語条件付き、リアクティブというVLAのような形を意図的に取っており、実モデルが使うのと同じようにランタイムが動作することが確認されます。しかし、ここで訓練されたものは何もなく、モデルの品質に関する主張は一切行われていません。

  • オブジェクトのポーズはシミュレータから取得され、知覚からではありません。 観測にはカメラ画像が含まれ、コントラクトもそれをサポートしていますが、スクリプト化されたポリシーはピクセルを無視します。実際のデプロイには知覚スタックが必要であり、そのギャップがここで最大のものです。

  • 単一アーム、単一の剛体オブジェクト、単一タスク。

  • ROSではありません。 MCAPが使われるのは、それが適切なコンテナであり、エコシステムがそれを読み取るからですが、ノードもTFツリーもlaunchファイルもありません。

  • 約1日で構築された、ランタイム層に焦点を当てたデモンストレーションです。

次のステップ

価値の高い順に:

  1. ループ内の知覚 — シミュレータではなくカメラ画像からのポーズ取得。このリストで最大のギャップを埋めます。

  2. ポリシーの訓練。 スクリプト化されたコントローラでデモンストレーションを生成し、アクションチャンキングの行動クローニングモデルを適合させ、同じ /predict コントラクトを通じて提供します。ランタイムは1行も変更する必要がないはずです — それが Policy インターフェースが行う主張であり、現在は未検証です。

  3. 2つのアーム — これにより、チャンクスケジューリングが単なる記帳問題ではなく、真の協調問題になります。

  4. 実機のPanda — settle() がno-opになり、このREADMEのすべてのレイテンシ数値が実際に再測定されます。

クレジット

Pandaモデルは MuJoCo Menagerie (Apache 2.0) から。物理エンジン: MuJoCo。ロギング: MCAP。

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Enables driving Omniverse Kit apps (Isaac Sim, Isaac Lab) over MCP, allowing agents to control simulations, run Python, and call namespace-scoped tools via a single bridge.
    1
    2
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables AI agents to control a simulated Pollen Robotics Microduck through MCP tools for movement, tricks, pushing, camera views, and state inspection.
    8
    3
    Apache 2.0
  • A
    license
    A
    quality
    B
    maintenance
    Enables LLM agents to safely inspect, plan, preview, and execute motions on ROS 2 manipulators, with server-enforced safety envelopes covering joint limits, workspace constraints, force/torque thresholds, rate limits, e-stop, human approval, and audit logging.
    22
    237 PyPI
    Apache 2.0