Compilando modelos para NPUs Qualcomm (QNN HTP)
Um procedimento, não um script: cada modelo precisa de sua própria exportação e verificações. Ele segue como o sherpa-onnx compila seus modelos NPU publicados (k2-fsa/sherpa-onnx, .github/workflows/export-*-qnn.yaml, .github/scripts/export-qnn/, scripts/qnn/), que são conhecidos por funcionar. Leia o workflow correspondente lá antes de começar; adapte, não invente.
0. Decida se o NPU vale a pena
- Meça primeiro no telefone alvo: % de CPU e tempo real do modelo enquanto executa atualmente (ticks
/proc/<pid>/stata cada 2 s,top -H -p <pid>por thread), e bateria durante uma tarefa fixa se o objetivo for economia de energia. O NPU compensa para trabalhos grandes e contínuos de matriz (codificadores, passes longos em lote); para um modelo pequeno em 1–2 threads o ganho pode ser pequeno. - Apenas shapes estáticos. O NPU executa gráficos de tamanho fixo: batch fixo (1), comprimento de entrada fixo (tamanho do chunk, ou 30 s para o codificador do Whisper). Um modelo cujo comprimento de entrada varia precisa de um chunk fixo, padding ou vários gráficos.
- Uma família de chip por build. Um binário de contexto é compilado para uma arquitetura Hexagon (v73, v75, v79, v81…) e funciona apenas nela; outros telefones (e Exynos / Tensor / MediaTek) mantêm o caminho da CPU. Planeje sempre um fallback para CPU.
- Limites de tamanho. Modelos muito grandes (acima de ~1B parâmetros em fp16) podem não caber em um contexto: eles precisam de pesos de 8 bits (w8a16) e/ou divisão em vários gráficos. As próprias builds do Whisper do sherpa-onnx param no medium (769M).
Chips (do scripts/qnn/device_info.py do sherpa-onnx): SM8550 Snapdragon 8 Gen 2 (v73), SM8650 8 Gen 3 (v75), SM8750 8 Elite (v79), SM8850 (v81), QCS9100 (v73). SM8350 (v68) não tem fp16: pule-o. Descubra o chip de um telefone com adb shell getprop ro.soc.model.
1. Ferramentas (apenas Linux x86-64)
O SDK QNN (QAIRT) roda em Linux x86-64 (ou Windows), não em macOS. Em um Mac: VM/cloud box Linux, ou Docker com --platform linux/amd64 (lento: emulado). O sherpa-onnx constrói uma imagem Docker para isso (Dockerfile-2.40, qnn-sdk-docker-build.yaml).
- QAIRT SDK, ex. 2.40.0.251030: da Qualcomm (conta gratuita), ou a cópia que o CI do sherpa-onnx baixa (
huggingface.co/csukuangfj/qnn-toolkit,v2.40.0.251030.zip). Verifique se a licença permite seu uso. source qairt/<version>/bin/envsetup.sh(defineQNN_SDK_ROOT), entãosudo ${QNN_SDK_ROOT}/bin/check-linux-dependency.shepython3 ${QNN_SDK_ROOT}/bin/check-python-dependency.- Ambiente virtual Python 3.10 com
numpy<2 onnx==1.17.0 onnxruntime onnxsimmais as dependências de exportação do próprio modelo. - Android NDK no
PATH(ndk-build), para as bibliotecas de modelo aarch64;LDFLAGS="-Wl,-z,max-page-size=16384"(páginas de 16 KB, exigido no Android 15+).
2. Exporte ONNX para o NPU
Re-exporte a partir do checkpoint original (PyTorch), não de um ONNX feito para CPUs:
--dynamic-batch 0(batch 1), sem quantização int8 no ONNX (--enable-int8-quantization 0: quantize no QNN),--use-int32-inputs 1(sem tensores int64), chunk/comprimento fixo.- Simplifique (
onnxsim) e verifique se cada entrada/saída tem um shape fixo (scripts/pyannote/segmentation/show-onnx.pydo sherpa-onnx). - Execute o ONNX na CPU (onnxruntime) contra o original em algumas entradas reais: mesma saída (ou mesmo texto).
Exemplos: Zipformer2 streaming (zipformer/export-onnx-streaming.py do icefall, veja scripts/zipformer-transducer/x-asr/qnn/export-streaming.sh do sherpa-onnx); Whisper (scripts/whisper/qnn/export_onnx.py do sherpa-onnx, codificador e decodificador como gráficos separados).
3. Converta, construa, compile (por gráfico, por chip)
Para cada gráfico ONNX $m (ex. encoder, decoder, joiner):
# a) ONNX → gráfico C++ + pesos do QNN. Ativações fp16: --act_bitwidth 16 --bias_bitwidth 32.
# Para pesos de 8 bits (modelos grandes): adicione quantização com um --input_list de entradas reais.
qnn-onnx-converter --input_network $m.onnx --output_path out/$m.cpp --act_bitwidth 16 --bias_bitwidth 32
# b) Configuração HTP para o chip (scripts/qnn/generate_config.py do sherpa-onnx: 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) Bibliotecas de modelo: x86_64 (para compilar o contexto no host) e 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) Binário de contexto para o chip: o que o telefone carrega.
$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
Mantenha os logs do conversor e do gerador: operações não suportadas e avisos de quantização aparecem lá.
4. Execute no telefone
- Envie as bibliotecas de runtime QNN para Android aarch64 (
libQnnHtp.so,libQnnHtpV79Skel.soe stub para o chip,libQnnSystem.so; o sherpa-onnx as publica comoqnn-libs-<version>.tar.bz2). A biblioteca Skel roda no DSP: oOnlineRecognizer.prependAdspLibraryPath(dir)do sherpa-onnx (ouADSP_LIBRARY_PATH) deve apontar para sua pasta. - sherpa-onnx:
QnnConfig(backendLib = ".../libQnnHtp.so", contextBinary = ".../$m.bin", systemLib = ".../libQnnSystem.so")na configuração do modelo (ex.OnlineTransducerModelConfig(..., qnnConfig = ...)),provider = "qnn". - Alternativa ONNX Runtime: o provedor de execução QNN (
QNNExecutionProvider,backend_path=libQnnHtp.so), que também pode armazenar em cache os binários de contexto. - Escolha por dispositivo: se
ro.soc.modelcorresponder a um chip compilado e as bibliotecas carregarem, NPU; senão, CPU. Nunca presuma.
5. Verifique
- Mesma saída: execute a mesma entrada/áudio na CPU e NPU; para fala, taxa de erro de palavras em 10+ clipes reais. Ativações de 16 bits geralmente são sem perdas; pesos de 8 bits devem ser verificados.
- Velocidade: fator de tempo real e latência da primeira saída no telefone.
- Energia: % de CPU do processo e queda de bateria durante uma tarefa fixa de 30–60 min, CPU vs NPU, tela desligada, mesmo brilho e rede. Repita; o estado térmico altera os resultados.
- Registre resultados (chip, versão do SDK, commit do modelo, quantização, números) junto ao modelo.
Problemas comuns
- Shapes dinâmicos, entradas int64, operações não suportadas (algumas operações de atenção ou controle de fluxo): corrija na exportação (enrole o modelo, corrija comprimentos), não no QNN.
- Binários de contexto estão vinculados à família da versão do SDK: construa com a mesma versão do QAIRT que as bibliotecas de runtime.
- Um modelo que cai em CPU para algumas operações silenciosamente perde a maior parte do ganho: verifique os logs para partições de CPU.
- Licenças: a do modelo (redistribuir uma forma compilada é distribuir o modelo) e a do SDK QNN.
- Rota alternativa sem Linux local: Qualcomm AI Hub (compilação em nuvem e perfilamento em dispositivos reais; precisa de conta; verifique os termos para seu aplicativo).