LLM 엔지니어링17장 · Project 02. LLM 추론 API 서빙8 / 14← 이전목차다음 →MetaContents

17장. Project 02. LLM 추론 API 서빙

묶어서 배포하기

프랜차이즈 가게를 새로 열 때는 주방 설비, 재료 창고, 메뉴판을 한 설명서대로 차립니다. 설명서만 있으면 어느 동네에서도 같은 가게가 됩니다. 추론 API 도 API 서버만 담아서는 안 되고, 모델 서버와 모델 파일까지 한 묶음으로 적어야 다른 서버에서 그대로 뜹니다.

ollama, 모델 받기, 요약 API, 분류 API 의 네 서비스를 카드로 놓은 그림

왼쪽부터 GPU 를 쓰고 모델 파일을 보관하는 Ollama, 처음 한 번 모델을 내려받고 끝나는 서비스, 8171 번 포트의 요약 API, 8172 번 포트의 분류 API 입니다. 두 API 는 GPU 를 쓰지 않고 Ollama 를 부릅니다. 여기서 설명서가 compose.yaml 이고, 재료 창고가 모델 파일을 담는 볼륨입니다.

compose 에 새로 들어가는 것

14장의 compose 에 LLM 서빙을 위해 더하는 항목입니다.

항목하는 일이 장의 값
image: ollama/ollama공식 Ollama 이미지로 모델 서버를 띄운다판을 붙여 고정한다
volumes내려받은 모델을 컨테이너 밖에 둔다다시 띄워도 다시 받지 않는다
deploy.resources.reservations.devices컨테이너에 GPU 를 준다Ollama 에만 준다
healthcheck서비스가 준비됐는지 검사한다Ollama 는 ollama list, API 는 /health
depends_on 의 condition앞 서비스가 준비되거나 끝난 뒤에 띄운다service_healthy, service_completed_successfully

뜨는 차례 읽기

Python
import yaml

compose = yaml.safe_load("""
services:
  ollama: {image: ollama/ollama}
  ollama-pull: {image: ollama/ollama, depends_on: {ollama: {condition: service_healthy}}}
  summary: {build: ./summary, depends_on: {ollama-pull: {condition: service_completed_successfully}}}
  inquiry: {build: ./inquiry, depends_on: {ollama-pull: {condition: service_completed_successfully}}}
""")
started, left, rounds = [], dict(compose["services"]), []
while left:                                            # 기다릴 서비스가 모두 뜬 것부터 차례로 띄운다
    ready = [name for name, svc in left.items() if all(d in started for d in svc.get("depends_on", {}))]
    rounds.append(ready)
    started += ready
    for name in ready:
        del left[name]
print(rounds)

[['ollama'], ['ollama-pull'], ['summary', 'inquiry']] 이 나옵니다. Ollama 가 먼저 뜨고, 모델 받기가 끝나야 두 API 가 함께 뜹니다.

코드에 나오는 함수

코드쉬운 뜻
yaml.safe_load(...)YAML 글을 파이썬 사전으로 읽는다. safe(안전한)는 글 안의 위험한 파이썬 객체는 만들지 않는다는 뜻이다. YAML 은 YAML Ain't Markup Language 의 머리글자다
dict(compose["services"])서비스 목록을 복사해 둔다. 띄운 것을 지워 가도 원래 사전은 그대로 남는다
svc.get("depends_on", {})depends on(~에 기대다)이다. 먼저 떠야 할 서비스 목록이고, 없으면 빈 사전이다
all(d in started for d in ...)기다릴 서비스가 모두 이미 떴으면 True 다. 기다릴 것이 없어도 True 다
rounds.append(ready)이번 차례에 함께 띄울 서비스를 한 묶음으로 적는다

이 코드가 뜻하는 것

새 가게를 차릴 때 전기를 먼저 넣고, 냉장고에 재료를 채운 뒤, 마지막에 홀과 배달 창구를 엽니다. 앞 일이 끝나야 다음 일을 할 수 있는 차례가 있습니다. 여기서 전기가 ollama, 재료 채우기가 ollama-pull, 홀과 배달 창구가 두 API 입니다.

줄하는 일쉽게 말하면
compose = yaml.safe_load(...)서비스 넷과 기다릴 서비스를 읽는다개업 설명서를 읽는다
while left:아직 띄우지 않은 서비스가 있으면 되풀이한다남은 일이 있으면 계속한다
ready = [...]기다릴 것이 다 뜬 서비스만 고른다지금 바로 할 수 있는 일
print(rounds)[['ollama'], ['ollama-pull'], ['summary', 'inquiry']]전기, 재료, 홀과 창구를 함께 연다

이 코드는 condition(준비됨, 끝남)의 차이는 보지 않고 차례만 셉니다. 실제 compose 는 Ollama 가 service_healthy 가 될 때까지, 모델 받기가 끝날 때까지 기다립니다.

작게 줄여서 눈으로 보기

YAML 글이 어떤 사전이 되는지 서비스 둘로 봅니다. 서비스 이름은 설명용입니다.

Python
import yaml

data = yaml.safe_load("""
services:
  web: {image: nginx, depends_on: {db: {condition: service_healthy}}}
  db: {image: postgres}
""")
print(data)
print(list(data["services"]["web"].get("depends_on", {})))
print(list(data["services"]["db"].get("depends_on", {})))
출력
{'services': {'web': {'image': 'nginx', 'depends_on': {'db': {'condition': 'service_healthy'}}}, 'db': {'image': 'postgres'}}}
['db']
[]
결과읽는 법
첫 줄의 사전들여쓰기와 { } 가 사전 안의 사전이 됐다. service_healthy 같은 값은 글자로 들어온다
['db']web 은 db 를 기다린다
[]db 에는 depends_on 이 없어 .get 의 기본값인 빈 사전이 쓰였다. 기다릴 것이 없으니 먼저 뜬다

주의할 점

  • GPU 를 쓰는 컨테이너에는 NVIDIA Container Toolkit 이 있어야 합니다. Windows 에서는 Docker Desktop 과 WSL2 가 필요합니다. 이 PC 에는 Docker 가 없어 compose 는 파일만 만들고 검사합니다(14장와 같다).
  • 폐쇄망에서는 ollama pull 이 인터넷에 닿지 못합니다. 모델 파일을 미리 받아 볼륨에 넣거나 이미지에 담습니다(11장).
  • 분류 API 의 임베딩 모델도 이미지를 만들 때 미리 받아 둡니다. 컨테이너가 뜰 때마다 내려받으면 느리고, 폐쇄망에서는 뜨지 않습니다.
← 이전 절17장 · 빠른 모델과 큰 모델 나눠 쓰기다음 절 →17장 · 평가표와 발표