deeppavlov / deeppavlov/AutoIntent

API scorers: `predict()` fails inside a running event loop; `LLMDescriptionScorer` leaks an unclosed loop per instance on Python 3.14

Open
#353 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Python
Stars
54
Forks
16
PR merge metrics
No merged PRs in 30d

Description

Both API scorers run their requests with a loop kept on the instance: _init_event_loop does asyncio.get_event_loop() (falling back to new_event_loop()) and _compute_similarities calls self._event_loop.run_until_complete(aiometer.run_all(...))LLMDescriptionScorer llm_encoder.py:282-291, :247-252; TypeSafeDescriptionScorer typesafe.py:259-270, :331-340. Two consequences:

1. predict() fails inside a running event loop (both scorers)

In a notebook, a FastAPI/uvicorn handler, or any async def caller, get_event_loop() returns the running loop and run_until_complete raises RuntimeError: This event loop is already running. A pipeline whose scoring node is one of these modules cannot be served from an async server without the max_concurrent=None (sequential, sync-client) escape hatch.

2. LLMDescriptionScorer leaks an unclosed event loop per instance on Python 3.14

On 3.14 asyncio.get_event_loop() raises RuntimeError when no loop is set, so the fallback new_event_loop() runs — without set_event_loop — for every instance; each HPO trial constructs a new module, and clear_cache delattrs the loop without closing it (:274-280). Reproduced with the _init_event_loop body verbatim on CPython 3.14.3:

$ python3.14 -W always repro.py     # three "trials"
ResourceWarning: unclosed event loop <_UnixSelectorEventLoop running=False closed=False debug=False>   (×3)
distinct loops: 3 closed: [False, False, False]

One selector fd per trial, never released until GC. On ≤3.13 the main-thread get_event_loop() creates and sets one loop, so it is shared and this does not show. TypeSafeDescriptionScorer calls set_event_loop and reuses the loop, so it only has problem 1.

Proposed

One helper in a shared base (#357) instead of a loop on the instance: no running loop → run the batch on a fresh loop that is closed afterwards (asyncio.run semantics); running loop → run it on a worker thread with its own loop (or expose an apredict coroutine for async callers). That also removes the Dumper.dump(..., exclude=[asyncio.BaseEventLoop, dict]) special-casing in both dump methods.

Deferred from #350.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with _init_event_loop, _compute_similarities, and dump in src/autointent/modules/scoring/_description/llm_encoder.py and typesafe.py, then review the proposed shared base in #357. Reproduce the failure from a running event loop and the Python 3.14 ResourceWarning. Done means both scorers work from synchronous and async callers without retaining or leaking event loops.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.