Instructions to use JSHNSL/cyclo-intelligence-patches with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LeRobot
How to use JSHNSL/cyclo-intelligence-patches with LeRobot:
- Notebooks
- Google Colab
- Kaggle
cyclo_intelligence patches: Diffusion/VQ-BeT queue-based policy fix
Browse files- InferenceModelSelector.js +151 -0
- README.md +176 -0
- apply_patches.sh +125 -0
- prediction.diff +54 -0
- prediction.py +76 -0
- 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 |
+
{
|