118f32f9b1
PR #20 (2026-05-14, GPU LLM 제거 + Mac mini 26B MLX 흡수) 의 swap 이 backends.json + 코드 주석/docstring 까지 따라가지 못한 표현 잔재 정리. - app/ai/client.py: AIClient docstring 및 call_triage / call_fallback docstring 의 "4B Ollama" → "Mac mini 26B MLX" / "현재는 triage 와 동일 엔드포인트" → "Claude Sonnet 4 API (PR #20 swap 완료)" - app/core/config.py: triage/primary/fallback 주석 통합 + Phase 3.5 classifier/verifier 주석에 PR #20 endpoint 명시 (history 보존) - app/services/search/{llm_gate,classifier_service,verifier_service, evidence_service}.py: "fallback(Ollama)" / "Ollama concurrent OK" / "triage(4B Ollama)" 표현을 Mac mini 26B MLX endpoint 기준으로 정정 + concurrent 안전성 별 검토 마커 추가 - app/services/digest/summarizer.py: "MLX hang/Ollama stall 방어" → "MLX hang / fallback Claude API stall 방어" - app/services/prompt_versions.py: SUMMARY_TRIAGE_TASK + ASK_PROMPT_VERSION 주석의 "4B Ollama" / "4B gemma Ollama" → Mac mini 26B MLX - app/workers/classify_worker.py: B-1 tier triage docstring 정정 코드 동작 변경 0 (주석/docstring 만). embed_worker / study_question_embed_worker 의 "Ollama bge-m3" 표현은 사실 정확이라 유지. 검증: - ollama list → bge-m3:latest 잔존 (embedding owner) - /api/embeddings probe → 1024-dim 200 OK - fastapi embed/ollama error 0 (last 10min) - document.hyungi.net 200 plan: ~/.claude/plans/4-stateless-dongarra.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59 lines
2.6 KiB
Python
59 lines
2.6 KiB
Python
"""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(Claude Sonnet 4 API) 경로는 gate 제외**. PR #20 이후 fallback = Claude API. 단 현재
|
|
구현상 `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
|