JSHNSL commited on
Commit
757177d
·
verified ·
1 Parent(s): 2b132c2

cyclo_intelligence patches: Diffusion/VQ-BeT queue-based policy fix

Browse files
Files changed (6) hide show
  1. InferenceModelSelector.js +151 -0
  2. README.md +176 -0
  3. apply_patches.sh +125 -0
  4. prediction.diff +54 -0
  5. prediction.py +76 -0
  6. ui.diff +12 -0
InferenceModelSelector.js ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright 2025 ROBOTIS CO., LTD.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ import React from 'react';
10
+ import { shallowEqual, useSelector, useDispatch } from 'react-redux';
11
+ import clsx from 'clsx';
12
+ import {
13
+ markLocalTaskInfoEdited,
14
+ selectInferenceTaskInfo,
15
+ setInferenceTaskInfo,
16
+ } from '../features/tasks/taskSlice';
17
+
18
+ // Inference models. Each option pairs a backend (orchestrator routing
19
+ // via TaskInfo.service_type) with a policy class (drives instruction
20
+ // visibility and future per-model UI knobs). LeRobot is the backend;
21
+ // ACT, SmolVLA, XVLA, Pi0, Pi0.5, and Diffusion are policy families that
22
+ // can be loaded by that backend when the selected checkpoint is compatible.
23
+ //
24
+ // Add an enabled entry once a policy is validated end-to-end. value is the
25
+ // composite key the dropdown stores; serviceType / policyType are the
26
+ // fields that get written into taskInfo on selection. comingSoon entries are
27
+ // displayed as disabled options only, so they do not affect runtime routing.
28
+ //
29
+ const MODEL_GROUPS = [
30
+ {
31
+ label: 'LeRobot',
32
+ options: [
33
+ { value: 'lerobot:act', label: 'ACT', serviceType: 'lerobot', policyType: 'act' },
34
+ { value: 'lerobot:smolvla', label: 'SmolVLA', serviceType: 'lerobot', policyType: 'smolvla' },
35
+ { value: 'lerobot:xvla', label: 'XVLA', serviceType: 'lerobot', policyType: 'xvla' },
36
+ { value: 'lerobot:pi0', label: 'Pi0', serviceType: 'lerobot', policyType: 'pi0' },
37
+ { value: 'lerobot:pi05', label: 'Pi0.5', serviceType: 'lerobot', policyType: 'pi05' },
38
+ { value: 'lerobot:diffusion', label: 'Diffusion', serviceType: 'lerobot', policyType: 'diffusion' },
39
+ { value: 'lerobot:vqbet', label: 'VQ-BeT', serviceType: 'lerobot', policyType: 'vqbet' },
40
+ ],
41
+ },
42
+ {
43
+ label: 'GR00T',
44
+ options: [
45
+ { value: 'groot:n17', label: 'N1.7', serviceType: 'groot', policyType: 'n17' },
46
+ ],
47
+ },
48
+ {
49
+ label: 'Coming Soon',
50
+ options: [
51
+ {
52
+ value: 'future:greenvla',
53
+ label: 'GreenVLA',
54
+ serviceType: 'future',
55
+ policyType: 'greenvla',
56
+ comingSoon: true,
57
+ },
58
+ {
59
+ value: 'future:openpi',
60
+ label: 'OpenPI',
61
+ serviceType: 'future',
62
+ policyType: 'openpi',
63
+ comingSoon: true,
64
+ },
65
+ {
66
+ value: 'future:rldx1',
67
+ label: 'RLDX-1',
68
+ serviceType: 'future',
69
+ policyType: 'rldx1',
70
+ comingSoon: true,
71
+ },
72
+ ],
73
+ },
74
+ ];
75
+
76
+ export const MODEL_OPTIONS = MODEL_GROUPS.flatMap((group) => group.options);
77
+ const AVAILABLE_MODEL_OPTIONS = MODEL_OPTIONS.filter((opt) => !opt.comingSoon);
78
+ const DEFAULT = AVAILABLE_MODEL_OPTIONS[0];
79
+
80
+ const classLabel = clsx(
81
+ 'text-sm', 'text-gray-600', 'w-28', 'flex-shrink-0', 'font-medium'
82
+ );
83
+
84
+ const InferenceModelSelector = ({ readonly = false }) => {
85
+ const dispatch = useDispatch();
86
+ const info = useSelector(selectInferenceTaskInfo, shallowEqual);
87
+ const serviceType = info.serviceType || DEFAULT.serviceType;
88
+ const policyType = info.policyType || DEFAULT.policyType;
89
+ const value = `${serviceType}:${policyType}`;
90
+
91
+ const handleChange = (e) => {
92
+ const sel = AVAILABLE_MODEL_OPTIONS.find((o) => o.value === e.target.value);
93
+ if (!sel) return;
94
+ dispatch(
95
+ setInferenceTaskInfo({
96
+ serviceType: sel.serviceType,
97
+ policyType: sel.policyType,
98
+ accelerationMode: sel.serviceType === 'groot'
99
+ ? (info.accelerationMode || 'pytorch')
100
+ : 'pytorch',
101
+ accelerationEnginePath: sel.serviceType === 'groot'
102
+ ? (info.accelerationEnginePath || '')
103
+ : '',
104
+ })
105
+ );
106
+ dispatch(markLocalTaskInfoEdited({ source: 'inference' }));
107
+ };
108
+
109
+ return (
110
+ <div className={clsx('flex', 'items-center', 'mb-2.5')}>
111
+ <span className={classLabel}>Model</span>
112
+ <select
113
+ className={clsx(
114
+ 'flex-1',
115
+ 'h-8',
116
+ 'px-2',
117
+ 'border',
118
+ 'border-gray-300',
119
+ 'rounded-md',
120
+ 'focus:outline-none',
121
+ 'focus:ring-2',
122
+ 'focus:ring-blue-500',
123
+ 'focus:border-transparent',
124
+ {
125
+ 'bg-gray-100 cursor-not-allowed text-gray-500': readonly,
126
+ 'bg-white': !readonly,
127
+ }
128
+ )}
129
+ value={value}
130
+ onChange={handleChange}
131
+ disabled={readonly}
132
+ >
133
+ {MODEL_GROUPS.map((group) => (
134
+ <optgroup key={group.label} label={group.label}>
135
+ {group.options.map((opt) => (
136
+ <option
137
+ key={opt.value}
138
+ value={opt.value}
139
+ disabled={Boolean(opt.comingSoon)}
140
+ >
141
+ {opt.label}
142
+ </option>
143
+ ))}
144
+ </optgroup>
145
+ ))}
146
+ </select>
147
+ </div>
148
+ );
149
+ };
150
+
151
+ export default InferenceModelSelector;
README.md ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ tags:
4
+ - robotics
5
+ - lerobot
6
+ - cyclo-intelligence
7
+ - patch
8
+ pretty_name: cyclo_intelligence patches (Diffusion / VQ-BeT)
9
+ ---
10
+
11
+ # cyclo_intelligence 패치 — Diffusion / VQ-BeT 정책이 안 돌아가는 문제
12
+
13
+ [ROBOTIS cyclo_intelligence](https://github.com/ROBOTIS-GIT/cyclo_intelligence) 에서
14
+ **Diffusion·VQ-BeT 정책을 배포하면 로봇이 움직이지 않는다.** ACT는 정상 동작한다.
15
+ 그 원인과 수정본이다.
16
+
17
+ **기준 커밋**: `8065dfc` (2026-07-10, `ROBOTIS-GIT/cyclo_intelligence` main)
18
+
19
+ ---
20
+
21
+ ## 한 줄 적용
22
+
23
+ ```bash
24
+ curl -fsSL https://huggingface.co/JSHNSL/cyclo-intelligence-patches/resolve/main/apply_patches.sh | bash
25
+ ```
26
+
27
+ VQ-BeT까지 쓰려면 (웹 UI 재빌드 포함):
28
+ ```bash
29
+ curl -fsSL https://huggingface.co/JSHNSL/cyclo-intelligence-patches/resolve/main/apply_patches.sh | WITH_UI=yes bash
30
+ ```
31
+
32
+ > cyclo_intelligence 가 `~/cyclo_intelligence` 가 아니면: `CYCLO_INTELLIGENCE_DIR=/경로 bash apply_patches.sh`
33
+ > 스크립트는 **백업(`*.bak`)을 만들고**, 이미 적용됐으면 건너뛴다.
34
+
35
+ ---
36
+
37
+ ## 무엇이 문제였나
38
+
39
+ ### ① 엔진이 큐 기반 정책을 직접 호출한다 (핵심)
40
+
41
+ `lerobot_engine/prediction.py` 는 정책 추론을 이렇게 한다:
42
+
43
+ ```python
44
+ action = self._policy.predict_action_chunk(batch) # 직접 호출
45
+ ```
46
+
47
+ 그런데 **Diffusion과 VQ-BeT의 `predict_action_chunk` 는 단독 호출이 불가능**하다.
48
+ 내부 observation 큐(`self._queues`)를 stack하는데, **그 큐를 채우는 건 `select_action` 뿐**이기 때문이다:
49
+
50
+ ```python
51
+ # lerobot/policies/diffusion/modeling_diffusion.py
52
+ def predict_action_chunk(self, batch, noise=None):
53
+ # stack n latest observations from the queue
54
+ batch = {k: torch.stack(list(self._queues[k]), dim=1) for k in batch if k in self._queues}
55
+ ```
56
+
57
+ 큐가 비어 있으니:
58
+ ```
59
+ RuntimeError: stack expects a non-empty TensorList
60
+ ```
61
+ → `get_action` 실패 → **액션이 발행되지 않음 → 로봇이 안 움직인다.**
62
+
63
+ **ACT는 `n_obs_steps=1` 이라 큐를 안 쓴다.** 그래서 ACT만 멀쩡했던 것이다.
64
+
65
+ 기존 fallback은 `except (NotImplementedError, AttributeError)` 만 잡아서 **`RuntimeError` 를 못 걸렀다.**
66
+
67
+ **수정**: 큐 기반 정책은 `predict_action_chunk` 를 아예 건너뛰고 `select_action` 으로 라우팅한다.
68
+
69
+ ```python
70
+ _QUEUE_BASED_POLICIES = {"VQBeTPolicy", "DiffusionPolicy"}
71
+
72
+ def _predict_chunk(self, batch):
73
+ if type(self._policy).__name__ in self._QUEUE_BASED_POLICIES:
74
+ return self._select_action_chunk(batch) # 큐를 채우는 경로
75
+ try:
76
+ ...
77
+ except (NotImplementedError, AttributeError, RuntimeError, AssertionError):
78
+ return self._select_action_chunk(batch) # 안전망
79
+ ```
80
+
81
+ ### ② `diffusers` 가 추론 컨테이너에 없다
82
+
83
+ Diffusion 정책이 요구한다. `lerobot_server` 컨테이너엔 **`pip` 이 없고 `uv` 를 쓴다**:
84
+
85
+ ```bash
86
+ docker exec lerobot_server sh -c 'VIRTUAL_ENV=/lerobot/.venv uv pip install diffusers'
87
+ ```
88
+ > `pip install` 은 `No module named pip` 으로 실패한다.
89
+
90
+ ### ③ 웹 UI 드롭다운에 VQ-BeT이 없다 (VQ-BeT만 해당)
91
+
92
+ 백엔드는 vqbet을 지원하는데 UI 목록에 항목이 없다. 한 줄 추가 + 재빌드가 필요하다.
93
+
94
+ > ⚠️ `./docker/container.sh build-ui` 는 **실패한다.** npm을 호스트 uid로 돌리는데 UI가 컨테이너
95
+ > `/root/` 밑이라 permission denied가 나고, 그게 **엉뚱하게 "no lockfile" 에러로 보고**된다.
96
+ > **root로 직접 빌드**해야 한다 (스크립트가 그렇게 한다).
97
+
98
+ ---
99
+
100
+ ## 파일
101
+
102
+ | 파일 | 내용 |
103
+ |------|------|
104
+ | `apply_patches.sh` | 전부 적용하는 스크립트 |
105
+ | `prediction.py` | 패치된 엔진 → `cyclo_brain/policy/lerobot/lerobot_engine/prediction.py` |
106
+ | `InferenceModelSelector.js` | VQ-BeT 항목 추가 → `orchestrator/ui/src/components/InferenceModelSelector.js` |
107
+ | `prediction.diff` / `ui.diff` | 원본 대비 변경분 (검토용) |
108
+
109
+ ---
110
+
111
+ ## 수동 적용
112
+
113
+ ```bash
114
+ CI=~/cyclo_intelligence
115
+ BASE=https://huggingface.co/JSHNSL/cyclo-intelligence-patches/resolve/main
116
+
117
+ # ① 엔진 패치
118
+ cp "$CI/cyclo_brain/policy/lerobot/lerobot_engine/prediction.py"{,.bak}
119
+ curl -fsSL "$BASE/prediction.py" -o "$CI/cyclo_brain/policy/lerobot/lerobot_engine/prediction.py"
120
+
121
+ # ② diffusers (Diffusion 쓸 때만)
122
+ docker exec lerobot_server sh -c 'VIRTUAL_ENV=/lerobot/.venv uv pip install diffusers'
123
+
124
+ # ③ 재시작
125
+ docker restart lerobot_server
126
+ ```
127
+
128
+ VQ-BeT도 쓰려면 추가로:
129
+ ```bash
130
+ cp "$CI/orchestrator/ui/src/components/InferenceModelSelector.js"{,.bak}
131
+ curl -fsSL "$BASE/InferenceModelSelector.js" -o "$CI/orchestrator/ui/src/components/InferenceModelSelector.js"
132
+
133
+ CID=$(docker ps -qf name=cyclo_intelligence)
134
+ UID_D=/root/ros2_ws/src/cyclo_intelligence/orchestrator/ui
135
+ docker exec -w "$UID_D" "$CID" bash -lc "npm ci --legacy-peer-deps"
136
+ docker exec -w "$UID_D" "$CID" bash -lc "npm run build"
137
+ docker exec "$CID" sh -c "cp -a $UID_D/build/. /usr/share/nginx/html/ && nginx -s reload"
138
+ # 브라우저 강력 새로고침 (Ctrl+Shift+R)
139
+ ```
140
+
141
+ ---
142
+
143
+ ## 확인
144
+
145
+ ```bash
146
+ # 엔진에 패치가 살아있나
147
+ docker exec lerobot_server sh -c 'grep -n _QUEUE_BASED_POLICIES /app/lerobot_engine/prediction.py'
148
+
149
+ # 추론 중 로그
150
+ docker logs -f lerobot_server
151
+ ```
152
+ - `RuntimeError: stack expects a non-empty TensorList` 가 **안 나오면** 성공
153
+ - `Action chunk: T=..., D=...` 가 반복되면 액션이 나가는 중
154
+
155
+ ## 되돌리기
156
+
157
+ ```bash
158
+ mv <파일>.bak <파일>
159
+ docker restart lerobot_server
160
+ ```
161
+
162
+ ---
163
+
164
+ ## 주의
165
+
166
+ - `prediction.py` 는 **호스트 파일**이고 컨테이너엔 read-only 마운트된다 → 호스트에서 고치고 **재시작**해야 반영된다.
167
+ - `diffusers` 설치는 컨테이너의 writable layer에 들어간다. `docker restart` 는 유지되지만
168
+ **컨테이너를 재생성(`--force-recreate`, `docker rm`)하면 사라진다** → 다시 설치해야 한다.
169
+ - cyclo_intelligence 가 업데이트되면 `prediction.py` 가 덮어써질 수 있다 → 그때 다시 적용한다.
170
+
171
+ ---
172
+
173
+ ## 관련
174
+
175
+ - 전체 학습 파이프라인 (Isaac Sim → LeRobot → 학습 → 배포): [JSHNSL/humanoid-imitation-learning](https://huggingface.co/JSHNSL/humanoid-imitation-learning)
176
+ - upstream: [ROBOTIS-GIT/cyclo_intelligence](https://github.com/ROBOTIS-GIT/cyclo_intelligence)
apply_patches.sh ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ #
3
+ # cyclo_intelligence patches — apply in one command.
4
+ #
5
+ # curl -fsSL https://huggingface.co/JSHNSL/cyclo-intelligence-patches/resolve/main/apply_patches.sh | bash
6
+ #
7
+ # or download this file and: bash apply_patches.sh
8
+ #
9
+ # What it does:
10
+ # 1. Patches lerobot_engine/prediction.py -> Diffusion / VQ-BeT policies actually run
11
+ # 2. Installs `diffusers` in lerobot_server -> required by Diffusion
12
+ # 3. (optional) Adds VQ-BeT to the web UI dropdown and rebuilds it
13
+ # 4. Restarts lerobot_server
14
+ #
15
+ set -euo pipefail
16
+
17
+ REPO="https://huggingface.co/JSHNSL/cyclo-intelligence-patches/resolve/main"
18
+ CI_DIR="${CYCLO_INTELLIGENCE_DIR:-$HOME/cyclo_intelligence}"
19
+ WITH_UI="${WITH_UI:-ask}" # yes | no | ask
20
+
21
+ say() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
22
+ warn() { printf '\033[33m ! %s\033[0m\n' "$*"; }
23
+ ok() { printf '\033[32m ✓ %s\033[0m\n' "$*"; }
24
+ die() { printf '\033[31m ✗ %s\033[0m\n' "$*" >&2; exit 1; }
25
+
26
+ # ---------------------------------------------------------------- checks
27
+ say "Checking environment"
28
+ [ -d "$CI_DIR" ] || die "cyclo_intelligence not found at $CI_DIR (set CYCLO_INTELLIGENCE_DIR)"
29
+ ok "cyclo_intelligence: $CI_DIR"
30
+
31
+ command -v docker >/dev/null || die "docker not found"
32
+ docker ps --format '{{.Names}}' | grep -q '^lerobot_server$' \
33
+ || die "lerobot_server container is not running (start cyclo_intelligence first)"
34
+ ok "lerobot_server is running"
35
+
36
+ # ---------------------------------------------------------------- 1. engine patch
37
+ say "1/4 Patching the inference engine (Diffusion / VQ-BeT)"
38
+ ENGINE="$CI_DIR/cyclo_brain/policy/lerobot/lerobot_engine/prediction.py"
39
+ [ -f "$ENGINE" ] || die "not found: $ENGINE"
40
+
41
+ if grep -q '_QUEUE_BASED_POLICIES' "$ENGINE"; then
42
+ ok "already patched — skipping"
43
+ else
44
+ cp "$ENGINE" "$ENGINE.bak"
45
+ ok "backup: $ENGINE.bak"
46
+ curl -fsSL "$REPO/prediction.py" -o "$ENGINE"
47
+ grep -q '_QUEUE_BASED_POLICIES' "$ENGINE" || die "download failed"
48
+ ok "patched prediction.py"
49
+ fi
50
+
51
+ # ---------------------------------------------------------------- 2. diffusers
52
+ say "2/4 Installing diffusers into lerobot_server (needed by Diffusion)"
53
+ if docker exec lerobot_server sh -c '/lerobot/.venv/bin/python -c "import diffusers"' 2>/dev/null; then
54
+ ok "diffusers already installed"
55
+ else
56
+ # this container has uv, not pip
57
+ docker exec lerobot_server sh -c 'VIRTUAL_ENV=/lerobot/.venv uv pip install diffusers' >/dev/null
58
+ docker exec lerobot_server sh -c '/lerobot/.venv/bin/python -c "import diffusers; print(diffusers.__version__)"' \
59
+ | sed 's/^/ diffusers /'
60
+ ok "installed"
61
+ fi
62
+
63
+ # ---------------------------------------------------------------- 3. UI (VQ-BeT)
64
+ say "3/4 Web UI — add VQ-BeT to the model dropdown"
65
+ if [ "$WITH_UI" = "ask" ]; then
66
+ if [ -t 0 ]; then
67
+ read -r -p " Add VQ-BeT to the dropdown? (rebuilds the UI, ~2-5 min) [y/N] " a
68
+ [[ "$a" =~ ^[Yy]$ ]] && WITH_UI=yes || WITH_UI=no
69
+ else
70
+ WITH_UI=no # piped into bash: skip by default
71
+ warn "non-interactive — skipping UI (re-run with WITH_UI=yes to include it)"
72
+ fi
73
+ fi
74
+
75
+ if [ "$WITH_UI" = "yes" ]; then
76
+ SEL="$CI_DIR/orchestrator/ui/src/components/InferenceModelSelector.js"
77
+ [ -f "$SEL" ] || die "not found: $SEL"
78
+
79
+ if grep -q "lerobot:vqbet" "$SEL"; then
80
+ ok "dropdown already has VQ-BeT"
81
+ else
82
+ cp "$SEL" "$SEL.bak"
83
+ curl -fsSL "$REPO/InferenceModelSelector.js" -o "$SEL"
84
+ ok "patched InferenceModelSelector.js"
85
+ fi
86
+
87
+ CID=$(docker ps -qf name=cyclo_intelligence)
88
+ [ -n "$CID" ] || die "cyclo_intelligence container is not running"
89
+ UID_D=/root/ros2_ws/src/cyclo_intelligence/orchestrator/ui
90
+
91
+ # NOTE: ./docker/container.sh build-ui runs npm as the host uid, but the UI
92
+ # lives under /root/ inside the container -> permission denied, reported as a
93
+ # bogus "no lockfile" error. Build as root instead.
94
+ warn "building the UI as root (container.sh build-ui fails on permissions)"
95
+ docker exec -w "$UID_D" "$CID" bash -lc "npm ci --legacy-peer-deps" >/dev/null 2>&1 || true
96
+ docker exec -w "$UID_D" "$CID" bash -lc "npm run build" >/dev/null
97
+ docker exec "$CID" sh -c "cp -a $UID_D/build/. /usr/share/nginx/html/ && nginx -s reload"
98
+ ok "UI rebuilt — hard-refresh the browser (Ctrl+Shift+R)"
99
+ else
100
+ ok "skipped (Diffusion works without it; VQ-BeT needs it)"
101
+ fi
102
+
103
+ # ---------------------------------------------------------------- 4. restart
104
+ say "4/4 Restarting lerobot_server"
105
+ docker restart lerobot_server >/dev/null
106
+ for _ in $(seq 1 30); do
107
+ [ "$(docker inspect lerobot_server --format '{{.State.Health.Status}}' 2>/dev/null)" = healthy ] && break
108
+ sleep 2
109
+ done
110
+ ok "lerobot_server restarted"
111
+
112
+ docker exec lerobot_server sh -c 'grep -c _QUEUE_BASED_POLICIES /app/lerobot_engine/prediction.py' >/dev/null \
113
+ && ok "engine patch is live in the container"
114
+
115
+ say "Done"
116
+ cat <<'EOF'
117
+ Diffusion and VQ-BeT policies will now produce actions instead of crashing with
118
+ "RuntimeError: stack expects a non-empty TensorList".
119
+
120
+ Watch the engine while running inference:
121
+ docker logs -f lerobot_server
122
+
123
+ Rollback:
124
+ mv <file>.bak <file> && docker restart lerobot_server
125
+ EOF
prediction.diff ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/cyclo_brain/policy/lerobot/lerobot_engine/prediction.py b/cyclo_brain/policy/lerobot/lerobot_engine/prediction.py
2
+ index 6e298f3..9db6547 100644
3
+ --- a/cyclo_brain/policy/lerobot/lerobot_engine/prediction.py
4
+ +++ b/cyclo_brain/policy/lerobot/lerobot_engine/prediction.py
5
+ @@ -21,22 +21,43 @@ logger = logging.getLogger("lerobot_engine")
6
+ class PredictionMixin:
7
+ """Policy input batch -> action chunk."""
8
+
9
+ + # Policies whose predict_action_chunk cannot be called standalone: it
10
+ + # stacks internal observation queues (self._queues) that ONLY select_action
11
+ + # populates, so calling it directly raises "stack expects a non-empty
12
+ + # TensorList" and the robot never receives an action.
13
+ + #
14
+ + # DiffusionPolicy.predict_action_chunk:
15
+ + # batch = {k: torch.stack(list(self._queues[k]), dim=1) for k in batch ...}
16
+ + # VQBeTPolicy.predict_action_chunk: same, plus a combined OBS_IMAGES key
17
+ + # that only select_action builds.
18
+ + #
19
+ + # ACT does not use queues (n_obs_steps=1), which is why it works with the
20
+ + # direct call. Route the queue-based ones through select_action instead;
21
+ + # they manage their own action-chunk queue internally.
22
+ + _QUEUE_BASED_POLICIES = {"VQBeTPolicy", "DiffusionPolicy"}
23
+ +
24
+ def _predict_chunk(self, batch: Dict[str, torch.Tensor]) -> torch.Tensor:
25
+ """Return a chunk tensor of shape (1, T, A)."""
26
+ assert self._policy is not None
27
+ + if type(self._policy).__name__ in self._QUEUE_BASED_POLICIES:
28
+ + return self._select_action_chunk(batch)
29
+ try:
30
+ action = self._policy.predict_action_chunk(batch)
31
+ if action.dim() == 2:
32
+ action = action.unsqueeze(1)
33
+ return action
34
+ - except (NotImplementedError, AttributeError):
35
+ + except (NotImplementedError, AttributeError, RuntimeError, AssertionError):
36
+ logger.debug(
37
+ - "predict_action_chunk unavailable; falling back to select_action"
38
+ + "predict_action_chunk unavailable/failed; falling back to select_action"
39
+ )
40
+ - action = self._policy.select_action(batch)
41
+ - if action.dim() == 1:
42
+ - action = action.unsqueeze(0)
43
+ - return action.unsqueeze(1)
44
+ + return self._select_action_chunk(batch)
45
+ +
46
+ + def _select_action_chunk(self, batch: Dict[str, torch.Tensor]) -> torch.Tensor:
47
+ + """select_action -> (1, 1, A) chunk."""
48
+ + action = self._policy.select_action(batch)
49
+ + if action.dim() == 1:
50
+ + action = action.unsqueeze(0)
51
+ + return action.unsqueeze(1)
52
+
53
+ @staticmethod
54
+ def _to_numpy_chunk(action: torch.Tensor) -> np.ndarray:
prediction.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ #
3
+ # Copyright 2026 ROBOTIS CO., LTD.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0
6
+
7
+ """LeRobot prediction helpers."""
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Dict
13
+
14
+ import numpy as np
15
+ import torch
16
+
17
+
18
+ logger = logging.getLogger("lerobot_engine")
19
+
20
+
21
+ class PredictionMixin:
22
+ """Policy input batch -> action chunk."""
23
+
24
+ # Policies whose predict_action_chunk cannot be called standalone: it
25
+ # stacks internal observation queues (self._queues) that ONLY select_action
26
+ # populates, so calling it directly raises "stack expects a non-empty
27
+ # TensorList" and the robot never receives an action.
28
+ #
29
+ # DiffusionPolicy.predict_action_chunk:
30
+ # batch = {k: torch.stack(list(self._queues[k]), dim=1) for k in batch ...}
31
+ # VQBeTPolicy.predict_action_chunk: same, plus a combined OBS_IMAGES key
32
+ # that only select_action builds.
33
+ #
34
+ # ACT does not use queues (n_obs_steps=1), which is why it works with the
35
+ # direct call. Route the queue-based ones through select_action instead;
36
+ # they manage their own action-chunk queue internally.
37
+ _QUEUE_BASED_POLICIES = {"VQBeTPolicy", "DiffusionPolicy"}
38
+
39
+ def _predict_chunk(self, batch: Dict[str, torch.Tensor]) -> torch.Tensor:
40
+ """Return a chunk tensor of shape (1, T, A)."""
41
+ assert self._policy is not None
42
+ if type(self._policy).__name__ in self._QUEUE_BASED_POLICIES:
43
+ return self._select_action_chunk(batch)
44
+ try:
45
+ action = self._policy.predict_action_chunk(batch)
46
+ if action.dim() == 2:
47
+ action = action.unsqueeze(1)
48
+ return action
49
+ except (NotImplementedError, AttributeError, RuntimeError, AssertionError):
50
+ logger.debug(
51
+ "predict_action_chunk unavailable/failed; falling back to select_action"
52
+ )
53
+ return self._select_action_chunk(batch)
54
+
55
+ def _select_action_chunk(self, batch: Dict[str, torch.Tensor]) -> torch.Tensor:
56
+ """select_action -> (1, 1, A) chunk."""
57
+ action = self._policy.select_action(batch)
58
+ if action.dim() == 1:
59
+ action = action.unsqueeze(0)
60
+ return action.unsqueeze(1)
61
+
62
+ @staticmethod
63
+ def _to_numpy_chunk(action: torch.Tensor) -> np.ndarray:
64
+ """(B, T, A) or (B, A) tensor -> (T, A) float64 numpy."""
65
+ chunk = action.detach().cpu()
66
+ if chunk.dim() == 3:
67
+ chunk = chunk[0]
68
+ elif chunk.dim() == 2:
69
+ pass
70
+ elif chunk.dim() == 1:
71
+ chunk = chunk.unsqueeze(0)
72
+ else:
73
+ raise ValueError(
74
+ f"Unexpected action tensor shape: {tuple(chunk.shape)}"
75
+ )
76
+ return chunk.to(torch.float64).numpy()
ui.diff ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/orchestrator/ui/src/components/InferenceModelSelector.js b/orchestrator/ui/src/components/InferenceModelSelector.js
2
+ index bf5487d..03360fb 100644
3
+ --- a/orchestrator/ui/src/components/InferenceModelSelector.js
4
+ +++ b/orchestrator/ui/src/components/InferenceModelSelector.js
5
+ @@ -36,6 +36,7 @@ const MODEL_GROUPS = [
6
+ { value: 'lerobot:pi0', label: 'Pi0', serviceType: 'lerobot', policyType: 'pi0' },
7
+ { value: 'lerobot:pi05', label: 'Pi0.5', serviceType: 'lerobot', policyType: 'pi05' },
8
+ { value: 'lerobot:diffusion', label: 'Diffusion', serviceType: 'lerobot', policyType: 'diffusion' },
9
+ + { value: 'lerobot:vqbet', label: 'VQ-BeT', serviceType: 'lerobot', policyType: 'vqbet' },
10
+ ],
11
+ },
12
+ {