"""MLX single-inference 전역 gate (Phase 3.1.1). Mac mini MLX primary(gemma-4-26b-a4b-it-8bit)는 **single-inference**다. 동시 호출이 들어오면 queue가 폭발한다(실측: 23 concurrent 요청 → 22개 15초 timeout). 이 모듈은 analyzer / evidence / synthesis 등 **모든 MLX-bound LLM 호출**이 공유하는 `asyncio.Semaphore(1)`를 제공한다. MLX를 호출하는 경로는 예외 없이 `async with get_mlx_gate():` 블록 안에서만 `AIClient._call_chat(ai.primary, ...)` 를 호출해야 한다. ## 영구 룰 - **MLX primary 호출 경로는 예외 없이 gate 획득 필수**. query_analyzer / evidence_service / synthesis_service 세 곳이 현재 사용자. 이후 경로가 늘어도 동일 gate를 import해서 사용한다. 새 Semaphore를 만들지 말 것 (큐 분할 시 동시 실행 발생). - **`asyncio.timeout(...)`은 gate 안쪽에서만 적용**. gate 대기 자체에 timeout을 걸면 "대기만으로 timeout 발동" 버그가 재발한다(query_analyzer 초기 이슈). - **fallback(Ollama) 경로는 gate 제외**. GPU Ollama는 concurrent OK. 단 현재 구현상 `AIClient._call_chat` 내부에서 primary→fallback 전환이 일어나므로 fallback도 gate 점유 상태로 실행된다. 허용 가능(fallback 빈도 낮음). - **MLX concurrency는 `MLX_CONCURRENCY = 1` 고정**. 모델이 바뀌어도 single- inference 특성이 깨지지 않는 한 이 값을 올리지 말 것. ## 확장 여지 (지금은 구현하지 않음) 트래픽 증가 시 "우선순위 역전"(/ask가 analyzer background task 뒤에 밀림)이 문제가 되면 `asyncio.PriorityQueue` 기반 우선순위 큐로 교체 가능. Gate 자체 분리(get_analyzer_gate / get_ask_gate)는 single-inference에서 throughput 개선이 없으므로 의미 없음. """ from __future__ import annotations import asyncio # MLX primary는 single-inference → 1 MLX_CONCURRENCY = 1 # 첫 호출 시 현재 event loop에 바인딩된 Semaphore 생성 (lazy init) _mlx_gate: asyncio.Semaphore | None = None def get_mlx_gate() -> asyncio.Semaphore: """MLX primary 호출 경로 공용 gate. 최초 호출 시 lazy init. 사용 예: async with get_mlx_gate(): async with asyncio.timeout(LLM_TIMEOUT_MS / 1000): raw = await ai_client._call_chat(ai_client.ai.primary, prompt) ⚠ `asyncio.timeout`은 반드시 gate 안쪽에 둘 것. 바깥에 두면 gate 대기만으로 timeout이 발동한다. """ global _mlx_gate if _mlx_gate is None: _mlx_gate = asyncio.Semaphore(MLX_CONCURRENCY) return _mlx_gate