4.2 KiB
4.2 KiB
Guide Hexagon NPU FP16 natif pour TTS
llama.cpp + ggml-hexagon + HMX FP16 — 47.8 tok/s
1. Pourquoi ça marche
Le QNN SDK quantifie automatiquement en int8/int16 → détruit le TTS. Le ggml-hexagon contourne le QNN SDK et accède directement aux unités HMX du Hexagon DSP en vrai FP16 IEEE-754 via des kernels reverse-engineerés (htp-ops-lib, Zixu Hao, EuroSys 2026).
| Approche | Précision | Vitesse | Audio TTS |
|---|---|---|---|
| QNN SDK HTP | int8/int16 quantifié | ~11ms/step | Silence/bruit |
| QNN SDK GPU | fp16 IEEE-754 | ~130ms/step | Parfait |
| ggml-hexagon HMX | fp16 IEEE-754 | ~21ms/step | À tester |
| ONNX Runtime CPU | fp32 | ~107ms/step | Parfait |
2. Build (snapdragon toolchain Docker)
Prérequis
- Podman ou Docker
- Source llama.cpp
Commandes
cd /opt/Kazeia/llama.cpp
cp docs/backend/snapdragon/CMakeUserPresets.json .
mkdir -p build-snapdragon
# Configure
podman run --rm --userns=keep-id \
--volume $(pwd):/workspace:Z \
--platform linux/amd64 \
ghcr.io/snapdragon-toolchain/arm64-android:v0.3 \
bash -c "cd /workspace && cmake --preset arm64-android-snapdragon-release -B build-snapdragon"
# Build
podman run --rm --userns=keep-id \
--volume $(pwd):/workspace:Z \
--platform linux/amd64 \
ghcr.io/snapdragon-toolchain/arm64-android:v0.3 \
bash -c "cd /workspace && cmake --build build-snapdragon -j\$(nproc)"
Outputs
build-snapdragon/bin/llama-cli # CLI (ARM64)
build-snapdragon/bin/lib*.so # Shared libs
build-snapdragon/ggml/src/ggml-hexagon/libggml-htp-v79.so # Hexagon v79 skel (HMX FP16)
3. Conversion talker → GGUF F16
Le talker TTS est un Qwen3 standard (28 layers, 1024 dim, q_norm/k_norm). On extrait ses poids et crée un GGUF compatible.
Script : extraction des poids
# Extraire les poids du talker dans un format HF standalone
state_dict = {}
for name, param in inner.named_parameters():
if name not in skip_set:
state_dict[f"model.{name}"] = param.detach().clone()
state_dict["model.embed_tokens.weight"] = inner.codec_embedding.weight.detach().clone()
state_dict["lm_head.weight"] = codec_head.weight.detach().clone()
Script : création GGUF manuelle
from gguf import GGUFWriter, GGMLQuantizationType
writer = GGUFWriter("talker_f16.gguf", "qwen3")
# ... add metadata (hidden_size, num_layers, etc.)
# ... add tensors with F16 weights, F32 norms
writer.add_tokenizer_model("none") # pas de tokenizer texte
Fichier : models_qnn/talker_f16.gguf (852 MB)
4. Déploiement sur tablette
# Push binaries
DST=/data/local/tmp/kazeia/llama-hex
adb shell "mkdir -p $DST"
adb push build-snapdragon/bin/llama-* $DST/
adb push build-snapdragon/bin/*.so $DST/
adb push build-snapdragon/ggml/src/ggml-hexagon/libggml-htp-v*.so $DST/
adb push models_qnn/talker_f16.gguf /data/local/tmp/kazeia/models/
# Benchmark
adb shell "cd $DST && LD_LIBRARY_PATH=. ./llama-bench \
-m /data/local/tmp/kazeia/models/talker_f16.gguf \
-mmp 0 -ngl 99 -pg 1,5"
5. Benchmark (SM8750, Hexagon v79)
| model | size | backend | test | tok/s |
|-----------------|----------|-------------|----------|----------|
| qwen3 0.6B F16 | 852 MiB | OpenCL,HTP | pp512 | 464 ± 16 |
| qwen3 0.6B F16 | 852 MiB | OpenCL,HTP | tg128 | 46.4 ± 1 |
| qwen3 0.6B F16 | 852 MiB | OpenCL,HTP | pp1+tg5 | 47.8 ± 1 |
47.8 tok/s = ~21ms/step (vs 107ms CPU = 5× plus rapide)
6. Prochaines étapes
Runner custom pour embeddings TTS
llama.cpp API supporte llama_batch.embd pour envoyer des embeddings
au lieu de token IDs. Il faut écrire un petit runner C++ qui :
- Charge le GGUF avec le backend hexagon
- Accepte des embeddings composites (1024 floats) via stdin/fichier
- Retourne les logits (3072 floats) sur stdout/fichier
- Gère le KV-cache entre les steps
Intégration dans l'app
- Le runner tourne en subprocess root (comme le LLM)
- L'app envoie les embeddings composites et lit les logits
- Le sampling (temp=0.9, top_k=50) reste sur CPU côté app
CP sur Hexagon NPU
- Même approche : convertir le CP (5 layers) en GGUF F16
- 5 layers → encore plus rapide que le talker
- Estimation : ~5ms pour les 17 steps CP