로봇이 넘어지기 전 3초를 어떻게 보존할 것인가: G1 incident trace·ring buffer·replay 계약

G1 v554의 nominal 5초 trace에서 실패는 0.70초에 갑자기 시작되지 않았습니다. 8개 환경의 첫 사건 시각을 평균하면 raw action 이상은 0.00초, arm 범위 이탈은 0.10초, waist 범위 이탈은 0.20초에 나타났습니다. 발 사이 간격이 기준 아래로 내려간 시점은 0.60초, 실제 발 교차는 0.62초였고 첫 done은 0.70초였습니다.

이 순서만으로 raw action이 실패의 원인이라고 단정할 수는 없습니다. 여러 신호가 같은 불안정 상태에 반응했을 수도 있습니다. 그래도 한 가지 결정에는 충분했습니다. 마지막 done row만 보거나 “발이 교차했다”는 결과만 남겼다면 다시 foot reward를 바꿨을 가능성이 큽니다. 그보다 앞선 upper-body action 변화를 함께 보았기 때문에 당시 판단은 reward edit와 PPO 재시작을 막고 action authority 쪽을 먼저 진단하는 것으로 바뀌었습니다.

실물 G1에서도 watchdog이 넘어짐을 감지한 순간만 저장하면 같은 문제가 생깁니다. 실패 원인은 trigger 이전에 시작되고, trigger 이후에는 fallback이 실제로 로봇을 안정시켰는지 확인해야 합니다. 그래서 이번에는 “로그에 무엇을 더 넣을까”보다 먼저 사건 전후의 인과 후보를 잃지 않는 flight recorder를 설계했습니다. 제목의 3초는 완료된 실물 측정값이 아니라 첫 구현에서 사용할 pre-trigger window입니다.

G1 v554 실패 신호의 발생 순서와 실물 로봇용 3초 ring buffer, trigger, post-window, incident bundle 저장 계약을 보여주는 다이어그램
v554에서는 done보다 0.7초 앞서 raw action 이상이 보였고, arm·waist·발 간격 변화가 뒤따랐습니다. 목표 구조는 최근 3초를 메모리에 순환 보관한 뒤 guard나 rollback trigger가 발생하면 post-window까지 포함해 identity와 함께 동결하는 것입니다. 아래 구조는 설계안이며 현재 live G1 recorder가 구현됐다는 뜻은 아닙니다. 그림을 누르면 원본 크기로 볼 수 있습니다.

마지막 한 프레임은 결과를 보여 주지만 경로를 보여 주지 않습니다

현재 G1 환경은 Isaac Lab의 auto-reset 문제를 이미 한 번 겪었습니다. env.step()에서 done이 발생하면 해당 환경이 즉시 reset되기 때문에, step 반환 뒤 robot state를 읽으면 넘어지기 직전 자세가 아니라 초기 자세를 읽을 수 있습니다. 이를 막기 위해 g1_walk_env.py는 reset 전에 root pose·quaternion·linear/angular velocity뿐 아니라 joint position·velocity, applied torque, foot contact와 sole sample을 _last_termination_*에 복사합니다.

debug_g1_motion_trace.py도 terminated row에서는 이 snapshot을 다시 끼워 넣습니다. 이것은 termination·timeout·reset을 분리한 글에서 다룬 first-done 보존의 실제 구현입니다. 덕분에 terminal row가 reset 자세와 섞이는 문제는 상당 부분 막았습니다.

그러나 terminal snapshot은 한 프레임입니다. v554에서 판단을 바꾼 정보는 0.00→0.10→0.20→0.60→0.62→0.70초의 순서였습니다. trigger 시점의 root·joint·torque가 정확해도 이전 action과 support geometry가 없다면 “왜 이 자세에 도착했는가”는 복원되지 않습니다. terminal snapshot과 incident history는 서로 대체 관계가 아니라 함께 필요한 두 증거입니다.

현재 simulation trace는 풍부하지만 live incident recorder는 아닙니다

v554 trace 자체는 상당히 상세합니다. 50Hz에서 5초 동안 8개 환경을 기록해 2,000 row가 생성됐고, nominal trace header에는 981개 column이 있습니다. root·CoM·관절·접촉·reward뿐 아니라 actor raw action, composed action, 실제 적용 action을 관절별로 분리해 남깁니다. nominal run에서는 48개 done row가 기록됐습니다. auto-reset 뒤 같은 env_id가 다시 실행되므로 이는 8개 로봇의 마지막 상태 8개가 아니라 5초 동안 반복된 여러 종료 사건입니다.

여기서 운영용 incident artifact로 쓰기 어려운 경계가 보입니다.

  • time_sstep * policy_dt인 simulation 진행 시간이며 sensor capture나 wall-clock timestamp가 아닙니다.
  • env_idepisode_length는 있지만 명시적인 episode_id나 incident ID가 없습니다.
  • v554 CSV 안에는 checkpoint, seed, scenario, schema version, source revision이 없습니다. 파일명과 별도 report에 의존합니다.
  • 현재 debug script는 policy_obs_00부터 policy_obs_72까지 쓰도록 되어 있지만, 앞서 생성된 v554 CSV에는 이 column들이 없습니다. trace만 보고 어느 schema와 코드 revision에서 생성됐는지 확정할 수 없습니다.
  • 정해진 5초 전체를 디스크에 쓰는 진단 실행이므로, 오랫동안 켜진 실물 로봇에서 최근 몇 초만 보관하는 bounded recorder가 아닙니다.

이 지적은 v554 trace가 쓸모없다는 뜻이 아닙니다. 오히려 이 파일이 있었기 때문에 failure timing을 비교할 수 있었습니다. 다만 “분석에 유용한 CSV”와 “사건을 독립적으로 증명하고 재생할 수 있는 artifact” 사이에는 identity·clock·lifecycle 계약이 더 필요합니다.

eval 결과와 router 로그도 서로 다른 질문에 답합니다

eval_rl.py는 고정 rollout 동안 position, torque, joint velocity, observation, contact, raw/composed/applied action을 메모리에 모은 뒤 scenario별 metric을 계산합니다. 출력 JSON은 task·checkpoint·seed·agent config와 scenario summary를 남기고 CSV는 scenario당 한 row를 씁니다. “이 checkpoint가 acceptance gate를 통과했는가”에는 맞지만, 특정 done의 직전 150 frame을 꺼내는 저장 구조는 아닙니다.

router 쪽은 반대입니다. Runner._save_log()가 매 backend 응답을 즉시 JSON으로 저장하지만 내용은 BackendResponse.to_dict()뿐입니다. backend name, action type, action, status, log는 남지만 그 응답을 만든 request의 observation과 state는 저장하지 않습니다. 파일명에는 time.time()에서 만든 밀리초가 들어가지만 JSON 본문에는 source timestamp, receive timestamp, sequence ID, control tick이 없습니다. 20ms latency 글에서 확인한 시간 계보 공백이 incident 분석에서는 입력과 출력을 다시 연결할 수 없는 문제로 나타납니다.

현재 경로 잘 답하는 질문 incident 분석에서 빠진 것
debug trace CSV 고정 simulation run에서 어떤 신호가 어떤 순서로 변했는가 live bounded window, clock 계보, self-contained identity
eval JSON/CSV scenario와 checkpoint가 gate를 통과했는가 개별 사건의 전후 frame과 trigger lifecycle
router JSON backend가 어떤 action/status를 반환했는가 같은 tick의 request·sensor·authority·actuator ack
M31 snapshot/replay 특정 simulation search를 같은 state에서 재현할 수 있는가 일반 G1 runtime과 hardware channel 연결

네 경로를 하나로 합치자는 뜻은 아닙니다. 각자의 목적을 유지하되, incident ID와 시간·artifact identity를 공유해야 합니다. evaluation summary는 incident bundle을 가리키고, router log는 같은 sequence의 measurement와 actuator feedback을 가리켜야 합니다.

ring buffer는 최근 기록을 덮어쓰다가 trigger에서 멈춥니다

ring buffer는 크기가 고정된 순환 메모리입니다. 새 frame이 들어오면 가장 오래된 frame을 덮어씁니다. 정상 운전이 계속되는 동안 디스크를 무한히 채우지 않으면서도 항상 “방금 전 몇 초”를 가지고 있다는 것이 핵심입니다.

첫 설계에서 control channel은 3초를 보존합니다. 현재 G1 policy period가 20ms이므로 stream 하나당 150 control frame입니다.

50 Hz * 3 s = 150 control frames

buffer before trigger t0 : [t0 - 3.0 s, t0]
post-trigger capture     : [t0, t0 + post_window]
frozen incident          : pre-window + trigger + post-window

3초가 최적이라는 실험 결과는 아직 없습니다. v554의 첫 done이 평균 0.70초였으므로 그 사건의 초기 action 변화까지 충분히 포함하면서, 첫 구현의 메모리 계산이 단순한 값으로 골랐습니다. 실제 IMU·encoder는 더 높은 주기로 들어올 수 있고 RGB·depth는 frame당 byte가 큽니다. 따라서 buffer는 시간과 byte 두 한계를 동시에 가져야 합니다. image channel은 낮은 frame rate나 별도 압축 queue를 쓸 수도 있지만, 구체적인 rate와 codec은 실제 bandwidth 측정 뒤에 정해야 합니다.

ROS 2의 공식 rosbag2에도 같은 목적의 snapshot mode가 있습니다. 메시지를 도착 즉시 디스크에 쓰지 않고 시간 또는 byte로 제한된 in-memory circular buffer에 보관하다가 service가 호출되면 전체 buffer를 저장합니다. double buffering 때문에 최악의 메모리 사용량이 설정한 byte 한계의 약 두 배까지 갈 수 있다는 설명도 있습니다. 공식 ros2_tracing도 오래된 memory buffer를 버리며 rolling history를 유지하는 snapshot 방식을 flight recorder mode로 설명합니다. 이 프로젝트가 현재 두 recorder를 사용한다는 뜻은 아니지만, time bound와 size bound를 함께 명시해야 한다는 직접적인 설계 근거가 됩니다.

trigger와 안전 반응은 같은 사건이지만 같은 작업은 아닙니다

shadow·canary·rollback 글에서 정의한 guard는 authority를 회수합니다. incident recorder의 trigger는 증거를 동결합니다. 둘은 같은 조건에서 발생할 수 있지만 안전 반응이 로그 저장을 기다리면 안 됩니다.

guard violation at t0
  1. authority -> safe controller / inactive   # real-time path
  2. incident_id allocated                     # bounded metadata
  3. pre-window frozen                         # recorder path
  4. passive post-window continues
  5. bundle committed and verified

첫 trigger 집합은 stale sensor, deadline miss, non-finite value, command envelope 위반, base height·tilt·lateral velocity, controller ownership 충돌, communication lease 만료, human stop입니다. 하나의 사건에서 여러 조건이 동시에 참이 될 수 있으므로 trigger_reason 하나만 고르지 않고 최초 이유와 동시 guard bitset을 모두 남겨야 합니다.

post-window는 fallback 이후를 보기 위해 필요합니다. authority를 0으로 내렸는데 q·dq가 계속 발산했는지, safe controller가 명령을 냈지만 actuator feedback이 따라오지 않았는지, 통신이 끊겨 ack 자체가 없었는지를 구분합니다. recorder는 관측을 조금 더 보존할 뿐이며 위험한 candidate command를 계속 publish해서는 안 됩니다.

한 frame에는 값보다 시간 출처가 먼저 붙어야 합니다

incident bundle에서 모든 channel을 같은 배열 index로 맞추는 것은 현실적이지 않습니다. IMU, encoder, camera, policy, actuator feedback의 주기가 다르기 때문입니다. 각 record가 자신의 capture time과 sequence를 가지고, control tick이 어떤 record들을 소비했는지 참조하는 편이 낫습니다.

source_clock_id
source_sequence_id
capture_timestamp_ns
receive_timestamp_ns
estimate_timestamp_ns
control_tick_id
command_send_timestamp_ns
actuator_ack_timestamp_ns
clock_sync_status / estimated_offset_ns

여기서 wall clock은 파일명과 사람의 사건 시각을 맞추는 데 쓰고, monotonic clock은 지연과 순서를 계산하는 데 씁니다. robot tick과 host monotonic time 사이의 offset을 추정했다면 그 값과 유효 상태도 artifact에 남깁니다. clock이 동기화되지 않았는데 timestamp 숫자만 정밀하게 쓰는 것은 오히려 잘못된 인과 순서를 만들 수 있습니다.

incident bundle은 데이터뿐 아니라 당시 실행 identity를 동결합니다

v554 CSV가 현재 script와 schema가 달라졌다는 사실은 manifest가 필요한 이유를 잘 보여 줍니다. “어떤 값이 기록됐는가”만큼 “어떤 코드와 설정이 그 값을 만들었는가”가 중요합니다.

incident_id / created_at / trigger_time
robot profile + hardware revision
checkpoint path + content sha256
normalizer + joint order + action scale
router / adapter source revision
baseline and candidate identities
deployment mode + authority alpha + command envelope
channel schema versions + units + frames
first/last timestamps + row counts + gap counters
file names + byte lengths + sha256
trigger reasons + rollback transition result

저장소의 M31 trajectory-search 경로에는 이 방향의 강한 선례가 있습니다. 전용 trace contract는 field schema와 dtype·shape를 고정하고 snapshot과 trace의 SHA-256을 검사합니다. 기존 trace 파일을 덮어쓰지 않으며, root·joint·action·episode state를 복원한 뒤 source trace와 clone trace의 최대 오차가 1e-6 이하인지 확인하는 코드도 있습니다. 현재 확인한 것은 이 특수 경로의 구현이며 live G1 incident replay 실행이 아닙니다. 그래도 no-overwrite·hash·state restore·parity gate는 범용 recorder로 가져올 가치가 있습니다.

message container는 처음부터 독자 포맷을 만들기보다 MCAP을 후보로 둘 수 있습니다. MCAP 공식 명세는 timestamped pub/sub channel, schema, chunk, index, metadata, attachment를 한 파일에 넣는 구조를 정의합니다. chunk와 data section의 CRC는 선택적으로 사용할 수 있습니다. 따라서 “MCAP으로 저장했다”가 무결성을 자동 보장하는 것은 아니며, 실제 writer 설정과 incident manifest의 전체 파일 hash를 따로 검증해야 합니다.

replay라는 단어도 세 단계로 나눠야 합니다

incident를 저장한 뒤 무엇을 재생할지도 구분해야 합니다.

replay 단계 질문 허용 범위
offline policy parity 기록된 observation을 같은 policy에 넣으면 같은 raw action이 나오는가 모터 연결 없이 가장 먼저 수행
simulator state replay state·controller memory·RNG를 복원하면 같은 failure signature가 재현되는가 모델 차이를 명시하고 진단용으로 수행
hardware command replay 기록된 actuator command를 실물에 다시 보내도 되는가 기본 금지. 별도 안전 승인 없이는 실행하지 않음

offline parity는 가장 값싸고 명확합니다. 기록된 73차원 actor observation, normalizer state, recurrent hidden state가 있다면 policy output을 다시 계산해 recorded raw action과 비교합니다. 값이 다르면 checkpoint·normalizer·dtype·device·코드 revision 중 하나가 맞지 않는 것입니다.

simulator replay는 더 어렵습니다. joint state 하나만 되돌리면 contact solver state, command manager, gait phase, previous action, recurrent memory, random state가 달라질 수 있습니다. M31 snapshot이 action manager의 current/previous action과 episode length까지 보존하는 이유입니다. 그래도 simulator가 실물 접촉과 actuator를 완전히 재현한다는 뜻은 아닙니다. 같은 신호 순서를 재현하면 원인 가설을 좁힐 수 있지만, 재현하지 못했다고 실물 incident가 거짓이 되는 것도 아닙니다.

실물 command replay는 진단 편의를 위해 자동화하면 안 됩니다. 실패를 만든 명령을 그대로 다시 보내는 작업이기 때문입니다. hardware에서는 recorded measurement를 policy에 공급하는 read-only shadow replay까지만 기본 경로로 두고, actuator publisher는 구조적으로 차단합니다.

첫 acceptance test는 낙상을 기다리지 않고 recorder를 고장 내는 시험입니다

실제 사고가 일어난 뒤 처음 recorder를 검사해서는 늦습니다. 다음 순서로 synthetic trigger를 넣어 저장 계약부터 검증할 계획입니다.

  1. 정상 window: 50Hz control frame 150개가 정확히 최근 3초를 유지하고 오래된 frame을 덮어쓰는지 확인합니다.
  2. sequence gap: sensor frame 하나를 의도적으로 누락해 manifest의 gap counter와 affected control tick이 일치하는지 봅니다.
  3. clock disorder: 같은 timestamp 또는 역순 timestamp를 넣어 artifact가 정상으로 통과하지 않는지 검사합니다.
  4. synthetic guard: 지정 tick에서 trigger를 발생시켜 pre-window가 사라지지 않고 post-window 뒤 한 번만 commit되는지 확인합니다.
  5. crash during commit: 임시 파일 쓰기 중 프로세스를 중단해 부분 artifact가 정상 incident로 보이지 않는지 확인합니다.
  6. offline parity: 저장 observation으로 actor를 다시 실행해 raw action과 허용 오차 안에서 일치하는지 확인합니다.
  7. rollback drill 연결: 같은 incident ID 안에서 authority transition과 fallback 이후 state가 이어지는지 검증합니다.
gate 최소 acceptance 현재 상태
pre-window completeness 시간·byte 한계 안에서 요구된 최근 frame과 gap 수가 설명됨 설계
clock coherence 각 source clock과 control tick 연결, 역순·중복 검출 미구현
identity integrity checkpoint·config·schema·source revision과 파일 hash 고정 M31 특수 경로에 일부 구현
trigger lifecycle authority 회수와 분리된 non-blocking freeze, 중복 trigger 병합 미구현
offline parity 동일 입력에서 recorded actor output 재계산 live G1 경로 미실행

실물 channel이 아직 없으므로 첫 실행 대상은 simulator와 router shadow runner입니다. 학습 episode의 시간 정렬을 다룬 글과 같은 원칙으로 data/의 실제 incident payload는 git에 넣지 않고, 600_Data_Repository에는 incident ID, URI, hash, schema, source revision만 등록해야 합니다. camera가 포함되면 작업 공간과 사람 영상이 들어갈 수 있으므로 보존 기간과 접근 권한도 manifest 바깥의 운영 정책으로 정해야 합니다.

현재는 실패를 분석할 수 있지만 실패를 독립적으로 재현할 수는 없습니다

v554 trace는 마지막 done row보다 앞선 0.7초가 왜 필요한지 보여 줬습니다. rich CSV, terminal snapshot, evaluation summary, router response log, M31 replay contract는 각각 유용합니다. 하지만 live G1의 한 사건을 같은 ID·clock·schema·deployment identity로 묶는 recorder는 아직 없습니다.

그래서 다음 구현의 성공 기준은 로그 column 수가 아닙니다. trigger 이전 3초와 이후 상태가 빠짐없이 묶이고, 어떤 policy와 controller가 어떤 sensor frame을 보고 무엇을 명령했는지 복원되며, 저장된 observation으로 actor output을 다시 계산할 수 있어야 합니다. 넘어지는 순간을 보존하는 것보다 어려운 일은 넘어지기 시작한 순간을 잃지 않는 것입니다.

댓글 달기

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

위로 스크롤