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:
- 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).
- Lazy import of
livekit.local_inference (on first VAD/EOT use) instead of eagerly on import livekit.agents.
- Deferred EOT model load until turn detection is actually exercised on an audio stream, so text-only sessions never pay it.
- 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)
Summary
Since 1.6.x,
AgentSessionauto-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-constructorvad=None/turn_handling={"turn_detection": None}, which is not reachable when sessions are constructed inside a third-party harness. For multi-process consumers usingmultiprocessing.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):Under 1.4.5,
AgentSession(llm=...)had no VAD and no turn-detector model. Additionally,import livekit.agentsnow eagerly importslivekit.local_inference(the pybind11 native module) — verified via"livekit.local_inference" in sys.modulesright after the import.Measurements (livekit-agents 1.6.4, Python 3.13, macOS arm64 + Linux)
Single process,
resource.getrusagemax-RSS deltas:import livekit.agentslivekit.local_inference.init_vad()livekit.local_inference.init_eot()Per-process duplication under
spawn(4 workers, each importinglivekit.local_inferenceand callinginit_eot()):livekit/agents/inference/_warmup.pyacknowledges 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 usingspawnget no such sharing.Reproduction
Feature request
Any of the following would resolve this cleanly:
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).livekit.local_inference(on first VAD/EOT use) instead of eagerly onimport livekit.agents.spawn) consumers, equivalent to the internalinference/_warmup.pyforkserver-preload approach.Environment
cimg/python:3.13)