Skip to content

AgentSession default VAD/turn-detector loads ~150MB per process with no global opt-out — OOMs multi-process (spawn) consumers #6369

Description

@maktoufzied

Summary

Since 1.6.x, AgentSession auto-provisions local inference models when the caller does not pass them: a bundled silero VAD and an end-of-turn (EOT) turn detector. The EOT model costs ~152MB of resident memory per Python process, and there is no global opt-out — only per-constructor vad=None / turn_handling={"turn_detection": None}, which is not reachable when sessions are constructed inside a third-party harness. For multi-process consumers using multiprocessing.spawn (evaluation frameworks, test runners), the cost multiplies per worker: our 16-worker evaluation CI gained ~2.4GB of RSS from the upgrade alone and was OOM-killed on an 8GB container. The sessions involved are text-only (no audio I/O at all), so the models are never used.

LiveKit support confirmed there is no environment variable or module-level switch and suggested an adapter that injects vad=None/turn_detection=None — which works, but requires patching private module internals when the constructor isn't reachable.

What changed

livekit/agents/voice/agent_session.py (v1.6.4):

raw_turn_detection: TurnDetectionMode | None = turn_handling.get(
    "turn_detection", inference.TurnDetector()          # ~line 366
)
...
if not is_given(vad):
    vad = inference.VAD(model="silero")                 # ~line 415

Under 1.4.5, AgentSession(llm=...) had no VAD and no turn-detector model. Additionally, import livekit.agents now eagerly imports livekit.local_inference (the pybind11 native module) — verified via "livekit.local_inference" in sys.modules right after the import.

Measurements (livekit-agents 1.6.4, Python 3.13, macOS arm64 + Linux)

Single process, resource.getrusage max-RSS deltas:

Step RSS delta
import livekit.agents +113MB (≈ same as 1.4.5)
livekit.local_inference.init_vad() +2MB
livekit.local_inference.init_eot() +152MB

Per-process duplication under spawn (4 workers, each importing livekit.local_inference and calling init_eot()):

EOT model RSS delta per spawned process (MB): [151, 151, 152, 152]

livekit/agents/inference/_warmup.py acknowledges the cost — it preloads the model singletons in a forkserver parent so livekit's own job subprocesses share the weight pages via COW — but external harnesses using spawn get no such sharing.

Reproduction

# single process
import resource, sys

def rss_mb():
    ru = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
    return ru / (1024 * 1024) if sys.platform == "darwin" else ru / 1024

b0 = rss_mb()
import livekit.agents
b1 = rss_mb(); print(f"import livekit.agents: +{b1-b0:.0f}MB")
print("local_inference eagerly imported:", "livekit.local_inference" in sys.modules)

import livekit.local_inference as li
li.init_vad();  b2 = rss_mb(); print(f"init_vad(): +{b2-b1:.0f}MB")
li.init_eot();  b3 = rss_mb(); print(f"init_eot(): +{b3-b2:.0f}MB")
# spawn duplication
import multiprocessing as mp
import resource, sys

def rss_mb():
    ru = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
    return ru / (1024 * 1024) if sys.platform == "darwin" else ru / 1024

def worker(q):
    import livekit.local_inference as li
    before = rss_mb()
    li.init_eot()
    q.put(round(rss_mb() - before))

if __name__ == "__main__":
    ctx = mp.get_context("spawn")
    q = ctx.Queue()
    procs = [ctx.Process(target=worker, args=(q,)) for _ in range(4)]
    for p in procs: p.start()
    print("per-process EOT RSS delta (MB):", [q.get() for _ in procs])
    for p in procs: p.join()

Feature request

Any of the following would resolve this cleanly:

  1. A supported global opt-out for the default components — e.g. AgentSession.set_default_components(vad=None, turn_detection=None) or an environment variable — for sessions constructed inside code the application doesn't control (harnesses, frameworks, tests).
  2. Lazy import of livekit.local_inference (on first VAD/EOT use) instead of eagerly on import livekit.agents.
  3. Deferred EOT model load until turn detection is actually exercised on an audio stream, so text-only sessions never pay it.
  4. Documented guidance for multi-process (spawn) consumers, equivalent to the internal inference/_warmup.py forkserver-preload approach.

Environment

  • livekit-agents: 1.6.4
  • Python: 3.13
  • OS: reproduced on macOS (arm64) and Linux (Docker, cimg/python:3.13)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions