
HyperCLOVA 모델을 로컬에서 더 효율적으로 돌려보고 싶다는 생각이 들었다. 특히 safetensors 형태로 제공되는 모델을 ONNX로 변환해두면 CPU나 다양한 환경에서 공통적으로 추론을 돌릴 수 있고, 양자화까지 진행하면 모델 크기나 속도 측면에서 이득이 크다. 그래서 이번 글에서는
ONNX 소개 → 변환이 필요한 이유 → safetensors 기반 모델을 ONNX로 변환하는 방식 → HyperCLOVA 모델 변환 과정
순서로 정리해본다.
ONNX(Open Neural Network Exchange)는 여러 딥러닝 프레임워크(PyTorch, TensorFlow 등)를 하나의 통일된 표현 형식으로 묶는 모델 포맷이다.
ONNX를 사용하는 목적은 다음과 같다.
HyperCLOVA처럼 크지 않은 모델을 다루는 경우 GPU가 없더라도 CPU 기반 최적화를 통해 운영할 수 있다는 점에서 ONNX는 실용성이 높다.
HyperCLOVA 모델은 대체로 safetensors 포맷으로 제공된다. safetensors는 안전성과 속도 측면에서 뛰어난 포맷이지만, ONNX 런타임에서 직접 사용할 수 없기 때문에 중간 과정으로 PyTorch 모델 형태로 불러온 뒤 ONNX로 변환하는 절차가 필요하다.
일반적인 변환 흐름은 다음과 같다.
1) safetensors → PyTorch 모델 로드
2) PyTorch 모델 → ONNX(FP32) 변환
3) 변환된 FP32 ONNX를 기준(baseline) 모델로 사용
4) 필요에 따라 Dynamic Quantization 또는 Static Quantization으로 INT8 버전 생성
이렇게 하면 고정밀 모델과 경량 모델을 모두 확보할 수 있다.
HyperCLOVA처럼 Transformer 계열 구조를 가진 모델은 ONNX 변환을 두 가지 방식으로 진행할 수 있다.
Hugging Face transformers 기반 모델이라면 가장 안정적인 방식이다.
from optimum.exporters.onnx import export, OnnxConfig
from transformers import AutoTokenizer, AutoModel
model_path = "./hyperclova"
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModel.from_pretrained(model_path)
model.eval()
onnx_config = OnnxConfig.from_model_config(model.config)
export(
model=model,
config=onnx_config,
output="hyperclova_fp32.onnx",
)
이 방식을 사용하면:
dynamic axes 설정을 자동으로 처리할 수 있다
모델 구조를 자동으로 분석한다
최신 opset 적용도 수월하다
커스텀 구조나 지원되지 않는 모델인 경우 직접 변환해야 한다.
import torch
dummy_input = torch.randint(0, 1000, (1, 16))
torch.onnx.export(
model,
(dummy_input,),
"hyperclova_fp32.onnx",
opset_version=17,
input_names=["input_ids"],
output_names=["logits"],
dynamic_axes={
"input_ids": {0: "batch", 1: "sequence"},
"logits": {0: "batch", 1: "sequence"},
}
)
여기서 중요한 점은 다음과 같다.
model.eval() 호출이 필요하다
dynamic axes를 정확하게 정의해야 한다
opset version은 16 이상이 안정적이다
FP32 ONNX 모델은 성능 기준 모델(baseline)로 사용하고, 추가로 INT8 버전을 만들어두면 CPU에서 속도 개선이 크다.
가장 간단한 방식은 Dynamic Quantization이다.
from onnxruntime.quantization import quantize_dynamic, QuantType
quantize_dynamic(
model_input="hyperclova_fp32.onnx",
model_output="hyperclova_int8_dynamic.onnx",
weight_type=QuantType.QInt8,
optimize_model=True
)
Dynamic 방식은 별도 calibration 데이터가 필요 없고, 정확도 손실이 상대적으로 적다.
Static Quantization은 calibration 데이터셋이 필요하지만, 메모리 절약 효과가 크다.
from onnxruntime.quantization import quantize_static, CalibrationDataReader, QuantType
class DataReader(CalibrationDataReader):
def __init__(self, dataloader):
self.dataloader = iter(dataloader)
def get_next(self):
try:
batch = next(self.dataloader)
except StopIteration:
return None
return {"input_ids": batch["input_ids"].numpy()}
quantize_static(
model_input="hyperclova_fp32.onnx",
model_output="hyperclova_int8_static.onnx",
calibration_data_reader=DataReader(dataloader),
weight_type=QuantType.QInt8,
activation_type=QuantType.QInt8,
)
Static 방식은 calibration 품질에 따라 정확도 영향이 달라지기 때문에 평가가 필요하다.
HyperCLOVA 관련 모델을 safetensors에서 ONNX로 변환할 때의 핵심 흐름은 다음과 같다.
safetensors 로드 → PyTorch 모델로 변환
optimum 또는 torch.onnx.export를 사용해 FP32 ONNX 생성
이 ONNX 모델을 baseline으로 활용
필요에 따라 dynamic / static quantization 수행
FP32와 INT8 모델의 정확도와 추론 속도 비교
운영 목적에 맞는 모델 선택
HyperCLOVA 계열 모델을 safetensors 형태로 받아서 로컬 환경에서 테스트해보니, 원본 모델 크기가 꽤 커서 메모리 부담이 생각보다 컸다. 그래서 우선 PyTorch로 로드한 후 FP32 ONNX baseline을 먼저 만들고, 그 상태에서 바로 Q4(4-bit) 양자화 버전까지 직접 생성해봤다.
Q4 양자화는 INT8보다 더 공격적인 방식이긴 하지만, 로컬 환경에서 메모리를 크게 줄여야 할 때 상당히 도움이 된다. 실제로 변환을 마치고 나서 CPU 기반 추론 환경에서 돌려보니 메모리 점유율과 로딩 속도가 확 줄었고, 테스트 용도로는 충분히 실용적인 수준이라고 느꼈다.
전체 과정은 다음과 같은 순서로 진행했다.
변환한 Q4 모델은 정리 차원에서 Hugging Face에도 업로드해두었다.
필요하면 누구나 바로 로컬에서 테스트해볼 수 있다.
🔗 내가 업로드한 Hugging Face 모델 링크:
https://huggingface.co/YOUR_MODEL_LINK_HERE
HyperCLOVA 모델을 ONNX로 변환해두면 실행 환경이 훨씬 넓어진다. 운영 환경에 따라 FP32와 INT8 버전을 선택해 사용할 수 있고, CPU 또는 Web 환경에서도 추론이 가능한 구조를 만들 수 있다.
특히 개발 과정에서 safetensors 포맷을 다루고 있다면 ONNX 변환 과정을 미리 준비해두는 것이 배포 단계에서 큰 도움이 된다.