RKNN Whisper 구현 실전 가이드
RKNN으로 Whisper를 구동할 때 가장 중요한 핵심은 "Encoder는 NPU로, Decoder는 CPU(혹은 최적화된 NPU)로 처리한다"는 전략입니다. Whisper의 구조적 특성상 전체를 NPU에 올리는 것은 비효율적이기 때문입니다.
기초 이론: Whisper 구조와 NPU 가속 전략
Whisper의 구조
Whisper는 Transformer 기반의 Encoder-Decoder 구조입니다.
- Encoder (무거움): 오디오(Mel Spectrogram)를 입력받아 압축된 특징(Feature)을 추출합니다. 연산량이 많지만 구조가 고정적이라 NPU 가속에 매우 적합합니다.
- Decoder (가벼움/복잡함): 텍스트를 생성합니다. 이전 토큰을 기반으로 다음 토큰을 예측하는 반복(Loop) 구조이며, KV Cache를 사용합니다. 동적 크기 때문에 CPU에서 처리하는 것이 일반적인 구현 방식입니다.
RKNN 전략
- Audio Encoder:
.rknn모델로 변환하여 NPU에서 실행. - Text Decoder:
whisper.cpp같은 경량화된 C++ 라이브러리를 사용하여 CPU(Arm Core)에서 실행하거나, ONNX Runtime 등으로 CPU 구동.
환경 설정
PC (x86_64, Linux 추천) - 모델 변환용
RKNN 모델 변환은 보드가 아닌 성능 좋은 PC에서 수행해야 합니다.
- Anaconda/Miniconda 설치
- RKNN-Toolkit2 설치:bash
git clone https://github.com/airockchip/rknn-toolkit2 cd rknn-toolkit2/rknn-toolkit2 pip install packages/rknn_toolkit2-*.whl - PyTorch & OpenAI Whisper 설치:bash
pip install torch openai-whisper onnx
보드 (RK3588 등) - 실행용
- RKNPU2 드라이버 확인: 최신 펌웨어 설치.
- rknn-toolkit2-lite (Python) 혹은 rknn-api (C++) 라이브러리 설치.
모델 변환 (Step-by-Step)
이 과정이 가장 중요합니다. PyTorch 모델에서 Encoder만 떼어내어 RKNN으로 만듭니다.
PyTorch ONNX (Encoder Export)
python
import torch
import whisper
import onnx
# 1. 모델 로드 (예: tiny, base, small)
model = whisper.load_model("base")
encoder = model.encoder
encoder.eval()
# 2. 더미 입력 생성 (Batch, Mel_bins, Time)
# Whisper base는 80 mel bins, 3000 frames (30초)
dummy_input = torch.randn(1, 80, 3000)
# 3. ONNX로 내보내기
torch.onnx.export(
encoder,
dummy_input,
"whisper_encoder.onnx",
opset_version=12,
input_names=["mel"],
output_names=["last_hidden_state"],
dynamic_axes=None # NPU 고정 입력을 위해 dynamic axes 비활성화 권장
)
print("ONNX Export 완료!")ONNX RKNN 변환
python
from rknn.api import RKNN
# 1. RKNN 객체 생성
rknn = RKNN()
# 2. 설정 (RK3588 기준)
rknn.config(mean_values=[[0]], std_values=[[1]], target_platform='rk3588')
# 3. ONNX 로드
print('--> Loading ONNX model')
ret = rknn.load_onnx(model='./whisper_encoder.onnx')
if ret != 0:
print('Load model failed!')
exit(ret)
# 4. 모델 빌드 (FP16이 NPU 성능/정확도 밸런스가 좋음)
print('--> Building model')
ret = rknn.build(do_quantization=False) # 혹은 True로 설정하여 i8 양자화 시도 가능
if ret != 0:
print('Build model failed!')
exit(ret)
# 5. RKNN 저장
print('--> Export rknn model')
ret = rknn.export_rknn('./whisper_encoder.rknn')
if ret != 0:
print('Export rknn model failed!')
exit(ret)
print("RKNN 변환 완료!")추론 구현 (Board Side)
보드에서는 Python 또는 C++로 추론을 수행합니다. 여기서는 이해하기 쉬운 Python 예시를 듭니다.
Python 추론 코드 (간소화 버전)
python
import numpy as np
from rknn.api import RKNNLite # 보드에서는 Lite 버전 사용
import whisper
import torch
# 1. 오디오 전처리 (Whisper 라이브러리 활용)
audio = whisper.load_audio("test.wav")
audio = whisper.pad_or_trim(audio)
mel = whisper.log_mel_spectrogram(audio).numpy()
mel = np.expand_dims(mel, axis=0) # (1, 80, 3000)
# 2. RKNN 초기화 및 Encoder 실행
rknn_lite = RKNNLite()
rknn_lite.load_rknn('./whisper_encoder.rknn')
rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0)
# NPU 추론 (Encoder)
outputs = rknn_lite.inference(inputs=[mel])
npu_features = torch.from_numpy(outputs[0]) # 결과: (1, 1500, 512) 등
# 3. Decoder 실행 (CPU)
# 원본 Whisper 모델의 디코더에 NPU가 계산한 feature를 주입
model = whisper.load_model("base")
options = whisper.DecodingOptions()
result = model.decode(npu_features, options)
print("결과:", result.text)
# 리소스 해제
rknn_lite.release()추천 오픈소스 및 학습 자료
처음부터 모두 구현하기 어렵다면, 이미 잘 만들어진 프로젝트를 분석하는 것이 가장 빠른 학습법입니다.
rockchip-linux/rknn-toolkit2 (공식)
- Rockchip 공식 예제 폴더(
examples)에 Whisper가 포함되어 있는 경우가 있습니다. (버전에 따라 다름)
useful-rknn / whisper-rknn (추천)
Github에서 useful-transformers 혹은 rknn-whisper를 검색하면 나오는 프로젝트들은 대부분 Encoder(NPU) + Decoder(CPU) 방식을 사용합니다.
- aishell: 중국 개발자 커뮤니티에서 Rockchip용 Whisper 포팅이 활발합니다.
- monitor-lizard/rknn-whisper: 이와 유사한 프로젝트를 찾아보시면 C++ 기반의 고성능 구현체를 볼 수 있습니다.
whisper.cpp 활용
가장 현실적인 고성능 솔루션은 whisper.cpp 프로젝트가 RK3588의 NPU를 지원하는지 확인하는 것입니다. 최근 whisper.cpp는 다양한 백엔드를 지원하려 노력 중이며, 일부 포크(Fork) 버전에서 RKNN 백엔드를 실험적으로 지원합니다.
요약 (학습 포인트)
- 변환 대상 선정: Whisper 전체를 변환하려 하지 말고 Encoder만 변환하십시오.
- 데이터 타입:
FP16모드로 빌드하는 것이 정확도에 좋습니다.INT8양자화는 소리 인식률을 크게 떨어뜨릴 수 있습니다. - 하이브리드 파이프라인: Python에서
RKNNLite(Encoder)와PyTorch(Decoder)를 섞어 쓰는 방식으로 프로토타입을 먼저 만드세요. 그 후 C++로 이식하여 속도를 최적화하는 순서를 추천합니다.