ONNX 는 모델이 아니라 포장 규격이다 — Xenova 는 BERT 를 만든 게 아니다
누가 학습시켰나와 어떤 상자에 담아 배포하나는 따로 논다
ONNX 는 모델이 아니다. 계산 순서(그래프)와 가중치를 한 파일에 담는 포장 규격이다. 누가 모델을 학습시켰는지와 어떤 포장으로 배포하는지는 따로 논다. 이걸 알면 Hugging Face 에서 「이 모델 브라우저에서 돌아가?」를 저장소 이름이 아니라 파일 목록으로 판단할 수 있다.
계기 — 한국어 BERT 를 못 넣었다
BERT 계열 모델 6 개를 브라우저(transformers.js)로 돌리는 실험 앱을 만들었다. 쓸 수 있는 건 Xenova/... 같은 ONNX 변환본뿐이었다. 한국어 BERT 인 klue/bert-base 는 Hugging Face API 로 봤을 때 .onnx 파일이 0 개라 넣지 못했다(2026-09-19 확인).
그러다 나온 질문이 「사전학습 · 파인튜닝은 Xenova 가 한 건가?」였다. 헷갈리는 게 당연하다. 저장소 이름에 만든 사람과 포장한 사람이 섞여 있으니까.
저장소 이름 하나를 풀어 보면
Xenova/bert-base-multilingual-uncased-sentiment 는 네 단계를 거쳤다.
학습은 앞의 두 단계에서 끝났다. Google 이 104 개 언어 위키백과로 사전학습했고, NLP Town 이 상품 리뷰 별점으로 파인튜닝했다. Xenova 는 같은 가중치를 ONNX 상자에 옮겨 담고 양자화(q8, fp16 처럼 숫자를 더 작은 형식으로 줄이는 것)한 버전을 같이 올렸다. transformers.js 는 그걸 ONNX Runtime(WASM 또는 WebGPU)으로 돌린다.
PyTorch 체크포인트와 비교하면
PyTorch 체크포인트 (.bin · .safetensors) |
ONNX (.onnx) |
|
|---|---|---|
| 안에 든 것 | 가중치(숫자)만 | 계산 그래프 + 가중치 |
| 돌리려면 | Python + 모델 클래스 코드(transformers 라이브러리) | ONNX Runtime 만 있으면 됨 |
| 어디서 도나 | 주로 Python 서버 | 브라우저(WASM · WebGPU), C#, Java, 모바일 등 |
| 겪은 제약 | — | 변환할 때 정한 출력만 나온다. 층별 hidden state · attention 값은 밖에서 못 봤다 |
마지막 줄이 실제로 아팠다. 실험 앱에서 「문장이 인코더 안에서 어떻게 바뀌나」를 보여 주고 싶었는데, ONNX 변환본은 마지막 층 출력만 내놓았다. 포장할 때 문을 몇 개 낼지 정해 버리는 셈이다.
파일 하나하나도 역할이 갈린다.
| 파일 | 역할 | 어느 단계의 결과물 |
|---|---|---|
tokenizer.json |
어휘 목록 | 사전학습 데이터로 만든 어휘 |
config.json |
층 수 · 차원 · 헤드 수 = 설계도의 숫자 | 모델 설계 |
onnx/model_*.onnx |
가중치 + 계산 그래프 | 사전학습(+파인튜닝) 결과 |
직접 재 본 숫자 (2026-09-19, macOS · Chrome · WebGPU shader-f16 사용 가능)
Xenova/paraphrase-multilingual-MiniLM-L12-v2 의 파일 크기는 model_fp16.onnx 235MB, model_quantized.onnx 118MB, tokenizer.json 17MB 였다. 같은 가중치인데 포장 방식에 따라 두 배 차이가 난다.
이 MiniLM 으로 문서 조각 372 개를 임베딩해 봤다. WebGPU + fp16 은 첫 다운로드까지 포함해 약 21 초. WASM + q8 은 단일 스레드(crossOriginIsolated=false)라 1 분이 지나도 25% 쯤이었다. 끝까지는 재지 않았다.
몇 가지 더 걸린 것.
npm 패키지가 무겁다. @huggingface/transformers 4.3.0 을 npm 으로 넣으면 onnxruntime-node(node_modules 에서 289MB)와 sharp 가 딸려 온다. 브라우저에서만 쓸 거라 CDN(jsDelivr) 버전 고정 import 로 바꿨다.
파일이 디스크에 이름으로 안 생긴다. 브라우저는 받은 파일을 Cache Storage transformers-cache 에 요청 URL 을 키로 저장한다. config.json 같은 파일을 찾아도 안 나온다.
그래서 모델을 고를 때
브라우저에서 돌릴 모델이면 저장소의 Files 탭에 onnx/ 폴더가 있는지부터 본다. 없으면 직접 변환해야 한다. 학습한 곳과 포장한 곳이 다를 수 있으니, 라이선스와 학습 데이터는 원래 저장소(위 예에선 nlptown/...)의 모델 카드에서 확인한다.
모델을 브라우저에서 돌릴지 서버에서 돌릴지, 쓸 모델에 변환본이 없을 때 어떻게 할지 막혀 있다면 negabaro 에 물어봐도 된다. 위 실험을 직접 굴려 본 경험으로 같이 볼 수 있다.
핵심 개념
저장소 이름의 앞부분은 「포장한 사람」일 수 있다 — 원래 모델은 모델 카드에서 찾는다
브라우저용이면 Files 탭에서 onnx/ 폴더부터 확인한다
같은 가중치라도 fp16 / q8 에 따라 크기가 두 배 다르다 — 속도 · 정확도와 맞바꾼다
층 안쪽 값이 필요하면 변환할 때 출력을 지정해야 한다 — 기본 변환본엔 마지막 층만 있다