Skip to content

Native memory accumulates ~50 MB per AudioStreamTranscriber recreation on the ANE path; survives full WhisperKit teardown (iPad Pro M5, iPadOS 26.5) #517

Description

@herbae

Environment

  • WhisperKit v1.0.0 (tag 25c6299), SPM
  • Model: openai_whisper-large-v3-v20240930_turbo_632MB (local folder load, prewarm+load)
  • iPad Pro 11" (M5, iPad17,1), iPadOS 26.5.2 — note: not present in the config.json device list, so likely outside your regression matrix
  • Compute: default (cpuAndNeuralEngine path) unless stated
  • App: React Native host, but the leak is measured native-side (task_vm_info.phys_footprint) and reproduces with JS payloads bounded

Pattern

Live streaming with AudioStreamTranscriber. Because transcribeCurrentBuffer() re-submits the entire accumulated buffer every pass, we bound per-pass work by tearing the transcriber down every 120 s and creating a fresh one against the same WhisperKit instance (bank confirmed text, stopStreamTranscription(), cancel the task, release the streamer, new AudioStreamTranscriber).

Measurements (phys_footprint logged at every teardown)

  1. ~50 MB is retained per teardown/recreate cycle under continuous field audio: two 15-minute runs measured +52 and +48 MB per 120 s cycle (8 cycles each, 422 MB baseline → 840/808 MB). Under dense speech we measured 75–165 MB per cycle. Growth is monotonic; quiet audio still leaks (fewer decode passes, same cycles).
  2. Session teardown is clean at the Swift level: in a 33-cycle run, 33 stopStreamTranscription() calls and 33 realtime-loop exits — no retained tasks or streamers.
  3. Destroying the entire WhisperKit instance does not return the memory. Every 4th cycle we set the WhisperKit reference to nil and re-created it from scratch (full model reload): footprint dropped only 59/65/165 MB at 869/1,274/1,569 MB respectively, then resumed climbing at the same rate.
  4. ANE-specific: under cpuAndGPU the footprint oscillates (±300 MB with full reclamations, no monotonic trend) around a much larger resident baseline (~3.5 GB, model in unified memory). Under all it accumulates like the default path.
  5. Partially pressure-reclaimable: in a 90-minute run (45 cycles) footprint peaked at 4,069 MB, the OS reclaimed ~1.5 GB in one event, and the process stabilized at 2.6–3.0 GB. A prior run under denser load lost that race: swift_abortAllocationFailure (SIGABRT) during the Array(currentBuffer) copy in transcribeCurrentBuffer, with MALLOC at 4.9 GB in the crash report's vmSummary.
  6. PR Fix avaudioengine deinit #508's deinit fix (applied locally, deinit hunk only): no effect on the leak. With the fixed teardown, a 12-minute run measured +58 MB/cycle (444 → 671 MB over 5 cycles) against +52/+48 in unpatched control runs on identical audio — within noise. (The fix still looks correct for its stated crash; it just is not this leak.)
  7. An autoreleasepool-per-prediction experiment (suggested in CoreML Audio Resource Leak #393's comments) is not mechanically applicable on this codepath: all predictions go through await model.asyncPrediction(...), which a synchronous autoreleasepool cannot wrap.

What we think this is

Consistent with per-session ANE prediction-context/IOSurface memory allocated out-of-process that is not released when its owners are deallocated — same class as the accumulation reported in #265 (there on MPSGraph/cpuAndGPU; for us the GPU path is the clean one and ANE the leaking one) and adjacent to #300 and #393.

Questions

  1. Is per-session ANE memory retention a known issue for AudioStreamTranscriber recreation? Any recommended session lifecycle for hour-scale live capture?
  2. The WhisperAX example contains a commented-out purgeAudioSamples(keepingLast:) sketch ("TODO: Implement silence buffer purging"). Is a long-lived transcriber with in-place buffer purging the intended direction? We are implementing it (with buffer-offset timestamp accounting per ufal/whisper_streaming) and can contribute it back if useful.
  3. AudioStreamTranscriber keeps lastBufferSize / lastConfirmedSegmentEndSeconds private, which forces a fork to implement purging externally. Would you accept a PR exposing a purge-aware API?

Full per-run device logs, footprint series, and crash reports available on request.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions