From 27d893c73f74b743516281592d2f5cf7e93ee3d2 Mon Sep 17 00:00:00 2001 From: Richard Loyer Date: Fri, 29 May 2026 22:28:16 +0200 Subject: [PATCH] =?UTF-8?q?Vulkan=20Adreno=20830=20test=C3=A9=20:=20neutre?= =?UTF-8?q?=20au=20mieux,=20r=C3=A9gresse=20au=20pire=20(commit=20aussi=20?= =?UTF-8?q?le=20code=20opt-in)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitignore | 1 + dist/PERF_VULKAN_TESTS.md | 171 ++++++++++++++++++++++++++++++++++++++ dist/build_ggml_vulkan.sh | 37 +++++++++ dist/jni/tts_engine.cpp | 87 ++++++++++++++++--- 4 files changed, 284 insertions(+), 12 deletions(-) create mode 100644 dist/PERF_VULKAN_TESTS.md create mode 100755 dist/build_ggml_vulkan.sh diff --git a/.gitignore b/.gitignore index aa82747..b35654b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ models/ b/ ql/ eval/out/ +dist/b-vulkan/ diff --git a/dist/PERF_VULKAN_TESTS.md b/dist/PERF_VULKAN_TESTS.md new file mode 100644 index 0000000..2144559 --- /dev/null +++ b/dist/PERF_VULKAN_TESTS.md @@ -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. diff --git a/dist/build_ggml_vulkan.sh b/dist/build_ggml_vulkan.sh new file mode 100755 index 0000000..a2aca5b --- /dev/null +++ b/dist/build_ggml_vulkan.sh @@ -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 diff --git a/dist/jni/tts_engine.cpp b/dist/jni/tts_engine.cpp index d71fdbb..5af26a7 100644 --- a/dist/jni/tts_engine.cpp +++ b/dist/jni/tts_engine.cpp @@ -182,6 +182,16 @@ TtsEngine * tts_engine_load(const TtsEngineLoadCfg & cfg) { setenv("GGML_HEXAGON_USE_HMX", "0", 1); 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) --- if (cfg.vocab_gguf && *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) --- auto mp = llama_model_default_params(); - ggml_backend_dev_t devs[2] = { cfg.use_htp ? find_htp() : nullptr, nullptr }; - if (devs[0]) { mp.n_gpu_layers = 99; mp.devices = devs; fprintf(stderr, "talker: HTP0\n"); } - else { mp.n_gpu_layers = 0; fprintf(stderr, "talker: CPU\n"); } + // KZTTS_TALKER_VULKAN=1 -> offload talker sur GPU Adreno via Vulkan device. + const bool talker_vulkan = (getenv("KZTTS_TALKER_VULKAN") && atoi(getenv("KZTTS_TALKER_VULKAN")) != 0); + 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); 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). const bool dec_htp = (getenv("KZTTS_DECODER_HTP") && atoi(getenv("KZTTS_DECODER_HTP")) != 0); 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 dec_backends; - // 1er backend = HTP si dispo, sinon CPU seul (équivalent au load classique). - 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"); + // Vulkan prioritaire si demandé (Adreno GPU compute, BW partagée mémoire). + if (dec_vulkan) { + // Note : Hexagon HTP est aussi enregistré comme DEVICE_TYPE_GPU. On filtre par + // nom non-HTP pour trouver le VRAI GPU (Adreno...). Liste tous les devs et choisis. + fprintf(stderr, "Devices enregistrés (%zu):\n", ggml_backend_dev_count()); + for (size_t i = 0; i < ggml_backend_dev_count(); ++i) { + auto d = ggml_backend_dev_get(i); + 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,