Isaac Lab은 왜 로봇 4,096대를 동시에 돌리는가: physics step·decimation·PPO rollout

G1 motion-tracking 실험 M66은 4,096개 환경에서 PPO 100 update를 실행했습니다. 처리한 transition은 9,830,400개였고 wall-clock time은 5분 32초였습니다. 숫자만 보면 로봇 한 대가 980만 번 차례로 움직인 것처럼 보이지만, 실제 실행 구조는 정반대에 가깝습니다. 4,096대의 상태를 하나의 tensor로 묶어 같은 policy가 동시에 action을 만들고, 물리 시뮬레이터도 복제된 환경을 batch로 전진시킵니다. M66의 motion-tracking 관측 계약은 현재 73차원 flat walk와 다르지만, `num_envs × num_steps_per_env`로 rollout을 수집하는 기반 구조는 같습니다.

그렇다고 “환경 수를 늘리면 학습이 4,096배 빨라진다”라고 말할 수는 없습니다. 한 action 안에서도 PhysX가 네 번 움직이고, 24번의 policy step이 모여야 PPO update가 한 번 시작됩니다. GPU 처리량, 환경별 simulated time, 연구자가 기다리는 wall time은 서로 다른 시계입니다.

이 글은 PPO의 clipping과 advantage를 다시 설명하지 않습니다. 현재 G1 설정의 5ms physics step, decimation 4, 4,096개 vectorized environment, 24-step rollout이 어떤 순서로 연결되는지를 한 시간축으로 정리합니다. 그 시간축을 따라가다 발견한 현재 코드의 두 가지 단위 불일치도 함께 남깁니다.

G1 Isaac Lab 환경에서 하나의 29차원 action이 네 번의 5밀리초 물리 step에 적용되고 4096개 환경의 24 step이 98304개 PPO transition을 만드는 시간축 다이어그램
현재 G1의 한 policy step은 20ms이며 그 안에 5ms PhysX step 네 번이 들어갑니다. 4,096개 환경에서 24 step을 모으면 update 한 번의 rollout은 98,304 transition이 됩니다. 아래쪽은 같은 시간 단위를 사용해야 할 현재 코드의 일치·결합·불일치 지점을 구분합니다.

먼저 네 개의 시계를 분리해야 합니다

학습 로그의 `step`이라는 단어는 문맥에 따라 다른 시간을 가리킵니다. 이 구분을 하지 않으면 200Hz physics와 50Hz policy를 같은 loop로 이해하거나, 9.83M transition을 9.83M초의 경험으로 잘못 읽게 됩니다.

시간축 현재 G1의 단위 답하는 질문
physics time 5ms, 200Hz 관절·접촉·마찰을 얼마나 잘게 적분하는가
policy / environment time 20ms, 50Hz 정책이 언제 새 action을 내고 reward·observation을 받는가
rollout time 환경당 24 step, 0.48초 PPO update 전까지 각 환경에서 얼마를 수집하는가
wall time GPU와 scene에 따라 달라짐 사람이 실제로 얼마나 기다렸는가

physics time과 policy time은 시뮬레이션 안의 시간입니다. wall time은 Windows와 GPU가 계산을 끝내는 데 걸린 현실 시간입니다. headless 실행이 빨라져도 physics dt가 바뀐 것은 아니고, 환경 수를 늘려 초당 transition이 많아져도 로봇 한 대의 0.48초 rollout이 더 긴 경험으로 변하지는 않습니다.

G1의 물리 세계는 5ms마다 한 번씩 전진합니다

`G1WalkEnvCfg`의 `SimulationCfg`는 `dt=1/200`으로 설정돼 있습니다. 따라서 PhysX의 한 step은 0.005초, 주파수로는 200Hz입니다.

sim = SimulationCfg(
    dt=1 / 200,
    render_interval=4,
)

decimation = 4

physics dt는 policy network를 얼마나 자주 실행할지 정하는 값이 아닙니다. 관절 drive, 링크 관성, 발 충돌과 마찰을 다음 상태로 적분하는 간격입니다. 간격이 지나치게 크면 빠른 접촉 변화와 stiff actuator를 거칠게 계산할 수 있고, 작게 만들면 같은 simulated second를 얻기 위해 더 많은 물리 계산이 필요합니다.

현재 G1의 20초 episode를 물리 기준으로 보면 최대 4,000 physics step입니다. 그러나 PPO가 4,000개의 새 action을 내는 것은 아닙니다. 그 사이에 decimation이라는 두 번째 주기가 들어갑니다.

decimation 4는 하나의 action을 네 물리 step 동안 유지합니다

Isaac Lab의 `DirectRLEnv.step()`은 policy action을 받은 뒤 `_pre_physics_step()`을 한 번 호출합니다. 그 다음 `decimation`만큼 loop를 돌면서 `_apply_action()`, simulator step, scene buffer update를 반복합니다. loop가 끝나야 reward와 done을 계산하고 종료된 환경을 reset한 뒤 observation을 만듭니다. 이 순서는 DirectRLEnv 공식 소스direct workflow 튜토리얼에 명시돼 있습니다.

현재 G1에서는 다음처럼 진행됩니다.

t = 0 ms   policy → 29D action, q_target 계산
t = 5 ms   같은 q_target으로 physics step 1
t = 10 ms  같은 q_target으로 physics step 2
t = 15 ms  같은 q_target으로 physics step 3
t = 20 ms  같은 q_target으로 physics step 4
            done → reward → reset → 73D observation

정책 주기는 `physics_dt × decimation`, 즉 `0.005 × 4 = 0.020초`입니다. policy는 50Hz로 새 action을 내고, implicit actuator는 그 사이 네 번의 물리 적분에서 같은 position target을 따라갑니다. 이 action이 nominal joint pose와 어떻게 합쳐지고 torque로 이어지는지는 position target·PD actuator 글이 소유하는 범위입니다.

reward와 observation은 물리 step마다 새로 계산되지 않습니다

현재 `G1WalkEnv`의 `_apply_action()`은 robot data를 simulator에 쓰고, Isaac Lab base class가 이를 decimation loop 안에서 네 번 호출합니다. 반면 `_get_rewards()`, `_get_dones()`, `_get_observations()`는 네 번의 물리 step 뒤 environment step 경계에서 호출됩니다.

이 차이는 미분과 누적값에서 바로 드러납니다. 발 체공시간은 reward가 호출되는 주기에 맞춰 한 번에 0.02초씩 증가해야 합니다. 현재 코드는 이 부분을 다음처럼 계산합니다.

policy_dt = cfg.sim.dt * cfg.decimation
self._feet_air_time += policy_dt

0.005초만 더하면 실제 20ms가 지나갔는데 5ms만 기록하게 되고, 네 번 더하면 reward 함수가 한 policy step에 한 번만 실행되므로 구현 순서와 맞지 않습니다. 시간 기반 reward와 termination을 작성할 때 “이 함수가 어느 clock에서 호출되는가”를 먼저 확인해야 하는 이유입니다.

4,096개 환경은 4,096개의 Python process가 아닙니다

`InteractiveSceneCfg`는 `num_envs=4096`, `env_spacing=2.5`, `replicate_physics=True`로 설정돼 있습니다. `G1WalkEnv._setup_scene()`은 기준 환경을 만들고 `clone_environments()`로 복제합니다. 각 로봇은 scene 안에서 다른 origin을 갖지만 Python에서 환경 객체 4,096개를 순서대로 호출하는 구조는 아닙니다.

상태는 첫 번째 축이 환경인 tensor로 저장됩니다.

commands     : [4096, 3]
observations : [4096, 73]
actions      : [4096, 29]
rewards      : [4096]
dones        : [4096]

actor는 `[4096, 73]` 관측 batch를 받아 한 번의 batched forward에서 `[4096, 29]` action을 만듭니다. 환경 코드도 `torch.sum(…, dim=-1)`처럼 같은 연산을 환경 축 전체에 적용합니다. 환경마다 서로 다른 command, pose, contact와 reset 상태를 가지면서 계산 형태는 같은 이유입니다.

이 구조가 병렬 수집의 핵심입니다. 다만 모든 operation이 한 GPU kernel에 들어간다거나 4,096배의 선형 speedup을 보장한다는 뜻은 아닙니다. articulation과 contact가 복잡해질수록 물리 비용이 커지고, VRAM과 tensor 이동, PPO update 시간도 함께 늘어납니다.

한 번의 PPO update는 98,304개 transition을 받습니다

G1의 RSL-RL 설정은 `num_steps_per_env=24`입니다. RSL-RL `OnPolicyRunner`는 이 횟수만큼 `env.step(action)`을 반복해 rollout을 모은 뒤 return을 계산하고 PPO update를 수행합니다.

현재 기본값의 산술은 다음과 같습니다.

policy step          = 0.005 s × 4 = 0.020 s
rollout per env      = 24 × 0.020 s = 0.48 s
transitions/update   = 4,096 × 24 = 98,304
physics substeps/env = 24 × 4 = 96

즉 update 한 번이 받는 경험은 환경별로는 0.48초짜리 조각이지만, 전체 batch에는 98,304개의 state-action-result 묶음이 있습니다. PPO 소개글에서 말한 advantage, value target, clipping은 바로 이 rollout이 만들어진 뒤의 일입니다.

0.48초 rollout이 20초 episode를 잘라 없애는 것은 아닙니다

`episode_length_s=20.0`이고 policy dt가 0.02초라면 한 episode의 최대 길이는 1,000 policy step입니다. 한 rollout의 24 step보다 훨씬 깁니다. 그렇다고 24 step이 끝날 때 모든 환경을 reset하지는 않습니다.

RSL-RL이 update를 수행한 뒤 다음 rollout은 각 환경의 이어지는 상태에서 시작합니다. 중간에 넘어진 환경만 done 신호에 따라 개별 reset됩니다. rollout 끝에서 critic의 value로 bootstrap하고 GAE를 계산하기 때문에, PPO update 경계와 episode 경계는 같은 선이 아닙니다.

이 차이는 낙상처럼 늦게 나타나는 결과를 해석할 때 필요합니다. 24-step rollout은 한 번의 update가 직접 담는 연속 구간이 0.48초라는 뜻이지, policy가 0.48초 이후의 결과를 전혀 배우지 못한다는 뜻은 아닙니다. 다만 value estimation과 여러 rollout에 걸친 credit assignment가 더 큰 역할을 하므로, 환경별 rollout을 지나치게 짧게 만들면 긴 시간의 원인과 결과를 연결하기 어려워질 수 있습니다.

M66의 9.83M transition은 처리량 증거이지 보행 합격표가 아닙니다

M66의 실제 실행값은 계산과 정확히 맞습니다.

4,096 env × 24 step × 100 update
= 9,830,400 transitions

해당 block은 5분 32초에 실행됐습니다. 이 기록은 vector environment와 PPO runner가 실제로 대규모 rollout을 처리했다는 증거입니다. 그러나 980만 transition이 서로 완전히 독립적이었다는 증거도, 그 policy가 자연스럽게 걸었다는 증거도 아닙니다. M66은 deterministic evaluation에서 별도의 구조·행동 gate를 통과해야 했고, 한 update canary는 wiring 검증으로만 분류됐습니다.

같은 이유로 `steps/s`가 높아졌다는 사실만 보고 checkpoint를 채택하지 않습니다. reset distribution이 좁거나 모든 환경이 같은 reward shortcut에 갇히면 큰 batch도 비슷한 실패를 반복할 수 있습니다. reward hacking 글에서 다룬 것처럼, transition 수는 환경이 어떤 경험을 만들었는지 대신 말해 주지 않습니다.

환경 수를 늘리면 바뀌는 것과 바뀌지 않는 것

4,096개 환경은 update 한 번의 batch를 크게 만들고 GPU의 병렬 계산을 활용합니다. 같은 wall time에 더 많은 command와 초기 상태를 볼 수 있다면 on-policy PPO가 새 policy로 다음 rollout을 얻는 속도도 빨라집니다. 그러나 다음 항목은 별도로 확인해야 합니다.

  • 처리량: rollout 수집 시간과 PPO update 시간을 따로 기록해야 합니다.
  • 메모리: observation, action, critic state, rollout storage와 physics buffer가 환경 수에 따라 커집니다.
  • 경험 다양성: seed 하나와 같은 command 분포를 쓴다고 sample이 자동으로 독립적이지는 않습니다.
  • episode 구성: 무작위 episode length 초기화와 asynchronous reset이 rollout의 phase 편향을 줄이는지 봅니다.
  • 실패 비율: 환경 수가 커도 특정 termination이 batch 대부분을 차지하면 update가 그 실패에 지배됩니다.

`training/rl/train.py`는 기본 4,096개를 사용하지만 CLI의 `–num_envs`가 scene config를 덮어씁니다. 그래서 64-env deterministic evaluation과 4,096-env learning block을 같은 종류의 실행으로 취급하지 않습니다. 앞의 것은 반복 가능한 행동 평가에, 뒤의 것은 대규모 on-policy 수집에 맞춘 조건입니다.

render interval은 policy dt와 같은 개념이 아닙니다

현재 설정에서는 `render_interval=4`와 `decimation=4`가 같아서 화면 갱신 주기와 environment step이 모두 20ms입니다. 하지만 두 값의 책임은 다릅니다. decimation은 action과 environment step의 주기를 정하고, render interval은 GUI 또는 RTX sensor가 simulation frame을 언제 그릴지 정합니다.

현재 `g1_walk_env.py`의 command ramp, gait phase, 일부 time window와 timeout 계산은 다음 형태를 사용합니다.

policy_dt = cfg.sim.dt * cfg.sim.render_interval

지금은 `0.005 × 4`라서 결과가 0.02초로 맞습니다. 그러나 카메라 주기나 GUI 성능 때문에 render interval만 2 또는 8로 바꾸면 policy가 여전히 50Hz인데 gait phase와 termination 시간은 두 배 빠르거나 느리게 흐르게 됩니다. 이것은 현재 rollout에서 수치 오류가 관찰됐다는 주장이 아니라, 두 설정값이 우연히 같아 잠복한 결합입니다.

Isaac Lab은 이미 `physics_dt`와 `step_dt` property를 구분합니다. 시간 기반 task logic은 `self.step_dt`, episode 길이는 base class의 `max_episode_length`를 기준으로 삼는 편이 의미를 보존합니다.

관절 가속도 계산에는 실제로 네 배의 시간 불일치가 있습니다

더 직접적인 검토 지점은 joint acceleration입니다. `_prev_joint_vel`은 `_get_rewards()` 끝에서 저장되고, 다음 `_get_rewards()`에서 현재 joint velocity와 비교됩니다. 공식 step 순서에 따르면 두 sample 사이에는 physics step 하나가 아니라 policy step 하나, 즉 0.02초가 흐릅니다.

하지만 현재 계산은 다음과 같습니다.

joint_accel = (joint_vel - prev_joint_vel) / cfg.sim.dt

분모가 0.005초이므로 같은 속도 변화에 대해 `Δv / 0.02`보다 가속도가 네 배 크게 계산됩니다. cost가 acceleration 제곱합이면 clip 이전 값은 16배가 됩니다.

여기서 상태를 과장하면 안 됩니다. 현재 기본 reward profile인 `compact_v344_inertia_wbo_swing_lane`은 `rew_accel`을 최종 compact reward에 포함하지 않습니다. 따라서 이 불일치가 현재 기본 profile의 보행 실패 원인이라고 말할 증거는 없습니다. 다만 값은 매 reward call에 계산되고 일반 reward 경로에는 포함될 수 있어, profile을 바꾸거나 telemetry를 해석할 때 잘못된 단위가 다시 활성화될 수 있습니다.

시간축의 현재 상태를 구현·실행·채택으로 나누면

단계 현재 상태 근거
설계됨 200Hz physics, 50Hz policy, 4,096-env rollout G1 environment·scene·agent config
구현됨 DirectRLEnv decimation과 RSL-RL vector wrapper 연결 `g1_walk_env.py`, `train.py`
실행됨 M66 4,096-env, 100-update block 9.83M transition, 5분 32초 기록
관측됨 대규모 rollout 처리와 checkpoint 생성 runner log와 experiment report
채택됨 시간 계약은 아직 보류 render coupling과 acceleration 단위 불일치
배포됨 없음 G1 target runtime에 승인된 동일 계약 없음

vectorized PPO 경로가 실행됐다는 사실과 시간 단위가 모든 reward·termination에서 일관되다는 결론은 다른 상태입니다. 이 글을 쓰면서 확인한 것은 전자이고, 후자는 추가 검사가 필요합니다.

다음 실험은 환경 수를 늘리는 일이 아니라 clock을 고정하는 일입니다

다음 수정에서는 먼저 수동으로 만든 `sim.dt × …` 표현을 의미 단위로 바꿀 계획입니다.

  1. physics 계산에는 `self.physics_dt`, policy·reward·phase에는 `self.step_dt`를 사용합니다.
  2. timeout은 수동 division 대신 `max_episode_length`와 같은 base contract에 맞춥니다.
  3. joint velocity sample 주기를 확인해 acceleration 분모를 `step_dt`로 고치거나, physics-step acceleration이 필요하면 physics loop에서 별도 측정합니다.
  4. decimation을 4에서 2로 바꾸고 render interval은 4로 유지하는 단위 검사를 만들어 gait phase·air time·termination의 실제 초가 보존되는지 확인합니다.
  5. 512, 1,024, 2,048, 4,096 환경에서 rollout collection time, PPO update time, peak VRAM, transition/s를 분리해 기록합니다.
  6. 같은 benchmark에서 command·reset·termination 분포까지 비교해 큰 batch가 같은 실패만 복제하지 않는지 검사합니다.

4,096개 환경의 의미는 로봇을 많이 띄웠다는 데 있지 않습니다. 20ms마다 정책이 만드는 4,096개의 서로 다른 결과를 같은 update에 모을 수 있다는 데 있습니다. 그 장점을 제대로 쓰려면 5ms physics clock, 20ms policy clock, 0.48초 rollout, 현실의 wall time을 끝까지 구분해야 합니다. 지금 남은 일은 더 큰 숫자를 만드는 것이 아니라, 코드 전체가 어느 시계를 읽는지 검증하는 것입니다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤