0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLMのタイムアウト後も動くPythonスレッドを検証

0
Posted at

ターミナルにタイムアウトが返った。今日、同期型LLMクライアントを別スレッドで呼ぶ構成で、裏に残る処理を数えた。

自分は待ち時間と実処理の同時実行数を別々に制限する。asyncio.Semaphore(1)を付けても、タイムアウトを挟むと実処理が3本まで増えた。

Semaphoreが1でも、スレッドは3本動く

CPython 3.14.8で実行した。標準ライブラリだけを使い、blocking()を同期クライアントの代役にする。API接続はない。

import asyncio
import threading
from concurrent.futures import ThreadPoolExecutor

async def probe(workers):
    loop = asyncio.get_running_loop()
    loop.set_default_executor(ThreadPoolExecutor(max_workers=workers))
    gate = asyncio.Semaphore(1)
    started = asyncio.Queue()
    release = threading.Event()
    lock = threading.Lock()
    active = 0

    def blocking():
        nonlocal active
        with lock:
            active += 1
        loop.call_soon_threadsafe(started.put_nowait, None)
        release.wait()
        with lock:
            active -= 1

    try:
        for i in range(3):
            async with gate:
                task = asyncio.create_task(asyncio.to_thread(blocking))
                if workers == 3 or i == 0:
                    await started.get()
                try:
                    await asyncio.wait_for(task, timeout=0.02)
                except TimeoutError:
                    pass
        with lock:
            print(f"workers={workers}, active={active}")
            assert active == workers
    finally:
        release.set()

asyncio.run(probe(3))
asyncio.run(probe(1))

出力。

workers=3, active=3
workers=1, active=1

activeは実行中の本数。releaseを開けるまで終了しないので、処理時間の偶然に頼らず数えられる。開始通知も待った。1本のプールでは最初の関数が居座るため、2回目以降は開始通知を待たない。

待機をキャンセルすると、枠だけが空く

wait_for()の期限が来るとTaskがキャンセルされる。async with gateを抜けてSemaphoreの枠が戻り、次の呼び出しが入る。実行中のスレッドはrelease.wait()に残る。

枠は空いた。関数は終わっていない。

20msで待機を打ち切るとSemaphoreの枠が次の呼び出しに渡る一方、前の同期関数はスレッド内に残る流れ。プールの上限を1本にすると次の関数は順番待ちになる。

to_threadの実装はrun_in_executor()に関数を渡す。Future.cancelはRUNNINGのFutureをキャンセルできない。実行中の関数を強制終了する仕組みにはならない。

20msは待機の期限だ。

同期クライアントを使うなら

2回目はmax_workers=1に変えた。Semaphoreが戻っても、次の関数は並んで待つ。未着手の呼び出しはキャンセルできる。

アプリでは他の処理も既定プールを使うので、LLM専用プールをrun_in_executor()へ渡し、終了処理も用意する。asyncio.run()も既定プールの完了を待つため、このコードのfinallyを外すと終了待ちで止まる。

スレッド数の上限だけでは待ち行列は制限できない。受付件数にも上限を置き、同期クライアントには通信タイムアウトを設定する。サーバー側の生成や課金まで止まるかはAPI側の仕様を確認する。

タイムアウト件数だけでは残った処理を数え損ねる。クライアント呼び出しの開始と終了を、待機側の終了と分けて記録する。同期SDKが増えるほど、この差はリトライ時の負荷に出ると見ている。上のコードなら、APIを呼ばずに残存本数を確かめられる。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?