RTX 40/50 시리즈에서 안 돌아가는 IsaacGym 정책, IsaacLab으로 포팅하기

IsaacGym ONNX 모델을 IsaacLab에서 추가학습하기

IsaacGym에서 학습된 정책을 ONNX로 내보낸 후, 원본 .pt 파일이 없을 때
ONNX → PyTorch 역변환 → IsaacLab fine-tuning 까지 이어가는 전체 과정을 정리합니다.

G1 휴머노이드 로봇 + RSL-RL 기준이지만, 네트워크 구조만 맞으면 다른 모델에도 적용 가능합니다.


목차

  1. 사전 확인: .pt 체크포인트가 있나요?
  2. 필요한 파일 준비
  3. ONNX → PyTorch 역변환
  4. 체크포인트 정리 (pickle 제거)
  5. train_new.py 수정 (pretrain 로직)
  6. IsaacLab에서 첫 학습 실행
  7. 이후 추가학습 (resume)
  8. 주의사항 & 트러블슈팅

0. 사전 확인: .pt 체크포인트가 있나요?

ONNX 역변환 작업에 들어가기 전에, 먼저 원본 .pt 파일이 남아있는지 확인하세요.
IsaacGym으로 학습하면 기본적으로 runs/ 또는 logs/ 폴더 안에 체크포인트가 자동 저장됩니다.

# 체크포인트 위치 확인
ls runs/<experiment_name>/
# 또는
ls logs/<experiment_name>/
# model_XXXXX.pt 파일이 있으면 그걸 바로 사용하세요
상황 난이도 방법
.pt 체크포인트가 있다 :white_check_mark: 쉬움 바로 5단계
ONNX 파일만 있다 :warning: 중간~어려움 아래 절차 전체를 따라가세요

1. 필요한 파일 준비

역변환을 위해 아래 두 파일이 반드시 필요합니다.

  • policy.onnx — IsaacGym에서 내보낸 ONNX 정책 파일
  • actor_critic_future.pyActorFuture / ActorCriticFuture 클래스가 정의된 파일
    (save_onnx.py의 import 경로를 참고해서 찾으세요)

왜 네트워크 클래스가 필요한가?
ONNX 파일 자체에 weight는 있지만, PyTorch state_dict 로딩을 위해서는 원래 클래스 구조와 키 이름이 일치해야 합니다. save_onnx.py에서 사용한 클래스를 그대로 가져오세요.

:warning: critic 없음 주의
ONNX export는 보통 actor만 내보냅니다. critic 네트워크는 복원 불가하므로, fine-tuning 시 랜덤 초기화된 critic에서 시작하게 됩니다. 초반에 value loss가 높은 것은 정상입니다.


2. ONNX → PyTorch 역변환

ONNX 모델에서 weight를 직접 추출해 원래 PyTorch 클래스 구조에 맞게 매핑합니다.
onnx2torch 같은 범용 변환 라이브러리는 레이어 이름이 달라져 RSL-RL과 호환이 안 될 수 있으므로, 직접 매핑 방식을 사용합니다.

2-1. ONNX weight 구조 확인

import onnx
import numpy as np

model = onnx.load("policy.onnx")
# initializer에 weight 이름과 shape이 모두 있습니다
for init in model.graph.initializer:
    arr = np.array(init.float_data or
                   np.frombuffer(init.raw_data, dtype=np.float32))
    print(init.name, arr.shape)

출력된 키 이름이 원래 PyTorch 모델의 state_dict 키와 일치하는지 확인하세요.
ONNX 변환 시 이름이 그대로 보존된 경우, 별도 매핑 없이 바로 로드할 수 있습니다.

2-2. weight 추출 및 .pt 저장

import onnx
import numpy as np
import torch

# 1. ONNX에서 weight 추출
model = onnx.load("policy.onnx")
weights = {}
for init in model.graph.initializer:
    arr = np.frombuffer(init.raw_data, dtype=np.float32).copy()
    weights[init.name] = torch.tensor(arr.reshape(
        list(init.dims) if init.dims else [-1]))

# 2. normalizer 파라미터 분리 (키 이름에 normalizer가 포함된 것)
normalizer_mean    = weights["actor.normalizer.mean"]    # 키 이름은 확인 후 수정
normalizer_divisor = weights["actor.normalizer.divisor"]

# 3. model_state_dict 구성
state_dict = {k: v for k, v in weights.items()}

# 4. 저장
torch.save({
    "model_state_dict": state_dict,
    "normalizer": {
        "mean":    normalizer_mean,
        "divisor": normalizer_divisor,
    },
    "config": {
        "num_observations": 1432,  # 본인 환경에 맞게 수정
        "num_actions": 23,
    }
}, "policy_restored.pt")

2-3. 복원 결과 검증

ONNX 출력과 복원된 PyTorch 모델의 출력을 비교합니다. 최대 오차가 1e-5 이하면 성공입니다.

import onnxruntime as ort

session = ort.InferenceSession("policy.onnx")
test_input = torch.randn(1, 1432)  # num_obs에 맞게

# ONNX 추론
onnx_out = session.run(None, {"obs": test_input.numpy()})[0]

# PyTorch 추론
model.eval()
with torch.no_grad():
    pt_out = model(test_input).numpy()

max_err = np.abs(onnx_out - pt_out).max()
print(f"최대 오차: {max_err:.8f}")  # 1e-5 이하면 성공

3. 체크포인트 정리 (pickle 제거)

변환 과정에서 생성된 .pt 파일에 클래스 정의가 pickle로 포함되어 있으면,
다른 환경에서 로드 시 ModuleNotFoundError가 발생합니다. 순수 텐서만 남기도록 다시 저장합니다.

import torch

# 기존 체크포인트 로드
ckpt = torch.load("policy_restored.pt", map_location="cpu", weights_only=False)

# state_dict를 순수 텐서 딕셔너리로 변환
clean_state_dict = {}
for k, v in ckpt["model_state_dict"].items():
    clean_state_dict[k] = v.detach().clone()

# 재저장 (weights_only 호환 형식)
torch.save({
    "model_state_dict": clean_state_dict,
    "actor_state_dict": {k.replace("actor.", ""): v
                         for k, v in clean_state_dict.items()
                         if k.startswith("actor.")},
    "normalizer_mean":    ckpt["normalizer"]["mean"].detach().clone(),
    "normalizer_divisor": ckpt["normalizer"]["divisor"].detach().clone(),
    "config": ckpt["config"],
}, "policy_clean.pt")

print("저장 완료: policy_clean.pt")

저장된 파일에는 다음 키가 있어야 합니다: model_state_dict, actor_state_dict, normalizer_mean, normalizer_divisor, config


4. train_new.py 수정 (pretrain 로직)

IsaacLab의 train 스크립트에 --pretrain 옵션을 추가합니다.
RSL-RL 버전에 따라 policy 속성명이 다를 수 있으므로, 자동 탐색 로직을 사용합니다.

4-1. argument parser에 옵션 추가

# train_new.py의 argparse 부분에 추가
parser.add_argument("--pretrain", type=str, default=None,
                    help="복원된 .pt 체크포인트 경로")

4-2. runner 생성 후 pretrain 로직 삽입

runner = OnPolicyRunner(...) 다음에 아래 코드를 삽입합니다.

if args_cli.pretrain:
    print(f"[INFO]: Loading pretrain weights from: {args_cli.pretrain}")
    ckpt = torch.load(args_cli.pretrain, map_location="cpu", weights_only=False)

    # RSL-RL 버전별 policy 속성명 자동 탐색
    # v2.x → runner.alg.actor_critic
    # v3.x → runner.alg.policy
    policy = None
    for attr in ["policy", "actor_critic", "actor"]:
        if hasattr(runner.alg, attr):
            policy = getattr(runner.alg, attr)
            print(f"[INFO]: Found policy at runner.alg.{attr}")
            break

    if policy is None:
        print(f"[WARN]: runner.alg attributes: {dir(runner.alg)}")
        raise RuntimeError("policy 속성을 찾을 수 없습니다")

    # Format A: model_state_dict가 있는 경우 (전체 로드)
    if "model_state_dict" in ckpt:
        missing, unexpected = policy.load_state_dict(
            ckpt["model_state_dict"], strict=False)
        print(f"[INFO]: Loaded model_state_dict")
        print(f"  missing:    {missing}")
        print(f"  unexpected: {unexpected}")

    # Format B: actor_state_dict만 있는 경우 (actor만 로드)
    elif "actor_state_dict" in ckpt:
        missing, unexpected = policy.actor.load_state_dict(
            ckpt["actor_state_dict"], strict=False)
        print(f"[INFO]: Loaded actor_state_dict only")

    # Format C: 그 외 (strict=False로 시도)
    else:
        policy.load_state_dict(ckpt, strict=False)
        print(f"[INFO]: Loaded raw checkpoint (strict=False)")

    # Normalizer 복원
    if "normalizer_mean" in ckpt:
        for attr in ["actor_obs_normalizer", "obs_normalizer", "normalizer"]:
            if hasattr(policy, attr):
                norm = getattr(policy, attr)
                norm.running_mean.data.copy_(ckpt["normalizer_mean"])
                norm.running_var.data.copy_(ckpt["normalizer_divisor"] ** 2)
                print(f"[INFO]: Restored normalizer at policy.{attr}")
                break

디버그 출력 확인
missing keys에 critic 관련 키가 있는 것은 정상입니다.
actor 관련 키가 missing에 있다면 키 이름 불일치 문제이므로, state_dict 키를 직접 출력해서 비교하세요.


5. IsaacLab에서 첫 학습 실행

일반 학습 (헤드리스, 빠름)

python train_new.py \
    --task Isaac-Velocity-Rough-G1-v0 \
    --pretrain /path/to/policy_clean.pt \
    --num_envs 4096 \
    --max_iterations 5000 \
    --exptid g1_finetune_v1

시각적 확인 (렌더링 ON)

# --debug 옵션: 환경 4개 + 렌더링 ON
python train_new.py \
    --task Isaac-Velocity-Rough-G1-v0 \
    --pretrain /path/to/policy_clean.pt \
    --debug

권장 PPO 하이퍼파라미터 (fine-tuning용)

파라미터 권장값 이유
learning_rate 1e-5 기본값보다 낮게 — actor 보존
clip_param 0.1 기본 0.2에서 낮춰 급격한 변화 방지
desired_kl 0.005 기본 0.01에서 낮춤
critic_learning_rate 3e-4 critic은 높게 — 빠른 수렴

6. 이후 추가학습 (resume)

첫 학습이 끝난 후, 저장된 체크포인트에서 이어서 학습하려면
--pretrain 대신 --resume을 사용합니다.

pretrain (1회) → model_5000.pt 저장 → resume (반복) → optimizer + iter 복원
# 체크포인트 저장 위치 확인
ls logs/rsl_rl/g1_finetune_v1/

# resume으로 추가학습
python train_new.py \
    --task Isaac-Velocity-Rough-G1-v0 \
    --resume True \
    --load_run g1_finetune_v1 \
    --load_checkpoint model_5000.pt \
    --max_iterations 10000

--pretrain vs --resume 차이

  • --pretrain: model weight만 복원, optimizer state와 iteration은 처음부터 시작
  • --resume: model weight + optimizer momentum + iteration 카운터 모두 복원. 완전히 이어서 학습됨

7. 주의사항 & 트러블슈팅

AttributeError: 'PPO' object has no attribute 'actor_critic'

RSL-RL 버전에 따라 속성명이 다릅니다. 위의 자동 탐색 코드를 사용하거나, 직접 확인하세요.

# RSL-RL 버전별 속성명
# v2.x: runner.alg.actor_critic
# v3.x: runner.alg.policy
print(dir(runner.alg))  # 실제 속성 목록 확인

Observation 차원 불일치

IsaacGym과 IsaacLab 환경의 obs 구성이 다를 수 있습니다. 반드시 맞추세요.

# ONNX 기준 (예시)
# motion(35) + priop(92) + history(127×10) + future(35) = 1432

# IsaacLab 환경의 obs 수 확인
print(env.observation_space)
print(agent_cfg.num_observations)

초반에 value loss가 매우 높음

critic이 랜덤 초기화이므로 정상입니다. 보통 500~1000 iteration 이내에 안정화됩니다.

Normalizer 복원 검증

print("mean:", policy.actor_obs_normalizer.running_mean[:5])
print("var:",  policy.actor_obs_normalizer.running_var[:5])
# 모두 0이면 복원 실패 → normalizer 속성명을 다시 확인

체크포인트에 pickle된 클래스가 포함된 경우

저장된 .pt 파일에 pickle된 클래스가 포함되어 있으면 다른 환경에서 로드 시 오류가 납니다.
3단계의 정리 과정을 반드시 거치세요.


IsaacGym (Legged Gym / RSL-RL) + IsaacLab 환경 기준 · G1 humanoid robot
환경 구성이나 네트워크 구조가 다르면 obs 차원 및 키 이름을 본인 환경에 맞게 수정하세요.

1개의 좋아요