chantier BigVGAN HMX Phase 2b: refactor sched-friendly + limite backend découverte

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) <noreply@anthropic.com>
This commit is contained in:
Richard Loyer 2026-05-29 14:05:35 +02:00
parent 9de6b76838
commit bdd528ed25
4 changed files with 271 additions and 18 deletions

View File

@ -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` | 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. | | `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, Refactor stage_bigvgan en pattern sched-friendly **fait** :
precompute_snake) pour le pattern sched : 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) **MAIS** : mesure montre que TOUS les nœuds tombent sur CPU malgré le sched.
2. `ggml_set_input(input_tensor)` sur l'input host Le sched_debug (`GGML_SCHED_DEBUG=2`) confirme :
3. `ggml_set_output(output_tensor)` sur l'output final - poids decoder bien sur HTP0 (buf_w 325 MB + buf_const 0.1 MB)
4. Remplacer `memset(zeros->data)` + `ggml_concat` par `ggml_pad_ext` - MUL_MAT/IM2COL/PAD/ADD assignés à CPU
5. Remplacer `precompute_snake` host (exp, 1/x) par ops in-graph - decoder time = 3.95s (régression vs baseline 3.34s due aux copies inutiles)
`ggml_exp + ggml_div`
6. Wrapping `sched_reset` + `sched_alloc_graph` +
`ggml_backend_tensor_set(input)` + `sched_graph_compute` +
`ggml_backend_tensor_get(output)`
Snake `sin` n'est pas supporté HTP → sched fallback automatique vers CPU **Cause** : `ggml_hexagon_supported_mul_mat` rejette les MUL_MAT avec
pour cette op. Le ping-pong est borné (snake = 0.4 Gops total sur 115). `ggml_nrows(src1) > 1024` (commentaire "no huge batches (for now)").
Estimation : ~70 lignes propres, ~1 session focalisée. Cibles : decoder BigVGAN block 3 résiduels font conv1d k=7 sur T=59520 → im2col output a 59520
3.2s → ~0.5-0.8s (gain ×4-6 réaliste avec HMX sur MUL_MAT). 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 ## Rebuild

View File

@ -514,6 +514,159 @@ static std::vector<float> stage_bigvgan(Decoder & d, const std::vector<float> &
return wav; 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<float> stage_bigvgan_sched(Decoder & d, const std::vector<float> & 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<int> & 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<float> 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 ============== // ============== Forward complet : chaîne staging strict ==============
// Dump an array of floats to disk if env var DECODER_DEBUG_DUMP_DIR is set. // Dump an array of floats to disk if env var DECODER_DEBUG_DUMP_DIR is set.
@ -554,7 +707,11 @@ std::vector<float> Decoder::forward(const std::vector<int32_t>& codes_flat, int
auto us_out = stage_upsample(*this, pt_out, T); // [1024, T*4] auto us_out = stage_upsample(*this, pt_out, T); // [1024, T*4]
t4 = nows(); t4 = nows();
debug_dump("stage4_upsample", us_out); 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<float> wav = sched
? stage_bigvgan_sched(*this, us_out, T * 4)
: stage_bigvgan (*this, us_out, T * 4);
t5 = nows(); t5 = nows();
debug_dump("stage5_wav", wav); debug_dump("stage5_wav", wav);

View File

@ -50,6 +50,15 @@ struct Decoder {
struct ggml_backend_sched * sched = nullptr; struct ggml_backend_sched * sched = nullptr;
bool owns_backends = false; // si true, unload() libère aussi backends[] 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<std::string, std::pair<struct ggml_tensor*, struct ggml_tensor*>> snake_consts;
// load() utilise le backend CPU par défaut. Pour Hexagon/Vulkan, passer // 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 // 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). // load_with_backends() (recommandé pour HTP : crée le sched + buft auto).

View File

@ -7,6 +7,7 @@
#include "decoder.h" #include "decoder.h"
#include <ggml-backend.h> #include <ggml-backend.h>
#include <ggml-cpu.h> #include <ggml-cpu.h>
#include <cmath>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <vector> #include <vector>
@ -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]); ggml_backend_buffer_type_t buft_w = ggml_backend_get_default_buffer_type(backends[0]);
if (!load_with_buft(path, buft_w)) return false; 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<SnakePair> 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<std::pair<ggml_tensor*, ggml_tensor*>> 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<float> 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<float> 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). // 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é) // 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. // pour que ggml_graph_compute_with_ctx CPU pur lise directement les poids HTP.
@ -165,6 +233,9 @@ void Decoder::compute_graph(struct ggml_context * ctx, struct ggml_cgraph * gf,
void Decoder::unload() { 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 (buf_w) { ggml_backend_buffer_free(buf_w); buf_w = nullptr; }
if (ctx_w) { ggml_free(ctx_w); ctx_w = nullptr; } if (ctx_w) { ggml_free(ctx_w); ctx_w = nullptr; }
if (ctx_g) { ggml_free(ctx_g); ctx_g = nullptr; } if (ctx_g) { ggml_free(ctx_g); ctx_g = nullptr; }