asr: allocate inference graphs with ggml_gallocr, not alloc_ctx_tensors - #276
Open
derekja wants to merge 1 commit into
Open
asr: allocate inference graphs with ggml_gallocr, not alloc_ctx_tensors#276derekja wants to merge 1 commit into
derekja wants to merge 1 commit into
Conversation
Several ASR graphs are built with no_alloc=true and then allocated with
ggml_backend_alloc_ctx_tensors, which allocates every tensor in the context
at once. Intermediate activations for all layers therefore stay resident for
the whole graph instead of being recycled, so peak memory scales with sequence
length at roughly 50x the legitimate cost.
Measured on a 12 GB H100 slice, Qwen3-ASR 0.6B, 32 s of audio with a 3000
character recognition prompt: a single cudaMalloc of 9739 MiB for 886 prompt
steps, i.e. 11.0 MiB per token, where the KV cache for a 0.6B model is about
224 KB per token. It fails as one monolithic allocation rather than as
gradual pressure.
The fix is the graph allocator already used elsewhere in this repository,
including in qwen3_asr/audio_encoder.cpp and hviske_asr/encoder.cpp -- one
file away from two of the sites changed here.
citrinet_asr/runtime.cpp 23.96 -> 0.43 MiB per second of audio (56x)
qwen3_asr/thinker.cpp
PrefillGraph 123.7 -> 31.2 MiB/s (0.6B), 149.3 -> 30.3 (1.7B)
1.657 -> 0.158 MiB per prompt character (10.5x)
PromptClassificationGraph same treatment, single output
Two cases that previously failed to allocate now run: 0.6B at 32 s + 2996
characters (9739 MiB -> 3227 MiB) and 1.7B at 32 s + 1795 characters (8246 MiB
-> 4294 MiB).
Two details that are not simply swapping the allocator:
K/V readback. PrefillGraph hands run() the decoder's K/V state, which is an
intermediate the allocator would recycle. Marking those tensors as outputs is
not sufficient: they may be views, and GGML_TENSOR_FLAG_OUTPUT on a view does
not protect its view_src. Each is copied into a tensor of its own with
ggml_cpy over ggml_dup_tensor and that copy is marked as an output, matching
framework/modules/transformers/qwen_causal_decode_runtime.cpp.
Constant leaves. Both qwen3 graphs uploaded position ids once at build time.
That is safe when every tensor is pinned, but the graph allocator may reuse a
leaf once its last consumer has run, so a second run() on a cached graph would
read stale positions. The upload moves into run().
Not changed: DecodeGraph in the same file keeps alloc_ctx_tensors. Its context
holds a persistent step cache that must retain stable storage across
successive decode steps, and its per-step activations are one token wide, so
it does not contribute to length scaling. hviske_asr/decoder.cpp is left alone
for a similar reason: it builds views onto prefill K/V that separate contexts
consume, which needs stable addresses across graph boundaries.
Output is unchanged: 22 transcript comparisons between the two builds are
byte-identical, covering both models, four audio lengths, four prompt lengths,
the CPU backend, and cached-graph batch runs.
Owner
|
@derekja Thank you for the PR! Using Could you share a few RTF comparisons from before and after the PR? |
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.
Summary
Several ASR inference graphs are built with
no_alloc = trueand then allocated withggml_backend_alloc_ctx_tensors, which allocates every tensor in the context simultaneously. Intermediate activations for all layers stay resident for the whole graph instead of being recycled, so peak memory scales with sequence length at roughly 50× the legitimate cost.This switches the affected graphs to
ggml_gallocr, which this repository already uses elsewhere — including inqwen3_asr/audio_encoder.cppandhviske_asr/encoder.cpp, one file away from two of the sites changed here.Output is unchanged: 22 transcript comparisons between the two builds are byte-identical.
The problem
ggml_init_params params{arena, nullptr, true}; // no_alloc ... graph_ = ggml_new_graph_custom(ctx_.get(), N, false); ggml_build_forward_expand(graph_, output_); buffer_ = ggml_backend_alloc_ctx_tensors(ctx_.get(), backend); // every tensor, no reuseMeasured on a 12 GB H100 slice with Qwen3-ASR 0.6B, 32 s of audio and a 3000-character recognition prompt, this asks for a single
cudaMallocof 9739 MiB for 886 prompt steps:That is 11.0 MiB per token, where the KV cache for a 0.6 B model across 28 layers is about 224 KB per token. It fails as one monolithic allocation rather than as gradual pressure, so it is not a fragmentation issue and more free VRAM does not help proportionally.
Results
Peak GPU, sampled at ~11 ms intervals, fresh process per measurement, three repetitions per point (deterministic to the MiB).
citrinet_asr— encoder, single output, no KV cacheqwen3_asr— audio length, no promptqwen3_asr0.6B — recognition-prompt length, fixed 8 s clipPreviously failing, now working
Two details that are not just swapping the allocator
K/V readback.
PrefillGraphhandsrun()the decoder's K/V state, which is an intermediate the allocator would recycle. Marking those tensors as outputs is not sufficient — they may be views, andGGML_TENSOR_FLAG_OUTPUTon a view does not protect itsview_src;ggml_gallocr_free_nodefreesview_srcon its own flag. Each is copied into a tensor of its own withggml_cpyoverggml_dup_tensor, and the copy is marked as an output. This matches the existing pattern inframework/modules/transformers/qwen_causal_decode_runtime.cpp.Constant leaves. Both
qwen3_asrgraphs uploaded position ids once at build time. That is safe when every tensor is pinned, but the graph allocator may reuse a leaf once its last consumer has run, so a secondrun()on a cached graph would read stale positions. The upload moves intorun(). Anyone converting the remaining sites should audit for this specifically.Deliberately not changed
DecodeGraphinqwen3_asr/thinker.cppkeepsalloc_ctx_tensors. Its context holds a persistent step cache that must retain stable storage across successive decode steps, and its per-step activations are one token wide, so it does not contribute to length scaling. Converting it would mean splitting the cache into its own buffer.hviske_asr/decoder.cpp, for a similar reason:PrefillGraphbuilds views onto prefill K/V that separateDecodeGraph/BeamDecodeGraphcontexts consume, needing stable addresses across graph boundaries. Its memory is also flat with audio length (fixed 30 s padded window), so it does not show the symptom.Correctness
22 comparisons of the CLI
text_outputline, baseline binary vs patched, same clip and flags — all byte-identical:citrinetat 4/8/16/32 sqwen30.6B and 1.7B at 4/8/16/32 sqwen30.6B at 8 s × prompts of 499/898/1795/2996 charshviskeat 4/32 s (unchanged code, both builds)--backend cpu), exercising theuses_host_graph_planpath--batch-audio-dir), exercising graph-cache reuse viamatches()— the path thepositions_change protectsPossibly affected elsewhere
51
.cppfiles build graphs and also callggml_backend_alloc_ctx_tensors. Many of those calls are legitimate — weight contexts and persistent caches — so this is a triage list rather than a bug list. Files that build graphs with nogallocranywhere includevibevoice/decoder.cpp,higgs_audio_tts/ar.cpp,qwen3_tts/talker.cpp,ace_step/condition_encoder.cpp,vevo2/ar.cpp,miotts/causal_lm.cpp, and several single-site runtimes. I have not measured those and make no claim about them here.