AWS Ambassador

10. Embedding Model 배포 (2) - EC2 직접 배포

Window to the world 2025. 4. 20. 11:54
반응형

SageMaker 엔드포인트로 임베딩 모델 배포 시 단점

SageMaker 엔드포인트는 모델 배포에 편리한 관리형 솔루션을 제공하지만, 임베딩 모델을 배포할 때 고려해야 할 몇 가지 중요한 단점이 있습니다:

1. 비용 관련 단점

  • 중지 기능 부재: EC2와 달리 사용하지 않는 시간에 엔드포인트를 중지할 수 없어, 24시간 지속적으로 비용이 발생합니다.
  • 높은 프리미엄: 동일한 인스턴스 유형 대비 EC2보다 약 25-30% 더 비싼 가격이 책정됩니다.
  • 최소 인스턴스 요구사항: 최소 1개 이상의 인스턴스가 항상 실행되어야 하므로 트래픽이 적은 시간에도 비용이 발생합니다.
  • 자원 낭비: 임베딩 요청이 간헐적인 경우에도 리소스가 항상 프로비저닝되어 있어 비효율적입니다.

2. 유연성 제약

  • 커스터마이징 제한: SageMaker 컨테이너는 특정 구조와 인터페이스를 따라야 하므로 모델 배포 방식에 제약이 있습니다.
  • 의존성 관리 어려움: 특정 라이브러리나 비표준 의존성을 사용할 때 호환성 문제가 발생할 수 있습니다.
  • 최신 모델 지원 지연: 최신 임베딩 모델이 나왔을 때 SageMaker 환경에 바로 통합하기 어려울 수 있습니다.
  • 디버깅 복잡성: 관리형 서비스 특성상 내부 동작을 확인하기 어려워 문제 해결이 복잡해집니다.

3. 운영 관련 문제

  • 콜드 스타트 성능: 자동 스케일링 시 새 인스턴스의 초기 로딩 시간이 길어 첫 요청의 지연시간이 증가합니다.
  • 배포 시간: 엔드포인트 생성 및 업데이트에 5-10분이 소요되어 빠른 반복 개발이 어렵습니다.
  • 일일 재배포 복잡성: 비용 절감을 위해 매일 엔드포인트를 생성/삭제하는 방식은 복잡한 자동화 스크립트와 추가 운영 부담을 요구합니다.
  • 로깅 및 모니터링 오버헤드: CloudWatch 통합은 편리하지만 세부적인 모니터링 설정을 위한 추가 작업이 필요합니다.

4. 벤더 종속성

  • AWS 생태계 의존: SageMaker에 최적화된 방식으로 개발하면 다른 클라우드 환경으로 마이그레이션하기 어려워집니다.
  • API 변경 위험: AWS가 SageMaker API나 기능을 변경할 경우 애플리케이션 수정이 필요할 수 있습니다.
  • 비용 협상 제한: 관리형 서비스로 제공되므로 비용 최적화를 위한 협상 여지가 적습니다.

5. 대안 제약

  • 서버리스 한계: 임베딩 모델은 메모리 요구사항이 높아 일부 서버리스 옵션에 적합하지 않을 수 있습니다.
  • 스팟 인스턴스 미지원: EC2의 스팟 인스턴스와 같은 비용 절감 옵션을 활용할 수 없습니다.
  • 하드웨어 최적화 제한: 특수 하드웨어 설정이나 커스텀 드라이버 설치 등 저수준 최적화가 제한됩니다.

SageMaker 엔드포인트는 프로덕션 환경에서 임베딩 모델을 안정적으로 서빙하는 데 유용하지만, 이러한 단점들을 고려하여 프로젝트의 요구사항, 예산, 운영 역량에 따라 적절한 배포 방식을 선택하는 것이 중요합니다.

 

 

EC2 인스턴스에 임베딩 모델 직접 배포의 장점

임베딩 모델을 AWS EC2 인스턴스에 직접 배포하는 방식은 SageMaker와 같은 관리형 서비스보다 여러 가지 장점을 제공합니다. 이 방식이 특히 뛰어난 점들을 살펴보겠습니다.

1. 비용 효율성

  • 중지/시작 유연성: 사용하지 않는 시간(야간, 주말)에 인스턴스를 중지하여 비용을 크게 절감할 수 있습니다.
  • 정확한 리소스 할당: 필요한 만큼의 정확한 컴퓨팅 자원만 할당할 수 있어 낭비를 최소화합니다.
  • 스팟 인스턴스 활용: 스팟 인스턴스를 사용하여 최대 90%까지 비용을 절감할 수 있습니다.
  • 관리 프리미엄 없음: SageMaker의 약 25-30% 추가 비용이 발생하지 않습니다.

2. 완전한 커스터마이징

  • 자유로운 환경 구성: 운영 체제부터 라이브러리까지 모든 환경을 자유롭게 구성할 수 있습니다.
  • 최신 모델 즉시 적용: 새로운 임베딩 모델이 출시되면 즉시 배포 가능합니다.
  • 커스텀 최적화: 모델 양자화, CUDA 최적화 등을 자유롭게 적용할 수 있습니다.
  • 복잡한 배포 아키텍처: 여러 모델을 하나의 API로 통합하거나 고급 라우팅 로직을 구현할 수 있습니다.

3. 개발 및 배포 유연성

  • 빠른 개발 반복: 변경 사항을 즉시 적용할 수 있어 개발 주기가 단축됩니다.
  • 쉬운 디버깅: 직접 서버에 접속하여 로그 확인, 문제 해결이 용이합니다.
  • CI/CD 통합: 기존 DevOps 파이프라인에 원활하게 통합할 수 있습니다.
  • 점진적 배포: 블루/그린 배포, 카나리 배포 등 고급 배포 전략을 자유롭게 구현할 수 있습니다.

4. 독립성 및 이식성

  • 벤더 종속성 없음: AWS 특화 기능에 의존하지 않아 다른 클라우드로 쉽게 이전 가능합니다.
  • 온프레미스 호환: 동일한 설정을 온프레미스 환경에도 적용할 수 있습니다.
  • 하이브리드 아키텍처: 클라우드와 온프레미스 환경을 혼합하여 사용할 수 있습니다.
  • 외부 의존성 감소: API 키나 서비스 제한 없이 독립적으로 운영 가능합니다.

5. 성능 최적화

  • 하드웨어 최적화: GPU 메모리, CPU 코어 수 등을 워크로드에 맞게 정확히 조정할 수 있습니다.
  • 네트워크 최적화: 특정 요구 사항에 맞게 네트워크 설정을 최적화할 수 있습니다.
  • 캐싱 메커니즘: 맞춤형 캐싱 전략을 구현하여 반복 요청의 성능을 향상시킬 수 있습니다.
  • 워크로드 특화 조정: 임베딩 생성 패턴에 맞춰 서버 설정을 미세 조정할 수 있습니다.

6. 보안 및 규정 준수

  • 데이터 통제: 데이터 흐름과 저장에 대한 완전한 통제가 가능합니다.
  • 맞춤형 보안 정책: 특정 규정 준수 요구 사항에 맞게 보안 설정을 구성할 수 있습니다.
  • 네트워크 격리: 필요에 따라 완전히 격리된 네트워크 환경을 구성할 수 있습니다.
  • 감사 및 모니터링: 사용자 지정 감사 로깅 및 모니터링 솔루션을 구현할 수 있습니다.

7. 기술적 자율성

  • 기술 스택 통제: 익숙하고 검증된 기술을 자유롭게 선택할 수 있습니다.
  • 기술 역량 활용: 팀의 기존 DevOps 지식과 경험을 활용할 수 있습니다.
  • 문제 직접 해결: 서비스 중단 시 외부 지원 없이 직접 문제를 해결할 수 있습니다.
  • 인프라 통합: 기존 인프라 관리 도구와 원활하게 통합할 수 있습니다.

직접 배포 방식은 특히 비용에 민감하거나, 커스터마이징이 필요하거나, 개발 민첩성이 중요한 프로젝트에 이상적입니다. 또한 인프라 관리 역량을 갖춘 팀에게 더 많은 통제력과 유연성을 제공합니다.

 

임베딩 모델 직접 배포 

 

본 게시글에서는 EC2 인스턴스에 Docker 컨테이너를 활용하여 텍스트 임베딩 API 서비스를 구축하는 방법을 설명합니다. 이 서비스는 다국어를 지원하고 한글 RAG에서 우수한 성능을 보이는 E5 임베딩 모델을 사용하며, FastAPI를 통해 API 엔드포인트를 제공합니다.

 

주요 구성 요소는 다음과 같습니다:

  • app/main.py: FastAPI 애플리케이션과 API 엔드포인트를 정의합니다.
  • app/model.py: 텍스트 임베딩 모델을 로드하고 추론 로직을 구현합니다.
  • requirements.txt: 필요한 Python 패키지 목록을 정의합니다.
  • Dockerfile: 도커 이미지 빌드를 위한 설정 파일입니다.

EC2 인스턴스 생성 및 설정

  1. 인스턴스 유형 선택:
    • GPU 사용 : g4dn.xlarge 이상 (NVIDIA T4 GPU 포함)
  2. 네트워크 설정:
    • 새 보안 그룹 생성
    • SSH(22), HTTP(80), HTTPS(443), 커스텀 TCP(8001) 포트 개방
  3. 스토리지 구성:  100GB 이상 (모델 파일 저장, 패키지 라이브러리 설치 등)

Elastic IP 할당 

  1. EC2 대시보드에서 "Elastic IP" 메뉴로 이동
  2. "Elastic IP 주소 할당" 클릭
  3. 생성된 Elastic IP를 인스턴스에 연결

FastAPI 애플리케이션 구현 (app/main.py)

from fastapi import FastAPI
from pydantic import BaseModel
from app.model import get_embeddings

app = FastAPI()

class EmbeddingRequest(BaseModel):
    texts: list[str]

@app.post("/embed")
async def embed(request: EmbeddingRequest):
    embeddings = await get_embeddings(request.texts)
    return {"embeddings": embeddings}

 

이 코드는 다음과 같은 기능을 제공합니다:

  • FastAPI 프레임워크를 사용하여 웹 API 생성
  • Pydantic의 BaseModel을 상속받아 요청 데이터 유효성 검사
  • /embed 경로에 POST 요청을 처리하는 엔드포인트 정의
  • 비동기 함수로 구현하여 요청 처리 효율성 향상

* 비동기 함수:  웹 서버, API 서비스, 데이터 처리 파이프라인과 같이 동시에 여러 작업을 처리해야 하는 상황에서 매우 유용합니다.

 

임베딩 모델 (app/model.py)

Sentence Transformer 모델을 로드하고 텍스트를 임베딩 벡터로 변환하는 로직을 포함합니다.
 
from sentence_transformers import SentenceTransformer
import torch
import asyncio
from concurrent.futures import ThreadPoolExecutor

model = SentenceTransformer('intfloat/multilingual-e5-large')
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)

executor = ThreadPoolExecutor(max_workers=4)

def _encode(texts: list[str]):
    return model.encode(texts, normalize_embeddings=True).tolist()

async def get_embeddings(texts: list[str]):
    loop = asyncio.get_event_loop()
    return await loop.run_in_executor(executor, _encode, texts)

 

주요 특징:

  • 다국어를 지원하는 E5 임베딩 모델 (intfloat/multilingual-e5-large) 사용
  • GPU 가속 자동 감지 및 활용
  • 임베딩 벡터 정규화를 통한 코사인 유사도 계산 최적화
  • ThreadPoolExecutor를 활용한 비차단 비동기 처리

Docker 컨테이너화 (Dockerfile)

Dockerfile은 애플리케이션을 컨테이너화하기 위한 설정을 포함합니다.

 

FROM python:3.10-slim

RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app ./app
EXPOSE 8000

CMD ["gunicorn", "app.main:app", "-k", "uvicorn.workers.UvicornWorker", "-w", "2", "--bind", "0.0.0.0:8000"]

 

Dockerfile의 역할:

  • Python 3.10 슬림 이미지를 기반으로 경량화된 환경 구성
  • 필요한 시스템 패키지(git) 설치 및 캐시 정리
  • Python 의존성 설치 및 애플리케이션 코드 복사
  • Gunicorn과 Uvicorn을 사용한 프로덕션 환경 웹 서버 설정
  • 컨테이너 내부 포트 8000 노출

* requirements.txt (필요한 패키지 라이브러리 지정)

fastapi
uvicorn[standard]
gunicorn
sentence_transformers~=2.2.2
torch
huggingface_hub==0.16.4

 

EC2 배포 (위 코드를 ec2 인스턴스로 가져오기)

1) 코드 가져오기 

git clone https://github.com/<github id>/e5-embedding.git
cd e5-embedding

* GitHub 저장소에서 프로젝트 코드를 ec2 에 복제

 

2) Docker 이미지 빌드

docker build -t e5-embedding .

 

프로젝트 디렉토리의 Dockerfile을 기반으로 Docker 이미지를 빌드하고 e5-embedding이라는 태그를 부여합니다.

 

3) 컨테이너 실행

docker run -d --gpus all \
  --restart unless-stopped \
  -p 8001:8000 \
  --name e5-embedding \
  e5-embedding

 

다음과 같은 옵션으로 Docker 컨테이너를 실행합니다:

  • -d: 백그라운드에서 컨테이너 실행 (detached 모드)
  • --gpus all: 모든 GPU를 컨테이너에서 사용 가능하게 설정
  • --restart unless-stopped: 서버 재부팅 시 자동으로 컨테이너 재시작
  • -p 8001:8000: 호스트의 8001 포트를 컨테이너의 8000 포트에 매핑
  • --name e5-embedding: 컨테이너에 이름 부여

배포한 임베딩 모델 호출

LangChain 프레임워크와 앞서 EC2에 배포한 E5 임베딩 API 서비스를 통합하여 LangChain의 Embeddings 인터페이스를 구현하고 원격 API를 통해 텍스트 임베딩을 생성하는 커스텀 임베딩 클래스를 정의합니다.

 

from typing import List
import requests
from langchain.embeddings.base import Embeddings

class E5RemoteEmbeddings(Embeddings):
    def __init__(self, api_url: str):
        self.api_url = api_url
        
    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        # 문서형 리스트는 prefix 없이 처리한다고 가정
        payload = {"texts": [f"passage: {text}" for text in texts]}
        response = requests.post(self.api_url, json=payload)
        response.raise_for_status()
        return response.json()["embeddings"]
        
    def embed_query(self, text: str) -> List[float]:
        # 쿼리는 반드시 prefix 포함
        payload = {"texts": [f"query: {text}"]}
        response = requests.post(self.api_url, json=payload)
        response.raise_for_status()
        return response.json()["embeddings"][0]

* E5 모델의 특수한 접두사 요구사항 반영

 E5 임베딩 모델은 검색 성능을 최적화하기 위해 쿼리와 문서에 다른 접두사를 사용하도록 설계되었습니다. 이 코드는 이러한 요구사항을  구현했습니다:

    - 쿼리: "query: [텍스트]"

    - 문서: "passage: [텍스트]"

주요 특징:

  1. LangChain 통합: langchain.embeddings.base.Embeddings 추상 클래스를 상속받아 LangChain 생태계와 호환되는 임베딩 클래스를 정의합니다.
  2. 비대칭 임베딩 구현:
    • 쿼리와 문서에 다른 접두사(prefix)를 사용하는 비대칭 임베딩 방식을 구현했습니다.
    • 문서에는 "passage:" 접두사 사용
    • 쿼리에는 "query:" 접두사 사용
  3. 원격 API 호출: requests 라이브러리를 사용하여 EC2에 배포된 임베딩 API에 HTTP 요청을 보냅니다.
  4. 에러 처리: response.raise_for_status()를 사용하여 API 요청 실패 시 예외를 발생시킵니다.
# EC2의 실제 퍼블릭 IP 사용 (Elastic IP)
embedding = E5RemoteEmbeddings(api_url="http://public ip:8001/embed")

# 쿼리 임베딩
query_vec = embedding.embed_query("how much protein should a female eat")

# 문서 임베딩
docs_vecs = embedding.embed_documents([
    "The CDC recommends 46 grams of protein per day for adult women.",
    "Eggs and meat are high in protein."
])

docs_vecs

주요 단계:

  1. 클래스 인스턴스 생성: EC2에 배포된 임베딩 API의 URL을 제공하여 E5RemoteEmbeddings 객체를 생성합니다.
  2. 쿼리 임베딩 생성: 단일 쿼리 문장에 대한 임베딩 벡터를 생성합니다. 내부적으로 "query:" 접두사가 추가됩니다.
  3. 문서 임베딩 생성: 문서 리스트에 대한 임베딩 벡터 리스트를 생성합니다. 각 문서에는 "passage:" 접두사가 추가됩니다.
  4. 결과 확인: 생성된 문서 임베딩 벡터를 출력합니다.

 

AWS EC2 직접 배포 임베딩 모델 자동 스케줄링 

 

임베딩 모델을 EC2에 직접 배포했을 때 비용을 최적화하려면 자동 스케줄링을 설정하는 것이 중요합니다. 이 글에서는 EC2 인스턴스를 필요한 시간에만 가동하고 불필요한 시간에는 자동으로 중지하는 방법을 설명합니다.EC2 인스턴스는 실행 중일 때만 비용이 발생하며, 중지된 상태에서는 EBS 볼륨 스토리지 비용만 발생합니다. 따라서 업무 시간 외에 인스턴스를 자동으로 중지하면 상당한 비용을 절감할 수 있습니다. 예를 들어:

  • 업무 시간(오전 9시 ~ 오후 6시)에만 실행: 월간 비용 약 65% 절감
  • 주중 업무 시간에만 실행: 월간 비용 약 75% 절감

 

자동 스케줄링 구현 방법

가장 일반적이고 안정적인 방법은 Lambda 함수와 CloudWatch Events(EventBridge)를 조합하는 것입니다.

 

EC2 시작 함수 (start-ec2-instance)

import boto3

def lambda_handler(event, context):
    ec2 = boto3.client('ec2', region_name='ap-northeast-2')  # 리전에 맞게 변경
    instance_id = 'i-xxxxxxxxxxxxxxxxx'  # 임베딩 모델이 실행 중인 EC2 인스턴스 ID
    
    # 인스턴스 상태 확인
    response = ec2.describe_instances(InstanceIds=[instance_id])
    state = response['Reservations'][0]['Instances'][0]['State']['Name']
    
    # 이미 실행 중이면 아무 작업도 하지 않음
    if state == 'running':
        return f"Instance {instance_id} is already running"
    
    # 중지 상태인 경우에만 시작
    if state == 'stopped':
        ec2.start_instances(InstanceIds=[instance_id])
        return f"Started instance {instance_id}"
        
    return f"Instance {instance_id} is in {state} state, no action taken"

 

EC2 중지 함수 (stop-ec2-instance)

import boto3

def lambda_handler(event, context):
    ec2 = boto3.client('ec2', region_name='ap-northeast-2')  # 리전에 맞게 변경
    instance_id = 'i-xxxxxxxxxxxxxxxxx'  # 임베딩 모델이 실행 중인 EC2 인스턴스 ID
    
    # 인스턴스 상태 확인
    response = ec2.describe_instances(InstanceIds=[instance_id])
    state = response['Reservations'][0]['Instances'][0]['State']['Name']
    
    # 이미 중지되어 있으면 아무 작업도 하지 않음
    if state == 'stopped':
        return f"Instance {instance_id} is already stopped"
    
    # 실행 중인 경우에만 중지
    if state == 'running':
        ec2.stop_instances(InstanceIds=[instance_id])
        return f"Stopped instance {instance_id}"
        
    return f"Instance {instance_id} is in {state} state, no action taken"

 

IAM 역할 설정

Lambda 함수가 EC2 인스턴스를 제어할 수 있도록 적절한 IAM 역할을 생성합니다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:Start*",
                "ec2:Stop*",
                "ec2:DescribeInstances"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        }
    ]
}

 

CloudWatch Events 규칙 설정

- 인스턴스 시작 규칙:

  • 평일(월-금) 오전 8시 30분(KST)에 실행
  • cron 표현식: cron(30 23 ? * SUN-THU *)
    (UTC 기준이므로 KST보다 9시간 이전)

- 인스턴스 중지 규칙:

  • 평일(월-금) 오후 7시 30분(KST)에 실행
  • cron 표현식: cron(30 10 ? * MON-FRI *)
    (UTC 기준이므로 KST보다 9시간 이전)

 

Docker 컨테이너 자동 재시작 설정

EC2 인스턴스가 시작될 때 Docker 컨테이너가 자동으로 시작되도록 설정해야 합니다.

 

/etc/systemd/system/embedding-api.service 파일 생성

[Unit]
Description=Embedding API Docker Container
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/docker start e5-embedding
ExecStop=/usr/bin/docker stop e5-embedding
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target

 

서비스 활성화 및 시작

sudo systemctl enable embedding-api.service
sudo systemctl start embedding-api.service

 

 

반응형