Skip to content

fix: bound the streaming mel buffer to a sliding window (fixes #63, the streaming RSS leak) - #69

Merged
mudler merged 1 commit into
mudler:masterfrom
simobenziane:fix/streaming-mel-buffer-window
Sep 26, 2026
Merged

mudler merged 1 commit into
mudler:masterfrom
simobenziane:fix/streaming-mel-buffer-window

Conversation

@simobenziane

Copy link
Copy Markdown
Contributor

Fixes #63.

Root cause (two sentences): parakeet_capi_stream_feed's streaming session keeps every mel frame from stream_begin onward in mel_buf and rebuilds that whole, ever-growing array on every feed call, even though the decoder (feed_available's window()) never reads a frame older than mel_buffer_idx - pre_encode_cache_size() frames back — so those old frames are provably dead the moment the decoder consumes past them. Because each rebuild requests a uniquely, ever-larger allocation for the life of the stream, the allocator can never reuse an earlier call's freed (now permanently too-small) block, so process RSS grows with the cumulative history of buffer sizes rather than the small, bounded set of frames actually still needed — explaining why neither stream_free nor a fresh stream_begin reclaims it.

The fix: track the absolute frame index of mel_buf's first column (mel_buf_origin) and, on every append, drop the prefix strictly before mel_buffer_idx - pre_encode_cache_size() — the exact bound window() already relies on — so mel_buf stays bounded to O(pre_encode_cache_size + chunk_size) instead of O(stream length). No API change, no behavioural change: window()'s absolute-index reads are translated into mel_buf's new origin-relative local columns, and the surviving frames are carried forward byte-for-byte. This is the same bounded-sliding-window discipline the per-layer conv/attention caches in StreamingEncoder already follow; the mel buffer was the one accumulator that did not.

Measurement (CPU + Accelerate build, realtime_eou_120m-v1 q8, 120 s of silence fed in 20 ms chunks, RSS sampled at 30/60/90/120 s of audio fed): growth over the last 60 s drops from 178 MB to 0.5 MB; the same run's wall time drops from ~14 minutes to ~13 seconds, since the unbounded rebuild was O(T) per call, O(T²) total. A LibriSpeech utterance's transcript is byte-identical before and after (sha256 d7a620231a849b5a531ab103a8aaac9ade7d088dc05af7a77fc0633d0d797b27).

Built and checked on macOS arm64 (CPU, -DPARAKEET_SHARED=ON -DPARAKEET_GGML_METAL=OFF), zero warnings.

, the streaming RSS leak)

Root cause: parakeet_capi_stream_feed's streaming session keeps every mel frame from
stream_begin onward in mel_buf and rebuilds that whole, ever-growing array on every feed call,
even though the decoder (feed_available's window()) never reads a frame older than
mel_buffer_idx - pre_encode_cache_size() frames back — so those old frames are provably dead
the moment the decoder consumes past them. Because each rebuild requests a uniquely, ever-larger
allocation for the life of the stream, the allocator can never reuse an earlier call's freed
(now permanently too-small) block, so process RSS grows with the cumulative history of buffer
sizes rather than the small, bounded set of frames actually still needed — which is why neither
stream_free nor a fresh stream_begin reclaims it.

The fix: track the absolute frame index of mel_buf's first column (mel_buf_origin) and, on
every append, drop the prefix strictly before mel_buffer_idx - pre_encode_cache_size() — the
exact bound window() already relies on — so mel_buf stays bounded to
O(pre_encode_cache_size + chunk_size) instead of O(stream length). No API change, no behavioural
change: window()'s absolute-index reads are translated into mel_buf's origin-relative local
columns, and the surviving frames are carried forward byte-for-byte.

Measurement (CPU + Accelerate build, realtime_eou_120m-v1 q8, 120 s of silence fed in 20 ms
chunks, RSS sampled at 30/60/90/120 s of audio fed): growth over the last 60 s drops from
178 MB to 0.5 MB; the same run's wall time drops from ~14 minutes to ~13 seconds, since the
unbounded rebuild was O(T) per call, O(T²) total. A LibriSpeech utterance's transcript is
byte-identical before and after.
@mudler
mudler merged commit 2bf8895 into mudler:master Sep 26, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming: parakeet_capi_stream_feed leaks ~20-40 MB per second of audio; stream_free + stream_begin does not reclaim

2 participants