codec: adaptive chunk width on the streaming decode

The stream graphs come in width classes T in {1, 2, 4, 8} sharing the
state and KV ring tensors, built lazily per class. The decoder ramps
1 -> 2 -> 4 -> 8 so the first frame keeps its latency while the steady
state interleaves the codec 8x less often and the batch amortizes the
kernel count; drain flushes the tail at EOS and the reference priming
runs in max width chunks.
This commit is contained in:
Pascal
2026-07-06 22:43:52 +02:00
parent 206ad06fde
commit 0725d2e53b
4 changed files with 261 additions and 149 deletions
+89 -27
View File
@@ -84,66 +84,128 @@ static inline std::vector<float> codec_chunked_decode(PipelineCodec * pc,
return out; return out;
} }
// Stateful streaming decoder over pipeline_codec_decode_stream: every // Stateful streaming decoder over pipeline_codec_decode_stream with an
// pushed frame decodes immediately through the persistent codec state // adaptive chunk ramp: the first pushed frame decodes immediately for
// and emits its TOKENIZER_HOP_LENGTH samples, no buffering, no left // the lowest first byte latency, then chunks grow 2 -> 4 -> 8 frames so
// context re-decode, no tail to drain at EOS. ICL priming feeds the // the steady state interleaves the codec with the talker 8x less often
// full reference through the same state with the audio discarded, // and the batch amortizes the kernel count. drain flushes the sub chunk
// which matches the upstream reference plus generated decode exactly. // tail at EOS with greedy width classes. ICL priming feeds the full
// reference through the same state in max width chunks with the audio
// discarded.
struct codec_stream_decoder { struct codec_stream_decoder {
int K; int K;
// Set true when push_frame returned false because the on_chunk // Set true when a flush returned false because the on_chunk
// callback requested a cancel. Stays false on decode failures so // callback requested a cancel. Stays false on decode failures so
// the caller can route to QT_STATUS_CANCELLED vs // the caller can route to QT_STATUS_CANCELLED vs
// QT_STATUS_GENERATE_FAILED on a negative return. // QT_STATUS_GENERATE_FAILED on a negative return.
bool cancelled; bool cancelled;
std::vector<float> frame; static const int MAX_CHUNK = 1 << (CODEC_STREAM_CLASSES - 1);
// Reset the persistent codec state to the zero context. Returns int target; // current ramp chunk width
// false when the state allocation fails. int pending_n; // frames accumulated, < target
std::vector<int32_t> pending; // [MAX_CHUNK, K] frame major
std::vector<int32_t> scratch; // [T, K] chunk upload layout
std::vector<float> frame; // [MAX_CHUNK * hop] audio out
// Reset the persistent codec state to the zero context and restart
// the chunk ramp. Returns false when the state allocation fails.
bool init(PipelineCodec * pc, int K_) { bool init(PipelineCodec * pc, int K_) {
K = K_; K = K_;
cancelled = false; cancelled = false;
frame.assign((size_t) TOKENIZER_HOP_LENGTH, 0.0f); target = 1;
pending_n = 0;
pending.assign((size_t) MAX_CHUNK * (size_t) K, 0);
scratch.assign((size_t) MAX_CHUNK * (size_t) K, 0);
frame.assign((size_t) MAX_CHUNK * (size_t) TOKENIZER_HOP_LENGTH, 0.0f);
return pipeline_codec_stream_reset(pc); return pipeline_codec_stream_reset(pc);
} }
// Decode and emit the first T pending frames, then compact the
// remainder to the front. The scratch reorders frame major pending
// rows into the [T, K] chunk layout (T contiguous per codebook).
bool flush_front(PipelineCodec * pc, int T, qt_audio_chunk_cb cb, void * cb_ud) {
for (int k = 0; k < K; k++) {
for (int t = 0; t < T; t++) {
scratch[(size_t) k * (size_t) T + (size_t) t] = pending[(size_t) t * (size_t) K + (size_t) k];
}
}
if (!pipeline_codec_decode_stream(pc, scratch.data(), T, frame.data())) {
return false;
}
if (cb && !cb(frame.data(), T * TOKENIZER_HOP_LENGTH, cb_ud)) {
cancelled = true;
return false;
}
pending_n -= T;
if (pending_n > 0) {
std::memmove(pending.data(), pending.data() + (size_t) T * (size_t) K,
(size_t) pending_n * (size_t) K * sizeof(int32_t));
}
return true;
}
// Prime the codec state with the full ICL reference: every frame // Prime the codec state with the full ICL reference: every frame
// runs through the streaming decode with the audio discarded, so // runs through the streaming decode in max width chunks with the
// the first generated frame sees the reference's exact causal // audio discarded, so the first generated frame sees the
// state. A reference already primed restores its snapshot device // reference's exact causal state. A reference already primed
// to device instead of re-decoding; a fresh one saves its primed // restores its snapshot device to device instead of re-decoding; a
// state into the LRU. ref_kt is K major [K, ref_T]. Call once, // fresh one saves its primed state into the LRU. ref_kt is K major
// after init and before any push_frame. // [K, ref_T]. Call once, after init and before any push_frame.
bool seed_reference(PipelineCodec * pc, const int32_t * ref_kt, int ref_T) { bool seed_reference(PipelineCodec * pc, const int32_t * ref_kt, int ref_T) {
const uint64_t key = pipeline_codec_ref_key(ref_kt, K, ref_T); const uint64_t key = pipeline_codec_ref_key(ref_kt, K, ref_T);
if (pipeline_codec_stream_restore(pc, key)) { if (pipeline_codec_stream_restore(pc, key)) {
return true; return true;
} }
std::vector<int32_t> codes((size_t) K); int t0 = 0;
for (int t = 0; t < ref_T; t++) { while (t0 < ref_T) {
for (int k = 0; k < K; k++) { int T = MAX_CHUNK;
codes[(size_t) k] = ref_kt[(size_t) k * (size_t) ref_T + (size_t) t]; while (T > ref_T - t0) {
T >>= 1;
} }
if (!pipeline_codec_decode_stream(pc, codes.data(), NULL)) { for (int k = 0; k < K; k++) {
for (int t = 0; t < T; t++) {
scratch[(size_t) k * (size_t) T + (size_t) t] =
ref_kt[(size_t) k * (size_t) ref_T + (size_t) (t0 + t)];
}
}
if (!pipeline_codec_decode_stream(pc, scratch.data(), T, NULL)) {
return false; return false;
} }
t0 += T;
} }
return pipeline_codec_stream_snapshot(pc, key); return pipeline_codec_stream_snapshot(pc, key);
} }
// Decode one frame (K int32 codes, one per codebook) and emit its // Accumulate one frame (K int32 codes, one per codebook) and decode
// samples through the callback. Returns false on decode failure or // when the ramp chunk fills. Returns false on decode failure or
// when cb returns false (cancellation). // when cb returns false (cancellation).
bool push_frame(PipelineCodec * pc, const int32_t * frame_codes, qt_audio_chunk_cb cb, void * cb_ud) { bool push_frame(PipelineCodec * pc, const int32_t * frame_codes, qt_audio_chunk_cb cb, void * cb_ud) {
if (!pipeline_codec_decode_stream(pc, frame_codes, frame.data())) { std::memcpy(pending.data() + (size_t) pending_n * (size_t) K, frame_codes, (size_t) K * sizeof(int32_t));
pending_n++;
if (pending_n < target) {
return true;
}
if (!flush_front(pc, target, cb, cb_ud)) {
return false; return false;
} }
if (!cb(frame.data(), TOKENIZER_HOP_LENGTH, cb_ud)) { if (target < MAX_CHUNK) {
cancelled = true; target <<= 1;
}
return true;
}
// Flush the sub chunk tail at EOS through greedy width classes.
bool drain(PipelineCodec * pc, qt_audio_chunk_cb cb, void * cb_ud) {
while (pending_n > 0) {
int T = MAX_CHUNK;
while (T > pending_n) {
T >>= 1;
}
if (!flush_front(pc, T, cb, cb_ud)) {
return false; return false;
} }
}
return true; return true;
} }
}; };
+126 -102
View File
@@ -28,9 +28,9 @@ bool pipeline_codec_load(PipelineCodec * pc, const char * gguf_path, BackendPair
pc->stream_ctx = NULL; pc->stream_ctx = NULL;
pc->stream_buf = NULL; pc->stream_buf = NULL;
pc->stream_pos = 0; pc->stream_pos = 0;
pc->stream_graph_ctx = NULL; for (int i = 0; i < CODEC_STREAM_CLASSES; i++) {
pc->stream_gf = NULL; pc->stream_graphs[i] = {};
pc->stream_galloc = NULL; }
for (int i = 0; i < CODEC_SNAP_SLOTS; i++) { for (int i = 0; i < CODEC_SNAP_SLOTS; i++) {
pc->snaps[i] = {}; pc->snaps[i] = {};
} }
@@ -267,87 +267,6 @@ static bool pipeline_codec_stream_ensure(PipelineCodec * pc) {
return false; return false;
} }
// Build the static T=1 frame graph once: fixed topology, fixed
// tensor addresses, computed directly on the backend every frame.
{
const int max_nodes = 4096;
struct ggml_init_params gp2 = {
ggml_tensor_overhead() * (size_t) max_nodes + ggml_graph_overhead_custom(max_nodes, false),
NULL,
true,
};
pc->stream_graph_ctx = ggml_init(gp2);
if (!pc->stream_graph_ctx) {
qt_log(QT_LOG_ERROR, "[Pipeline] stream graph ggml_init failed");
kv_cache_free(&pc->stream_kv);
ggml_backend_buffer_free(pc->stream_buf);
pc->stream_buf = NULL;
ggml_free(pc->stream_ctx);
pc->stream_ctx = NULL;
return false;
}
struct ggml_context * gctx = pc->stream_graph_ctx;
struct ggml_cgraph * gf = ggml_new_graph_custom(gctx, max_nodes, false);
const int ring = CODEC_STREAM_RING;
const int K = TOKENIZER_NUM_CODEBOOKS;
struct ggml_tensor * codes_in = ggml_new_tensor_2d(gctx, GGML_TYPE_I32, 1, K);
struct ggml_tensor * pos_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I32, 1);
struct ggml_tensor * rows_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I64, 1);
struct ggml_tensor * mask_in = ggml_new_tensor_2d(gctx, GGML_TYPE_F32, ring, 1);
ggml_set_name(codes_in, "codes_in");
ggml_set_name(pos_in, "positions");
ggml_set_name(rows_in, "kv_rows");
ggml_set_name(mask_in, "ring_mask");
ggml_set_input(codes_in);
ggml_set_input(pos_in);
ggml_set_input(rows_in);
ggml_set_input(mask_in);
// Same module chain as pipeline_codec_decode with the stateful
// variants threaded through the persistent stream tensors. The
// quantizer uses the alignment safe variant: no scheduler input
// duplication happens on the direct backend compute path.
struct ggml_tensor * h = quant_decode_stream(gctx, &pc->qdec, codes_in); // [512, 1] C-first
h = ggml_cont(gctx, ggml_transpose(gctx, h)); // [1, 512] T-first
h = qwen_causal_conv1d_stream(gctx, gf, pc->pre_conv_w, pc->pre_conv_b, h, 3, 1, pc->stream_pre_conv);
h = ggml_cont(gctx, ggml_transpose(gctx, h)); // [1024, 1] C-first
h = tok_trans_forward_stream(gctx, gf, &pc->transformer, h, pos_in, mask_in, rows_in, &pc->stream_kv);
h = ggml_cont(gctx, ggml_transpose(gctx, h)); // [1, 1024] T-first
h = upsample_stage_forward_stream(gctx, gf, &pc->upsample, h, &pc->stream_up);
h = dac_decoder_forward_stream(gctx, gf, &pc->dac, h, &pc->stream_dac);
h = ggml_clamp(gctx, h, -1.0f, 1.0f);
ggml_set_name(h, "audio_out");
ggml_set_output(h);
ggml_build_forward_expand(gf, h);
pc->stream_galloc = ggml_gallocr_new(ggml_backend_get_default_buffer_type(pc->backend));
if (!pc->stream_galloc || !ggml_gallocr_alloc_graph(pc->stream_galloc, gf)) {
qt_log(QT_LOG_ERROR, "[Pipeline] stream graph allocation failed");
if (pc->stream_galloc) {
ggml_gallocr_free(pc->stream_galloc);
pc->stream_galloc = NULL;
}
ggml_free(pc->stream_graph_ctx);
pc->stream_graph_ctx = NULL;
kv_cache_free(&pc->stream_kv);
ggml_backend_buffer_free(pc->stream_buf);
pc->stream_buf = NULL;
ggml_free(pc->stream_ctx);
pc->stream_ctx = NULL;
return false;
}
pc->stream_gf = gf;
pc->stream_in_codes = codes_in;
pc->stream_in_pos = pos_in;
pc->stream_in_rows = rows_in;
pc->stream_in_mask = mask_in;
pc->stream_out = h;
}
pc->stream_ready = true; pc->stream_ready = true;
qt_log(QT_LOG_INFO, "[Pipeline] Codec stream state ready: %d conv contexts, KV ring %d", n_state - 4, qt_log(QT_LOG_INFO, "[Pipeline] Codec stream state ready: %d conv contexts, KV ring %d", n_state - 4,
CODEC_STREAM_RING); CODEC_STREAM_RING);
@@ -470,35 +389,135 @@ bool pipeline_codec_stream_snapshot(PipelineCodec * pc, uint64_t key) {
return true; return true;
} }
bool pipeline_codec_decode_stream(PipelineCodec * pc, const int32_t * codes, float * audio_out) { // 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
// sized for T.
static bool codec_stream_graph_ensure(PipelineCodec * pc, int cls) {
CodecStreamGraph * sg = &pc->stream_graphs[cls];
if (sg->ctx) {
return true;
}
const int T = 1 << cls;
const int ring = CODEC_STREAM_RING;
const int K = TOKENIZER_NUM_CODEBOOKS;
// The ring must hold the sliding window plus the whole fresh chunk.
if (pc->transformer.sliding_window + T > ring) {
qt_log(QT_LOG_ERROR, "[Pipeline] sliding window %d plus chunk %d exceeds KV ring %d",
pc->transformer.sliding_window, T, ring);
return false;
}
const int max_nodes = 4096;
struct ggml_init_params gp = {
ggml_tensor_overhead() * (size_t) max_nodes + ggml_graph_overhead_custom(max_nodes, false),
NULL,
true,
};
sg->ctx = ggml_init(gp);
if (!sg->ctx) {
qt_log(QT_LOG_ERROR, "[Pipeline] stream graph ggml_init failed (T=%d)", T);
return false;
}
struct ggml_context * gctx = sg->ctx;
struct ggml_cgraph * gf = ggml_new_graph_custom(gctx, max_nodes, false);
struct ggml_tensor * codes_in = ggml_new_tensor_2d(gctx, GGML_TYPE_I32, T, K);
struct ggml_tensor * pos_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I32, T);
struct ggml_tensor * rows_in = ggml_new_tensor_1d(gctx, GGML_TYPE_I64, T);
struct ggml_tensor * mask_in = ggml_new_tensor_2d(gctx, GGML_TYPE_F32, ring, T);
ggml_set_name(codes_in, "codes_in");
ggml_set_name(pos_in, "positions");
ggml_set_name(rows_in, "kv_rows");
ggml_set_name(mask_in, "ring_mask");
ggml_set_input(codes_in);
ggml_set_input(pos_in);
ggml_set_input(rows_in);
ggml_set_input(mask_in);
// Same module chain as pipeline_codec_decode with the stateful
// variants threaded through the persistent stream tensors. The
// quantizer uses the alignment safe variant: no scheduler input
// duplication happens on the direct backend compute path.
struct ggml_tensor * h = quant_decode_stream(gctx, &pc->qdec, codes_in); // [512, T] C-first
h = ggml_cont(gctx, ggml_transpose(gctx, h)); // [T, 512] T-first
h = qwen_causal_conv1d_stream(gctx, gf, pc->pre_conv_w, pc->pre_conv_b, h, 3, 1, pc->stream_pre_conv);
h = ggml_cont(gctx, ggml_transpose(gctx, h)); // [1024, T] C-first
h = tok_trans_forward_stream(gctx, gf, &pc->transformer, h, pos_in, mask_in, rows_in, &pc->stream_kv);
h = ggml_cont(gctx, ggml_transpose(gctx, h)); // [T, 1024] T-first
h = upsample_stage_forward_stream(gctx, gf, &pc->upsample, h, &pc->stream_up);
h = dac_decoder_forward_stream(gctx, gf, &pc->dac, h, &pc->stream_dac);
h = ggml_clamp(gctx, h, -1.0f, 1.0f);
ggml_set_name(h, "audio_out");
ggml_set_output(h);
ggml_build_forward_expand(gf, h);
sg->galloc = ggml_gallocr_new(ggml_backend_get_default_buffer_type(pc->backend));
if (!sg->galloc || !ggml_gallocr_alloc_graph(sg->galloc, gf)) {
qt_log(QT_LOG_ERROR, "[Pipeline] stream graph allocation failed (T=%d)", T);
if (sg->galloc) {
ggml_gallocr_free(sg->galloc);
}
ggml_free(sg->ctx);
*sg = {};
return false;
}
sg->gf = gf;
sg->codes = codes_in;
sg->pos = pos_in;
sg->rows = rows_in;
sg->mask = mask_in;
sg->out = h;
return true;
}
bool pipeline_codec_decode_stream(PipelineCodec * pc, const int32_t * codes, int T, float * audio_out) {
const int K = TOKENIZER_NUM_CODEBOOKS; const int K = TOKENIZER_NUM_CODEBOOKS;
const int ring = CODEC_STREAM_RING; const int ring = CODEC_STREAM_RING;
ggml_backend_tensor_set(pc->stream_in_codes, codes, 0, (size_t) K * sizeof(int32_t)); int cls = 0;
while ((1 << cls) < T) {
cls++;
}
if (cls >= CODEC_STREAM_CLASSES || (1 << cls) != T) {
qt_log(QT_LOG_ERROR, "[Pipeline] invalid stream chunk width %d", T);
return false;
}
if (!codec_stream_graph_ensure(pc, cls)) {
return false;
}
CodecStreamGraph * sg = &pc->stream_graphs[cls];
int32_t pos = pc->stream_pos; ggml_backend_tensor_set(sg->codes, codes, 0, (size_t) T * (size_t) K * sizeof(int32_t));
ggml_backend_tensor_set(pc->stream_in_pos, &pos, 0, sizeof(int32_t));
int64_t row = (int64_t) (pc->stream_pos % ring); std::vector<int32_t> pos((size_t) T);
ggml_backend_tensor_set(pc->stream_in_rows, &row, 0, sizeof(int64_t)); std::vector<int64_t> rows((size_t) T);
for (int t = 0; t < T; t++) {
pos[(size_t) t] = pc->stream_pos + t;
rows[(size_t) t] = (int64_t) ((pc->stream_pos + t) % ring);
}
ggml_backend_tensor_set(sg->pos, pos.data(), 0, (size_t) T * sizeof(int32_t));
ggml_backend_tensor_set(sg->rows, rows.data(), 0, (size_t) T * sizeof(int64_t));
std::vector<float> mask_buf; std::vector<float> mask_buf;
tok_trans_build_stream_mask(pc->stream_pos, 1, ring, pc->transformer.sliding_window, mask_buf); tok_trans_build_stream_mask(pc->stream_pos, T, ring, pc->transformer.sliding_window, mask_buf);
ggml_backend_tensor_set(pc->stream_in_mask, mask_buf.data(), 0, mask_buf.size() * sizeof(float)); ggml_backend_tensor_set(sg->mask, mask_buf.data(), 0, mask_buf.size() * sizeof(float));
enum ggml_status st = ggml_backend_graph_compute(pc->backend, pc->stream_gf); enum ggml_status st = ggml_backend_graph_compute(pc->backend, sg->gf);
if (st != GGML_STATUS_SUCCESS) { if (st != GGML_STATUS_SUCCESS) {
qt_log(QT_LOG_ERROR, "[Pipeline] stream graph_compute status=%d", (int) st); qt_log(QT_LOG_ERROR, "[Pipeline] stream graph_compute status=%d", (int) st);
return false; return false;
} }
if (audio_out) { if (audio_out) {
ggml_backend_tensor_get(pc->stream_out, audio_out, 0, (size_t) TOKENIZER_HOP_LENGTH * sizeof(float)); ggml_backend_tensor_get(sg->out, audio_out, 0, (size_t) T * (size_t) TOKENIZER_HOP_LENGTH * sizeof(float));
} }
// The state tensors and the graph allocation persist into the next // The state tensors and every class allocation persist into the
// frame. // next chunk.
pc->stream_pos++; pc->stream_pos += T;
return true; return true;
} }
@@ -755,11 +774,16 @@ void pipeline_codec_free(PipelineCodec * pc) {
} }
graph_arena_free(&pc->dec_arena); graph_arena_free(&pc->dec_arena);
if (pc->stream_ready) { if (pc->stream_ready) {
ggml_gallocr_free(pc->stream_galloc); for (int i = 0; i < CODEC_STREAM_CLASSES; i++) {
pc->stream_galloc = NULL; CodecStreamGraph * sg = &pc->stream_graphs[i];
ggml_free(pc->stream_graph_ctx); if (sg->galloc) {
pc->stream_graph_ctx = NULL; ggml_gallocr_free(sg->galloc);
pc->stream_gf = NULL; }
if (sg->ctx) {
ggml_free(sg->ctx);
}
*sg = {};
}
kv_cache_free(&pc->stream_kv); kv_cache_free(&pc->stream_kv);
ggml_backend_buffer_free(pc->stream_buf); ggml_backend_buffer_free(pc->stream_buf);
pc->stream_buf = NULL; pc->stream_buf = NULL;
+27 -9
View File
@@ -49,6 +49,23 @@
// Primed stream state snapshots kept per reference, LRU evicted. // Primed stream state snapshots kept per reference, LRU evicted.
static const int CODEC_SNAP_SLOTS = 8; static const int CODEC_SNAP_SLOTS = 8;
// Chunk width classes of the streaming decode: T in {1, 2, 4, 8}.
static const int CODEC_STREAM_CLASSES = 4;
// One static stream graph of chunk width T: built once, allocated
// once, replayed with per chunk uploads of codes, positions, ring
// rows, and the sliding window mask.
struct CodecStreamGraph {
struct ggml_context * ctx = nullptr;
struct ggml_cgraph * gf = nullptr;
ggml_gallocr_t galloc = nullptr;
struct ggml_tensor * codes = nullptr;
struct ggml_tensor * pos = nullptr;
struct ggml_tensor * rows = nullptr;
struct ggml_tensor * mask = nullptr;
struct ggml_tensor * out = nullptr;
};
// One primed stream state snapshot: a mirror of every conv context and // One primed stream state snapshot: a mirror of every conv context and
// KV ring tensor in a single backend buffer, plus the host position // KV ring tensor in a single backend buffer, plus the host position
// cursor. key is the content hash of the reference codes, stamp orders // cursor. key is the content hash of the reference codes, stamp orders
@@ -112,14 +129,11 @@ struct PipelineCodec {
// constant, so the graph builds and allocates once and every frame // constant, so the graph builds and allocates once and every frame
// is input uploads + one backend compute + one readback. The single // is input uploads + one backend compute + one readback. The single
// backend runs the whole graph, no scheduler involved. // backend runs the whole graph, no scheduler involved.
struct ggml_context * stream_graph_ctx; // Static stream graphs, one per chunk width class (T = 1 << cls).
struct ggml_cgraph * stream_gf; // Every class shares the stream state and KV ring tensors above;
ggml_gallocr_t stream_galloc; // only the input shapes and the intermediates differ. Classes
struct ggml_tensor * stream_in_codes; // build lazily on their first decode.
struct ggml_tensor * stream_in_pos; CodecStreamGraph stream_graphs[CODEC_STREAM_CLASSES];
struct ggml_tensor * stream_in_rows;
struct ggml_tensor * stream_in_mask;
struct ggml_tensor * stream_out;
// Snapshot LRU over the stream state: after an ICL reference // Snapshot LRU over the stream state: after an ICL reference
// priming the conv contexts, KV ring, and position copy device to // priming the conv contexts, KV ring, and position copy device to
@@ -163,7 +177,11 @@ bool pipeline_codec_stream_reset(PipelineCodec * pc);
// as a side effect. When audio_out is non NULL the frame's // as a side effect. When audio_out is non NULL the frame's
// TOKENIZER_HOP_LENGTH samples copy into it; a NULL audio_out primes // TOKENIZER_HOP_LENGTH samples copy into it; a NULL audio_out primes
// the state without a readback (ICL reference priming). // the state without a readback (ICL reference priming).
bool pipeline_codec_decode_stream(PipelineCodec * pc, const int32_t * codes, float * audio_out); // Decode one chunk of T frames (T in {1, 2, 4, 8}) through the
// persistent stream state. codes is [T, K] with T contiguous per
// codebook; audio_out receives T * TOKENIZER_HOP_LENGTH samples, NULL
// discards the audio (reference priming).
bool pipeline_codec_decode_stream(PipelineCodec * pc, const int32_t * codes, int T, float * audio_out);
// Content hash of an ICL reference, the snapshot LRU key. // Content hash of an ICL reference, the snapshot LRU key.
// codes: flat int32, [K, T] row-major. // codes: flat int32, [K, T] row-major.
+11 -3
View File
@@ -833,10 +833,18 @@ qt_status pipeline_tts_synthesize(PipelineTTS * pt,
debug_dump_i32_as_f32(&d, "codes-full", flat.data(), shape, 2); debug_dump_i32_as_f32(&d, "codes-full", flat.data(), shape, 2);
} }
// Streaming tail: nothing to drain, every frame already emitted at // Streaming tail: drain the sub chunk remainder of the ramp, then
// generation time through the stateful decoder. The buffered output // finish with an empty buffered output.
// stays empty in this branch.
if (streaming) { if (streaming) {
if (!stream.drain(&pt->codec, params->on_chunk, params->on_chunk_user_data)) {
if (stream.cancelled) {
qt_log(QT_LOG_INFO, "[Pipeline] on_chunk callback aborted the synthesis");
return QT_STATUS_CANCELLED;
}
qt_set_error("pipeline_tts_synthesize: streaming codec drain failed");
qt_log(QT_LOG_ERROR, "[Pipeline] streaming codec drain failed");
return QT_STATUS_GENERATE_FAILED;
}
out->samples = NULL; out->samples = NULL;
out->n_samples = 0; out->n_samples = 0;
out->sample_rate = TOKENIZER_SAMPLE_RATE; out->sample_rate = TOKENIZER_SAMPLE_RATE;