Skip to content

ONNX 최적화 / 양자화

원본 Style-Bert-VITS2 는 PyTorch 기반이라 CPU 단독으로 실시간 추론이 어려웠습니다.

HayaKoe 는 BERT 를 Q8 양자화된 ONNX 로, Synthesizer 를 FP32 ONNX 로 내보내고 ONNX Runtime 위에서 실행하여, CPU 추론 속도를 텍스트 길이에 따라 1.5× ~ 3.3× 까지 향상 시켰습니다.

동시에 1 화자 로드 시 RAM 사용량을 5,122 MB → 2,346 MB (−54 %) 로 줄였습니다.

동일한 경로 덕분에 x86_64 뿐 아니라 aarch64 (Raspberry Pi 등) 에서도 같은 코드로 동작 합니다.

아쉬운 점

원본 SBV2 (CPU, PyTorch FP32) 에는 두 가지 아쉬운 점이 있었습니다.

  • 속도 — 텍스트가 길어질수록 추론 시간이 기하급수적으로 증가합니다. short (1.7 s 분량) 에서는 배속 1.52× 수준이지만, xlong (38.5 s 분량) 에서는 추론 35.3 초 · 배속 1.09× 로 실시간을 간신히 따라가는 수준까지 떨어집니다.
  • 메모리 — 1 화자 추론 시 Peak 메모리 약 5 GB 이상은 부담스러운 규모입니다.

분석

모델 파라미터 분포를 먼저 보면, 전체 중 약 84 % 가 BERT (DeBERTa-v2-Large-Japanese, 약 329 M) 에 집중되어 있고, Synthesizer (VITS) 는 63 M 으로 약 16 % 수준입니다.

BERT 가 모델의 대부분을 차지하므로, BERT 를 양자화하면 메모리를 크게 줄일 수 있을 것으로 예상했습니다. PyTorch 에서 BERT 만 Q8 Dynamic Quantization (torch.quantization.quantize_dynamic) 을 적용해 검증했습니다.

구성평균 추론 시간RAM
PyTorch BERT FP324.796 s+1,698 MB
PyTorch BERT Q84.536 s+368 MB (−78 %)

BERT 양자화가 속도는 개선시켜주진 않지만, 메모리 사용량을 크게 줄일 수 있음을 확인했습니다.

여기에 추가로 ONNX Runtime 전환을 통해 속도 개선까지 확보하는 방향으로 진행했습니다.

ONNX Runtime 은 모델을 로드할 때 그래프 수준 최적화 를 자동으로 적용합니다.

  • Kernel fusion — 연속된 여러 연산을 하나의 연산으로 합칩니다. 예를 들어 Conv → BatchNorm → Activation 세 단계를 하나로 묶으면, 중간 결과를 메모리에 쓰고 다시 읽는 과정이 사라져 메모리 접근이 줄어들어 빨라집니다.
  • Constant folding — 입력에 관계없이 항상 같은 값을 내는 연산을 로드 시점에 미리 계산해 두고, 추론 시에는 미리 계산한 값을 사용해 속도를 높입니다.
  • 불필요한 노드 제거 — 사용하지 않거나, 중복되거나, 무의미한 연산을 하는 노드를 찾아 제거합니다.

결론적으로, 추론에 최적화된 수학적으로 동일한 연산을 재구성해 더 빠르게 추론할 수 있게 만들어줍니다.

Synthesizer 는 파라미터가 63 M 으로 작아 양자화의 메모리 이득이 제한적이고, Flow 레이어 (rational_quadratic_spline) 가 FP16 이하에서 수치적으로 불안정하여 양자화 대상에서 제외했습니다. 대신 ONNX 로만 export 하여 그래프 최적화 이득을 확보했습니다.

BERT 최적화

양자화가 음질에 영향을 주는지 확인하기 위해, 동일 텍스트·동일 화자로 FP32 · Q8 · Q4 BERT 세 구성의 출력을 비교했습니다 (Synthesizer 는 모두 FP32 고정).

旅の途中で不思議な街に辿り着きました。少し寄り道していきましょう。

(여행 도중 신비로운 마을에 도착했습니다. 잠시 들러볼까요.)

つくよみちゃんBERT dtype = FP32원본 baseline
0:00 / 0:00

FP32 와 Q8 은 직접 청취 시 일관되게 구분하기 어려운 수준이었습니다.

Q4 는 대부분의 구간에서 FP32 · Q8 과 유사하지만, 끝부분에서 미세한 차이가 들립니다.

구성BERT 크기RAM (1 화자)
FP321,157 MB1,599 MB
Q8497 MB1,079 MB (−33 %)
Q4394 MB958 MB (−40 %)

Q4 로 추가 양자화해도 얻을 수 있는 메모리 이득이 음질보다 크지 않다고 판단해, 기본값으로 Q8 을 사용하기로 결정했습니다.

Synthesizer 최적화

BERT 가 파라미터의 84 % 를 차지하니, BERT 를 빠르게 만들면 전체가 빨라질 것 같습니다.

하지만 실제로 BERT 와 Synthesizer 의 추론 시간을 따로 측정해 보면, CPU 시간의 대부분은 Synthesizer 쪽에서 소비됩니다.

PyTorch FP32 CPU 에서의 실측 결과입니다 (5 회 평균).

텍스트BERTSynthesizerBERT 비중Synth 비중
short (1.7 s)0.489 s0.885 s36 %64 %
medium (5.3 s)0.602 s2.504 s19 %81 %
long (7.8 s)0.690 s3.714 s16 %84 %
xlong (30 s)1.074 s11.410 s9 %91 %

텍스트가 길어질수록 Synthesizer 의 비중이 커지는데, BERT 는 텍스트 길이에 비교적 둔감한 반면 Synthesizer 는 생성할 오디오 길이에 비례하여 시간이 늘어나기 때문입니다.

실제로 BERT 만 Q8 양자화했을 때 전체 추론 시간은 약 5 % 밖에 줄지 않았습니다.

즉, 속도를 높이려면 Synthesizer 구간을 최적화해야 합니다.

Synthesizer 는 양자화 대신 ONNX 변환만 적용했습니다.

  • VITS 의 Flow 레이어 (rational_quadratic_spline) 가 FP16 이하에서 부동소수점 오차로 인한 assertion error 를 일으켜 양자화가 불가능합니다.
  • 파라미터 수가 63 M 으로 작아 양자화의 메모리 이득도 제한적입니다.

대신 ONNX Runtime 으로 변환하여 앞서 설명한 그래프 수준 최적화 (kernel fusion · constant folding · 불필요 노드 제거) 를 Synthesizer 에도 동일하게 적용했습니다.

ONNX Runtime + CPUExecutionProvider

BERT 양자화와 Synthesizer 그래프 최적화는 모두 ONNX Runtime 위에서 동작합니다.

또한 intra-op parallelism 으로 단일 연산을 여러 CPU 코어에 분산하여, 요청이 하나뿐이어도 CPU 전체를 활용할 수 있습니다.

개선 효과

CPU 성능 비교 (배속, 동일 하드웨어)

배속 은 오디오 길이 / 추론 시간 (값이 클수록 빠름).

구성short (1.7 s)medium (7.6 s)long (10.7 s)xlong (38.5 s)
SBV2 PyTorch FP321.52×2.27×2.16×1.09×
SBV2 ONNX FP321.76×3.09×3.26×2.75×
HayaKoe (Q8 BERT + FP32 ONNX)2.50×3.35×3.33×3.60×

PyTorch FP32 대비 속도 향상은 텍스트 길이에 따라 1.5× ~ 3.3× 입니다.

메모리 (1 화자 로드 기준)

구성RAM
SBV2 PyTorch FP325,122 MB
SBV2 ONNX FP322,967 MB
HayaKoe (Q8 BERT + FP32 ONNX)2,346 MB (−54 %)