engine: true parallel batching of the talker and predictor, per slot codec streams

This commit is contained in:
Pascal
2026-07-20 16:53:36 +02:00
parent fbff3317a4
commit 37ea692be6
14 changed files with 1663 additions and 672 deletions
+178 -126
View File
@@ -1,26 +1,29 @@
#pragma once
// 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.
// growing context to produce the 15 acoustic codes of one audio frame
// for every active slot in one batched pass, KV cached per slot.
//
// Input:
// hidden_bridge [hidden] f32 -- persistent backend tensor holding
// the talker last position hidden
// (post final norm), written on
// device by the talker graph and
// read here as a graph leaf
// c0 -- semantic code sampled from the
// Talker codec_head (codebook 0)
// hidden_bridge [hidden, max_batch] f32 -- persistent backend tensor
// holding the talker last
// position hidden per slot (post
// final norm), written on device
// by the talker graph and read
// here as a graph leaf
// c0[N] -- semantic codes sampled from the
// Talker codec_head (codebook 0),
// one per slot
// Output:
// codes[16] = [c0, c1, ..., c15] -- the full set of codes for one
// frame, ready for decode through
// the codec
// codes[N * 16] -- per slot [c0, c1, ..., c15],
// slot major, ready for decode
// through the codec
//
// 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,
// roughly 90x for the inner loop.
// The predictor cache is local to a single frame and holds one set per
// slot: the prefill writes the first two positions (talker_hidden +
// embed(c0)) of every slot, then 14 batched single-token steps follow.
// Every slot runs the same sub-step sequence every frame, so the batch
// stays in perfect lockstep and the graphs bake positions, kv rows and
// masks at build time.
//
// Architecture mirrors the Talker block, only differences are:
// - 5 layers instead of 28
@@ -28,10 +31,10 @@
// - one private embedding table and one private linear head per
// acoustic codebook (1..15)
//
// Graph metadata lives in caller owned static graphs, one for the T=2
// prefill and one per T=1 step: each graph is built and allocated once
// at load, then replayed directly on the backend with a 4 byte code id
// upload per call, the positions, kv rows, and causal mask baked in.
// Graph metadata lives in caller owned static graphs, one per flavor
// and batch width N (prefill plus one per acoustic step), built lazily
// on the first frame at a given N, then replayed directly on the
// backend with an N * 4 byte code id upload per call.
#include "code-predictor-graph.h"
#include "code-predictor-weights.h"
@@ -51,13 +54,15 @@
#include <vector>
struct CodePredictorOutput {
// Sixteen codes: c0 from the talker plus c1..c15 from the predictor.
// Per slot sixteen codes, slot major: codes[slot * 16 + g] holds c0
// from the talker plus c1..c15 from the predictor.
std::vector<int32_t> codes;
};
// Manual F32 attention chain for the code predictor block. Same shape
// contract as talker_attn_f32: q [hd, T, n_q_heads], k/v [hd, T_full,
// n_kv], output [hd, n_q_heads, T]. Used when use_flash_attn is false.
// contract as talker_attn_f32: q [hd, T, n_q_heads, N], k/v
// [hd, T_full, n_kv, N], output [hd, n_q_heads, T, N]; the mul_mat
// broadcasts over dims 2 and 3. Used when use_flash_attn is false.
static struct ggml_tensor * code_predictor_attn_f32(struct ggml_context * ctx,
struct ggml_tensor * q,
struct ggml_tensor * k,
@@ -76,10 +81,12 @@ static int code_predictor_graph_max_nodes(int n_layers) {
return 48 * n_layers + 64;
}
// 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
// attention reads the fixed [0, n_kv_pad) window with the mask carrying
// neg inf beyond n_past+T. Returns the layer output [hidden, T].
// One batched Qwen3 decoder block, KV cached over sets [0, N). x holds
// the N slots' token columns flattened slot major: column j = n * T + t
// is position t of slot n. K and V for the fresh positions are written
// into each slot's set at the rows carried by kv_rows; the attention
// reads the fixed [0, n_kv_pad) window per set with the mask carrying
// neg inf beyond n_past + T. Returns the layer output [hidden, T * N].
// use_flash_attn and clamp_fp16 follow the same contract as in
// talker-forward.h.
static struct ggml_tensor * code_predictor_layer_forward(struct ggml_context * ctx,
@@ -89,9 +96,10 @@ static struct ggml_tensor * code_predictor_layer_forward(struct ggml_context *
struct ggml_tensor * positions,
struct ggml_tensor * mask,
struct ggml_tensor * kv_rows,
struct ggml_tensor * k_cache,
struct ggml_tensor * v_cache,
struct ggml_tensor * k4,
struct ggml_tensor * v4,
int T,
int N,
int n_kv_pad,
bool use_flash_attn,
bool clamp_fp16,
@@ -100,6 +108,7 @@ static struct ggml_tensor * code_predictor_layer_forward(struct ggml_context *
const int n_kv = cw->num_key_value_heads;
const int hd = cw->head_dim;
const float eps = cw->rms_norm_eps;
const int TN = T * N;
struct ggml_tensor * h = ggml_rms_norm(ctx, x, eps);
h = ggml_mul(ctx, h, layer.input_norm_w);
@@ -108,34 +117,43 @@ static struct ggml_tensor * code_predictor_layer_forward(struct ggml_context *
struct ggml_tensor * k = ggml_mul_mat(ctx, layer.attn.k_proj_w, h);
struct ggml_tensor * v = ggml_mul_mat(ctx, layer.attn.v_proj_w, h);
q = ggml_reshape_3d(ctx, q, hd, n_q_heads, T);
k = ggml_reshape_3d(ctx, k, hd, n_kv, T);
v = ggml_reshape_3d(ctx, v, hd, n_kv, T);
q = ggml_reshape_3d(ctx, q, hd, n_q_heads, TN);
k = ggml_reshape_3d(ctx, k, hd, n_kv, TN);
v = ggml_reshape_3d(ctx, v, hd, n_kv, TN);
q = ggml_rms_norm(ctx, q, eps);
q = ggml_mul(ctx, q, layer.attn.q_norm_w);
k = ggml_rms_norm(ctx, k, eps);
k = ggml_mul(ctx, k, layer.attn.k_norm_w);
// RoPE over the flattened token axis: positions [T * N] repeat the
// same in-frame offsets for every slot.
q = ggml_rope_ext(ctx, q, positions, NULL, hd, GGML_ROPE_TYPE_NEOX, 0, cw->rope_theta, 1.0f, 0.0f, 1.0f, 0.0f,
0.0f);
k = ggml_rope_ext(ctx, k, positions, NULL, hd, GGML_ROPE_TYPE_NEOX, 0, cw->rope_theta, 1.0f, 0.0f, 1.0f, 0.0f,
0.0f);
// Write the fresh positions into the cache via set_rows: positions
// travel as data so every step keeps an identical topology and the
// captured CUDA graph replays without an update.
struct ggml_tensor * k_perm = ggml_cont(ctx, ggml_permute(ctx, k, 0, 2, 1, 3)); // [hd, T, n_kv]
struct ggml_tensor * v_perm = ggml_cont(ctx, ggml_permute(ctx, v, 0, 2, 1, 3));
// Write the fresh positions into each slot's set via set_rows:
// [hd, heads, T*N] unflattens to [hd, heads, T, N], permutes to
// [hd, T, heads, N] and lands at the kv_rows [T, 1, N] destinations,
// broadcast across the n_kv head dim.
struct ggml_tensor * k_perm =
ggml_cont(ctx, ggml_permute(ctx, ggml_reshape_4d(ctx, k, hd, n_kv, T, N), 0, 2, 1, 3));
struct ggml_tensor * v_perm =
ggml_cont(ctx, ggml_permute(ctx, ggml_reshape_4d(ctx, v, hd, n_kv, T, N), 0, 2, 1, 3));
ggml_build_forward_expand(gf, ggml_set_rows(ctx, k_cache, k_perm, kv_rows));
ggml_build_forward_expand(gf, ggml_set_rows(ctx, v_cache, v_perm, kv_rows));
struct ggml_tensor * k_sets = ggml_view_4d(ctx, k4, hd, k4->ne[1], n_kv, N, k4->nb[1], k4->nb[2], k4->nb[3], 0);
struct ggml_tensor * v_sets = ggml_view_4d(ctx, v4, hd, v4->ne[1], n_kv, N, v4->nb[1], v4->nb[2], v4->nb[3], 0);
struct ggml_tensor * k_full = ggml_view_3d(ctx, k_cache, hd, n_kv_pad, n_kv, k_cache->nb[1], k_cache->nb[2], 0);
struct ggml_tensor * v_full = ggml_view_3d(ctx, v_cache, hd, n_kv_pad, n_kv, v_cache->nb[1], v_cache->nb[2], 0);
ggml_build_forward_expand(gf, ggml_set_rows(ctx, k_sets, k_perm, kv_rows));
ggml_build_forward_expand(gf, ggml_set_rows(ctx, v_sets, v_perm, kv_rows));
// Q permute [hd, n_q_heads, T] -> [hd, T, n_q_heads] for flash_attn_ext.
struct ggml_tensor * q_p = ggml_permute(ctx, q, 0, 2, 1, 3);
struct ggml_tensor * k_full = ggml_view_4d(ctx, k4, hd, n_kv_pad, n_kv, N, k4->nb[1], k4->nb[2], k4->nb[3], 0);
struct ggml_tensor * v_full = ggml_view_4d(ctx, v4, hd, n_kv_pad, n_kv, N, v4->nb[1], v4->nb[2], v4->nb[3], 0);
// Q [hd, n_q_heads, T, N] -> [hd, T, n_q_heads, N] for
// flash_attn_ext, taken as a view like the talker path does.
struct ggml_tensor * q_p = ggml_permute(ctx, ggml_reshape_4d(ctx, q, hd, n_q_heads, T, N), 0, 2, 1, 3);
// Clamp V before attention when clamp_fp16 is set, same rationale
// as the talker block: sub Ampere CUDA tensor cores accumulate in
@@ -144,9 +162,7 @@ static struct ggml_tensor * code_predictor_layer_forward(struct ggml_context *
v_full = ggml_clamp(ctx, v_full, -65504.0f, 65504.0f);
}
// Attention: fused flash kernel or manual F32 chain. Matches the
// working acestep qw3lm_build_attn pattern on the fused branch and
// the omnivoice qwen3_attn_f32 helper on the manual one.
// Attention: fused flash kernel or manual F32 chain.
float scale = 1.0f / sqrtf((float) hd);
struct ggml_tensor * attn;
if (use_flash_attn) {
@@ -156,7 +172,9 @@ static struct ggml_tensor * code_predictor_layer_forward(struct ggml_context *
attn = code_predictor_attn_f32(ctx, q_p, k_full, v_full, mask, scale);
}
attn = ggml_reshape_2d(ctx, attn, n_q_heads * hd, T);
// [hd, n_q_heads, T, N] -> [n_q_heads*hd, T*N], flatten heads for
// o_proj, token order matching x.
attn = ggml_reshape_2d(ctx, attn, n_q_heads * hd, TN);
struct ggml_tensor * o = ggml_mul_mat(ctx, layer.attn.o_proj_w, attn);
x = ggml_add(ctx, x, o);
@@ -180,18 +198,20 @@ static struct ggml_tensor * code_predictor_layer_forward(struct ggml_context *
return x;
}
// Build one static predictor graph. A non NULL hidden_bridge selects
// the T=2 prefill flavor reading [talker_hidden, embed(c0)] through
// lm_head[0]; otherwise the graph is the single token step for g_head,
// appending at the fixed cache row g_head + 1. The logits node holds
// the last position only, so every flavor reads back one row at
// offset zero. use_flash_attn / clamp_fp16 apply to every layer.
// Build one static batched predictor graph over sets [0, N). A non
// NULL hidden_bridge selects the T=2 prefill flavor reading
// [talker_hidden, embed(c0)] per slot through lm_head[0]; otherwise
// the graph is the single token step for g_head, appending at the
// fixed cache row g_head + 1. The logits node holds the last position
// of every slot as [Vg, N]. use_flash_attn / clamp_fp16 apply to every
// layer.
static bool code_predictor_graph_build(const CodePredictorWeights * cw,
KVCache * kv,
ggml_backend_t backend,
struct ggml_tensor * embd_table,
struct ggml_tensor * hidden_bridge,
int g_head,
int N,
bool use_flash_attn,
bool clamp_fp16,
CodePredGraph * cp) {
@@ -214,17 +234,18 @@ static bool code_predictor_graph_build(const CodePredictorWeights * cw,
}
struct ggml_context * gctx = cp->ctx;
// Inputs: one code id gathered in graph from embd_table, positions,
// attention mask. The prefill path (T == 2, hidden_bridge non NULL)
// concats the resident talker hidden ahead of embed(c0), both on
// device: the sequence is [talker_hidden, embed(c0)] with zero row
// upload. Steps (T == 1) are pure gathers: the only per step upload
// is 4 bytes of code id.
struct ggml_tensor * ids_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I32, 1);
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, n_kv_pad, T);
struct ggml_tensor * rows_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I64, T);
ggml_set_name(ids_in, "sub_code_id");
// Inputs: one code id per slot gathered in graph from embd_table,
// positions, kv rows and the attention mask. The prefill path
// (T == 2) concats each slot's resident talker hidden ahead of
// embed(c0), both on device: the per slot sequence is
// [talker_hidden, embed(c0)] with zero row upload. Steps (T == 1)
// are pure gathers: the only per step upload is N * 4 bytes of
// code ids.
struct ggml_tensor * ids_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I32, N);
struct ggml_tensor * pos_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I32, T * N);
struct ggml_tensor * mask_in = ggml_new_tensor_4d(gctx, GGML_TYPE_F16, n_kv_pad, T, 1, N);
struct ggml_tensor * rows_in = ggml_new_tensor_3d(gctx, GGML_TYPE_I64, T, 1, N);
ggml_set_name(ids_in, "sub_code_ids");
ggml_set_name(pos_in, "positions");
ggml_set_name(mask_in, "causal_mask");
ggml_set_name(rows_in, "kv_rows");
@@ -240,9 +261,14 @@ static bool code_predictor_graph_build(const CodePredictorWeights * cw,
ggml_set_input(rows_in);
ggml_set_output(rows_in);
struct ggml_tensor * x_in = ggml_get_rows(gctx, embd_table, ids_in);
struct ggml_tensor * x_in = ggml_get_rows(gctx, embd_table, ids_in); // [hidden, N]
if (T == 2) {
x_in = ggml_concat(gctx, hidden_bridge, x_in, 1);
struct ggml_tensor * bridge_cols =
ggml_view_2d(gctx, hidden_bridge, hidden_bridge->ne[0], N, hidden_bridge->nb[1], 0);
struct ggml_tensor * b3 = ggml_reshape_3d(gctx, bridge_cols, hidden_bridge->ne[0], 1, N);
struct ggml_tensor * e3 = ggml_reshape_3d(gctx, x_in, x_in->ne[0], 1, N);
x_in = ggml_concat(gctx, b3, e3, 1); // [in_dim, 2, N]
x_in = ggml_reshape_2d(gctx, x_in, x_in->ne[0], T * N);
}
ggml_set_name(x_in, "sub_input");
@@ -261,16 +287,17 @@ static bool code_predictor_graph_build(const CodePredictorWeights * cw,
for (int l = 0; l < n_layers; l++) {
h = code_predictor_layer_forward(gctx, cw, cw->layers[(size_t) l], h, pos_in, mask_in, rows_in,
kv->k[(size_t) l], kv->v[(size_t) l], T, n_kv_pad, use_flash_attn, clamp_fp16,
gf);
kv->k4[(size_t) l], kv->v4[(size_t) l], T, N, n_kv_pad, use_flash_attn,
clamp_fp16, gf);
}
struct ggml_tensor * h_final = ggml_rms_norm(gctx, h, cw->rms_norm_eps);
h_final = ggml_mul(gctx, h_final, cw->norm_w);
if (T > 1) {
// Last position only: the prefill pays a single lm_head row.
h_final = ggml_cont(
gctx, ggml_view_2d(gctx, h_final, h_final->ne[0], 1, h_final->nb[1], (size_t) (T - 1) * h_final->nb[1]));
// Last position of every slot: columns (T - 1) + n * T, one
// strided view so the prefill pays N lm_head rows.
h_final = ggml_cont(gctx, ggml_view_2d(gctx, h_final, h_final->ne[0], N, (size_t) T * h_final->nb[1],
(size_t) (T - 1) * h_final->nb[1]));
}
struct ggml_tensor * logits = ggml_mul_mat(gctx, cw->lm_head[(size_t) g_head], h_final);
@@ -286,30 +313,36 @@ static bool code_predictor_graph_build(const CodePredictorWeights * cw,
}
{
std::vector<int32_t> pos((size_t) T);
for (int i = 0; i < T; i++) {
pos[(size_t) i] = n_past + i;
std::vector<int32_t> pos((size_t) T * (size_t) N);
for (int n = 0; n < N; n++) {
for (int t = 0; t < T; t++) {
pos[(size_t) n * (size_t) T + (size_t) t] = n_past + t;
}
}
ggml_backend_tensor_set(pos_in, pos.data(), 0, (size_t) T * sizeof(int32_t));
ggml_backend_tensor_set(pos_in, pos.data(), 0, pos.size() * sizeof(int32_t));
std::vector<int64_t> rows((size_t) T);
for (int i = 0; i < T; i++) {
rows[(size_t) i] = (int64_t) (n_past + i);
std::vector<int64_t> rows((size_t) T * (size_t) N);
for (int n = 0; n < N; n++) {
for (int t = 0; t < T; t++) {
rows[(size_t) n * (size_t) T + (size_t) t] = (int64_t) (n_past + t);
}
}
ggml_backend_tensor_set(rows_in, rows.data(), 0, (size_t) T * sizeof(int64_t));
ggml_backend_tensor_set(rows_in, rows.data(), 0, rows.size() * sizeof(int64_t));
}
{
std::vector<ggml_fp16_t> mask((size_t) T * (size_t) n_kv_pad);
std::vector<ggml_fp16_t> mask((size_t) n_kv_pad * (size_t) T * (size_t) N);
const ggml_fp16_t zero = ggml_fp32_to_fp16(0.0f);
const ggml_fp16_t neg_inf = ggml_fp32_to_fp16(-INFINITY);
for (size_t i = 0; i < mask.size(); i++) {
mask[i] = neg_inf;
}
for (int q = 0; q < T; q++) {
const int q_pos = n_past + q;
for (int k = 0; k <= q_pos; k++) {
mask[(size_t) q * (size_t) n_kv_pad + (size_t) k] = zero;
for (int n = 0; n < N; n++) {
for (int q = 0; q < T; q++) {
const int q_pos = n_past + q;
for (int k = 0; k <= q_pos; k++) {
mask[((size_t) n * (size_t) T + (size_t) q) * (size_t) n_kv_pad + (size_t) k] = zero;
}
}
}
ggml_backend_tensor_set(mask_in, mask.data(), 0, mask.size() * sizeof(ggml_fp16_t));
@@ -318,86 +351,105 @@ static bool code_predictor_graph_build(const CodePredictorWeights * cw,
cp->gf = gf;
cp->ids_in = ids_in;
cp->logits = logits;
cp->N = N;
return true;
}
// Replay one static predictor graph: upload the code id, run the graph
// directly on the backend, read the single logits row back.
// Replay one static predictor graph: upload the N code ids, run the
// graph directly on the backend, read the [Vg, N] logits back.
static bool code_predictor_replay(CodePredGraph * cp,
ggml_backend_t backend,
int32_t code_id,
const int32_t * code_ids,
int N,
std::vector<float> * logits_out) {
ggml_backend_tensor_set(cp->ids_in, &code_id, 0, sizeof(int32_t));
ggml_backend_tensor_set(cp->ids_in, code_ids, 0, (size_t) N * sizeof(int32_t));
if (ggml_backend_graph_compute(backend, cp->gf) != GGML_STATUS_SUCCESS) {
fprintf(stderr, "[CodePredictor] FATAL: graph compute failed\n");
return false;
}
logits_out->resize((size_t) cp->logits->ne[0]);
ggml_backend_tensor_get(cp->logits, logits_out->data(), 0, (size_t) cp->logits->ne[0] * sizeof(float));
logits_out->resize((size_t) cp->logits->ne[0] * (size_t) N);
ggml_backend_tensor_get(cp->logits, logits_out->data(), 0, logits_out->size() * sizeof(float));
return true;
}
// Run the predictor for one audio frame over the static graphs. The
// prefill replay consumes the persistent hidden bridge already written
// by the talker graph and the sampled c0; the 14 step replays each
// feed the code sampled just before. Sampling parameters control
// greedy (temperature <= 0) vs stochastic. subseq_base 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.
// Run the predictor for one audio frame over the static graphs, all N
// slots in lockstep. The prefill replay consumes the persistent hidden
// bridge columns already written by the talker graph and the sampled
// c0 of every slot; the 14 step replays each feed the codes sampled
// just before. Per slot sampling parameters control greedy
// (temperature <= 0) vs stochastic; seed and subseq_base index each
// slot's own Philox stream, so per slot outputs match a single
// sequence run bit for bit. subseq_base[i] is the subsequence of slot
// i's c0 sample for this step; its 15 acoustic samples consume
// subseq_base[i] + 1 .. subseq_base[i] + 15. Fills out->codes as
// [N * 16] slot major. dump_dir may be NULL and applies to slot 0.
static bool code_predictor_step(const CodePredictorWeights * cw,
ggml_backend_t backend,
CodePredGraph * prefill_graph,
CodePredGraph * step_graphs,
int c0,
float temperature,
int top_k,
float top_p,
int64_t seed,
int64_t subseq_base,
const int32_t * c0,
int N,
const float * temperature,
const int * top_k,
const float * top_p,
const int64_t * seed,
const int64_t * subseq_base,
const char * dump_dir,
CodePredictorOutput * out) {
const int n_acoustic = cw->num_acoustic_codebooks;
const int n_codes = n_acoustic + 1;
out->codes.assign((size_t) (n_acoustic + 1), 0);
out->codes[0] = c0;
out->codes.assign((size_t) N * (size_t) n_codes, 0);
for (int i = 0; i < N; i++) {
out->codes[(size_t) i * (size_t) n_codes] = c0[i];
}
std::vector<float> logits;
if (!code_predictor_replay(prefill_graph, backend, c0, &logits)) {
std::vector<float> logits;
std::vector<int32_t> ids((size_t) N);
if (!code_predictor_replay(prefill_graph, backend, c0, N, &logits)) {
return false;
}
{
const int V0 = (int) (logits.size() / (size_t) N);
for (int i = 0; i < N; i++) {
float u_g = 0.0f;
int cg = sample_top_k_p(logits.data(), (int) logits.size(), temperature, top_k, top_p, 1.0f, nullptr, 0, seed,
subseq_base + 1, &u_g);
int cg = sample_top_k_p(logits.data() + (size_t) i * (size_t) V0, V0, temperature[i], top_k[i], top_p[i], 1.0f,
nullptr, 0, seed[i], subseq_base[i] + 1, &u_g);
if (cg < 0) {
fprintf(stderr, "[CodePredictor] FATAL: sample returned no candidate at g=0\n");
fprintf(stderr, "[CodePredictor] FATAL: sample returned no candidate at g=0 (slot %d)\n", i);
return false;
}
out->codes[1] = cg;
out->codes[(size_t) i * (size_t) n_codes + 1] = cg;
}
// Decode loop: 14 single-token replays. At step g (g=1..14) we feed
// the id of the code we just sampled, gathered in graph from the
// group's private embedding table, and read lm_head[g].
// Decode loop: 14 batched single-token replays. At step g
// (g=1..14) every slot feeds the id of the code it just sampled,
// gathered in graph from the group's private embedding table, and
// reads lm_head[g].
for (int g = 1; g < n_acoustic; g++) {
if (!code_predictor_replay(&step_graphs[(size_t) (g - 1)], backend, out->codes[(size_t) g], &logits)) {
for (int i = 0; i < N; i++) {
ids[(size_t) i] = out->codes[(size_t) i * (size_t) n_codes + (size_t) g];
}
if (!code_predictor_replay(&step_graphs[(size_t) (g - 1)], backend, ids.data(), N, &logits)) {
return false;
}
float u_g = 0.0f;
int cg = sample_top_k_p(logits.data(), (int) logits.size(), temperature, top_k, top_p, 1.0f, nullptr, 0, seed,
subseq_base + 1 + g, &u_g);
if (cg < 0) {
fprintf(stderr, "[CodePredictor] FATAL: sample returned no candidate at g=%d\n", g);
return false;
const int Vg = (int) (logits.size() / (size_t) N);
for (int i = 0; i < N; i++) {
float u_g = 0.0f;
int cg = sample_top_k_p(logits.data() + (size_t) i * (size_t) Vg, Vg, temperature[i], top_k[i], top_p[i],
1.0f, nullptr, 0, seed[i], subseq_base[i] + 1 + g, &u_g);
if (cg < 0) {
fprintf(stderr, "[CodePredictor] FATAL: sample returned no candidate at g=%d (slot %d)\n", g, i);
return false;
}
out->codes[(size_t) i * (size_t) n_codes + (size_t) (g + 1)] = cg;
}
out->codes[(size_t) (g + 1)] = cg;
}
if (dump_dir) {
DebugDumper d;
debug_init(&d, dump_dir);
std::vector<int32_t> codes32(out->codes.begin(), out->codes.end());
std::vector<int32_t> codes32(out->codes.begin(), out->codes.begin() + n_codes);
int n = (int) codes32.size();
debug_dump_i32_as_f32(&d, "codes-step0", codes32.data(), &n, 1);
}
+10 -7
View File
@@ -1,9 +1,10 @@
#pragma once
// code-predictor-graph.h: one static predictor graph, built once at
// load, allocated once, and replayed with a 4 byte code id upload per
// call. Positions, kv rows, and the causal mask bake in at build time
// because the frame cache layout repeats identically: the prefill
// always writes rows 0..1 and step g always appends at row g + 1.
// code-predictor-graph.h: one static predictor graph per flavor and
// batch width, built lazily, allocated once, and replayed with an N
// code id upload per call. Positions, kv rows, and the causal mask
// bake in at build time because the frame cache layout repeats
// identically for every slot: the prefill always writes rows 0..1 and
// step g always appends at row g + 1.
#include "ggml-alloc.h"
#include "ggml.h"
@@ -12,8 +13,9 @@ struct CodePredGraph {
struct ggml_context * ctx = nullptr;
struct ggml_cgraph * gf = nullptr;
ggml_gallocr_t galloc = nullptr;
struct ggml_tensor * ids_in = nullptr;
struct ggml_tensor * logits = nullptr;
struct ggml_tensor * ids_in = nullptr; // [N] i32, one code id per slot
struct ggml_tensor * logits = nullptr; // [Vg, N] f32
int N = 0; // batch width this build covers
};
static void code_predictor_graph_free(CodePredGraph * cp) {
@@ -28,4 +30,5 @@ static void code_predictor_graph_free(CodePredGraph * cp) {
cp->gf = nullptr;
cp->ids_in = nullptr;
cp->logits = nullptr;
cp->N = 0;
}
+80 -28
View File
@@ -1,16 +1,19 @@
#pragma once
// 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
// Code Predictor, batched over n_sets independent sequences. Kept
// minimal: the cache is sized at init for a fixed max sequence length
// and never reallocates. Reset just rewinds cur_len to 0.
// and set count and never reallocates. Reset just rewinds cur_len.
//
// Layout per layer, both K and V :
// ggml_tensor [hd, max_seq_len, n_kv] f32, contiguous on hd
// This matches the layout the attention block already uses for K, so
// the write path is a ggml_cpy of the freshly RoPE'd K into a view of
// the cache, and the read path is just a view spanning [0, cur_len+T)
// on dim 1. V uses the same layout and gets permuted to [T, n_kv, hd]
// at read time for the value matmul, identical to the prefill path.
// 4D ggml_tensor [hd, max_seq_len, n_kv, n_sets] f32, contiguous on hd
// plus one 3D view [hd, max_seq_len, n_kv] per set for the per-sequence
// prefill path and device side set copies. The batched decode graph
// views the 4D tensor directly with ne3 = N over sets [0, N).
//
// The 3D layout matches what the attention block uses for K, so the
// write path is a set_rows of the freshly RoPE'd K into the set view,
// and the read path is a view spanning the padded causal window on
// dim 1. V uses the same layout.
#include "ggml-alloc.h"
#include "ggml-backend.h"
@@ -26,9 +29,16 @@ struct KVCache {
int n_kv_heads;
int head_dim;
int max_seq_len;
int cur_len;
int n_sets;
// One pair per layer, both tensors live in `buffer` allocated below.
// Write head per set.
std::vector<int> cur_len;
// Per layer 4D tensors, both in `buffer` allocated below.
std::vector<struct ggml_tensor *> k4;
std::vector<struct ggml_tensor *> v4;
// Per set 3D views into the 4D tensors, indexed [set * n_layers + layer].
std::vector<struct ggml_tensor *> k;
std::vector<struct ggml_tensor *> v;
@@ -36,25 +46,38 @@ struct KVCache {
ggml_backend_buffer_t buffer;
};
static struct ggml_tensor * kv_cache_k(const KVCache * kv, int set, int layer) {
return kv->k[(size_t) set * (size_t) kv->n_layers + (size_t) layer];
}
static struct ggml_tensor * kv_cache_v(const KVCache * kv, int set, int layer) {
return kv->v[(size_t) set * (size_t) kv->n_layers + (size_t) layer];
}
// Allocate a fresh KV cache backed by a dedicated buffer on `backend`.
// Tensors are zero initialised (the attention path never reads past
// cur_len so this is mostly cosmetic, but it keeps debug dumps clean).
// Tensors are zero initialised: the padded attention window reads past
// cur_len and the masked tail must see finite values.
static bool kv_cache_init(KVCache * kv,
int n_layers,
int n_kv_heads,
int head_dim,
int max_seq_len,
int n_sets,
ggml_backend_t backend) {
kv->n_layers = n_layers;
kv->n_kv_heads = n_kv_heads;
kv->head_dim = head_dim;
kv->max_seq_len = max_seq_len;
kv->cur_len = 0;
kv->k.assign((size_t) n_layers, NULL);
kv->v.assign((size_t) n_layers, NULL);
kv->n_sets = n_sets;
kv->cur_len.assign((size_t) n_sets, 0);
kv->k4.assign((size_t) n_layers, NULL);
kv->v4.assign((size_t) n_layers, NULL);
kv->k.assign((size_t) n_sets * (size_t) n_layers, NULL);
kv->v.assign((size_t) n_sets * (size_t) n_layers, NULL);
struct ggml_init_params gp = {
ggml_tensor_overhead() * (size_t) (2 * n_layers + 4),
const size_t n_tensors = (size_t) (2 * n_layers) * (size_t) (1 + n_sets) + 4;
struct ggml_init_params gp = {
ggml_tensor_overhead() * n_tensors,
NULL,
true,
};
@@ -65,13 +88,26 @@ static bool kv_cache_init(KVCache * kv,
}
for (int l = 0; l < n_layers; l++) {
kv->k[(size_t) l] = ggml_new_tensor_3d(kv->ctx, GGML_TYPE_F32, head_dim, max_seq_len, n_kv_heads);
kv->v[(size_t) l] = ggml_new_tensor_3d(kv->ctx, GGML_TYPE_F32, head_dim, max_seq_len, n_kv_heads);
kv->k4[(size_t) l] = ggml_new_tensor_4d(kv->ctx, GGML_TYPE_F32, head_dim, max_seq_len, n_kv_heads, n_sets);
kv->v4[(size_t) l] = ggml_new_tensor_4d(kv->ctx, GGML_TYPE_F32, head_dim, max_seq_len, n_kv_heads, n_sets);
char name[64];
snprintf(name, sizeof(name), "kv_k_l%d", l);
ggml_set_name(kv->k[(size_t) l], name);
ggml_set_name(kv->k4[(size_t) l], name);
snprintf(name, sizeof(name), "kv_v_l%d", l);
ggml_set_name(kv->v[(size_t) l], name);
ggml_set_name(kv->v4[(size_t) l], name);
// Per set 3D views, created before the buffer allocation so
// ggml_backend_alloc_ctx_tensors runs its view init on them
// (buffer and data resolve against the owning 4D tensor).
struct ggml_tensor * k4 = kv->k4[(size_t) l];
struct ggml_tensor * v4 = kv->v4[(size_t) l];
for (int s = 0; s < n_sets; s++) {
size_t off = (size_t) s * k4->nb[3];
kv->k[(size_t) s * (size_t) n_layers + (size_t) l] =
ggml_view_3d(kv->ctx, k4, head_dim, max_seq_len, n_kv_heads, k4->nb[1], k4->nb[2], off);
kv->v[(size_t) s * (size_t) n_layers + (size_t) l] =
ggml_view_3d(kv->ctx, v4, head_dim, max_seq_len, n_kv_heads, v4->nb[1], v4->nb[2], off);
}
}
kv->buffer = ggml_backend_alloc_ctx_tensors(kv->ctx, backend);
@@ -85,16 +121,29 @@ static bool kv_cache_init(KVCache * kv,
// Zero-init the buffer so any out of bounds read returns a known value.
ggml_backend_buffer_clear(kv->buffer, 0);
size_t bytes_per_layer = (size_t) head_dim * (size_t) max_seq_len * (size_t) n_kv_heads * sizeof(float);
size_t total_mb = (size_t) (2 * n_layers) * bytes_per_layer / (1024 * 1024);
fprintf(stderr, "[KVCache] Allocated: %d layers, %d KV heads, head_dim %d, max_seq_len %d -> %zu MB\n", n_layers,
n_kv_heads, head_dim, max_seq_len, total_mb);
size_t bytes_per_layer =
(size_t) head_dim * (size_t) max_seq_len * (size_t) n_kv_heads * (size_t) n_sets * sizeof(float);
size_t total_mb = (size_t) (2 * n_layers) * bytes_per_layer / (1024 * 1024);
fprintf(stderr, "[KVCache] Allocated: %d layers, %d KV heads, head_dim %d, max_seq_len %d, %d sets -> %zu MB\n",
n_layers, n_kv_heads, head_dim, max_seq_len, n_sets, total_mb);
return true;
}
// Rewind the cache so the next forward starts a fresh sequence.
static void kv_cache_reset(KVCache * kv) {
kv->cur_len = 0;
// Rewind one set so its next forward starts a fresh sequence.
static void kv_cache_reset(KVCache * kv, int set) {
kv->cur_len[(size_t) set] = 0;
}
// Device side copy of one whole set into another through the
// persistent 3D views. Used by the batch engine to compact the active
// slot range after a retirement: the tail set moves into the freed one
// so the batched decode keeps viewing a consecutive [0, N) span.
static void kv_cache_copy_set(KVCache * kv, int src, int dst) {
for (int l = 0; l < kv->n_layers; l++) {
ggml_backend_tensor_copy(kv_cache_k(kv, src, l), kv_cache_k(kv, dst, l));
ggml_backend_tensor_copy(kv_cache_v(kv, src, l), kv_cache_v(kv, dst, l));
}
kv->cur_len[(size_t) dst] = kv->cur_len[(size_t) src];
}
static void kv_cache_free(KVCache * kv) {
@@ -106,6 +155,9 @@ static void kv_cache_free(KVCache * kv) {
ggml_free(kv->ctx);
kv->ctx = NULL;
}
kv->k4.clear();
kv->v4.clear();
kv->k.clear();
kv->v.clear();
kv->cur_len.clear();
}
+51 -8
View File
@@ -259,7 +259,7 @@ static bool pipeline_codec_stream_ensure(PipelineCodec * pc) {
}
if (!kv_cache_init(&pc->stream_kv, pc->transformer.num_layers, pc->transformer.num_kv_heads,
pc->transformer.head_dim, CODEC_STREAM_RING, pc->backend)) {
pc->transformer.head_dim, CODEC_STREAM_RING, 1, pc->backend)) {
ggml_backend_buffer_free(pc->stream_buf);
pc->stream_buf = NULL;
ggml_free(pc->stream_ctx);
@@ -281,7 +281,7 @@ bool pipeline_codec_stream_reset(PipelineCodec * pc) {
// the zeroed KV ring stays hidden behind the sliding window mask.
ggml_backend_buffer_clear(pc->stream_buf, 0);
ggml_backend_buffer_clear(pc->stream_kv.buffer, 0);
kv_cache_reset(&pc->stream_kv);
kv_cache_reset(&pc->stream_kv, 0);
pc->stream_pos = 0;
return true;
}
@@ -305,14 +305,17 @@ static bool codec_snap_ensure(PipelineCodec * pc, CodecStateSnap * s) {
if (s->ctx) {
return true;
}
// Views alias memory the owning tensors already cover, so the
// walker skips them: the mirror holds one duplicate per real
// tensor and the positional pairing in codec_snap_copy holds.
int n = 0;
for (struct ggml_tensor * t = ggml_get_first_tensor(pc->stream_ctx); t;
t = ggml_get_next_tensor(pc->stream_ctx, t)) {
n++;
n += t->view_src ? 0 : 1;
}
for (struct ggml_tensor * t = ggml_get_first_tensor(pc->stream_kv.ctx); t;
t = ggml_get_next_tensor(pc->stream_kv.ctx, t)) {
n++;
n += t->view_src ? 0 : 1;
}
struct ggml_init_params gp = { ggml_tensor_overhead() * (size_t) n, NULL, true };
s->ctx = ggml_init(gp);
@@ -322,11 +325,15 @@ static bool codec_snap_ensure(PipelineCodec * pc, CodecStateSnap * s) {
}
for (struct ggml_tensor * t = ggml_get_first_tensor(pc->stream_ctx); t;
t = ggml_get_next_tensor(pc->stream_ctx, t)) {
ggml_dup_tensor(s->ctx, t);
if (!t->view_src) {
ggml_dup_tensor(s->ctx, t);
}
}
for (struct ggml_tensor * t = ggml_get_first_tensor(pc->stream_kv.ctx); t;
t = ggml_get_next_tensor(pc->stream_kv.ctx, t)) {
ggml_dup_tensor(s->ctx, t);
if (!t->view_src) {
ggml_dup_tensor(s->ctx, t);
}
}
s->buf = ggml_backend_alloc_ctx_tensors(s->ctx, pc->backend);
if (!s->buf) {
@@ -343,12 +350,20 @@ static bool codec_snap_ensure(PipelineCodec * pc, CodecStateSnap * s) {
static void codec_snap_copy(PipelineCodec * pc, CodecStateSnap * s, bool save) {
struct ggml_tensor * m = ggml_get_first_tensor(s->ctx);
for (struct ggml_tensor * t = ggml_get_first_tensor(pc->stream_ctx); t;
t = ggml_get_next_tensor(pc->stream_ctx, t), m = ggml_get_next_tensor(s->ctx, m)) {
t = ggml_get_next_tensor(pc->stream_ctx, t)) {
if (t->view_src) {
continue;
}
ggml_backend_tensor_copy(save ? t : m, save ? m : t);
m = ggml_get_next_tensor(s->ctx, m);
}
for (struct ggml_tensor * t = ggml_get_first_tensor(pc->stream_kv.ctx); t;
t = ggml_get_next_tensor(pc->stream_kv.ctx, t), m = ggml_get_next_tensor(s->ctx, m)) {
t = ggml_get_next_tensor(pc->stream_kv.ctx, t)) {
if (t->view_src) {
continue;
}
ggml_backend_tensor_copy(save ? t : m, save ? m : t);
m = ggml_get_next_tensor(s->ctx, m);
}
}
@@ -389,6 +404,34 @@ bool pipeline_codec_stream_snapshot(PipelineCodec * pc, uint64_t key) {
return true;
}
bool pipeline_codec_stream_save(PipelineCodec * pc, CodecStateSnap * s) {
if (!pc->stream_ready || !codec_snap_ensure(pc, s)) {
return false;
}
codec_snap_copy(pc, s, true);
s->pos = pc->stream_pos;
return true;
}
bool pipeline_codec_stream_load(PipelineCodec * pc, CodecStateSnap * s) {
if (!pc->stream_ready || !s->ctx) {
return false;
}
codec_snap_copy(pc, s, false);
pc->stream_pos = s->pos;
return true;
}
void pipeline_codec_snap_free(CodecStateSnap * s) {
if (s->buf) {
ggml_backend_buffer_free(s->buf);
}
if (s->ctx) {
ggml_free(s->ctx);
}
*s = {};
}
// Build the static stream graph of chunk width T = 1 << cls: the
// same module chain as the T=1 frame graph, every stream state and
// KV ring tensor shared across classes, inputs and intermediates
+9
View File
@@ -198,6 +198,15 @@ bool pipeline_codec_stream_restore(PipelineCodec * pc, uint64_t key);
// the least recently used slot when all are taken.
bool pipeline_codec_stream_snapshot(PipelineCodec * pc, uint64_t key);
// Caller owned stream state mirror, enabling multiple interleaved
// streamed utterances over the single live state: save parks the live
// state (and position cursor) into s, load restores it. The mirror
// allocates lazily on the first save; key and stamp stay unused. Free
// with pipeline_codec_snap_free.
bool pipeline_codec_stream_save(PipelineCodec * pc, CodecStateSnap * s);
bool pipeline_codec_stream_load(PipelineCodec * pc, CodecStateSnap * s);
void pipeline_codec_snap_free(CodecStateSnap * s);
// Encode a 24 kHz mono waveform into RVQ codes.
// audio : [n_samples] f32 mono 24 kHz. Must be a multiple of
// TOKENIZER_HOP_LENGTH (1920); the caller is expected
+718 -347
View File
File diff suppressed because it is too large Load Diff
+77 -15
View File
@@ -90,6 +90,13 @@ struct PromptCache {
size_t max_prefix_entries;
};
// One set of static predictor graphs for a given batch width: the T=2
// prefill plus one T=1 step per acoustic codebook after the first.
struct CodePredGraphSet {
CodePredGraph prefill;
std::vector<CodePredGraph> steps;
};
struct PipelineTTS {
GGUFModel gguf_talker;
TalkerWeights talker;
@@ -108,6 +115,11 @@ struct PipelineTTS {
std::string model_type;
int num_code_groups;
// Batch capacity: number of KV sets, bridge columns and maximum
// concurrent slots the batch engine drives. 1 keeps the exact
// single sequence layout and behavior.
int max_batch;
CodecSpecials codec_specials;
TextSpecials text_specials;
std::vector<LanguageEntry> languages;
@@ -128,43 +140,47 @@ struct PipelineTTS {
bool use_flash_attn;
bool clamp_fp16;
// 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.
// Persistent KV caches, one set per slot: the talker holds the LM
// contexts, the predictor holds one frame's 16 sub-steps per slot,
// rewritten every frame at baked rows.
KVCache talker_kv;
KVCache code_predictor_kv;
// Hidden bridge: the talker last position hidden stays resident on
// device. The talker graph copies it in, the code predictor prefill
// graph reads it as a leaf, so the AR hot loop never round trips
// the row through the host. [talker_hidden] f32 on `backend`.
// Hidden bridge: the talker last position hidden of every slot
// stays resident on device as one column of [talker_hidden,
// max_batch] f32. The talker graphs copy their columns in, the code
// predictor prefill graph reads them as a leaf, so the AR hot loop
// never round trips the rows through the host.
struct ggml_context * bridge_ctx;
ggml_backend_buffer_t bridge_buf;
struct ggml_tensor * hidden_bridge;
// Persistent graph arena for the talker prefill (T_ctx varies per
// request, rebuilt through the sched). The talker decode and the
// whole predictor run on static graphs instead: the decode keeps
// one graph per attention window class built lazily, the predictor
// one prefill (T=2) plus one per acoustic step built at load, all
// replayed directly on the backend.
// whole predictor run on static batched graphs instead: the decode
// keeps one graph per attention window class built lazily and
// rebuilt when the batch width changes, the predictor one graph
// set (prefill T=2 plus one per acoustic step) per batch width
// built lazily, all replayed directly on the backend.
GraphArena talker_arena;
std::vector<TalkerDecodeGraph> talker_decode_graphs; // one per 256 step window class, lazy
CodePredGraph cp_prefill_graph;
std::vector<CodePredGraph> cp_step_graphs;
std::vector<CodePredGraphSet> cp_graphs; // index N - 1, lazy per batch width
};
// Open the talker GGUF and the codec GGUF, load every module on the
// shared backend. Aborts with a logged error on any missing tensor or
// invalid metadata. use_fa is gated on bp.has_gpu inside the load:
// CPU only runs always use the manual F32 attention chain. clamp_fp16
// is forwarded as is. Caller frees with pipeline_tts_free.
// is forwarded as is. max_batch sizes the KV sets, the bridge columns
// and the maximum concurrent slots (minimum 1). Caller frees with
// pipeline_tts_free.
bool pipeline_tts_load(PipelineTTS * pt,
const char * talker_gguf_path,
const char * codec_gguf_path,
BackendPair bp,
bool use_fa,
bool clamp_fp16);
bool clamp_fp16,
int max_batch);
void pipeline_tts_free(PipelineTTS * pt);
@@ -201,3 +217,49 @@ qt_status pipeline_tts_synthesize(PipelineTTS * pt,
// rate (24000 / TOKENIZER_HOP_LENGTH). Clamps to a
// minimum of one frame.
int pipeline_tts_duration_sec_to_tokens(const PipelineTTS * pt, float duration_sec);
// One synthesis request driven by the batch engine. The caller owns
// params / out for the whole lifetime of the job; status and error
// fill at retirement. error carries the qt_last_error() text captured
// on the thread that ran the engine, so a scheduler on a worker thread
// can replay it into the caller's thread local slot. done is reserved
// for the owner's completion signaling; the engine never touches it.
struct TtsJob {
const struct qt_tts_params * params;
int64_t resolved_seed;
struct qt_audio * out;
qt_status status;
std::string error;
bool done;
};
// Batch engine: drives up to pt->max_batch concurrent synthesis slots
// in lockstep over the batched talker decode and code predictor
// graphs. Slots always occupy KV sets [0, N); a retirement compacts
// the range with one device side set copy so the batched views stay
// consecutive. Single threaded: every call runs on the thread that
// owns the GPU. pipeline_tts_synthesize drives a transient engine
// synchronously for the one request case; the facade scheduler keeps a
// long lived one on its worker thread.
struct TtsEngine;
TtsEngine * tts_engine_new(PipelineTTS * pt, BPETokenizer * tok);
void tts_engine_free(TtsEngine * e);
// Admit one job: prompt assembly, reference handling and talker
// prefill into the next free slot (KV set N). Returns false when the
// admit fails; job->status and job->error are then final and the job
// never occupies a slot. Logs the prefill stall the join imposes on
// the already active slots.
bool tts_engine_admit(TtsEngine * e, TtsJob * job);
// Run one frame for every active slot: batched talker decode for the
// slots past their prefill, per slot c0 sampling, batched code
// predictor, per slot codec streaming, then retirement of finished
// slots (EOS, max_new_tokens, cancel, error) including their buffered
// codec decode or streaming drain. Retired jobs append to *retired
// with status, error and out final.
void tts_engine_step(TtsEngine * e, std::vector<TtsJob *> * retired);
// Number of currently occupied slots.
int tts_engine_active(const TtsEngine * e);
+139 -2
View File
@@ -25,24 +25,49 @@
#include "version.h"
#include <atomic>
#include <condition_variable>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <mutex>
#include <new>
#include <random>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
// Internal definition of the opaque handle. C++ types are fine here
// because nothing in this struct ever crosses the public ABI boundary :
// callers only ever see `struct qt_context *`. PipelineTTS already
// embeds the PipelineCodec, so no separate codec field is needed.
//
// gpu_mu serializes every GPU touching entry: the batch worker holds
// it per engine phase (admit, frame step) so a voice extraction or a
// max_batch == 1 synthesize slips between frames instead of racing the
// backend from another thread.
//
// The scheduler members drive the max_batch > 1 mode: qt_synthesize
// enqueues a TtsJob and blocks on cv_done until the worker retires it.
// The worker owns the long lived TtsEngine, admits queued jobs into
// free slots between frames and steps the batch until both the queue
// and the slots drain.
struct qt_context {
BackendPair bp;
PipelineTTS pt;
BPETokenizer tok;
std::mutex gpu_mu;
int max_batch = 1;
std::thread worker;
std::mutex mu;
std::condition_variable cv_work;
std::condition_variable cv_done;
std::deque<TtsJob *> queue;
bool stop = false;
};
// Thread-local backing store for qt_last_error(). std::string sized once
@@ -193,6 +218,7 @@ void qt_init_default_params(struct qt_init_params * p) {
p->codec_path = nullptr;
p->use_fa = true;
p->clamp_fp16 = false;
p->max_batch = 1;
}
void qt_tts_default_params(struct qt_tts_params * p) {
@@ -236,6 +262,62 @@ int qt_num_codebooks(const struct qt_context * q) {
return q->pt.num_code_groups;
}
// Batch worker: owns the long lived TtsEngine and the GPU hot loop.
// Sleeps until work arrives, then admits queued jobs into free slots
// between frames and steps the batch until both the queue and the
// slots drain. Every GPU touching phase runs under gpu_mu so a voice
// extraction or another entry can slip between frames from its own
// thread. Retired jobs get their done flag under mu and a cv_done
// broadcast so the blocked qt_synthesize callers wake.
static void qt_batch_worker(qt_context * q) {
TtsEngine * e = tts_engine_new(&q->pt, &q->tok);
std::vector<TtsJob *> retired;
std::unique_lock<std::mutex> lk(q->mu);
for (;;) {
q->cv_work.wait(lk, [&] { return q->stop || !q->queue.empty(); });
if (q->stop && q->queue.empty()) {
break;
}
while (!q->queue.empty() || tts_engine_active(e) > 0) {
// Admit up to max_batch: joins happen at frame boundaries,
// each one stalls the active slots for one prefill.
while (!q->queue.empty() && tts_engine_active(e) < q->max_batch) {
TtsJob * j = q->queue.front();
q->queue.pop_front();
lk.unlock();
bool admitted;
{
std::lock_guard<std::mutex> gpu(q->gpu_mu);
admitted = tts_engine_admit(e, j);
}
lk.lock();
if (!admitted) {
j->done = true;
q->cv_done.notify_all();
}
}
if (tts_engine_active(e) == 0) {
break;
}
lk.unlock();
retired.clear();
{
std::lock_guard<std::mutex> gpu(q->gpu_mu);
tts_engine_step(e, &retired);
}
lk.lock();
for (TtsJob * j : retired) {
j->done = true;
}
if (!retired.empty()) {
q->cv_done.notify_all();
}
}
}
lk.unlock();
tts_engine_free(e);
}
struct qt_context * qt_init(const struct qt_init_params * params) {
if (!params || !params->talker_path || !params->codec_path) {
qt_set_error("qt_init: params, talker_path or codec_path is NULL");
@@ -252,10 +334,14 @@ struct qt_context * qt_init(const struct qt_init_params * params) {
qt_log(QT_LOG_INFO, "[Qwen] qwentts.cpp %s", qt_version());
// ABI v3 tail field: zero init from older callers means 1.
const int max_batch = (params->abi_version >= 3 && params->max_batch > 1) ? params->max_batch : 1;
// new qt_context() value-initialises every field: POD aggregates
// (BackendPair, PipelineTTS) are zero-init, std containers in
// BPETokenizer construct empty.
qt_context * q = new qt_context();
q->max_batch = max_batch;
// 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
@@ -269,7 +355,7 @@ struct qt_context * qt_init(const struct qt_init_params * params) {
}
if (!pipeline_tts_load(&q->pt, params->talker_path, params->codec_path, q->bp, params->use_fa,
params->clamp_fp16)) {
params->clamp_fp16, max_batch)) {
qt_throw("qt_init: pipeline_tts_load failed for '%s' / '%s'", params->talker_path, params->codec_path);
}
@@ -292,6 +378,13 @@ struct qt_context * qt_init(const struct qt_init_params * params) {
return nullptr;
}
// Batch mode: one worker thread owns the long lived engine and the
// GPU hot loop; qt_synthesize enqueues and blocks on completion.
if (q->max_batch > 1) {
q->worker = std::thread(qt_batch_worker, q);
qt_log(QT_LOG_INFO, "[Qwen] Batch scheduler started (max_batch=%d)", q->max_batch);
}
return q;
}
@@ -299,6 +392,14 @@ void qt_free(struct qt_context * q) {
if (!q) {
return;
}
if (q->worker.joinable()) {
{
std::lock_guard<std::mutex> lk(q->mu);
q->stop = true;
}
q->cv_work.notify_all();
q->worker.join();
}
pipeline_tts_free(&q->pt);
backend_release(q->bp.backend, q->bp.cpu_backend);
delete q;
@@ -353,6 +454,10 @@ enum qt_status qt_extract_voice_ref(struct qt_context * q,
}
try {
// Serialize against the batch worker and any concurrent
// synthesize: the extraction slips between two engine frames.
std::lock_guard<std::mutex> gpu(q->gpu_mu);
// Lazy residency: the first reference audio request pays the
// weight load once, mirroring the qt_synthesize ref_audio path.
if (!q->pt.spk_enc_loaded) {
@@ -534,7 +639,39 @@ enum qt_status qt_synthesize(struct qt_context * q, const struct qt_tts_params *
// crosses the extern "C" boundary.
try {
const int64_t resolved_seed = qt_resolve_seed(params->seed);
return pipeline_tts_synthesize(&q->pt, &q->tok, params, resolved_seed, out);
if (q->max_batch <= 1) {
// Single sequence mode: run synchronously on the calling
// thread under gpu_mu, so concurrent callers serialize FIFO
// and callbacks fire on their own caller's thread.
std::lock_guard<std::mutex> gpu(q->gpu_mu);
return pipeline_tts_synthesize(&q->pt, &q->tok, params, resolved_seed, out);
}
// Batch mode: enqueue and block until the worker retires the
// job. qt_last_error is thread local, so the engine captured
// the worker side message into job.error at retirement; replay
// it into this caller's slot so the errno style contract holds
// across the thread hop.
TtsJob job;
job.params = params;
job.resolved_seed = resolved_seed;
job.out = out;
job.status = QT_STATUS_OK;
job.done = false;
{
std::lock_guard<std::mutex> lk(q->mu);
q->queue.push_back(&job);
}
q->cv_work.notify_all();
{
std::unique_lock<std::mutex> lk(q->mu);
q->cv_done.wait(lk, [&] { return job.done; });
}
if (job.status != QT_STATUS_OK && !job.error.empty()) {
qt_set_error("%s", job.error.c_str());
}
return job.status;
} catch (const std::exception & e) {
qt_set_error("%s", e.what());
qt_log(QT_LOG_ERROR, "[Qwen] %s", e.what());
+20 -2
View File
@@ -57,7 +57,7 @@ extern "C" {
// git short hash + commit date string returned by qt_version(); for
// binding compat checks, QT_ABI_VERSION is the only number that
// matters.
#define QT_ABI_VERSION 2
#define QT_ABI_VERSION 3
// Returns a static string of the form "<git-hash> (<date>)" identifying
// the exact commit this binary was built from. Safe to call from any
@@ -121,6 +121,19 @@ struct qt_init_params {
const char * codec_path;
bool use_fa;
bool clamp_fp16;
// ABI v3. Maximum number of concurrent synthesis requests batched
// on the GPU. 0 and 1 select the single sequence behavior (zero
// init from older callers keeps the previous semantics); values
// above 1 size the KV cache sets accordingly and start an internal
// worker thread that coalesces concurrent qt_synthesize calls into
// batched decode steps, queueing FIFO beyond max_batch. With
// max_batch > 1 the on_chunk and cancel callbacks of every request
// are invoked from that worker thread, not from the calling
// thread; callbacks must be safe to run there and must not call
// back into the qwen_* API. qt_synthesize itself stays blocking
// and thread safe in both modes.
int max_batch;
};
// Initialise to the standard defaults: both paths NULL (caller must set
@@ -289,7 +302,12 @@ struct qt_tts_params {
// the streaming pipeline: audio chunks emit through on_chunk and
// `out` stays empty on success. on_chunk NULL keeps the buffered
// path. The last chunk on EOS or max_new flushes whatever frames
// remain.
// remain. With qt_init_params.max_batch > 1 the callback runs on
// the internal batch worker thread, not the qt_synthesize caller
// thread: it must be safe there, must not call back into the
// qwen_* API, and a blocking body stalls every batched request, so
// hand the samples to the consumer thread through a queue instead
// of blocking.
qt_audio_chunk_cb on_chunk;
void * on_chunk_user_data;
+20 -13
View File
@@ -1,10 +1,11 @@
#pragma once
// talker-decode-graph.h: one static talker decode graph, built once
// per attention window class (the kv window rounds up in 256 step
// spans) and replayed directly on the backend. Every input re-uploads
// before each replay: positions, kv row, and mask carry the moving
// n_past, the frame code ids and the overlay row carry the previous
// frame, so nothing bakes and the allocator contract holds.
// talker-decode-graph.h: one static batched talker decode graph, built
// per (attention window class, batch width N) and replayed directly on
// the backend. The kv window rounds up in 256 step spans; every input
// re-uploads before each replay: positions, kv rows, and masks carry
// the moving per-slot n_past, the frame code ids and the overlay rows
// carry each slot's previous frame, so nothing bakes and the allocator
// contract holds. The graph always covers KV sets [0, N).
#include "ggml-alloc.h"
#include "ggml.h"
@@ -15,14 +16,17 @@ struct TalkerDecodeGraph {
struct ggml_context * ctx = nullptr;
struct ggml_cgraph * gf = nullptr;
ggml_gallocr_t galloc = nullptr;
struct ggml_tensor * ids_in = nullptr; // [1 + n_acoustic] i32
struct ggml_tensor * overlay = nullptr; // [hidden, 1] f32
struct ggml_tensor * pos_in = nullptr; // [1] i32
struct ggml_tensor * rows_in = nullptr; // [1] i64
struct ggml_tensor * mask_in = nullptr; // [n_kv_pad, 1] f16
struct ggml_tensor * logits = nullptr; // [vocab, 1] f32
std::vector<ggml_fp16_t> mask; // [n_kv_pad] f16
struct ggml_tensor * ids_in = nullptr; // [(1 + n_acoustic) * N] i32, group major: g * N + slot
struct ggml_tensor * overlay = nullptr; // [hidden, N] f32
struct ggml_tensor * pos_in = nullptr; // [N] i32
struct ggml_tensor * rows_in = nullptr; // [1, 1, N] i64
struct ggml_tensor * mask_in = nullptr; // [n_kv_pad, 1, 1, N] f16
struct ggml_tensor * logits = nullptr; // [vocab, N] f32
std::vector<ggml_fp16_t> mask; // [n_kv_pad * N] f16
std::vector<int32_t> pos_data; // [N] host staging
std::vector<int64_t> rows_data; // [N] host staging
int n_kv_pad = 0; // window class width, 0 marks an empty slot
int N = 0; // batch width this build covers
};
static void talker_decode_graph_free(TalkerDecodeGraph * tg) {
@@ -42,5 +46,8 @@ static void talker_decode_graph_free(TalkerDecodeGraph * tg) {
tg->mask_in = nullptr;
tg->logits = nullptr;
tg->mask.clear();
tg->pos_data.clear();
tg->rows_data.clear();
tg->n_kv_pad = 0;
tg->N = 0;
}
+242 -80
View File
@@ -253,16 +253,17 @@ static struct ggml_tensor * talker_layer_forward(struct ggml_context * ctx,
// Prefill core: builds the graph in the caller owned arena, allocates
// through the sched, uploads the raw embedding, runs it and pulls out
// the last position logits. T tokens are appended to the cache
// the last position logits. T tokens are appended to KV set `kv_set`
// starting at n_past. The last position hidden copies in graph into
// the caller owned persistent hidden_bridge tensor the code predictor
// prefill reads on device; the host copy in out->hidden_last fills
// only under read_hidden_host. When n_past == 0 and dump_dir is set,
// the bisect taps fire. use_fa / clamp_fp16 are forwarded as is to
// every layer. The decode hot path runs on the static graphs below
// instead.
// column `slot` of the caller owned persistent hidden_bridge tensor
// [hidden, max_batch] the code predictor prefill reads on device; the
// host copy in out->hidden_last fills only under read_hidden_host.
// When n_past == 0 and dump_dir is set, the bisect taps fire. use_fa /
// clamp_fp16 are forwarded as is to every layer. The decode hot path
// runs on the static batched graphs below instead.
static bool talker_forward_core(const TalkerWeights * tw,
KVCache * kv,
int kv_set,
ggml_backend_sched_t sched,
GraphArena * arena,
struct ggml_tensor * hidden_bridge,
@@ -317,8 +318,9 @@ static bool talker_forward_core(const TalkerWeights * tw,
struct ggml_tensor * h = x_in;
std::vector<struct ggml_tensor *> taps(TALKER_N_BISECT_LAYERS, NULL);
for (int l = 0; l < n_layers; l++) {
h = talker_layer_forward(gctx, tw, tw->layers[(size_t) l], h, pos_in, mask_in, rows_in, kv->k[(size_t) l],
kv->v[(size_t) l], T, n_kv_pad, use_flash_attn, clamp_fp16, gf);
h = talker_layer_forward(gctx, tw, tw->layers[(size_t) l], h, pos_in, mask_in, rows_in,
kv_cache_k(kv, kv_set, l), kv_cache_v(kv, kv_set, l), T, n_kv_pad, use_flash_attn,
clamp_fp16, gf);
if (record_taps && talker_is_bisect_layer(l)) {
for (int i = 0; i < TALKER_N_BISECT_LAYERS; i++) {
if (TALKER_BISECT_LAYERS[i] == l) {
@@ -340,12 +342,14 @@ static bool talker_forward_core(const TalkerWeights * tw,
ggml_set_output(h_final);
}
// Bridge: the last position hidden copies on device into the
// persistent tensor. Constant destination address across steps, so
// the decode graph topology stays replayable.
// Bridge: the last position hidden copies on device into column
// `kv_set` of the persistent [hidden, max_batch] tensor. Constant
// destination address across steps, so the decode graph topology
// stays replayable.
struct ggml_tensor * h_last =
ggml_view_1d(gctx, h_final, hidden, (size_t) (T - 1) * (size_t) hidden * sizeof(float));
struct ggml_tensor * bridge_cpy = ggml_cpy(gctx, h_last, hidden_bridge);
struct ggml_tensor * bridge_col = ggml_view_1d(gctx, hidden_bridge, hidden, (size_t) kv_set * hidden_bridge->nb[1]);
struct ggml_tensor * bridge_cpy = ggml_cpy(gctx, h_last, bridge_col);
// codec_head: [hidden, vocab]. ggml_mul_mat returns [vocab, T].
struct ggml_tensor * logits = ggml_mul_mat(gctx, tw->codec_head_w, h_final);
@@ -455,14 +459,15 @@ static bool talker_forward_core(const TalkerWeights * tw,
// Advance the cache write head. The graph already executed the cpy
// nodes so positions [n_past, n_past + T) are now populated. The
// arena and the sched allocation persist into the next forward.
kv->cur_len = T_full;
kv->cur_len[(size_t) kv_set] = T_full;
return true;
}
// 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.
// Prefill: reset KV set `kv_set` and write T_ctx positions in one
// shot. input_embed is [T, hidden] f32 row-major. dump_dir may be NULL.
static bool talker_forward_prefill(const TalkerWeights * tw,
KVCache * kv,
int kv_set,
ggml_backend_sched_t sched,
GraphArena * arena,
struct ggml_tensor * hidden_bridge,
@@ -472,21 +477,147 @@ static bool talker_forward_prefill(const TalkerWeights * tw,
bool clamp_fp16,
const char * dump_dir,
TalkerForwardOutput * out) {
kv_cache_reset(kv);
kv_cache_reset(kv, kv_set);
if (T > kv->max_seq_len) {
fprintf(stderr, "[TalkerForward] FATAL: prefill T=%d exceeds cache max_seq_len=%d\n", T, kv->max_seq_len);
return false;
}
return talker_forward_core(tw, kv, sched, arena, hidden_bridge, input_embed, T, 0, use_flash_attn, clamp_fp16,
dump_dir != NULL, dump_dir, out);
return talker_forward_core(tw, kv, kv_set, sched, arena, hidden_bridge, input_embed, T, 0, use_flash_attn,
clamp_fp16, dump_dir != NULL, dump_dir, out);
}
// Build one static decode graph for the given attention window. The
// previous frame code ids gather and sum in graph with the overlay
// row on top; the last position hidden copies into hidden_bridge and
// the codec head logits are the single output row. Positions, kv row,
// and mask stay plain inputs re-uploaded before every replay since
// n_past moves each step.
// Build the batched per-layer block over KV sets [0, N), one fresh
// token per set. Projections, norms, RoPE and the MLP run on [*, N]
// with per column math identical to the single sequence layer. The
// attention is per set: fresh K,V write into the 4D cache at the rows
// carried by kv_rows via set_rows, the read views the padded causal
// window with ne3 = N, and the mask kills everything past each set's
// own context. Returns the layer output [hidden, N].
static struct ggml_tensor * talker_layer_forward_batch(struct ggml_context * ctx,
const TalkerWeights * tw,
const TalkerLayer & layer,
struct ggml_tensor * x,
struct ggml_tensor * positions,
struct ggml_tensor * mask,
struct ggml_tensor * kv_rows,
struct ggml_tensor * k4,
struct ggml_tensor * v4,
int N,
int n_kv_pad,
bool use_flash_attn,
bool clamp_fp16,
struct ggml_cgraph * gf) {
const int n_q_heads = tw->num_attention_heads;
const int n_kv = tw->num_key_value_heads;
const int hd = tw->head_dim;
const float eps = tw->rms_norm_eps;
// Pre-norm
struct ggml_tensor * h = ggml_rms_norm(ctx, x, eps);
h = ggml_mul(ctx, h, layer.input_norm_w);
// Q/K/V projections, batched over the N columns
struct ggml_tensor * q = ggml_mul_mat(ctx, layer.attn.q_proj_w, h); // [n_q_heads*hd, N]
struct ggml_tensor * k = ggml_mul_mat(ctx, layer.attn.k_proj_w, h); // [n_kv*hd, N]
struct ggml_tensor * v = ggml_mul_mat(ctx, layer.attn.v_proj_w, h); // [n_kv*hd, N]
q = ggml_reshape_3d(ctx, q, hd, n_q_heads, N); // [hd, n_q_heads, N]
k = ggml_reshape_3d(ctx, k, hd, n_kv, N);
v = ggml_reshape_3d(ctx, v, hd, n_kv, N);
// Per-head QK-norm: RMS over hd, then multiply by [hd] gain.
q = ggml_rms_norm(ctx, q, eps);
q = ggml_mul(ctx, q, layer.attn.q_norm_w);
k = ggml_rms_norm(ctx, k, eps);
k = ggml_mul(ctx, k, layer.attn.k_norm_w);
// RoPE NEOX: positions [N] map to dim 2 of [hd, heads, N], one
// absolute position per set.
q = ggml_rope_ext(ctx, q, positions, NULL, hd, GGML_ROPE_TYPE_NEOX, 0, tw->rope_theta, 1.0f, 0.0f, 1.0f, 0.0f,
0.0f);
k = ggml_rope_ext(ctx, k, positions, NULL, hd, GGML_ROPE_TYPE_NEOX, 0, tw->rope_theta, 1.0f, 0.0f, 1.0f, 0.0f,
0.0f);
// Write the fresh K,V into the 4D cache via set_rows over the N
// consecutive sets: src reshapes [hd, n_kv, N] to [hd, 1, n_kv, N]
// (same layout) and kv_rows [1, 1, N] carries one destination row
// per set, broadcast across the n_kv head dim. The row ids travel
// as data so every step keeps an identical topology and the
// captured CUDA graph replays without an update.
struct ggml_tensor * k_sets = ggml_view_4d(ctx, k4, hd, k4->ne[1], n_kv, N, k4->nb[1], k4->nb[2], k4->nb[3], 0);
struct ggml_tensor * v_sets = ggml_view_4d(ctx, v4, hd, v4->ne[1], n_kv, N, v4->nb[1], v4->nb[2], v4->nb[3], 0);
struct ggml_tensor * k_new = ggml_reshape_4d(ctx, k, hd, 1, n_kv, N);
struct ggml_tensor * v_new = ggml_reshape_4d(ctx, v, hd, 1, n_kv, N);
ggml_build_forward_expand(gf, ggml_set_rows(ctx, k_sets, k_new, kv_rows));
ggml_build_forward_expand(gf, ggml_set_rows(ctx, v_sets, v_new, kv_rows));
// Batched read: [hd, n_kv_pad, n_kv, N] views of the 4D cache. The
// window covers every set's causal context and rounds up so the
// shape stays constant across 256 consecutive decode steps; the
// mask carries neg inf past each set's own context and the cache
// buffer is zero initialized, so the padded tail contributes
// nothing.
struct ggml_tensor * k_batch = ggml_view_4d(ctx, k4, hd, n_kv_pad, n_kv, N, k4->nb[1], k4->nb[2], k4->nb[3], 0);
struct ggml_tensor * v_batch = ggml_view_4d(ctx, v4, hd, n_kv_pad, n_kv, N, v4->nb[1], v4->nb[2], v4->nb[3], 0);
// Q: [hd, n_q_heads, N] -> [hd, 1, n_q_heads, N] (n_batch=1, ne3=N
// for batched flash_attn).
struct ggml_tensor * q4 = ggml_reshape_4d(ctx, q, hd, 1, n_q_heads, N);
// Clamp V before attention, same rationale as the prefill block:
// sub Ampere CUDA tensor cores accumulate in FP16 and a V overflow
// corrupts every subsequent attention.
if (clamp_fp16) {
v_batch = ggml_clamp(ctx, v_batch, -65504.0f, 65504.0f);
}
// Attention: fused flash kernel (set_prec(F32) promotes the
// accumulator) or the manual F32 chain, which broadcasts its
// mul_mat over dims 2 and 3 so the same helper covers 4D.
float scale = 1.0f / sqrtf((float) hd);
struct ggml_tensor * attn;
if (use_flash_attn) {
attn = ggml_flash_attn_ext(ctx, q4, k_batch, v_batch, mask, scale, 0.0f, 0.0f);
ggml_flash_attn_ext_set_prec(attn, GGML_PREC_F32);
} else {
attn = talker_attn_f32(ctx, q4, k_batch, v_batch, mask, scale);
}
// Output [hd, n_q_heads, 1, N] -> [n_q_heads*hd, N], flatten heads
// for o_proj.
attn = ggml_reshape_2d(ctx, attn, n_q_heads * hd, N);
struct ggml_tensor * o = ggml_mul_mat(ctx, layer.attn.o_proj_w, attn);
x = ggml_add(ctx, x, o);
if (clamp_fp16) {
x = ggml_clamp(ctx, x, -65504.0f, 65504.0f);
}
// 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);
struct ggml_tensor * gate = ggml_mul_mat(ctx, layer.mlp.gate_proj_w, h2);
struct ggml_tensor * up = ggml_mul_mat(ctx, layer.mlp.up_proj_w, h2);
gate = ggml_silu(ctx, gate);
struct ggml_tensor * gu = ggml_mul(ctx, gate, up);
struct ggml_tensor * mlp = ggml_mul_mat(ctx, layer.mlp.down_proj_w, gu);
x = ggml_add(ctx, x, mlp);
if (clamp_fp16) {
x = ggml_clamp(ctx, x, -65504.0f, 65504.0f);
}
return x;
}
// Build one static batched decode graph for the given attention window
// and batch width N. Each slot's previous frame code ids gather and
// sum in graph with its overlay row on top; the batch of last position
// hiddens copies into columns [0, N) of hidden_bridge and the codec
// head logits [vocab, N] are the single output. Positions, kv rows,
// and masks stay plain inputs re-uploaded before every replay since
// each slot's n_past moves every step.
static bool talker_decode_graph_build(const TalkerWeights * tw,
KVCache * kv,
ggml_backend_t backend,
@@ -494,6 +625,7 @@ static bool talker_decode_graph_build(const TalkerWeights * tw,
struct ggml_tensor * const * acoustic_embd,
int n_acoustic,
int n_kv_pad,
int N,
bool use_flash_attn,
bool clamp_fp16,
TalkerDecodeGraph * tg) {
@@ -511,26 +643,29 @@ static bool talker_decode_graph_build(const TalkerWeights * tw,
}
struct ggml_context * gctx = tg->ctx;
struct ggml_tensor * pos_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I32, 1);
struct ggml_tensor * mask_in = ggml_new_tensor_2d(gctx, GGML_TYPE_F16, n_kv_pad, 1);
struct ggml_tensor * rows_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I64, 1);
struct ggml_tensor * ids_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I32, 1 + n_acoustic);
struct ggml_tensor * overlay = ggml_new_tensor_2d(gctx, GGML_TYPE_F32, hidden, 1);
struct ggml_tensor * pos_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I32, N);
struct ggml_tensor * mask_in = ggml_new_tensor_4d(gctx, GGML_TYPE_F16, n_kv_pad, 1, 1, N);
struct ggml_tensor * rows_in = ggml_new_tensor_3d(gctx, GGML_TYPE_I64, 1, 1, N);
struct ggml_tensor * ids_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I32, (1 + n_acoustic) * N);
struct ggml_tensor * overlay = ggml_new_tensor_2d(gctx, GGML_TYPE_F32, hidden, N);
ggml_set_name(pos_in, "positions");
ggml_set_name(mask_in, "causal_mask");
ggml_set_name(rows_in, "kv_rows");
ggml_set_name(ids_in, "frame_code_ids");
ggml_set_name(overlay, "overlay_row");
ggml_set_name(overlay, "overlay_rows");
ggml_set_input(pos_in);
ggml_set_input(mask_in);
ggml_set_input(rows_in);
ggml_set_input(ids_in);
ggml_set_input(overlay);
struct ggml_tensor * id0 = ggml_view_1d(gctx, ids_in, 1, 0);
// ids_in is group major: entry g * N + slot. Each group slices a
// contiguous [N] view and gathers its own table, summed across the
// 16 codebooks, plus the per slot overlay row.
struct ggml_tensor * id0 = ggml_view_1d(gctx, ids_in, N, 0);
struct ggml_tensor * x_in = ggml_get_rows(gctx, tw->codec_embedding, id0);
for (int g = 0; g < n_acoustic; g++) {
struct ggml_tensor * idg = ggml_view_1d(gctx, ids_in, 1, (size_t) (g + 1) * sizeof(int32_t));
struct ggml_tensor * idg = ggml_view_1d(gctx, ids_in, N, (size_t) (g + 1) * (size_t) N * sizeof(int32_t));
x_in = ggml_add(gctx, x_in, ggml_get_rows(gctx, acoustic_embd[g], idg));
}
x_in = ggml_add(gctx, x_in, overlay);
@@ -540,16 +675,17 @@ static bool talker_decode_graph_build(const TalkerWeights * tw,
struct ggml_tensor * h = x_in;
for (int l = 0; l < n_layers; l++) {
h = talker_layer_forward(gctx, tw, tw->layers[(size_t) l], h, pos_in, mask_in, rows_in, kv->k[(size_t) l],
kv->v[(size_t) l], 1, n_kv_pad, use_flash_attn, clamp_fp16, gf);
h = talker_layer_forward_batch(gctx, tw, tw->layers[(size_t) l], h, pos_in, mask_in, rows_in,
kv->k4[(size_t) l], kv->v4[(size_t) l], N, n_kv_pad, use_flash_attn, clamp_fp16,
gf);
}
struct ggml_tensor * h_final = ggml_rms_norm(gctx, h, tw->rms_norm_eps);
h_final = ggml_mul(gctx, h_final, tw->norm_w);
ggml_set_name(h_final, "hidden_final");
struct ggml_tensor * h_last = ggml_view_1d(gctx, h_final, hidden, 0);
struct ggml_tensor * bridge_cpy = ggml_cpy(gctx, h_last, hidden_bridge);
struct ggml_tensor * bridge_cols = ggml_view_2d(gctx, hidden_bridge, hidden, N, hidden_bridge->nb[1], 0);
struct ggml_tensor * bridge_cpy = ggml_cpy(gctx, h_final, bridge_cols);
struct ggml_tensor * logits = ggml_mul_mat(gctx, tw->codec_head_w, h_final);
ggml_set_name(logits, "logits");
@@ -571,61 +707,83 @@ static bool talker_decode_graph_build(const TalkerWeights * tw,
tg->rows_in = rows_in;
tg->mask_in = mask_in;
tg->logits = logits;
tg->mask.resize((size_t) n_kv_pad);
tg->mask.resize((size_t) n_kv_pad * (size_t) N);
tg->pos_data.resize((size_t) N);
tg->rows_data.resize((size_t) N);
tg->n_kv_pad = n_kv_pad;
tg->N = N;
return true;
}
// Decode: append one position from the previous frame's codes over the
// static graph of the current window class, built lazily on the first
// step entering the span. frame_ids holds [c0, c1..c15], acoustic_embd
// the 15 group tables owned by the code predictor, overlay the trailing
// text / pad row summed on top. Reads positions [0, kv->cur_len + 1);
// caller ensures kv->cur_len + 1 <= kv->max_seq_len holds by cache
// sizing. read_hidden_host pulls the bridge back for dump paths.
static bool talker_forward_decode(const TalkerWeights * tw,
KVCache * kv,
ggml_backend_t backend,
TalkerDecodeGraph * graphs,
struct ggml_tensor * hidden_bridge,
const int32_t * frame_ids,
struct ggml_tensor * const * acoustic_embd,
int n_acoustic,
const float * overlay,
bool use_flash_attn,
bool clamp_fp16,
bool read_hidden_host,
TalkerForwardOutput * out) {
if (kv->cur_len + 1 > kv->max_seq_len) {
fprintf(stderr, "[TalkerForward] FATAL: decode would overflow cache (%d + 1 > %d)\n", kv->cur_len,
kv->max_seq_len);
return false;
// Batched decode: append one position per KV set over the static graph
// of the current window class and batch width, built lazily on the
// first step entering the (span, N) pair. frame_ids holds the previous
// frame codes of every slot, group major: entry g * N + slot.
// acoustic_embd holds the 15 group tables owned by the code predictor,
// overlays the [hidden, N] trailing text / pad rows summed on top. The
// window covers max over sets of (cur_len + 1); caller ensures every
// cur_len + 1 <= max_seq_len holds by cache sizing. Logits fill
// out->logits_last as [vocab, N] column blocks; read_hidden_host pulls
// the bridge back as [hidden, N] for dump paths.
static bool talker_forward_decode(const TalkerWeights * tw,
KVCache * kv,
ggml_backend_t backend,
std::vector<TalkerDecodeGraph> & graphs,
struct ggml_tensor * hidden_bridge,
const int32_t * frame_ids,
struct ggml_tensor * const * acoustic_embd,
int n_acoustic,
const float * overlays,
int N,
bool use_flash_attn,
bool clamp_fp16,
bool read_hidden_host,
TalkerForwardOutput * out) {
int max_len = 0;
for (int i = 0; i < N; i++) {
const int len = kv->cur_len[(size_t) i] + 1;
if (len > kv->max_seq_len) {
fprintf(stderr, "[TalkerForward] FATAL: decode would overflow cache (%d > %d, set %d)\n", len,
kv->max_seq_len, i);
return false;
}
if (len > max_len) {
max_len = len;
}
}
const int n_past = kv->cur_len;
const int kv_pad_raw = (int) GGML_PAD(n_past + 1, 256);
const int kv_pad_raw = (int) GGML_PAD(max_len, 256);
const int n_kv_pad = kv_pad_raw < kv->max_seq_len ? kv_pad_raw : kv->max_seq_len;
TalkerDecodeGraph * tg = &graphs[(n_kv_pad + 255) / 256 - 1];
if (!tg->ctx && !talker_decode_graph_build(tw, kv, backend, hidden_bridge, acoustic_embd, n_acoustic, n_kv_pad,
TalkerDecodeGraph * tg = &graphs[(size_t) ((n_kv_pad + 255) / 256 - 1)];
if (tg->ctx && tg->N != N) {
talker_decode_graph_free(tg);
}
if (!tg->ctx && !talker_decode_graph_build(tw, kv, backend, hidden_bridge, acoustic_embd, n_acoustic, n_kv_pad, N,
use_flash_attn, clamp_fp16, tg)) {
return false;
}
ggml_backend_tensor_set(tg->ids_in, frame_ids, 0, (size_t) (1 + n_acoustic) * sizeof(int32_t));
ggml_backend_tensor_set(tg->overlay, overlay, 0, (size_t) tw->hidden_size * sizeof(float));
ggml_backend_tensor_set(tg->ids_in, frame_ids, 0, (size_t) (1 + n_acoustic) * (size_t) N * sizeof(int32_t));
ggml_backend_tensor_set(tg->overlay, overlays, 0, (size_t) tw->hidden_size * (size_t) N * sizeof(float));
const int32_t pos = n_past;
ggml_backend_tensor_set(tg->pos_in, &pos, 0, sizeof(int32_t));
const int64_t row = (int64_t) n_past;
ggml_backend_tensor_set(tg->rows_in, &row, 0, sizeof(int64_t));
for (int i = 0; i < N; i++) {
tg->pos_data[(size_t) i] = kv->cur_len[(size_t) i];
tg->rows_data[(size_t) i] = (int64_t) kv->cur_len[(size_t) i];
}
ggml_backend_tensor_set(tg->pos_in, tg->pos_data.data(), 0, (size_t) N * sizeof(int32_t));
ggml_backend_tensor_set(tg->rows_in, tg->rows_data.data(), 0, (size_t) N * sizeof(int64_t));
// Causal mask: keys [0, n_past] carry 0, the padded tail neg inf.
// Causal masks: for slot i keys [0, cur_len[i]] carry 0, the rest
// of the padded window neg inf.
{
std::vector<ggml_fp16_t> & mask = tg->mask;
const ggml_fp16_t zero = ggml_fp32_to_fp16(0.0f);
const ggml_fp16_t neg_inf = ggml_fp32_to_fp16(-INFINITY);
for (size_t i = 0; i < mask.size(); i++) {
mask[i] = (int) i <= n_past ? zero : neg_inf;
for (int i = 0; i < N; i++) {
const int n_past = kv->cur_len[(size_t) i];
for (int j = 0; j < n_kv_pad; j++) {
mask[(size_t) i * (size_t) n_kv_pad + (size_t) j] = j <= n_past ? zero : neg_inf;
}
}
ggml_backend_tensor_set(tg->mask_in, mask.data(), 0, mask.size() * sizeof(ggml_fp16_t));
}
@@ -637,13 +795,17 @@ static bool talker_forward_decode(const TalkerWeights * tw,
out->hidden = tw->hidden_size;
out->vocab = tw->vocab_size;
out->logits_last.assign((size_t) tw->vocab_size, 0.0f);
ggml_backend_tensor_get(tg->logits, out->logits_last.data(), 0, (size_t) tw->vocab_size * sizeof(float));
out->logits_last.assign((size_t) tw->vocab_size * (size_t) N, 0.0f);
ggml_backend_tensor_get(tg->logits, out->logits_last.data(), 0,
(size_t) tw->vocab_size * (size_t) N * sizeof(float));
if (read_hidden_host) {
out->hidden_last.assign((size_t) tw->hidden_size, 0.0f);
ggml_backend_tensor_get(hidden_bridge, out->hidden_last.data(), 0, (size_t) tw->hidden_size * sizeof(float));
out->hidden_last.assign((size_t) tw->hidden_size * (size_t) N, 0.0f);
ggml_backend_tensor_get(hidden_bridge, out->hidden_last.data(), 0,
(size_t) tw->hidden_size * (size_t) N * sizeof(float));
}
kv->cur_len = n_past + 1;
for (int i = 0; i < N; i++) {
kv->cur_len[(size_t) i]++;
}
return true;
}
+2 -2
View File
@@ -393,8 +393,8 @@ static struct ggml_tensor * tok_trans_forward_stream(struct ggml_context *
h = ggml_add(ctx, h, tr->input_proj_b);
for (int l = 0; l < tr->num_layers; l++) {
h = tok_trans_layer_forward_stream(ctx, gf, tr, tr->layers[l], h, positions, mask, kv_rows, kv->k[(size_t) l],
kv->v[(size_t) l], T, ring);
h = tok_trans_layer_forward_stream(ctx, gf, tr, tr->layers[l], h, positions, mask, kv_rows,
kv_cache_k(kv, 0, l), kv_cache_v(kv, 0, l), T, ring);
}
h = ggml_rms_norm(ctx, h, tr->rms_norm_eps);
+73 -23
View File
@@ -26,14 +26,18 @@
#include "audio-io.h"
#include "yyjson.h"
#include <atomic>
#include <cfloat>
#include <cmath>
#include <condition_variable>
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
// One synthesis request parsed from the OAI JSON body.
@@ -95,8 +99,9 @@ struct server_config {
int port = 8080;
};
// Single GPU context : synthesis is serialised FIFO across connections.
static std::mutex g_synth_mutex;
// Concurrency lives behind the ABI: qt_synthesize is thread safe and
// batches concurrent requests when the context was initialised with
// max_batch > 1, so connection threads call the backend directly.
static httplib::Server * g_svr = nullptr;
static void tts_on_signal(int) {
@@ -252,18 +257,18 @@ static void tts_handle_speech(const tts_backend & be, const httplib::Request & h
}
if (req.format == "wav") {
// One-shot : collect the whole utterance, then emit a RIFF file.
// One-shot : collect the whole utterance, then emit a RIFF
// file. The backend call blocks this connection thread; in
// batch mode the chunks arrive from the ABI worker thread while
// this thread waits inside qt_synthesize, so the plain append
// stays single producer and the blocking return synchronizes.
std::vector<float> buf;
tts_sink sink = [&buf](const float * s, int n) {
buf.insert(buf.end(), s, s + n);
return true;
};
std::string synth_err;
int rc;
{
std::lock_guard<std::mutex> lock(g_synth_mutex);
rc = be.synthesize(req, sink, synth_err);
}
int rc = be.synthesize(req, sink, synth_err);
if (rc != 0) {
tts_json_error(res, tts_status_to_http(rc), "server_error",
synth_err.empty() ? "synthesis failed" : synth_err.c_str());
@@ -274,26 +279,71 @@ static void tts_handle_speech(const tts_backend & be, const httplib::Request & h
return;
}
// Streaming : run synthesis inside the chunked provider on the connection
// thread, pushing s16le frames as the codec produces them. A failed
// sink.write means the client disconnected, which aborts generation and
// frees the GPU instead of finishing a stream nobody reads.
res.set_header("Cache-Control", "no-cache");
res.set_header("X-Accel-Buffering", "no");
res.set_chunked_content_provider("audio/pcm", [&be, req](size_t, httplib::DataSink & sink) mutable -> bool {
tts_sink push = [&sink](const float * s, int n) {
// Streaming : the synthesis runs on its own thread pushing s16le
// bytes into a queue; the connection thread drains the queue into
// the chunked sink. The decoupling keeps a slow client from
// stalling the ABI worker (head of line blocking across the whole
// batch), and a client disconnect flips client_gone so the next
// chunk callback aborts generation and frees the GPU instead of
// finishing a stream nobody reads. Backpressure is the utterance
// itself: pending grows at most to the full PCM of one synthesis.
struct stream_state {
std::mutex mu;
std::condition_variable cv;
std::string pending;
bool done = false;
std::atomic<bool> client_gone{ false };
std::thread th;
};
auto st = std::make_shared<stream_state>();
st->th = std::thread([&be, req, st]() {
tts_sink push = [st](const float * s, int n) {
if (st->client_gone.load(std::memory_order_acquire)) {
return false;
}
std::string bytes;
tts_append_s16le(bytes, s, n);
return sink.write(bytes.data(), bytes.size());
std::lock_guard<std::mutex> lk(st->mu);
st->pending += bytes;
st->cv.notify_all();
return true;
};
std::string synth_err;
{
std::lock_guard<std::mutex> lock(g_synth_mutex);
be.synthesize(req, push, synth_err);
}
sink.done();
return true;
be.synthesize(req, push, synth_err);
std::lock_guard<std::mutex> lk(st->mu);
st->done = true;
st->cv.notify_all();
});
res.set_header("Cache-Control", "no-cache");
res.set_header("X-Accel-Buffering", "no");
res.set_chunked_content_provider(
"audio/pcm",
[st](size_t, httplib::DataSink & sink) -> bool {
std::string chunk;
{
std::unique_lock<std::mutex> lk(st->mu);
st->cv.wait(lk, [&] { return st->done || !st->pending.empty(); });
chunk.swap(st->pending);
}
if (!chunk.empty()) {
if (!sink.write(chunk.data(), chunk.size())) {
st->client_gone.store(true, std::memory_order_release);
return false;
}
return true;
}
sink.done();
return true;
},
[st](bool) {
st->client_gone.store(true, std::memory_order_release);
if (st->th.joinable()) {
st->th.join();
}
});
}
// Decode standard base64 (with optional padding) into out. Returns