fix: let a cancel stop the Qwen3-TTS audio decode inside a step - #87
Merged
Merged
Conversation
llama_dart_tts_step checked for a cancel only on entry, so a cancel that landed in a step running the code2wav decode waited out the whole decode: 1.3-1.4 s on CPU and 0.36 s on Metal on an M4 Max. Export llama_dart_tts_eval_callback for mtmd_context_params.cb_eval. Inside a step it splits mtmd graph computes into chunks of about 2.5e9 multiply-adds, ends chunks only after a MUL_MAT, and stops at the next chunk end once the task is cancelled. The step re-checks the cancel after frame generation and audio output and returns CANCELLED. Export llama_dart_tts_set_cancel_flag so a caller can cancel through a byte it owns without sharing the task pointer with the cancelling thread. Refs #86
The prompt step of the long text runs about 216 ms on CPU and the first frame step about 82 ms cold on Metal, close to a quarter of the decode. Waiting for a frame keeps the cancel on the decode step.
Lowering the cancel flag byte after the eval callback broke a decode let the step return OK with the truncated decode's PCM. The first read that sees a cancel now latches it for the rest of the task. The smoke lowers the byte right after the break at the end-of-speech and frame-72 decodes, and fails any step that returns OK after a break.
This was referenced Sep 23, 2026
This was referenced Sep 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
For #86.
llama_dart_tts_stepchecked for a cancel only on entry. The step that fills a 72-frame window and the step that ends speech run the whole code2wav decode in one upstream call, so a cancel landing there waited it out.Change
llama_dart_tts_eval_callback(new export) formtmd_context_params.cb_eval, the one upstream hook on clip's scheduler. Whilellama_dart_tts_stepruns on the calling thread, it ends a scheduler chunk at the firstMUL_MATonce the work since the last chunk end reaches 2.5e9 (multiply-adds forMUL_MAT, elements for other nodes). Once the task is cancelled, the compute stops at the next chunk end. Outside a step it asks for nothing.MUL_MAT. No CPU or Metal fusion continues past one, so uncancelled output stays byte-identical.get_rows, and at 1.7B never reaches the budget; a scheduler break stops only the current split.mtmd_helper_gen_audio_step_genandmtmd_helper_gen_audio_get_outputand returnsCANCELLED. The scheduler reports success after a break.llama_dart_tts_cancelor the flag, cancels the task for good. No step returnsOKafter a break.llama_dart_tts_set_cancel_flag(new export) attaches a caller-owned byte. Once a step reads it as nonzero, the task behaves as afterllama_dart_tts_cancel, even if the byte returns to zero. llamadart's worker isolate is inside the FFI step when the cancel arrives, so the main isolate needs to cancel without the task pointer. It is not a request field:llama_dart_tts_startrejects a smallerstruct_size, which would break older callers.LLAMA_DART_TTS_API_VERSIONstays 1; both exports are additive.tools/validate_exports.pyrequires them. No llama.cpp change.Measurements
M4 Max, Qwen3-TTS-12Hz-1.7B-Base Q4_K_M + mmproj Q8_0, Release,
GGML_OPENMP=OFF, both heavy-work locks held, a fresh process per run, 1-min loadavg 6.27-10.69.beforeis v0.4.1 (a4ee6b9fa7).afteris this branch with the callback installed; half its cancels go throughllama_dart_tts_cancel, half through the flag. Time from the cancel to the return of the step it landed in. Metal decode steps last 359-395 ms, so Metal offsets end at 320 and 340 ms:COMPLETEDCANCELLEDCANCELLEDCANCELLEDCOMPLETEDCANCELLEDCANCELLEDCANCELLEDA cancel waits for the running chunk to end, so the decode's longest chunk caps that wait; a window step then still runs one backbone decode before it returns. Longest chunk in the uncancelled runs below (5 end-of-speech and 2 window decodes per backend): CPU 128.7-148.6 ms and 131.4-135.4 ms, Metal 48.8-49.3 ms and 52.8-53.0 ms. In one CPU end-of-speech decode (1-min loadavg 18.18), the two longest chunks (131.2 and 136.1 ms) each hold 28 nodes, including 2
MUL_MATs (1.0e10 multiply-adds together) and 2IM2COLs.Uncancelled output, fresh processes, 1-min loadavg 10.21-12.35: every branch run (10 with the callback, 4 without) and every v0.4.1 repeat (6) is byte-identical to the first v0.4.1 run of its text and backend (
cmpon raw float32 PCM; CPU and Metal; 27-38 and 120-124 frames). With the callback, the Metal end-of-speech decode takes 363.5-365.8 ms against 359.9-362.8 ms before; the CPU ranges overlap (1314.9-1426.8 ms against 1319.1-1416.1 ms).Tests
tests/tts_eval_test.cpp(new, runs inwrapper-contract): the chunk rule on synthetic tensors; the cancel latch; and, on a real CPUggml_backend_schedgraph ofMUL_MAT,RMS_NORM,MULlayers, chunked output equals unchunked, a cancel stops the scheduler at the chunk end, and an early cancel still runs to the first chunk end.tests/tts_api_test.c: the callback is a no-op outside a step; the flag export rejects a null task.tools/tts_smoke.cpp(local, model-backed): PCM is byte-identical with the callback; frame steps stay whole; no final-decode chunk exceeds a quarter of the decode; a cancel a quarter into the end-of-speech decode (llama_dart_tts_cancel) and into the frame-72 window decode (flag) returnsCANCELLEDfrom that step within a third of the decode; so do flag cancels at both decodes when the byte drops to zero right after the break; no step returnsOKafter a break; flag validation; a raised flag from an ended task does not cancel the next one. Passes 3 of 3 on CPU and on Metal.Mutation proofs, each reverted after its run:
tts_eval_test; smoke: both lowered-flag probes returnOKafter the breakget_outputOKafter a breakstep_genOKafter a breakset_cancel_flagaccepts an idle task / a null flagtts_api_testcrashestts_eval_test; smoke: in-step cancel over its boundtts_eval_test(the smoke stays green)tts_eval_testMUL_MATor views mis-weighedtts_eval_testSmoke rows ran on CPU; the latch, re-check, flag-read, flag-kept, scope, never-break, and any-node rows also ran on Metal, with the same result.
ASan reports nothing across
ctest, the smoke on CPU and Metal, and 12 cancel runs (llama_dart_tts_cancel, flag, and flag lowered after the break, at both decodes, on both backends, each followed by a synthesis). TSan reports nothing across 5 CPU cancel runs and the CPU smoke. CI passes 18 of 18 checks, includingtts_eval_testin bothwrapper-contractlanes.Follow-up in llamadart (not here)
After a wrapper-only release: set
cb_evalincreateMultimodalContextwhen the symbol resolves, attach a per-task flag the main isolate owns, and bump the pin. Without the flag, the cancel still waits for the worker isolate.Not verified
MUL_MAT(MUL_MAT, ADD, MUL_MAT, ADD, GLU), so a chunk end can split one and change output. Only CUDA's graph-uid fast path needs a nonzero uid; scheduler views have uid 0 and fall through to the per-node comparison.ggml_backend_sched_graph_computedoes today.CANCELLEDinstead ofCOMPLETED.