← Start here · Module 5 · Project

Module 5 · Project — VLA on your laptop

진짜 ~450M 파라미터 VLA(SmolVLA)가 MuJoCo 시뮬 속 로봇 팔을 CPU만으로 구동합니다. obs → action → step 지각-행동 루프를 한 스텝씩 눈으로 확인하는 게 이 프로젝트의 전부예요.

🤖 이 페이지는 스타터 robot.zip(파일 4개)을 한 줄씩 뜯어보는 코드 워크스루예요. VLA가 처음이면 Project: fine-tuning에서 개요를 먼저 훑어도 좋습니다.
🚧 CPU-only · 느립니다(스텝당 ~1–3초). 첫 실행에 SmolVLA 가중치 ~1.8 GB를 받고, MuJoCo 에셋도 내려받아요. 그리고 능숙한 동작은 기대하지 마세요 — 왜 그런지가 이 프로젝트의 가장 중요한 교훈입니다(5C.9).

5C.1 · What you'll build

SmolVLA가 gym-aloha(MuJoCo)의 양팔 로봇을 구동 → 결과로 rollout.mp4(팔이 움직이는 영상) + 스텝별 로그(지시와 VLA가 낸 action 벡터)를 남깁니다. 루프를 눈에 보이게 만드는 게 목적이에요.

저가형 팔이 말로 준 지시를 따라 움직이는 모습(실제 하드웨어).

범용 정책(generalist policy)이 여러 태스크·물체로 일반화하는 모습.

🧭 우리가 돌릴 건 이 영상만큼 능숙하진 않아요. 메커니즘(루프)을 CPU에서 직접 굴려보는 게 목표고, 왜 동작이 어설픈지는 5C.9 embodiment에서 정확히 짚습니다.
VLA란? — 한 문장으로

LLM이 텍스트 → 텍스트라면, Vision-Language-Action 모델은 (이미지 + 자연어 지시) → action이에요:

  • Vision — 로봇이 지금 보는 카메라 프레임 한 장 이상.
  • Language — 자연어 목표("transfer the cube to the other arm").
  • Action — 관절/엔드이펙터 델타 + 그리퍼 개폐 같은 저수준 명령 벡터. 이 데모의 양팔 ALOHA는 스텝마다 14차원(6-DoF 팔 2개 + 그리퍼 2개)을 냅니다.

구조적으로 VLA는 멀티모달 LLM(비전 인코더 + 언어 모델)에서 텍스트 생성 head를 action head로 갈아끼운 것과 비슷해요. SmolVLA는 작은 vision-language backbone에 "action expert"를 붙여 짧은 action 청크를 냅니다. 학습은 텔레오퍼레이션 시연의 모방(imitation) — (카메라, state, 지시, action) 튜플로 이뤄져요. LLM에서 넘어올 때의 결정적 전환은 출력이 언어가 아니라 특정 로봇 몸체를 위한 모터 명령이라는 것 — 그래서 embodiment가 그렇게 중요해집니다.

5C.2 · Scaffold — 파일 4개가 전부

robot.zip을 풀면 딱 4개예요. 무겁게 느껴져도 실제 프로젝트 코드는 작습니다 — 무거운 건 다운로드되는 모델·시뮬 에셋이지 우리 코드가 아니에요.

flowchart TB Zip["robot.zip"] --> R["README.md
concepts + setup + troubleshooting"] Zip --> Req["requirements.txt
lerobot[smolvla] · gym-aloha · imageio"] Zip --> Chk["check_setup.py
run first — prints the specs"] Zip --> Demo["run_demo.py
the loop (~40 lines of substance)"] class Demo core;
파일역할
check_setup.py설치·버전을 검증하고 정확한 obs/action 스펙을 출력. 롤아웃 도중 크래시 대신 여기서 드리프트를 먼저 드러냄.
run_demo.pyVLA 닫힌 루프 본체. observation 조립 → 예측 → 시뮬 step → 반복 → rollout.mp4.
requirements.txtlerobot[smolvla](torch·transformers·정책 코드) + gym-aloha(MuJoCo) + imageio(영상).
README.mdVLA란 무엇인가 · 스택 선택 이유 · embodiment 한계 · 트러블슈팅 표.

5C.3 · The stack & why — 왜 이 조합인가

다섯 저장소를 붙이는 대신 LeRobot 하나가 정책·데이터 규약·체크포인트를 묶어줘요. 모델은 SmolVLA(~450M), 시뮬은 gym-aloha(MuJoCo), 런타임은 PyTorch CPU.

flowchart LR I["instruction + camera frame
+ robot state"] --> LR["LeRobot
policies · processors · checkpoints"] LR --> M["SmolVLA (~450M)
vision-language backbone + action expert"] M --> A(["action vector"]) A --> Sim["gym-aloha · MuJoCo
bimanual ALOHA sim"] Sim -. "next frame" .-> I class M core;
레이어선택이유
프레임워크LeRobot (Hugging Face)VLA 정책 + obs/action 규약 + 사전학습 체크포인트를 한 곳에. 저장소 5개를 손으로 붙일 필요 없음.
VLA 모델SmolVLA (~450M) · lerobot/smolvla_base노트북 CPU에서 도는 가장 작은 진짜 VLA. 액션당 ~1–3초. (OpenVLA-7B는 CPU에서 스텝당 수 분)
시뮬레이터gym-aloha (MuJoCo)Apple Silicon CPU 네이티브, GPU 없이 렌더. 양팔 ALOHA 로봇 시뮬.
런타임PyTorch · CPUCUDA 불필요. 이 스택의 MPS(애플 GPU) 지원은 들쭉날쭉 → CPU가 신뢰 기본값.
왜 이렇게 골랐나 — 한 겹 더
  • 왜 LeRobot 하나로? VLA를 손으로 돌리려면 보통 정책 모델·obs/action 규약·전처리기·체크포인트 로더를 서로 다른 저장소에서 긁어와 아귀를 맞춰야 해요. LeRobot은 이 네 조각을 같은 버전으로 묶어 배포하니, from_pretrained 하나로 정책과 그 정책이 기대하는 규약이 함께 딸려옵니다. "glue code를 안 쓰는 게 아니라 이미 붙어서 온다"가 핵심이에요.
  • 왜 하필 SmolVLA(~450M)? "노트북 CPU에서 실제로 도는 진짜 VLA"의 하한선이라서예요. 더 유명한 OpenVLA-7B는 아키텍처는 같지만 CPU 한 스텝에 수 이 걸려 라이브 루프로는 비현실적입니다. 작다고 장난감이 아니라, 메커니즘을 눈으로 볼 수 있는 최소 크기라는 게 선택 이유예요.
  • 왜 GPU가 아니라 CPU? 이 스택에서 MPS(애플 GPU) 지원이 들쭉날쭉해, "빠르지만 가끔 터지는" 경로보다 "느리지만 항상 도는" 경로를 기본값으로 뒀어요. 이 데모의 목적은 처리량이 아니라 루프를 한 스텝씩 이해하는 것이라 CPU의 느림이 오히려 관찰에 유리합니다(스텝당 ~1–3초 = 로그를 읽을 시간).

5C.4 · The loop — 핵심 아이디어

VLA의 본질을 한 루프로: 카메라 이미지 + 지시 → VLA → action 벡터 → 시뮬이 로봇을 움직임 → 다음 프레임 → 반복. run_demo.py가 정확히 이 루프예요(실질 ~40줄).

flowchart LR OBS["camera frame + robot state
+ instruction"] --> VLA["SmolVLA"] VLA --> ACT(["action vector
(14 numbers — not words)"]) ACT --> SIM["MuJoCo steps the arms"] SIM -. "next camera frame" .-> OBS class VLA core;

🔵 점(dot)이 이 네 노드를 영원히 도는 게 이 프로젝트의 전부예요 — OBS → VLA → ACT → SIM → OBS. "루프가 엔진"이라는 걸 눈으로 붙잡아 두세요. 아래 5C.5는 이 루프를 실제 함수 이름으로 펼칩니다.

🔁 5A의 agent loop와 구조가 똑같아요: 관찰이 들어가고 예측이 나옵니다 — 다만 예측이 단어가 아니라 모션이고, 다음 관찰이 그 모션에 대한 세계의 반응이라는 점만 달라요.

LLM 턴과 비교했을 때, 이 VLA 루프에서 가장 본질적으로 다른 점은?

멀티모달 LLM도 이미지를 받지만, 출력은 여전히 텍스트예요. VLA는 출력이 연속 action 벡터(모터 명령)라서 "몸(embodiment)"이 결정적으로 중요해집니다 — 그리고 다음 관찰이 방금 한 행동의 결과라, 열린 텍스트 생성이 아니라 닫힌 지각-행동 루프가 됩니다.

5C.5 · main() 호출 순서 — 흐름을 함수 이름으로 ★

위 루프를 run_demo.py실제 함수 호출 순서로 펼친 지도예요. 앞쪽 4개는 한 번만 하는 셋업, 뒤쪽은 매 스텝 도는 루프 — 초록 점이 그 루프를 계속 돕니다.

flowchart LR LOADC["load_smolvla_class()"] --> PRET["from_pretrained()"] PRET --> PROC["make_pre_post_processors()"] PROC --> RESET["env.reset(seed=0)"] RESET --> BOBS["build_observation()"] BOBS --> PRE["preprocessor()"] PRE --> SEL["select_action()"] SEL --> POST["postprocessor()"] POST --> FIT2["force-fit dims + clip"] FIT2 --> STEP["env.step(action)"] STEP --> REND["env.render()"] STEP -. "next obs" .-> BOBS REND -. "when done" .-> SAVE["imageio.mimsave()"] class SEL core;
🧭 셋업(LOADC→PRET→PROC→RESET)은 딱 한 번, 루프(BOBS→…→STEP)는 스텝마다예요. 점이 도는 여섯 노드가 곧 5C.4의 OBS→VLA→ACT→SIM을 코드로 확대한 것 — 같은 루프, 다른 배율입니다. REND는 루프와 별개로 프레임을 쌓다가, 끝나면 SAVE로 영상을 저장해요.

셋업 네 노드(LOADC→PRET→PROC→RESET)의 실제 코드는 이렇게 짧아요. 두 군데를 눈여겨보세요 — ① policy.reset()이 비우는 action-chunk 큐, ② 전·후처리기를 만들 때의 device 오버라이드.

    SmolVLAPolicy = load_smolvla_class()
    policy = SmolVLAPolicy.from_pretrained(args.checkpoint)
    policy.config.device = args.device
    policy.to(args.device)
    policy.eval()
    policy.reset()  # clears the internal action-chunk queue

    # The pre-processor tokenizes the instruction + normalizes inputs; the
    # post-processor un-normalizes the predicted action. These live alongside the
    # checkpoint on the hub, so we load them from the same path.
    #
    # The saved processor config hard-codes device 'cuda' — its DeviceProcessorStep
    # asserts CUDA at construction time, which fails on a Mac. Override it to our
    # device via the factory's per-step overrides (keyed by the step name).
    device_override = {"device_processor": {"device": args.device}}
    preprocessor, postprocessor = make_pre_post_processors(
        policy.config,
        pretrained_path=args.checkpoint,
        preprocessor_overrides=device_override,
        postprocessor_overrides=device_override,
    )

    env = gym.make(env_id, obs_type="pixels_agent_pos", render_mode="rgb_array")
    obs, _ = env.reset(seed=0)
🧩 action-chunk 큐: SmolVLA의 action expert는 한 번에 여러 스텝치 action(청크)을 예측해요. 그래서 select_action은 매번 새로 추론하는 게 아니라 큐에서 하나씩 꺼내 주고, 큐가 비면 다시 채웁니다. policy.reset()은 이 큐를 비워 깨끗한 상태에서 롤아웃을 시작하게 해요 — 이전 에피소드의 잔여 action이 새 롤아웃에 새지 않도록요. device_override는 CUDA 머신에서 저장된 처리기 설정을 Mac의 CPU로 되돌리는 실무 트릭이고요(README 트러블슈팅과 이어져요).

5C.6 · 먼저 실행: check_setup.py

로봇-러닝 생태계는 빨리 변해요(import 경로·env id·키가 드리프트). 그래서 롤아웃 전에 스펙을 출력해 어긋남을 먼저 드러냅니다.

# check_setup.py — 4단계로 "지금 내 환경의 진짜 스펙"을 찍어줌
# 1) torch / lerobot import 되나
# 2) gym-aloha 가 env 를 registry 에 등록했나  → 등록된 id 목록 출력
# 3) env 만들어 스펙 출력: action_space.shape (정책이 내야 할 action dim!)
#                          카메라 키, agent_pos dim
# 4) SmolVLA 로드 → 체크포인트가 '기대하는' 스펙 출력:
#      cfg.input_features   (카메라 키 · state dim)
#      cfg.output_features  (action dim — 학습 당시 로봇 기준)

진짜 main()의 네 단계는 이렇게 생겼어요 — 각 단계가 print로 자기 결과를 찍어 어디서 어긋났는지 눈에 보이게 합니다.

def main() -> int:
    print("\n1. Core imports")
    # … import torch, lerobot

    print("\n2. Simulator (gym-aloha / MuJoCo)")
    import gym_aloha  # noqa: F401  (registers the gym_aloha/* envs)
    env_ids = [s for s in gym.registry.keys() if "aloha" in s.lower()]

    print("\n3. Make an env and show its spec")
    env = gym.make(env_id, obs_type="pixels_agent_pos", render_mode="rgb_array")
    ok(f"action_space: {env.action_space.shape}  (this is the # of action dims the policy must output)")
    ok(f"agent_pos dim: {obs['agent_pos'].shape}")

    print("\n4. Load the SmolVLA policy and show what IT expects")
    policy = SmolVLAPolicy.from_pretrained("lerobot/smolvla_base")
    cfg = policy.config
    in_feats = getattr(cfg, "input_features", None)
    out_feats = getattr(cfg, "output_features", None)
    print(f"     input_features:  {in_feats}")
    print(f"     output_features: {out_feats}")

왜 중요하냐면 — 4단계가 찍는 체크포인트의 action dim(output_features)과 3단계의 시뮬 action dim(action_space.shape)이 다를 수 있어요. check_setup.py는 바로 이 두 숫자를 나란히 찍고 "다르면 다른 로봇 몸체용 체크포인트"라는 노트까지 출력합니다 — 그 불일치가 곧 embodiment 교훈의 씨앗입니다.

flowchart LR C["check_setup.py"] --> S1["1·2 · imports + env registration"] C --> S2["3 · sim spec
action_space, cameras, agent_pos"] C --> S3["4 · SmolVLA spec
input_features / output_features"] S2 --> Cmp{"do the two
action dims match?"} S3 --> Cmp Cmp -. "if not" .-> Note["checkpoint for a different robot body
→ see 5C.9"] class Cmp core;

5C.7 · build_observation — obs 조립

매 스텝, 시뮬이 준 raw 데이터를 체크포인트가 기대하는 모양으로 맞춰요. 핵심: 모양을 하드코딩하지 않고 policy.config.input_features에서 읽어와서 맞춥니다. 아래 다이어그램이 함수 안 흐름의 지도, 그 아래가 실제 코드예요.

def build_observation(policy, obs: dict, instruction: str):
    # … read expected keys/shapes from config.input_features; fit the sim to them
    import torch
    feats = policy.config.input_features  # {key: PolicyFeature(type, shape)}
    visual_keys = [k for k in feats if k.startswith("observation.images")]
    pixels = obs.get("pixels")
    frames = list(pixels.values()) if isinstance(pixels, dict) else [pixels]
    batch = {}
    for i, key in enumerate(visual_keys):
        frame = frames[i] if i < len(frames) else frames[-1]
        _, h, w = feats[key].shape  # (C, H, W)
        img = torch.from_numpy(frame).float().permute(2, 0, 1) / 255.0  # (C,H,W), unbatched
        img = torch.nn.functional.interpolate(
            img.unsqueeze(0), size=(h, w), mode="bilinear", align_corners=False
        ).squeeze(0)
        batch[key] = img
    if "observation.state" in feats:
        want = feats["observation.state"].shape[0]
        s = np.asarray(obs["agent_pos"], dtype=np.float32)
        s = np.pad(s, (0, want - len(s)))[:want] if len(s) < want else s[:want]
        batch["observation.state"] = torch.from_numpy(s)  # (D,), unbatched
    batch["task"] = instruction  # the language half of "vision-language-action"
    return batch
flowchart LR SIMOBS["sim obs
pixels · agent_pos"] --> ASM["build_observation"] FEAT["policy.config
input_features"] --> ASM INSTR["instruction"] --> ASM ASM --> IMGV["images.* → resize to HxW"] ASM --> STATE["observation.state → pad/truncate"] ASM --> TASKN["task = instruction"] IMGV --> BATCH(["raw batch dict"]) STATE --> BATCH TASKN --> BATCH class FEAT core;

모양(HxW·state 길이)을 코드에 하드코딩하지 않고 config.input_features에서 읽는 이유는?

체크포인트는 자기가 학습된 카메라 키·state dim·action dim을 config에 실어 옵니다. 거기서 읽어 맞추면, 모델이나 라이브러리 버전이 바뀌어도 스펙의 단일 출처(single source of truth)를 따라가 깨지지 않아요 — 드리프트에 강한 설계입니다.

5C.8 · The rollout loop — 예측 한 스텝

조립한 raw obs를 전처리기(토크나이즈+정규화+배치+device)에 통과 → select_action후처리기(역정규화) → 시뮬 step. 이게 매 틱 반복돼요 — 초록 점이 도는 사이클이 곧 아래 코드의 한 바퀴입니다.

for t in range(args.steps):
    raw = build_observation(policy, obs, instruction)
    batch = preprocessor(raw)                 # tokenize + normalize + batch + device
    with torch.inference_mode():
        action = policy.select_action(batch)  # (1, action_dim), normalized
    action = postprocessor(action)            # un-normalize, back to cpu
    action = np.asarray(action.squeeze(0).detach().cpu(), dtype=np.float32)

    # Embodiment adaptation: the base checkpoint outputs actions for a
    # different robot than this sim, so the dims won't match. We force-fit
    # (truncate / zero-pad) and clip to the sim's range so the LOOP runs. …
    if action.shape[0] != env_dim and not warned:
        print(…)                              # one-time mismatch warning (see 5C.9)
        warned = True
    if action.shape[0] > env_dim:
        action = action[:env_dim]
    elif action.shape[0] < env_dim:
        action = np.pad(action, (0, env_dim - action.shape[0]))
    action = np.clip(action, env.action_space.low, env.action_space.high).astype(np.float32)

    obs, reward, terminated, truncated, _ = env.step(action)
    frames.append(env.render())
flowchart LR RAWB["build_observation()"] --> PREP["preprocessor()"] PREP --> PRED["select_action()"] PRED --> POSTP["postprocessor()"] POSTP --> FFIT["force-fit + clip"] FFIT --> ESTEP["env.step()"] ESTEP --> FRND["env.render()"] ESTEP -. "next obs" .-> RAWB class PRED core;
🧩 전처리기/후처리기는 체크포인트 옆에 저장돼 함께 로드돼요(make_pre_post_processors). 지시문을 토큰화하는 게 여기라, 이 단계를 빼먹으면 select_actionKeyError: observation.language.tokens로 터집니다.

preprocessor(raw)가 하는 일이 아닌 것은?

전처리기는 입력을 모델이 먹을 수 있게 다듬는 단계예요(토크나이즈·정규화·배치·device). 팔을 움직이는 건 env.step(action) — 후처리기로 역정규화된 action을 시뮬에 넣을 때죠. 전처리 → 예측 → 후처리 → step 순서가 이 루프의 뼈대입니다.

전·후처리기(make_pre_post_processors)를 "있으면 편한 선택적 glue"라고 봐도 될까요?

전처리기는 이 체크포인트가 학습 때 쓴 그대로의 토큰화기·정규화 통계를 담고 있어요. 그래서 지시문이 여기서 observation.language.tokens로 바뀌고, 입력이 학습 분포로 정규화됩니다. 이걸 건너뛰면 모델이 기대하는 키가 없어 KeyError: observation.language.tokens로 죽고, 설령 통과해도 정규화가 어긋나 값이 무의미해져요 — 그래서 체크포인트와 한 몸이지 곁다리 glue가 아닙니다.

5C.9 · Embodiment & 정직한 한계

루프는 끝까지 돌지만 동작은 의미가 없어요. base 체크포인트는 SO-101(단일 저가 팔)용인데 시뮬은 양팔 ALOHA라, 카메라·state·action의 모양과 의미가 다릅니다. 그 간극이 바로 embodiment 수업이에요.

flowchart LR Ck["lerobot/smolvla_base
trained body = SO-101"] --> Aout["action dim = A"] Env["gym-aloha sim
body = bimanual ALOHA"] --> Bout["action dim = B (14)"] Aout --> FF{"A == B?"} Bout --> FF FF -. "no" .-> Fit["force-fit
truncate/pad + clip"] Fit --> Move["arms move
— but meaninglessly"] class FF core;

force-fit이 실제로 뭘 하는지 눈으로 보면 이래요 — 모델이 낸 벡터를 시뮬이 원하는 슬롯 수에 억지로 맞춥니다:

위 숫자(모델 6 · 시뮬 14)는 대략이에요 — SO-101 단일 팔(≈6) 정책을 양팔 ALOHA(14) 시뮬에 넣으면 모자란 슬롯을 zero-pad합니다(반대로 모델이 더 크면 truncate). 정확한 값은 각자 check_setup.py가 찍어주는 output_featuresaction_space를 보세요; 라이브러리·체크포인트 버전마다 드리프트할 수 있어요. 핵심은 숫자가 아니라 억지로 맞춘다는 사실입니다.

run_demo.py는 이걸 알고 일부러 처리해요: action dim이 안 맞으면 한 번 경고를 찍고 잘라내기/제로패딩 + 시뮬 범위로 clip해서 루프가 끝까지 돌게 합니다. 메커니즘은 정확히 보여주되, 정책이 이 몸에 안 맞으니 동작은 무의미 — 이 정직함이 포인트입니다.

제대로 된 동작을 원하면 (노력 순)
  • ① 시뮬에 맞는 체크포인트 — LeRobot 허브에서 ALOHA-sim 정책. 단, 신뢰성 있게 구할 수 있는 sim 체크포인트는 종종 ACT/Diffusion Policy(image+state→action, 언어 없음)라 "language" 부분은 못 보여줌.
  • ② SmolVLA를 ALOHA-sim 데이터로 fine-tune — 이 몸에 맞는 진짜 VLA를 얻는 정공법. CPU로도 밤새 돌리면 수십 에피소드는 가능. (☜ 5B fine-tuning과 바로 이어지는 아이디어)
  • ③ 더 큰, sim-매칭 VLA (예: LIBERO로 fine-tune된 OpenVLA) — 다만 7B라 CPU 라이브 루프엔 비현실적.

이 데모의 팔 동작이 어설픈 근본 이유는?

모델 크기나 CPU 속도의 문제가 아니에요. action head는 특정 로봇 몸체에 맞춰 학습됩니다. 학습 몸체(SO-101)와 실행 몸체(ALOHA)가 다르면 force-fit으로 루프는 돌아도 모션은 의미가 없어요. 해결책은 몸이 맞는 정책(매칭 체크포인트 또는 fine-tune)입니다.

데모를 돌렸더니 루프가 끝까지 잘 돌았어요. 그럼 이 정책이 이 로봇에 맞는다고 봐도 될까요?

"루프가 돈다 ≠ 정책이 이 몸에 맞는다." force-fit(truncate/zero-pad + clip)은 dim 불일치를 숨기고 루프만 굴러가게 만들 뿐이에요. 크래시가 없다고 embodiment가 맞는 게 아니라, 오히려 force-fit이 불일치를 눈에 안 띄게 덮어버린 거죠. 맞는지 확인하려면 check_setup.py의 두 dim이 실제로 같은지, 그리고 태스크가 완수되는지를 봐야 합니다.

5C.10 · Going further — 이 몸에 맞는 정책으로

base 체크포인트로 "루프를 구경"하는 게 이 폴더의 목적(=옵션 0)이에요. 여기서 제대로 동작까지 가고 싶다면, 자연스러운 다음 걸음들이 있습니다 — 대부분 5B fine-tuning과 곧장 이어져요.

README · Going further
  • LIBERO언어로 지정된 조작 태스크 벤치마크예요. VLA 평가용으로 만들어져 OpenVLA류 체크포인트와 잘 맞고, "지시를 바꾸면 행동이 바뀌는지"를 정량으로 확인할 수 있어요.
  • SmolVLA를 fine-tune — 작은 (sim 또는 실제 SO-101) 데이터셋으로 이 몸에 특화시키면, VLA가 embodiment에 어떻게 맞춰지는지를 몸으로 느낄 수 있어요. CPU로도 밤새 돌리면 수십 에피소드는 가능합니다.
  • 실제 하드웨어 — SmolVLA는 저가형 SO-101 팔을 겨냥해요. LeRobot 문서가 teleoperate → record → train → deploy 전 과정을 안내합니다(시뮬의 embodiment 간극이 사라지는 지점이에요).
  • 정책 계열 비교 — 같은 시뮬에서 ACT / Diffusion Policy / SmolVLA를 바꿔 끼워 "언어 없는" 행동복제(behavior cloning)와 언어 조건 VLA의 차이를 대조해 보세요. 단, 신뢰성 있게 구할 수 있는 sim 체크포인트는 종종 언어가 없는 ACT/Diffusion이라, "language" 부분은 fine-tune으로 채워야 할 수 있어요.
🧵 이 목록의 밑줄은 전부 하나예요: 무거운 backbone은 그대로, 얇은 층(action head)만 이 몸에 맞춘다 — 그게 5B가 embedder를 얼리고 head만 학습한 것과 정확히 같은 리듬입니다.

5C.11 · Build & run locally

Python 3.10 권장, CPU면 충분. 순서: 설치 → check_setup.py(스펙 확인) → run_demo.py(루프).

# 스타터: robot.zip (README + requirements + check_setup.py + run_demo.py)
#   https://ksetp.netlify.app/assets/special-topics/robot.zip
unzip robot.zip && cd robot
python3.10 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt      # lerobot[smolvla] · gym-aloha · imageio[ffmpeg]

python check_setup.py                # 먼저! 스펙 출력 (첫 실행 ~1.8GB 다운로드)
python run_demo.py                   # transfer-cube, 200 steps → rollout.mp4
python run_demo.py --task insertion --steps 300
python run_demo.py --instruction "pick up the cube and pass it to the other hand"
⏱️ 성능(Mac CPU): 모델 로드+다운로드는 첫 회 수 분, 추론은 스텝당 ~1–3초 → 200스텝이면 수 분. 빠르게 보려면 --steps를 줄이세요. 라이브 창은 macOS에서 까다로워, 프레임을 rollout.mp4로 렌더합니다.
🛠️ import 경로·env id가 드리프트하면 README의 Troubleshooting 표를 보세요. 코드는 SmolVLA import 경로를 두 가지 시도하고, obs 키는 config.input_features에서 자동으로 읽어 맞춥니다 — "루프는 오래 가는 아이디어, 주변 API 이름은 아니다"가 이 프로젝트의 태도예요.

5C.12 · The key idea — 루프가 본질, API는 곁가지

VLA = 멀티모달 LLM + action head닫힌 지각-행동 루프로 돌리는 것. 이 루프가 durable하고, 라이브러리 이름·키는 흐릅니다. 그리고 몸(embodiment)이 안 맞으면 정책은 안 통해요.

세 모듈을 관통하는 공통 패턴을 가장 잘 요약한 것은?

5A는 모델을 그대로 두고 루프(제어 구조)로 도구를 부르고, 5B는 얼어붙은 embedder 위 작은 head만 학습하고, 5C는 멀티모달 backbone에 action head만 갈아끼웁니다. 셋 다 무거운 표현은 재사용하고 얇은 층만 목적에 맞춘다는 같은 리듬이에요 — 그게 이 커리큘럼의 관통 아이디어입니다.