#pragma once // speaker-encoder-forward.h: ECAPA-TDNN forward graph in GGML. // // Mirrors qwen_tts.core.models.modeling_qwen3_tts.Qwen3TTSSpeakerEncoder // for the single utterance unbatched path. The forward fuses the mel // spectrogram extraction so the whole pipeline lives in one graph : // // audio [T_pad] f32 // -> mel [128, T_frames] (audio-mel.h) // -> conv0 TDNN k=5 + ReLU [512, T_frames] // -> SE-Res2Net dil=2 [512, T_frames] // -> SE-Res2Net dil=3 [512, T_frames] // -> SE-Res2Net dil=4 [512, T_frames] // -> cat blk[1..3] + MFA k=1 + ReLU [1536, T_frames] // -> ASP attentive pooling [3072, 1] // -> FC k=1 [2048, 1] // -> squeeze [2048] // // Tensor convention: [C, T] inside the graph (ne[0]=C, ne[1]=T) so that // ggml_im2col reads each Conv1d along the time axis and ggml_mul_mat // contracts over the input channel axis. This matches the layout the // upstream PyTorch code uses after its (1, 2) transpose. #include "audio-mel.h" #include "ggml-alloc.h" #include "ggml-backend.h" #include "ggml.h" #include "speaker-encoder-weights.h" #include #include #include // Conv1d k=K with padding="same" mode="reflect" + bias add. The weight // tensor lives in upstream layout [K, in_c, out_c]. We implement it with // reflect pad + im2col + matmul. // // x [in_c, T] input // w [K, in_c, out_c] weights // b [out_c] bias (broadcast over T) // Returns [out_c, T] // // Padding for "same" with kernel K and dilation d is (K - 1) * d / 2 on // each side (PyTorch convention, kernel size always odd here so the // division is exact). static struct ggml_tensor * spk_conv1d_same(struct ggml_context * ctx, struct ggml_tensor * x, struct ggml_tensor * w, struct ggml_tensor * b, int dilation) { const int K = (int) w->ne[0]; const int IC = (int) w->ne[1]; const int OC = (int) w->ne[2]; const int pad = ((K - 1) * dilation) / 2; // ggml_pad_reflect_1d pads the innermost axis ne[0]. Our temporal // axis is ne[1], so we transpose to bring T to ne[0], pad, and keep // it that way: the im2col downstream expects ne[0]=T_pad, ne[1]=IC, // which is exactly the layout we end up with here. struct ggml_tensor * x_t = ggml_cont(ctx, ggml_transpose(ctx, x)); // ne=(T, IC) if (pad > 0) { x_t = ggml_pad_reflect_1d(ctx, x_t, pad, pad); // ne=(T+2*pad, IC) } // Reshape to 4D for ggml_im2col 1D: ne=(T_pad, IC, 1, 1). struct ggml_tensor * x4d = ggml_reshape_4d(ctx, x_t, x_t->ne[0], IC, 1, 1); // Dummy F32 kernel with the (K, IC) shape ggml_im2col needs to read // off both axes. Borrowing only the ne and not the real weight data // avoids the impl's src0 type assert when w is quantized. struct ggml_tensor * dummy = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, K, IC, 1, 1); ggml_set_name(dummy, "spk.im2col_kernel"); // im2col with is_2D=false: the constructor declares ne[0]=IC*K and // ne[1]=OW, and the impl writes the buffer in (k inner, ic middle, // t outer) order, which matches that ne directly. A reshape_2d to // (IC*K, T_out) reads col_2d[ic*K + k, t], lining up with the // weight reshape w_2d[ic*K + k, oc] for the mul_mat below. struct ggml_tensor * col = ggml_im2col(ctx, dummy, x4d, 1, 1, 0, 0, dilation, 1, false, GGML_TYPE_F32); int T_out = (int) col->ne[1]; col = ggml_reshape_2d(ctx, col, K * IC, T_out); // Weight reshape: [K, IC, OC] -> [K*IC, OC]. mul_mat returns [OC, T_out]. struct ggml_tensor * w2d = ggml_reshape_2d(ctx, w, K * IC, OC); struct ggml_tensor * y = ggml_mul_mat(ctx, w2d, col); ggml_mul_mat_set_prec(y, GGML_PREC_F32); // Add bias broadcast over T_out. b is [out_c], reshape [out_c, 1]. struct ggml_tensor * b2d = ggml_reshape_2d(ctx, b, OC, 1); y = ggml_add(ctx, y, b2d); return y; } // TDNN block: Conv1d(same, reflect) + ReLU. Used both as the conv0 // frontend (k=5) and inside SE-Res2Net (k=1) and the MFA / ASP TDNNs. static struct ggml_tensor * spk_tdnn(struct ggml_context * ctx, const SpkEncTDNN & t, struct ggml_tensor * x, int dilation) { struct ggml_tensor * y = spk_conv1d_same(ctx, x, t.weight, t.bias, dilation); y = ggml_relu(ctx, y); return y; } // Res2Net block: split the channel axis in 8 chunks. chunk 0 passes // through, chunk 1 goes through TDNN[0], chunks 2..7 mix with the // previous chunk output before going through TDNN[i-1]. The 7 TDNN // branches share dilation but operate on hidden / 8 channels each. // // x [C, T] // Returns [C, T] static struct ggml_tensor * spk_res2net(struct ggml_context * ctx, const SpkEncRes2Net & rn, struct ggml_tensor * x, int dilation, int scale) { const int C = (int) x->ne[0]; const int T = (int) x->ne[1]; const int Cs = C / scale; std::vector outs; outs.reserve(scale); // chunk i is the slice along ne[0] of width Cs starting at i * Cs. auto chunk = [&](int i) -> struct ggml_tensor * { return ggml_view_2d(ctx, x, Cs, T, x->nb[1], (size_t) (i * Cs) * x->nb[0]); }; struct ggml_tensor * prev = NULL; for (int i = 0; i < scale; i++) { struct ggml_tensor * c = ggml_cont(ctx, chunk(i)); if (i == 0) { outs.push_back(c); continue; } struct ggml_tensor * inp = c; if (i >= 2) { inp = ggml_add(ctx, c, prev); } struct ggml_tensor * y = spk_conv1d_same(ctx, inp, rn.weight[i - 1], rn.bias[i - 1], dilation); y = ggml_relu(ctx, y); outs.push_back(y); prev = y; } // Concat along ne[0]. ggml_concat with dim=0 stacks along the // fastest axis. Build the concat tree iteratively. struct ggml_tensor * acc = outs[0]; for (int i = 1; i < scale; i++) { acc = ggml_concat(ctx, acc, outs[i], 0); } return acc; } // Squeeze and Excitation: compute the temporal mean per channel, // project down to se_c with a 1x1 conv + ReLU, project back up to // out_c with a 1x1 conv + sigmoid, then scale the input by the gate // broadcast over T. static struct ggml_tensor * spk_se(struct ggml_context * ctx, const SpkEncSE & se, struct ggml_tensor * x) { const int T = (int) x->ne[1]; // Mean over T, keep dim. ggml_mean reduces along ne[0], so transpose // to put T on ne[0], reduce, transpose back. struct ggml_tensor * x_t = ggml_cont(ctx, ggml_transpose(ctx, x)); // [T, C] struct ggml_tensor * mean = ggml_mean(ctx, x_t); // [1, C] mean = ggml_cont(ctx, ggml_transpose(ctx, mean)); // [C, 1] // conv1 1x1 reduces C -> se_c. dilation 1, padding "same" trivial // since k=1. struct ggml_tensor * h = spk_conv1d_same(ctx, mean, se.conv1_w, se.conv1_b, 1); h = ggml_relu(ctx, h); h = spk_conv1d_same(ctx, h, se.conv2_w, se.conv2_b, 1); // Sigmoid over [out_c, 1]. h = ggml_sigmoid(ctx, h); // Scale x by the gate. h is [C, 1], x is [C, T]. ggml_mul broadcasts // ne[1]=1 to T. struct ggml_tensor * y = ggml_mul(ctx, x, h); (void) T; return y; } // SE-Res2Net block: tdnn1 (1x1) -> Res2Net -> tdnn2 (1x1) -> SE plus // a residual add over the whole stack. static struct ggml_tensor * spk_block(struct ggml_context * ctx, const SpkEncBlock & blk, struct ggml_tensor * x, int res2net_scale) { struct ggml_tensor * residual = x; struct ggml_tensor * h = spk_tdnn(ctx, blk.tdnn1, x, 1); h = spk_res2net(ctx, blk.res2net, h, blk.dilation, res2net_scale); h = spk_tdnn(ctx, blk.tdnn2, h, 1); h = spk_se(ctx, blk.se, h); return ggml_add(ctx, h, residual); } // Attentive Statistical Pooling: compute global mean and std along T, // concat with x, run an attention TDNN + tanh + 1x1 conv, softmax along // T, recompute weighted mean and std, return the [2C, 1] concat. // // x [C, T] // Returns [2C, 1] static struct ggml_tensor * spk_asp(struct ggml_context * ctx, const SpkEncASP & asp, struct ggml_tensor * x) { const int C = (int) x->ne[0]; const int T = (int) x->ne[1]; // Mean and std over T axis. The mask reduction is uniform 1/T. // mean: [C, 1] struct ggml_tensor * x_t = ggml_cont(ctx, ggml_transpose(ctx, x)); struct ggml_tensor * mean = ggml_mean(ctx, x_t); mean = ggml_cont(ctx, ggml_transpose(ctx, mean)); // var = mean( (x - mean)^2 ) over T, then std = sqrt(clamp(var, eps)). // Broadcasting (x - mean) requires mean repeated to T. ggml_repeat // handles this when shapes are compatible. struct ggml_tensor * mean_T = ggml_repeat(ctx, mean, x); struct ggml_tensor * centered = ggml_sub(ctx, x, mean_T); struct ggml_tensor * var_t = ggml_cont(ctx, ggml_transpose(ctx, ggml_sqr(ctx, centered))); struct ggml_tensor * var = ggml_mean(ctx, var_t); var = ggml_cont(ctx, ggml_transpose(ctx, var)); var = ggml_scale_bias(ctx, var, 1.0f, 1e-12f); struct ggml_tensor * std = ggml_sqrt(ctx, var); // Build [x, mean_repeat, std_repeat] concat along channel axis. struct ggml_tensor * std_T = ggml_repeat(ctx, std, x); struct ggml_tensor * cat = ggml_concat(ctx, x, mean_T, 0); cat = ggml_concat(ctx, cat, std_T, 0); // [3C, T] // Attention TDNN: 3C -> attn_c, ReLU, then tanh, then 1x1 conv // attn_c -> C. Upstream applies tanh on the TDNN output before the // second conv; the TDNN itself already runs ReLU so the order is // ReLU then tanh which is unusual but mirrored faithfully. struct ggml_tensor * a = spk_tdnn(ctx, asp.tdnn, cat, 1); a = ggml_tanh(ctx, a); a = spk_conv1d_same(ctx, a, asp.conv_w, asp.conv_b, 1); // Softmax along T axis. ggml_soft_max reduces ne[0], transpose first. struct ggml_tensor * a_t = ggml_cont(ctx, ggml_transpose(ctx, a)); // [T, C] struct ggml_tensor * w_t = ggml_soft_max(ctx, a_t); struct ggml_tensor * w = ggml_cont(ctx, ggml_transpose(ctx, w_t)); // [C, T] // Weighted mean: sum(w * x) over T, w already sums to 1 over T. struct ggml_tensor * wx = ggml_mul(ctx, w, x); struct ggml_tensor * wx_t = ggml_cont(ctx, ggml_transpose(ctx, wx)); // ggml_mean averages over ne[0]=T, giving 1/T scaling. We want the // un-normalized sum since w already encodes the soft selection // probability, so multiply back by T. struct ggml_tensor * w_mean = ggml_mean(ctx, wx_t); w_mean = ggml_scale(ctx, w_mean, (float) T); w_mean = ggml_cont(ctx, ggml_transpose(ctx, w_mean)); // [C, 1] // Weighted std: sum(w * (x - w_mean)^2) over T. struct ggml_tensor * w_mean_T = ggml_repeat(ctx, w_mean, x); struct ggml_tensor * dev = ggml_sub(ctx, x, w_mean_T); struct ggml_tensor * w_var_in = ggml_mul(ctx, w, ggml_sqr(ctx, dev)); struct ggml_tensor * w_var_t = ggml_cont(ctx, ggml_transpose(ctx, w_var_in)); struct ggml_tensor * w_var = ggml_mean(ctx, w_var_t); w_var = ggml_scale(ctx, w_var, (float) T); w_var = ggml_cont(ctx, ggml_transpose(ctx, w_var)); w_var = ggml_scale_bias(ctx, w_var, 1.0f, 1e-12f); struct ggml_tensor * w_std = ggml_sqrt(ctx, w_var); // Stack [w_mean, w_std] along channel -> [2C, 1]. Time axis already // collapsed. struct ggml_tensor * stats = ggml_concat(ctx, w_mean, w_std, 0); (void) C; return stats; } // Full speaker encoder forward graph. Assumes the audio waveform has // already been resampled to sr=24000 and reflect padded by // (n_fft - hop) / 2 on each side. The padded buffer must outlive the // graph compute call. // // Inputs : // audio_padded [T_pad] f32, host or backend tensor // mel constants hann/dft_real/dft_imag/mel_basis backend tensors // mel_out optional out param. When non NULL, receives the post // mel_spectrogram tensor [n_mels, T_frames] so the caller // can mark it as a graph output and pull its values back. // mag_out optional out param. When non NULL, receives the post // STFT magnitude tensor [n_freq, T_frames] for debug // bisection between the STFT and the mel filtering. // frontend_out optional. Post conv0 TDNN k=5 + ReLU output [512, T]. // block3_out optional. Post third SE-Res2Net block output [512, T]. // mfa_out optional. Post multi-layer feature aggregation [1536, T]. // asp_out optional. Post attentive statistical pooling [3072, 1]. // Output: [enc_dim] f32, the speaker embedding (typically 2048 dims). static struct ggml_tensor * speaker_encoder_forward(struct ggml_context * ctx, const SpeakerEncoderWeights * sw, struct ggml_tensor * audio_padded, struct ggml_tensor * hann, struct ggml_tensor * dft_real, struct ggml_tensor * dft_imag, struct ggml_tensor * mel_basis, const AudioMelConfig & mel_cfg, struct ggml_tensor ** mel_out = NULL, struct ggml_tensor ** mag_out = NULL, struct ggml_tensor ** frontend_out = NULL, struct ggml_tensor ** block3_out = NULL, struct ggml_tensor ** mfa_out = NULL, struct ggml_tensor ** asp_out = NULL) { // Mel: [n_mels=128, T_frames] struct ggml_tensor * mel = audio_mel_build_graph(ctx, audio_padded, hann, dft_real, dft_imag, mel_basis, mel_cfg, mag_out); if (mel_out) { *mel_out = mel; } // Frontend conv0 TDNN k=5 + ReLU: 128 -> 512, T preserved. struct ggml_tensor * h = spk_tdnn(ctx, sw->conv0, mel, 1); if (frontend_out) { *frontend_out = h; } // Three SE-Res2Net blocks at dilations 2, 3, 4. struct ggml_tensor * b1 = spk_block(ctx, sw->blocks[0], h, sw->res2net_scale); struct ggml_tensor * b2 = spk_block(ctx, sw->blocks[1], b1, sw->res2net_scale); struct ggml_tensor * b3 = spk_block(ctx, sw->blocks[2], b2, sw->res2net_scale); if (block3_out) { *block3_out = b3; } // Multi-layer feature aggregation: cat blk1..3 then 1x1 TDNN + ReLU. struct ggml_tensor * cat = ggml_concat(ctx, b1, b2, 0); cat = ggml_concat(ctx, cat, b3, 0); // [1536, T] struct ggml_tensor * mfa = spk_tdnn(ctx, sw->mfa, cat, 1); // [1536, T] if (mfa_out) { *mfa_out = mfa; } // Attentive statistical pooling: [1536, T] -> [3072, 1]. struct ggml_tensor * stats = spk_asp(ctx, sw->asp, mfa); if (asp_out) { *asp_out = stats; } // Final FC k=1: [3072, 1] -> [enc_dim, 1]. struct ggml_tensor * emb = spk_conv1d_same(ctx, stats, sw->fc_w, sw->fc_b, 1); // Squeeze T axis, return [enc_dim]. ggml_cont is required so the sched // assigns a fresh backend buffer to the graph output rather than // forwarding a view of the FC bias add. emb = ggml_reshape_1d(ctx, emb, sw->enc_dim); emb = ggml_cont(ctx, emb); ggml_set_name(emb, "spk.embedding"); return emb; }