This commit is contained in:
Pascal
2026-05-14 16:47:45 +02:00
parent b1e864cb8d
commit e8f1c2a053
40 changed files with 337 additions and 340 deletions
+16 -2
View File
@@ -2,7 +2,7 @@
// audio-io.h: WAV read/write for qwentts.cpp.
// Reads any WAV (PCM16 / PCM24 / float32, mono or stereo, any rate).
// Writes mono WAV in S16, S24 or F32 at the source sample rate.
// Internal pipelines : planar stereo float [L:T][R:T] for reads,
// Internal pipelines: planar stereo float [L:T][R:T] for reads,
// flat mono float [T] for writes (qwen output is mono only).
#include <cmath>
@@ -12,6 +12,11 @@
#include <cstring>
#include <string>
#if defined(_WIN32)
# include <fcntl.h>
# include <io.h>
#endif
// wav.h: WAV reader (returns interleaved, we deinterleave below)
#include "wav.h"
@@ -323,7 +328,8 @@ static std::string audio_encode_wav(const float * audio, int T_audio, int sr, Wa
case WAV_F32:
return audio_encode_wav_f32(audio, T_audio, sr);
}
return audio_encode_wav_s16(audio, T_audio, sr);
fprintf(stderr, "[WAV] unknown format %d\n", (int) fmt);
return {};
}
// Write mono float audio to WAV file in the requested format. path "-"
@@ -341,6 +347,14 @@ static bool audio_write_wav(const char * path, const float * audio, int T_audio,
fprintf(stderr, "[WAV] Cannot open %s for writing\n", path);
return false;
}
#if defined(_WIN32)
// stdout defaults to text mode on Windows; binary mode is mandatory
// for WAV bytes to survive without CRLF translation. The mode is set
// once per process and is harmless on the second call.
if (to_stdout) {
_setmode(_fileno(stdout), _O_BINARY);
}
#endif
if (fwrite(wav.data(), 1, wav.size(), fp) != wav.size()) {
fprintf(stderr, "[WAV] Failed to write %s\n", path);
if (!to_stdout) {
+8 -8
View File
@@ -1,5 +1,5 @@
#pragma once
// audio-mel.h : log mel spectrogram extractor matching Qwen3TTS upstream.
// audio-mel.h: log mel spectrogram extractor matching Qwen3TTS upstream.
//
// Pipeline mirrored from qwen_tts/core/models/modeling_qwen3_tts.py
// mel_spectrogram() at lines 399 to 464 :
@@ -13,7 +13,7 @@
// Spec for the speaker encoder path :
// sr=24000, n_fft=1024, hop=256, n_mels=128, fmin=0, fmax=12000
//
// GGML strategy : no native FFT op, so the DFT is folded into two
// GGML strategy: no native FFT op, so the DFT is folded into two
// real matmuls. We precompute on CPU two F32 matrices :
// dft_real [n_freq, n_fft] with cos(2 pi k n / n_fft)
// dft_imag [n_freq, n_fft] with -sin(2 pi k n / n_fft)
@@ -39,7 +39,7 @@ struct AudioMelConfig {
float fmax;
};
// CPU side constants : Hann window, DFT real/imag matrices, mel filter.
// CPU side constants: Hann window, DFT real/imag matrices, mel filter.
// Allocated once per AudioMelConfig and uploaded to the backend as
// regular ggml tensors during graph build.
struct AudioMelConstants {
@@ -53,7 +53,7 @@ struct AudioMelConstants {
// Slaney mel scale, the default of librosa.filters.mel.
static inline float audio_mel_hz_to_mel(float hz) {
// Slaney : linear below 1000 Hz, log above.
// Slaney: linear below 1000 Hz, log above.
const float f_min = 0.0f;
const float f_sp = 200.0f / 3.0f;
const float min_log_hz = 1000.0f;
@@ -84,7 +84,7 @@ static void audio_mel_compute_constants(const AudioMelConfig & cfg, AudioMelCons
c.cfg = cfg;
c.n_freq = cfg.n_fft / 2 + 1;
// Hann periodic : 0.5 * (1 - cos(2 pi i / N)) for i in [0, N).
// Hann periodic: 0.5 * (1 - cos(2 pi i / N)) for i in [0, N).
c.hann.assign(cfg.n_fft, 0.0f);
for (int i = 0; i < cfg.n_fft; i++) {
c.hann[i] = 0.5f * (1.0f - (float) std::cos(2.0 * M_PI * (double) i / (double) cfg.n_fft));
@@ -102,7 +102,7 @@ static void audio_mel_compute_constants(const AudioMelConfig & cfg, AudioMelCons
}
}
// Slaney mel filterbank : n_mels triangular filters between fmin and
// Slaney mel filterbank: n_mels triangular filters between fmin and
// fmax, normalized by 2 / (mel_freqs[i+2] - mel_freqs[i]). Matches
// librosa.filters.mel(htk=False, norm='slaney') byte for byte.
const float fmin = cfg.fmin;
@@ -138,7 +138,7 @@ static void audio_mel_compute_constants(const AudioMelConfig & cfg, AudioMelCons
}
c.mel_basis[(size_t) m * (size_t) c.n_freq + (size_t) k] = w;
}
// Slaney area normalization : 2 / (hi - lo).
// Slaney area normalization: 2 / (hi - lo).
const float enorm = 2.0f / (hi - lo);
for (int k = 0; k < c.n_freq; k++) {
c.mel_basis[(size_t) m * (size_t) c.n_freq + (size_t) k] *= enorm;
@@ -161,7 +161,7 @@ static void audio_mel_compute_constants(const AudioMelConfig & cfg, AudioMelCons
// tensor [n_freq, T_frames] for debug bisection. NULL by
// default. Caller marks it as graph output if needed.
//
// Output : [n_mels, T_frames] f32 log mel.
// Output: [n_mels, T_frames] f32 log mel.
//
// The im2col path produces frames [n_fft, T_frames] T-fastest, which is
// the layout ggml_mul_mat expects on the right operand (ne[0] = K = n_fft,
+2 -2
View File
@@ -1,11 +1,11 @@
#pragma once
// audio-postproc.h: TTS waveform post-processing
//
// Generic post-processing for neural TTS output : silence trimming
// Generic post-processing for neural TTS output: silence trimming
// (pydub-strict bit-for-bit), fade-in/out, padding. Public functions take
// and return float32 mono PCM in [-1, 1] at the pipeline sample rate.
// Internal silence detection runs on int16 samples to match pydub.
// Math reference : omnivoice/utils/audio.py (1:1 port).
// Math reference: omnivoice/utils/audio.py (1:1 port).
#include <algorithm>
#include <cmath>
+3 -3
View File
@@ -97,9 +97,9 @@ static int utf8_codepoint(const char * s, int * advance) {
*advance = 4;
return ((c & 0x07) << 18) | ((s[1] & 0x3F) << 12) | ((s[2] & 0x3F) << 6) | (s[3] & 0x3F);
}
// invalid lead byte : advance one and return the raw byte to avoid an
// invalid lead byte: advance one and return the raw byte to avoid an
// infinite loop. The Python tokenizer never reaches this path since Python
// str guarantees valid UTF 8 ; in C++ the std::string input has no such
// str guarantees valid UTF 8; in C++ the std::string input has no such
// guarantee, so this branch handles malformed input defensively.
*advance = 1;
return c;
@@ -580,7 +580,7 @@ static void encode_chunk(const BPETokenizer * tok, const std::string & chunk, st
}
}
// Full encode : text -> token ids.
// Full encode: text -> token ids.
// Walks the text from left to right, matching any registered special token
// verbatim. For each segment between specials, runs the GPT-2 byte-level
// pre-tokenizer + BPE merges. The endoftext sentinel is auto-registered as
+5 -7
View File
@@ -4,7 +4,7 @@
//
// PyTorch reference (Qwen3TTSTokenizerV2CausalTransConvNet):
// y = ConvTranspose1d(x, k, stride) # raw length (T-1)*stride + K
// y = y[..., : y.shape[-1] - (K - stride)] # right-trim K-stride frames
// y = y[...,: y.shape[-1] - (K - stride)] # right-trim K-stride frames
// final length: T * stride
//
// GGML implementation: the weight is pre-permuted at load time from the
@@ -17,6 +17,7 @@
#include "ggml.h"
#include "gguf-weights.h"
#include "qt-error.h"
#include "weight-ctx.h"
#include <cstdio>
@@ -33,19 +34,16 @@
static struct ggml_tensor * qwen_load_ctw_f32(WeightCtx * wctx, const GGUFModel & gf, const std::string & name) {
struct ggml_tensor * src = ggml_get_tensor(gf.meta, name.c_str());
if (!src) {
fprintf(stderr, "[CausalTransConv] FATAL: tensor '%s' not found\n", name.c_str());
exit(1);
qt_throw("[CausalTransConv] tensor '%s' not found", name.c_str());
}
// Source dtype follows the GGUF norm (pure llama.cpp policy). The F32
// master keeps tensors in F32, the BF16 variant keeps them in their
// source BF16, and the K-quant variants land them in F16 through the
// aligned fallback (kernel rows of width K=2 do not divide a K-quant
// block size). All three are widened to F32 here ; the K*OC*IC
// block size). All three are widened to F32 here; the K*OC*IC
// permutation always lands in a freshly allocated F32 buffer anyway.
if (src->type != GGML_TYPE_F32 && src->type != GGML_TYPE_F16 && src->type != GGML_TYPE_BF16) {
fprintf(stderr, "[CausalTransConv] FATAL: '%s' expected F32, F16 or BF16, got type %d\n", name.c_str(),
(int) src->type);
exit(1);
qt_throw("[CausalTransConv] '%s' expected F32, F16 or BF16, got type %d", name.c_str(), (int) src->type);
}
int K = (int) src->ne[0];
int OC = (int) src->ne[1];
+12 -14
View File
@@ -1,4 +1,4 @@
// code-predictor-forward.cpp : eager full-recompute graph for the
// code-predictor-forward.cpp: eager full-recompute graph for the
// Qwen3-TTS code predictor (5-layer Qwen3 stack with plain 1D NEOX
// RoPE, GQA attention with QK-norm, SwiGLU MLP, head-per-codebook
// output projection).
@@ -20,6 +20,7 @@
#include "ggml-alloc.h"
#include "ggml-backend.h"
#include "ggml.h"
#include "qt-error.h"
#include <cmath>
#include <cstdio>
@@ -28,7 +29,7 @@
#include <vector>
// One Qwen3 decoder block, KV cached. K and V for the T fresh positions
// are written into the cache at [n_past, n_past+T) on dim 1 ; the
// are written into the cache at [n_past, n_past+T) on dim 1; the
// attention reads the contiguous slice [0, n_past+T). Returns the layer
// output [hidden, T].
static struct ggml_tensor * code_predictor_layer_forward(struct ggml_context * ctx,
@@ -114,7 +115,7 @@ static struct ggml_tensor * code_predictor_layer_forward(struct ggml_context *
return x;
}
// Run one predictor pass : feed `T` fresh embeddings starting at cache
// Run one predictor pass: feed `T` fresh embeddings starting at cache
// position `n_past`, run all 5 layers, and pull the logits for the last
// position through lm_head[g_head]. The cache is written as a side
// effect so subsequent decode steps can append a single token.
@@ -141,7 +142,7 @@ static bool code_predictor_run(const CodePredictorWeights * cw,
return false;
}
// Inputs : fresh embeddings (talker_hidden), positions, attention mask
// Inputs: fresh embeddings (talker_hidden), positions, attention mask
struct ggml_tensor * x_in = ggml_new_tensor_2d(gctx, GGML_TYPE_F32, talker_hidden, T);
struct ggml_tensor * pos_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I32, T);
struct ggml_tensor * mask_in = ggml_new_tensor_2d(gctx, GGML_TYPE_F16, T_full, T);
@@ -151,7 +152,7 @@ static bool code_predictor_run(const CodePredictorWeights * cw,
struct ggml_cgraph * gf = ggml_new_graph_custom(gctx, max_nodes, false);
// small_to_mtp projection : Linear(talker_hidden -> hidden) with bias.
// small_to_mtp projection: Linear(talker_hidden -> hidden) with bias.
// When absent (Identity case) the input is already at predictor hidden.
struct ggml_tensor * h = x_in;
if (cw->mtp_proj_w) {
@@ -231,12 +232,10 @@ static bool code_predictor_run(const CodePredictorWeights * cw,
// dispatched through ggml_get_type_traits so quants are accepted.
static void embed_row_from_backend(struct ggml_tensor * t, int row_id, int dim, float * dst) {
if (t->ne[0] != dim) {
fprintf(stderr, "[CodePredictor] FATAL: embed dim mismatch %lld vs %d\n", (long long) t->ne[0], dim);
std::exit(1);
qt_throw("[CodePredictor] embed dim mismatch %lld vs %d", (long long) t->ne[0], dim);
}
if (row_id < 0 || row_id >= (int) t->ne[1]) {
fprintf(stderr, "[CodePredictor] FATAL: row %d out of range (vocab=%lld)\n", row_id, (long long) t->ne[1]);
std::exit(1);
qt_throw("[CodePredictor] row %d out of range (vocab=%lld)", row_id, (long long) t->ne[1]);
}
const size_t row_bytes = ggml_row_size(t->type, dim);
if (t->type == GGML_TYPE_F32) {
@@ -245,8 +244,7 @@ static void embed_row_from_backend(struct ggml_tensor * t, int row_id, int dim,
}
const struct ggml_type_traits * tt = ggml_get_type_traits(t->type);
if (!tt || !tt->to_float) {
fprintf(stderr, "[CodePredictor] FATAL: unsupported embed dtype %d\n", (int) t->type);
std::exit(1);
qt_throw("[CodePredictor] unsupported embed dtype %d", (int) t->type);
}
std::vector<uint8_t> tmp(row_bytes);
ggml_backend_tensor_get(t, tmp.data(), (size_t) row_id * row_bytes, row_bytes);
@@ -266,7 +264,7 @@ bool code_predictor_step(const TalkerWeights * tw,
int64_t subseq_base,
const char * dump_dir,
CodePredictorOutput * out) {
// sub_input slots live at the talker hidden dimension : both the
// sub_input slots live at the talker hidden dimension: both the
// talker last hidden and the codec_embedding rows feeding the sub
// network are talker sized in the upstream checkpoint. The graph's
// mtp_proj brings them down to predictor hidden when present.
@@ -282,7 +280,7 @@ bool code_predictor_step(const TalkerWeights * tw,
out->codes.assign((size_t) (n_acoustic + 1), 0);
out->codes[0] = c0;
// Prefill : two positions, talker_hidden_last and embed_talker(c0).
// Prefill: two positions, talker_hidden_last and embed_talker(c0).
kv_cache_reset(kv);
std::vector<float> prefill_input((size_t) 2 * (size_t) talker_hidden, 0.0f);
std::memcpy(prefill_input.data(), talker_hidden_last, (size_t) talker_hidden * sizeof(float));
@@ -307,7 +305,7 @@ bool code_predictor_step(const TalkerWeights * tw,
out->codes[1] = cg;
}
// Decode loop : 14 single-token steps. At step g (g=1..14) we feed
// Decode loop: 14 single-token steps. At step g (g=1..14) we feed
// the embedding of the code we just sampled and read lm_head[g].
std::vector<float> step_input((size_t) talker_hidden);
for (int g = 1; g < n_acoustic; g++) {
+4 -4
View File
@@ -1,5 +1,5 @@
#pragma once
// code-predictor-forward.h : run the 5-layer Qwen3 code predictor over a
// code-predictor-forward.h: run the 5-layer Qwen3 code predictor over a
// growing context to produce the 15 acoustic codes of one audio frame,
// KV cached.
//
@@ -13,7 +13,7 @@
// frame, ready for decode through
// the codec
//
// The predictor cache is local to a single frame : we reset it at every
// The predictor cache is local to a single frame: we reset it at every
// frame, prefill the first two positions (talker_hidden + embed(c0)),
// then decode 14 single-token steps. Total work drops from
// O(sum_{g=0..14} (g+2)^2) = O(1496 token-steps) to O(16) per frame,
@@ -29,14 +29,14 @@
#include <vector>
struct CodePredictorOutput {
// Sixteen codes : c0 from the talker plus c1..c15 from the predictor.
// Sixteen codes: c0 from the talker plus c1..c15 from the predictor.
std::vector<int32_t> codes;
};
// Run the predictor for one audio frame. Caller passes the talker hidden
// state for the current frame and the already-sampled c0. Sampling
// parameters control greedy (temperature <= 0) vs stochastic. subseq_base
// is the Philox subsequence of the c0 sample for this step ; the 15
// is the Philox subsequence of the c0 sample for this step; the 15
// acoustic samples consume subseq_base + 1 .. subseq_base + 15.
// Returns the full vector of 16 codes. dump_dir may be NULL.
bool code_predictor_step(const TalkerWeights * tw,
+3 -3
View File
@@ -1,10 +1,10 @@
#pragma once
// code-predictor-weights.h : 5-layer Qwen3 stack that predicts the
// code-predictor-weights.h: 5-layer Qwen3 stack that predicts the
// acoustic codebooks 1..15 of every audio frame conditioned on the
// Talker hidden state and the codebook 0 token just sampled.
//
// Architecture mirrors the Talker block (pre-norm, GQA attention with
// QK-norm, SwiGLU MLP) with one important difference : RoPE is plain
// QK-norm, SwiGLU MLP) with one important difference: RoPE is plain
// 1D (half-split, neox-style in GGUF terms) at freq base 1e6, not the
// multimodal interleaved variant the Talker uses.
//
@@ -53,7 +53,7 @@ struct CodePredictorWeights {
// Optional small_to_mtp projection that brings the talker hidden
// dimension down to the predictor hidden dimension when the two
// differ (1.7B-base case : 2048 -> 1024). Both tensors are NULL when
// differ (1.7B-base case: 2048 -> 1024). Both tensors are NULL when
// the upstream sets nn.Identity() i.e. talker_hidden == predictor_hidden
// (0.6B case). Loaded with gf_try_load_tensor so absence is silent.
struct ggml_tensor * mtp_proj_w;
+8 -8
View File
@@ -1,5 +1,5 @@
#pragma once
// convnext-block.h : 2-block upsample stage for the Qwen3-TTS 12Hz
// convnext-block.h: 2-block upsample stage for the Qwen3-TTS 12Hz
// tokenizer decoder.
//
// Each block is a CausalTransConv1d (kernel 2, stride 2) followed by a
@@ -122,7 +122,7 @@ static void qwen_upsample_stage_free(QwenUpsampleStage * stage) {
}
// One ConvNeXt block forward.
// x : [T, C] f32 T-first
// x: [T, C] f32 T-first
// returns [T, C] f32 T-first
static struct ggml_tensor * qwen_convnext_block_forward(struct ggml_context * ctx,
const QwenConvNeXtBlock & block,
@@ -133,7 +133,7 @@ static struct ggml_tensor * qwen_convnext_block_forward(struct ggml_context *
struct ggml_tensor * residual = x;
// dwconv : depthwise causal Conv1d. ggml_conv_1d_dw expects [T, C, B=1].
// dwconv: depthwise causal Conv1d. ggml_conv_1d_dw expects [T, C, B=1].
// Pre-pad left by (kernel-1) zeros for causal behavior, no internal padding.
struct ggml_tensor * y = ggml_reshape_3d(ctx, x, T, C, 1);
y = ggml_pad_ext(ctx, y, kernel - 1, 0, 0, 0, 0, 0, 0, 0);
@@ -144,20 +144,20 @@ static struct ggml_tensor * qwen_convnext_block_forward(struct ggml_context *
y = ggml_add(ctx, y, b2d);
}
// LayerNorm wants the channel dim on ne[0] : transpose to [C, T].
// LayerNorm wants the channel dim on ne[0]: transpose to [C, T].
y = ggml_cont(ctx, ggml_transpose(ctx, y));
y = ggml_norm(ctx, y, 1e-6f);
y = ggml_mul(ctx, y, block.norm_w);
y = ggml_add(ctx, y, block.norm_b);
// pwconv1 : Linear C -> 4*C. mul_mat contracts ne[0]=C of weight against
// pwconv1: Linear C -> 4*C. mul_mat contracts ne[0]=C of weight against
// ne[0]=C of input.
y = ggml_mul_mat(ctx, block.pwconv1_w, y);
y = ggml_add(ctx, y, block.pwconv1_b);
y = ggml_gelu(ctx, y);
// pwconv2 : Linear 4*C -> C
// pwconv2: Linear 4*C -> C
y = ggml_mul_mat(ctx, block.pwconv2_w, y);
y = ggml_add(ctx, y, block.pwconv2_b);
@@ -171,8 +171,8 @@ static struct ggml_tensor * qwen_convnext_block_forward(struct ggml_context *
return y;
}
// Full upsample stage forward : 2 (CausalTransConv + ConvNeXt) blocks.
// x : [T, C] f32 T-first
// Full upsample stage forward: 2 (CausalTransConv + ConvNeXt) blocks.
// x: [T, C] f32 T-first
// returns [T * 4, C] f32 T-first
//
// The top-level upsample stage uses kernel == stride (no causal trim).
+4 -7
View File
@@ -26,6 +26,7 @@
#include "ggml-backend.h"
#include "ggml.h"
#include "gguf-weights.h"
#include "qt-error.h"
#include "weight-ctx.h"
#include <cmath>
@@ -100,18 +101,14 @@ static void qwen_dac_load_snakebeta(WeightCtx * wctx,
struct ggml_tensor * alpha_meta = ggml_get_tensor(gf.meta, alpha_name.c_str());
struct ggml_tensor * beta_meta = ggml_get_tensor(gf.meta, beta_name.c_str());
if (!alpha_meta || !beta_meta) {
fprintf(stderr, "[DAC] FATAL: snake tensor '%s' or '%s' not found\n", alpha_name.c_str(), beta_name.c_str());
exit(1);
qt_throw("[DAC] snake tensor '%s' or '%s' not found", alpha_name.c_str(), beta_name.c_str());
}
if (alpha_meta->type != GGML_TYPE_F32 || beta_meta->type != GGML_TYPE_F32) {
fprintf(stderr, "[DAC] FATAL: snake '%s' expects F32 alpha/beta\n", alpha_name.c_str());
exit(1);
qt_throw("[DAC] snake '%s' expects F32 alpha/beta", alpha_name.c_str());
}
int C = (int) alpha_meta->ne[0];
if ((int) beta_meta->ne[0] != C) {
fprintf(stderr, "[DAC] FATAL: snake '%s' alpha/beta size mismatch (%d vs %d)\n", alpha_name.c_str(), C,
(int) beta_meta->ne[0]);
exit(1);
qt_throw("[DAC] snake '%s' alpha/beta size mismatch (%d vs %d)", alpha_name.c_str(), C, (int) beta_meta->ne[0]);
}
s->a = ggml_new_tensor_2d(wctx->ctx, GGML_TYPE_F32, 1, C);
+7 -7
View File
@@ -1,8 +1,8 @@
#pragma once
// debug.h : tensor dump and compare helpers for Python vs GGML validation.
// debug.h: tensor dump and compare helpers for Python vs GGML validation.
// Dumps raw f32 arrays to binary files, both backends convert to f32 before
// dump.
// File format : [int32 ndims] [int32 dim0] [int32 dim1] ... [float data...]
// File format: [int32 ndims] [int32 dim0] [int32 dim1] ... [float data...]
#include "utf8.h"
@@ -24,7 +24,7 @@ static void debug_init(DebugDumper * d, const char * dir) {
}
// Dump f32 tensor to binary file.
// Format : [ndims:i32] [shape:i32 x ndims] [data:f32 x numel]
// Format: [ndims:i32] [shape:i32 x ndims] [data:f32 x numel]
static void debug_dump(const DebugDumper * d, const char * name, const float * data, const int * shape, int ndims) {
if (!d->enabled) {
return;
@@ -58,24 +58,24 @@ static void debug_dump(const DebugDumper * d, const char * name, const float * d
fprintf(stderr, "\n");
}
// Convenience : dump 1D tensor [n].
// Convenience: dump 1D tensor [n].
static void debug_dump_1d(const DebugDumper * d, const char * name, const float * data, int n) {
debug_dump(d, name, data, &n, 1);
}
// Convenience : dump 2D tensor [rows, cols].
// Convenience: dump 2D tensor [rows, cols].
static void debug_dump_2d(const DebugDumper * d, const char * name, const float * data, int dim0, int dim1) {
int shape[2] = { dim0, dim1 };
debug_dump(d, name, data, shape, 2);
}
// Convenience : dump 3D tensor [d0, d1, d2].
// Convenience: dump 3D tensor [d0, d1, d2].
static void debug_dump_3d(const DebugDumper * d, const char * name, const float * data, int d0, int d1, int d2) {
int shape[3] = { d0, d1, d2 };
debug_dump(d, name, data, shape, 3);
}
// Convenience : dump 4D tensor [d0, d1, d2, d3].
// Convenience: dump 4D tensor [d0, d1, d2, d3].
static void
debug_dump_4d(const DebugDumper * d, const char * name, const float * data, int d0, int d1, int d2, int d3) {
int shape[4] = { d0, d1, d2, d3 };
+3 -3
View File
@@ -136,7 +136,7 @@ static void qwen_encoder_transformer_free(QwenEncoderTransformer * tr) {
}
// Build a [T, T] additive causal mask (0 where allowed, -inf where masked).
// Pure causal : k <= q. The upstream config carries a sliding_window
// Pure causal: k <= q. The upstream config carries a sliding_window
// value but neither MimiAttention's eager forward nor MimiTransformerModel
// (create_causal_mask) ever apply it. The Qwen3TTS encoder inherits this
// convention, so we mirror it bit for bit here.
@@ -160,7 +160,7 @@ static void qwen_encoder_build_positions(int T, std::vector<int32_t> & dst) {
// q/k/v/o biases, MLP with fc1 -> GELU -> fc2 (no SwiGLU), LayerScale on
// both residual paths.
// x : [hidden, T] f32 C-first
// positions : [T] i32
// positions: [T] i32
// mask : [T, T] f32 additive
// Returns [hidden, T] f32 C-first.
static struct ggml_tensor * qwen_encoder_transformer_layer_forward(struct ggml_context * ctx,
@@ -230,7 +230,7 @@ static struct ggml_tensor * qwen_encoder_transformer_layer_forward(struct ggml_c
// Full encoder transformer forward. No top-level input_proj or output_proj
// brackets: the SEANet output already has hidden_size channels.
// x : [hidden, T] f32 C-first
// positions : [T] i32
// positions: [T] i32
// mask : [T, T] f32 additive
// Returns [hidden, T] f32 C-first.
static struct ggml_tensor * qwen_encoder_transformer_forward(struct ggml_context * ctx,
+11 -19
View File
@@ -14,6 +14,7 @@
// gf_close(&gf); // safe after wctx_alloc copied data to GPU
#include "gguf.h"
#include "qt-error.h"
#include "weight-ctx.h"
#include <cstdio>
@@ -167,15 +168,13 @@ static struct ggml_tensor * gf_load_tensor(WeightCtx * wctx,
int n_dims_override = 0) {
int64_t idx = gguf_find_tensor(gf.gguf, name.c_str());
if (idx < 0) {
fprintf(stderr, "[GGUF] FATAL: tensor '%s' not found\n", name.c_str());
exit(1);
qt_throw("[GGUF] tensor '%s' not found", name.c_str());
}
// Get metadata from the context populated by gguf_init_from_file
struct ggml_tensor * src = ggml_get_tensor(gf.meta, name.c_str());
if (!src) {
fprintf(stderr, "[GGUF] FATAL: tensor '%s' not in meta context\n", name.c_str());
exit(1);
qt_throw("[GGUF] tensor '%s' not in meta context", name.c_str());
}
int n_dims;
@@ -218,8 +217,7 @@ static struct ggml_tensor * gf_try_load_tensor(WeightCtx * wctx, const GGUFModel
static struct ggml_tensor * gf_load_tensor_f32(WeightCtx * wctx, const GGUFModel & gf, const std::string & name) {
int64_t idx = gguf_find_tensor(gf.gguf, name.c_str());
if (idx < 0) {
fprintf(stderr, "[GGUF] FATAL: tensor '%s' not found\n", name.c_str());
exit(1);
qt_throw("[GGUF] tensor '%s' not found (f32 load)", name.c_str());
}
struct ggml_tensor * src = ggml_get_tensor(gf.meta, name.c_str());
int n_dims = ggml_n_dims(src);
@@ -270,7 +268,7 @@ static struct ggml_tensor * gf_load_tensor_f32(WeightCtx * wctx, const GGUFModel
// Load a Conv1d / Conv1dDW kernel weight, forcing F16 storage on the
// backend regardless of the source GGUF dtype.
//
// TODO upstream GGML : ggml_conv_1d and ggml_conv_1d_dw in
// TODO upstream GGML: ggml_conv_1d and ggml_conv_1d_dw in
// ggml/src/ggml.c hardcode dst_type = GGML_TYPE_F16 in their internal
// ggml_im2col call (currently ggml.c lines around 4508 and 4542).
// ggml_conv_2d at the equivalent site uses the adaptive pattern
@@ -296,8 +294,7 @@ static struct ggml_tensor * gf_load_tensor_f32(WeightCtx * wctx, const GGUFModel
static struct ggml_tensor * gf_load_conv(WeightCtx * wctx, const GGUFModel & gf, const std::string & name) {
int64_t idx = gguf_find_tensor(gf.gguf, name.c_str());
if (idx < 0) {
fprintf(stderr, "[GGUF] FATAL: tensor '%s' not found\n", name.c_str());
exit(1);
qt_throw("[GGUF] tensor '%s' not found (conv load)", name.c_str());
}
struct ggml_tensor * src = ggml_get_tensor(gf.meta, name.c_str());
int n_dims = ggml_n_dims(src);
@@ -306,14 +303,12 @@ static struct ggml_tensor * gf_load_conv(WeightCtx * wctx, const GGUFModel & gf,
ne[i] = src->ne[i];
}
// F16 source : direct passthrough, no conversion.
// F16 source: direct passthrough, no conversion.
if (src->type == GGML_TYPE_F16) {
return gf_load_tensor(wctx, gf, name);
}
if (src->type != GGML_TYPE_F32 && src->type != GGML_TYPE_BF16) {
fprintf(stderr, "[GGUF] FATAL: gf_load_conv unsupported source type %s for '%s'\n", ggml_type_name(src->type),
name.c_str());
exit(1);
qt_throw("[GGUF] gf_load_conv unsupported source type %s for '%s'", ggml_type_name(src->type), name.c_str());
}
// Allocate F16 backend tensor in the WeightCtx graph.
@@ -336,7 +331,7 @@ static struct ggml_tensor * gf_load_conv(WeightCtx * wctx, const GGUFModel & gf,
if (src->type == GGML_TYPE_F32) {
ggml_fp32_to_fp16_row((const float *) raw, data, (int) n);
} else {
// BF16 source : widen to F32 first, then narrow to F16 in
// BF16 source: widen to F32 first, then narrow to F16 in
// one pass to preserve mantissa bits the BF16-to-F16 direct
// cast would otherwise leave undefined.
std::vector<float> f32(n);
@@ -370,8 +365,7 @@ static const void * gf_get_data(const GGUFModel & gf, const char * name) {
static enum ggml_type gf_get_type(const GGUFModel & gf, const std::string & name) {
struct ggml_tensor * src = ggml_get_tensor(gf.meta, name.c_str());
if (!src) {
fprintf(stderr, "[GGUF] FATAL: tensor '%s' not in meta context\n", name.c_str());
exit(1);
qt_throw("[GGUF] tensor '%s' not in meta context", name.c_str());
}
return src->type;
}
@@ -388,9 +382,7 @@ static struct ggml_tensor * gf_load_qkv_fused(WeightCtx * wctx,
struct ggml_tensor * k_src = ggml_get_tensor(gf.meta, k_name.c_str());
struct ggml_tensor * v_src = ggml_get_tensor(gf.meta, v_name.c_str());
if (!q_src || !k_src || !v_src) {
fprintf(stderr, "[GGUF] FATAL: QKV tensor not found: %s / %s / %s\n", q_name.c_str(), k_name.c_str(),
v_name.c_str());
exit(1);
qt_throw("[GGUF] QKV tensor not found: %s / %s / %s", q_name.c_str(), k_name.c_str(), v_name.c_str());
}
// All must share ne[0] (input dim) and type - otherwise can't fuse
GGML_ASSERT(q_src->ne[0] == k_src->ne[0] && k_src->ne[0] == v_src->ne[0]);
+2 -2
View File
@@ -1,7 +1,7 @@
#pragma once
// kv-cache.h : persistent per-layer KV cache for the Talker LM and the
// kv-cache.h: persistent per-layer KV cache for the Talker LM and the
// Code Predictor. Mirrors the standard llama.cpp ring approach but kept
// minimal : the cache is sized at init for a fixed max sequence length
// minimal: the cache is sized at init for a fixed max sequence length
// and never reallocates. Reset just rewinds cur_len to 0.
//
// Layout per layer, both K and V :
+2 -2
View File
@@ -139,8 +139,8 @@ static inline void philox_randn(int64_t seed, float * out, int n, bool bf16_roun
// PyTorch CUDA torch.rand kernels.
//
// Required by the multinomial sampler used during stochastic
// generation : torch.multinomial(probs, 1) decomposes mathematically
// as u ~ Uniform[0, 1) ; cdf = cumsum(probs) ; argmin{ i : cdf[i] >= u }.
// generation: torch.multinomial(probs, 1) decomposes mathematically
// as u ~ Uniform[0, 1); cdf = cumsum(probs); argmin{ i: cdf[i] >= u }.
// To stay byte-exact with the upstream Python pipeline, both sides
// must consume the same u from the same Philox state, hence we need a
// uniform draw, not a Box-Muller normal.
+1 -1
View File
@@ -387,7 +387,7 @@ std::vector<int32_t> pipeline_codec_encode(PipelineCodec * pc,
if (dump) {
DebugDumper d;
debug_init(&d, dump);
// Raw audio input dump : the SEANet sees this, and any divergence
// Raw audio input dump: the SEANet sees this, and any divergence
// in the resampler (torchaudio reimpl C++ vs librosa Python) shows
// up here as a phase or amplitude drift.
debug_dump_1d(&d, "audio-input", audio, n_samples);
+1 -1
View File
@@ -89,7 +89,7 @@ std::vector<float> pipeline_codec_decode(PipelineCodec * pc, const int32_t * cod
// audio : [n_samples] f32 mono 24 kHz. Must be a multiple of
// QWEN_TOKENIZER_HOP_LENGTH (1920); the caller is expected
// to pad with zeros if needed.
// dump_dir : optional path. When non NULL, dumps the SEANet, encoder
// dump_dir: optional path. When non NULL, dumps the SEANet, encoder
// transformer and post-downsample (pre-FSQ latents) buffers
// into seanet-out.bin, enc-transformer-out.bin and
// codec-pre-fsq.bin under that directory. Quiet otherwise.
+38 -30
View File
@@ -1,4 +1,4 @@
// pipeline-tts.cpp : load and verify both GGUF files (talker + codec)
// pipeline-tts.cpp: load and verify both GGUF files (talker + codec)
// onto the same shared backend, parse all metadata into typed structs,
// and provide a structured load-time summary for --load-only mode.
@@ -47,7 +47,7 @@ static void parse_languages(const GGUFModel & gf, std::vector<LanguageEntry> & o
size_t n_names = gguf_get_arr_n(gf.gguf, name_idx);
size_t n_ids = gguf_get_arr_n(gf.gguf, id_idx);
if (n_names != n_ids) {
fprintf(stderr, "[Pipeline] WARNING: language arrays size mismatch (names=%zu, ids=%zu)\n", n_names, n_ids);
qt_log(QT_LOG_WARN, "[Pipeline] language arrays size mismatch (names=%zu, ids=%zu)", n_names, n_ids);
return;
}
const uint32_t * ids = (const uint32_t *) gguf_get_arr_data(gf.gguf, id_idx);
@@ -61,7 +61,7 @@ static void parse_languages(const GGUFModel & gf, std::vector<LanguageEntry> & o
}
// Parse the speaker table for CustomVoice variants. Three parallel arrays
// produced by convert.py : speaker_names, speaker_ids, speaker_dialects.
// produced by convert.py: speaker_names, speaker_ids, speaker_dialects.
// Empty dialect string means the speaker keeps the user supplied language.
// Skipped silently when the GGUF carries no speaker table (Base / VoiceDesign).
static void parse_speakers(const GGUFModel & gf, std::vector<SpeakerEntry> & out) {
@@ -75,8 +75,8 @@ static void parse_speakers(const GGUFModel & gf, std::vector<SpeakerEntry> & out
size_t n_ids = gguf_get_arr_n(gf.gguf, id_idx);
size_t n_dialects = gguf_get_arr_n(gf.gguf, dialect_idx);
if (n_names != n_ids || n_names != n_dialects) {
fprintf(stderr, "[Pipeline] WARNING: speaker arrays size mismatch (names=%zu, ids=%zu, dialects=%zu)\n",
n_names, n_ids, n_dialects);
qt_log(QT_LOG_WARN, "[Pipeline] speaker arrays size mismatch (names=%zu, ids=%zu, dialects=%zu)", n_names,
n_ids, n_dialects);
return;
}
const uint32_t * ids = (const uint32_t *) gguf_get_arr_data(gf.gguf, id_idx);
@@ -145,7 +145,7 @@ bool pipeline_tts_load(PipelineTTS * pt, const char * talker_gguf_path, const ch
}
// Speaker encoder is only present in Base checkpoints. Treat absence
// as a soft condition : voice clone path stays disabled, base-direct
// as a soft condition: voice clone path stays disabled, base-direct
// synthesis still works.
if (pt->model_type == "base") {
if (!speaker_encoder_weights_load(&pt->speaker_encoder, pt->gguf_talker, pt->backend)) {
@@ -168,10 +168,10 @@ bool pipeline_tts_load(PipelineTTS * pt, const char * talker_gguf_path, const ch
}
// Scheduler shared by talker_forward_* and code_predictor_step.
// Routes ops the GPU backend cannot run (typical case : K-quant
// Routes ops the GPU backend cannot run (typical case: K-quant
// get_rows on CUDA) to the CPU backend. 4096 nodes covers the 28L
// Qwen3 talker graph (~48 ops per layer with KV cache writes) with
// headroom ; the 5L code predictor uses a fraction of that.
// headroom; the 5L code predictor uses a fraction of that.
pt->sched = backend_sched_new(bp, 4096);
if (!pt->sched) {
pipeline_codec_free(&pt->codec);
@@ -184,7 +184,7 @@ bool pipeline_tts_load(PipelineTTS * pt, const char * talker_gguf_path, const ch
return false;
}
// KV caches : talker holds the LM context up to 4096 positions (the
// KV caches: talker holds the LM context up to 4096 positions (the
// longest ICL prompt observed is ~250 + max_new_tokens ~ 1500, so
// 4096 has 60% headroom). Predictor holds one frame of 16 sub-steps.
if (!kv_cache_init(&pt->talker_kv, pt->talker.num_hidden_layers, pt->talker.num_key_value_heads,
@@ -279,7 +279,7 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
const std::string speaker = params.speaker ? params.speaker : "";
const std::string ref_text = params.ref_text ? params.ref_text : "";
// Voice clone mode A : if ref_audio_24k is given, run the speaker
// Voice clone mode A: if ref_audio_24k is given, run the speaker
// encoder on the pre-decoded mono buffer and feed the resulting
// embedding straight into the prompt builder. Mutually exclusive
// with --speaker.
@@ -288,7 +288,9 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
const float * ref_spk_emb_ptr = NULL;
if (has_ref_audio) {
if (!pt->has_speaker_encoder) {
fprintf(stderr, "[Pipeline] FATAL: --ref-wav requires a model with a loaded speaker encoder (Base only)\n");
qt_set_error(
"pipeline_tts_synthesize: --ref-wav requires a model with a loaded speaker encoder (Base only)");
qt_log(QT_LOG_ERROR, "[Pipeline] --ref-wav requires a model with a loaded speaker encoder (Base only)");
return false;
}
if (!speaker_encoder_extract(&pt->speaker_encoder, pt->sched, params.ref_audio_24k, params.ref_n_samples,
@@ -296,14 +298,16 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
return false;
}
if ((int) ref_spk_emb.size() != pt->talker.hidden_size) {
fprintf(stderr, "[Pipeline] FATAL: speaker embedding size %zu mismatches talker hidden %d\n",
ref_spk_emb.size(), pt->talker.hidden_size);
qt_set_error("pipeline_tts_synthesize: speaker embedding size %zu mismatches talker hidden %d",
ref_spk_emb.size(), pt->talker.hidden_size);
qt_log(QT_LOG_ERROR, "[Pipeline] speaker embedding size %zu mismatches talker hidden %d",
ref_spk_emb.size(), pt->talker.hidden_size);
return false;
}
ref_spk_emb_ptr = ref_spk_emb.data();
}
// Voice clone mode B : if ref_text is also given, encode the
// Voice clone mode B: if ref_text is also given, encode the
// reference audio into 16 codebook indices via the codec encoder.
// Layout returned by pipeline_codec_encode is [num_codebooks, T_codec]
// row major, matching what the prompt builder expects for the ICL
@@ -312,23 +316,27 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
int ref_codes_T = 0;
if (!ref_text.empty()) {
if (!has_ref_audio) {
fprintf(stderr, "[Pipeline] FATAL: --ref-text requires --ref-wav\n");
qt_set_error("pipeline_tts_synthesize: --ref-text requires --ref-wav");
qt_log(QT_LOG_ERROR, "[Pipeline] --ref-text requires --ref-wav");
return false;
}
// The codec hop is 1920 samples at 24 kHz so n_samples must be
// a multiple of 1920. Truncate to the nearest hop boundary.
if (params.ref_n_samples < QWEN_TOKENIZER_HOP_LENGTH) {
fprintf(stderr, "[Pipeline] FATAL: ref_wav too short for ICL (%d samples)\n", params.ref_n_samples);
qt_set_error("pipeline_tts_synthesize: ref_wav too short for ICL (%d samples)", params.ref_n_samples);
qt_log(QT_LOG_ERROR, "[Pipeline] ref_wav too short for ICL (%d samples)", params.ref_n_samples);
return false;
}
int aligned_T = (params.ref_n_samples / QWEN_TOKENIZER_HOP_LENGTH) * QWEN_TOKENIZER_HOP_LENGTH;
ref_codes = pipeline_codec_encode(&pt->codec, params.ref_audio_24k, aligned_T, params.dump_dir);
if (ref_codes.empty()) {
fprintf(stderr, "[Pipeline] FATAL: pipeline_codec_encode returned empty codes\n");
qt_set_error("pipeline_tts_synthesize: pipeline_codec_encode returned empty codes");
qt_log(QT_LOG_ERROR, "[Pipeline] pipeline_codec_encode returned empty codes");
return false;
}
ref_codes_T = (int) ref_codes.size() / pt->num_code_groups;
fprintf(stderr, "[Pipeline] ICL ref_codes: %d frames at 12.5 Hz (%d audio samples)\n", ref_codes_T, aligned_T);
qt_log(QT_LOG_INFO, "[Pipeline] ICL ref_codes: %d frames at 12.5 Hz (%d audio samples)", ref_codes_T,
aligned_T);
}
if (!prompt_builder_build(pt, tok, params.text, params.lang, instruct, speaker, ref_spk_emb_ptr, ref_text,
@@ -346,7 +354,7 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
debug_dump_2d(&d, "trailing-text-hidden", prompt.trailing_text_hidden.data(), prompt.T_trailing, prompt.hidden);
debug_dump_1d(&d, "tts-pad-embed", prompt.tts_pad_embed.data(), prompt.hidden);
// Voice clone dumps : speaker-emb fires when ref_wav is set
// Voice clone dumps: speaker-emb fires when ref_wav is set
// (modes A and B), ref-codes fires only when ref_text is also set
// (mode B ICL). Both are no-ops in base / tts / customvoice modes,
// the dump files simply do not appear in those runs.
@@ -359,7 +367,7 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
}
}
// Generation loop : at each step we recompute the full Talker prefix
// Generation loop: at each step we recompute the full Talker prefix
// (no KV cache yet) over the prompt prefix concatenated with all the
// next-token embeddings produced so far, sample c0, run the code
// predictor for the 15 acoustic codes, build the next-token
@@ -376,7 +384,7 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
float subtk_T = params.subtalker_do_sample ? params.subtalker_temperature : 0.0f;
float talker_rp = params.repetition_penalty;
// Generation loop : step 0 prefills the talker over the full prompt
// Generation loop: step 0 prefills the talker over the full prompt
// and writes T_ctx positions into the KV cache. Subsequent steps
// feed one next_emb at a time and append one position. The code
// predictor maintains its own per-frame cache that gets reset at
@@ -408,7 +416,7 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
return false;
}
// Bisection dump : the talker hidden_last at step 1 is the input
// Bisection dump: the talker hidden_last at step 1 is the input
// the code predictor consumes after consuming the next-emb of
// step 0. Pairing it byte for byte with the Python hook tells us
// whether the next-emb composition + talker decode round trip
@@ -419,7 +427,7 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
debug_dump_1d(&d, "talker-hidden-step1", fw.hidden_last.data(), hidden);
}
// Apply codec suppression : forbid [vocab - 1024, vocab) except
// Apply codec suppression: forbid [vocab - 1024, vocab) except
// codec_eos. Then run the upstream sampling chain.
apply_suppress(fw.logits_last.data(), talker_vocab, talker_vocab - 1024, talker_vocab, codec_eos_id);
float u_c0 = 0.0f;
@@ -435,8 +443,8 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
// up with [Sample-PY] / [Sample-CP] across the 16 codes of step
// 0 and step 1 the Python harness emits.
if ((subseq_counter - 1) < 32) {
fprintf(stderr, "[Sample] step=%d c0=%d u=%.10f subseq=%lld\n", step, c0, (double) u_c0,
(long long) (subseq_counter - 1));
qt_log(QT_LOG_DEBUG, "[Sample] step=%d c0=%d u=%.10f subseq=%lld", step, c0, (double) u_c0,
(long long) (subseq_counter - 1));
}
if (c0 == codec_eos_id) {
@@ -458,7 +466,7 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
all_codes.push_back(cp.codes);
talker_history.push_back(c0);
// Build next-token embedding : sum of 16 codebook embeddings.
// Build next-token embedding: sum of 16 codebook embeddings.
// codebook 0 uses talker.codec_embedding, the 15 acoustic
// codebooks use the predictor's private embedding tables.
std::fill(next_emb.begin(), next_emb.end(), 0.0f);
@@ -478,8 +486,8 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
}
}
// Trailing text overlay : while we still have utterance text
// hiddens to consume, add the next one ; otherwise add the
// Trailing text overlay: while we still have utterance text
// hiddens to consume, add the next one; otherwise add the
// tts_pad embedding.
const float * overlay = (step < prompt.T_trailing) ?
prompt.trailing_text_hidden.data() + (size_t) step * (size_t) hidden :
@@ -488,7 +496,7 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
next_emb[(size_t) i] += overlay[(size_t) i];
}
// Bisection dump : the next-token embedding produced at step 0
// Bisection dump: the next-token embedding produced at step 0
// is the only thing controlling the talker forward at step 1, so
// matching it bit-exact against Python pinpoints any drift in
// the codebook embedding sums or the trailing text overlay.
@@ -519,7 +527,7 @@ bool pipeline_tts_synthesize(PipelineTTS * pt,
debug_dump_i32_as_f32(&d, "codes-full", flat.data(), shape, 2);
}
// Codec decode : transpose codes from [T_frames, K] to [K, T_frames]
// Codec decode: transpose codes from [T_frames, K] to [K, T_frames]
// because pipeline_codec_decode expects K-major layout (codebooks
// first, frames second), then return the 24 kHz mono audio.
if (all_codes.empty()) {
+6 -6
View File
@@ -1,5 +1,5 @@
#pragma once
// pipeline-tts.h : full TTS pipeline composition (Talker LM + code
// pipeline-tts.h: full TTS pipeline composition (Talker LM + code
// predictor MTP head + optional speaker encoder + 12Hz codec decoder).
//
// Phase 2.0 covers load-only: parse hyperparameters from both GGUF
@@ -90,7 +90,7 @@ struct PipelineTTS {
ggml_backend_t backend;
ggml_backend_sched_t sched;
// Persistent KV caches : the talker holds the LM context, the
// Persistent KV caches: the talker holds the LM context, the
// predictor holds one frame's 16 sub-steps and gets reset every
// frame in code_predictor_step.
KVCache talker_kv;
@@ -106,10 +106,10 @@ void pipeline_tts_free(PipelineTTS * pt);
struct BPETokenizer;
// Parameters for one synthesis call. Lifetime constraint : text and lang
// Parameters for one synthesis call. Lifetime constraint: text and lang
// are borrowed pointers, must outlive the call. dump_dir, when non-NULL,
// captures step 0 prefill activations plus the codes-full / output-audio
// dumps under the named directory ; debug only, slows the run.
// dumps under the named directory; debug only, slows the run.
struct PipelineTTSSynthesizeParams {
const char * text;
const char * lang;
@@ -133,13 +133,13 @@ struct PipelineTTSSynthesizeParams {
};
// Output of one synthesis call. audio is a 24 kHz mono F32 PCM buffer
// already decoded through the codec ; the caller writes it to disk.
// already decoded through the codec; the caller writes it to disk.
struct PipelineTTSSynthesizeOutput {
std::vector<float> audio;
int sample_rate;
};
// Run the full TTS pipeline : prompt assembly, prefill, frame loop with
// Run the full TTS pipeline: prompt assembly, prefill, frame loop with
// sampling, codec decode. Returns false on any failure with a diagnostic
// already routed through qt_log / qt_set_error.
bool pipeline_tts_synthesize(PipelineTTS * pt,
+43 -51
View File
@@ -1,10 +1,10 @@
// prompt-builder.cpp : CPU-side construction of the talker prefix
// prompt-builder.cpp: CPU-side construction of the talker prefix
// input embedding. Mirrors generate() in qwen_tts/core/models/modeling_qwen3_tts.py
// for the strict subset {non-streaming, no voice clone}.
//
// Two streams are aligned then summed :
// text stream : text_projection(text_embedding(ids)) 151936 -> 2048 -> 1024
// codec stream : codec_embedding(ids) 3072 -> 1024
// text stream: text_projection(text_embedding(ids)) 151936 -> 2048 -> 1024
// codec stream: codec_embedding(ids) 3072 -> 1024
//
// Layout (lang_id != none, no speaker, no instruct) :
//
@@ -31,6 +31,7 @@
#include "prompt-builder.h"
#include "ggml.h"
#include "qt-error.h"
#include <cmath>
#include <cstdio>
@@ -46,24 +47,18 @@
static void embed_row_to_f32(const GGUFModel & gf, const char * tensor_name, int row_id, int dim, float * dst) {
struct ggml_tensor * src = ggml_get_tensor(gf.meta, tensor_name);
if (!src) {
fprintf(stderr, "[Prompt] FATAL: tensor '%s' not in meta context\n", tensor_name);
std::exit(1);
qt_throw("[Prompt] tensor '%s' not in meta context", tensor_name);
}
if (src->ne[0] != dim) {
fprintf(stderr, "[Prompt] FATAL: tensor '%s' dim mismatch %lld vs %d\n", tensor_name, (long long) src->ne[0],
dim);
std::exit(1);
qt_throw("[Prompt] tensor '%s' dim mismatch %lld vs %d", tensor_name, (long long) src->ne[0], dim);
}
if (row_id < 0 || row_id >= (int) src->ne[1]) {
fprintf(stderr, "[Prompt] FATAL: row %d out of range for '%s' (vocab=%lld)\n", row_id, tensor_name,
(long long) src->ne[1]);
std::exit(1);
qt_throw("[Prompt] row %d out of range for '%s' (vocab=%lld)", row_id, tensor_name, (long long) src->ne[1]);
}
const uint8_t * base = (const uint8_t *) gf_get_data(gf, tensor_name);
if (!base) {
fprintf(stderr, "[Prompt] FATAL: tensor '%s' has no data\n", tensor_name);
std::exit(1);
qt_throw("[Prompt] tensor '%s' has no data", tensor_name);
}
const size_t row_bytes = ggml_row_size(src->type, dim);
@@ -76,8 +71,7 @@ static void embed_row_to_f32(const GGUFModel & gf, const char * tensor_name, int
const struct ggml_type_traits * tt = ggml_get_type_traits(src->type);
if (!tt || !tt->to_float) {
fprintf(stderr, "[Prompt] FATAL: unsupported dtype %d for '%s'\n", (int) src->type, tensor_name);
std::exit(1);
qt_throw("[Prompt] unsupported dtype %d for '%s'", (int) src->type, tensor_name);
}
tt->to_float(row, dst, dim);
}
@@ -88,8 +82,7 @@ static void embed_row_to_f32(const GGUFModel & gf, const char * tensor_name, int
static void read_tensor_f32(const GGUFModel & gf, const char * tensor_name, std::vector<float> & dst) {
struct ggml_tensor * src = ggml_get_tensor(gf.meta, tensor_name);
if (!src) {
fprintf(stderr, "[Prompt] FATAL: tensor '%s' not in meta context\n", tensor_name);
std::exit(1);
qt_throw("[Prompt] tensor '%s' not in meta context", tensor_name);
}
int64_t n = ggml_nelements(src);
const uint8_t * base = (const uint8_t *) gf_get_data(gf, tensor_name);
@@ -102,8 +95,7 @@ static void read_tensor_f32(const GGUFModel & gf, const char * tensor_name, std:
const struct ggml_type_traits * tt = ggml_get_type_traits(src->type);
if (!tt || !tt->to_float) {
fprintf(stderr, "[Prompt] FATAL: unsupported dtype %d for '%s'\n", (int) src->type, tensor_name);
std::exit(1);
qt_throw("[Prompt] unsupported dtype %d for '%s'", (int) src->type, tensor_name);
}
tt->to_float(base, dst.data(), (int64_t) n);
}
@@ -127,7 +119,7 @@ static inline float silu(float v) {
return v / (1.0f + std::exp(-v));
}
// Apply text_projection : F1 (text_hidden -> text_hidden) -> SiLU -> F2
// Apply text_projection: F1 (text_hidden -> text_hidden) -> SiLU -> F2
// (text_hidden -> hidden), both with bias.
struct TextProjection {
int in_dim; // text_hidden_size
@@ -185,7 +177,7 @@ static void embed_codec(const GGUFModel & gf, int id, int hidden_size, std::vect
embed_row_to_f32(gf, "talker.codec_embd.weight", id, hidden_size, dst.data() + old);
}
// Vector add : a += b, length n.
// Vector add: a += b, length n.
static void vec_add(float * a, const float * b, int n) {
for (int i = 0; i < n; i++) {
a[i] += b[i];
@@ -211,7 +203,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
return false;
}
// Voice clone mode B : ref_text and ref_codes drive an ICL prefix.
// Voice clone mode B: ref_text and ref_codes drive an ICL prefix.
// Mode B requires ref_spk_emb so the speaker slot is also filled.
const bool icl = !ref_text.empty() && ref_codes != NULL && ref_codes_T > 0;
if (icl && ref_spk_emb == NULL) {
@@ -220,7 +212,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
}
// Build the chat-templated prompt fed to the BPE tokenizer.
// Same wrap as the upstream demos : assistant role + utterance +
// Same wrap as the upstream demos: assistant role + utterance +
// im_end + newline + assistant role.
std::string full_text;
full_text.reserve(utterance_text.size() + 64);
@@ -242,7 +234,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
return false;
}
// Resolve language : "auto" -> no language id, prefill is 3 codec
// Resolve language: "auto" -> no language id, prefill is 3 codec
// tokens (nothink, think_bos, think_eos). Otherwise insert the
// configured language id between think_bos and think_eos.
int language_id = -1;
@@ -265,7 +257,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
}
}
// Resolve speaker : empty name -> no speaker. Otherwise lookup case
// Resolve speaker: empty name -> no speaker. Otherwise lookup case
// insensitively in pt->speakers and override the language id with the
// dialect entry when the user supplied language is chinese or auto,
// mirroring modeling_qwen3_tts.py lines 2118 to 2122.
@@ -288,7 +280,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
}
speaker_id = found->id;
// Dialect override : applied only when the user supplied language
// Dialect override: applied only when the user supplied language
// is chinese or auto, the dialect string is non empty, and the
// dialect resolves to a known language id.
if (!found->dialect.empty()) {
@@ -336,8 +328,8 @@ bool prompt_builder_build(const PipelineTTS * pt,
embed_row_to_f32(pt->gguf_talker, "talker.codec_embd.weight", pt->codec_specials.pad_id, hidden,
codec_pad_emb.data());
// Codec prefill list : 3 ids if auto (no language), 4 otherwise.
// Speaker insertion : if a speaker id is set, the codec embedding row
// Codec prefill list: 3 ids if auto (no language), 4 otherwise.
// Speaker insertion: if a speaker id is set, the codec embedding row
// for that speaker slips between think_eos and codec_pad in the codec
// stream, mirroring modeling_qwen3_tts.py lines 2167 to 2172.
std::vector<int> codec_prefill;
@@ -351,7 +343,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
if (speaker_id >= 0) {
codec_prefill.push_back(speaker_id);
} else if (ref_spk_emb != NULL) {
// Sentinel : the codec_left builder below copies ref_spk_emb in
// Sentinel: the codec_left builder below copies ref_spk_emb in
// place of an embedding lookup whenever it sees -2.
codec_prefill.push_back(-2);
}
@@ -360,7 +352,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
const int n_pad_pre = T_codec_prefix - 2;
// Tokenize the instruct segment when non empty. The wrapper mirrors
// _build_instruct_text upstream : <|im_start|>user\n{instruct}<|im_end|>\n
// _build_instruct_text upstream: <|im_start|>user\n{instruct}<|im_end|>\n
// The result is a flat list of text token ids that will be projected
// and placed as standalone vectors at the head of the input embed,
// with no codec stream contribution.
@@ -376,7 +368,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
const int N_instruct = (int) instruct_ids.size();
// Tokenize the reference utterance when ICL is active. The wrap is
// identical to the main utterance : assistant role + ref_text +
// identical to the main utterance: assistant role + ref_text +
// im_end + newline + assistant role. We slice [3:-5] later to keep
// only the inner text body, mirroring input_id[:, 3:-5] upstream.
std::vector<int> ref_ids;
@@ -392,7 +384,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
fprintf(stderr, "[Prompt] FATAL: ref_text tokenized too short (%d tokens)\n", (int) ref_ids.size());
return false;
}
// ref_ids[3 : -5] is the inner ref text body without role tokens
// ref_ids[3: -5] is the inner ref text body without role tokens
N_ref_text = (int) ref_ids.size() - 3 - 5;
if (N_ref_text <= 0) {
fprintf(stderr, "[Prompt] FATAL: empty ref_text body\n");
@@ -424,7 +416,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
return out->input_embed.data() + (size_t) r * (size_t) hidden;
};
// Instruct prefix : text_proj(text_embed(instruct_ids)). Standalone
// Instruct prefix: text_proj(text_embed(instruct_ids)). Standalone
// vectors with no codec stream (zero pad_id sum, ie nothing added).
if (N_instruct > 0) {
std::vector<float> dst;
@@ -433,7 +425,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
row += N_instruct;
}
// Role : text_proj(text_embed(ids[0:3]))
// Role: text_proj(text_embed(ids[0:3]))
{
std::vector<float> dst;
dst.reserve((size_t) 3 * (size_t) hidden);
@@ -442,7 +434,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
row += 3;
}
// Codec prefix : tts_pad x n_pad_pre + tts_bos, summed with
// Codec prefix: tts_pad x n_pad_pre + tts_bos, summed with
// codec_emb([codec_prefill_list[:-1]] + codec_pad). The Python code
// takes codec_input_embedding[:, :-1] which drops the codec_bos,
// leaving [codec_prefill_list..., codec_pad].
@@ -451,10 +443,10 @@ bool prompt_builder_build(const PipelineTTS * pt,
codec_left.push_back(pt->codec_specials.pad_id);
for (int i = 0; i < (int) codec_left.size(); i++) {
float * r = row_ptr(row + i);
// text stream : tts_pad * (n - 1) then tts_bos at the end
// text stream: tts_pad * (n - 1) then tts_bos at the end
const float * text_vec = (i == (int) codec_left.size() - 1) ? tts_bos_emb.data() : tts_pad_emb.data();
std::memcpy(r, text_vec, (size_t) hidden * sizeof(float));
// codec stream : either an embedding lookup or, when the
// codec stream: either an embedding lookup or, when the
// sentinel -2 marks the speaker slot, a direct copy of the
// user supplied ref_spk_emb (voice clone mode A).
std::vector<float> ce((size_t) hidden);
@@ -469,11 +461,11 @@ bool prompt_builder_build(const PipelineTTS * pt,
row += (int) codec_left.size();
}
// From here, two paths : standard (no ICL) builds the trailing
// From here, two paths: standard (no ICL) builds the trailing
// utterance text + tts_eos + final_pad, ICL builds an aligned
// text/codec block that replaces those rows entirely.
if (!icl) {
// Standard layout : trailing utterance text + tts_eos rows summed
// Standard layout: trailing utterance text + tts_eos rows summed
// with codec_pad, then a final tts_pad + codec_bos row.
for (int i = 0; i < N_text; i++) {
std::vector<float> e((size_t) text_hid);
@@ -500,23 +492,23 @@ bool prompt_builder_build(const PipelineTTS * pt,
row++;
}
} else {
// ICL layout : compute the text stream and the codec stream
// ICL layout: compute the text stream and the codec stream
// separately then add them. The text stream is text_proj of
// [ref_text_ids ; utterance_text_ids] followed by tts_eos. The
// [ref_text_ids; utterance_text_ids] followed by tts_eos. The
// codec stream is codec_emb(codec_bos) followed by sum over the
// 16 codebook embeddings of ref_codes[i, t] for each frame t.
// Both streams are aligned to length icl_T per the upstream
// non_streaming_mode=False branch of generate_icl_prompt.
const int T_icl = codec_lens_icl; // text_lens > codec : truncate to codec, else pad text up to codec
const int T_icl = codec_lens_icl; // text_lens > codec: truncate to codec, else pad text up to codec
// Build the codec stream [T_icl, hidden]. Row 0 : codec_emb(codec_bos).
// Row 1..ref_codes_T : sum over k=0..15 of codebook_k_emb(ref_codes[k, t]).
// Build the codec stream [T_icl, hidden]. Row 0: codec_emb(codec_bos).
// Row 1..ref_codes_T: sum over k=0..15 of codebook_k_emb(ref_codes[k, t]).
std::vector<float> codec_stream((size_t) T_icl * (size_t) hidden, 0.0f);
{
// Row 0 : codec_bos lookup.
// Row 0: codec_bos lookup.
embed_row_to_f32(pt->gguf_talker, "talker.codec_embd.weight", pt->codec_specials.bos_id, hidden,
codec_stream.data());
// Row 1..ref_codes_T : sum over codebooks.
// Row 1..ref_codes_T: sum over codebooks.
std::vector<float> tmp((size_t) hidden);
for (int t = 0; t < ref_codes_T; t++) {
float * dst = codec_stream.data() + (size_t) (1 + t) * (size_t) hidden;
@@ -535,7 +527,7 @@ bool prompt_builder_build(const PipelineTTS * pt,
}
// Build the text stream [text_lens_icl, hidden] = text_proj of
// [ref_text ; utterance_text] then tts_eos.
// [ref_text; utterance_text] then tts_eos.
std::vector<float> text_stream((size_t) text_lens_icl * (size_t) hidden, 0.0f);
for (int i = 0; i < N_ref_text; i++) {
std::vector<float> e((size_t) text_hid);
@@ -553,9 +545,9 @@ bool prompt_builder_build(const PipelineTTS * pt,
std::memcpy(text_stream.data() + (size_t) (text_lens_icl - 1) * (size_t) hidden, tts_eos_emb.data(),
(size_t) hidden * sizeof(float));
// Align the two streams to T_icl. text_lens > codec : truncate
// Align the two streams to T_icl. text_lens > codec: truncate
// text and stash the leftover into trailing_text_hidden. text_lens
// <= codec : pad text with tts_pad up to codec, trailing reduces
// <= codec: pad text with tts_pad up to codec, trailing reduces
// to tts_pad.
std::vector<float> aligned_text((size_t) T_icl * (size_t) hidden, 0.0f);
if (text_lens_icl >= T_icl) {
@@ -598,11 +590,11 @@ bool prompt_builder_build(const PipelineTTS * pt,
return false;
}
// Trailing text hidden : non streaming mode (no ICL) collapses the
// Trailing text hidden: non streaming mode (no ICL) collapses the
// overlay to a single row equal to tts_pad_embed
// (modeling_qwen3_tts.py line 2227). The full utterance text is
// already integrated into the prefill above as codec_pad summed text
// rows + tts_eos, so the overlay loop only ever needs tts_pad : step
// rows + tts_eos, so the overlay loop only ever needs tts_pad: step
// 0 reads trailing_text_hidden[0] which is tts_pad, every later step
// falls through to the else branch and reads tts_pad_embed. One row,
// bit exact with the Python hook dump.
+5 -5
View File
@@ -1,9 +1,9 @@
#pragma once
// prompt-builder.h : assemble the talker prefix input embedding from
// prompt-builder.h: assemble the talker prefix input embedding from
// a tokenized text plus a language tag, mirroring the upstream
// generate() function of Qwen3-TTS.
//
// Output shape : [T_ctx, hidden_size] f32 row-major. Two pad-aligned
// Output shape: [T_ctx, hidden_size] f32 row-major. Two pad-aligned
// streams (text and codec) are summed at the granularity of single
// vectors. The trailing text hidden buffer is also produced for the
// streaming-text overlay used during generation.
@@ -30,7 +30,7 @@ struct PromptBuilderOutput {
int T_ctx;
int hidden;
// Trailing text overlay : added on top of the next-token-input during
// Trailing text overlay: added on top of the next-token-input during
// the autoregressive loop, one vector per generated frame until
// exhausted, then tts_pad_embed for every following frame.
std::vector<float> trailing_text_hidden;
@@ -52,9 +52,9 @@ struct PromptBuilderOutput {
// (empty for none). speaker_name is the lowercased speaker key looked up
// in pt->speakers (empty for none). ref_spk_emb is an optional pointer to
// an [hidden] f32 vector that takes the place of the speaker preset row
// for voice clone mode A : when non NULL it is inserted between think_eos
// for voice clone mode A: when non NULL it is inserted between think_eos
// and codec_pad in the codec stream, mutually exclusive with speaker_name.
// ref_text and ref_codes activate voice clone mode B (ICL) : the prompt
// ref_text and ref_codes activate voice clone mode B (ICL): the prompt
// becomes [icl_text + tts_eos] aligned with [codec_bos + ref_codes_summed],
// where ref_codes is a flat [num_code_groups, T_codec] int32 buffer
// produced by pipeline_codec_encode on the resampled reference audio.
+5 -5
View File
@@ -1,5 +1,5 @@
#pragma once
// qt-error.h : internal helpers backing the public qwen_last_error
// qt-error.h: internal helpers backing the public qwen_last_error
// entry and the qwen_log callback routing.
//
// Not part of the public ABI. Translation units that emit user-facing
@@ -10,11 +10,11 @@
//
// Storage is thread_local so concurrent qwen_synthesize calls on
// different threads never race on each other's messages. The setter is
// variadic with printf semantics ; messages longer than the internal
// variadic with printf semantics; messages longer than the internal
// buffer are truncated, never split. Passing NULL as fmt clears the
// slot.
//
// qt_throw is the load-path counterpart : functions deep inside the
// qt_throw is the load-path counterpart: functions deep inside the
// GGUF reader and the codec load chain cannot return false up dozens
// of call sites without a massive cascade. They throw a
// std::runtime_error instead, which the ABI boundary entries
@@ -33,7 +33,7 @@
#include <cstdarg>
// Internal log level alias. Same values, same layout as the public
// qwen_log_level enum : a single underlying type means a single log
// qwen_log_level enum: a single underlying type means a single log
// callback installed through qwen_log_set routes every diagnostic
// without any cast or translation.
typedef enum qwen_log_level qt_log_level;
@@ -64,7 +64,7 @@ void qt_set_error_v(const char * fmt, va_list ap);
// Routes a formatted message at the requested level to the installed
// callback, or to stderr when none is set. The message is the full
// line without trailing newline ; routing layers add their own framing.
// line without trailing newline; routing layers add their own framing.
void qt_log(qt_log_level level, const char * fmt, ...)
#if defined(__GNUC__) || defined(__clang__)
__attribute__((format(printf, 2, 3)))
+10 -12
View File
@@ -1,10 +1,10 @@
#pragma once
// quantizer-decode.h : split RVQ decode for the Qwen3-TTS 12Hz tokenizer
// quantizer-decode.h: split RVQ decode for the Qwen3-TTS 12Hz tokenizer
// (GGML).
// Reads 16 codebooks (1 semantic + 15 acoustic) of 2048 entries with
// internal dim 256, and produces a 512-channel hidden representation.
//
// Decode side : codes [T, 16] i32 -> hidden [T, 512] f32 by summing
// Decode side: codes [T, 16] i32 -> hidden [T, 512] f32 by summing
// F.embedding(codes[:, k], codebook_k) within each split, then applying
// a per-split output_proj 1x1 conv (256 -> 512), then summing the two
// splits.
@@ -12,6 +12,7 @@
#include "ggml-backend.h"
#include "ggml.h"
#include "gguf-weights.h"
#include "qt-error.h"
#include "weight-ctx.h"
#include <cstdio>
@@ -46,13 +47,10 @@ struct QwenQuantizerDecoder {
static struct ggml_tensor * qwen_load_proj_1x1(WeightCtx * wctx, const GGUFModel & gf, const std::string & name) {
struct ggml_tensor * src = ggml_get_tensor(gf.meta, name.c_str());
if (!src) {
fprintf(stderr, "[Quantizer] FATAL: tensor '%s' not found\n", name.c_str());
exit(1);
qt_throw("[Quantizer] tensor '%s' not found", name.c_str());
}
if (src->ne[0] != 1) {
fprintf(stderr, "[Quantizer] FATAL: '%s' expected kernel=1 on ne[0], got %lld\n", name.c_str(),
(long long) src->ne[0]);
exit(1);
qt_throw("[Quantizer] '%s' expected kernel=1 on ne[0], got %lld", name.c_str(), (long long) src->ne[0]);
}
int64_t shape2d[2] = { src->ne[1], src->ne[2] }; // (in_dim, out_dim) in ggml row-major
return gf_load_tensor(wctx, gf, name, shape2d, 2);
@@ -127,7 +125,7 @@ static void qwen_quantizer_decoder_free(QwenQuantizerDecoder * dec) {
// split, then project from internal_dim (256) to hidden (512) via a
// Conv1d 1x1 (mat_mul against out_proj_w).
//
// codes_split : [T, K] i32, K is the codebook count of this split
// codes_split: [T, K] i32, K is the codebook count of this split
// returns : [hidden, T] f32
static struct ggml_tensor * qwen_rvq_group_decode(struct ggml_context * ctx,
const QwenRVQGroup & g,
@@ -139,14 +137,14 @@ static struct ggml_tensor * qwen_rvq_group_decode(struct ggml_context * ctx,
struct ggml_tensor * emb = ggml_get_rows(ctx, g.embed[k], idx);
sum = (sum == NULL) ? emb : ggml_add(ctx, sum, emb);
}
// sum : [internal_dim=256, T]
// out_proj_w : [internal_dim=256, hidden=512]
// sum: [internal_dim=256, T]
// out_proj_w: [internal_dim=256, hidden=512]
// ggml_mul_mat returns [hidden=512, T]
return ggml_mul_mat(ctx, g.out_proj_w, sum);
}
// codes : [T, num_quantizers=16] i32
// returns : [hidden=512, T] f32
// codes: [T, num_quantizers=16] i32
// returns: [hidden=512, T] f32
static struct ggml_tensor * qwen_quantizer_decode(struct ggml_context * ctx,
const QwenQuantizerDecoder * dec,
struct ggml_tensor * codes) {
+2 -2
View File
@@ -9,7 +9,7 @@
// Each side has the same shape:
// input_proj : Conv1d k=1, 512 -> 256 (linear projection on channels)
// codebooks : list of [2048, 256] f32 entries used as kNN centroids
// output_proj : Conv1d k=1, 256 -> 512 (used only inside the residual loop)
// output_proj: Conv1d k=1, 256 -> 512 (used only inside the residual loop)
//
// At encode time we run, for each side:
// y = input_proj(x)
@@ -230,7 +230,7 @@ static void qwen_quantizer_encode_side_loop(const QwenQuantizerEncodeHost * h,
// Full RVQ encode. Takes the post-downsample hidden [T, hidden_size] f32
// row-major buffer and returns flat codes [K, T] row-major, where K is
// QWEN_ENC_QUANT_TOTAL = 16.
// hidden : [T, hidden_size] f32 row-major (T fast in pseudo, but here
// hidden: [T, hidden_size] f32 row-major (T fast in pseudo, but here
// row-major means index = t*hidden + c, t slow, c fast)
//
// Returns codes flat as [16, T] row-major: codes[k*T + t].
+19 -19
View File
@@ -43,7 +43,7 @@ struct qwen_context {
};
// Thread-local backing store for qt_last_error(). std::string sized once
// per thread, grows on demand, never freed across calls : the std runtime
// per thread, grows on demand, never freed across calls: the std runtime
// reclaims it on thread exit. An empty string means "no error recorded
// on this thread yet", which qt_last_error() exposes as "".
static thread_local std::string g_last_error;
@@ -53,14 +53,14 @@ void qt_set_error_v(const char * fmt, va_list ap) {
g_last_error.clear();
return;
}
// Two-pass vsnprintf : first call sizes the buffer, second writes the
// Two-pass vsnprintf: first call sizes the buffer, second writes the
// message. va_copy keeps the original ap valid for the second pass.
va_list ap2;
va_copy(ap2, ap);
int needed = std::vsnprintf(nullptr, 0, fmt, ap2);
va_end(ap2);
if (needed < 0) {
g_last_error = "qt_set_error : vsnprintf failed";
g_last_error = "qt_set_error: vsnprintf failed";
return;
}
g_last_error.resize(static_cast<size_t>(needed));
@@ -96,7 +96,7 @@ void qt_throw(const char * fmt, ...) {
}
// Process-wide log callback. Atomic so qwen_log_set can replace it without
// locking : write happens with memory_order_release, every reader sees a
// locking: write happens with memory_order_release, every reader sees a
// fully published callback pointer paired with its user_data slot.
// std::atomic on a function pointer is lock-free on every platform we
// target. user_data is a plain pointer because it is only ever published
@@ -206,20 +206,20 @@ void qwen_tts_default_params(struct qwen_tts_params * p) {
struct qwen_context * qwen_init(const struct qwen_init_params * params) {
if (!params || !params->talker_path || !params->codec_path) {
qt_set_error("qwen_init : params, talker_path or codec_path is NULL");
qt_log(QT_LOG_ERROR, "[qwen] qwen_init requires talker_path and codec_path");
qt_set_error("qwen_init: params, talker_path or codec_path is NULL");
qt_log(QT_LOG_ERROR, "[Qwen] qwen_init requires talker_path and codec_path");
return nullptr;
}
if (params->abi_version > QWEN_ABI_VERSION) {
qt_set_error(
"qwen_init : params->abi_version %d > QWEN_ABI_VERSION %d (binding compiled against a newer header)",
"qwen_init: params->abi_version %d > QWEN_ABI_VERSION %d (binding compiled against a newer header)",
params->abi_version, QWEN_ABI_VERSION);
qt_log(QT_LOG_ERROR, "[qwen] qwen_init params struct is from a newer ABI (%d > %d)", params->abi_version,
qt_log(QT_LOG_ERROR, "[Qwen] qwen_init params struct is from a newer ABI (%d > %d)", params->abi_version,
QWEN_ABI_VERSION);
return nullptr;
}
qt_log(QT_LOG_INFO, "[qwen] qwentts.cpp %s", qwen_version());
qt_log(QT_LOG_INFO, "[Qwen] qwentts.cpp %s", qwen_version());
// new qwen_context() value-initialises every field: POD aggregates
// (BackendPair, PipelineTTS) are zero-init, std containers in
@@ -228,17 +228,17 @@ struct qwen_context * qwen_init(const struct qwen_init_params * params) {
// The load chain runs inside a try block. Any failure deep in the
// GGUF reader, the codec load or the LM weight load throws via
// qt_throw ; the catch funnels every variant into one cleanup via
// qt_throw; the catch funnels every variant into one cleanup via
// qwen_free, which is idempotent on partial state (NULL-safe sched,
// NULL GGUF handles, refcount-correct backend release).
try {
q->bp = backend_init("Talker");
if (!q->bp.backend) {
qt_throw("qwen_init : backend_init failed (no GGML backend available)");
qt_throw("qwen_init: backend_init failed (no GGML backend available)");
}
if (!pipeline_tts_load(&q->pt, params->talker_path, params->codec_path, q->bp)) {
qt_throw("qwen_init : pipeline_tts_load failed for '%s' / '%s'", params->talker_path, params->codec_path);
qt_throw("qwen_init: pipeline_tts_load failed for '%s' / '%s'", params->talker_path, params->codec_path);
}
// BPE tokenizer payload lives inside the talker GGUF. Load the
@@ -246,7 +246,7 @@ struct qwen_context * qwen_init(const struct qwen_init_params * params) {
// specials key list mirrors what the standalone CLI used to
// do before the facade hoisted the load chain.
if (!load_bpe_from_gguf(&q->tok, params->talker_path)) {
qt_throw("qwen_init : load_bpe_from_gguf failed for '%s'", params->talker_path);
qt_throw("qwen_init: load_bpe_from_gguf failed for '%s'", params->talker_path);
}
const char * specials_keys[] = {
"qwen3-tts.text.im_start_id", "qwen3-tts.text.im_end_id", "qwen3-tts.text.tts_pad_id",
@@ -255,7 +255,7 @@ struct qwen_context * qwen_init(const struct qwen_init_params * params) {
bpe_load_specials_from_keys(&q->tok, params->talker_path, specials_keys, 5);
} catch (const std::exception & e) {
qt_set_error("%s", e.what());
qt_log(QT_LOG_ERROR, "[qwen] %s", e.what());
qt_log(QT_LOG_ERROR, "[Qwen] %s", e.what());
qwen_free(q);
return nullptr;
}
@@ -286,7 +286,7 @@ enum qwen_status qwen_synthesize(struct qwen_context * q,
const struct qwen_tts_params * params,
struct qwen_audio * out) {
if (!q || !params || !out) {
qt_set_error("qwen_synthesize : q, params or out is NULL");
qt_set_error("qwen_synthesize: q, params or out is NULL");
if (out) {
qwen_audio_free(out);
}
@@ -294,7 +294,7 @@ enum qwen_status qwen_synthesize(struct qwen_context * q,
}
if (params->abi_version > QWEN_ABI_VERSION) {
qt_set_error(
"qwen_synthesize : params->abi_version %d > QWEN_ABI_VERSION %d (binding compiled against a newer header)",
"qwen_synthesize: params->abi_version %d > QWEN_ABI_VERSION %d (binding compiled against a newer header)",
params->abi_version, QWEN_ABI_VERSION);
qwen_audio_free(out);
return QWEN_STATUS_INVALID_PARAMS;
@@ -345,7 +345,7 @@ enum qwen_status qwen_synthesize(struct qwen_context * q,
// Translate the public POD params into the internal C++ struct
// expected by pipeline_tts_synthesize. Borrowed pointers are
// forwarded verbatim ; the lifetime contract on the public side
// forwarded verbatim; the lifetime contract on the public side
// (caller keeps strings alive for the duration of the call)
// matches what the pipeline already requires.
PipelineTTSSynthesizeParams p = {};
@@ -389,7 +389,7 @@ enum qwen_status qwen_synthesize(struct qwen_context * q,
const size_t bytes = n * sizeof(float);
float * buf = (float *) std::malloc(bytes > 0 ? bytes : 1);
if (!buf) {
qt_set_error("qwen_synthesize : malloc failed for %zu samples", n);
qt_set_error("qwen_synthesize: malloc failed for %zu samples", n);
qwen_audio_free(out);
return QWEN_STATUS_OOM;
}
@@ -403,7 +403,7 @@ enum qwen_status qwen_synthesize(struct qwen_context * q,
return QWEN_STATUS_OK;
} catch (const std::exception & e) {
qt_set_error("%s", e.what());
qt_log(QT_LOG_ERROR, "[qwen] %s", e.what());
qt_log(QT_LOG_ERROR, "[Qwen] %s", e.what());
qwen_audio_free(out);
return QWEN_STATUS_GENERATE_FAILED;
}
+12 -12
View File
@@ -54,7 +54,7 @@ extern "C" {
// callers and the lib reads only what its abi_version permits.
//
// There is no separate semver triple. The runtime build identity is the
// git short hash + commit date string returned by qwen_version() ; for
// git short hash + commit date string returned by qwen_version(); for
// binding compat checks, QWEN_ABI_VERSION is the only number that
// matters. Aligned on OV_ABI_VERSION = 2 for the omnivoice ABI cousin.
#define QWEN_ABI_VERSION 2
@@ -77,7 +77,7 @@ enum qwen_status {
// Returns the last error message produced on the calling thread by any
// qwen_* entry, as a NUL terminated UTF-8 string. errno-style semantics:
// the pointer is only meaningful right after a failure (qwen_init
// returning NULL, or any qwen_* entry returning a negative qwen_status) ;
// returning NULL, or any qwen_* entry returning a negative qwen_status);
// calling it after a successful entry yields the previous message or an
// empty string. Storage is thread local so two threads running
// qwen_synthesize concurrently never race on each other's diagnostics.
@@ -106,10 +106,10 @@ struct qwen_context;
// Initialisation parameters. Both GGUF paths are required: the talker
// GGUF holds the LM weights, the code predictor MTP head and (for
// custom_voice / voice_design checkpoints) the speaker encoder ; the
// custom_voice / voice_design checkpoints) the speaker encoder; the
// codec GGUF holds the 12 Hz audio tokenizer. abi_version stays first
// so a future struct growth keeps reading the version field at offset
// 0. No use_fa / clamp_fp16 yet : the current pipeline_tts_load picks
// 0. No use_fa / clamp_fp16 yet: the current pipeline_tts_load picks
// flash attention from backend capability without a user knob.
struct qwen_init_params {
int abi_version;
@@ -134,8 +134,8 @@ QWEN_API void qwen_free(struct qwen_context * q);
// Log severity. Numerically ordered so a callback can filter with a
// simple `if (level < threshold) return;`. ERROR is reserved for
// failure reports that the lib also surfaces via qwen_status /
// qwen_last_error ; WARN for recoverable surprises ; INFO for the
// normal load and synthesis cadence ; DEBUG for tensor-level cossim
// qwen_last_error; WARN for recoverable surprises; INFO for the
// normal load and synthesis cadence; DEBUG for tensor-level cossim
// diagnostics.
enum qwen_log_level {
QWEN_LOG_DEBUG = 0,
@@ -152,17 +152,17 @@ enum qwen_log_level {
typedef void (*qwen_log_cb)(enum qwen_log_level level, const char * msg, void * user_data);
// Install a global log callback. Passing cb == NULL restores the
// default behaviour (write to stderr). Safe to call at any point ;
// default behaviour (write to stderr). Safe to call at any point;
// takes effect immediately on subsequent log emissions across every
// thread. Storage is process wide, not per handle, matching
// whisper_log_set / llama_log_set / ov_log_set.
QWEN_API void qwen_log_set(qwen_log_cb cb, void * user_data);
// Synthesis parameters. Strings are NULL terminated UTF-8 ; NULL maps
// Synthesis parameters. Strings are NULL terminated UTF-8; NULL maps
// to empty where the underlying pipeline accepts it. The selection
// between base / custom_voice / voice_design synthesis mode is driven
// by the model_type read from the talker GGUF at qwen_init time, not
// by an explicit flag here ; the seven mode rules are enforced inside
// by an explicit flag here; the seven mode rules are enforced inside
// qwen_synthesize and surface as QWEN_STATUS_MODE_INVALID with a
// descriptive qwen_last_error(). abi_version stays first so the lib
// can route on it before reading any field that may have shifted in a
@@ -172,7 +172,7 @@ struct qwen_tts_params {
// Input text and language hint. lang accepts the upstream
// qwen3-tts language names ("english", "chinese", "auto", ...).
// instruct is the style instruction string ; required for
// instruct is the style instruction string; required for
// voice_design, optional for custom_voice, rejected for base.
// speaker is the named speaker for custom_voice models, rejected
// for the other two modes.
@@ -182,7 +182,7 @@ struct qwen_tts_params {
const char * speaker;
// Optional voice reference for base mode voice cloning. Mode A
// (x_vector_only) sets ref_audio_24k only ; mode B (ICL) sets
// (x_vector_only) sets ref_audio_24k only; mode B (ICL) sets
// both ref_audio_24k and ref_text. ref_audio_24k is a mono float
// PCM buffer sampled at the codec sample rate (24 kHz). Mutually
// exclusive with speaker. Rejected for custom_voice / voice_design.
@@ -223,7 +223,7 @@ QWEN_API void qwen_tts_default_params(struct qwen_tts_params * p);
// model_type (the seven base / custom_voice / voice_design rules),
// resolves the seed, hands off to pipeline_tts_synthesize and fills
// `out` with mono float PCM at the codec sample rate. Returns
// QWEN_STATUS_OK on success ; on any failure returns a negative
// QWEN_STATUS_OK on success; on any failure returns a negative
// qwen_status describing the cause and leaves `out` empty.
QWEN_API enum qwen_status qwen_synthesize(struct qwen_context * q,
const struct qwen_tts_params * params,
+2 -2
View File
@@ -76,7 +76,7 @@ static inline void apply_repetition_penalty(float * logits,
// 5. softmax
// 6. multinomial via philox_uniform_fill(seed, philox_subseq, 0)
//
// Greedy path : temperature <= 0 returns argmax over the suppressed
// Greedy path: temperature <= 0 returns argmax over the suppressed
// logits, no rep_pen, no philox draw.
//
// Buffers are thread_local to avoid alloc per token.
@@ -154,7 +154,7 @@ static int sample_top_k_p(float * logits,
if (K > 0) {
std::sort(sorted_buf.begin(), sorted_buf.end(),
[](const TokenProb & a, const TokenProb & b) { return a.prob > b.prob; });
// HF convention : keep tokens until the cumulative probability
// HF convention: keep tokens until the cumulative probability
// crosses top_p, drop the rest. Test before accumulate so the
// first crossing entry is kept.
float cum = 0.0f;
+1 -1
View File
@@ -190,7 +190,7 @@ static struct ggml_tensor * qwen_seanet_resnet_forward(struct ggml_context *
// layout ne=(C, T_out) for debug bisection. Each is NULL by default and
// the caller decides whether to mark them as graph outputs.
// init_out : post init MimiConv1d k=7, [T_audio, 64]
// resnet0_out : post stage 0 resnet block, before ELU+downsample, [T_audio, 64]
// resnet0_out: post stage 0 resnet block, before ELU+downsample, [T_audio, 64]
// stage0_out : post stage 0 (resnet + ELU + downsample 4x), [T_audio/4, 128]
// stage1_out : post stage 1 (resnet + ELU + downsample 5x), [T_audio/20, 256]
// stage3_out : post stage 3 (resnet + ELU + downsample 8x), [T_audio/960, 1024]
+7 -7
View File
@@ -1,5 +1,5 @@
#pragma once
// speaker-encoder-extract.h : end to end speaker embedding extraction
// speaker-encoder-extract.h: end to end speaker embedding extraction
// from a WAV path. Loads, mono-mixes and resamples to 24 kHz, reflect
// pads by (n_fft - hop) / 2 = 384 samples, builds the fused mel + ECAPA
// graph and returns the f32 [enc_dim] embedding.
@@ -12,7 +12,7 @@
// hop=256, win=1024, fmin=0, fmax=12000, center=False)
// spk_emb = speaker_encoder(mels)[0]
//
// Memory layout : the audio waveform input is passed as a regular ggml
// Memory layout: the audio waveform input is passed as a regular ggml
// input tensor [T_pad] f32 living on the talker backend. Caller owns the
// returned vector. The graph context is freed after each call.
@@ -80,7 +80,7 @@ static bool speaker_encoder_extract(const SpeakerEncoderWeights * sw,
audio_padded[(size_t) (pad + T_in + i)] = raw[T_in - 2 - i];
}
// Bake CPU constants once per call : Hann, DFT, mel basis. The cost
// Bake CPU constants once per call: Hann, DFT, mel basis. The cost
// is dominated by the DFT precompute which is 524 KB of f32.
AudioMelConstants mel_c;
audio_mel_compute_constants(mel_cfg, mel_c);
@@ -95,7 +95,7 @@ static bool speaker_encoder_extract(const SpeakerEncoderWeights * sw,
init.no_alloc = true;
struct ggml_context * gctx = ggml_init(init);
// Graph inputs : audio waveform and 4 mel constants.
// Graph inputs: audio waveform and 4 mel constants.
struct ggml_tensor * audio_in = ggml_new_tensor_1d(gctx, GGML_TYPE_F32, T_pad);
struct ggml_tensor * hann_in = ggml_new_tensor_1d(gctx, GGML_TYPE_F32, mel_cfg.n_fft);
struct ggml_tensor * dft_re_in = ggml_new_tensor_2d(gctx, GGML_TYPE_F32, mel_cfg.n_fft, mel_c.n_freq);
@@ -199,7 +199,7 @@ static bool speaker_encoder_extract(const SpeakerEncoderWeights * sw,
ggml_build_forward_expand(graph, asp_dump);
}
// Reset the shared sched before allocating : the talker may have left
// Reset the shared sched before allocating: the talker may have left
// a residual graph state from a previous synthesis call.
ggml_backend_sched_reset(sched);
if (!ggml_backend_sched_alloc_graph(sched, graph)) {
@@ -237,7 +237,7 @@ static bool speaker_encoder_extract(const SpeakerEncoderWeights * sw,
// raw ggml memory layout, matching the Python side dump.
debug_dump_2d(&d, "mel-spk", buf.data(), (int) mel_dump->ne[1], (int) mel_dump->ne[0]);
// CPU side mel constants : audit against torch.hann_window and
// CPU side mel constants: audit against torch.hann_window and
// librosa.filters.mel produced by the Python upstream. Layouts
// are kept as numpy [n_fft] for hann and [n_mels, n_freq] for
// mel_basis, matching the librosa convention.
@@ -249,7 +249,7 @@ static bool speaker_encoder_extract(const SpeakerEncoderWeights * sw,
std::vector<float> bm(nm);
ggml_backend_tensor_get(mag_dump, bm.data(), 0, nm * sizeof(float));
// mag_dump has ggml ne=(n_freq, T_frames). Same dumping
// convention as mel-spk : passing (ne[1], ne[0]) writes
// convention as mel-spk: passing (ne[1], ne[0]) writes
// shape [T_frames, n_freq] over the raw memory layout.
debug_dump_2d(&d, "mel-mag", bm.data(), (int) mag_dump->ne[1], (int) mag_dump->ne[0]);
}
+22 -22
View File
@@ -1,5 +1,5 @@
#pragma once
// speaker-encoder-forward.h : ECAPA-TDNN forward graph in GGML.
// 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
@@ -16,7 +16,7 @@
// -> FC k=1 [2048, 1]
// -> squeeze [2048]
//
// Tensor convention : [C, T] inside the graph (ne[0]=C, ne[1]=T) so that
// 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.
@@ -55,14 +55,14 @@ static struct ggml_tensor * spk_conv1d_same(struct ggml_context * ctx,
// 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,
// 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).
// 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
@@ -71,7 +71,7 @@ static struct ggml_tensor * spk_conv1d_same(struct ggml_context * ctx,
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
// 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
@@ -80,7 +80,7 @@ static struct ggml_tensor * spk_conv1d_same(struct ggml_context * ctx,
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].
// 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);
@@ -91,7 +91,7 @@ static struct ggml_tensor * spk_conv1d_same(struct ggml_context * ctx,
return y;
}
// TDNN block : Conv1d(same, reflect) + ReLU. Used both as the conv0
// 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,
@@ -102,7 +102,7 @@ static struct ggml_tensor * spk_tdnn(struct ggml_context * ctx,
return y;
}
// Res2Net block : split the channel axis in 8 chunks. chunk 0 passes
// 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.
@@ -152,7 +152,7 @@ static struct ggml_tensor * spk_res2net(struct ggml_context * ctx,
return acc;
}
// Squeeze and Excitation : compute the temporal mean per channel,
// 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.
@@ -180,7 +180,7 @@ static struct ggml_tensor * spk_se(struct ggml_context * ctx, const SpkEncSE & s
return y;
}
// SE-Res2Net block : tdnn1 (1x1) -> Res2Net -> tdnn2 (1x1) -> SE plus
// 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,
@@ -194,7 +194,7 @@ static struct ggml_tensor * spk_block(struct ggml_context * ctx,
return ggml_add(ctx, h, residual);
}
// Attentive Statistical Pooling : compute global mean and std along T,
// 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.
//
@@ -205,7 +205,7 @@ static struct ggml_tensor * spk_asp(struct ggml_context * ctx, const SpkEncASP &
const int T = (int) x->ne[1];
// Mean and std over T axis. The mask reduction is uniform 1/T.
// mean : [C, 1]
// 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));
@@ -226,9 +226,9 @@ static struct ggml_tensor * spk_asp(struct ggml_context * ctx, const SpkEncASP &
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
// 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
// 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);
@@ -239,7 +239,7 @@ static struct ggml_tensor * spk_asp(struct ggml_context * ctx, const SpkEncASP &
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.
// 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
@@ -249,7 +249,7 @@ static struct ggml_tensor * spk_asp(struct ggml_context * ctx, const SpkEncASP &
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.
// 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));
@@ -285,7 +285,7 @@ static struct ggml_tensor * spk_asp(struct ggml_context * ctx, const SpkEncASP &
// 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).
// 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,
@@ -300,14 +300,14 @@ static struct ggml_tensor * speaker_encoder_forward(struct ggml_context *
struct ggml_tensor ** block3_out = NULL,
struct ggml_tensor ** mfa_out = NULL,
struct ggml_tensor ** asp_out = NULL) {
// Mel : [n_mels=128, T_frames]
// 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.
// 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;
@@ -321,7 +321,7 @@ static struct ggml_tensor * speaker_encoder_forward(struct ggml_context *
*block3_out = b3;
}
// Multi-layer feature aggregation : cat blk1..3 then 1x1 TDNN + ReLU.
// 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]
@@ -329,13 +329,13 @@ static struct ggml_tensor * speaker_encoder_forward(struct ggml_context *
*mfa_out = mfa;
}
// Attentive statistical pooling : [1536, T] -> [3072, 1].
// 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].
// 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
+9 -9
View File
@@ -1,5 +1,5 @@
#pragma once
// speaker-encoder-weights.h : ECAPA-TDNN x-vector extractor used by the
// speaker-encoder-weights.h: ECAPA-TDNN x-vector extractor used by the
// Base checkpoint to condition the Talker on a reference voice.
//
// Topology (from qwen_tts.core.models.modeling_qwen3_tts) :
@@ -15,7 +15,7 @@
// quantizing because should_quantize keeps spk_enc as is (small
// channel counts make quantization meaningless here).
//
// Constants : enc_dim 2048 (size of the speaker embedding fed into
// Constants: enc_dim 2048 (size of the speaker embedding fed into
// the codec_prefill slot), input mel_dim 128, ECAPA hidden 512,
// res2net scale 8 -> 7 dilated TDNN branches, se hidden 128,
// asp attention 128.
@@ -31,7 +31,7 @@
#include <string>
#include <vector>
// Initial TDNN block : Conv1d(in=128, out=512, k=5, padding=same, reflect)
// Initial TDNN block: Conv1d(in=128, out=512, k=5, padding=same, reflect)
// followed by ReLU. Stored as 3D tensor [k, in_c, out_c] in the GGUF.
struct SpkEncTDNN {
struct ggml_tensor * weight; // [k, in_c, out_c]
@@ -41,7 +41,7 @@ struct SpkEncTDNN {
int out_c;
};
// Squeeze-Excitation attention : conv1 (out -> se), conv2 (se -> out),
// Squeeze-Excitation attention: conv1 (out -> se), conv2 (se -> out),
// k=1 padding=same. Operates on the temporal mean of the input then
// broadcasts a sigmoid gate back over the time axis.
struct SpkEncSE {
@@ -51,7 +51,7 @@ struct SpkEncSE {
struct ggml_tensor * conv2_b; // [out_c]
};
// Res2Net branch : 7 dilated TDNN k=3 conv1d, dilation comes from the
// Res2Net branch: 7 dilated TDNN k=3 conv1d, dilation comes from the
// parent SE-Res2Net block. We keep flat arrays since enc_res2net_scale
// is 8 (which yields scale - 1 = 7 branches).
struct SpkEncRes2Net {
@@ -59,7 +59,7 @@ struct SpkEncRes2Net {
struct ggml_tensor * bias[7]; // each [out_c/8]
};
// SE-Res2Net block : tdnn1 (k=1) -> Res2Net (k=3, dil=d) -> tdnn2 (k=1)
// SE-Res2Net block: tdnn1 (k=1) -> Res2Net (k=3, dil=d) -> tdnn2 (k=1)
// -> SE attention, plus a residual add over the whole stack.
struct SpkEncBlock {
SpkEncTDNN tdnn1;
@@ -69,7 +69,7 @@ struct SpkEncBlock {
int dilation;
};
// Attentive Statistical Pooling : tdnn maps from 3*1536 to 128 (channels
// Attentive Statistical Pooling: tdnn maps from 3*1536 to 128 (channels
// concat of [x, mean, std]), conv maps 128 back to 1536. The mask
// branch reduces to a no-op for unbatched single-utterance inference,
// which is the only path the C++ side exposes.
@@ -156,7 +156,7 @@ static bool speaker_encoder_weights_load(SpeakerEncoderWeights * sw, const GGUFM
sw->se_channels = 128;
sw->res2net_scale = 8;
// Probe : Base GGUFs ship the speaker encoder, CustomVoice and
// Probe: Base GGUFs ship the speaker encoder, CustomVoice and
// VoiceDesign do not. A missing conv0.weight aborts cleanly.
if (gguf_find_tensor(gf.gguf, "spk_enc.conv0.weight") < 0) {
fprintf(stderr, "[SpeakerEncoder] No spk_enc.conv0.weight, base/clone mode unavailable\n");
@@ -165,7 +165,7 @@ static bool speaker_encoder_weights_load(SpeakerEncoderWeights * sw, const GGUFM
return true;
}
// Roughly 80 tensors total : 1 conv0 + 3 * (2 tdnn + 7 res2net + 4 se) + 1 mfa
// Roughly 80 tensors total: 1 conv0 + 3 * (2 tdnn + 7 res2net + 4 se) + 1 mfa
// + asp.tdnn + asp.conv + fc, with weight + bias each. Allocate 100 slots
// for safety.
WeightCtx wctx;
+15 -15
View File
@@ -1,4 +1,4 @@
// talker-forward.cpp : eager prefill graph for the Talker LM.
// talker-forward.cpp: eager prefill graph for the Talker LM.
//
// Mirrors Qwen3TTSTalkerDecoderLayer for TTS-only operation :
// pre-norm, GQA attention with per-head QK-norm, mrope collapsed to
@@ -7,7 +7,7 @@
// RMSNorm and codec_head. Eager softmax in F32, no flash-attention,
// no KV cache.
//
// Tensor shapes follow ggml row-major convention : ne[0] is the fastest
// Tensor shapes follow ggml row-major convention: ne[0] is the fastest
// axis. Our input embedding lives as [hidden, T] inside the graph and
// the loader feeds it from a [T, hidden] f32 row-major host buffer
// (which becomes [hidden, T] in ggml after a 2d view because rows on
@@ -25,7 +25,7 @@
#include <vector>
// Bisect layers dumped when a dump_dir is set. Match the Python hook list
// in tests/debug-tts-cossim.py : 0, 7, 14, 21, 27.
// in tests/debug-tts-cossim.py: 0, 7, 14, 21, 27.
static const int BISECT_LAYERS[] = { 0, 7, 14, 21, 27 };
static const int N_BISECT_LAYERS = (int) (sizeof(BISECT_LAYERS) / sizeof(BISECT_LAYERS[0]));
@@ -72,7 +72,7 @@ static struct ggml_tensor * talker_layer_forward(struct ggml_context * ctx,
k = ggml_reshape_3d(ctx, k, hd, n_kv, T);
v = ggml_reshape_3d(ctx, v, hd, n_kv, T);
// Per-head QK-norm : RMS over hd, then multiply by [hd] gain. The
// Per-head QK-norm: RMS over hd, then multiply by [hd] gain. The
// norm operates on ne[0] = hd, identical layout for q (16 heads) and
// k (8 heads), so the same code path covers both.
q = ggml_rms_norm(ctx, q, eps);
@@ -114,9 +114,9 @@ static struct ggml_tensor * talker_layer_forward(struct ggml_context * ctx,
struct ggml_tensor * v_full = ggml_view_3d(ctx, v_cache, hd, T_full, n_kv, v_cache->nb[1], v_cache->nb[2], 0);
// Q permute [hd, n_q_heads, T] -> [hd, T, n_q_heads]. flash_attn_ext
// expects ne[1] = n_batch and ne[2] = n_head ; the GQA broadcast
// expects ne[1] = n_batch and ne[2] = n_head; the GQA broadcast
// check ggml_can_mul_mat is n_head % n_head_kv == 0, matching K layout.
// No cont : flash_attn_ext takes the view directly, like acestep does.
// No cont: flash_attn_ext takes the view directly, like acestep does.
struct ggml_tensor * q_p = ggml_permute(ctx, q, 0, 2, 1, 3);
// Fused flash attention. The manual mul_mat + soft_max_ext + mul_mat
@@ -137,7 +137,7 @@ static struct ggml_tensor * talker_layer_forward(struct ggml_context * ctx,
x = ggml_add(ctx, x, o);
// MLP block : pre-norm + SwiGLU + residual
// MLP block: pre-norm + SwiGLU + residual
struct ggml_tensor * h2 = ggml_rms_norm(ctx, x, eps);
h2 = ggml_mul(ctx, h2, layer.post_attn_norm_w);
@@ -169,7 +169,7 @@ static bool talker_forward_core(const TalkerWeights * tw,
const int T_full = n_past + T;
// Dedicated context for graph + IO tensors. Counts approximate :
// per layer : ~38 ops (added 2 cpy + 4 views per layer for KV)
// per layer: ~38 ops (added 2 cpy + 4 views per layer for KV)
// IO : 4 tensors (input embed, positions, mask, output norm)
// final : norm + codec_head + dump branches
const int max_nodes = 48 * n_layers + 256;
@@ -186,8 +186,8 @@ static bool talker_forward_core(const TalkerWeights * tw,
return false;
}
// IO tensors : input embedding, positions, causal mask. The mask
// spans [T_full, T] : for each fresh query q in [0, T) we allow
// IO tensors: input embedding, positions, causal mask. The mask
// spans [T_full, T]: for each fresh query q in [0, T) we allow
// keys k in [0, n_past + q]. In the decode case (T=1) this is a
// single row of zeros of length T_full.
struct ggml_tensor * x_in = ggml_new_tensor_2d(gctx, GGML_TYPE_F32, hidden, T);
@@ -225,7 +225,7 @@ static bool talker_forward_core(const TalkerWeights * tw,
ggml_set_name(h_final, "hidden_final");
ggml_set_output(h_final);
// codec_head : [hidden, vocab]. ggml_mul_mat returns [vocab, T].
// codec_head: [hidden, vocab]. ggml_mul_mat returns [vocab, T].
struct ggml_tensor * logits = ggml_mul_mat(gctx, tw->codec_head_w, h_final);
ggml_set_name(logits, "logits");
@@ -249,7 +249,7 @@ static bool talker_forward_core(const TalkerWeights * tw,
// Upload input embedding (host [T, hidden] -> ggml [hidden, T]).
ggml_backend_tensor_set(x_in, input_embed, 0, (size_t) T * (size_t) hidden * sizeof(float));
// Positions : n_past .. n_past + T - 1
// Positions: n_past .. n_past + T - 1
{
std::vector<int32_t> pos((size_t) T);
for (int i = 0; i < T; i++) {
@@ -258,7 +258,7 @@ static bool talker_forward_core(const TalkerWeights * tw,
ggml_backend_tensor_set(pos_in, pos.data(), 0, (size_t) T * sizeof(int32_t));
}
// Causal mask : 0 where k <= n_past + q, -inf otherwise. Stored
// Causal mask: 0 where k <= n_past + q, -inf otherwise. Stored
// row-major [T_q, T_k] with T_k as the fast axis (ne[0]). F16 dtype
// matches ggml_flash_attn_ext convention used by the attention path.
{
@@ -284,7 +284,7 @@ static bool talker_forward_core(const TalkerWeights * tw,
return false;
}
// Bisect dumps : pull each tap [hidden, T] back to host as [T, hidden].
// Bisect dumps: pull each tap [hidden, T] back to host as [T, hidden].
if (record_taps) {
DebugDumper d;
debug_init(&d, dump_dir);
@@ -302,7 +302,7 @@ static bool talker_forward_core(const TalkerWeights * tw,
debug_dump_2d(&d, "talker-hidden-prefill-final", buf.data(), T, hidden);
}
// Pull the last position : final hidden + logits
// Pull the last position: final hidden + logits
out->hidden = hidden;
out->vocab = vocab;
out->hidden_last.assign((size_t) hidden, 0.0f);
+5 -5
View File
@@ -1,5 +1,5 @@
#pragma once
// talker-forward.h : prefill + decode forwards of the Talker LM, KV
// talker-forward.h: prefill + decode forwards of the Talker LM, KV
// cached.
//
// Both entry points run the same 28-layer Qwen3 decoder stack with
@@ -8,13 +8,13 @@
// and projected through codec_head to produce codebook 0 logits over a
// 3072-entry vocab.
//
// talker_forward_prefill : feeds a [T_ctx, hidden] input embedding,
// talker_forward_prefill: feeds a [T_ctx, hidden] input embedding,
// rewinds the KV cache to 0 and writes T_ctx positions into it. Used
// once per utterance at the start of generation, and re-runnable for
// bisect dumps. Optional dump_dir captures L0/7/14/21/27 hidden taps
// plus the final hidden and logits.
//
// talker_forward_decode : feeds a single [1, hidden] embedding,
// talker_forward_decode: feeds a single [1, hidden] embedding,
// appends one position to the cache at index kv->cur_len, attends to
// the [0, cur_len+1) window. Called once per generated frame after
// the predictor has produced its 15 acoustic codes and the loop has
@@ -44,7 +44,7 @@ struct TalkerForwardOutput {
int vocab;
};
// Prefill : reset the cache and write T_ctx positions in one shot.
// Prefill: reset the cache and write T_ctx positions in one shot.
// input_embed is [T, hidden] f32 row-major. dump_dir may be NULL.
bool talker_forward_prefill(const TalkerWeights * tw,
KVCache * kv,
@@ -54,7 +54,7 @@ bool talker_forward_prefill(const TalkerWeights * tw,
const char * dump_dir,
TalkerForwardOutput * out);
// Decode : feed exactly one embedding and append one position to the
// Decode: feed exactly one embedding and append one position to the
// cache. Reads positions [0, kv->cur_len + 1). Caller is responsible
// for ensuring kv->cur_len + 1 <= kv->max_seq_len.
bool talker_forward_decode(const TalkerWeights * tw,
+2 -2
View File
@@ -1,10 +1,10 @@
#pragma once
// talker-weights.h : Qwen3-style autoregressive Talker LM weights.
// talker-weights.h: Qwen3-style autoregressive Talker LM weights.
//
// Carries 28 decoder layers in 0.6B (36 in 1.7B), each with pre-norm
// attention plus pre-norm SwiGLU MLP. Attention is multi-head with GQA
// (16 query heads, 8 kv heads, head_dim 128) and per-head QK-norm. RoPE
// is mrope-interleaved with sections [24, 20, 20] and freq base 1e6 ;
// is mrope-interleaved with sections [24, 20, 20] and freq base 1e6;
// in TTS-only mode this collapses to plain interleaved 1D RoPE since
// the three multimodal axes carry the same position index.
//
+1 -1
View File
@@ -7,7 +7,7 @@
// undersized chunks into a neighbour. Strings are UTF-8 in, UTF-8 out.
// Comparison and length are codepoint-based, matching Python str
// semantics.
// Math reference : omnivoice/utils/text.py (1:1 port).
// Math reference: omnivoice/utils/text.py (1:1 port).
#include <set>
#include <string>
+12 -12
View File
@@ -1,5 +1,5 @@
#pragma once
// tokenizer-transformer.h : 8-layer Qwen3-style local-causal transformer
// tokenizer-transformer.h: 8-layer Qwen3-style local-causal transformer
// for the Qwen3-TTS 12Hz tokenizer decoder.
//
// Hidden size 512, head_dim 64, 16 query and 16 KV heads (no GQA), FFN
@@ -166,7 +166,7 @@ static void qwen_tokenizer_transformer_free(QwenTokenizerTransformer * tr) {
// Fill a [T, T] f32 mask with 0 where attention is allowed and -inf
// elsewhere. Storage is row-major with k (key index) on the fast axis :
// dst[q * T + k] is the additive bias for query q attending to key k.
// Causal sliding window : mask[k, q] = 0 if (k <= q AND q - k < window),
// Causal sliding window: mask[k, q] = 0 if (k <= q AND q - k < window),
// else -inf.
static void qwen_build_causal_sliding_mask(int T, int sliding_window, std::vector<float> & dst) {
dst.assign((size_t) T * (size_t) T, -INFINITY);
@@ -188,7 +188,7 @@ static void qwen_build_positions(int T, std::vector<int32_t> & dst) {
}
}
// One transformer layer : attention block then MLP block, both with
// One transformer layer: attention block then MLP block, both with
// pre-RMSNorm, post-LayerScale and residual connection.
static struct ggml_tensor * qwen_transformer_layer_forward(struct ggml_context * ctx,
const QwenTokenizerTransformer * tr,
@@ -202,7 +202,7 @@ static struct ggml_tensor * qwen_transformer_layer_forward(struct ggml_context *
int n_kv = tr->num_kv_heads;
int hd = tr->head_dim;
// Attention block : pre-RMSNorm + project Q/K/V + RoPE + scaled dot product
// Attention block: pre-RMSNorm + project Q/K/V + RoPE + scaled dot product
// + softmax with causal sliding mask + V combine + o_proj.
struct ggml_tensor * ln1 = ggml_rms_norm(ctx, x, tr->rms_norm_eps);
ln1 = ggml_mul(ctx, ln1, layer.input_norm_w);
@@ -220,19 +220,19 @@ static struct ggml_tensor * qwen_transformer_layer_forward(struct ggml_context *
k = ggml_rope_ext(ctx, k, positions, NULL, hd, GGML_ROPE_TYPE_NEOX, 0, tr->rope_theta, 1.0f, 0.0f, 1.0f, 0.0f,
0.0f);
// Permute to head-as-batch layout : [hd, T, n_heads]
// Permute to head-as-batch layout: [hd, T, n_heads]
struct ggml_tensor * q_p = ggml_cont(ctx, ggml_permute(ctx, q, 0, 2, 1, 3));
struct ggml_tensor * k_p = ggml_cont(ctx, ggml_permute(ctx, k, 0, 2, 1, 3));
struct ggml_tensor * v_p = ggml_permute(ctx, v, 1, 2, 0, 3); // [T, n_kv, hd] for V mul_mat
v_p = ggml_cont(ctx, v_p);
// Scores : mul_mat(K, Q) -> [T_k, T_q, n_heads]
// Scores: mul_mat(K, Q) -> [T_k, T_q, n_heads]
struct ggml_tensor * scores = ggml_mul_mat(ctx, k_p, q_p);
float scale = 1.0f / sqrtf((float) hd);
scores = ggml_soft_max_ext(ctx, scores, mask, scale, 0.0f);
// Attention output : mul_mat(V_T, scores) -> [hd, T_q, n_heads]
// Attention output: mul_mat(V_T, scores) -> [hd, T_q, n_heads]
// V_T has T_k as ne[0], hd as ne[1], n_heads as ne[2].
struct ggml_tensor * attn = ggml_mul_mat(ctx, v_p, scores);
@@ -246,7 +246,7 @@ static struct ggml_tensor * qwen_transformer_layer_forward(struct ggml_context *
o = ggml_mul(ctx, o, layer.attn_scale);
x = ggml_add(ctx, x, o);
// MLP block : pre-RMSNorm + SwiGLU + LayerScale + residual.
// MLP block: pre-RMSNorm + SwiGLU + LayerScale + residual.
struct ggml_tensor * ln2 = ggml_rms_norm(ctx, x, tr->rms_norm_eps);
ln2 = ggml_mul(ctx, ln2, layer.post_attn_norm_w);
@@ -263,10 +263,10 @@ static struct ggml_tensor * qwen_transformer_layer_forward(struct ggml_context *
return x;
}
// Full forward pass : input_proj, 8 layers, final norm, output_proj.
// Full forward pass: input_proj, 8 layers, final norm, output_proj.
//
// x : [latent_dim, T] f32
// positions : [T] i32
// positions: [T] i32
// mask : [T, T] f32, additive (-inf where masked)
// returns : [latent_dim, T] f32
static struct ggml_tensor * qwen_tokenizer_transformer_forward(struct ggml_context * ctx,
@@ -276,7 +276,7 @@ static struct ggml_tensor * qwen_tokenizer_transformer_forward(struct ggml_conte
struct ggml_tensor * mask) {
int T = (int) x->ne[1];
// input_proj : [latent_dim, T] -> [hidden, T]
// input_proj: [latent_dim, T] -> [hidden, T]
struct ggml_tensor * h = ggml_mul_mat(ctx, tr->input_proj_w, x);
h = ggml_add(ctx, h, tr->input_proj_b);
@@ -287,7 +287,7 @@ static struct ggml_tensor * qwen_tokenizer_transformer_forward(struct ggml_conte
h = ggml_rms_norm(ctx, h, tr->rms_norm_eps);
h = ggml_mul(ctx, h, tr->norm_w);
// output_proj : [hidden, T] -> [latent_dim, T]
// output_proj: [hidden, T] -> [latent_dim, T]
h = ggml_mul_mat(ctx, tr->output_proj_w, h);
h = ggml_add(ctx, h, tr->output_proj_b);
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
// utf8.h: portable UTF-8 boundary for Windows. Inside the project everything
// is UTF-8 ; this header bridges to the Windows-native UTF-16 APIs at the
// is UTF-8; this header bridges to the Windows-native UTF-16 APIs at the
// three places where the OS forces a recode: argv (CRT decodes from CP_ACP),
// fopen (CRT does too), and any direct Win32 *A call (CreateFileA etc.).
// POSIX is UTF-8 by convention and every helper degrades to a passthrough.
+12 -12
View File
@@ -1,7 +1,7 @@
/* tests/abi-c.c : link-only ABI smoke test for qwen.h.
*
* Compiled in pure C99 with -Wall -Werror -pedantic. The purpose of this
* test is NOT to run a full synthesis (no GGUF loaded, no model required) ;
* test is NOT to run a full synthesis (no GGUF loaded, no model required);
* it is to guarantee at every build that :
*
* 1. qwen.h parses with a C compiler (no <cstdio>, no std::*, no
@@ -46,7 +46,7 @@ static void stub_log(enum qwen_log_level level, const char * msg, void * user_da
int main(void) {
/* Static version string, always reachable. */
const char * version = qwen_version();
printf("qwen ABI probe : %s\n", version);
printf("[Probe] %s\n", version);
/* Default-initialise the public structs from C. */
struct qwen_init_params iparams;
@@ -57,11 +57,11 @@ int main(void) {
/* Sanity-check a few default values, including the abi_version. */
if (params.max_new_tokens != 2048 || params.temperature != 0.9f) {
fprintf(stderr, "ABI probe : default values do not match\n");
fprintf(stderr, "[Probe] default values do not match\n");
return 1;
}
if (iparams.abi_version != QWEN_ABI_VERSION || params.abi_version != QWEN_ABI_VERSION) {
fprintf(stderr, "ABI probe : abi_version not set by qwen_*_default_params\n");
fprintf(stderr, "[Probe] abi_version not set by qwen_*_default_params\n");
return 1;
}
@@ -80,7 +80,7 @@ int main(void) {
* but the linker must resolve every name to satisfy the call. */
struct qwen_context * dummy = qwen_init(NULL);
if (dummy != NULL) {
fprintf(stderr, "ABI probe : qwen_init(NULL) was supposed to return NULL\n");
fprintf(stderr, "[Probe] qwen_init(NULL) was supposed to return NULL\n");
qwen_free(dummy);
return 2;
}
@@ -91,23 +91,23 @@ int main(void) {
* check the first byte to confirm an error was actually recorded. */
const char * err = qwen_last_error();
if (err == NULL || err[0] == '\0') {
fprintf(stderr, "ABI probe : qwen_last_error() empty after a known failure\n");
fprintf(stderr, "[Probe] qwen_last_error() empty after a known failure\n");
return 5;
}
/* The same failure must have surfaced through the log callback at
* ERROR level. */
if (g_log_lines == 0) {
fprintf(stderr, "ABI probe : qwen_log_set callback never invoked\n");
fprintf(stderr, "[Probe] qwen_log_set callback never invoked\n");
return 6;
}
if (g_last_log_level != QWEN_LOG_ERROR) {
fprintf(stderr, "ABI probe : last log level was %d, expected %d\n", (int) g_last_log_level,
fprintf(stderr, "[Probe] last log level was %d, expected %d\n", (int) g_last_log_level,
(int) QWEN_LOG_ERROR);
return 7;
}
printf("qwen ABI probe : qwen_log_set routed %d line(s), last : '%s'\n", g_log_lines, g_last_log_msg);
printf("qwen ABI probe : qwen_last_error reads '%s'\n", err);
printf("[Probe] qwen_log_set routed %d line(s), last: '%s'\n", g_log_lines, g_last_log_msg);
printf("[Probe] qwen_last_error reads '%s'\n", err);
/* abi_version validation : a struct claiming a future ABI must be
* rejected up front, before any allocation. Both paths are filled
@@ -120,14 +120,14 @@ int main(void) {
future_iparams.abi_version = QWEN_ABI_VERSION + 1;
struct qwen_context * rejected = qwen_init(&future_iparams);
if (rejected != NULL) {
fprintf(stderr, "ABI probe : qwen_init accepted a future abi_version\n");
fprintf(stderr, "[Probe] qwen_init accepted a future abi_version\n");
qwen_free(rejected);
return 8;
}
enum qwen_status rc = qwen_synthesize(NULL, &params, &audio);
if (rc != QWEN_STATUS_INVALID_PARAMS) {
fprintf(stderr, "ABI probe : qwen_synthesize(NULL) returned %d, expected %d\n", (int) rc,
fprintf(stderr, "[Probe] qwen_synthesize(NULL) returned %d, expected %d\n", (int) rc,
(int) QWEN_STATUS_INVALID_PARAMS);
return 3;
}
+8 -8
View File
@@ -1,4 +1,4 @@
// quantize.cpp : GGUF requantizer for qwen
// quantize.cpp: GGUF requantizer for qwen
// Reads BF16 GGUF, writes quantized GGUF with mixed-precision K-quant policy.
// Policy mirrors llama-quantize: important tensors (v_proj, down_proj) get
// bumped in S/M variants, embed_tokens always Q6_K, norms promoted to F32.
@@ -84,7 +84,7 @@ static bool is_important_l(const char * name) {
// Tensors accessed via ggml_get_rows (text token embeddings, audio token
// embeddings, codebook lookups). These must use a type the CUDA get_rows
// kernel supports : F32, F16, BF16, Q4_0, Q4_1, Q5_0, Q5_1, Q8_0. K-quants
// kernel supports: F32, F16, BF16, Q4_0, Q4_1, Q5_0, Q5_1, Q8_0. K-quants
// are NOT supported.
//
// In the qwen3-tts naming convention :
@@ -103,7 +103,7 @@ static bool is_embed(const char * name) {
// Should this tensor be quantized at all?
//
// Single source of truth for the quantization policy. Applies to EVERY
// variant (BF16, Q8_0, Q6_K, Q5_K_M, Q4_K_M, ...) : tensors that return
// variant (BF16, Q8_0, Q6_K, Q5_K_M, Q4_K_M, ...): tensors that return
// false here keep their source dtype (F32) regardless of the requested
// type. Conv weights pass through the main loop and fall back to F16 when
// the row width does not divide the variant block size (kernel K=7,3,1,...).
@@ -117,7 +117,7 @@ static bool is_embed(const char * name) {
// tok_enc.vq_*.input_proj linear wrapping the RVQ encode loop
// tok_enc.vq_*.output_proj linear wrapping the RVQ encode loop
// tok_dec.vq_*.output_proj linear wrapping the RVQ decode loop
// Nearest-neighbor lookup is sensitive to per-row quantization noise ;
// Nearest-neighbor lookup is sensitive to per-row quantization noise;
// even BF16 destroys the mantissa enough to mis-select codes and break
// voice cloning. Same philosophy as acestep.cpp keeping VAE-critical
// paths in full precision.
@@ -140,7 +140,7 @@ static bool should_quantize(const char * name, int n_dims, const char * arch) {
if (strstr(name, "null_condition_emb")) {
return false;
}
// Snake activation parameters : stored as per-channel floats, are
// Snake activation parameters: stored as per-channel floats, are
// activation parameters, not weights. The DAC loaders widen them to
// F32 on the backend with a reciprocal transform, no other dtype
// path. Keep them source-dtype in every variant. Both the legacy
@@ -151,9 +151,9 @@ static bool should_quantize(const char * name, int n_dims, const char * arch) {
strstr(name, ".snake1.alpha") || strstr(name, ".snake2.alpha")) {
return false;
}
// RVQ codebooks and the linear projections wrapping them : nearest
// RVQ codebooks and the linear projections wrapping them: nearest
// neighbor lookup is sensitive to per-row quantization noise. Q8_0
// and K-quants break reference audio encoding and tank voice cloning ;
// and K-quants break reference audio encoding and tank voice cloning;
// BF16 already loses enough mantissa to drift codes. Keep at F32 in
// every variant.
if (strstr(name, "tok_enc.vq_") || strstr(name, "tok_dec.vq_")) {
@@ -377,7 +377,7 @@ int main(int argc, char ** argv) {
bool can_convert = (t->type == GGML_TYPE_BF16 || t->type == GGML_TYPE_F16 || t->type == GGML_TYPE_F32);
bool aligned = (t->ne[0] % ggml_blck_size(target) == 0);
// Conv kernels (K=7,3,1,...) cannot fit a block-quant row : fall back
// Conv kernels (K=7,3,1,...) cannot fit a block-quant row: fall back
// to F16. F16 has no block size, 10-bit mantissa beats BF16 (7) and
// Q* effective on these weights, and gf_load_conv on the C++ side
// loads every conv kernel as F16 regardless of source dtype.
+8 -8
View File
@@ -1,13 +1,13 @@
// qwen-tts.cpp : thin CLI wrapper around the qwentts.cpp public ABI.
// qwen-tts.cpp: thin CLI wrapper around the qwentts.cpp public ABI.
// Parses arguments, reads the optional reference WAV plus transcript,
// hands off to qwen_synthesize and writes the resulting waveform as a
// WAV file. All synthesis logic, mode validation and seed resolution
// live behind the qwen_* facade declared in qwen.h.
//
// Talker variants : 0.6B-Base / 0.6B-CustomVoice / 1.7B-Base /
// Talker variants: 0.6B-Base / 0.6B-CustomVoice / 1.7B-Base /
// 1.7B-CustomVoice / 1.7B-VoiceDesign. The decoder path is selected
// from GGUF metadata at qwen_init time. The CLI surface mirrors the
// omnivoice.cpp tooling : kebab-case flags, --format wav16/wav24/wav32,
// omnivoice.cpp tooling: kebab-case flags, --format wav16/wav24/wav32,
// -o '-' streams to stdout, --seed -1 means non deterministic
// (resolved inside qwen_synthesize), the utterance text comes from
// --text or stdin if --text is absent.
@@ -23,7 +23,7 @@
#include <sstream>
#include <string>
// Tokenizer sample rate for the 12 Hz Qwen3-TTS codec : 24 kHz. Used
// Tokenizer sample rate for the 12 Hz Qwen3-TTS codec: 24 kHz. Used
// by audio_read_mono to resample the optional --ref-wav file before
// handing it to the facade. The output sample rate is reported by
// qwen_audio.sample_rate after a successful synthesis.
@@ -178,7 +178,7 @@ static bool parse_args(int argc, char ** argv, Args & a) {
} else if (std::strcmp(arg, "--seed") == 0 && i + 1 < argc) {
a.seed = (int64_t) std::atoll(argv[++i]);
} else if (std::strcmp(arg, "--greedy") == 0) {
// Greedy mode : argmax sampling on both stacks. The sampling
// Greedy mode: argmax sampling on both stacks. The sampling
// fast path in sampling.h uses temperature <= 0 to short
// circuit to argmax, bypassing rep penalty and top-k/p
// truncation, which exactly mirrors the Python reference
@@ -212,7 +212,7 @@ static bool parse_args(int argc, char ** argv, Args & a) {
static int run(const Args & a) {
// Init the facade. The seven mode validations
// (base / custom_voice / voice_design rules) and the BPE tokenizer
// load live inside qwen_init / qwen_synthesize ; the CLI just hands
// load live inside qwen_init / qwen_synthesize; the CLI just hands
// off the two GGUF paths and reports qwen_last_error on failure.
qwen_init_params iparams;
qwen_init_default_params(&iparams);
@@ -265,7 +265,7 @@ static int run(const Args & a) {
ref_n_samples = T_in;
}
// Resolve output WAV format string : wav16 / wav24 / wav32. Default
// Resolve output WAV format string: wav16 / wav24 / wav32. Default
// wav16 mirrors the omnivoice.cpp default.
WavFormat wav_fmt;
if (!audio_parse_format(a.format, wav_fmt)) {
@@ -274,7 +274,7 @@ static int run(const Args & a) {
return 1;
}
// Resolve utterance text : explicit --text wins, otherwise read stdin
// Resolve utterance text: explicit --text wins, otherwise read stdin
// fully. Empty stdin combined with no --text triggers a clean error.
std::string text_buf;
const char * text = a.text;