← Docs hub

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 arm64 libmodel.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에 재사용해서는 안 된다.

공식 문서:

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은 아래 둘 중 한 세트를 명시적으로 선택해야 한다.

  1. 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. 기기 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

그러므로 선택지는 두 개다.

초기 개발 속도와 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, ...)

추가로 필요한 동작:

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 변환에는 아래 중 하나가 필요하다.

  1. 모델 공급자가 원본 PyTorch/float checkpoint와 export 조건을 제공한다.
  2. 동일 학습 checkpoint를 다시 확보해 공식 QNN 2-input export를 수행한다.
  3. 기존 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

검증 항목:

Phase 3 — 앱 통합

작업 범위:

  1. QNN model asset → versioned filesDir copy manager
  2. 실제 QNN config와 별도 CPU fallback config
  3. QNN arm64 build variant의 native packaging
  4. provider/context/fallback 관찰 로그
  5. 기존 AudioRecord/preprocess/stream/decode 경로 유지
  6. recognizer/stream release와 재초기화 lifecycle 유지
  7. 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.somodel.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 패키징을 결정한다.

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:

  1. matched 한국어 corpus의 CPU/QNN CER·WER
  2. 10초 초과 발화의 truncate/CPU fallback 정책
  3. 앱 lifecycle 반복 초기화·해제와 native heap 안정성
  4. QNN STT와 CPU 화자인식 동시 실행 시 latency·CPU·thermal
  5. rollback APK와 QNN context cleanup 절차

Keyboard shortcuts

⌘K / Ctrl+KOpen command palette
/Focus search
g hGo to home
g pGo to projects
g sGo to sessions
j / kNext / prev row (tables)
?Show this help
EscClose dialogs

Structured queries

Mix key:value filters with free text in the palette:

type:sessionOnly session pages
project:llm-wikiFilter by project name (substring)
model:claudeFilter by model name (substring)
date:>2026-03-01Sessions after a date
date:<2026-04-01Sessions before a date
tags:rustPages mentioning a tag/topic
sort:dateSort results by date (newest first)

Example: type:session project:llm-wiki date:>2026-04 sort:date