LabHub
배우기 러닝패스 코스

AI 다이어트 실패 사건 · 내보내기에서 굳는 것들 · 이론

내보내는 순간 굳는 것들 — opset · IR · initializer

LabHub 에서 이어서 보기

한 줄 요약

ONNX 파일은 그래프만 담고 있는 것이 아니라 어느 판(opset)으로 썼는지, 어떤 IR 버전으로 적었는지, 무엇이 입력이고 무엇이 이미 정해진 값인지를 함께 굳혀 놓는다. 이 네 가지는 내보내는 순간 정해지고, 받는 쪽은 그 결정을 바꿀 수 없다.

왜 이게 필요했나

양자화가 실패했다는 신고는 대개 변환 명령에서 시작하지 않는다. "우리 서버에서는 이 파일이 안 열립니다" 로 시작한다. 파일은 멀쩡해 보이고, onnx.checker 도 아무 말 없이 통과시킨다. 그런데 런타임은 세션을 여는 데서 멈춘다.

이런 자리에서 시간을 가장 많이 잡아먹는 것은 원인을 파일 밖에서 찾는 일이다. 변환 옵션을 바꿔 보고, 런타임 판을 올려 보고, 하드웨어를 의심한다. 사실은 파일 첫머리 몇 바이트에 답이 적혀 있다.

파일에 굳는 것 네 가지

첫째, 연산자 집합 판과 도메인. 모델은 opset_import 에 도메인마다 판 번호를 적어 둔다. 도메인이 빈 문자열이면 표준 연산자(ai.onnx)이고, com.microsoft 같은 다른 도메인은 특정 런타임의 확장이다. 판 번호는 "이 파일 안의 연산자를 어느 판의 정의로 읽어라" 는 지시이지 성능 설정이 아니다.

둘째, IR 버전. 그래프를 담는 그릇의 형식 판이다. [ONNX Versioning](https://onnx.ai/onnx/repo-docs/Versioning.html) 이 opset 과 IR 을 따로 매기는 이유가 여기 있다 — 연산자 정의가 바뀌는 속도와 파일 형식이 바뀌는 속도가 다르다. 실제로 IR 4 이전에는 initializer 를 반드시 graph.input 에도 함께 선언해야 했고, 그 뒤로는 그러지 않아도 된다. 그래서 낡은 도구가 만든 파일에는 지금도 가중치가 입력 목록에 함께 적혀 있다.

셋째, producer 정보. producer_nameproducer_version 은 누가 이 파일을 만들었는지 남긴다. 아무 기능도 하지 않지만, 사고가 났을 때 가장 먼저 보는 줄이다. 비어 있는 파일이 오면 그것부터 문제다.

넷째, initializer 와 입력의 경계. 가중치는 graph.initializer 에 들어가고, 실행할 때 사람이 넣어 주는 값은 graph.input 에 남는다. 둘 다에 이름이 적혀 있으면 그것은 "기본값이 있는 입력" 이라는 뜻이고, 런타임은 그 이름을 필수 입력으로 요구하지 않는다. 그래서 파일만 보고 입력 개수를 세면 틀린다 — [ONNX Concepts](https://onnx.ai/onnx/intro/concepts.html) 가 이 둘을 갈라 설명한다.

검사기가 잡는 것과 못 잡는 것

onnx.checker.check_model(model) 은 기본적으로 구조를 본다. 노드가 위상 순서로 놓였는지, 가리키는 이름이 존재하는지, 필수 필드가 있는지. 여기에 full_check=True 를 주면 모양 추론까지 돌린다. 이 차이가 실무에서 크다. 곱셈이 성립하지 않는 모양의 MatMul 은 기본 검사를 그대로 통과하고, full_check 에서야 걸린다.

그리고 두 검사 모두 잡지 못하는 것이 있다. 등록되지 않은 도메인의 연산자는 검사기가 통과시킨다 — 검사기는 모르는 도메인을 "누군가의 확장" 으로 보고 지나가기 때문이다. 판 번호를 낮춰 찍은 파일도 마찬가지로 통과한다. 두 경우 모두 런타임에서 멈춘다.

onnx.checker(기본)      구조만onnx.checker(full)      구조 + 모양 추론onnxruntime 세션 열기    구조 + 모양 + 이 런타임에 그 커널이 있는가

세 번째 줄이 가장 좁고, 배포가 실제로 요구하는 것은 세 번째 줄이다.

현장에서 만나는 모습

첫째, "판을 낮춰 주세요". 받는 쪽 런타임이 낡았다고 opset 번호만 낮춰 다시 저장하는 일이 흔하다. 그러나 판 번호는 도장일 뿐이라 연산자 정의가 따라 내려가지 않는다. 낮춘 판에 그 연산자의 정의가 없으면 검사기는 통과시키고 런타임이 거절한다. 오류 문구도 "판이 낮다" 가 아니라 "이 연산자의 구현을 찾을 수 없다" 로 나와서 원인이 안 보인다.

둘째, 내보내는 쪽이 쓸 수 있는 판이 돌리는 쪽이 여는 판보다 넓다. 라이브러리는 최신 판으로 쓸 수 있는데 런타임은 아직 그 판을 열지 않는다. [ONNX Runtime Compatibility](https://onnxruntime.ai/docs/reference/compatibility.html) 가 런타임 판마다 여는 opset 범위를 표로 적어 두는 이유다. 그래서 "최신 판으로 내보내라" 는 조언은 그 자체로 위험하다.

셋째, 가중치가 입력으로 보인다. 낡은 도구가 만든 파일을 열면 입력이 다섯 개로 보이는데 런타임은 하나만 달라고 한다. 이 차이를 모르고 입력 다섯 개를 채워 넣는 코드를 쓰면, 그 코드는 가중치를 매 호출마다 새로 밀어 넣는다.

넷째, 검사기 통과를 배포 근거로 쓴다. "checker 통과" 를 릴리스 조건에 넣어 둔 팀이 많다. 그 조건은 필요조건이지 충분조건이 아니다. 배포 게이트에는 받는 쪽과 같은 런타임 판으로 세션을 실제로 열어 보는 단계가 들어가야 한다.

실무에서 진짜 중요한 것

다음 실습에서 할 것

onnx.helper 로 두 층짜리 MLP 를 직접 짓고, 그 파일에서 굳어 있는 것들을 읽어 내는 도구 modelmeta.py 를 한 단계씩 키운다. initializer 를 입력으로도 선언한 낡은 형식의 파일을 만나 입력과 가중치를 갈라 보고, 기본 검사와 full_check 의 차이를 직접 재고, 판 번호만 갈아 끼우며 런타임이 여는 범위의 하한과 상한을 찾는다. 채점기는 매번 다른 모양·이름·판 번호·활성 함수로 자기 파일을 지어 여러분의 도구를 실제로 돌리고 답을 대조한다.