VLA - Recent Models Review

VLA - Recent Models Review

action 을 토큰으로 낼까, 연속값으로 낼까. OpenVLA, OpenVLA-OFT, π0, π0-FAST 4 개 모델을 내가 만든 π0 스타일 VLA 코드 기준으로, 각 모델이 어디를 어떻게 바꿨는지 정리한다.

VLA - Recent Models Review

Introduction


지금까지 π0 스타일 VLA (VLM prefix + flow matching action expert) 를 직접 만들어서 PushT 66% 까지 왔다. 이제 요즘 모델들 (WAM 등) 로 넘어가기 전에, VLA 설계에서 제일 큰 갈림길 하나를 먼저 정리하고 가자.

action 을 어떻게 내보낼까? → 글자처럼 토큰으로 (이산) vs 숫자 그대로 연속값으로

이번 글에서 볼 모델 4 개는 전부 이 질문에 대한 서로 다른 답이다.

모델나온 시점action 표현action 을 내는 방식논문
OpenVLA2024.06토큰 (차원마다 256 bin)LLM 이 글자처럼 하나씩 생성 (autoregressive)arXiv 2406.09246
OpenVLA-OFT2025.02연속값chunk 전체를 한 번에 (parallel) + MLP 회귀 (L1)arXiv 2502.19645
π02024.10연속값action expert + flow matching (noise → action, 10 step)arXiv 2410.24164
π0-FAST2025.01토큰 (chunk 를 주파수 변환 후 압축)LLM 이 생성 (autoregressive)arXiv 2501.09747

기준은 내가 만든 코드 다. 각 모델이 내 코드에서 어디를 어떻게 바꾼 것인지로 정리해보자. 내 코드의 핵심은 이 두 군데다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Training
# 내 코드 (= π0 방식) 학습: 정답 action 에 noise 섞고 -> expert 가 속도 예측 -> MSE
x_t = t * noise + (1 - t) * action                    # [B, 16, 2]
u_t = noise - action                                  # 정답 속도
v_t = self.expert(x_t, t, cache, ...)                 # expert 가 예측한 속도
loss = ((v_t - u_t) ** 2 * valid).sum() / ...         # 연속값 회귀 (MSE)


# Inference
# 내 코드 추론: noise 에서 시작해서 Euler 10 step
x_t = torch.randn(B, 16, 2)
for _ in range(10):
    x_t = x_t + dt * self.expert(x_t, t, cache, ...)
    t = t + dt

1. OpenVLA — action 을 “글자” 로


출처: Kim et al., “OpenVLA: An Open-Source Vision-Language-Action Model”, Fig. 2 (arXiv 2406.09246, CC BY 4.0)

  • 7B. Prismatic VLM 기반 (vision: DINOv2 + SigLIP 두 개를 합친 encoder, LLM: Llama 2 7B)
  • Open X-Embodiment 의 로봇 시연 약 97 만 개로 학습했다.
  • action expert 가 없다. LLM 하나가 텍스트처럼 action 을 그대로 생성한다.

핵심 아이디어: action 을 bin 으로 쪼개서 단어로 만든다

  • action 7 차원 (xyz 이동 3 + 회전 3 + gripper 1) 각각을 256 칸 (bin) 으로 나눈다. 칸 범위는 데이터의 1% ~ 99% 분위수 (극단값 무시).
  • 각 칸 번호를 Llama 단어장에서 거의 안 쓰는 단어 256 개에 덮어씌운다. → action 1 개 = 토큰 7 개.
  • 학습은 그냥 LLM 다음 토큰 맞히기 (cross-entropy). VLM 때 캡션 / 답변 학습이랑 완전히 똑같다.

!!!내 코드에서 바뀌는 곳


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# [OpenVLA] expert / flow matching 이 통째로 사라지고, VLM 의 lm_head 가 action 까지 낸다

# 1. 연속 action -> bin 번호 -> 토큰 id  (Dataset 쪽 전처리)
lo, hi = q01, q99                                         # 데이터 분위수 (차원별)
bins = ((action.clamp(lo, hi) - lo) / (hi - lo) * 255).round().long()   # [B, 7]  0~255
action_ids = vocab_size - 256 + bins                       # 단어장 맨 끝 256 개를 action 용으로

# 2. 학습: VLM 때 labels 만든 것처럼 action 토큰에만 loss
input_ids = cat([prompt_ids, action_ids])                  # [이미지 | 지시문 | action 7 토큰]
labels    = cat([-100 * ones_like(prompt_ids), action_ids])
loss = self.lm(inputs_embeds=..., labels=labels).loss      # cross-entropy (MSE 아님!)

# 3. 추론: generate 로 토큰 7 개 생성 -> bin 중앙값으로 되돌림
ids = generate(prefix, max_new_tokens=7)                   # 7 번 forward (KV cache)
action = lo + (ids - (vocab_size - 256)) / 255 * (hi - lo) # [7]  chunk 아님, 딱 1 step
 내 코드 (π0 방식)OpenVLA
action 표현연속값256 bin 토큰 (정밀도에 한계)
lossMSE (속도)cross-entropy (다음 토큰)
한 번 예측chunk 16 개1 step (토큰 7 개)
추론expert 10 번 (cache 고정)LLM 7 번 (토큰마다 1 번)
  • 장점: VLM 구조를 그대로 쓴다. 새로 만들 게 거의 없다.
  • 단점: 256 칸이라 정밀도에 한계. chunk 가 없어서 느리고 (논문 기준 약 6Hz) 동작이 떨린다. LIBERO 평균 약 76.5%.

2. OpenVLA-OFT — 같은 모델, fine-tuning 레시피만 바꿨더니


출처: Kim et al., “Fine-Tuning Vision-Language-Action Models: Optimizing Speed and Success”, Fig. 1 (arXiv 2502.19645, CC BY 4.0)

OpenVLA 를 그대로 두고 fine-tuning 방법만 바꿔서 성능과 속도를 둘 다 크게 올린 논문이다. 바꾼 건 딱 세 가지다.

  1. parallel decoding: 토큰을 하나씩 생성하지 않는다. action 자리에 빈 embedding 을 넣고 양방향 attention 으로 한 번에 다 채운다.
  2. action chunking: 한 번에 여러 step 을 낸다 (우리 chunk 16 과 같은 아이디어).
  3. 연속값 + L1 회귀: 토큰 / 단어장을 버리고, LLM 마지막 hidden 에 MLP head 를 붙여서 숫자를 바로 뽑는다.

!!!내 코드에서 바뀌는 곳


1
2
3
4
5
6
7
8
9
10
11
12
13
14
# [OpenVLA-OFT] noise / t / Euler 가 없다. "빈칸 채우기" 를 한 번에

# 1. action 자리에 빈 embedding 을 chunk 길이만큼 붙임
empty = self.action_query.expand(B, 16 * 7, D)             # 학습되는 placeholder
h = self.lm(inputs_embeds=cat([prefix, empty]),
            attention_mask=bidirectional_on_action_part)    # action 부분은 서로 다 봄 (causal X)

# 2. MLP head 로 바로 연속값
action_pred = self.action_head(h[:, -16 * 7:]).view(B, 16, 7)   # [B, 16, 7]

# 3. loss = L1 (MSE 도 아니고 cross-entropy 도 아님)
loss = (action_pred - action).abs().mean()

# 추론: forward 딱 1 번 -> chunk 전체 (OpenVLA 는 토큰 수만큼, 우리는 expert 10 번)
  • 결과: LIBERO 평균 약 97% (OpenVLA 76.5% → 97%), action 생성 속도도 수십 배 빨라졌다.
  • 내 코드와 비교하면: chunk + 연속값 은 같은데, flow matching (noise → 10 step) 대신 한 방 회귀다. 단점은 시연이 여러 갈래 (왼쪽으로 돌기 / 오른쪽으로 돌기) 일 때 회귀는 평균을 내버릴 수 있다는 것. flow matching 은 갈래 중 하나를 뽑을 수 있다. (PushT 에서 n_action_steps 줄였을 때 흔들렸던 그 “갈래” 이야기)

3. π0 — 내가 만든 구조의 원본


  • PaliGemma 3B (VLM) + 약 300M action expert, layer 별 attention 으로 연결 (내 코드의 A 방식)
  • action 은 flow matching 으로 연속값 chunk (논문은 chunk 50 step, 50Hz 로봇. noise → action 적분은 나랑 똑같이 10 step)
  • 내 코드와 차이는 크기와 데이터뿐이다. 구조 / mask / Euler 10 step / Beta(1.5, 1) 시간 분포까지 같다.
1
2
3
4
5
# [π0] = 내 코드. 크기만 다름
VLM     PaliGemma 3B          # 나: SigLIP + SmolLM2 135M (12 layer)
expert  Gemma 300M            # 나: 21M (12 layer, hidden 384)
chunk   50                    # 나: 16
data    7 종 로봇 + OXE 로 사전학습 후 task 별 fine-tune   # 나: PushT 만

4. π0-FAST — 토큰인데, 똑똑하게 압축한 토큰


출처: Pertsch et al., “FAST: Efficient Action Tokenization for Vision-Language-Action Models”, Fig. 2 (arXiv 2501.09747, CC BY 4.0)

OpenVLA 처럼 토큰으로 가는데, step 마다 bin 으로 쪼개는 게 문제라고 본 논문이다. 로봇 action 은 매 step 이 이전 step 과 거의 비슷해서 (부드러운 곡선), step 별 토큰은 중복 정보투성이고 모델이 “앞 토큰 복사” 만 배운다.

FAST tokenizer: chunk 를 주파수로 바꿔서 압축

  1. action chunk 정규화
  2. 차원마다 DCT (이산 코사인 변환, JPEG 압축이랑 같은 원리) → 부드러운 곡선은 저주파 계수 몇 개로 거의 다 표현됨
  3. 계수를 scale 하고 반올림 (quantize) → 대부분 0
  4. 저주파부터 차원을 섞어서 일렬로 펴고 → BPE 로 압축 → 짧은 토큰열
1
2
3
4
5
6
7
8
9
10
11
12
# [π0-FAST] 토큰화만 바뀌고, 학습 / 추론은 OpenVLA 처럼 LLM next-token

from scipy.fft import dct, idct
chunk = normalize(action)                                  # [16, 7]  (논문은 더 긴 chunk)
coef  = dct(chunk, axis=0, norm="ortho")                   # 시간축 -> 주파수축, 차원마다
q     = (coef * scale).round().astype(int)                 # 고주파는 거의 0
flat  = q.T.flatten()  # (논문은 저주파부터 차원을 섞는 순서)
action_ids = bpe.encode(flat)                              # 0 이 많아서 확 줄어든 토큰열

loss = self.lm(..., labels=[-100 ... | action_ids]).loss   # cross-entropy

# 추론: generate -> bpe.decode -> /scale -> idct -> 역정규화 -> chunk
  • 결과: π0 (flow matching) 와 비슷한 성능을, 학습은 약 5 배 빠르게. 대신 추론은 토큰을 하나씩 생성하니까 π0 보다 느리다.
  • 1M 개 궤적으로 미리 학습시킨 범용 tokenizer FAST+ 도 공개했다.

5. 한 장으로 정리


 OpenVLAOpenVLA-OFTπ0 (= 내 코드)π0-FAST
action토큰 (bin)연속값연속값토큰 (DCT + BPE)
chunkX (1 step)OOO
losscross-entropyL1flow matching MSEcross-entropy
action expertXX (MLP head)OX
추론 forward 수action 차원 수만큼1prefix 1 + expert 10토큰 수만큼
여러 갈래 시연토큰 분포라 OK평균 내버릴 수 있음OK (샘플링)토큰 분포라 OK

결국 축은 두 개다. (1) 토큰 vs 연속값, (2) 한 번에 vs 여러 번. π0.5 는 FAST 토큰과 flow matching 을 둘 다 쓰고, 그 다음 WAM 은 여기에 “미래 영상까지 같이 예측” 을 얹는다. 다음 글에서 이어서.

References