Sherpa SenseVoice CPU → QNN HTP(v68) 전환 사전 분석
이 문서는 skmagic_ondeviceai_agent의 현재 SenseVoice STT가 실제로 어떤
모델과 라이브러리를 사용하는지 확인하고, QCS6490 계열 v68 HTP에서 QNN으로
전환할 수 있는지, 현재 한국어 모델을 그대로 양자화할 수 있는지, 구현을 어떤
순서로 진행해야 하는지를 정리한 사전 의사결정 문서다.
2026-08-06 진행 갱신
사전 분석 당시에는 원본 float checkpoint가 없어 활성 CPU INT8 모델의 동일-weight QNN 변환을 blocked로 판단했다. 이후
DynamicQuantizeLinear + MatMulInteger의 constant weight를 역복구하고 고정 10초 graph로 재구성하는 경로를 검증했다. 5개 언어 calibration을 사용한 QNN PTQ(weights 8 / activations 16 / bias 32), Android arm64libmodel.so, QCS6490 HTP graph finalize와 context 생성까지 성공했다. 첫 산출물은logits=[1,25055,171]레이아웃 문제로 decoder text가 깨졌지만,--preserve_io layout logits를 적용한 재변환에서는[1,171,25055]가 확인됐다. QCS6490/v68에서조금만 생각을 하면서 살면 훨씬 편할거야를 정상 디코딩했고, 생성된 context로 warm 3회도 동일 문장을 반환했다. 따라서 현재 판정은 활성 모델 QNN 양자화와 단일 음원 기능 검증 완료다.STT QNN과 화자인식 CPU의 동시 실행 구조는 병렬 실행 설계에서 이어서 다룬다.
결론
현재 기기와 SDK에서 Sherpa QNN 기능 디코딩이 확인됐다. QNN 2.44의 v68 Stub/Skel, 양자화 model library, QCS6490 context를 같은 bundle로 사용했고, Sherpa SenseVoice QNN 경로에서 정상 한국어 text를 얻었다.
그러나 현재 CPU용 model.int8.onnx를 그대로 NPU에서 실행할 수는 없다.
provider 문자열만 cpu에서 qnn으로 변경하는 작업도 아니다.
현재 CPU 경로
model.int8.onnx
+ ONNX Runtime CPU provider
+ 동적 길이, 4개 입력
필요한 QNN 경로
QNN 전용 고정 길이 ONNX export
-> calibration 기반 QNN quantization
-> Android arm64 libmodel.so
-> QCS6490/QNN 버전에 맞는 model.bin
+ Sherpa QNN JNI/API
+ v68 HTP runtime
사전 분석에서는 원본 model.pt/float checkpoint 부재를 blocker로 봤다.
실제 구현에서는 활성 CPU graph의 constant dynamic-quantized weight 281개를
역복구해 동일 weight 기반 고정 10초 QNN graph를 만들었다. 복구 graph는
CPU 원본과 같은 한국어 문장을 냈고, 이후 Qualcomm PTQ와 HTP 실행까지
연결했다.
따라서 현재 판정은 다음과 같다.
| 항목 | 판정 | 이유 |
|---|---|---|
| QCS6490/v68에서 Sherpa QNN bring-up | Go | v68 Stub/Skel과 최신 Sherpa QNN 코드가 존재 |
| 현재 ONNX를 provider만 바꿔 실행 | No-Go | QNN SenseVoice 입력 계약과 모델 형식이 다름 |
| 공개 SenseVoice 모델 QNN 양자화 | Go | 공식 export/converter 흐름과 QNN SDK 2.44 보유 |
| 현재 한국어 weight 기반 QNN 양자화 | 완료 | 281개 weight 복구, W8A16B32 PTQ, HTP 정상 text 확인 |
| 기존 앱에 AAR만 교체 | No-Go | Java API, JNI, QNN runtime, 모델, 패키징을 함께 맞춰야 함 |
sherpa_qnn 앱 전환 준비 |
완료 | QNN 우선/CPU fallback 코드, signed release, 전달 bundle 생성 |
| 제품 품질 승인 | 아직 미판정 | matched corpus CER/WER, 전력, 장시간 안정성 검증이 남음 |
증거 수준
이 문서에서는 결론의 강도를 다음처럼 구분한다.
| 라벨 | 의미 |
|---|---|
| 확인 | 현재 소스, 산출물 또는 192.168.123.118:5555 실기기에서 직접 확인 |
| 해석 | 확인된 여러 사실을 연결한 기술적 결론 |
| 제안 | 아직 코드 적용 또는 실기기 반복 검증 전인 작업안 |
| 미확인 | 실험으로 닫아야 하는 항목 |
1. 현재 앱이 실제로 사용하는 STT
1.1 코드 경로
대상 checkout:
/home/silogood/work/2.A1_LLM_Aent/skmagic_ondeviceai_agent
branch: sherpa_qnn
commit: c37b68fe92fbc9d3b3f452ed0d056efb25156693
sherpa_qnn은 현재 main, origin/main, mr7-hotfix와 같은 commit을
가리킨다. 즉 브랜치 이름과 달리 추적된 QNN 구현 commit은 아직 없다.
ForegroundService.getModelConfig(15)는 한국어 STT에 다음 모델을 지정한다.
sherpa-onnx-sense-voice-ko/model.int8.onnx
sherpa-onnx-sense-voice-ko/tokens.txt
language = ko
modelType = sense_voice
/data/sherpa-onnx-sense-voice-ko가 있으면 그 경로를 우선하고, 없으면 APK
asset을 사용한다. 확인한 기기에는 해당 /data 디렉터리가 없으므로 실제
로딩 대상은 표준 Android source set인 아래 파일이다.
app/src/main/assets/sherpa-onnx-sense-voice-ko/model.int8.onnx
size: 241,227,951 bytes
sha256: be57dfcc9f0565f96236de68989dae988f11d6c5af339c889797e1f77a45dcba
사용자가 처음 지목한 app/assets/...는 Gradle의 main asset source set이
아니다. 두 파일은 이름만 같고 내용도 다르다.
app/assets/sherpa-onnx-sense-voice-ko/model.int8.onnx
size: 241,227,896 bytes
sha256: 08630caa08c417acf31f3e9d8ebf71675a71b62a5880df4db6dc2ecd908b9215
따라서 앞으로 모델 변환의 기준 SHA는 be57...dcba로 고정해야 한다.
1.2 실제 실행 provider
현재 initializeSTT()의 첫 시도와 예외 fallback은 모두 동일한
OfflineModelConfig를 만든다. 첫 경로에도 provider="qnn"이나
QnnConfig가 없고, fallback은 numThreads=4만 추가한다.
첫 시도: OfflineRecognizer(default provider)
fallback: OfflineRecognizer(default provider, numThreads=4)
따라서 코드 주석의 “DSP 포함”, “CPU 모드로 전환”과 달리 두 경로 모두 ONNX Runtime CPU다. 2026-08-05 실기기 로그에도 아래 값이 확인됐다.
provider="cpu"
sense_voice.model="sherpa-onnx-sense-voice-ko/model.int8.onnx"
language="ko"
1.3 오디오 입력부터 decode까지
현재 데이터 흐름은 다음과 같다.
AudioRecord
-> saveStreamRecordAudio()
-> SttPreprocessPipeline.processChunk()
-> OfflineStream.acceptWaveform()
-> stopRecording()
-> prepareDecode()
-> OfflineRecognizer.decode()
-> OfflineRecognizer.getResult()
QNN 전환 시 바꿔야 하는 경계는 recognizer model/provider 아래다. AudioRecord,
기존 SttPreprocessPipeline, stream accept 경로, 멀티턴/취소 처리는 QNN
전환과 별도 변수로 유지해야 한다. 전처리까지 동시에 바꾸면 CPU/QNN decoder
차이와 입력 신호 차이를 분리할 수 없다.
2. 현재 CPU INT8 ONNX가 QNN 모델이 아닌 이유
활성 model.int8.onnx의 graph를 검사한 결과는 다음과 같다.
inputs:
x [N, T, 560]
x_length [N]
language [N]
text_norm [N]
nodes: 10,218
initializers: 1,479
DynamicQuantizeLinear: 281
MatMulInteger: 281
MatMul: 140
Conv: 70
이는 ONNX Runtime CPU가 실행하는 동적 INT8 graph다. 파일 이름에 int8이
있다는 사실은 QNN HTP가 바로 실행할 수 있다는 뜻이 아니다.
Sherpa 최신 QNN SenseVoice 구현은 모델 입력을 다음 두 개로 강제한다.
x
prompt # int32[4] = language, 1, 2, text_norm
입력 개수가 2개가 아니거나 두 번째 입력 이름이 prompt가 아니면
프로세스를 종료한다. 현재 CPU graph는 동적 길이의 4-input contract이므로,
QNN converter가 일부 op를 변환하더라도 현재 Sherpa QNN runtime contract와
맞지 않는다.
즉 필요한 작업은 “기존 INT8 ONNX의 재양자화”가 아니라 원본 weight에서 QNN용 graph를 다시 export하는 것이다.
3. 공식 SenseVoice → QNN 변환이 하는 일
Sherpa 공식 workflow는 다음 순서를 사용한다.
model.pt + am.mvn + tokenizer
-> export-onnx.py --input-len-in-seconds <fixed duration>
-> QNN input calibration WAV/raw 생성
-> qnn-onnx-converter
x: float32
prompt: int32
activation: 16 bit
bias: 32 bit
x layout: NTF
-> qnn-model-lib-generator
-> aarch64-android/libmodel.so
-> qnn-context-binary-generator
-> SoC/QNN-specific model.bin
산출물 의미는 구분해야 한다.
| 파일 | 역할 | 종속성 |
|---|---|---|
libmodel.so |
QNN model library | Android arm64 OS/ABI 종속, 특정 SoC에는 비종속 |
model.bin |
QNN serialized context | SoC와 QNN SDK/runtime 버전에 종속 |
libsherpa-onnx-jni.so |
Kotlin/Java ↔ Sherpa native bridge | Sherpa API/빌드 옵션과 맞아야 함 |
libQnnHtp.so |
QNN HTP backend | QNN runtime 버전 |
libQnnHtpV68Stub.so |
Android CPU 측 v68 RPC stub | v68와 QNN runtime 버전 |
libQnnHtpV68Skel.so |
DSP 측 v68 implementation | v68와 QNN runtime 버전 |
libQnnSystem.so |
context binary 로더 | QNN runtime 버전 |
libmodel.so로 첫 실행하면 기기에서 model.bin을 생성할 수 있다. 이후
실행은 model.bin을 재사용해 초기화를 단축한다. 다른 SoC용으로 미리 생성된
model.bin을 QCS6490에 재사용해서는 안 된다.
공식 문서:
- Sherpa QNN 개요
- QNN SDK 준비
- Sherpa QNN 빌드
libmodel.so와model.bin실행 구조- Android QNN demo
- QNN model과 SoC 종속성
4. QCS6490/v68 실기기 호환성
확인 대상:
ADB: 192.168.123.118:5555
ro.board.platform: lahaina
ABI: arm64-v8a
application: com.skmagic.ondeviceai.agent
APK: /system/priv-app/MaumAi/MaumAi.apk
기기에서 다음 파일을 확인했다.
/system/lib64/libQnnHtp.so
/system/lib64/libQnnHtpPrepare.so
/system/lib64/libQnnHtpV68Stub.so
/system/lib64/libQnnSystem.so
/vendor/lib/rfsa/adsp/libQnnHtpV68Skel.so
따라서 사용자가 말한 QNN v68 구조와 파일 배치는 일치한다. 앞선 검색에서
Skel이 없다고 보였던 것은 /system만 확인한 결과였고, 실제 DSP Skel은
RFSA 경로에 있다.
다만 이것은 “파일이 있다”는 loader 준비 증거이지, 새 Sherpa model이 HTP에서 성공적으로 실행됐다는 증거는 아니다. 최종 호환 판정에는 QNN log와 동일 음원 decode가 필요하다.
5. 현재 가장 큰 통합 위험: 버전 혼용
현재 확인된 버전은 세 묶음이다.
| 위치 | QNN 버전 | 상태 |
|---|---|---|
실기기 /system/lib64 |
2.34.0 | 현재 기기 기본 runtime |
device_backups_lib와 앱의 untracked jniLibs |
2.35.0 | 서로 hash 일치 |
| 로컬 QAIRT/QNN SDK | 2.44.0 | 새 Sherpa QNN 빌드에 사용 중 |
SDK 2.44 metadata는 backend API 2.18.0, 권장 Android NDK r26c다. 현재
설정한 NDK는 26.3.11579264(r26d)이며, 정확한 r26c는 설치돼 있지 않다.
r26d로 컴파일될 가능성은 높지만 제품 기준으로는 Qualcomm 권장 조합과
동일하지 않다는 사실을 기록해야 한다.
다음과 같은 혼용은 금지한다.
Sherpa JNI built with QNN 2.44 headers
+ backend 2.34
+ Stub 2.35
+ Skel 2.34
+ model.bin generated by 2.40
첫 bring-up은 아래 둘 중 한 세트를 명시적으로 선택해야 한다.
-
2.44 self-contained 실험 세트
Sherpa 1.13.4, QNN 2.44 backend/system/prepare/v68 Stub/Skel, QNN 2.44로 만든libmodel.so와 QCS6490에서 처음 생성한model.bin. -
기기 2.34 system 세트
QNN 2.34 SDK로 Sherpa와 모델을 다시 만들고 system runtime을 그대로 사용.
현재는 2.34 SDK 전체가 없고 2.44 SDK가 있으므로, 격리된
/data/local/tmp bring-up에서는 1번이 현실적이다. 제품 이미지 반영 시에는
플랫폼 QNN upgrade 정책과 vendor DSP library 호환성을 별도 승인받아야 한다.
6. AAR만 복사하면 안 되는 이유
앱은 현재 다음 AAR에 의존한다.
app/libs/sherpa-onnx-1.11.3.aar
이 AAR의 classes.jar에는 QnnConfig class가 없다. 반면 현재 Sherpa
source는 1.13.4이며 OfflineSenseVoiceModelConfig.qnnConfig,
OfflineModelConfig.provider="qnn" API가 있다.
따라서 새 통합은 아래를 하나의 산출물로 취급해야 한다.
Sherpa 1.13.4 Kotlin/Java classes
+ 동일 commit의 libsherpa-onnx-jni.so
+ 필요한 ONNX Runtime native library
+ 동일 QNN SDK 계열의 runtime libraries
현재 Gradle은 debug에서 **/*.so를 제외하고, task 이름에 Release/Debug가
포함되면 다시 lib/**를 제외한다. 설치된 MaumAi APK도 lib/* entry가 0개다.
실행 중인 CPU JNI와 ONNX Runtime은 아래 system library를 사용한다.
/system/lib64/libsherpa-onnx-jni.so
/system/lib64/libonnxruntime.so
그러므로 선택지는 두 개다.
- 제품 system image에서 Sherpa JNI/QNN runtime을 일관되게 교체한다.
- QNN 실기기용 build variant만 native library를 APK에 포함하고, app native library 경로를 사용한다.
초기 개발 속도와 rollback을 위해 두 번째를 먼저 권고한다. 기존 emulator용 native exclude 정책은 별도 variant에 유지하고, 실제 arm64 QNN variant에서는 제외를 해제해야 한다.
7. QNN Android 코드에서 반드시 달라질 부분
Sherpa QNN SenseVoice 생성자는 Android AssetManager 직접 로딩을 거부하고
“assets의 파일을 저장소로 복사하고 assetManager=null을 사용하라”고
명시한다. 공식 Android demo도 같은 방식이다.
따라서 앱 초기화는 다음 형태가 되어야 한다.
assets/qnn-sensevoice/
libmodel.so
tokens.txt
앱 최초 실행:
-> filesDir/qnn-sensevoice/<model-version>/ 로 원자적 복사
-> contextBinary = filesDir/.../model-qcs6490-qnn244.bin
-> provider = qnn
-> QnnConfig(
backendLib = libQnnHtp.so,
systemLib = libQnnSystem.so,
contextBinary = writable absolute path
)
-> OfflineRecognizer(assetManager = null, ...)
추가로 필요한 동작:
- model SHA/QNN version/SoC를 context cache key에 포함한다.
- model 또는 QNN runtime이 바뀌면 기존
model.bin을 무효화한다. prependAdspLibraryPath(applicationInfo.nativeLibraryDir)를 초기화 전에 호출한다.- QNN 초기화와 CPU fallback의 config를 실제로 분리한다.
- log에 요청 provider, 최종 provider, fallback reason, context 생성/재사용을 남긴다.
- QNN 실패 시 기존 CPU model과 stream 경로로 안전하게 rollback한다.
8. 현재 한국어 모델을 양자화할 수 있는가
가능한 것
공개 SenseVoice model.pt, am.mvn, tokenizer와 calibration WAV를 사용해
QNN SDK 2.44용 libmodel.so를 만드는 작업은 가능하다. QCS6490 실기기에서
model.bin을 생성하고 HTP 실행 여부도 검증할 수 있다.
지금 막힌 것
로컬 전체 검색 결과, 현재 한국어 model.int8.onnx에 대응하는 다음 파일은
발견되지 않았다.
model.pt
float32 model.onnx
am.mvn
training/export checkpoint
APK의 assets.zip에도 동일한 INT8 ONNX와 tokens만 있다. 또한 한국어
모델과 공개 다국어 모델은 graph 구조는 유사하지만 initializer 값이 거의
모두 달라 동일 weight가 아니다.
따라서 현재 한국어 모델의 정확한 QNN 변환에는 아래 중 하나가 필요하다.
- 모델 공급자가 원본 PyTorch/float checkpoint와 export 조건을 제공한다.
- 동일 학습 checkpoint를 다시 확보해 공식 QNN 2-input export를 수행한다.
- 기존 INT8 graph를 역으로 재구성하고 4-input을 2-input으로 바꾸는 실험적 graph surgery를 한다.
3번은 quantization 오차, dynamic shape, 입력 contract, unsupported op를 동시에 해결해야 하고 동일 결과 보장도 어렵다. 첫 작업으로 권고하지 않는다.
9. 권장 진행 순서
Phase 0 — 독립 HTP bring-up
앱을 수정하기 전에 /data/local/tmp/sherpa-qnn-smoke에 완전한 QNN 2.44
실험 세트를 배치한다.
목적:
QCS6490 v68에서 QNN loader, Stub/Skel, HTP 실행을 먼저 증명
입력:
공식 QNN SenseVoice Android arm64 libmodel.so
QNN 2.44 runtime + v68 Stub/Skel
동일한 5초/10초 테스트 WAV
통과 증거:
libQnnHtp.so 로딩 성공
v68 deviceCreate 성공
HTP graph 실행 성공
첫 실행 model.bin 생성
두 번째 실행 model.bin 재사용
transcript 생성
이 단계에서 공개 모델을 쓰므로 기존 한국어 STT의 품질 비교 결론은 내리지 않는다. 목적은 HTP plumbing 검증뿐이다.
Phase 1 — 모델 기준선과 checkpoint 확보
활성 모델 SHA be57...dcba를 기준선으로 고정하고, 같은 음원/정답 문장
세트를 CPU에서 decode해 transcript, CER/WER, latency, PSS를 저장한다.
동시에 현재 한국어 모델의 원본 checkpoint 출처를 찾는다. 확보되면 Sherpa QNN export script로 5초와 10초 graph를 각각 만들고, 제품의 최대 발화 길이와 padding/cropping 정책에 맞는 하나를 선택한다.
Phase 2 — Sherpa QNN AAR fresh build
현재 Sherpa checkout의 기존 Android output은 source보다 오래된 산출물이다. 재사용하지 않고 깨끗한 build directory에서 새로 만든다.
Sherpa source: 1.13.4
ABI: arm64-v8a
QNN: ENABLE_QNN=ON
JNI: ON
BINARY: ON
SDK: 선택한 하나의 QNN version
NDK: 가능하면 SDK 권장 r26c
검증 항목:
- AAR
classes.jar에QnnConfig가 존재한다. - AAR의 JNI가 같은 Sherpa commit에서 만들어졌다.
- JNI string/symbol에 QNN path가 존재한다.
- ELF
NEEDED, ABI, page size를 검사한다. - 독립 demo에서 동일 AAR로 QNN decode한다.
Phase 3 — 앱 통합
작업 범위:
- QNN model asset → versioned
filesDircopy manager - 실제 QNN config와 별도 CPU fallback config
- QNN arm64 build variant의 native packaging
- provider/context/fallback 관찰 로그
- 기존 AudioRecord/preprocess/stream/decode 경로 유지
- recognizer/stream release와 재초기화 lifecycle 유지
- runtime feature flag와 즉시 CPU rollback
이 단계에서는 기존 model.int8.onnx를 삭제하지 않는다. QNN이 실기기 Gate를
통과할 때까지 CPU fallback과 기준선으로 보존한다.
Phase 4 — 동일 조건 실기기 검증
같은 PCM을 CPU와 QNN에 넣어 비교한다.
| Gate | 측정 |
|---|---|
| backend proof | 요청/최종 provider, QNN HTP log, CPU fallback 부재 |
| cold start | libmodel.so → model.bin 생성 시간 |
| warm start | 기존 model.bin 재사용 초기화 시간 |
| decode | RTF, p50/p95 latency |
| memory | Java/native PSS, peak RSS |
| thermal/power | 반복 decode 중 온도, throttling, 전력 |
| quality | 동일 정답 세트 CER/WER, 명령 성공률 |
| lifecycle | start/stop, cancel, destroy-during-decode, error/reinit 반복 |
신호 RMS나 SNR 변화만으로 STT 품질을 판정하지 않는다. 실제 decoder transcript와 matched ground truth의 CER/WER가 있어야 제품 승격을 결정한다.
Phase 5 — system app 반영
실험 APK가 통과한 뒤 release/system app 패키징을 결정한다.
- 기존
/system/priv-app/MaumAi/MaumAi.apkcertificate와 새 APK certificate를apksigner verify --print-certs로 비교한다. - debug certificate APK를 system app으로 교체하지 않는다.
- Sherpa/QNN native library 위치와 SELinux/linker namespace를 제품 이미지 기준으로 확정한다.
- CPU rollback APK와 context cache cleanup 절차를 준비한다.
10. 결정과 실행 결과
| 결정 | 적용 결과 | 근거 |
|---|---|---|
| runtime | 격리된 QNN 2.44 full set | backend build ID와 v68 Stub/Skel을 bundle로 고정 |
| 모델 | 활성 CPU model weight 기반 QNN PTQ | 281개 constant weight 복구 후 W8A16B32 변환 |
| 입력 길이 | 고정 10초 | x[1,167,560], 짧은 입력 zero padding |
| context | QCS6490 실기기에서 생성 | warm 초기화 2.111~2.767초 확인 |
| 앱 API | QNN-enabled arm64 Sherpa AAR | QnnConfig와 QNN JNI 포함 |
| CPU fallback | 유지 | bundle 누락·QNN 초기화 실패·JNI 미지원 시 복귀 |
| 기존 모델 | 유지 | fallback과 CPU/QNN 품질 비교 기준 |
| release | platform certificate signing | 설치 system APK와 certificate SHA-256 일치 |
11. 현재 산출물과 다음 품질 Gate
sherpa_qnn 작업 트리에는 다음이 준비됐다.
ForegroundService
-> 한국어 language code 15: QNN 우선
-> QNN bundle/JNI 실패: 기존 CPU fallback
QnnSttRuntime
-> /data/sherpa-onnx-sense-voice-ko-qnn-10s
-> model/context/QNN 2.44 v68 필수 파일 검증
deliverables/sherpa_qnn_stt_delivery_qcs6490_v68_20260806
-> signed release APK
-> QNN-enabled AAR와 system JNI/ORT
-> libmodel.so와 QCS6490 context
-> QNN 2.44 v68 runtime
-> SHA256SUMS와 cold/warm 실행 로그
기기에서 모델과 context의 standalone QNN 정상 실행은 확인했지만
/system/priv-app과 /system/lib64의 실제 제품 교체는 이 문서 갱신 시점에
수행하지 않았다. 다음 단계는 debug 설치가 아니라 동일 platform certificate의
release 산출물로 system image 통합 후 앱 end-to-end STT를 반복 검증하는 것이다.
제품 품질 승인을 위해 남은 Gate:
- matched 한국어 corpus의 CPU/QNN CER·WER
- 10초 초과 발화의 truncate/CPU fallback 정책
- 앱 lifecycle 반복 초기화·해제와 native heap 안정성
- QNN STT와 CPU 화자인식 동시 실행 시 latency·CPU·thermal
- rollback APK와 QNN context cleanup 절차