Vulkan Adreno 830 testé : neutre au mieux, régresse au pire (commit aussi le code opt-in)

Re-analyse globale plateforme : le GPU n'avait JAMAIS été testé. Build
ggml-vulkan pour Android arm64 (NDK r27d + SPIRV-Headers + vulkan.hpp
téléchargés depuis github, libomp.so poussé sur tablette).

Adreno 830 détecté : uma=1 fp16=1 matrix cores=NONE (compute shaders seuls).

3 patterns testés :
  (1) Decoder weights Vulkan buft + CPU compute : NEUTRE
      RTF 2.45 -> 2.40-2.47 (bruit), WAV bit-exact
      Cause : UMA = même DRAM, pas de gain BW
  (2) Decoder + sched (GPU compute attendu) : tout CPU
      sched route UMA buffer -> CPU (logique is_host)
      Cause : sched ne déclenche pas l'offload pour buffers UMA
  (3) Talker offload 29/29 layers via llama.cpp : REGRESSION +128%
      Prefill 87->775 ms, talker 32->45 ms, decoder 3.3->6.6s
      Cause : 0.6B + batch=1 + bus contention GPU/CPU sur DRAM partagée

Verdict cumulé HTP + Vulkan : sur SM8750 ce TTS spécifique, ni HTP ni
GPU n'apportent gain net. NEON CPU optimisé reste la voie unique
réaliste sur ce hardware.

Conso énergie : Vulkan talker = pipeline ×2.3 plus long, conso ~×2.
NEON CPU = meilleure efficience.
Verdict : RESTE SUR NEON CPU pour la prod.

Code committé opt-in (KZTTS_VULKAN_LIB, KZTTS_DECODER_VULKAN,
KZTTS_TALKER_VULKAN), désactivé par défaut. Path principal RTF 2.45 intact.

Détails complets dans dist/PERF_VULKAN_TESTS.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Richard Loyer 2026-05-29 22:28:16 +02:00
parent a5d591a07b
commit 27d893c73f
4 changed files with 284 additions and 12 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ models/
b/ b/
ql/ ql/
eval/out/ eval/out/
dist/b-vulkan/

171
dist/PERF_VULKAN_TESTS.md vendored Normal file
View File

@ -0,0 +1,171 @@
# Tests Vulkan Adreno 830 — verdict + raisons (29/05)
Session de re-analyse globale plateforme. Le HTP avait été exclu. Le GPU
n'avait JAMAIS été testé. Suite à l'autorisation de Richard, exploration
ggml-vulkan sur Adreno 830 du SM8750.
## Préparatifs build
Chaîne build non-triviale, dépendances manquantes dans NDK r27d :
1. `SPIRV-Headers` : téléchargé depuis github (vulkan-sdk-1.3.275.0), installé
dans `/tmp/spirv-install` et dans le sysroot NDK.
2. `vulkan.hpp` (header C++) : téléchargé depuis Vulkan-Hpp v1.3.275,
copié dans le sysroot NDK Vulkan include.
3. `libomp.so` : copié depuis NDK clang/18/lib/linux/aarch64 vers tablette
(ggml-vulkan linked OpenMP).
Build cmake : `dist/build_ggml_vulkan.sh` produit `libggml-vulkan.so` (41 MB
avec shaders SPIR-V embarqués). Détection device :
```
ggml_vulkan: Found 1 Vulkan devices:
ggml_vulkan: 0 = Adreno (TM) 830 (Qualcomm Technologies Inc. Adreno Vulkan Driver)
uma: 1 | fp16: 1 | bf16: 0 | warp size: 64
shared memory: 32768 | int dot: 0 | matrix cores: none
```
Caractéristiques clés :
- **UMA=1** (Unified Memory Architecture) : même DRAM que CPU
- **fp16=1** OK ; bf16=0 ; **matrix cores: none** (juste compute shaders)
- Warp size 64
- L2/shared 32 KB
## Test 1 : decoder Vulkan buffer + CPU compute (sans sched)
KZTTS_DECODER_VULKAN=1 sans KZTTS_DECODER_SCHED=1 :
| Phrase | Total | RTF | WAV md5 |
|---|---:|---:|---|
| Bonjour Kazeia (baseline CPU) | 6.75 s | 2.45 | c3dd71... |
| Bonjour Kazeia + Vulkan buf | 6.80 s | 2.47 | c3dd71... ✓ |
| Bonsoir 41 frames + Vulkan buf | 8.34 s | 2.44 | — |
- Poids decoder sur Vulkan-allocated buffer (host-visible UMA)
- Compute reste CPU pur via `ggml_graph_compute_with_ctx`
- **WAV bit-exact identique au baseline**
- Perf : **neutre, marge bruit**
UMA = pas de gain BW exclusif (CPU et GPU partagent la DRAM). Poids alloués
par Vulkan driver ont peut-être un meilleur alignment/prefetch que CPU
malloc mais l'effet est non-mesurable (±2 ms par frame, bruit).
## Test 2 : decoder Vulkan + sched (GPU compute attendu)
KZTTS_DECODER_VULKAN=1 KZTTS_DECODER_SCHED=1 :
Sched créé avec `[Vulkan, CPU]`. Mais GGML_SCHED_DEBUG=2 montre :
```
node # 1 (CONT): bigvgan_input [CPU]
node # 2 (PAD): [CPU]
node # 3 (IM2COL): [CPU] -- decoder.initial.weight (21M) [CPU]
...
Stats : 14394 [CPU], 0 [Vulkan0]
```
**Tous les nœuds sur CPU**. Pourquoi ?
- Adreno UMA buffer is_host=true (Vulkan considère que c'est de la mémoire
CPU-accessible).
- Le sched logic (`ggml-backend.cpp:916-928`) : si weight is_host AND
src_backend_id == last (CPU) → offload candidat. Sinon → reste sur weight
backend.
- Avec UMA, `backend_from_buffer(vulkan_buf)` retourne probablement l'index
CPU (logique inversée pour host-visible buffers).
- Donc tous les ops tombent sur CPU.
Pour forcer compute Vulkan, faudrait soit :
- Refactor `stage_bigvgan_sched` pour appeler `ggml_backend_graph_compute(vk, gf)`
directement (mais alors ops non-supportées crashent au lieu de fallback CPU)
- Implémenter sched custom forcé
Hors scope pour ce test exploratoire.
## Test 3 : talker offload via llama.cpp (n_gpu_layers=99 → Vulkan)
KZTTS_TALKER_VULKAN=1 :
```
load_tensors: offloaded 29/29 layers to GPU
talker: Vulkan0 (GPU offload)
```
**29/29 couches sur GPU** confirmé. Mais perf :
| Étape | Baseline CPU | Talker Vulkan | Delta |
|---|---:|---:|---:|
| Prefill | 87 ms | **775 ms** | **+790%** |
| Talker/frame | 32 ms | 45 ms | +44% |
| CP/frame | 70 ms | 81 ms | +15% (thermal) |
| Decoder | 3.3 s | 6.6 s | +100% (thermal/GPU bus contention) |
| **Total** | **6.75 s** | **15.4 s** | **+128%** |
WAV md5 différent (a8f03d66 vs c3dd71cb) : GPU fp16 diverge légèrement
de CPU NEON f32.
Raisons de la régression massive :
1. **Talker = Qwen3-TTS 0.6B** : trop petit pour amortir overhead pipeline
GPU (kernel launch, sync, copy embedding/logits CPU↔GPU à chaque token).
2. **Decode batch=1** : pattern défavorable pour GPU compute shaders
(warps largement vides).
3. **Prefill 775 ms** : probablement compile shaders à chaud + premier
chargement des poids sur Adreno.
4. **GPU bus contention** : Adreno + CPU sur la même DRAM 25 GB/s effective ;
les jobs GPU saturent la BW ce qui ralentit aussi le CP et decoder CPU
qui tournent en parallèle.
## Verdict Vulkan
| Pattern | Gain net | Pourquoi |
|---|---|---|
| Vulkan buf + CPU compute | **0% (neutre)** | UMA = pas de BW gain exclusif |
| Vulkan compute via sched | impossible | sched route UMA → CPU automatiquement |
| Talker Vulkan offload | **-128% (régression massive)** | 0.6B + batch=1 + bus contention |
| BigVGAN Vulkan compute direct | **non testé** (faut refactor sched) | exigerait crash si ops non-supportées |
**Conclusion** : comme le HTP, Vulkan Adreno 830 sur SM8750 **n'apporte
pas de gain net** sur ce workload TTS. Trois facteurs :
1. **UMA** : pas de mémoire exclusive GPU, donc pas de gain BW
2. **Pas de matrix cores** : Adreno 830 fait du compute shader standard
(vs NEON 4-wide CPU)
3. **Workload TTS** : autorégressif batch=1, mauvais pour GPU paradigm
Cette confirmation valide définitivement l'orientation **NEON CPU optimisé**
comme la voie unique réaliste pour ce hardware sur ce TTS.
## Consommation énergétique (préoccupation Richard)
Non mesurée précisément, mais qualitativement :
- Vulkan talker offload : pipeline ×2.3 plus long. Probable consommation
ENERGIE × 2 (CPU encore actif pendant l'attente GPU).
- Vulkan buffer + CPU : conso similaire au baseline.
- NEON CPU optim : meilleure efficience énergétique (compute compact,
pas de driver overhead, pas de bus traffic supplémentaire).
**Reste sur NEON CPU pour la prod, conso optimale.**
## Path résiduels non explorés (par honnêteté)
- **BigVGAN compute forcé sur Vulkan via API directe** : skipper le sched,
appeler `ggml_backend_graph_compute(vk, gf)` directement. Ops non-supportées
crashent. Refactor lourd pour wrapping fallback.
- **Vulkan shader custom** pour conv1d optimisé pour Adreno spécifique :
chantier sérieux, semaines.
- **Mix CPU/GPU avec gros chunks** : faire petits ops sur CPU, gros matmul
isolés sur GPU. Demande analyse fine + refactor.
Aucun ne paraît livrable dans une session vu le pattern de régression
observé.
## Code committé
- `dist/build_ggml_vulkan.sh` : script build ggml-vulkan pour Android arm64
- `dist/jni/tts_engine.cpp` : nouveau path optionnel
- `KZTTS_VULKAN_LIB=/path/libggml-vulkan.so` : charge backend Vulkan
- `KZTTS_DECODER_VULKAN=1` : decoder utilise buft Vulkan (neutre)
- `KZTTS_TALKER_VULKAN=1` : talker offload (régresse, à éviter)
Tous opt-in, désactivés par défaut. Path principal (NEON CPU) intact.

37
dist/build_ggml_vulkan.sh vendored Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Build ggml-vulkan pour Android arm64 dans un répertoire de test.
# Cible : produire libggml-vulkan.so + précompiler les SPIR-V shaders.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
NDK="${ANDROID_NDK_ROOT:-/opt/Kazeia/android-ndk-r27d}"
SRC="/opt/Kazeia-engine/ql"
B="$HERE/b-vulkan"
mkdir -p "$B"
cd "$B"
# Vulkan SDK headers + lib viennent du NDK sysroot directement.
# glslc pour compiler les shaders compute en SPIR-V.
GLSLC="$NDK/shader-tools/linux-x86_64/glslc"
cmake "$SRC" \
-DCMAKE_TOOLCHAIN_FILE="$NDK/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-31 \
-DCMAKE_BUILD_TYPE=Release \
-DGGML_VULKAN=ON \
-DGGML_HEXAGON=OFF \
-DGGML_BACKEND_DL=ON \
-DBUILD_SHARED_LIBS=ON \
-DLLAMA_BUILD_TOOLS=OFF \
-DLLAMA_BUILD_EXAMPLES=OFF \
-DLLAMA_BUILD_SERVER=OFF \
-DLLAMA_BUILD_TESTS=OFF \
-DSPIRV-Headers_DIR=/tmp/spirv-install/share/cmake/SPIRV-Headers \
-DVulkan_GLSLC_EXECUTABLE="$GLSLC" \
-G Ninja 2>&1 | tail -20
echo ""
echo "=== cmake done, building libggml-vulkan ==="
ninja ggml-vulkan 2>&1 | tail -10
echo ""
find . -name "libggml-vulkan*" 2>/dev/null

View File

@ -182,6 +182,16 @@ TtsEngine * tts_engine_load(const TtsEngineLoadCfg & cfg) {
setenv("GGML_HEXAGON_USE_HMX", "0", 1); setenv("GGML_HEXAGON_USE_HMX", "0", 1);
llama_backend_init(); llama_backend_init();
// --- 2.5) Chargement dynamique du backend Vulkan (DOIT être AVANT talker load,
// pour que llama.cpp voit le device Adreno lors de mp.devices = {vk_dev, ...}).
if (const char * vk_path = getenv("KZTTS_VULKAN_LIB")) {
if (ggml_backend_load(vk_path)) {
fprintf(stderr, "Vulkan backend chargé : %s\n", vk_path);
} else {
fprintf(stderr, "ggml_backend_load(%s) FAIL\n", vk_path);
}
}
// --- 3) Tokenizer optionnel (vocab_only -> ~50 MB RAM) --- // --- 3) Tokenizer optionnel (vocab_only -> ~50 MB RAM) ---
if (cfg.vocab_gguf && *cfg.vocab_gguf) { if (cfg.vocab_gguf && *cfg.vocab_gguf) {
if (!kz_tok_load(eng->kz_tok, cfg.vocab_gguf)) { if (!kz_tok_load(eng->kz_tok, cfg.vocab_gguf)) {
@ -193,9 +203,34 @@ TtsEngine * tts_engine_load(const TtsEngineLoadCfg & cfg) {
// --- 4) Talker (libllama) --- // --- 4) Talker (libllama) ---
auto mp = llama_model_default_params(); auto mp = llama_model_default_params();
ggml_backend_dev_t devs[2] = { cfg.use_htp ? find_htp() : nullptr, nullptr }; // KZTTS_TALKER_VULKAN=1 -> offload talker sur GPU Adreno via Vulkan device.
if (devs[0]) { mp.n_gpu_layers = 99; mp.devices = devs; fprintf(stderr, "talker: HTP0\n"); } const bool talker_vulkan = (getenv("KZTTS_TALKER_VULKAN") && atoi(getenv("KZTTS_TALKER_VULKAN")) != 0);
else { mp.n_gpu_layers = 0; fprintf(stderr, "talker: CPU\n"); } ggml_backend_dev_t vk_dev = nullptr;
if (talker_vulkan) {
// Adreno UMA -> ggml-vulkan registre le device comme IGPU (type=2, integrated GPU
// utilisant la host memory). HTP s'enregistre comme GPU (type=1). On accepte les deux
// GPU et IGPU mais filtre par nom pour exclure HTP/Hexagon explicitement.
for (size_t i = 0; i < ggml_backend_dev_count(); ++i) {
auto d = ggml_backend_dev_get(i);
auto t = ggml_backend_dev_type(d);
if (t != GGML_BACKEND_DEVICE_TYPE_GPU && t != GGML_BACKEND_DEVICE_TYPE_IGPU) continue;
const char * nm = ggml_backend_dev_name(d);
if (strstr(nm, "HTP") || strstr(nm, "Hexagon")) continue;
vk_dev = d;
fprintf(stderr, "talker: GPU candidate found : %s (type=%d)\n", nm, (int)t);
break;
}
}
ggml_backend_dev_t devs[2] = {
vk_dev ? vk_dev : (cfg.use_htp ? find_htp() : nullptr),
nullptr
};
if (devs[0]) {
mp.n_gpu_layers = 99; mp.devices = devs;
fprintf(stderr, "talker: %s (GPU offload)\n", ggml_backend_dev_name(devs[0]));
} else {
mp.n_gpu_layers = 0; fprintf(stderr, "talker: CPU\n");
}
eng->talker_m = llama_model_load_from_file(cfg.talker_gguf, mp); eng->talker_m = llama_model_load_from_file(cfg.talker_gguf, mp);
if (!eng->talker_m) { fprintf(stderr, "talker load FAIL %s\n", cfg.talker_gguf); delete eng; return nullptr; } if (!eng->talker_m) { fprintf(stderr, "talker load FAIL %s\n", cfg.talker_gguf); delete eng; return nullptr; }
@ -256,16 +291,44 @@ TtsEngine * tts_engine_load(const TtsEngineLoadCfg & cfg) {
// Sinon : load() classique = CPU pur via ggml_graph_compute_with_ctx (path actuel). // Sinon : load() classique = CPU pur via ggml_graph_compute_with_ctx (path actuel).
const bool dec_htp = (getenv("KZTTS_DECODER_HTP") && atoi(getenv("KZTTS_DECODER_HTP")) != 0); const bool dec_htp = (getenv("KZTTS_DECODER_HTP") && atoi(getenv("KZTTS_DECODER_HTP")) != 0);
bool dec_ok; bool dec_ok;
if (dec_htp) { const bool dec_vulkan = (getenv("KZTTS_DECODER_VULKAN") && atoi(getenv("KZTTS_DECODER_VULKAN")) != 0);
if (dec_htp || dec_vulkan) {
std::vector<ggml_backend_t> dec_backends; std::vector<ggml_backend_t> dec_backends;
// 1er backend = HTP si dispo, sinon CPU seul (équivalent au load classique). // Vulkan prioritaire si demandé (Adreno GPU compute, BW partagée mémoire).
if (auto htp_dev = find_htp()) { if (dec_vulkan) {
ggml_backend_t htp = ggml_backend_dev_init(htp_dev, nullptr); // Note : Hexagon HTP est aussi enregistré comme DEVICE_TYPE_GPU. On filtre par
if (htp) { // nom non-HTP pour trouver le VRAI GPU (Adreno...). Liste tous les devs et choisis.
dec_backends.push_back(htp); fprintf(stderr, "Devices enregistrés (%zu):\n", ggml_backend_dev_count());
fprintf(stderr, "decoder: HTP backend initialisé\n"); for (size_t i = 0; i < ggml_backend_dev_count(); ++i) {
} else { auto d = ggml_backend_dev_get(i);
fprintf(stderr, "decoder: ggml_backend_dev_init(HTP0) FAIL, fallback CPU\n"); fprintf(stderr, " [%zu] %s (type=%d)\n", i,
ggml_backend_dev_name(d), (int)ggml_backend_dev_type(d));
}
for (size_t i = 0; i < ggml_backend_dev_count(); ++i) {
auto d = ggml_backend_dev_get(i);
auto t = ggml_backend_dev_type(d);
// Adreno UMA = type IGPU. HTP = type GPU. On accepte les deux et filtre par nom.
if (t != GGML_BACKEND_DEVICE_TYPE_GPU && t != GGML_BACKEND_DEVICE_TYPE_IGPU) continue;
const char * nm = ggml_backend_dev_name(d);
if (strstr(nm, "HTP") || strstr(nm, "Hexagon")) continue;
ggml_backend_t vk = ggml_backend_dev_init(d, nullptr);
if (vk) {
dec_backends.push_back(vk);
fprintf(stderr, "decoder: Vulkan backend initialisé (%s)\n", nm);
}
break;
}
}
// HTP (option backend si pas Vulkan ou ajout)
if (dec_htp) {
if (auto htp_dev = find_htp()) {
ggml_backend_t htp = ggml_backend_dev_init(htp_dev, nullptr);
if (htp) {
dec_backends.push_back(htp);
fprintf(stderr, "decoder: HTP backend initialisé\n");
} else {
fprintf(stderr, "decoder: ggml_backend_dev_init(HTP0) FAIL, fallback CPU\n");
}
} }
} }
// CPU TOUJOURS en DERNIER (ggml_backend_sched_new assert que backends[end]=CPU, // CPU TOUJOURS en DERNIER (ggml_backend_sched_new assert que backends[end]=CPU,