이미지 로딩 중...
AI Generated
2025. 11. 17. · 3 Views
파인튜닝 모델 배포 완벽 가이드 vLLM, TGI, Ollama
직접 파인튜닝한 AI 모델을 실제 서비스에 배포하는 방법을 배웁니다. vLLM, TGI, Ollama 등 주요 배포 도구들의 특징과 활용법을 초급자도 쉽게 이해할 수 있도록 설명합니다.
목차
- vLLM_기본_개념
- OpenAI_호환_API_서버
- TGI_Text_Generation_Inference
- Ollama_로컬_모델_관리
- 모델_양자화와_최적화
- Docker_컨테이너_배포
- 성능_모니터링과_로깅
- 로드_밸런싱과_스케일링
- 보안과_인증
- 비용_최적화_전략
1. vLLM_기본_개념
시작하며
여러분이 몇 주간 고생해서 파인튜닝한 LLM 모델을 드디어 완성했다고 상상해보세요. 그런데 막상 서비스에 올려보니 응답 속도가 너무 느려서 사용자들이 기다리다 지쳐버리는 상황을 겪어본 적 있나요?
이런 문제는 실제 AI 서비스 개발 현장에서 정말 자주 발생합니다. 아무리 좋은 모델을 만들어도 배포 단계에서 성능이 나빠지면 실제 서비스로 쓸 수 없게 됩니다.
특히 여러 사용자가 동시에 요청을 보내면 서버가 감당하지 못하는 경우가 많죠. 바로 이럴 때 필요한 것이 vLLM입니다.
vLLM은 마치 택배 회사가 여러 주문을 효율적으로 배송하듯이, 여러 AI 요청을 빠르고 효율적으로 처리해주는 도구입니다.
개요
간단히 말해서, vLLM은 대규모 언어 모델을 실제 서비스 환경에서 빠르게 실행할 수 있게 해주는 초고속 추론 엔진입니다. 왜 이 도구가 필요할까요?
일반적인 방법으로 LLM을 실행하면 GPU 메모리를 비효율적으로 사용하고, 한 번에 하나의 요청만 처리하게 됩니다. 예를 들어, 챗봇 서비스를 운영할 때 100명의 사용자가 동시에 질문하면 99명은 기다려야 하는 상황이 발생합니다.
기존에는 모델을 여러 개 복사해서 서버를 늘리는 방식으로 해결했다면, 이제는 vLLM의 PagedAttention 기술로 하나의 GPU에서 여러 요청을 동시에 처리할 수 있습니다. vLLM의 핵심 특징은 세 가지입니다.
첫째, PagedAttention으로 메모리를 최대 4배까지 절약합니다. 둘째, 동적 배치로 여러 요청을 똑똑하게 묶어서 처리합니다.
셋째, OpenAI API와 호환되어 기존 코드를 거의 그대로 사용할 수 있습니다. 이러한 특징들이 실제 서비스에서 비용 절감과 속도 향상이라는 두 마리 토끼를 잡을 수 있게 해줍니다.
코드 예제
# vLLM 설치 및 기본 서버 실행
# 먼저 vLLM을 설치합니다
# pip install vllm
from vllm import LLM, SamplingParams
# 파인튜닝한 모델 경로를 지정합니다
model_path = "./my-finetuned-llama-7b"
# vLLM 엔진 초기화 - GPU 메모리를 효율적으로 사용
llm = LLM(model=model_path, gpu_memory_utilization=0.9)
# 생성 파라미터 설정 - 응답 품질 조절
sampling_params = SamplingParams(temperature=0.8, top_p=0.95, max_tokens=512)
# 여러 프롬프트를 동시에 처리 - 배치 처리의 위력
prompts = [
"Python에서 비동기 프로그래밍이란?",
"리스트와 튜플의 차이점은?",
"데코레이터는 어떻게 사용하나요?"
]
# 한 번에 모든 요청 처리 - 빠른 응답 생성
outputs = llm.generate(prompts, sampling_params)
# 결과 출력
for output in outputs:
print(f"프롬프트: {output.prompt}")
print(f"응답: {output.outputs[0].text}\n")
설명
이것이 하는 일: vLLM은 여러분이 파인튜닝한 언어 모델을 실제 서비스에서 사용할 수 있도록 최적화된 방식으로 실행시켜주는 도구입니다. 마치 고성능 스포츠카의 엔진처럼, 같은 모델이라도 훨씬 빠르게 작동하게 만들어줍니다.
첫 번째로, LLM 클래스로 모델을 불러올 때 gpu_memory_utilization 파라미터를 사용합니다. 이 부분은 GPU 메모리를 얼마나 사용할지 결정하는데, 0.9는 90%를 사용하겠다는 의미입니다.
왜 이렇게 하냐면, vLLM은 일반 방식과 달리 메모리를 작은 블록으로 나누어 관리하기 때문에 더 많은 메모리를 안전하게 활용할 수 있기 때문입니다. 그 다음으로, SamplingParams에서 temperature와 top_p를 설정합니다.
이 설정이 실행되면서 모델이 얼마나 창의적인 답변을 할지가 결정됩니다. temperature가 높을수록 더 다양한 답변을, 낮을수록 더 일관된 답변을 생성합니다.
내부에서는 확률 분포를 조정하여 다음 단어를 선택하는 방식으로 작동합니다. 마지막으로, llm.generate()가 여러 프롬프트를 동시에 처리하여 최종적으로 모든 응답을 한꺼번에 반환합니다.
이것이 vLLM의 가장 큰 장점인데, 각 요청을 하나씩 처리하는 대신 똑똑하게 묶어서 처리하기 때문에 전체 처리 시간이 크게 줄어듭니다. 여러분이 이 코드를 사용하면 챗봇 서비스, 문서 요약 API, 코드 생성 도구 등을 실제로 배포할 수 있습니다.
실무에서의 이점으로는 첫째, 같은 하드웨어로 더 많은 사용자를 처리할 수 있어 비용이 절감됩니다. 둘째, 응답 속도가 빨라져 사용자 경험이 향상됩니다.
셋째, OpenAI API 형식과 호환되어 기존 애플리케이션과 쉽게 통합할 수 있습니다.
실전 팁
💡 gpu_memory_utilization은 처음에는 0.7~0.8 정도로 시작하세요. 너무 높게 설정하면 OOM(Out of Memory) 에러가 발생할 수 있습니다. 안정성을 확인한 후 점진적으로 높이는 것이 안전합니다.
💡 tensor_parallel_size 파라미터로 여러 GPU를 사용할 수 있습니다. 예를 들어 tensor_parallel_size=4로 설정하면 4개의 GPU로 모델을 분산 처리하여 더 큰 모델도 실행할 수 있습니다.
💡 실제 서비스에서는 vLLM의 OpenAI 호환 서버 모드를 사용하세요. python -m vllm.entrypoints.openai.api_server 명령으로 REST API 서버를 띄우면 기존 OpenAI 클라이언트 코드를 그대로 사용할 수 있습니다.
💡 모니터링을 위해 --enable-metrics 플래그를 사용하세요. 프로메테우스 형식의 메트릭이 제공되어 처리량, 지연시간, GPU 사용률 등을 실시간으로 확인할 수 있습니다.
💡 max_model_len 파라미터로 컨텍스트 길이를 조절하세요. 짧게 설정하면 메모리를 절약하고 더 많은 동시 요청을 처리할 수 있습니다. 예를 들어 챗봇이라면 4096, 긴 문서 처리라면 8192 정도가 적당합니다.
2. OpenAI_호환_API_서버
시작하며
여러분이 vLLM으로 모델을 실행할 수 있게 되었다면, 이제 실제 애플리케이션에서 어떻게 호출할지 고민하게 됩니다. 프론트엔드 개발자나 다른 팀원들이 쉽게 사용할 수 있는 API를 만들어야 하는데, 처음부터 API 서버를 개발하는 것은 시간도 오래 걸리고 복잡하죠.
이런 문제는 AI 모델을 팀 프로젝트에 통합할 때 항상 나타나는 어려움입니다. 모델은 잘 작동하는데 API 형식이 제각각이라 매번 새로운 클라이언트 코드를 작성해야 하고, 문서화도 어렵습니다.
바로 이럴 때 필요한 것이 vLLM의 OpenAI 호환 API 서버입니다. 이미 많은 개발자들이 익숙한 OpenAI API 형식을 그대로 사용하면서, 여러분의 파인튜닝 모델을 서비스할 수 있습니다.
개요
간단히 말해서, vLLM의 OpenAI 호환 API 서버는 여러분의 커스텀 모델을 ChatGPT API와 똑같은 방식으로 호출할 수 있게 해주는 기능입니다. 왜 이것이 필요할까요?
대부분의 개발자들은 이미 OpenAI API를 사용해본 경험이 있고, 수많은 라이브러리와 도구들이 OpenAI API 형식을 지원합니다. 예를 들어, LangChain, LlamaIndex, AutoGPT 같은 인기 프레임워크들은 모두 OpenAI API 형식으로 작동하도록 설계되어 있습니다.
기존에는 각 모델마다 다른 API 클라이언트를 만들어야 했다면, 이제는 OpenAI 클라이언트 라이브러리를 그대로 사용하고 엔드포인트 URL만 바꾸면 됩니다. 핵심 특징은 세 가지입니다.
첫째, /v1/chat/completions, /v1/completions 등 표준 OpenAI 엔드포인트를 지원합니다. 둘째, 스트리밍 응답을 지원하여 긴 답변도 실시간으로 받을 수 있습니다.
셋째, 자동으로 요청을 큐에 넣고 배치 처리하여 높은 처리량을 달성합니다. 이러한 특징들이 개발 시간을 단축하고 기존 인프라와의 호환성을 보장합니다.
코드 예제
# vLLM OpenAI 호환 API 서버 실행
# 터미널에서 다음 명령으로 서버 시작
# python -m vllm.entrypoints.openai.api_server \
# --model ./my-finetuned-llama-7b \
# --host 0.0.0.0 \
# --port 8000
# 클라이언트 코드 - OpenAI 라이브러리 사용
from openai import OpenAI
# vLLM 서버를 가리키도록 base_url만 변경
# API 키는 필수가 아니지만 형식상 필요
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="not-needed" # vLLM은 인증 없이도 작동
)
# ChatGPT와 똑같은 방식으로 호출
response = client.chat.completions.create(
model="./my-finetuned-llama-7b", # 실행 중인 모델 이름
messages=[
{"role": "system", "content": "당신은 친절한 코딩 선생님입니다."},
{"role": "user", "content": "파이썬 리스트 컴프리헨션을 설명해주세요."}
],
temperature=0.7,
max_tokens=500,
stream=True # 스트리밍으로 실시간 응답 받기
)
# 스트리밍 응답 처리 - 답변이 생성되는 대로 출력
for chunk in response:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
설명
이것이 하는 일: vLLM API 서버는 표준 REST API 형식으로 모델에 접근할 수 있게 해주며, OpenAI의 공식 클라이언트 라이브러리를 그대로 활용할 수 있게 합니다. 마치 ChatGPT를 사용하는 것처럼 여러분의 커스텀 모델을 호출할 수 있는 것이죠.
첫 번째로, 서버를 시작할 때 --host 0.0.0.0으로 설정하는 부분이 중요합니다. 이렇게 하면 외부에서도 접근할 수 있는 서버가 되며, 로컬 테스트 시에는 127.0.0.1로만 접근하도록 제한할 수도 있습니다.
--port는 원하는 포트 번호를 지정하는데, 일반적으로 8000이나 8080을 많이 사용합니다. 그 다음으로, 클라이언트 코드에서 base_url을 vLLM 서버 주소로 변경합니다.
이 설정만으로 OpenAI 서버 대신 여러분의 로컬 서버로 요청이 전송됩니다. 내부적으로는 HTTP POST 요청이 /v1/chat/completions 엔드포인트로 전송되며, vLLM이 이를 받아서 모델 추론을 수행합니다.
stream=True 옵션은 매우 유용한 기능입니다. 이것이 활성화되면 모델이 토큰을 생성하는 즉시 클라이언트로 전송되어, 사용자는 답변이 완성되기를 기다리지 않고 실시간으로 확인할 수 있습니다.
ChatGPT 웹사이트에서 답변이 타자기처럼 나타나는 것과 같은 효과를 만들 수 있죠. 여러분이 이 코드를 사용하면 웹 애플리케이션, 모바일 앱, 데스크톱 프로그램 등 어디서든 HTTP로 통신할 수 있는 환경이라면 모델을 호출할 수 있습니다.
실무에서의 이점으로는 첫째, 프론트엔드와 백엔드를 명확히 분리할 수 있어 개발이 용이합니다. 둘째, 로드 밸런서나 API 게이트웨이 같은 기존 인프라를 그대로 활용할 수 있습니다.
셋째, OpenAI에서 제공하는 다양한 SDK(Python, Node.js, Go 등)를 모두 사용할 수 있습니다.
실전 팁
💡 프로덕션 환경에서는 반드시 NGINX나 Caddy 같은 리버스 프록시 뒤에 vLLM을 배치하세요. SSL/TLS 인증서를 적용하고, 레이트 리미팅과 인증 레이어를 추가하여 보안을 강화할 수 있습니다.
💡 --max-num-seqs 파라미터로 동시에 처리할 최대 시퀀스 수를 제한하세요. 기본값이 너무 높으면 메모리가 부족할 수 있으니, GPU 메모리 크기에 맞춰 적절히 조절하는 것이 중요합니다.
💡 스트리밍을 사용할 때는 클라이언트 측에서 타임아웃 설정을 충분히 길게 하세요. 긴 응답의 경우 생성에 시간이 걸릴 수 있어, 기본 타임아웃(보통 30초)으로는 부족할 수 있습니다.
💡 Docker 컨테이너로 배포할 때는 vllm/vllm-openai 공식 이미지를 사용하세요. GPU 드라이버와 CUDA 설정이 이미 되어 있어 환경 설정 시간을 크게 줄일 수 있습니다.
💡 여러 모델을 동시에 서비스하려면 각 모델마다 다른 포트로 서버를 띄우고 NGINX로 경로 기반 라우팅을 설정하세요. 예를 들어 /v1/llama, /v1/mistral 처럼 경로로 구분할 수 있습니다.
3. TGI_Text_Generation_Inference
시작하며
여러분이 Hugging Face에서 모델을 받아서 서비스하는 경우가 많다면, vLLM 외에도 알아두면 정말 유용한 도구가 있습니다. 특히 Hugging Face의 생태계와 긴밀하게 통합되어 있어서 모델을 다운로드하고 실행하는 과정이 한결 쉬워지는 경험을 하게 됩니다.
이런 상황은 특히 오픈소스 모델을 자주 테스트하고 교체해야 하는 개발 환경에서 자주 발생합니다. vLLM도 훌륭하지만, Hugging Face Hub와의 통합, 자동 모델 다운로드, 그리고 다양한 양자화 지원 면에서 더 편리한 선택지가 필요할 때가 있습니다.
바로 이럴 때 필요한 것이 TGI(Text Generation Inference)입니다. Hugging Face에서 직접 만든 배포 도구로, 클릭 몇 번으로 최신 모델을 프로덕션 환경에 올릴 수 있습니다.
개요
간단히 말해서, TGI는 Hugging Face가 공식적으로 제공하는 고성능 텍스트 생성 모델 추론 서버입니다. 왜 TGI를 사용해야 할까요?
Hugging Face Hub에 있는 수만 개의 모델을 모델명만 입력하면 자동으로 다운로드하고 최적화해서 실행해줍니다. 예를 들어, mistralai/Mistral-7B-Instruct-v0.2 같은 모델을 직접 다운로드하고 설정할 필요 없이 명령어 하나로 서비스를 시작할 수 있습니다.
기존에는 모델 파일을 다운로드하고, 적절한 라이브러리를 설치하고, 추론 코드를 작성해야 했다면, 이제는 Docker 컨테이너 하나로 모든 것이 준비됩니다. TGI의 핵심 특징은 네 가지입니다.
첫째, Flash Attention 2와 Paged Attention을 자동으로 적용하여 최고 성능을 냅니다. 둘째, GPTQ, AWQ, GGUF 등 다양한 양자화 형식을 지원합니다.
셋째, 토큰 스트리밍과 배치 처리를 자동으로 최적화합니다. 넷째, 프로메테우스 메트릭과 분산 추적을 기본 제공합니다.
이러한 특징들이 엔터프라이즈급 서비스를 구축할 때 필요한 모든 기능을 제공합니다.
코드 예제
# TGI Docker 컨테이너로 모델 배포
# 터미널에서 다음 명령 실행
# Hugging Face 토큰 설정 (private 모델 사용 시)
export HUGGING_FACE_HUB_TOKEN=your_token_here
# TGI 컨테이너 실행 - GPU 사용
docker run --gpus all --shm-size 1g -p 8080:80 \
-v $PWD/data:/data \
ghcr.io/huggingface/text-generation-inference:latest \
--model-id mistralai/Mistral-7B-Instruct-v0.2 \
--max-input-length 4096 \
--max-total-tokens 8192 \
--max-batch-prefill-tokens 8192
# Python 클라이언트로 TGI 호출
from huggingface_hub import InferenceClient
# TGI 서버에 연결
client = InferenceClient(model="http://localhost:8080")
# 텍스트 생성 요청 - 스트리밍 방식
prompt = "파이썬에서 비동기 프로그래밍의 장점을 설명하면:"
# 실시간으로 응답 받기
for token in client.text_generation(
prompt,
max_new_tokens=500,
temperature=0.7,
stream=True # 토큰이 생성되는 즉시 전송
):
print(token, end="", flush=True)
# 배치 요청도 가능 - 여러 프롬프트 동시 처리
prompts = ["Python이란?", "JavaScript란?", "Rust란?"]
for p in prompts:
response = client.text_generation(p, max_new_tokens=100)
print(f"\n질문: {p}\n답변: {response}\n")
설명
이것이 하는 일: TGI는 Docker 컨테이너로 패키징되어 있어 복잡한 환경 설정 없이 바로 모델 서비스를 시작할 수 있게 해줍니다. 마치 완성된 레스토랑 프랜차이즈를 오픈하는 것처럼, 필요한 모든 것이 이미 준비되어 있습니다.
첫 번째로, docker run 명령에서 --gpus all은 모든 GPU를 컨테이너에서 사용할 수 있게 합니다. --shm-size 1g는 공유 메모리 크기를 설정하는데, 이것이 부족하면 멀티프로세싱 시 에러가 발생할 수 있습니다.
-v $PWD/data:/data는 모델 캐시를 로컬에 저장하여 다음 실행 시 다시 다운로드하지 않도록 합니다. 그 다음으로, --model-id 파라미터에 Hugging Face Hub의 모델 경로를 지정하면 TGI가 자동으로 모델을 다운로드하고 최적 설정을 적용합니다.
--max-input-length와 --max-total-tokens는 메모리 사용량을 제어하는 중요한 파라미터입니다. 값이 클수록 더 긴 문맥을 처리할 수 있지만 메모리도 더 많이 사용합니다.
클라이언트 코드에서 InferenceClient는 TGI 서버와 통신하는 공식 클라이언트입니다. stream=True로 설정하면 서버가 토큰을 생성하는 즉시 클라이언트로 전송되어, 사용자는 전체 응답을 기다리지 않고 실시간으로 결과를 볼 수 있습니다.
이는 사용자 경험을 크게 향상시킵니다. 여러분이 이 코드를 사용하면 Hugging Face Hub의 최신 오픈소스 모델을 몇 분 안에 서비스로 배포할 수 있습니다.
실무에서의 이점으로는 첫째, 모델 교체가 매우 쉬워서 A/B 테스트나 성능 비교가 용이합니다. 둘째, Docker 기반이라 쿠버네티스 같은 오케스트레이션 시스템과 바로 통합됩니다.
셋째, Hugging Face Inference Endpoints와 동일한 API를 사용하여 클라우드와 온프레미스 간 전환이 매끄럽습니다.
실전 팁
💡 양자화된 모델을 사용하려면 --quantize 플래그를 추가하세요. 예를 들어 --quantize bitsandbytes-nf4를 사용하면 메모리를 4분의 1로 줄이면서도 성능 손실은 최소화할 수 있습니다.
💡 여러 GPU로 모델을 분산하려면 --num-shard 옵션을 사용하세요. 큰 모델(70B 이상)은 단일 GPU에 들어가지 않으므로 --num-shard 4처럼 설정하여 4개 GPU에 나누어 로드할 수 있습니다.
💡 프로덕션에서는 --max-concurrent-requests로 동시 처리 요청 수를 제한하세요. 기본값이 너무 높으면 메모리 초과로 서버가 크래시할 수 있으니, GPU 메모리에 맞춰 조절하는 것이 중요합니다.
💡 /metrics 엔드포인트를 활용하여 모니터링을 설정하세요. Prometheus와 Grafana를 연동하면 처리량, 레이턴시, GPU 사용률 등을 실시간으로 확인할 수 있습니다.
💡 커스텀 파인튜닝 모델을 사용할 때는 Hugging Face Hub에 private 모델로 업로드한 후 HUGGING_FACE_HUB_TOKEN 환경변수로 인증하세요. 로컬 경로도 지원하지만 Hub를 사용하면 버전 관리와 팀 협업이 쉬워집니다.
4. Ollama_로컬_모델_관리
시작하며
여러분이 개발용 노트북에서 빠르게 AI 모델을 테스트하고 싶거나, 인터넷 연결 없이도 모델을 실행해야 하는 상황을 겪어본 적 있나요? vLLM과 TGI는 훌륭하지만 서버 환경에 최적화되어 있어서 개인 컴퓨터에서 사용하기에는 다소 복잡할 수 있습니다.
이런 문제는 특히 개발 초기 단계나 로컬에서 프로토타입을 만들 때 자주 발생합니다. Docker 설정, GPU 드라이버, CUDA 버전 등을 신경 쓰다 보면 정작 모델 테스트는 나중 일이 되어버리죠.
바로 이럴 때 필요한 것이 Ollama입니다. 마치 npm이나 pip처럼 간단한 명령어로 AI 모델을 다운로드하고 실행할 수 있는 도구입니다.
개요
간단히 말해서, Ollama는 로컬 컴퓨터에서 대규모 언어 모델을 쉽게 실행하고 관리할 수 있게 해주는 데스크톱 애플리케이션입니다. 왜 Ollama가 필요할까요?
복잡한 설정 없이 명령어 하나로 모델을 다운로드하고 바로 사용할 수 있습니다. 예를 들어, "ollama run llama2"라고 입력하면 Llama 2 모델이 자동으로 다운로드되고 대화를 시작할 수 있습니다.
Docker도, Python 환경도, CUDA 설정도 필요 없습니다. 기존에는 모델 파일을 찾아서 다운로드하고, 적절한 라이브러리를 설치하고, 실행 스크립트를 작성해야 했다면, 이제는 앱 하나 설치하고 명령어 한 줄이면 끝입니다.
Ollama의 핵심 특징은 네 가지입니다. 첫째, Mac, Windows, Linux에서 모두 작동하며 설치가 매우 간단합니다.
둘째, llama2, mistral, codellama 등 인기 모델을 이름만으로 바로 사용할 수 있습니다. 셋째, REST API를 자동으로 제공하여 다른 애플리케이션과 쉽게 연동됩니다.
넷째, Modelfile로 커스텀 모델을 쉽게 만들고 공유할 수 있습니다. 이러한 특징들이 개발자 경험을 극대화하고 진입 장벽을 낮춰줍니다.
코드 예제
# Ollama 기본 사용법 (터미널)
# 모델 다운로드 및 실행 - 대화형 모드
# ollama run llama2
# Python으로 Ollama API 호출
import requests
import json
# Ollama API 엔드포인트 (기본 포트 11434)
url = "http://localhost:11434/api/generate"
# 요청 데이터 준비
data = {
"model": "llama2", # 사용할 모델 이름
"prompt": "파이썬 데코레이터를 초보자도 이해할 수 있게 설명해주세요.",
"stream": False # False면 완성된 응답 한 번에 받기
}
# API 호출
response = requests.post(url, json=data)
# 응답 처리
if response.status_code == 200:
result = response.json()
print(result['response'])
else:
print(f"에러 발생: {response.status_code}")
# 스트리밍 방식으로 실시간 응답 받기
data["stream"] = True
response = requests.post(url, json=data, stream=True)
# 라인별로 처리 - 토큰이 생성되는 즉시 출력
for line in response.iter_lines():
if line:
chunk = json.loads(line)
if 'response' in chunk:
print(chunk['response'], end="", flush=True)
설명
이것이 하는 일: Ollama는 복잡한 AI 모델 실행 과정을 추상화하여 일반 애플리케이션처럼 쉽게 사용할 수 있게 만들어줍니다. 마치 Spotify에서 음악을 재생하듯이, 클릭 몇 번으로 AI 모델을 실행할 수 있습니다.
첫 번째로, Ollama를 설치하면 백그라운드에서 서버가 자동으로 실행됩니다. 이 서버는 11434 포트에서 REST API를 제공하며, 여러분은 이 API를 통해 어떤 프로그래밍 언어로든 모델을 호출할 수 있습니다.
설치는 ollama.ai에서 설치 파일을 다운로드하거나, Mac에서는 brew install ollama로 가능합니다. 그 다음으로, requests.post()로 API를 호출할 때 model 파라미터에 사용할 모델 이름을 지정합니다.
만약 해당 모델이 로컬에 없다면 Ollama가 자동으로 다운로드합니다. prompt는 모델에게 전달할 질문이나 지시사항이며, stream은 응답 방식을 결정합니다.
stream=False일 때는 모델이 전체 응답을 완성한 후 한 번에 반환합니다. 이는 짧은 응답이나 배치 처리에 적합합니다.
반면 stream=True일 때는 토큰이 생성되는 즉시 전송되어, 사용자가 실시간으로 응답을 확인할 수 있습니다. iter_lines()로 각 라인을 순회하면서 점진적으로 출력하는 방식입니다.
여러분이 이 코드를 사용하면 로컬에서 완전히 독립적으로 AI 기능을 개발할 수 있습니다. 실무에서의 이점으로는 첫째, 인터넷 연결이 불안정한 환경에서도 작동합니다.
둘째, 데이터가 외부로 전송되지 않아 프라이버시가 보장됩니다. 셋째, API 비용 걱정 없이 무제한으로 테스트할 수 있습니다.
넷째, 개발 초기 단계에서 빠른 프로토타이핑이 가능합니다.
실전 팁
💡 ollama list 명령으로 다운로드된 모델 목록을 확인하고, ollama rm <model>로 불필요한 모델을 삭제하여 디스크 공간을 관리하세요. 각 모델이 수 GB씩 차지하므로 정기적인 정리가 필요합니다.
💡 Modelfile을 사용하여 커스텀 모델을 만들 수 있습니다. 기존 모델에 시스템 프롬프트를 추가하거나 파라미터를 조정한 후 ollama create my-model -f Modelfile로 새 모델을 생성하세요.
💡 LangChain이나 LlamaIndex와 통합할 때는 Ollama 클래스를 사용하세요. from langchain.llms import Ollama 후 llm = Ollama(model="llama2")로 간단히 연동됩니다.
💡 파인튜닝한 GGUF 형식 모델을 Ollama로 가져올 수 있습니다. Modelfile에서 FROM ./my-model.gguf를 지정하면 로컬 모델 파일을 Ollama 모델로 변환할 수 있습니다.
💡 여러 모델을 동시에 실행할 수 있지만 메모리를 많이 사용합니다. ollama ps로 현재 실행 중인 모델을 확인하고, ollama stop <model>로 사용하지 않는 모델을 종료하여 리소스를 절약하세요.
5. 모델_양자화와_최적화
시작하며
여러분이 70억 파라미터 모델을 배포하려는데 GPU 메모리가 부족해서 실행조차 안 되는 경험을 해본 적 있나요? 또는 모델이 실행은 되는데 너무 느려서 실제 서비스로 쓰기 힘든 상황에 처한 적이 있나요?
이런 문제는 AI 모델 배포의 가장 큰 걸림돌입니다. 좋은 모델을 만들었어도 하드웨어 제약 때문에 서비스하지 못하거나, 비싼 GPU를 여러 대 구매해야 하는 상황이 발생합니다.
특히 스타트업이나 개인 개발자에게는 큰 부담이죠. 바로 이럴 때 필요한 것이 모델 양자화입니다.
마치 4K 영상을 1080p로 변환하여 용량을 줄이듯이, 모델의 크기를 줄이면서도 성능은 거의 유지하는 기술입니다.
개요
간단히 말해서, 모델 양자화는 모델의 가중치를 더 적은 비트로 표현하여 메모리 사용량과 연산 속도를 개선하는 기술입니다. 왜 양자화가 필요할까요?
기본적으로 모델은 32비트 또는 16비트 부동소수점으로 저장되는데, 이를 8비트, 4비트, 심지어 2비트로 줄이면 메모리를 절반에서 8분의 1까지 줄일 수 있습니다. 예를 들어, 16비트로 28GB를 차지하는 Llama 2 13B 모델을 4비트로 양자화하면 7GB로 줄어들어 일반 게이밍 GPU에서도 실행 가능합니다.
기존에는 큰 모델을 실행하려면 비싼 A100 GPU가 필요했다면, 이제는 양자화를 통해 RTX 3090이나 4090 같은 소비자용 GPU에서도 실행할 수 있습니다. 양자화의 핵심 기법은 네 가지입니다.
첫째, GPTQ는 학습 데이터를 사용하여 최적의 양자화 파라미터를 찾습니다. 둘째, AWQ는 중요한 가중치는 보존하고 덜 중요한 가중치만 양자화합니다.
셋째, GGUF는 CPU에서도 효율적으로 실행되도록 설계된 형식입니다. 넷째, bitsandbytes는 동적 양자화로 추론 시점에 실시간으로 양자화합니다.
이러한 기법들이 각기 다른 상황에서 최적의 성능을 제공합니다.
코드 예제
# AutoGPTQ로 모델 양자화하기
from transformers import AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
# 양자화 설정 - 4비트로 압축
quantize_config = BaseQuantizeConfig(
bits=4, # 4비트 양자화
group_size=128, # 가중치를 128개씩 묶어서 양자화
desc_act=False, # activation 순서 최적화
)
# 원본 모델 로드
model = AutoGPTQForCausalLM.from_pretrained(
"./my-finetuned-llama-7b",
quantize_config=quantize_config
)
tokenizer = AutoTokenizer.from_pretrained("./my-finetuned-llama-7b")
# 양자화 수행 - 보정 데이터 필요
# 보정 데이터는 모델이 처리할 대표적인 텍스트 샘플
calibration_data = [
"파이썬 프로그래밍의 기초",
"머신러닝 알고리즘 설명",
"웹 개발 베스트 프랙티스"
]
# 실제 양자화 실행 - 시간이 다소 걸림
model.quantize(calibration_data)
# 양자화된 모델 저장
model.save_quantized("./my-model-gptq-4bit")
tokenizer.save_pretrained("./my-model-gptq-4bit")
# vLLM으로 양자화 모델 로드
from vllm import LLM
# 양자화된 모델은 자동으로 인식됨
llm = LLM(model="./my-model-gptq-4bit", quantization="gptq")
설명
이것이 하는 일: 양자화는 모델의 수치 정밀도를 낮춰서 메모리와 연산량을 줄이는 과정입니다. 마치 고화질 사진을 적절히 압축하여 용량을 줄이되 육안으로는 차이를 느끼기 어렵게 만드는 것과 비슷합니다.
첫 번째로, BaseQuantizeConfig에서 bits=4로 설정하는 것이 핵심입니다. 이것은 각 가중치를 4비트(16개 값 중 하나)로 표현하겠다는 의미입니다.
원래 16비트(65,536개 값)였던 것을 대폭 줄이는 것이죠. group_size=128은 128개의 가중치를 하나의 그룹으로 묶어서 함께 양자화한다는 의미로, 이렇게 하면 정확도 손실을 최소화할 수 있습니다.
그 다음으로, calibration_data는 양자화 과정에서 중요한 역할을 합니다. 모델이 이 데이터를 처리하면서 어떤 가중치가 중요한지 파악하고, 중요한 부분은 더 정밀하게 유지합니다.
실제로는 여러분의 실제 사용 사례를 대표하는 100~1000개 정도의 샘플을 사용하는 것이 좋습니다. model.quantize()가 실행되면 내부적으로 각 레이어를 순회하면서 가중치 행렬을 분석하고 최적의 양자화 파라미터를 계산합니다.
이 과정에서 재구성 오류(reconstruction error)를 최소화하는 방향으로 양자화가 진행됩니다. 시간이 걸리지만 한 번만 수행하면 되고, 결과물은 계속 재사용할 수 있습니다.
여러분이 이 코드를 사용하면 13B, 70B 같은 큰 모델도 일반 GPU에서 실행할 수 있게 됩니다. 실무에서의 이점으로는 첫째, 하드웨어 비용을 크게 절감할 수 있습니다(A100 대신 RTX 4090 사용).
둘째, 추론 속도가 빨라져 더 많은 사용자를 처리할 수 있습니다. 셋째, 배치 크기를 늘릴 수 있어 처리량이 증가합니다.
넷째, 에지 디바이스나 모바일에서도 모델 실행이 가능해집니다.
실전 팁
💡 양자화 전후로 성능을 반드시 비교 테스트하세요. lm-evaluation-harness 같은 도구로 벤치마크를 돌려보고, 정확도 하락이 5% 이내인지 확인하는 것이 중요합니다.
💡 GPTQ는 추론 성능이 우수하고, AWQ는 정확도 손실이 적으며, GGUF는 CPU 실행에 최적화되어 있습니다. 여러분의 배포 환경(GPU/CPU, 메모리 제약)에 맞는 기법을 선택하세요.
💡 Hugging Face Hub에서 이미 양자화된 모델을 찾아보세요. TheBloke 같은 사용자가 인기 모델의 다양한 양자화 버전을 공유하고 있어, 직접 양자화하지 않아도 됩니다.
💡 mixed precision 양자화를 고려하세요. 중요한 레이어(첫 번째와 마지막 레이어)는 8비트로, 중간 레이어는 4비트로 설정하면 성능과 크기의 균형을 맞출 수 있습니다.
💡 양자화 모델을 배포할 때는 추론 프레임워크가 해당 형식을 지원하는지 확인하세요. vLLM은 GPTQ와 AWQ를, TGI는 GPTQ와 bitsandbytes를, Ollama는 GGUF를 지원합니다.
6. Docker_컨테이너_배포
시작하며
여러분이 로컬에서는 완벽하게 작동하는 모델 서비스를 만들었는데, 프로덕션 서버에 배포하려니 Python 버전이 다르고, CUDA 버전이 맞지 않고, 라이브러리 의존성이 꼬이는 악몽 같은 상황을 겪어본 적 있나요? 이런 문제는 "내 컴퓨터에서는 잘 되는데요" 증후군으로 불리는 개발자들의 영원한 숙제입니다.
환경 차이로 인한 문제는 디버깅하기도 어렵고, 팀원마다 다른 환경을 사용하면 협업도 어려워집니다. 바로 이럴 때 필요한 것이 Docker 컨테이너입니다.
모델과 실행 환경을 하나의 패키지로 묶어서 어디서든 동일하게 실행할 수 있게 만들어줍니다.
개요
간단히 말해서, Docker는 애플리케이션과 그것이 필요로 하는 모든 것(라이브러리, 설정, 환경 변수)을 하나의 컨테이너로 패키징하는 기술입니다. 왜 Docker로 배포해야 할까요?
한 번 만든 Docker 이미지는 개발 노트북, 테스트 서버, 프로덕션 클라우드 어디서든 똑같이 작동합니다. 예를 들어, 여러분의 로컬 Mac에서 만든 컨테이너를 AWS GPU 인스턴스에 그대로 배포할 수 있고, 동료가 Windows에서도 동일하게 실행할 수 있습니다.
기존에는 서버마다 CUDA, cuDNN, Python, 각종 라이브러리를 일일이 설치하고 버전을 맞춰야 했다면, 이제는 Docker 이미지 하나로 모든 환경을 표준화할 수 있습니다. Docker 배포의 핵심 특징은 네 가지입니다.
첫째, 환경 격리로 여러 모델을 각자의 환경에서 실행할 수 있습니다. 둘째, 버전 관리로 이미지를 태그하여 롤백이 쉽습니다.
셋째, 스케일링이 용이하여 컨테이너 개수를 늘려 부하를 분산할 수 있습니다. 넷째, CI/CD 파이프라인과 자연스럽게 통합됩니다.
이러한 특징들이 현대적인 클라우드 네이티브 배포의 기반이 됩니다.
코드 예제
# Dockerfile - vLLM 모델 서비스 컨테이너화
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
# 필요한 패키지 설치
RUN apt-get update && apt-get install -y \
python3.10 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
# vLLM 설치
RUN pip3 install vllm
# 작업 디렉토리 설정
WORKDIR /app
# 모델 파일 복사 (또는 컨테이너 실행 시 마운트)
COPY ./my-finetuned-model /app/model
# 환경 변수 설정
ENV MODEL_PATH=/app/model
ENV PORT=8000
# vLLM 서버 실행 스크립트
CMD python3 -m vllm.entrypoints.openai.api_server \
--model $MODEL_PATH \
--host 0.0.0.0 \
--port $PORT \
--gpu-memory-utilization 0.9
# Docker 이미지 빌드 (터미널에서 실행)
# docker build -t my-llm-service:v1.0 .
# 컨테이너 실행 - GPU 사용
# docker run --gpus all -p 8000:8000 \
# -v $(pwd)/models:/app/model \
# my-llm-service:v1.0
# Docker Compose로 여러 서비스 관리 (docker-compose.yml)
# version: '3.8'
# services:
# llm-service:
# image: my-llm-service:v1.0
# deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: 1
# capabilities: [gpu]
# ports:
# - "8000:8000"
# volumes:
# - ./models:/app/model
# environment:
# - MODEL_PATH=/app/model
# - GPU_MEMORY_UTIL=0.9
설명
이것이 하는 일: Dockerfile은 컨테이너를 만드는 레시피입니다. 이 파일에 적힌 순서대로 이미지가 빌드되며, 완성된 이미지는 실행 가능한 독립적인 패키지가 됩니다.
마치 요리 레시피를 따라하면 누구나 같은 요리를 만들 수 있듯이, Dockerfile로 누구나 같은 환경을 만들 수 있습니다. 첫 번째로, FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04는 베이스 이미지를 지정합니다.
이것은 이미 CUDA가 설치된 Ubuntu 이미지로, GPU를 사용하는 애플리케이션의 기초가 됩니다. 직접 CUDA를 설치할 필요 없이 검증된 환경을 바로 사용할 수 있어 시간과 노력을 크게 절약합니다.
그 다음으로, RUN 명령어들이 실행되면서 필요한 패키지를 설치합니다. apt-get update로 패키지 목록을 업데이트하고, python3와 pip를 설치합니다.
rm -rf /var/lib/apt/lists/*는 캐시를 삭제하여 이미지 크기를 줄이는 최적화 기법입니다. 이미지가 작을수록 다운로드와 배포가 빠르기 때문에 중요합니다.
COPY 명령은 로컬의 모델 파일을 컨테이너 안으로 복사합니다. 하지만 실제로는 모델이 수십 GB로 크기 때문에 이미지에 포함시키기보다는 실행 시 볼륨 마운트(-v 옵션)를 사용하는 것이 일반적입니다.
이렇게 하면 같은 모델을 여러 컨테이너에서 공유할 수 있습니다. 여러분이 이 코드를 사용하면 일관된 배포 환경을 구축할 수 있습니다.
실무에서의 이점으로는 첫째, 새로운 서버를 추가할 때 설치 스크립트나 문서가 필요 없습니다. 둘째, 쿠버네티스 같은 오케스트레이션 시스템과 바로 통합됩니다.
셋째, Blue-Green 배포, Canary 배포 같은 고급 배포 전략을 쉽게 구현할 수 있습니다. 넷째, 로컬에서 프로덕션과 동일한 환경으로 테스트할 수 있어 "내 컴퓨터에서는 되는데" 문제가 사라집니다.
실전 팁
💡 멀티스테이지 빌드를 사용하여 이미지 크기를 줄이세요. 빌드 도구는 첫 번째 스테이지에서만 사용하고, 실행에 필요한 파일만 최종 이미지로 복사하면 크기를 절반 이하로 줄일 수 있습니다.
💡 .dockerignore 파일로 불필요한 파일이 이미지에 포함되지 않도록 하세요. node_modules, pycache, .git 같은 폴더는 제외하면 빌드 속도가 크게 향상됩니다.
💡 헬스체크를 추가하여 컨테이너가 정상 작동하는지 확인하세요. HEALTHCHECK CMD curl -f http://localhost:8000/health || exit 1을 추가하면 오케스트레이션 시스템이 자동으로 문제를 감지하고 재시작합니다.
💡 레이어 캐싱을 활용하세요. 자주 변경되지 않는 명령(패키지 설치)은 위에, 자주 변경되는 명령(코드 복사)은 아래에 배치하면 재빌드 시간이 단축됩니다.
💡 프로덕션에서는 latest 태그 대신 명시적인 버전 태그를 사용하세요. my-llm-service:v1.2.3처럼 구체적인 버전을 지정하면 예상치 못한 업데이트를 방지하고 롤백이 쉬워집니다.
7. 성능_모니터링과_로깅
시작하며
여러분이 모델을 프로덕션에 배포했는데 갑자기 응답 속도가 느려지거나, 어떤 요청은 실패하는데 왜 그런지 알 수 없는 상황을 겪어본 적 있나요? 사용자들은 불만을 토로하는데 정작 어디서 문제가 생기는지 파악하기 어려운 경우가 많죠.
이런 문제는 모니터링과 로깅이 제대로 설정되지 않았을 때 발생합니다. AI 서비스는 특히 GPU 메모리, 처리 시간, 토큰 생성 속도 등 일반 웹 서비스와는 다른 메트릭들을 추적해야 하는데, 이를 간과하면 문제 발생 시 대응이 어렵습니다.
바로 이럴 때 필요한 것이 체계적인 모니터링과 로깅 시스템입니다. 실시간으로 서비스 상태를 파악하고, 문제가 생기면 즉시 원인을 찾을 수 있게 해줍니다.
개요
간단히 말해서, 모니터링은 서비스의 건강 상태를 실시간으로 추적하는 것이고, 로깅은 무슨 일이 일어났는지 기록을 남기는 것입니다. 왜 이것들이 중요할까요?
AI 모델 서비스는 GPU 사용률, 배치 크기, 토큰 생성 속도, 큐 대기 시간 등 다양한 지표들이 성능에 영향을 줍니다. 예를 들어, GPU 메모리가 거의 다 찼는데 요청이 계속 들어오면 OOM 에러가 발생하는데, 모니터링 없이는 이를 미리 알 수 없습니다.
기존에는 문제가 발생한 후에야 로그를 뒤지며 원인을 찾아야 했다면, 이제는 실시간 대시보드로 문제를 예측하고 사전에 대응할 수 있습니다. 핵심 요소는 네 가지입니다.
첫째, 메트릭 수집(Prometheus)으로 수치 데이터를 시계열로 저장합니다. 둘째, 시각화(Grafana)로 대시보드를 만들어 한눈에 상태를 파악합니다.
셋째, 구조화된 로깅(JSON 형식)으로 검색과 분석이 쉽도록 합니다. 넷째, 알림(Alert)으로 임계값 초과 시 자동으로 통지합니다.
이러한 요소들이 안정적인 서비스 운영의 기반이 됩니다.
코드 예제
# Python 로깅 설정 - 구조화된 JSON 로깅
import logging
import json
from datetime import datetime
from prometheus_client import Counter, Histogram, Gauge, start_http_server
# JSON 포맷터 - 로그를 구조화된 형태로 저장
class JsonFormatter(logging.Formatter):
def format(self, record):
log_data = {
"timestamp": datetime.utcnow().isoformat(),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName
}
return json.dumps(log_data)
# 로거 설정
logger = logging.getLogger("llm_service")
logger.setLevel(logging.INFO)
handler = logging.FileHandler("llm_service.log")
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
# Prometheus 메트릭 정의
REQUEST_COUNT = Counter("llm_requests_total", "Total requests", ["model", "status"])
REQUEST_DURATION = Histogram("llm_request_duration_seconds", "Request duration")
GPU_MEMORY = Gauge("gpu_memory_used_bytes", "GPU memory usage")
ACTIVE_REQUESTS = Gauge("active_requests", "Currently processing requests")
# Prometheus 메트릭 서버 시작 (포트 8001)
start_http_server(8001)
# 추론 함수에 모니터링 추가
@REQUEST_DURATION.time() # 자동으로 실행 시간 측정
def generate_text(prompt, model_name):
ACTIVE_REQUESTS.inc() # 활성 요청 수 증가
try:
logger.info(f"Starting generation for prompt: {prompt[:50]}...")
# 실제 모델 추론 (예시)
# response = model.generate(prompt)
response = "생성된 텍스트 예시"
# 성공 메트릭 기록
REQUEST_COUNT.labels(model=model_name, status="success").inc()
logger.info(f"Generation completed. Length: {len(response)}")
return response
except Exception as e:
# 실패 메트릭 기록
REQUEST_COUNT.labels(model=model_name, status="error").inc()
logger.error(f"Generation failed: {str(e)}", exc_info=True)
raise
finally:
ACTIVE_REQUESTS.dec() # 활성 요청 수 감소
설명
이것이 하는 일: 이 코드는 모델 서비스의 모든 중요한 이벤트와 지표를 기록하고 추적할 수 있게 해줍니다. 마치 자동차의 계기판처럼, 서비스의 현재 상태를 숫자와 그래프로 보여주고 문제가 생기면 경고등이 켜지는 것입니다.
첫 번째로, JsonFormatter는 로그를 사람이 읽기 쉬운 텍스트가 아닌 JSON 형식으로 저장합니다. 왜 이렇게 할까요?
JSON 형식이면 나중에 ElasticSearch나 Splunk 같은 로그 분석 도구로 쉽게 검색하고 집계할 수 있기 때문입니다. "2024년 1월에 발생한 모든 에러를 찾아줘" 같은 복잡한 쿼리도 빠르게 처리됩니다.
그 다음으로, Prometheus 메트릭들이 정의됩니다. Counter는 계속 증가만 하는 값(총 요청 수), Histogram은 분포를 추적(응답 시간의 평균, 중앙값, 99%타일), Gauge는 증가와 감소가 모두 가능한 값(현재 GPU 메모리 사용량)입니다.
각 메트릭 타입은 특정 목적에 최적화되어 있습니다. @REQUEST_DURATION.time() 데코레이터는 함수 실행 시간을 자동으로 측정합니다.
함수가 시작될 때 타이머를 시작하고, 끝날 때 경과 시간을 Histogram에 기록합니다. 이렇게 수집된 데이터로 평균 응답 시간, 가장 느린 요청 등을 분석할 수 있죠.
labels(model=..., status=...)는 메트릭에 태그를 붙여서 모델별, 상태별로 따로 집계할 수 있게 합니다. 여러분이 이 코드를 사용하면 서비스의 건강 상태를 실시간으로 파악할 수 있습니다.
실무에서의 이점으로는 첫째, 문제가 발생하기 전에 경고를 받아 사전 대응할 수 있습니다(예: GPU 메모리 90% 도달 시 알림). 둘째, 성능 병목 지점을 정확히 찾아낼 수 있습니다(어떤 모델이 느린지, 어떤 시간대에 요청이 몰리는지).
셋째, SLA 달성 여부를 객관적으로 측정할 수 있습니다. 넷째, 장애 발생 시 정확한 시점과 원인을 빠르게 특정할 수 있습니다.
실전 팁
💡 Grafana 대시보드에서 P50, P95, P99 백분위수를 모니터링하세요. 평균만 보면 느린 요청을 놓칠 수 있으니, 99%의 사용자가 경험하는 최대 응답 시간을 추적하는 것이 중요합니다.
💡 로그 레벨을 환경에 따라 조절하세요. 개발 환경에서는 DEBUG, 프로덕션에서는 INFO 이상만 기록하여 로그 볼륨을 관리하고 성능 오버헤드를 줄이세요.
💡 분산 추적(Distributed Tracing)을 OpenTelemetry로 구현하세요. 요청이 여러 마이크로서비스를 거칠 때 전체 경로를 추적할 수 있어, 어느 구간에서 지연이 발생하는지 정확히 알 수 있습니다.
💡 알림 피로도를 방지하세요. 너무 많은 알림은 오히려 역효과를 냅니다. 중요한 메트릭만 알림을 설정하고, 알림 발생 시 정확히 무엇을 해야 하는지 문서화된 runbook을 준비하세요.
💡 GPU 관련 메트릭은 nvidia-smi를 정기적으로 파싱하거나 DCGM Exporter를 사용하세요. GPU 온도, 전력 사용량, 활용률 등을 Prometheus로 수집하면 하드웨어 문제를 조기에 발견할 수 있습니다.
8. 로드_밸런싱과_스케일링
시작하며
여러분의 AI 서비스가 인기를 얻어서 사용자가 폭발적으로 늘어났는데, 서버 하나로는 모든 요청을 처리할 수 없는 상황을 상상해보세요. 응답이 점점 느려지고, 일부 사용자는 타임아웃 에러를 경험하게 됩니다.
이런 문제는 성공적인 서비스라면 반드시 맞닥뜨리게 되는 행복한 고민입니다. 하지만 대응하지 못하면 사용자 이탈로 이어질 수 있어 빠른 조치가 필요합니다.
단순히 서버를 추가한다고 해결되는 것이 아니라 트래픽을 효율적으로 분산하는 메커니즘이 필요하죠. 바로 이럴 때 필요한 것이 로드 밸런싱과 스케일링입니다.
여러 서버를 운영하면서 요청을 고르게 분배하고, 트래픽에 따라 서버 개수를 자동으로 조절하는 기술입니다.
개요
간단히 말해서, 로드 밸런싱은 들어오는 요청을 여러 서버에 분산하는 것이고, 스케일링은 부하에 따라 서버 개수를 늘리거나 줄이는 것입니다. 왜 이것들이 필요할까요?
AI 모델 추론은 GPU를 사용하는 무거운 작업이라 한 서버로는 한계가 있습니다. 예를 들어, 한 대의 GPU 서버가 초당 10개 요청을 처리할 수 있다면, 100개 요청이 들어오면 10대의 서버가 필요합니다.
로드 밸런서가 없으면 특정 서버만 과부하되고 나머지는 놀게 되죠. 기존에는 서버 용량을 미리 예측해서 고정된 개수로 운영해야 했다면, 이제는 실시간 트래픽에 따라 자동으로 서버를 추가하거나 제거할 수 있습니다.
핵심 기법은 네 가지입니다. 첫째, NGINX나 HAProxy로 HTTP 레벨 로드 밸런싱을 구현합니다.
둘째, Round Robin, Least Connections 같은 알고리즘으로 요청을 분배합니다. 셋째, Kubernetes HPA(Horizontal Pod Autoscaler)로 자동 스케일링을 설정합니다.
넷째, Health Check로 문제 있는 서버를 자동으로 제외합니다. 이러한 기법들이 안정적이고 확장 가능한 서비스를 만듭니다.
코드 예제
# NGINX 로드 밸런서 설정 (nginx.conf)
# upstream 블록에 백엔드 서버들 정의
# upstream llm_backend {
# least_conn; # 가장 연결이 적은 서버로 전달
# server 192.168.1.10:8000 max_fails=3 fail_timeout=30s;
# server 192.168.1.11:8000 max_fails=3 fail_timeout=30s;
# server 192.168.1.12:8000 max_fails=3 fail_timeout=30s;
# keepalive 32; # 연결 재사용
# }
#
# server {
# listen 80;
#
# location /v1/ {
# proxy_pass http://llm_backend;
# proxy_http_version 1.1;
# proxy_set_header Connection "";
# proxy_read_timeout 300s; # 긴 응답 대기 시간
# }
# }
# Kubernetes HPA 설정 - 자동 스케일링 (hpa.yaml)
# apiVersion: autoscaling/v2
# kind: HorizontalPodAutoscaler
# metadata:
# name: llm-service-hpa
# spec:
# scaleTargetRef:
# apiVersion: apps/v1
# kind: Deployment
# name: llm-service
# minReplicas: 2 # 최소 Pod 개수
# maxReplicas: 10 # 최대 Pod 개수
# metrics:
# - type: Resource
# resource:
# name: cpu
# target:
# type: Utilization
# averageUtilization: 70 # CPU 70% 넘으면 스케일 아웃
# - type: Pods
# pods:
# metric:
# name: active_requests
# target:
# type: AverageValue
# averageValue: "10" # Pod당 평균 10개 요청 유지
# Python 클라이언트 - 로드 밸런서 통해 요청
import requests
from concurrent.futures import ThreadPoolExecutor
# 로드 밸런서 엔드포인트 (여러 백엔드 서버 뒤)
LOAD_BALANCER_URL = "http://lb.example.com/v1/chat/completions"
def send_request(prompt_id):
"""단일 요청 전송 - 로드 밸런서가 자동으로 서버 선택"""
response = requests.post(
LOAD_BALANCER_URL,
json={
"model": "llama2",
"messages": [{"role": "user", "content": f"질문 {prompt_id}"}]
},
timeout=60
)
return response.json()
# 동시에 여러 요청 전송 - 자동으로 분산 처리됨
with ThreadPoolExecutor(max_workers=50) as executor:
results = list(executor.map(send_request, range(100)))
print(f"완료된 요청: {len(results)}")
설명
이것이 하는 일: 로드 밸런서는 클라이언트와 서버 사이의 중개자 역할을 합니다. 마치 은행 창구 안내 직원이 고객을 가장 덜 바쁜 창구로 안내하듯이, 요청을 가장 여유 있는 서버로 보내줍니다.
첫 번째로, NGINX의 upstream 블록에서 least_conn 알고리즘을 사용합니다. 이것은 현재 연결 수가 가장 적은 서버로 새 요청을 보내는 방식입니다.
AI 모델 추론처럼 각 요청의 처리 시간이 다를 때 Round Robin보다 효율적입니다. max_fails=3 fail_timeout=30s는 3번 연속 실패하면 30초 동안 해당 서버를 제외하는 설정으로, 장애 서버로 요청이 가는 것을 방지합니다.
그 다음으로, Kubernetes HPA(Horizontal Pod Autoscaler)가 메트릭을 기반으로 Pod 개수를 자동 조절합니다. CPU 사용률이 70%를 넘으면 Pod를 추가하고, 낮아지면 제거합니다.
또한 커스텀 메트릭인 active_requests(Prometheus에서 수집)를 사용하여 각 Pod가 평균 10개 요청을 처리하도록 유지합니다. 이렇게 하면 GPU 활용률을 최적화할 수 있습니다.
proxy_read_timeout 300s는 매우 중요한 설정입니다. AI 모델 응답은 일반 웹 요청보다 오래 걸릴 수 있는데, 기본 타임아웃(60초)으로는 부족할 수 있습니다.
특히 긴 텍스트 생성이나 복잡한 추론은 수 분이 걸릴 수도 있어 충분한 시간을 주어야 합니다. 여러분이 이 설정을 사용하면 트래픽 급증에도 안정적으로 대응할 수 있습니다.
실무에서의 이점으로는 첫째, 한 서버에 장애가 생겨도 서비스는 계속됩니다(고가용성). 둘째, 비용 효율적으로 운영할 수 있습니다(야간에는 서버 줄이고 피크 시간에 늘림).
셋째, 무중단 배포가 가능합니다(한 서버씩 업데이트). 넷째, 지리적으로 분산된 사용자에게 가까운 서버로 라우팅할 수 있습니다.
실전 팁
💡 AI 모델 서비스에는 Least Connections 알고리즘이 Round Robin보다 효과적입니다. 요청마다 처리 시간이 크게 다르기 때문에 단순히 번갈아 보내는 것보다 부하를 고려하는 것이 중요합니다.
💡 스티키 세션(Sticky Session)은 AI 서비스에서는 일반적으로 불필요합니다. 각 요청이 독립적이므로 같은 사용자가 다른 서버로 가도 문제없으며, 오히려 부하 분산이 더 고르게 됩니다.
💡 HPA의 스케일 업/다운 속도를 조절하세요. --horizontal-pod-autoscaler-upscale-delay와 --horizontal-pod-autoscaler-downscale-delay로 너무 빈번한 스케일링을 방지할 수 있습니다. GPU 인스턴스는 시작이 느리므로 조금 여유 있게 설정하는 것이 좋습니다.
💡 프리워밍(Pre-warming) 전략을 사용하세요. 새로 추가된 Pod는 모델을 로드하는 데 시간이 걸리므로, readinessProbe로 준비가 완료된 후에만 트래픽을 받도록 설정하세요.
💡 지리적 로드 밸런싱(GeoDNS)을 고려하세요. 글로벌 서비스라면 사용자와 가까운 리전의 서버로 연결하여 레이턴시를 크게 줄일 수 있습니다. AWS Route 53, Cloudflare 등이 이 기능을 제공합니다.
9. 보안과_인증
시작하며
여러분이 공개 인터넷에 AI 모델 서비스를 배포했는데, 누구나 무제한으로 사용할 수 있다면 어떤 일이 벌어질까요? 악의적인 사용자가 무한 요청을 보내 서버를 마비시키거나, API를 도용하여 자신의 서비스에 무료로 사용할 수 있습니다.
이런 문제는 보안과 인증이 없는 서비스에서 반드시 발생합니다. GPU 리소스는 비용이 많이 들기 때문에 무단 사용은 곧 금전적 손실로 이어지고, 민감한 데이터가 모델에 입력될 경우 프라이버시 문제도 발생할 수 있습니다.
바로 이럴 때 필요한 것이 API 키 인증, 레이트 리미팅, TLS 암호화 같은 보안 조치입니다. 서비스를 안전하게 보호하면서도 정당한 사용자에게는 원활한 접근을 제공하는 균형이 필요합니다.
개요
간단히 말해서, 보안과 인증은 누가 여러분의 API를 사용할 수 있는지 제어하고, 데이터를 안전하게 전송하며, 남용을 방지하는 모든 조치를 의미합니다. 왜 이것이 중요할까요?
AI 모델 서비스는 컴퓨팅 비용이 높기 때문에 무단 사용은 직접적인 금전 손실입니다. 예를 들어, 하루에 1,000,000개의 무단 요청이 들어온다면 GPU 비용만 수백만 원이 나갈 수 있습니다.
또한 GDPR 같은 규정을 준수하려면 데이터 암호화와 접근 제어가 필수입니다. 기존에는 보안을 나중 문제로 미루다가 사고가 발생한 후에야 조치하는 경우가 많았다면, 이제는 처음부터 설계 단계에서 보안을 고려하는 것이 표준입니다.
핵심 보안 조치는 네 가지입니다. 첫째, API 키 인증으로 승인된 사용자만 접근하게 합니다.
둘째, 레이트 리미팅으로 과도한 요청을 차단합니다. 셋째, TLS/SSL로 데이터를 암호화하여 전송합니다.
넷째, 입력 검증으로 악의적인 프롬프트를 필터링합니다. 이러한 조치들이 안전하고 신뢰할 수 있는 서비스의 기반입니다.
코드 예제
# FastAPI로 보안 API 서버 구축
from fastapi import FastAPI, HTTPException, Depends, Security
from fastapi.security import APIKeyHeader
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
import hashlib
import secrets
app = FastAPI()
# 레이트 리미터 설정 - IP 기반
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# API 키 검증
API_KEY_HEADER = APIKeyHeader(name="X-API-Key")
VALID_API_KEYS = {
"sk_live_abc123": {"user": "user1", "tier": "premium"},
"sk_live_def456": {"user": "user2", "tier": "free"}
}
async def verify_api_key(api_key: str = Security(API_KEY_HEADER)):
"""API 키 검증 및 사용자 정보 반환"""
if api_key not in VALID_API_KEYS:
raise HTTPException(status_code=403, detail="Invalid API key")
return VALID_API_KEYS[api_key]
# 입력 검증 - 악의적인 프롬프트 차단
def validate_prompt(prompt: str):
"""위험한 패턴 감지"""
dangerous_patterns = ["ignore previous", "system:", "你好"]
if len(prompt) > 10000: # 너무 긴 입력 차단
raise HTTPException(status_code=400, detail="Prompt too long")
if any(pattern in prompt.lower() for pattern in dangerous_patterns):
raise HTTPException(status_code=400, detail="Invalid prompt")
return True
# 보호된 엔드포인트
@app.post("/v1/generate")
@limiter.limit("10/minute") # 분당 10 요청으로 제한
async def generate(
prompt: str,
user_info: dict = Depends(verify_api_key)
):
"""인증 및 레이트 리미팅이 적용된 생성 API"""
# 입력 검증
validate_prompt(prompt)
# 사용자 티어에 따른 제한
if user_info["tier"] == "free" and len(prompt) > 500:
raise HTTPException(status_code=403, detail="Prompt too long for free tier")
# 실제 모델 추론 (예시)
# response = model.generate(prompt)
response = f"Generated response for {user_info['user']}"
return {"response": response, "user": user_info["user"]}
# TLS 인증서로 HTTPS 서버 실행 (터미널)
# uvicorn main:app --host 0.0.0.0 --port 443 \
# --ssl-keyfile ./key.pem \
# --ssl-certfile ./cert.pem
설명
이것이 하는 일: 이 코드는 여러 보안 레이어를 구축하여 API를 보호합니다. 마치 은행의 보안 시스템처럼, 신원 확인(인증), 접근 제한(레이트 리미팅), 금고(암호화), 감시 카메라(로깅)가 모두 작동하는 것입니다.
첫 번째로, APIKeyHeader는 HTTP 헤더에서 X-API-Key를 추출합니다. 클라이언트는 요청 시마다 이 헤더에 자신의 API 키를 포함시켜야 하며, verify_api_key 함수가 이를 검증합니다.
유효하지 않은 키면 403 Forbidden 에러를 반환하여 접근을 차단합니다. API 키는 secrets.token_urlsafe(32)로 생성하여 추측 불가능하게 만드는 것이 중요합니다.
그 다음으로, @limiter.limit("10/minute") 데코레이터가 레이트 리미팅을 적용합니다. 같은 IP에서 1분에 10번 이상 요청하면 자동으로 429 Too Many Requests 에러를 반환합니다.
이것은 DDoS 공격이나 무분별한 크롤링을 방지하며, GPU 리소스를 공정하게 분배합니다. 프리미엄 사용자에게는 더 높은 한도를 주는 것도 가능합니다.
validate_prompt 함수는 프롬프트 인젝션 공격을 방지합니다. "ignore previous instructions"나 "system:" 같은 패턴은 모델의 시스템 프롬프트를 우회하려는 시도일 수 있습니다.
또한 길이 제한으로 메모리 초과 공격을 막고, 의심스러운 문자열을 차단하여 모델이 악의적으로 사용되는 것을 방지합니다. 여러분이 이 코드를 사용하면 안전한 상용 서비스를 구축할 수 있습니다.
실무에서의 이점으로는 첫째, 무단 사용으로 인한 비용 증가를 방지합니다. 둘째, 서비스 약관을 위반하는 사용자를 차단할 수 있습니다.
셋째, 사용자별 사용량을 추적하여 과금이나 할당량 관리가 가능합니다. 넷째, GDPR, HIPAA 같은 규정 준수 요구사항을 충족할 수 있습니다.
실전 팁
💡 API 키를 데이터베이스에 저장할 때는 반드시 해시화하세요. bcrypt나 Argon2로 해싱하여 원본 키가 유출되지 않도록 보호하는 것이 중요합니다. 절대 평문으로 저장하지 마세요.
💡 레이트 리미팅은 여러 레벨에서 적용하세요. IP 기반(전체 제한), API 키 기반(사용자별 제한), 엔드포인트 기반(특정 기능 제한)으로 다층 방어를 구축하면 더 세밀한 제어가 가능합니다.
💡 Let's Encrypt로 무료 TLS 인증서를 자동으로 갱신하세요. Certbot을 사용하면 인증서 발급과 자동 갱신이 쉬우며, 모든 트래픽을 HTTPS로 보호할 수 있습니다.
💡 프롬프트 필터링에 AI 모델을 사용하는 것도 고려하세요. OpenAI Moderation API나 Perspective API를 사용하면 유해 콘텐츠, 혐오 발언, 개인정보 등을 더 정확히 탐지할 수 있습니다.
💡 감사 로그를 남기세요. 누가, 언제, 어떤 요청을 했는지 기록하면 보안 사고 발생 시 추적이 가능하고, 이상 패턴을 조기에 발견할 수 있습니다. 로그는 암호화하여 저장하고 일정 기간 후 삭제하세요.
10. 비용_최적화_전략
시작하며
여러분이 AI 서비스를 성공적으로 런칭했는데 월말에 클라우드 요금 청구서를 보고 깜짝 놀란 경험이 있나요? GPU 인스턴스 비용이 예상보다 10배나 나와서 수익보다 비용이 더 큰 상황을 마주할 수 있습니다.
이런 문제는 AI 서비스 운영의 가장 큰 도전 과제 중 하나입니다. GPU 한 대를 24시간 돌리면 한 달에 수백만 원이 나올 수 있는데, 최적화 없이는 사업성을 확보하기 어렵습니다.
특히 트래픽이 시간대별로 변동이 큰 경우 리소스 낭비가 심각합니다. 바로 이럴 때 필요한 것이 체계적인 비용 최적화 전략입니다.
성능은 유지하면서 비용을 절반 이하로 줄일 수 있는 다양한 기법들이 있습니다.
개요
간단히 말해서, 비용 최적화는 동일한 서비스 품질을 유지하면서 클라우드 및 GPU 리소스 비용을 최소화하는 모든 활동을 의미합니다. 왜 비용 최적화가 중요할까요?
AI 서비스의 주요 비용은 GPU 인스턴스인데, AWS A100 인스턴스는 시간당 수만 원입니다. 예를 들어, p4d.24xlarge는 시간당 약 $32(약 4만 원)로, 하루면 100만 원, 한 달이면 3,000만 원이 나옵니다.
최적화 없이는 서비스가 적자를 면하기 어렵죠. 기존에는 안전하게 과도한 리소스를 미리 확보해두는 방식이었다면, 이제는 실시간 수요에 맞춰 탄력적으로 리소스를 조절하고 저렴한 대안을 활용하는 것이 표준입니다.
핵심 최적화 기법은 다섯 가지입니다. 첫째, 스팟 인스턴스로 최대 90% 비용 절감합니다.
둘째, 배치 처리로 GPU 활용률을 극대화합니다. 셋째, 모델 양자화로 더 작고 저렴한 GPU 사용이 가능합니다.
넷째, 캐싱으로 중복 요청을 줄입니다. 다섯째, 오토 스케일링으로 유휴 시간 비용을 제거합니다.
이러한 기법들을 조합하면 비용을 50~80% 절감할 수 있습니다.
코드 예제
# AWS 스팟 인스턴스 활용 전략 (Terraform)
# resource "aws_spot_instance_request" "llm_server" {
# ami = "ami-gpu-optimized"
# instance_type = "g5.xlarge" # 온디맨드 $1.006/hr, 스팟 ~$0.30/hr
# spot_price = "0.50" # 최대 지불 의사
#
# tags = {
# Name = "llm-spot-instance"
# }
# }
# 응답 캐싱으로 중복 요청 처리 비용 절감
from functools import lru_cache
import hashlib
import redis
# Redis 캐시 연결
cache = redis.Redis(host='localhost', port=6379, db=0)
def get_cache_key(prompt: str, params: dict) -> str:
"""프롬프트와 파라미터로 캐시 키 생성"""
content = f"{prompt}:{str(sorted(params.items()))}"
return hashlib.md5(content.encode()).hexdigest()
def cached_generate(prompt: str, temperature: float = 0.7, max_tokens: int = 500):
"""캐싱이 적용된 생성 함수"""
# 캐시 키 생성
cache_key = get_cache_key(prompt, {"temp": temperature, "tokens": max_tokens})
# 캐시 확인
cached_result = cache.get(cache_key)
if cached_result:
print("캐시 히트! GPU 연산 생략")
return cached_result.decode('utf-8')
# 캐시 미스 - 실제 모델 추론
print("캐시 미스. 모델 실행 중...")
# response = model.generate(prompt, temperature, max_tokens)
response = f"Generated: {prompt[:50]}..."
# 결과를 캐시에 저장 (1시간 TTL)
cache.setex(cache_key, 3600, response)
return response
# 배치 처리로 GPU 활용률 극대화
from collections import deque
import asyncio
class BatchProcessor:
def __init__(self, max_batch_size=8, max_wait_time=0.1):
self.queue = deque()
self.max_batch_size = max_batch_size
self.max_wait_time = max_wait_time # 최대 대기 시간(초)
async def add_request(self, prompt):
"""요청을 큐에 추가하고 배치 처리 대기"""
future = asyncio.Future()
self.queue.append((prompt, future))
# 배치가 차거나 시간이 되면 처리
if len(self.queue) >= self.max_batch_size:
await self.process_batch()
return await future
async def process_batch(self):
"""큐에 있는 요청들을 한 번에 처리"""
if not self.queue:
return
batch = [self.queue.popleft() for _ in range(min(len(self.queue), self.max_batch_size))]
prompts = [item[0] for item in batch]
# 배치로 한 번에 추론 - GPU 활용률 극대화
# results = model.generate_batch(prompts)
results = [f"Result for {p}" for p in prompts]
# 각 요청에 결과 전달
for (_, future), result in zip(batch, results):
future.set_result(result)
설명
이것이 하는 일: 비용 최적화 코드는 같은 작업을 더 적은 리소스로 수행하게 만듭니다. 마치 에너지 효율이 좋은 가전제품처럼, 동일한 결과를 내면서 전기세(클라우드 비용)를 크게 줄이는 것입니다.
첫 번째로, 스팟 인스턴스는 클라우드 제공자의 유휴 용량을 저렴하게 사용하는 방식입니다. 온디맨드 가격의 10~30%로 사용할 수 있지만, 언제든 회수될 수 있다는 단점이 있습니다.
그래서 중요한 것은 spot_price를 적절히 설정하고, 회수 알림을 받으면 작업을 다른 인스턴스로 마이그레이션하는 메커니즘을 갖추는 것입니다. 그 다음으로, Redis 캐싱은 동일하거나 유사한 프롬프트에 대한 중복 연산을 제거합니다.
예를 들어, "Python이란?"이라는 질문이 100번 들어오면 첫 번째만 GPU로 처리하고 나머지 99번은 캐시에서 즉시 반환합니다. MD5 해시로 프롬프트와 파라미터를 조합하여 고유한 키를 만들고, TTL(Time To Live)로 오래된 캐시는 자동 삭제됩니다.
BatchProcessor는 여러 요청을 모아서 한 번에 처리합니다. GPU는 병렬 처리에 최적화되어 있어서 요청 1개를 처리하나 8개를 처리하나 시간 차이가 크지 않습니다.
예를 들어, 1개 처리에 0.5초 걸린다면 8개를 순차 처리하면 4초지만, 배치로 처리하면 0.7초에 끝납니다. GPU 활용률이 20%에서 80%로 올라가는 효과가 있습니다.
여러분이 이 기법들을 사용하면 비용을 극적으로 줄일 수 있습니다. 실무에서의 이점으로는 첫째, 스팟 인스턴스만으로도 월 3,000만 원이 300만 원으로 줄어듭니다.
둘째, 캐싱으로 실제 GPU 연산이 5070% 감소합니다. 셋째, 배치 처리로 같은 GPU로 35배 더 많은 요청을 처리할 수 있습니다.
넷째, 오토 스케일링으로 새벽 시간 같은 저트래픽 시간대의 낭비를 제거합니다. 이 모든 것을 조합하면 전체 비용을 80% 이상 절감할 수 있습니다.
실전 팁
💡 예약 인스턴스(Reserved Instances)를 장기 계약으로 사용하세요. 1년 약정 시 40%, 3년 약정 시 60% 할인받을 수 있어, 베이스 라인 트래픽 처리용으로 적합합니다. 스팟과 조합하면 안정성과 비용 모두 잡을 수 있습니다.
💡 CDN 캐싱도 활용하세요. Cloudflare나 CloudFront 같은 CDN에서 응답을 캐싱하면 서버까지 요청이 오지 않아 대역폭 비용과 연산 비용을 모두 절감할 수 있습니다.
💡 오프피크 시간대에 배치 작업을 처리하세요. 야간에는 인스턴스를 스케일 다운하고, 미리 모아둔 배치 작업(데이터 처리, 파인튜닝 등)을 저렴한 스팟 인스턴스로 처리하면 효율적입니다.
💡 모델 크기별 인스턴스를 분리하세요. 7B 모델은 T4($0.526/hr), 13B는 A10G($1.006/hr), 70B는 A100($4.1/hr)처럼 작은 모델은 작은 GPU로 처리하여 비용을 최적화하세요.
💡 FinOps 도구를 사용하여 비용을 모니터링하세요. AWS Cost Explorer, Kubecost 같은 도구로 어느 부분에서 비용이 많이 나가는지 파악하고, 최적화 우선순위를 정하세요. 측정하지 않으면 개선할 수 없습니다.