From bdd528ed2523810eda7165c347a4959c2e644d7d Mon Sep 17 00:00:00 2001 From: Richard Loyer Date: Fri, 29 May 2026 14:05:35 +0200 Subject: [PATCH] =?UTF-8?q?chantier=20BigVGAN=20HMX=20Phase=202b:=20refact?= =?UTF-8?q?or=20sched-friendly=20+=20limite=20backend=20d=C3=A9couverte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor stage_bigvgan en variante stage_bigvgan_sched : - ggml_init(no_alloc=true) + mem_size réduit 12 GB -> 16 MB - ggml_set_input/output sur input/output - causal_conv1d_TC : ggml_pad_ext au lieu de memset zeros + concat - apply_snake_TC : lookup d.snake_consts pré-calculé (au lieu de host exp() per-frame) - Pattern sched_reset + alloc_graph + tensor_set + compute + tensor_get - Switch dans Decoder::forward : sched ? stage_bigvgan_sched : stage_bigvgan Snake constants pré-calculées au load_with_backends : - 29 paires (a_eff, inv_b) stockées dans ctx_const + buf_const sur le même backend que buf_w. exp() host-side une fois au load au lieu de chaque frame. - buf_const ~0.1 MB sur HTP0 (29 × 2 × C × f32 avec C de 96 à 1536). Mesure tablette (KZTTS_DECODER_HTP=1 KZTTS_DECODER_SCHED=1 GGML_HEXAGON_USE_HMX=1) : baseline CPU pur : decoder 3.34s, bigvgan 3.19s sched HTP path neuf : decoder 3.95s, bigvgan 3.76s -> REGRESSION 18% Cause : GGML_SCHED_DEBUG=2 révèle que TOUS les MUL_MAT tombent sur CPU. ggml_hexagon_supported_mul_mat rejette ggml_nrows(src1) > 1024 (commentaire 'no huge batches (for now)'). BigVGAN block 3 résiduels : conv1d k=7 sur T=59520, im2col_output [672, 59520] -> 59520 rows >> 1024. Tous les MUL_MAT BigVGAN sont rejetés -> fallback CPU + copies inutiles -> régression. PHASE 2b STATUT : refactor fait et stable, mais le gain HMX nécessite soit (a) modifier ggml-hexagon pour gros batches, (b) splitter MUL_MAT en chunks ≤ 1024, (c) reprendre via étape F (CP HMX) qui n'a pas ce problème (nrows ≤ 16 par sub-forward). Snapshots updatés dans dist/decoder_patches/. Le path activé seulement si KZTTS_DECODER_SCHED=1 (opt-in, désactivé par défaut). En mode KZTTS_DECODER_HTP=1 sans SCHED : path legacy CPU pur, perf inchangée vs baseline (poids juste relogés sur HTP buffer via opt_hostbuf=1). Co-Authored-By: Claude Opus 4.7 (1M context) --- dist/decoder_patches/README.md | 48 ++++-- dist/decoder_patches/decoder.cpp.snapshot | 159 +++++++++++++++++- dist/decoder_patches/decoder.h.snapshot | 9 + dist/decoder_patches/gguf_loader.cpp.snapshot | 73 +++++++- 4 files changed, 271 insertions(+), 18 deletions(-) diff --git a/dist/decoder_patches/README.md b/dist/decoder_patches/README.md index 824decf..2ecd85f 100644 --- a/dist/decoder_patches/README.md +++ b/dist/decoder_patches/README.md @@ -37,26 +37,42 @@ Phase 2 du chantier BigVGAN HMX. | `KZTTS_DECODER_HTP=1` | poids alloués sur HTP buffer, compute CPU pur via `opt_hostbuf=1` (HTP buffer CPU-mappable). Pas de gain HMX mais valide la chaîne. | | `KZTTS_DECODER_HTP=1 KZTTS_DECODER_SCHED=1` | sched créé, mais stages encore en compute_with_ctx → assertion `buffer_id >= 0` au premier compute. **NE FONCTIONNE PAS** sans refactor stages. | -## Reste à faire (Phase 2 session suivante) +## Phase 2b livrée — mais bloquée par limite backend -Refactor de `stage_bigvgan` (et causal_conv1d_TC, apply_snake_TC, -precompute_snake) pour le pattern sched : +Refactor stage_bigvgan en pattern sched-friendly **fait** : +1. ✅ snake constants pré-calculées au load (`ctx_const` + `buf_const`) +2. ✅ `stage_bigvgan_sched` (variante du stage_bigvgan legacy) +3. ✅ `ggml_init(no_alloc=true)` + `ggml_set_input/output` +4. ✅ `memset zeros + concat` remplacé par `ggml_pad_ext` +5. ✅ Wrapping sched_reset + alloc + tensor_set + compute + tensor_get +6. ✅ Switch dans `Decoder::forward` : `d.sched ? stage_bigvgan_sched : stage_bigvgan` -1. `ggml_init(no_alloc=true)` avec mem_size réduit (metadata only) -2. `ggml_set_input(input_tensor)` sur l'input host -3. `ggml_set_output(output_tensor)` sur l'output final -4. Remplacer `memset(zeros->data)` + `ggml_concat` par `ggml_pad_ext` -5. Remplacer `precompute_snake` host (exp, 1/x) par ops in-graph - `ggml_exp + ggml_div` -6. Wrapping `sched_reset` + `sched_alloc_graph` + - `ggml_backend_tensor_set(input)` + `sched_graph_compute` + - `ggml_backend_tensor_get(output)` +**MAIS** : mesure montre que TOUS les nœuds tombent sur CPU malgré le sched. +Le sched_debug (`GGML_SCHED_DEBUG=2`) confirme : + - poids decoder bien sur HTP0 (buf_w 325 MB + buf_const 0.1 MB) + - MUL_MAT/IM2COL/PAD/ADD assignés à CPU + - decoder time = 3.95s (régression vs baseline 3.34s due aux copies inutiles) -Snake `sin` n'est pas supporté HTP → sched fallback automatique vers CPU -pour cette op. Le ping-pong est borné (snake = 0.4 Gops total sur 115). +**Cause** : `ggml_hexagon_supported_mul_mat` rejette les MUL_MAT avec +`ggml_nrows(src1) > 1024` (commentaire "no huge batches (for now)"). -Estimation : ~70 lignes propres, ~1 session focalisée. Cibles : decoder -3.2s → ~0.5-0.8s (gain ×4-6 réaliste avec HMX sur MUL_MAT). +BigVGAN block 3 résiduels font conv1d k=7 sur T=59520 → im2col output a 59520 +rows × 672 cols → `ggml_nrows(src1) >> 1024`. Tous les MUL_MAT BigVGAN sont +au-dessus de la limite, donc rejetés par HTP, fallback CPU. + +## Reprise Phase 2b plus tard + +Trois options pour débloquer : + +**(a) Modifier ggml-hexagon** pour supporter les gros batches MUL_MAT. +Sous-chantier kernel hexagon (VTCM tiling, copies multiples). Semaines. + +**(b) Split BigVGAN MUL_MAT en chunks ≤ 1024 rows** côté graphe decoder. +Refactor + copies CPU↔HTP par chunk. Gain incertain. + +**(c) Reprendre via étape F (CP HMX)** d'abord. CP a des MUL_MAT +nrows(src1) ≤ 16 (1 prefill 2 tokens + 14 decode 1 token), largement +sous la limite. Pattern identique au talker Qwen3.5 déjà validé. ## Rebuild diff --git a/dist/decoder_patches/decoder.cpp.snapshot b/dist/decoder_patches/decoder.cpp.snapshot index 66056a6..0389276 100644 --- a/dist/decoder_patches/decoder.cpp.snapshot +++ b/dist/decoder_patches/decoder.cpp.snapshot @@ -514,6 +514,159 @@ static std::vector stage_bigvgan(Decoder & d, const std::vector & return wav; } +// ============== Variante sched (Phase 2b BigVGAN HMX) ============== +// stage_bigvgan_sched : pattern sched-friendly pour activer le routage MUL_MAT +// vers HTP HMX. Diffs avec stage_bigvgan : +// - ggml_init no_alloc=true + mem_size réduit (~4 MB) +// - ggml_set_input/output sur entrée/sortie +// - causal_conv1d_TC : ggml_pad_ext au lieu de memset zeros + ggml_concat +// - precompute_snake : lookup dans d.snake_consts (précalculé au load) +// - exécution : sched_reset + sched_alloc_graph + tensor_set + sched_compute + tensor_get +// Le sched split automatiquement : MUL_MAT/ADD/MUL/PAD -> HTP HMX, +// IM2COL/SIN/CONV_TRANSPOSE_1D -> CPU. Copies CPU<->HTP gérées par sched. +static std::vector stage_bigvgan_sched(Decoder & d, const std::vector & us_out, int T_in_us) { + const int latent = d.cfg.latent_dim; // 1024 + const int decoder_dim = d.cfg.decoder_dim; // 1536 + const std::vector & up_rates = d.cfg.upsample_rates; + + // mem_size : juste métadonnées tensors (no_alloc=true). BigVGAN a ~30 snake + 26 conv1d + // + 4 convtr + adds → ~1000 nœuds max. ~256 octets/nœud → ~256 KB ; on prend 16 MB safe. + size_t mem_size = 16ULL * 1024 * 1024; + struct ggml_init_params p = { mem_size, nullptr, /*no_alloc=*/true }; + struct ggml_context * ctx = ggml_init(p); + + auto get = [&](const std::string & key) { return d.tensors[key]; }; + + auto apply_snake_TC = [&](ggml_tensor * x_TC, const std::string & prefix) -> ggml_tensor * { + auto it = d.snake_consts.find(prefix); + if (it == d.snake_consts.end()) { + fprintf(stderr, "bigvgan_sched: snake const not found for %s\n", prefix.c_str()); + return x_TC; + } + ggml_tensor * a_eff = it->second.first; // [C], précalculé exp(α) + ggml_tensor * inv_b = it->second.second; // [C], précalculé 1/(exp(β)+eps) + const int C = (int)a_eff->ne[0]; + ggml_tensor * a2 = ggml_reshape_2d(ctx, a_eff, 1, C); + ggml_tensor * b2 = ggml_reshape_2d(ctx, inv_b, 1, C); + ggml_tensor * xa = ggml_mul(ctx, x_TC, a2); + ggml_tensor * s = ggml_sin(ctx, xa); // SIN -> CPU fallback via sched + ggml_tensor * s2 = ggml_mul(ctx, s, s); + ggml_tensor * mod = ggml_mul(ctx, s2, b2); + return ggml_add(ctx, x_TC, mod); + }; + + auto causal_conv1d_TC = [&](ggml_tensor * x_TC, const std::string & w_key, const std::string & b_key, + int K, int dilation) -> ggml_tensor * { + ggml_tensor * w = get(w_key); + ggml_tensor * b = get(b_key); + const int C_out = (int)w->ne[2]; + const int T_cur = (int)x_TC->ne[0]; + ggml_tensor * x_pad; + if (K == 1) { + x_pad = x_TC; + } else { + const int pad_left = (K - 1) * dilation; + // Pad gauche sur dim 0 (T) par pad_left zéros — équivalent à concat(zeros, x_TC) + // mais en pur graphe (compatible no_alloc=true). + x_pad = ggml_pad_ext(ctx, x_TC, pad_left, 0, 0, 0, 0, 0, 0, 0); + } + ggml_tensor * y = ggml_conv_1d(ctx, w, x_pad, 1, 0, dilation); + const int T_out_y = (int)y->ne[0]; + if (T_out_y > T_cur) { + y = ggml_view_2d(ctx, y, T_cur, C_out, y->nb[1], 0); + } + ggml_tensor * b2 = ggml_reshape_2d(ctx, b, 1, C_out); + return ggml_add(ctx, y, b2); + }; + + auto causal_convtr1d_TC = [&](ggml_tensor * x_TC, const std::string & w_key, const std::string & b_key, + int s) -> ggml_tensor * { + ggml_tensor * w = get(w_key); + ggml_tensor * b = get(b_key); + const int C_out = (int)w->ne[1]; + const int T_cur = (int)x_TC->ne[0]; + ggml_tensor * y = ggml_conv_transpose_1d(ctx, w, x_TC, s, 0, 1); + const int T_full = (int)y->ne[0]; + const int T_target = T_cur * s; + if (T_full > T_target) { + y = ggml_view_2d(ctx, y, T_target, C_out, y->nb[1], 0); + } + ggml_tensor * b2 = ggml_reshape_2d(ctx, b, 1, C_out); + return ggml_add(ctx, y, b2); + }; + + // --- Input --- + ggml_tensor * x = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, latent, T_in_us); + ggml_set_name(x, "bigvgan_input"); + ggml_set_input(x); // CRITIQUE : marque comme input pour sched_alloc_graph + + // Switch to TC layout + ggml_tensor * x_TC = ggml_cont(ctx, ggml_transpose(ctx, x)); + + // initial Conv k=7 + x_TC = causal_conv1d_TC(x_TC, "decoder.initial.weight", "decoder.initial.bias", 7, 1); + + // 4 DecoderBlocks + for (int B = 0; B < 4; B++) { + char prefix[64]; snprintf(prefix, sizeof(prefix), "decoder.block.%d", B); + const int up = up_rates[B]; + + x_TC = apply_snake_TC(x_TC, std::string(prefix) + ".block.0"); + x_TC = causal_convtr1d_TC(x_TC, + std::string(prefix) + ".block.1.conv.weight", + std::string(prefix) + ".block.1.conv.bias", up); + + const int dilations[3] = {1, 3, 9}; + for (int R = 0; R < 3; R++) { + char rprefix[80]; snprintf(rprefix, sizeof(rprefix), "%s.block.%d", prefix, 2 + R); + ggml_tensor * res = x_TC; + x_TC = apply_snake_TC(x_TC, std::string(rprefix) + ".act1"); + x_TC = causal_conv1d_TC(x_TC, std::string(rprefix) + ".conv1.conv.weight", + std::string(rprefix) + ".conv1.conv.bias", 7, dilations[R]); + x_TC = apply_snake_TC(x_TC, std::string(rprefix) + ".act2"); + x_TC = causal_conv1d_TC(x_TC, std::string(rprefix) + ".conv2.conv.weight", + std::string(rprefix) + ".conv2.conv.bias", 1, 1); + x_TC = ggml_add(ctx, res, x_TC); + } + } + + // final SnakeBeta + Conv k=7 + x_TC = apply_snake_TC(x_TC, "decoder.final_act"); + x_TC = causal_conv1d_TC(x_TC, "decoder.final_conv.weight", "decoder.final_conv.bias", 7, 1); + + ggml_set_name(x_TC, "bigvgan_output"); + ggml_set_output(x_TC); + + struct ggml_cgraph * gf = ggml_new_graph_custom(ctx, 16384, false); + ggml_build_forward_expand(gf, x_TC); + + // --- Sched execution --- + ggml_backend_sched_reset(d.sched); + if (!ggml_backend_sched_alloc_graph(d.sched, gf)) { + fprintf(stderr, "bigvgan_sched: sched_alloc_graph FAIL\n"); + ggml_free(ctx); return {}; + } + // Set input data AFTER alloc (le tensor a maintenant un backend buffer) + ggml_backend_tensor_set(x, us_out.data(), 0, (size_t)latent * T_in_us * sizeof(float)); + if (ggml_backend_sched_graph_compute(d.sched, gf) != GGML_STATUS_SUCCESS) { + fprintf(stderr, "bigvgan_sched: sched_graph_compute FAIL\n"); + ggml_free(ctx); return {}; + } + // Get output + const int T_final = (int)x_TC->ne[0]; + std::vector wav(T_final); + ggml_backend_tensor_get(x_TC, wav.data(), 0, (size_t)T_final * sizeof(float)); + // Clamp [-1, 1] + for (int t = 0; t < T_final; t++) { + if (wav[t] > 1.0f) wav[t] = 1.0f; + if (wav[t] < -1.0f) wav[t] = -1.0f; + } + + ggml_free(ctx); + (void)decoder_dim; + return wav; +} + // ============== Forward complet : chaîne staging strict ============== // Dump an array of floats to disk if env var DECODER_DEBUG_DUMP_DIR is set. @@ -554,7 +707,11 @@ std::vector Decoder::forward(const std::vector& codes_flat, int auto us_out = stage_upsample(*this, pt_out, T); // [1024, T*4] t4 = nows(); debug_dump("stage4_upsample", us_out); - auto wav = stage_bigvgan(*this, us_out, T * 4); // T*1920 samples + // BigVGAN : si sched actif (KZTTS_DECODER_SCHED=1), variante sched-friendly + // qui route les MUL_MAT sur HTP HMX. Sinon, path legacy CPU pur. + std::vector wav = sched + ? stage_bigvgan_sched(*this, us_out, T * 4) + : stage_bigvgan (*this, us_out, T * 4); t5 = nows(); debug_dump("stage5_wav", wav); diff --git a/dist/decoder_patches/decoder.h.snapshot b/dist/decoder_patches/decoder.h.snapshot index fc4ccab..754d2ad 100644 --- a/dist/decoder_patches/decoder.h.snapshot +++ b/dist/decoder_patches/decoder.h.snapshot @@ -50,6 +50,15 @@ struct Decoder { struct ggml_backend_sched * sched = nullptr; bool owns_backends = false; // si true, unload() libère aussi backends[] + // Snake constants pré-calculées au load_with_backends pour rendre + // stage_bigvgan sched-friendly. Sans ça, precompute_snake host-side ne + // marche pas avec ctx no_alloc=true. Map clé "{prefix}.alpha" -> (a_eff, inv_b) + // avec a_eff = exp(α), inv_b = 1 / (exp(β) + eps). + // Alloués dans ctx_const + buf_const sur le même backend que buf_w. + struct ggml_context * ctx_const = nullptr; + struct ggml_backend_buffer * buf_const = nullptr; + std::unordered_map> snake_consts; + // load() utilise le backend CPU par défaut. Pour Hexagon/Vulkan, passer // un buft explicite via load_with_buft(), OU une liste de backends via // load_with_backends() (recommandé pour HTP : crée le sched + buft auto). diff --git a/dist/decoder_patches/gguf_loader.cpp.snapshot b/dist/decoder_patches/gguf_loader.cpp.snapshot index 91ef920..2bdf614 100644 --- a/dist/decoder_patches/gguf_loader.cpp.snapshot +++ b/dist/decoder_patches/gguf_loader.cpp.snapshot @@ -7,6 +7,7 @@ #include "decoder.h" #include #include +#include #include #include #include @@ -123,6 +124,73 @@ bool Decoder::load_with_backends(const std::string & path, ggml_backend_buffer_type_t buft_w = ggml_backend_get_default_buffer_type(backends[0]); if (!load_with_buft(path, buft_w)) return false; + // --- Précalcul snake constants (Phase 2b) --- + // Snake activation = x + sin²(α*x)/β. Le code historique faisait host-side + // exp() à chaque frame dans precompute_snake (incompatible avec sched + // no_alloc=true). On précalcule a_eff = exp(α), inv_b = 1/(exp(β)+eps) UNE FOIS + // ici, stockés dans ctx_const + buf_const sur le même backend que buf_w. + // Le forward stage_bigvgan les utilise comme des poids normaux. + { + const float SNAKE_EPS = 1e-9f; + // 1. Recenser tous les (alpha_key, beta_key) en parcourant les tensors. + struct SnakePair { std::string prefix; ggml_tensor * alpha; ggml_tensor * beta; }; + std::vector pairs; + for (auto & kv : tensors) { + const std::string & name = kv.first; + const std::string suf = ".alpha"; + if (name.size() > suf.size() && name.compare(name.size()-suf.size(), suf.size(), suf) == 0) { + std::string prefix = name.substr(0, name.size() - suf.size()); + std::string beta_name = prefix + ".beta"; + auto bit = tensors.find(beta_name); + if (bit != tensors.end()) { + pairs.push_back({prefix, kv.second, bit->second}); + } + } + } + // 2. Allouer ctx_const : 2 tensors par paire (a_eff, inv_b), tous f32 [C]. + size_t mem_meta = (2 * pairs.size() + 4) * ggml_tensor_overhead(); + ggml_init_params pc{mem_meta, nullptr, /*no_alloc=*/true}; + ctx_const = ggml_init(pc); + std::vector> created; + created.reserve(pairs.size()); + for (auto & p : pairs) { + const int C = (int)p.alpha->ne[0]; + ggml_tensor * a_eff = ggml_new_tensor_1d(ctx_const, GGML_TYPE_F32, C); + ggml_tensor * inv_b = ggml_new_tensor_1d(ctx_const, GGML_TYPE_F32, C); + ggml_set_name(a_eff, (p.prefix + ".a_eff").c_str()); + ggml_set_name(inv_b, (p.prefix + ".inv_b").c_str()); + created.push_back({a_eff, inv_b}); + } + // 3. Allouer buf_const sur le même backend que buf_w. + buf_const = ggml_backend_alloc_ctx_tensors_from_buft(ctx_const, buft_w); + if (!buf_const) { + fprintf(stderr, "load_with_backends: snake buf_const alloc FAIL\n"); + return false; + } + ggml_backend_buffer_set_usage(buf_const, GGML_BACKEND_BUFFER_USAGE_WEIGHTS); + // 4. Calculer host-side puis tensor_set. + std::vector tmp_a, tmp_b; + for (size_t i = 0; i < pairs.size(); ++i) { + const int C = (int)pairs[i].alpha->ne[0]; + // Lire α et β depuis leur backend buffer (opt_hostbuf=1 -> mappable CPU). + tmp_a.resize(C); tmp_b.resize(C); + ggml_backend_tensor_get(pairs[i].alpha, tmp_a.data(), 0, C * sizeof(float)); + ggml_backend_tensor_get(pairs[i].beta, tmp_b.data(), 0, C * sizeof(float)); + std::vector a_eff(C), inv_b(C); + for (int c = 0; c < C; ++c) { + a_eff[c] = std::exp(tmp_a[c]); + inv_b[c] = 1.0f / (std::exp(tmp_b[c]) + SNAKE_EPS); + } + ggml_backend_tensor_set(created[i].first, a_eff.data(), 0, C * sizeof(float)); + ggml_backend_tensor_set(created[i].second, inv_b.data(), 0, C * sizeof(float)); + snake_consts[pairs[i].prefix] = created[i]; + } + fprintf(stderr, "Decoder: %zu snake constants pré-calculées (buf_const %.1f MB sur %s)\n", + pairs.size(), + ggml_backend_buffer_get_size(buf_const) / 1048576.0, + ggml_backend_buffer_name(buf_const)); + } + // Mode minimal d'essai : KZTTS_DECODER_SCHED=1 active un vrai sched (refactor stages requis). // Par défaut : pas de sched, on profite juste de opt_hostbuf=1 (HTP buffer CPU-mappé) // pour que ggml_graph_compute_with_ctx CPU pur lise directement les poids HTP. @@ -164,7 +232,10 @@ void Decoder::compute_graph(struct ggml_context * ctx, struct ggml_cgraph * gf, } void Decoder::unload() { - if (sched) { ggml_backend_sched_free(sched); sched = nullptr; } + if (sched) { ggml_backend_sched_free(sched); sched = nullptr; } + if (buf_const) { ggml_backend_buffer_free(buf_const); buf_const = nullptr; } + if (ctx_const) { ggml_free(ctx_const); ctx_const = nullptr; } + snake_consts.clear(); if (buf_w) { ggml_backend_buffer_free(buf_w); buf_w = nullptr; } if (ctx_w) { ggml_free(ctx_w); ctx_w = nullptr; } if (ctx_g) { ggml_free(ctx_g); ctx_g = nullptr; }