Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Embedding Server

Mac Mini (Apple Silicon) 홈서버에서 MPS GPU를 활용한 고성능 텍스트 임베딩 서버.
BGE-M3 모델 기반, FastAPI + FlagEmbedding 구현.


목차


아키텍처 개요

Request
  │
  ▼
FastAPI (async)
  │
  ├─ /health  ──────────────────────────────── 즉시 반환
  │
  └─ /embed  ──── input validation
                      │
                      ▼
              asyncio.Semaphore  ◄── GPU 동시 호출 수 제한 (OOM 방어)
                      │
                      ▼
              run_in_executor  ◄─── blocking encode()를 thread pool에 위임
                      │
                      ▼
              BGEM3FlagModel.encode()  ◄── MPS / CUDA / CPU 자동 선택
                      │
                      ▼
              dense_vecs  (+sparse lexical_weights 선택)
                      │
                      ▼
              EmbeddingResponse (JSON)

디렉토리 구조

embedding-server/
├── app/
│   ├── main.py                  # FastAPI app, lifespan 정의
│   ├── api/
│   │   └── routes.py            # 엔드포인트
│   ├── core/
│   │   └── config.py            # pydantic-settings 기반 설정
│   ├── models/
│   │   └── schema.py            # Request / Response 스키마
│   └── services/
│       └── embedding_service.py # 모델 로딩, 추론, async 처리
├── Dockerfile
├── docker-compose.yml
└── requirements.txt

설계 결정

1. FlagEmbedding (BGEM3FlagModel) 사용

sentence-transformers에서 FlagEmbedding으로 교체.

항목 sentence-transformers FlagEmbedding
BGE-M3 dense O O
Sparse (lexical) X O
ColBERT (multi-vec) X O
공식 지원 X O (BAAI 직접 관리)

현재 API는 dense를 기본으로 반환하고, return_sparse: true 요청 시 sparse weight도 함께 반환.
이후 하이브리드 검색(dense + sparse re-rank)으로 확장 가능.

2. MPS (Apple Silicon GPU) 자동 감지

# 우선순위: MPS > CUDA > CPU
if torch.backends.mps.is_available():
    return "mps"
if torch.cuda.is_available():
    return "cuda"
return "cpu"
  • fp16은 CUDA에서만 활성화. MPS는 fp16 지원 op가 불완전하므로 기본 정밀도 사용.
  • PYTORCH_ENABLE_MPS_FALLBACK=1 환경변수로 MPS 미지원 op는 CPU로 자동 fallback.

주의: Docker Desktop on macOS는 Metal GPU를 컨테이너에 노출하지 않는다.
MPS 가속을 사용하려면 반드시 네이티브로 실행해야 한다. (→ 실행 방법)

3. 비동기 안전 설계

model.encode()는 blocking call이다. async 핸들러에서 직접 호출하면 이벤트 루프 전체가 블로킹된다.

# 잘못된 방법 — 이벤트 루프 블로킹
async def embed(...):
    result = self._model.encode(texts)  # ❌

# 올바른 방법 — thread pool에 위임
async def embed(...):
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(None, encode_fn)  # ✅

4. GPU 동시성 제어 (Semaphore)

여러 요청이 동시에 들어올 때 GPU에 encode()가 중첩 호출되면 OOM이 발생할 수 있다.
asyncio.Semaphore(max_concurrent)로 동시 추론 횟수를 제한한다.

max_concurrent = 2 (기본값)

Request A ──► Semaphore acquire ──► encode() ──► release
Request B ──► Semaphore acquire ──► encode() ──► release
Request C ──► Semaphore 대기 ──────────────────► acquire → encode()

Mac Mini M 시리즈는 통합 메모리이므로 max_concurrent를 낮게 유지하는 것이 안전하다.

5. lifespan 기반 초기화

모듈 레벨 전역 초기화(embedding_service = EmbeddingService()) 방식에서 FastAPI lifespan으로 교체.

@asynccontextmanager
async def lifespan(app: FastAPI):
    loop = asyncio.get_event_loop()
    app.state.embedding_service = await loop.run_in_executor(None, EmbeddingService)
    yield
  • 모델 로딩이 완전히 끝나기 전에는 트래픽을 받지 않는다.
  • 모델 인스턴스를 app.state에 저장하고 라우터는 request.app.state에서 참조 → 전역 상태 없음.

6. 환경변수 기반 설정

pydantic-settings를 사용. EMBED_ prefix 환경변수 또는 .env 파일로 오버라이드 가능.

EMBED_MODEL_NAME=BAAI/bge-m3
EMBED_MAX_BATCH_SIZE=32
EMBED_MAX_TEXT_LENGTH=8192
EMBED_MAX_CONCURRENT=2
EMBED_MODEL_CACHE_DIR=/cache/models

7. API 단순화

기존의 /embed (단일 텍스트) + /embed/batch (복수 텍스트) 이중 구조를 /embed 하나로 통합.
단일 텍스트도 배열로 받으므로 클라이언트 코드가 단일/배치를 분기할 필요가 없다.


API 명세

GET /health

서버 상태 및 모델 정보 반환.

Response

{
  "status": "ok",
  "model": "BAAI/bge-m3",
  "dimension": 1024,
  "device": "mps"
}

POST /embed

텍스트 배열을 임베딩 벡터로 변환.

Request

{
  "texts": ["안녕하세요", "embedding server"],
  "return_sparse": false
}
필드 타입 필수 설명
texts string[] O 임베딩할 텍스트 목록 (1 ~ max_batch_size)
return_sparse bool X sparse weight 반환 여부 (기본값: false)

Response

{
  "model": "BAAI/bge-m3",
  "dimension": 1024,
  "embeddings": [[0.021, -0.043, ...]],
  "sparse_embeddings": null
}

return_sparse: true일 때 sparse_embeddings:

{
  "sparse_embeddings": [
    {"30001": 0.31, "9876": 0.18, ...}
  ]
}

키는 token ID(str), 값은 lexical weight(float). Qdrant / Weaviate 하이브리드 검색에 바로 사용 가능.


설정

환경변수 기본값 설명
EMBED_MODEL_NAME BAAI/bge-m3 HuggingFace 모델 ID
EMBED_MODEL_CACHE_DIR ~/.cache/huggingface 모델 가중치 캐시 경로
EMBED_MAX_BATCH_SIZE 32 요청당 최대 텍스트 수
EMBED_MAX_TEXT_LENGTH 8192 텍스트 최대 문자 수 (초과 시 자름)
EMBED_MAX_CONCURRENT 2 동시 GPU 추론 수 (Semaphore 상한)

.env 파일을 프로젝트 루트에 두면 자동으로 로드된다.


실행 방법

네이티브 (MPS GPU 사용 — 권장)

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# 첫 실행 시 BGE-M3 모델 (~2.5GB) 자동 다운로드
PYTORCH_ENABLE_MPS_FALLBACK=1 uvicorn app.main:app --host 0.0.0.0 --port 8001

백그라운드 상시 실행이 필요하면 launchd plist 또는 brew services로 등록한다.

Docker (CPU only)

docker compose up -d

모델 가중치는 model-cache named volume에 저장되어 컨테이너 재빌드 후에도 유지된다.

동작 확인

# 헬스체크
curl http://localhost:8001/health

# 임베딩
curl -X POST http://localhost:8001/embed \
  -H "Content-Type: application/json" \
  -d '{"texts": ["Mac Mini is great", "벡터 검색"]}'

Docker vs 네이티브

네이티브 Docker
MPS (GPU) O X
설치 편의성 가상환경 필요 docker compose up
모델 캐시 ~/.cache/huggingface named volume
권장 용도 운영 (홈서버) 개발 / CI

About

TEAM-04 embedding server

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages