← 목록으로
2026-05-19 · memer · engineering

병렬 Init-State Oracle Driver — Preempt-Resilient, NAS Checkpoint

6h44m 손실 사고 후 hardening · commit 88e3a32

TL;DR

~15min
W=28 외삽
5-6h
순차 (이전)
10-20x
속도 향상
5/5
Unit tests

1 배경/목적 (왜)

기존 scripts/init_state_oracle.py:build_manifest는 858 scenario를 단일 코어로 순차 처리(~25s/scenario → 5-6h)하며, 진행 로그 없이 마지막에 단 한 번만 매니페스트를 기록했다. 이 작업은 preemptible --qos=extra로 실행되는데, 이미 6h44m짜리 sequential job이 preempt로 전량 손실되는 사고가 발생했다.

현재 또 다른 sequential job이 돌고 있어, 그 job이 다시 죽을 경우를 대비한 hardened fallback driver가 필요했다. 이번 작업은 build + unit-test + commit만 — SLURM 제출은 orchestrator가 기존 job 사망 시에만 수행.

사고 원인: git binary shellout(_rc365_commit)이 worker pod에 없어 6h44m을 날린 별도 사고도 있었다. 신규 드라이버는 NAS file IO 전용으로 git shellout을 완전 제거.

2 작업 내용 (어떻게)

spawn 멀티프로세싱 + maxtasksperchild=1

fork는 MuJoCo/robosuite를 deadlock시킴(공식적으로 알려진 위험) → mp.get_context("spawn") 강제. robosuite/MuJoCo는 한 프로세스 내 env 간 전역 상태가 누수되므로 scenario마다 새 프로세스(maxtasksperchild=1). 재import 비용(~수백 ms)은 env build(~25s) 대비 무시 가능.

NAS-persistent append-only 체크포인트

시나리오 결과를 계산 즉시 ./tmp/oracle_shards/results.jsonl에 한 줄씩 append + flush() + os.fsync()(Manager.Lock 보호). kill 시 in-flight 시나리오만 잃고 완료분은 절대 안 잃음. torn 마지막 줄은 {로 시작 }로 끝나는지 구조 검사로 식별.

Resume (멱등)

시작 시 results*.jsonl 전체를 읽어 완료 시나리오 set을 만들고 skip. preempt→resubmit이 재시작이 아닌 이어하기.

No-clobber 2-게이트

  1. 시작 시: out이 이미 존재하고 선택 set 전체를 포함하는 완전 매니페스트면 덮어쓰지 않고 exit 0
  2. 계산 끝난 후: race로 다른 job이 그새 완전본을 썼는지 재확인 후에만 기록. 기록은 tmp + os.replace 원자적, sort_keys=True로 워커 완료 순서와 무관하게 byte-identical

테스트 seam

spawn 자식은 모듈을 재import하므로 monkeypatch가 안 닿음 → 명시적 $INIT_ORACLE_MODULE_DIR 환경변수(프로덕션 미사용, unset)로 stub 주입. 자식이 env 상속해 동일 stub import.

sbm "bash scripts/run_init_oracle.sh" \ --gres=gpu:0 -c 32 --mem 128GB --qos=extra --time=01:00:00 # run_init_oracle.sh: python scripts/run_init_oracle_parallel.py \ # --workers 28 --out data/scene_mem_init_state.parallel.json \ # --shard-dir ./tmp/oracle_shards

3 결과 (수치)

5/5
Unit tests
8/8
Real smoke
0.14
sc/s @ W=4
~15min
858 sc, W=28

Unit test 5개 (memer env, robocasa 없이 stub)

Real smoke (robocasa-main, limit=8, W=4, ./tmp scratch)

8/8 성공. rate 0.14 sc/s @ W=4, elapsed 56s. env build이 직렬 부분이고 spawn 재import 포함이라 per-worker 처리율이 율속.

Workerssc/s (외삽)858 sc 소요 (외삽)비고
4 (실측)0.14~6143s (~102min)smoke 기준
28 (외삽)~0.98~875s (~15min)이론 상한; 실제 20-30min 예상
1 (순차)~0.045-6hpreempt 취약
Resume 실증: 완전본 존재 시 재실행 → "already complete, exit 0" 클로버 안 함. out만 삭제하고 shards 유지 → 8개 전부 skip, env 호출 0, 매니페스트 재병합.

ruff clean, py_compile OK (memer + robocasa-main 양쪽), bash -n OK. data/scene_mem_init_state.json / task_subtasks.json / scene_mem_v2 전부 untouched.

4 Takeaway

preempt 내성 fallback 확보: job이 죽어도 ./tmp/oracle_shards에서 무손실 이어하기 가능, 동시 실행 중인 job의 산출물을 절대 덮어쓰지 않음. spawn-context 정확성 + crash-safe 증분 체크포인트 + no-clobber 3축이 핵심이며 real smoke로 실증됐다. 순차 대비 wall-clock을 한 자릿수 시간 → 십수 분대로 압축.

MuJoCo fork 금지 원칙: mp.fork는 MuJoCo/robosuite에서 deadlock을 유발한다(공식 주의사항). 병렬 oracle driver처럼 env를 spawning하는 코드는 반드시 mp.get_context("spawn") + maxtasksperchild=1을 사용해야 한다. 성능 비용(재import)은 env build 시간에 비해 무시 가능.

5 Next Steps