carrot-in-pot IFQL — model card

작성: S1 (Sonnet), 2026-09-15 — 2026-09-16 갱신(Claude, Opus): aug8 run, Stage-2 pixel real 5 run, DSRL-NA sim 3 run(negative result), sim eval 영상 8개 추가. 다른 PC에서 이 모델을 로드해서 그대로 돌릴 수 있게 만드는 게 이 문서 목적. 수치는 전부 리포·npz meta·flags.json·train.csv·eval summary에서 직접 읽은 값이고, 어디서 읽었는지는 cards/CARD_SOURCES.md에 file:line으로 남겨둠(09-16 추가분은 그 파일의 §2026-09-16). 필드 뜻은 cards/FIELD_GLOSSARY.md. 한 줄 요약(09-16): 이 repo엔 31 run(sim 9 · real frozen 16 · real aug8 1 · real px 5) + DSRL 3 run이 있음. sim은 50-seed held-out eval까지 끝났고(§7), 실물(UR7e) 정량 eval은 여전히 0건 — carrot 실물 배포는 엔벨로프·start pose 문제로 아직 안 돌렸음(§13). 새로 추가된 aug8·px 체크포인트도 실물 미검증. 자매 태스크 orange-bowl은 Bigenlight/orange-bowl-in-purple-bowl-ifql.

0. repo 구성 (2026-09-16)

README.md, models.json(31 run 매니페스트), cards/, code/carrot_ifql_code_20260915|16.tar.gz, hf_release/(make_release.py, stage_extra.py, load_example.py, infer_numpy.py, ...)
ifql_sim_k{0.8,0.9,0.93,0.95}_s{0,1}, ifql_sim_k0.9_s0_qmin        Stage-1 frozen, sim (mix_r18_ss_stage)        9
ifql_real_k{0.8,0.9,0.93,0.95}_s{0,1}                               Stage-1 frozen, real lead0                    8
ifql_real_lead6_k{0.8,0.9,0.93,0.95}_s{0,1}                         Stage-1 frozen, real lead6                    8
ifql_real_k0.9_lead6_aug8_p1.0_s0                                   Stage-1 frozen + feature-space aug8 (§9)      1   ← 09-16
px_real_impala_s{0,1}, px_real_impala_lead6_s{0,1}, px_real_r18ss_s0   Stage-2 end-to-end pixel, real, @50k (§10)  5   ← 09-16
dsrl/{dsrl_v0_s0, dsrl_v2ns075_s0, dsrl_v3aobs_s0}                  DSRL-NA sim, negative result (§11)           3   ← 09-16
videos/ep_{100,101,107,111,122,125,129,146}_*.mp4 + README.md      ifql_sim_k0.9_s0@100k bc 4 success/4 timeout    ← 09-16
  └ 각 run: params_<step>.pkl, params_<step>.infer.pkl(슬림), flags.json, train.csv, norm_stats_*.json, serve_meta.json, RUN.md

1. 태스크 & 로봇

  • 태스크: "Put carrot in pot" — carrot을 집어 pot 안에 넣고 그리퍼를 연 채로 1초(dwell) 유지하면 성공.
  • 로봇: UR7e 6-DOF 팔 + Robotiq 2F-85 평행 그리퍼, teleop은 GELLO.
  • 카메라: cam1(정면, 삼각대 고정) + cam2(손목 장착), 각 1280×720, 30 Hz, JPEG로 전송/저장(q92).
  • 관측 상태: ur_q1..6(rad) + grip_pos(0..1) = 7-D. 액션: cmd1..6(절대 관절 목표 rad) + grip_cmd(0..1) = 7-D.
  • sim 쌍둥이: 같은 태스크의 MuJoCo 환경, 같은 관측/액션 스키마·같은 전처리·같은 인코더로 sim/real이 코드를 공유함(PLAN_FINAL 결정).

2. 모델 계열 (families)

IFQL(Implicit Flow Q-Learning) — actor는 순수 BC flow-matching으로만 학습, critic은 IQL expectile로 학습, Q는 배포 시점에만 best-of-n(BoN) rejection sampling으로 개입. num_samples=1이면 그냥 BC control이 되므로 같은 체크포인트로 BC와 BoN을 둘 다 평가할 수 있음.

family 관측 인코더 obs dim (D_a) 학습 데이터 npz κ sweep seed steps 체크포인트 위치(예시) eval 결과
stage1_frozen_sim frozen ResNet18 r18_ss (§4) 2055 mix_r18_ss_stage.npz {0.8, 0.9, 0.93, 0.95} × 2 seed + k0.9_s0_qmin(q_agg=min ablation) 0, 1 100,000 (ckpt @25k/50k/75k/100k) kanu:~/ifql_carrot/exp/ifql/vision/ifql_sim_k{κ}_s{seed}[_qmin]/params_100000.pkl §7 표, 50-seed(100–149) held-out, sim harness
stage1_frozen_real 위와 동일 인코더 2055 real_demo_r18_ss.npz (lead=0, action_lead 보정 전) {0.8, 0.9, 0.93, 0.95} × 2 seed 0, 1 100,000 (real: save_interval 50k → ckpt @50k/100k) kanu:~/ifql_carrot/exp/ifql/vision/ifql_real_k{κ}_s{seed}/params_100000.pkl 실물 eval 미실시(§7)
stage1_frozen_real_lead6 위와 동일 인코더 2055 real_demo_r18_ss_lead6.npz (action_lead=6, §3(g)) {0.8, 0.9, 0.93, 0.95} × 2 seed 0, 1 100,000 (ckpt @25k/50k/75k/100k, 릴리스 50k/100k) ifql_real_lead6_k{κ}_s{seed}/params_100000.pkl 실물 eval 미실시
stage1_frozen_real_lead6_aug8 (09-16) 위와 동일 인코더, 학습 시 feature-space aug (§9) 2055 real_demo_r18_ss_lead6_aug8.npz (obs_aug 8 view, feat_aug_p=1.0) 0.9 0 100,000 ifql_real_k0.9_lead6_aug8_p1.0_s0/params_100000.pkl 실물 eval 미실시 — val actor 0.187 (frozen lead6 0.340)
stage2_px_sim end-to-end multicam_r18ss 또는 multicam_impala_small (§5) — (dict obs, encoder가 (B,F)로 변환) mix_px96.npz κ=0.9 0, 1(impala만) 50,000 목표 이 repo에 없음 — 09-16 현재 kanu_mirror의 px_sim_* 디렉토리가 비어 있음(체크포인트 미동기화). 들어오면 추가 없음
stage2_px_real (09-16) multicam_impala_small ×4, multicam_r18ss ×1 (§10) real_demo_px96.npz (lead0) / real_demo_px96_lead6.npz (lead6) κ=0.9 0, 1 50,000 (ckpt @12.5k 간격, 릴리스 50k만) px_real_{impala,impala_lead6}_s{0,1}/, px_real_r18ss_s0/params_50000.pkl 실물 eval 미실시 — val actor 0.121~0.200
dsrl_na_sim (09-16) frozen IFQL ifql_sim_k0.9_s0@100k 위의 noise-space SAC (§11) 2055 (critic 2076 옵션) mix_r18_ss_stage.npz (offline) + sim online 1000 ep — (base κ 0.9) 0 offline 100k + online ~460k grad step dsrl/<run>/params_<final>.pkl sim 50-seed: base 대비 개선 없음(negative)

세부 설정(레이어 폭, 액션 lo/hi 등)은 SPEC_<family>.md 참고. 필드 하나하나의 뜻은 FIELD_GLOSSARY.md.

3. 추론 계약 — 공통

모든 family는 같은 ZMQ REP 서버(ifql_server.py)와 같은 56-D action chunk → 24-frame action queue 뒤에서 서빙됨. 다른 건 관측을 만드는 전처리(§4 frozen-feature vs §5 pixel)뿐.

(a) 관측 만들기

frozen-feature family (stage1_*) — 서버가 torch로 ResNet18을 돌려서 특징을 뽑고, 그 벡터를 IFQL agent(JAX)에 넣음:

cam JPEG bytes (ZMQ로 온 그대로, harness가 1280x720 q92로 보냄)
  -> cv2.imdecode -> BGR -> RGB                                   # preprocess.jpeg_bytes_to_rgb
  -> cv2.resize(..., (224, 224), interpolation=cv2.INTER_AREA)    # 정사각 squash, crop 없음 (전체 프레임)
  -> float32, x/255, (x - IMAGENET_MEAN) / IMAGENET_STD            # channels-first (3,224,224)
     IMAGENET_MEAN = [0.485, 0.456, 0.406]  IMAGENET_STD = [0.229, 0.224, 0.225]
  -> torchvision resnet18(weights=IMAGENET1K_V1), fp16 autocast    # conv1→bn1→relu→maxpool→layer1..4
  -> layer4 feature map (B, 512, 7, 7), pooling은 fp32로
  -> per-channel spatial softmax over 49 cells
       ex = sum_hw softmax(h)_hw * x_grid,  ey = sum_hw softmax(h)_hw * y_grid
       x_grid = y_grid = linspace(-1, 1, 7)
  -> 1024-D per cam, layout [x_1..x_512, y_1..y_512]               # r18_ss variant

standardize: f_cam_n = (f_cam_raw - norm_stats["feature"][cam]["mean"]) / norm_stats["feature"][cam]["std"]   # elementwise, 1024-D
proprio_n  = (state_7 - norm_stats["proprio"]["mean"]) / norm_stats["proprio"]["std"]                          # elementwise, 7-D

observations = concat([f_cam1_n(1024), f_cam2_n(1024), proprio_n(7)])   # = 2055-D, 이 순서 그대로 (cam1 먼저)

이 파이프라인은 offline feature extraction(extract_features.py)과 서버(EncoderBridge/VariantEncoder)가 같은 함수(preprocess.py)를 공유해서 만듦 — 훈련·서빙 특징이 바이트 단위로 같지는 않아도(offline은 AV1→JPEG round-trip, online은 하네스가 이미 JPEG로 줌) 같은 코드 경로. Real 데이터 parity 실측 worst cosine 0.999999(6프레임, real_pipeline/parity_real.py).

pixel family (stage2_px_*) — torch 없음, 인코더가 JAX agent 안에 있음:

cam JPEG bytes
  -> cv2.imdecode -> RGB
  -> cv2.resize(..., (96, 96), interpolation=cv2.INTER_AREA)   # 빌더와 동일 (squash, crop 없음)
  -> CENTRE crop 84x84, offset ((96-84)//2, (96-84)//2) = (6, 6), 두 캠 동일   # 배포는 항상 center crop (random 아님)
  -> uint8 (84, 84, 3)  — /255 하지 않음 (인코더가 함)
proprio: 7-D state -> standardize with norm_stats["proprio"]{mean,std}   # px json의 proprio 블록, feature 방식과 동일 공식
observations = {'cam1': uint8(1,84,84,3), 'cam2': uint8(1,84,84,3), 'proprio': f32(1,7)}

PxObsBuilder는 학습 로더(utils.datasets.crop_gather)와 동일한 함수로 crop해서 offset이 항상 일치하게 만듦(재구현 아님). 트랩: sim이라도 JPEG round-trip을 생략하면 안 됨(학습 이미지가 이미 그 라운드트립을 거침); crop은 항상 center(random은 학습 전용); img/crop 크기는 npz meta에서 읽으므로 나중에 64² 데이터셋이 와도 서버 코드는 안 바뀜.

(b) 액션 디코딩

에이전트가 뱉는 건 56-D, [-1, 1] 범위 (8 knots × 7-D: joint1..6 rad target + grip [0,1], 표준화된 값).

chunk56_n (56,) in [-1,1]
  -> reshape (8, 7)                                    # knot k = 원본 30Hz 스트림의 frame i+3*k, k=0..7 (10Hz decision rate)
  -> denormalize per-dim:  raw = lo + (n + 1)/2 * (hi - lo)     # lo, hi = norm_stats["action"]{lo,hi} (7,)
     grip dim(index 6)만 추가로 clip(0, 1)
  -> knots (8, 7) 절대 관절 rad(×6) + grip 0..1

lo/hi는 학습 episode의 raw action min/max에서 ±5% 범위를 늘린 값(grip은 항상 [0,1]). 정확한 배열은 SPEC_<family>.md.

24-frame 큐로 확장 (action_queue.py, H=8, STRIDE=3, N_INTERP=22, N_HOLD=2, QUEUE_LEN=24):

관절(0..5): frame f=0..21 은 knot floor(f/3)과 floor(f/3)+1 사이 선형보간, frac=(f/3 - floor(f/3))
            frame 21 = knot 7 정확히 일치
            frame 22, 23 = knot 7을 그대로 hold
그리퍼(6):  frame f 는 knot floor(f/3)의 grip 값 — zero-order hold, 절대 보간 안 함

서버는 ACT 한 번에 큐에서 프레임 1개(7-D: rad×6 + grip 0..1)를 pop해서 돌려줌. 큐가 비면(24번째 ACT 이후) 새 관측으로 재계획(decode → encode → sample)해서 refill. 즉 0.8초(30Hz×24)마다 재계획, 그 사이는 미리 계산된 궤적을 그대로 따라감.

action_lead (stage1_frozen_real_lead6 전용) — 학습 데이터 빌드 단계에서만 쓰인 보정: joints 1-6의 knot k는 frame min(i + 3k + 6, last)에서 가져오고(그리퍼는 lead 0 그대로), real teleop 로그의 "command ≈ state"(lag 0) 관행을 sim의 "command가 state보다 ~6프레임 앞섬" 관행에 맞춤(§8(g)). 추론 시점 디코딩 절차는 lead=0/6 두 체크포인트가 완전히 동일 — 체크포인트가 이미 그 보정을 반영해서 학습됐을 뿐, 서버 쪽 코드는 안 바뀜. norm_stats 파일만 맞는 걸 써야 함(norm_stats_real_r18_ss_lead6.json).

(c) 샘플러 (--sampler bc|bon|actor)

sampler K 동작
bc 1 순수 BC flow 샘플 1개 — sample_candidates(obs, seed, num_samples=1)의 유일한 후보
bon --num-samples(기본 32) K개 노이즈 → 각각 Euler 적분 → critic Q 평가(ensemble aggregate, stop-grad) → argmax
actor agent.sample_actions(obs, seed) 그대로 (IFQL엔 안 씀, qflow_rc류 폴백)

IFQL의 실제 백엔드는 항상 sample_candidates(agent가 이 메서드를 노출하므로):

actions, qs = agent.sample_candidates(obs, seed=key, num_samples=K)   # (1,K,56), (1,K)
j = argmax(qs[0]); chunk = actions[0, j]

내부적으로 K개 노이즈를 10-step Euler로 적분:

x_0 ~ N(0, I) in R^56
for i in 0..9:  t = i/10;  v = actor_flow(obs_encoded, x, t);  x = x + v/10
x = clip(x, -1, 1)                        # 매 스텝 clip 아님, 최종에만

Q 집계(q_agg): meanmean(qs, axis=0) - rho*std(qs, axis=0) (기본 rho=0이라 그냥 mean), minqs.min(axis=0). num_qs=2 앙상블. num_samples는 JAX static_argname이라 값이 바뀌면 재컴파일됨 — 서버는 시작할 때 한 값으로 고정해서 씀.

(d) params_<step>.pkl / flags.json에서 에이전트 재구성

  • params_<step>.pkl = pickle.load(){"agent": flax.serialization.to_state_dict(agent)}. IFQL agent의 network (ModuleDict: value, critic, target_critic, actor_flow, [pixel일 때 actor_flow_encoder]) 파라미터 전체 + optimizer state. config는 이 파일에 없음flags.json에서 따로 읽어야 함. Stage1 sim 체크포인트 파일 크기 ≈ 134,885,042 bytes (128.6 MB), 4개(25k/50k/75k/100k) 저장(sim), real은 2개(50k/100k).

  • flags.json은 학습 CLI 전체를 담은 JSON. 필요한 건 flags.json["agent"] 딕셔너리 그대로 — 실제 관측값(2026-09-15 sim run 예시):

    {"agent_name": "ifql", "lr": 0.0003, "batch_size": 256,
     "actor_hidden_dims": [512,512,512,512], "actor_layer_norm": false,
     "value_hidden_dims": [512,512,512,512], "value_layer_norm": true,
     "num_qs": 2, "q_agg": "mean", "rho": 0.0, "kappa": 0.9, "discount": 0.923,
     "tau": 0.005, "flow_steps": 10, "num_samples": 32, "use_critic_obs": false,
     "encoder": null, "ob_dims": null, "action_dim": null}
    

    (ob_dims/action_dimnull로 저장되지만 create()가 example 배열에서 자동으로 채움.)

  • 재구성 절차 (ifql_server.py가 정확히 이렇게 함, §6):

    1. flags.json 로드 → agent_module_from_flagsifql
    2. agents.ifql.IFQLAgent, get_config import
    3. cfg = ml_collections.ConfigDict(flags["agent"]); cfg.ob_dims = None; cfg.action_dim = None
    4. frozen-feature family: 예시 배열 ex_obs = zeros((1, D_a), f32), ex_act = zeros((1, 56), f32), ex_cobs = zeros((1, D_c), f32)D_a, D_c서빙에 쓸 norm_stats jsonD_a/D_c에서 읽음 (r18_ss 계열: D_a=2055; D_c는 있어도 use_critic_obs=False라 실제로는 안 쓰임 — 아래 주의 참고). IFQLAgent.create(seed, ex_obs, ex_act, cfg, ex_critic_observations=ex_cobs) 호출.
    5. pixel family: ex_obs = {'cam1': zeros((1,84,84,3),u8), 'cam2': zeros((1,84,84,3),u8), 'proprio': zeros((1,7),f32)}, 같은 dict를 critic 예시로도 씀.
    6. from utils.flax_utils import restore_agent_with_file; agent = restore_agent_with_file(agent, "<run_dir>/params_<step>.pkl")flax.serialization.from_state_dict(agent, load_dict["agent"])로 방금 초기화한 agent에 저장된 파라미터를 덮어씀. 네트워크가 없던(옛) 체크포인트도 허용: 저장본에 없는 키는 방금 초기화한 값 그대로 유지(restore_agent_with_file의 관용).
    7. 이제 agent.sample_actions(...) / agent.sample_candidates(...) 호출 가능.

    주의(D_c 함정): norm_stats_r18_ss_stage.jsonD_c=2076(=D_a+21, z_priv 포함)을 기록하지만, 실제 학습된 sim/real 체크포인트는 전부 use_critic_obs=False(flags.json 확인됨) — 즉 critic/value 네트워크는 observations(D_a=2055)로 만들어졌지 critic_observations(D_c=2076)로 만들어진 게 아님. ex_critic_observations를 넘겨도 use_critic_obs=FalseIFQLAgent.create가 무시하고 ex_cobs = ex_observations를 씀 (qflow_svf/agents/ifql.py:302-304). 이 차원으로 네트워크를 잘못 초기화하면 restore_agent가 파라미터 shape mismatch로 실패함 — critic/value의 입력 차원은 항상 D_a로 만들 것.

  • 아키텍처 요약 (모든 family 공통, utils/networks.py):

    • Value(critic/value 겸용): MLP(hidden_dims=(512,512,512,512)+[1], activation=gelu, activate_final=False, layer_norm=value_layer_norm=True), critic은 num_ensembles=2로 vmap 앙상블(ensemblize), value는 num_ensembles=1. 입력 = [obs, actions](actions는 critic에만) concat.
    • ActorVectorField: MLP(hidden_dims=(512,512,512,512)+[action_dim=56], layer_norm=actor_layer_norm=False). 입력 = [obs, actions(noisy x_t), t] concat (t는 scalar broadcast, Fourier feature 옵션은 기본 꺼짐).
    • MLP 내부: Dense → gelu → (LayerNorm if layer_norm), 마지막 레이어는 activate_final=False라 활성화/LN 없음.

(e) 안전 클램프 (배포 클라이언트, 정책과 무관하게 항상 적용됨 — policy_leader_node._tick_execute)

  1. 엔벨로프: target[i] = clip(action[i], joint_limits_lo[i], joint_limits_hi[i]) — 데이터셋 관절 범위의 1.2배.
  2. 최대 편차: target[i] = clip(target[i], live_q[i] - max_dev_rad, live_q[i] + max_dev_rad), max_dev_rad = 0.5 rad.
  3. 그리퍼: clip(action[6], 0, 1) → 0..255 컨트롤 값으로 스케일.
  4. 속도 상한(브릿지, 250 Hz): max_step_rad = 0.0025 rad/tick → 0.625 rad/s 상한 — 이 값은 올리면 안 됨.
  5. 타임아웃 / ok:false / obs stale → FAULT: 아무것도 publish 안 함, 브릿지 staleness 0.8s 지나면 팔 정지. 재개는 start_execution 재호출뿐.
  6. arming 게이트: /joint_states 살아있고 obs 4개 전부 신선, 현재 자세가 start_pose에서 0.1 rad 이내여야 EXECUTE로 진입.

부등식 유지: act_timeout_s(0.6) < obs_timeout_s(0.7) < staleness_timeout_s(0.8).

(f) ZMQ 와이어 프로토콜

REQ/REP, sock.bind(f"tcp://{host}:{port}") (기본 host 127.0.0.1, 0.0.0.0 bind 금지), 기본 포트 5595.

RESET  req  [ b'{"cmd":"reset"}' ]                                             (1 frame)
       rep  [ json {...} ]                                                     (아래 필드 전부)

ACT    req  [ b'{"cmd":"act","state":[q1..q6,grip]}', cam1_jpeg, cam2_jpeg ]    (3 frames, state는 7-D finite float)
       rep  [ json {"ok": true, "action": [t1..t6, grip]} ]                    (7-D: rad*6 + grip 0..1, 1개 30Hz 프레임)

error  rep  [ json {"ok": false, "err": "ExceptionType: message"} ]            (서버는 절대 안 죽음 — 그 요청만 실패)

RESET reply 필드: ok, policy_type, checkpoint, checkpoint_revision, state_dim(=7), action_dim(=7), n_action_steps(=24), sampler, num_samples, seed, torch_seed, reset_counter, agent, variant, encoder, D_a, D_c, norm_stats, chunk:{H:8,stride:3,interp:22,hold:2}, random_init, git, log_dir, warmup_refill_ms.

서버 부팅 시 warmup(기본 3회, 더미 1280×720 랜덤 JPEG)을 돌려서 XLA 컴파일을 끝내둠 — warmup refill이 300ms 넘으면 경고 로그(하네스 타임아웃 600ms 기준). 실물에선 --budget-sact_timeout_s보다 작아야 함.

(g) 알려진 함정 / 캐비어트

  • real proprio가 이미지보다 ~200ms 미래(lead6로 command-vs-state lag은 고쳤지만, proprio 자체의 시간 정렬 잔여 이슈는 남아있음) — 실물 결과가 이상하면 proprio를 obs에서 빼는 ablation을 해볼 것(ledger, 09-15 저녁).
  • lead6 근거: real teleop 로그는 action[i] ≈ state[i](lag 0)인데 sim 커맨드는 state보다 ~6프레임(0.2s) 앞섬 → 두 도메인의 청크 컨벤션이 다르면 sim에서 학습한 직관이 real에 안 옮겨감. --action-lead 6(joints만, 그리퍼는 lead 0)으로 real 빌드를 다시 해서 두 도메인을 맞춤.
  • val split: sim 데모는 episode [7, 16](고정, demo_r18_ss_stage.npz 기준), real은 episode [10, 25, 40, 53]. eval 성공률은 전부 이 val split과 무관한 held-out seed(sim: 100–149, MuJoCo world reset seed)에서 측정됨 — val split은 학습 통계(정규화·action bound)에서만 제외됨.
  • 성공 라벨 가정: real 54 take, sim 22 demo 전부 "성공한 시연"이라는 가정 하에 sparse reward(-1/step, 마지막 프레임 0)를 만듦 — 프레임 단위 라벨은 없음. real 쪽은 Theo가 직접 확인 대기 중(ledger "Theo 확인 대기 ① real 54 take 전부 성공 시연 맞는지").
  • sim eval 텍스처 함정: scripted rollout·eval world가 seamless_wood_planks_floor.png(.gitignore됨, 리포에 없음)를 요구함. 없으면 경고 없이 체커보드 바닥으로 폴백 — 카메라 조건부 정책 eval이 조용히 무효화됨. eval 전에 텍스처 PNG 존재를 반드시 확인.
  • validation critic 일반화 약함: mix_r18_ss_stage 학습 로그에서 validation critic loss가 700~2900인데 train은 ~0.3 (ledger 09-15) — frozen feature critic이 held-out demo state에 일반화를 잘 못함. BoN 효과가 그런 state 근처에서는 약할 수 있음.
  • carrot 실물 엔벨로프 미해결: carrot_eef_limits.json(J1 ∈ [-3.80, -2.58])이 배포 PC에 없고, sim 데모는 J1 -4.05까지 감 — sim 엔벨로프를 그대로 쓰면 냄비 근처에서 팔이 잘림. 실물 54 take로 엔벨로프를 다시 뽑아서 ifql_deploy.yaml에 넣기 전엔 실물 배포 금지.
  • 모든 릴리스 체크포인트가 use_critic_obs=False: 훈련 npz는 critic_observations(privileged object pose)를 담고 있지만, 실제로 학습된 critic/value는 그걸 무시하고 observations만 읽음(sim/real 동일 레시피를 위한 설계 선택). 비대칭 critic은 문서화만 된 미사용 sim 전용 ablation 경로.
  • 텍스트로 명시된 "512 MLP" = SERL/HIL-SERL식 관행 해석: Stage 1(frozen feature + 512×4 MLP)이 사실상 그거고, Stage 2가 end-to-end 버전 (ledger §0 "512 MLP" 해석 각주).

4. r18_ss frozen encoder — 정확한 스펙

  • 모델: torchvision.models.resnet18(weights=ResNet18_Weights.IMAGENET1K_V1), 파인튠 없음, freeze.
  • 입력: 224×224×3, ImageNet 정규화(§3(a)).
  • 특징: layer4 출력(512, 7, 7) → 채널별 spatial softmax(49 cells) → x/y expected coords, linspace(-1,1,7) 그리드 → [x_1..x_512, y_1..y_512] 순서로 concat = 1024-D per cam.
  • r18_avg(avgpool, 512-D)도 같은 파이프라인에서 계산되지만 이 IFQL 릴리스는 r18_ss만 씀.

5. Stage 2 pixel encoder 레지스트리 (Q/utils/encoders.py)

registry name per-cam trunk 출력 F (2캠+proprio7) encoder params tower params 3060 A4000
multicam_r18ss ResNet18-GroupNorm(16ch/group) + spatial softmax(32 kp), pretrain 없음 135 22.39 M 23.25 M 155 ms 91.8 ms
multicam_impala_small ImpalaEncoder(num_blocks=1) 1031 4.08 M 5.40 M 51 ms 38.8 ms

MultiCamEncoder(cams=('cam1','cam2'), backbone=..., proprio_key='proprio')는 obs dict를 받아서 캠별 독립 trunk → concat(비주얼 특징 ‖ raw proprio) → (B, F). ResNet18-GN stem은 torchvision 기본(7×7 s2 conv + 3×3 s2 maxpool) → layer4에서 3×3×512 맵(robomimic의 84² 레시피와 동일). 측정: A4000, mix_px96.npz(N=57,892), batch 256, multicam_r18ss 336 ms/step(XLA 컴파일 별도 ~260s), 50k step ≈ 4.7h; multicam_impala_small 솔로 ≈130ms/step, GPU 공유 시 ≈325ms/step.

6. 서버 실행 예시

# frozen-feature sim 체크포인트
XLA_PYTHON_CLIENT_PREALLOCATE=false W/.venv-svf/bin/python ifql_server.py \
    --run-dir W/exp/ifql/vision/ifql_sim_k0.9_s0 --step 100000 \
    --sampler bon --num-samples 32 --port 5595

# frozen-feature real 체크포인트 (norm-stats 명시 필수!)
TORCH_HOME=$W/.cache/torch XLA_PYTHON_CLIENT_PREALLOCATE=false \
$W/.venv-svf/bin/python ifql_server.py \
    --run-dir $W/exp/ifql/real/ifql_real_k0.9_s0 --step 100000 \
    --sampler bon --num-samples 32 --port 5595 --budget-s 0.6 \
    --norm-stats $W/data/rl/norm_stats_r18_ss_real.json

# 프로토콜만 테스트 (체크포인트 없이)
... ifql_server.py --random-init --variant r18_ss --D-a 2055 --agent-module ifql --port 5595
  • --norm-stats를 안 주면 서버가 flags.jsonenv_name(학습 npz 경로) → 그 npz의 meta.norm_stats → sibling norm_stats_<variant>.json → real/sim fallback 순으로 추론함. 로그의 norm_stats: ... (source: ...) 한 줄을 반드시 눈으로 확인 — 엉뚱한 파일이면 관절이 다른 범위로 denormalize돼서 위험.
  • bool 플래그는 학습 시에도 서빙 시에도 항상 = 형태로 (--agent.use_critic_obs=False), --agent.x False는 조용히 True가 됨.

7. 결과 (검증된 수치, sim held-out seed 100–149, 50 episodes)

W/results/RESULTS_ifql_stage1.md는 이 카드 작성 시점(2026-09-15 21:35)에 아직 작성되지 않음(ledger: "집계 Sonnet이 작성 예정"). 아래 표는 이 카드 작성 중 eval_runs/*/summary.md를 직접 읽어서 만든 것 — 그 파일이 생기면 이 표를 그걸로 교체할 것.

κ seed ckpt step bc(K=1) SR bon(K=32) SR
0.8 0 100k (summary.md 없음) 64.0% (32/50)
0.8 1 100k 66.0% (33/50) 68.0% (34/50)
0.8 1 50k 46.0% (23/50)
0.9 0 100k 68.0% (34/50) 68.0% (34/50)
0.9 0 50k 60.0% (30/50)
0.9 0 qmin 100k (1 ep만, 무의미) 64.0% (32/50)
0.9 1 100k 62.0% (31/50) 74.0% (37/50)
0.9 1 50k 42.0% (21/50)
0.93 0 100k 62.0% (31/50) 62.0% (31/50)
0.93 0 50k 54.0% (27/50)
0.93 1 100k 64.0% (32/50) 70.0% (35/50)
0.93 1 50k 54.0% (27/50)
0.95 0 100k 56.0% (28/50) 66.0% (33/50)
0.95 0 50k 50.0% (25/50)
0.95 1 100k 46.0% (23/50) 68.0% (34/50)
0.95 1 50k 53.3% (16/30, n=30만)

Wilson 95% CI는 각 summary.md에 있음 (예: n=50 SR 6070%대는 대략 ±13pp). fault/failure 0건, envelope/max_dev/grip clamp hit 0건(sim). 경향: 100k가 50k보다 대체로 낫고, bon32가 bc보다 대체로 낫지만 κ=0.93_s0처럼 동률인 경우도 있음 — κ 단일 승자는 아직 없음(sweep 전체 6274% 범위).

중요한 정정: ledger 09-15 저녁 로그의 "IFQL k0.9_s0@100k seed 100-104: bc 3/5, bon32 4/5, fault 0, refill 13ms"는 표현상 마치 실물 로봇 결과처럼 읽히지만, 실제로는 eval_runs/ifql_sim_k0.9_s0_100000_{bc1,bon32}_s100to104/summary.md의 5-episode SIM 서브셋(성공 3/5, 4/5)과 정확히 일치함 — sim 스모크 체크였음. 이 카드 작성 시점까지 실물(UR7e) eval은 한 건도 진행되지 않음 (eval_runs/*real* 디렉토리 없음, REALROBOT_RUNBOOK_IFQL.md §6 체크리스트 미완료 — carrot 엔벨로프 이슈가 blocking).

8. 파일 위치 요약

무엇 어디
tracked 에이전트 코드 /home/theo_lab/workspace/research/fmrl-vision/qflow_svf/agents/ifql.py (실행 사본: W/ext/qflow_svf_merged/agents/ifql.py, bash FV/sync_runtime.sh로 동기화)
서버 /home/theo_lab/workspace/research/fmrl-vision/vision_carrot/ifql_server.py
액션 큐/정규화 /home/theo_lab/workspace/research/fmrl-vision/vision_carrot/action_queue.py
전처리 공유 함수 /home/theo_lab/workspace/research/fmrl-vision/vision_carrot/preprocess.py
네트워크 정의 /home/theo_lab/workspace/research/fmrl-vision/utils/networks.py (Value, ActorVectorField, MLP)
저장/복원 /home/theo_lab/workspace/research/fmrl-vision/utils/flax_utils.py (save_agent, restore_agent, restore_agent_with_file)
sim 체크포인트(로컬 미러) ~/workspace/research/kanu_mirror/ifql_carrot/exp/ifql/vision/ifql_sim_k{κ}_s{seed}[_qmin]/
real 체크포인트(로컬 미러) ~/workspace/research/kanu_mirror/ifql_carrot/exp/ifql/vision/ifql_real_k{κ}_s{seed}/
실물 배포 런북 /home/theo_lab/workspace/research/fmrl-vision/vision_carrot/REALROBOT_RUNBOOK_IFQL.md
eval 결과(sim, 50-seed) ~/workspace/research/fmrl-vision-work/eval_runs/ifql_sim_*/summary.{md,json}

2026-09-16 추가분

9. 전체 run 표 (31 + DSRL 3) · 추천 체크포인트

val actor/critic = train.csv 마지막 행의 validation/actor/actor_loss / validation/critic/critic_loss. sim SR = 50-seed(100–149) held-out, 최종 step, bc(K=1) / bon32. 같은 seed·같은 데이터면 κ가 달라도 val actor loss가 같음 — IFQL actor는 순수 BC flow라 critic expectile κ와 무관(버그 아님). sim의 val critic이 800~4000인 건 §3(g) "validation critic 일반화 약함".

run family data npz lead κ seed steps ckpt files val actor / val critic @final sim SR @final (bc / bon32) real
ifql_sim_k0.8_s0 frozen mix_r18_ss_stage.npz sim(+6) 0.8 0 100k 50k, 100k (+.infer) 0.387 / 815.7 60% (30/50) / 64% (32/50) n/a (sim)
ifql_sim_k0.8_s1 frozen mix_r18_ss_stage.npz sim(+6) 0.8 1 100k 50k, 100k (+.infer) 0.387 / 2339.5 66% (33/50) / 68% (34/50) n/a (sim)
ifql_sim_k0.9_s0 frozen mix_r18_ss_stage.npz sim(+6) 0.9 0 100k 50k, 100k (+.infer) 0.387 / 1810.4 68% (34/50) / 68% (34/50) n/a (sim) — videos/ 이 run
ifql_sim_k0.9_s0_qmin frozen (q_agg=min) mix_r18_ss_stage.npz sim(+6) 0.9 0 100k 50k, 100k (+.infer) 0.387 / 2896.7 58% (29/50) / 64% (32/50) n/a (sim)
ifql_sim_k0.9_s1 frozen mix_r18_ss_stage.npz sim(+6) 0.9 1 100k 50k, 100k (+.infer) 0.387 / 4079.3 62% (31/50) / 74% (37/50) n/a (sim)
ifql_sim_k0.93_s0 frozen mix_r18_ss_stage.npz sim(+6) 0.93 0 100k 50k, 100k (+.infer) 0.387 / 1521.8 62% (31/50) / 62% (31/50) n/a (sim)
ifql_sim_k0.93_s1 frozen mix_r18_ss_stage.npz sim(+6) 0.93 1 100k 50k, 100k (+.infer) 0.387 / 1540.3 64% (32/50) / 70% (35/50) n/a (sim)
ifql_sim_k0.95_s0 frozen mix_r18_ss_stage.npz sim(+6) 0.95 0 100k 50k, 100k (+.infer) 0.387 / 2062.3 56% (28/50) / 66% (33/50) n/a (sim)
ifql_sim_k0.95_s1 frozen mix_r18_ss_stage.npz sim(+6) 0.95 1 100k 50k, 100k (+.infer) 0.387 / 1739.3 46% (23/50) / 68% (34/50) n/a (sim)
ifql_real_k0.8_s0 frozen real_demo_r18_ss.npz 0 0.8 0 100k 50k, 100k (+.infer) 0.276 / 0.233 not evaluated
ifql_real_k0.8_s1 frozen real_demo_r18_ss.npz 0 0.8 1 100k 50k, 100k (+.infer) 0.271 / 0.314 not evaluated
ifql_real_k0.9_s0 frozen real_demo_r18_ss.npz 0 0.9 0 100k 50k, 100k (+.infer) 0.276 / 0.237 not evaluated
ifql_real_k0.9_s1 frozen real_demo_r18_ss.npz 0 0.9 1 100k 50k, 100k (+.infer) 0.271 / 0.240 not evaluated
ifql_real_k0.93_s0 frozen real_demo_r18_ss.npz 0 0.93 0 100k 50k, 100k (+.infer) 0.276 / 0.231 not evaluated
ifql_real_k0.93_s1 frozen real_demo_r18_ss.npz 0 0.93 1 100k 50k, 100k (+.infer) 0.271 / 0.283 not evaluated
ifql_real_k0.95_s0 frozen real_demo_r18_ss.npz 0 0.95 0 100k 50k, 100k (+.infer) 0.276 / 0.246 not evaluated
ifql_real_k0.95_s1 frozen real_demo_r18_ss.npz 0 0.95 1 100k 50k, 100k (+.infer) 0.271 / 0.270 not evaluated
ifql_real_lead6_k0.8_s0 frozen real_demo_r18_ss_lead6.npz 6 0.8 0 100k 50k, 100k (+.infer) 0.340 / 0.251 not evaluated
ifql_real_lead6_k0.8_s1 frozen real_demo_r18_ss_lead6.npz 6 0.8 1 100k 50k, 100k (+.infer) 0.304 / 0.237 not evaluated
ifql_real_lead6_k0.9_s0 frozen real_demo_r18_ss_lead6.npz 6 0.9 0 100k 50k, 100k (+.infer) 0.340 / 0.263 not evaluated (실물 첫 rollout 후보였음, §13)
ifql_real_lead6_k0.9_s1 frozen real_demo_r18_ss_lead6.npz 6 0.9 1 100k 50k, 100k (+.infer) 0.304 / 0.213 not evaluated
ifql_real_lead6_k0.93_s0 frozen real_demo_r18_ss_lead6.npz 6 0.93 0 100k 50k, 100k (+.infer) 0.340 / 0.251 not evaluated
ifql_real_lead6_k0.93_s1 frozen real_demo_r18_ss_lead6.npz 6 0.93 1 100k 50k, 100k (+.infer) 0.303 / 0.237 not evaluated
ifql_real_lead6_k0.95_s0 frozen real_demo_r18_ss_lead6.npz 6 0.95 0 100k 50k, 100k (+.infer) 0.340 / 0.253 not evaluated
ifql_real_lead6_k0.95_s1 frozen real_demo_r18_ss_lead6.npz 6 0.95 1 100k 50k, 100k (+.infer) 0.304 / 0.244 not evaluated
ifql_real_k0.9_lead6_aug8_p1.0_s0 aug (feat_aug_p 1.0) real_demo_r18_ss_lead6_aug8.npz 6 0.9 0 100k 50k, 100k (+.infer) 0.187 / 0.138 not evaluated
px_real_impala_s0 px multicam_impala_small real_demo_px96.npz 0 0.9 0 50k 50k (+.infer) 0.148 / 0.231 not evaluated
px_real_impala_s1 px multicam_impala_small real_demo_px96.npz 0 0.9 1 50k 50k (+.infer) 0.121 / 0.215 not evaluated
px_real_impala_lead6_s0 px multicam_impala_small real_demo_px96_lead6.npz 6 0.9 0 50k 50k (+.infer) 0.165 / 0.156 not evaluated
px_real_impala_lead6_s1 px multicam_impala_small real_demo_px96_lead6.npz 6 0.9 1 50k 50k (+.infer) 0.136 / 0.221 not evaluated
px_real_r18ss_s0 px multicam_r18ss real_demo_px96.npz 0 0.9 0 50k 50k (+.infer) 0.200 / 0.212 not evaluated
dsrl/dsrl_v0_s0 dsrl_na (b_W 1.5) mix_r18_ss_stage.npz + online sim base 0.9 0 558,480 params_558480.pkl det 80% (40/50) @ep1000, 신선 seed 62%/54% (§11) n/a (sim)
dsrl/dsrl_v2ns075_s0 dsrl_na (b_W 0.75) 위와 동일 sim base 0.9 0 570,200 params_570200.pkl det 70% (35/50) @ep1000 n/a (sim)
dsrl/dsrl_v3aobs_s0 dsrl_na (privileged obs, actor+critic) 위와 동일 sim base 0.9 0 574,280 params_574280.pkl det 54% (27/50) @ep1000 n/a (sim)

체크포인트 크기: frozen/aug params_<step>.pkl 134,885,042 B(+ .infer.pkl ≈45 MB); px impala 292,902,321 B(+ ≈98 MB); px r18ss 1,138,713,070 B(+ ≈380 MB); dsrl 165~166 MB(슬림 없음, §11). px sim run(px_sim_impala_s{0,1}, px_sim_r18ss_s0)은 09-16 현재 kanu_mirror에 체크포인트가 없어서 이 repo에 없음.

추천 (실물 rollout 후보 — 셋 다 실물 미검증)

순위 체크포인트 norm_stats 이유
1 ifql_real_k0.9_lead6_aug8_p1.0_s0 @100k norm_stats_r18_ss_real_lead6.json real 전체 중 val actor 0.187·val critic 0.138로 frozen lead6 대응 run(0.340 / 0.263) 대비 45 % / 48 % 개선. lead6(sim convention). 서버 변경 없이 frozen과 같이 drop-in. AUG_IMPL_NOTES.md의 추천 ckpt
2 px_real_impala_lead6_s0 @50k norm_stats_real_px96_lead6.json end-to-end 인코더라 frozen ImageNet feature의 위치 정보 결핍(§11 probe)을 우회할 유일한 계열; lead6; px 중 val critic 최저 0.156(val actor 0.165). px_real_impala_s1(val actor 0.121)이 actor loss는 더 낮지만 lead0. impala가 r18ss보다 4× 빠름(refill 21 ms @3060)
fallback ifql_real_lead6_k0.9_s0 @100k norm_stats_r18_ss_real_lead6.json aug 없는 frozen lead6 — 09-15 HANDOFF의 첫 실물 rollout 후보
sim 검증됨 ifql_sim_k0.9_s1 @100k (bon32 74 %) / ifql_sim_k0.9_s0 @100k (68 %) norm_stats_r18_ss_stage.json sim 전용. real엔 쓰지 말 것(도메인·엔벨로프 다름)

어느 것도 실물 UR7e에서 검증되지 않음. 실물 배포 전 blocking 항목은 §13.

10. Stage-2 pixel real run (09-16 추가)

§5의 인코더 레지스트리대로 end-to-end 학습. 데이터 real_demo_px96.npz(lead0) / real_demo_px96_lead6.npz(lead6), N=15,941 / val 1,147, px_v1(JPEG q92 → 96² INTER_AREA → uint8, 학습 시 random crop 84 p=1.0, --strong_aug=real = SVEA/SODA식 actor-only 두 번째 view: brightness/contrast/saturation ±0.3, hue ±0.05, 3-tap Gaussian blur p=0.2, random erasing p=0.2). val은 항상 p_aug=0·strong_aug=none(centre crop). 50,000 step(save 12,500 간격, 릴리스는 50k만; 중간 ckpt는 kanu에만), κ 0.9, 나머지 하이퍼파라미터 frozen과 동일(§12). kanu A4000 실측 impala ≈294~320 ms/step(GPU 공유), r18ss 516→335 ms/step.

run encoder lead val actor val critic train actor train critic
px_real_impala_s0 multicam_impala_small 0 0.1478 0.2314 0.0332 0.0037
px_real_impala_s1 multicam_impala_small 0 0.1207 0.2151 0.0468 0.0074
px_real_impala_lead6_s0 multicam_impala_small 6 0.1648 0.1562 0.0359 0.0042
px_real_impala_lead6_s1 multicam_impala_small 6 0.1363 0.2210 0.0477 0.0034
px_real_r18ss_s0 multicam_r18ss 0 0.2001 0.2122 0.0259 0.0028

읽기: px val actor 0.120.20 < frozen carrot real 0.270.34; val critic 0.160.23 < frozen 0.210.31. NaN 0. 실물 판정 0회, sim eval 불가(real 데이터로 학습). 로딩·전처리 계약은 §3(a) pixel family / §3(d) 5번 — load_example.py·ifql_server.pyflags.json["agent"]["encoder"](multicam_*) 또는 *_px96.npz env_name을 보고 자동으로 px 경로로 감(is_px_run; --px/--no-px로 강제). 슬림 .infer.pkl은 px에도 있지만 conv 인코더 forward가 flax 모듈이라 numpy-only 추론은 frozen 계열만(§14).

11. DSRL-NA sim run — negative result (09-16 추가, dsrl/)

무엇: DSRL-NA(Wagenmaker et al. 2025, Alg. 1) — frozen IFQL BC-flow actor(ifql_sim_k0.9_s0@100k, bc 68 %) 위에 noise-space SAC: 정책이 flow의 초기 noise z ∈ [-b_W, b_W]^56(TanhNormal × noise_scale)를 고르고, 그 z를 frozen flow가 action으로 바꿈. critic Q^A(s, a56)는 TD, Q^W(s, z)는 distill(distill_actor_frac 0.5), auto-α(target entropy −28). offline 100k step(mix_r18_ss_stage.npz) → sim online 1000 episode(utd 20, 100 ep마다 50-seed det eval). 코드 vision_carrot/dsrl/train_dsrl.py + agents/dsrl_na.py(코드 타르볼 포함). mujoco 3.10.0 핀, actor objective는 mean(ρ·std grad NaN).

run 변형 base ref (같은 seed, bc) offline-only online ep 100 → 1000 (det SR /50) final ckpt 신선 seed 200–249 / 400–449 (DSRL vs base)
dsrl_v0_s0 b_W 1.5, r18_ss obs 35 15 27 · 21 · 29 · 32 · 20 · 28 · 32 · 34 · 25 · 40 558,480 31 vs 31 / 27 vs 37
dsrl_v2ns075_s0 b_W 0.75 25 (base도 ±0.75 clip) 28 32 · 28 · 29 · 31 · 30 · 29 · 34 · 28 · 31 · 35 570,200 25 / 31
dsrl_v3aobs_s0 privileged z_priv(21)를 actor+critic 둘 다에 (sim oracle) 35 20 24 · 20 · 21 · 23 · 27 · 28 · 22 · 22 · 26 · 27 574,280 26 / 23

결론(RESULTS_dsrl_v0.md): base 대비 개선 없음 — v0의 ep1000 80 %는 신선 seed에서 재현 안 됨(62 % / 54 %), 곡선은 55~65 % 주변 ±15 pp 체크포인트 노이즈. v3가 privileged state를 actor·critic 양쪽에 넣고도 base(35/50)를 못 넘어서 "관측이 병목"이라는 가설은 기각. oracle headroom은 있음(같은 state에서 8개 z 중 any-of-8 = 100 %, best-of-2 88 %, best-of-4 98 %) — 즉 문제는 후보가 아니라 critic이 후보를 못 고르는 것.

critic best-of-N probe (dsrl/probe_critic_bon.py, seeds 100–149, macro-step마다 z~N(0,I) 16개, actor 없이 critic argmax만):

ckpt critic argmax argmin (control) Q spread over 16 z
v3aobs @574280 (privileged) Q^A(s, π_dp(s,z)) 32/50 33/50 1.5 (
v3aobs @574280 (privileged) Q^W(s, z) 38/50 34/50 1.8
v0 @558480 (r18_ss) Q^A 30/50 23/50 1.5
v0 @558480 (r18_ss) Q^W 33/50 32/50 1.9

base(random z) 35/50. argmax ≈ argmin ≈ base, Q가 16개 후보 사이에서 ~1 %만 변함 → Q(s,·) ≈ V(s): critic이 z를 랭킹하지 못함. probe의 해석: frozen r18_ss feature엔 당근 위치 정보가 거의 없음(privileged를 줘도 안 되니 critic 레시피 자체 문제도 겹침). 결정: critic 레시피가 바뀌기 전엔 DSRL-NA 중단, 실물 DSRL은 argmax > argmin이 될 때까지 시도 안 함. 이 결과는 §3(c) BoN에도 그대로 적용됨 — sim에서 bon32가 bc보다 0~+22 pp 나오긴 하지만(§7, 9 run 중 2 run은 동률) 그 기제가 위치 인식은 아닐 수 있음.

파일: dsrl/<run>/params_<final>.pkl(165 MB; agent.base에 frozen base params 포함 → make_release 슬림 포맷 미적용), flags.json, train.csv, eval.jsonl(base ref 1행 + eval 11행), checkpoints.jsonl, episodes.jsonl, eval_<final>_det/{summary.json,summary.md,per_seed.csv}, eval_ref_base/, RUN.md(변형 플래그 + eval 곡선), dsrl/dsrl.json(sha256 매니페스트).

로딩: vision_carrot/dsrl/common.py::load_dsrl_run(run_dir, epoch)flags.json으로 DSRLNAAgent 재구성, run_flags['base_run_dir']/['base_epoch']에서 frozen base(ifql_sim_k0.9_s0/params_100000.pkl, 이 repo에 있음)를 다시 읽고 restore_agent. 서빙 dsrl/dsrl_server.py --run_dir <dsrl/dsrl_v0_s0> --epoch 558480 --sampler {base,dsrl_det,dsrl_sample} --port 5596(v3의 actor_use_critic_obs=True는 서버가 거부). base_run_dir는 학습 박스 절대경로라 다른 PC에선 flags.json의 그 필드를 다운로드 경로로 바꿔야 함.

12. 하이퍼파라미터 (전 run 공통, flags.json)

key frozen / aug px dsrl
kappa 0.8 / 0.9 / 0.93 / 0.95 0.9 (base 0.9)
discount 0.923 (= 0.99⁸, 청크 단위) 0.923 0.923
num_qs / q_agg / rho 2 / mean (_qmin만 min) / 0.0 2 / mean / 0.0 2 / mean / 0.0
flow_steps 10 10 10 (frozen base)
num_samples (BoN K) 32 32
lr / batch_size 3e-4 / 256 3e-4 / 256 critic 3e-4, actor 1e-4 / 256
MLP 512×4 (actor·value·critic) 512×4 + encoder 512×4
value_layer_norm / actor_layer_norm true / false true / false true / false
tau 0.005 0.005 0.005
use_critic_obs false false v0/v2 false, v3 true(+actor)
reward sim: stage(+absorb) · real: sparse(-1/step, 흡수 0) + absorb-tail real sparse + absorb-tail sim chunk-MDP
offline_steps 100,000 (save_interval sim 25k, real 25k·구 real 50k) 50,000 (12.5k) 100,000 + online 1000 ep
aug feat_aug_p 1.0 (aug8만) p_aug 1.0, strong_aug real

13. 실물 배포 상태 · 배포 절차 (carrot)

실물 eval 0건(09-15 HANDOFF "이 세션이 첫 실물 rollout" → 아직 안 돌림; eval_runs/*real* 없음). blocking: (1) start_poseifql_deploy.yaml이 fm_deploy의 [3.106, -1.817, 1.653, -1.618, -1.628, -3.195](j0 = +3.106)를 상속하는데 carrot 코퍼스의 episode-start 평균은 [-3.1632, -1.4899, 1.7266, -1.8469, -1.579, -3.2694](j0 = −3.16, −π convention) → 교체 필요. (2) 엔벨로프는 해결됨: joint_limits_lo = [-3.8051, -1.8059, 1.2622, -2.6921, -1.9311, -4.6439], hi = [-2.5783, -0.8822, 2.1445, -1.4393, -1.1990, -1.8221](carrot_real_limits.json 54 ep 1.2×) — §3(g)의 "carrot 실물 엔벨로프 미해결"은 09-16 기준 yaml에 반영됨. (3) real proprio가 이미지보다 ~200 ms 앞섬(잔여).

# 서버 — 추천 1순위
TORCH_HOME=$W/.cache/torch XLA_PYTHON_CLIENT_PREALLOCATE=false \
$W/.venv-svf/bin/python $FV/ifql_server.py \
    --run-dir <download>/ifql_real_k0.9_lead6_aug8_p1.0_s0 --step 100000 \
    --sampler bon --num-samples 32 --port 5595 --budget-s 0.6 \
    --norm-stats <download>/ifql_real_k0.9_lead6_aug8_p1.0_s0/norm_stats_r18_ss_real_lead6.json \
    --log-dir $W/eval_runs/real_$(date +%Y%m%d)/ifql_aug8_bon32
#   px: --run-dir <download>/px_real_impala_lead6_s0 --step 50000 --norm-stats <download>/px_real_impala_lead6_s0/norm_stats_real_px96_lead6.json
# 로봇 PC
ros2 launch gello_policy ur7e_diffusion_real.launch.py robot_ip:=192.168.10.11 start_mode:=gello \
    params_file:=$(ros2 pkg prefix gello_policy)/share/gello_policy/config/ifql_deploy.yaml act_host:=127.0.0.1 act_port:=5595 headless_mode:=true
  • ZMQ REP tcp://127.0.0.1:5595, 30 Hz, chunk 8 knots → 24 frames(0.8 s마다 재계획), §3(f).
  • ifql_deploy.yaml: act_timeout_s 0.8 < obs_timeout_s 0.9 < staleness_timeout_s 1.0(fm_deploy의 0.6/0.7/0.8에서 0.1 간격 유지하며 올림 — §3(e)의 0.6/0.7/0.8은 구값), max_dev_rad 0.5, 브릿지 max_step_rad 0.0025 @250 Hz = 0.625 rad/s 상한, auto_start_on_stream false, --budget-s < act_timeout_s, warmup 3회(refill > 300 ms 경고).
  • carrot j0는 −π convention(−3.70..−2.68) — orange(+2.40..+3.63)와 반대. 두 태스크 yaml을 섞어 쓰면 max_dev_rad 클램프가 모든 커맨드를 막음.
  • 프로토콜: 20 placement × {bc, bon32} paired, 순서 교대, 1 s dwell, 최대 20 s, McNemar.

14. 슬림 .infer.pkl numpy-only 추론 (frozen/aug 계열)

params_<step>.infer.pkl은 numpy 배열만 담고(§8 hf_release README) actor가 512×4 MLP라 JAX 없이 돌릴 수 있음. hf_release/infer_numpy.py가 완전한 구현이고 JAX agent와 대조 검증됨(같은 noise 입력, ifql_sim_k0.9_s0_qmin@100k: actions max|Δ| 4.6e-7, Q(min agg) max|Δ| 1.6e-4; orange aug8: 2.5e-7 / 1.0e-6, argmax 일치). 핵심만:

import pickle, json, numpy as np
run = "ifql_real_k0.9_lead6_aug8_p1.0_s0"
P  = pickle.load(open(f"{run}/params_100000.infer.pkl", "rb"))["agent"]["network"]["params"]
ns = json.load(open(f"{run}/norm_stats_r18_ss_real_lead6.json")); cfg = json.load(open(f"{run}/flags.json"))["agent"]
gelu = lambda x: 0.5*x*(1+np.tanh(np.sqrt(2/np.pi)*(x+0.044715*x**3)))                       # flax nn.gelu
def ln(x, s, b): m=x.mean(-1,keepdims=True); v=((x-m)**2).mean(-1,keepdims=True); return (x-m)/np.sqrt(v+1e-6)*s+b
def actor_v(inp):                                                    # modules_actor_flow/mlp: layer_0..4, LN 없음
    p = P["modules_actor_flow"]["mlp"]; x = inp
    for i in range(5):
        x = x @ p[f"layer_{i}"]["kernel"] + p[f"layer_{i}"]["bias"]
        if i < 4: x = gelu(x)
    return x
def critic_q(obs, act):                                              # modules_critic/value_net: (2,in,out) 앙상블 + LN
    p = P["modules_critic"]["value_net"]; x = np.concatenate([obs, act], -1)[None]
    for i in range(5):
        x = np.einsum("ebi,eio->ebo", np.broadcast_to(x, (2,)+x.shape[1:]), p[f"layer_{i}"]["kernel"]) + p[f"layer_{i}"]["bias"][:, None]
        if i < 4: x = ln(gelu(x), p[f"layer_norm_{i}"]["scale"][:, None], p[f"layer_norm_{i}"]["bias"][:, None])
    qs = x[..., 0]; return qs.min(0) if cfg["q_agg"] == "min" else qs.mean(0) - cfg["rho"]*qs.std(0)
def sample(obs2055, K=32, seed=0):                                   # == agent.sample_candidates + argmax; K=1 == bc
    obs = np.broadcast_to(obs2055.astype(np.float32)[None], (K, 2055)); x = np.random.default_rng(seed).standard_normal((K, 56)).astype(np.float32)
    for i in range(cfg["flow_steps"]):
        x = x + actor_v(np.concatenate([obs, x, np.full((K,1), i/cfg["flow_steps"], np.float32)], -1)) / cfg["flow_steps"]
    x = np.clip(x, -1, 1); q = critic_q(obs, x); j = int(q.argmax())
    lo, hi = np.array(ns["action"]["lo"]), np.array(ns["action"]["hi"])
    knots = lo + (x[j].reshape(8, 7) + 1)/2*(hi - lo); knots[:, 6] = np.clip(knots[:, 6], 0, 1)
    return knots, q                                                  # (8,7) 절대 관절 rad ×6 + grip 0..1

obs2055(frozen ResNet18 spatial-softmax feature + 표준화, §3(a))는 torchvision이 필요 — load_example.py가 서버와 같은 코드로 만듦. full 체크포인트 로딩(JAX)은 §3(d), px는 §10. 서버(ifql_server.py)는 슬림 파일이 아니라 full params_<step>.pkl을 읽음.

15. 재현 (데이터 빌드 → 학습)

# real 데이터 (carrot): HF 스냅샷 68b1250d…, val 10,25,40,53   (FV/real_pipeline/build_real.sh, TASK 기본값 carrot)
cd $W && ACTION_LEAD=6 env -u PYTHONPATH bash $FV/real_pipeline/build_real.sh                       # real_demo_r18_ss_lead6{,_val}.npz
cd $W && AUG_K=8 STEPS=features env -u PYTHONPATH bash $FV/real_pipeline/build_real.sh            # 8 view feature 추출 (features_real_aug8/)
cd $W && ACTION_LEAD=6 AUG_K=8 STEPS=build,load env -u PYTHONPATH bash $FV/real_pipeline/build_real.sh   # real_demo_r18_ss_lead6_aug8{,_val}.npz
#   내부: build_dataset.py --variant r18_ss --features ... --decoded ... --out $W/data/rl --reward sparse --no-critic --absorb-tail
#         --val-episodes 10,25,40,53 --action-lead 6 --tag real_lead6 --aug-k 8
# px96: decode_px.py / build_px_dataset.py (--action-lead 6 → real_demo_px96_lead6.npz, norm_stats_real_px96_lead6.json)   ← FV/PIXEL_PIPELINE.md

# 학습 (kanu, run_ifql.sh <npz> <seed> <steps> <run> [--agent.*]; 고정 플래그 --online_steps 0 --buffer_size 1 --eval_interval 0
#       --log_interval 500 --save_interval 25000 --agent.flow_steps=10 --agent.num_samples=32 --agent.rho=0.0 --agent.actor_layer_norm=False)
L03 agents/ifql.py real_demo_r18_ss_lead6.npz 0 100000 ifql_real_lead6_k0.9_s0 --agent.kappa=0.9 --agent.num_qs=2 --agent.q_agg=mean --agent.discount=0.923 --agent.value_layer_norm=True
A05 agents/ifql.py real_demo_r18_ss_lead6_aug8.npz 0 100000 ifql_real_k0.9_lead6_aug8_p1.0_s0 <같은 5 flags> --agent.use_critic_obs=False --feat_aug_p=1.0
#   px (bin/launch_px_stage2.sh, 1 job/GPU): ... real_demo_px96_lead6.npz 0 50000 px_real_impala_lead6_s0 <같은 flags> --agent.encoder=multicam_impala_small --p_aug=1.0 --strong_aug=real
# DSRL: bash FV/dsrl/run_dsrl.sh dsrl_v0_s0 0 --offline_steps 100000 --online_episodes 1000 --utd 20   (v2: --agent.noise_scale=0.75, v3: --agent.actor_use_critic_obs=True --agent.use_critic_obs=True)

코드 스냅샷: code/carrot_ifql_code_20260916.tar.gz sha256 f3f67aa7e5b377a7786f765e3e8c3f93c92a9daec3e7e8ec762f23ab13d5fb45(09-16 트리: dsrl/, stage_extra.py, infer_numpy.py, orange 배포 파일 포함). 09-15 타르볼(e8d36c6c…)도 그대로 둠.

16. 영상 (videos/)

ifql_sim_k0.9_s0@100k, sampler bc(K=1), held-out seed 100–149 eval(34/50 = 68 %)에서 8 episode를 kinematic replay(eval/render_eval_h5.py, 기록된 ep_<seed>.h5 125 Hz를 mj_forward로 재생 — 정책을 다시 돌린 게 아님). cam1(고정) | cam2(손목) 640×360 나란히, 30 fps, 실시간, 오버레이 seed/t=/결과.

file 결과 길이
ep_101_success.mp4, ep_111_success.mp4, ep_125_success.mp4, ep_146_success.mp4 SUCCESS (dwell 성립) 8.87 / 8.00 / 12.93 / 16.33 s
ep_100_timeout.mp4, ep_107_timeout.mp4, ep_122_timeout.mp4, ep_129_timeout.mp4 TIMEOUT (600 step = 20 s, success latch 없음; 122는 당근이 냄비 안인데 vz=+0.061 > max_vz 0.05) 20.00 s

주의: 렌더 카메라(640×360)는 정책 입력(1280×720 JPEG 스트림)과 다르고, 장면 XML SHA가 cosmetic하게 다름. 상세 videos/README.md.

Downloads last month

-

Downloads are not tracked for this model. How to track
Video Preview
loading

Datasets used to train Bigenlight/carrot-in-pot-ifql