Qualcomm NPU(QNN HTP)용 모델 컴파일
절차이지 스크립트가 아닙니다: 모든 모델은 자체 내보내기와 검증이 필요합니다. sherpa-onnx가 공개 NPU 모델을 컴파일하는 방식(k2-fsa/sherpa-onnx, .github/workflows/export-*-qnn.yaml, .github/scripts/export-qnn/, scripts/qnn/)을 따르며, 이 방식은 실제로 작동하는 것으로 알려져 있습니다. 시작하기 전에 해당 워크플로우를 읽고, 상황에 맞게 적응하고 새로 만들지 마십시오.
0. NPU의 가치 판단
- 먼저 측정: 대상 폰에서 현재 모델의 CPU 사용률 및 소요 시간(
/proc/<pid>/stat2초마다 틱,top -H -p <pid>스레드별), 전력이 목표라면 고정 작업에서 배터리 소모를 측정하십시오. NPU는 크고 일정한 매트릭스 작업(인코더, 긴 배치)에서 효과적입니다. 1-2개 스레드의 작은 모델에서는 이득이 적을 수 있습니다. - 정적 크기만 지원: NPU는 고정 크기 그래프(고정 배치 1, 고정 입력 길이: 청크 크기 또는 Whisper 인코더의 경우 30초)를 실행합니다. 입력 길이가 변하는 모델은 고정 청크, 패딩 또는 여러 그래프가 필요합니다.
- 빌드당 하나의 칩 제품군: 컨텍스트 바이너리는 하나의 Hexagon 아키텍처(v73, v75, v79, v81…)용으로 컴파일되며 해당 아키텍처에서만 실행됩니다. 다른 폰(및 Exynos / Tensor / MediaTek)은 CPU 경로를 유지하십시오. 항상 CPU 폴백을 계획하십시오.
- 크기 제한: 매우 큰 모델(fp16에서 약 10억 파라미터 초과)은 하나의 컨텍스트에 맞지 않을 수 있습니다. 8비트 가중치(w8a16) 및/또는 여러 그래프로 분할이 필요합니다. sherpa-onnx 자체 Whisper 빌드는 medium(769M)에서 멈춥니다.
칩(sherpa-onnx scripts/qnn/device_info.py 기준): SM8550 Snapdragon 8 Gen 2 (v73), SM8650 8 Gen 3 (v75),
SM8750 8 Elite (v79), SM8850 (v81), QCS9100 (v73). SM8350 (v68)에는 fp16이 없습니다: 건너뛰십시오. 폰의 칩은 adb shell getprop ro.soc.model로 확인하십시오.
1. 도구 (Linux x86-64 전용)
QNN SDK(QAIRT)는 Linux x86-64(또는 Windows)에서 실행되며 macOS는 지원하지 않습니다. Mac의 경우: Linux VM/클라우드 박스, 또는 --platform linux/amd64를 사용한 Docker(느림: 에뮬레이션). sherpa-onnx는 이를 위한 Docker 이미지를 빌드합니다(Dockerfile-2.40, qnn-sdk-docker-build.yaml).
- QAIRT SDK, 예: 2.40.0.251030: Qualcomm(무료 계정 필요)에서 다운로드하거나 sherpa-onnx CI가 다운로드하는 복사본(
huggingface.co/csukuangfj/qnn-toolkit,v2.40.0.251030.zip) 사용. 라이선스가 사용자의 용도를 허용하는지 확인하십시오. source qairt/<version>/bin/envsetup.sh(QNN_SDK_ROOT설정), 이후sudo ${QNN_SDK_ROOT}/bin/check-linux-dependency.sh및python3 ${QNN_SDK_ROOT}/bin/check-python-dependency실행.numpy<2 onnx==1.17.0 onnxruntime onnxsim및 모델 자체 내보내기 의존성이 포함된 Python 3.10 venv.- Android NDK를
PATH에 설정(ndk-build), aarch64 모델 라이브러리용;LDFLAGS="-Wl,-z,max-page-size=16384"(16KB 페이지, Android 15+에서 필수).
2. NPU용 ONNX 내보내기
CPU용으로 만든 ONNX가 아닌 원본 체크포인트(PyTorch)에서 다시 내보내기:
--dynamic-batch 0(배치 1), ONNX에 int8 양자화 없음(--enable-int8-quantization 0: QNN에서 양자화),--use-int32-inputs 1(int64 텐서 없음), 고정 청크/길이.- 단순화(
onnxsim) 후 모든 입출력이 고정 크기인지 확인(sherpa-onnxscripts/pyannote/segmentation/show-onnx.py). - ONNX를 CPU(onnxruntime)에서 몇 가지 실제 입력으로 원본과 비교 실행: 동일한 출력(또는 동일한 텍스트).
예시: 스트리밍 Zipformer2 (icefall zipformer/export-onnx-streaming.py, sherpa-onnx
scripts/zipformer-transducer/x-asr/qnn/export-streaming.sh 참조); Whisper (sherpa-onnx scripts/whisper/qnn/export_onnx.py,
인코더와 디코더를 별도 그래프로).
3. 변환, 빌드, 컴파일 (그래프별, 칩별)
각 ONNX 그래프 $m(예: encoder, decoder, joiner)에 대해:
# a) ONNX → QNN C++ 그래프 + 가중치. fp16 활성화: --act_bitwidth 16 --bias_bitwidth 32.
# 8비트 가중치(큰 모델)의 경우: 실제 입력의 --input_list로 보정 양자화 추가.
qnn-onnx-converter --input_network $m.onnx --output_path out/$m.cpp --act_bitwidth 16 --bias_bitwidth 32
# b) 칩에 맞는 HTP 설정 (sherpa-onnx scripts/qnn/generate_config.py: soc_id, dsp_arch, vtcm).
python3 generate_config.py --soc SM8750 --graph-name $m --output-dir out/config-$m --qnn-sdk-root $QNN_SDK_ROOT
# c) 모델 라이브러리: x86_64 (호스트에서 컨텍스트를 컴파일하기 위해) 및 aarch64-android.
python3 $QNN_SDK_ROOT/bin/x86_64-linux-clang/qnn-model-lib-generator -c out/$m.cpp -b out/$m.bin -o out/model_libs
# d) 칩용 컨텍스트 바이너리: 폰이 로드하는 것.
$QNN_SDK_ROOT/bin/x86_64-linux-clang/qnn-context-binary-generator \
--backend $QNN_SDK_ROOT/lib/x86_64-linux-clang/libQnnHtp.so \
--model out/model_libs/x86_64-linux-clang/lib$m.so \
--output_dir out/binary --binary_file $m \
--config_file out/config-$m/htp_backend_extensions.json
변환기 및 생성기 로그를 보관하십시오: 지원되지 않는 연산과 양자화 경고가 여기에 나타납니다.
4. 폰에서 실행
- Android aarch64용 QNN 런타임 라이브러리 제공(
libQnnHtp.so,libQnnHtpV79Skel.so및 칩용 스텁,libQnnSystem.so; sherpa-onnx는 이를qnn-libs-<version>.tar.bz2로 배포). Skel 라이브러리는 DSP에서 실행됩니다: sherpa-onnx의OnlineRecognizer.prependAdspLibraryPath(dir)(또는ADSP_LIBRARY_PATH)가 해당 폴더를 가리켜야 합니다. - sherpa-onnx: 모델 설정(예:
OnlineTransducerModelConfig(..., qnnConfig = ...))에QnnConfig(backendLib = ".../libQnnHtp.so", contextBinary = ".../$m.bin", systemLib = ".../libQnnSystem.so"),provider = "qnn". - ONNX Runtime 대안: QNN 실행 제공자(
QNNExecutionProvider,backend_path=libQnnHtp.so), 자체적으로 컨텍스트 바이너리를 캐시할 수도 있음. - 장치별 선택:
ro.soc.model이 컴파일된 칩과 일치하고 라이브러리가 로드되면 NPU, 그렇지 않으면 CPU. 절대 가정하지 마십시오.
5. 검증
- 동일한 출력: 동일한 오디오/입력을 CPU와 NPU에서 실행; 음성의 경우 10개 이상의 실제 클립에서 단어 오류율 확인. 16비트 활성화는 일반적으로 무손실; 8비트 가중치는 반드시 확인.
- 속도: 폰에서 실시간 계수 및 첫 출력 지연 시간 측정.
- 전력: 프로세스의 CPU %, 고정 30-60분 작업 동안 CPU vs NPU의 배터리 소모, 화면 꺼짐, 동일 밝기 및 네트워크 조건에서 측정. 반복; 열 상태가 결과를 변화시킴.
- 결과(칩, SDK 버전, 모델 커밋, 양자화, 수치)를 모델 옆에 기록.
함정
- 동적 크기, int64 입력, 지원되지 않는 연산(일부 attention 또는 제어 흐름 연산): QNN이 아닌 내보내기에서 수정(모델 래핑, 길이 수정).
- 컨텍스트 바이너리는 SDK 버전 제품군에 종속됨: 런타임 라이브러리와 동일한 QAIRT 버전으로 빌드.
- 일부 연산에서 CPU로 폴백되는 모델은 이득의 대부분을 조용히 잃음: CPU 파티션 로그 확인.
- 라이선스: 모델(컴파일된 형태의 재배포는 모델 재배포) 및 QNN SDK.
- 로컬 Linux 없는 대안: Qualcomm AI Hub(클라우드 컴파일 및 실제 장치에서 프로파일링; 계정 필요; 앱에 대한 조건 확인).