17장. Project 02. LLM 추론 API 서빙
묶어서 배포하기
프랜차이즈 가게를 새로 열 때는 주방 설비, 재료 창고, 메뉴판을 한 설명서대로 차립니다. 설명서만 있으면 어느 동네에서도 같은 가게가 됩니다. 추론 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 |
뜨는 차례 읽기
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 글이 어떤 사전이 되는지 서비스 둘로 봅니다. 서비스 이름은 설명용입니다.
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 의 기본값인 빈 사전이 쓰였다. 기다릴 것이 없으니 먼저 뜬다 |