질문은 하나였는데
왕복은 네 번이었습니다.
Model Serving Best Practices — 2·3장이 "모델을 어떻게 실행하는가"였다면, 이 장은 "프로덕션에서 무엇이 더 필요한가"입니다.
에이전트가 등장하면서 요청 한 건의 의미가 달라졌고, 그래서 서빙은 순수 추론 문제가 아니라 시스템 아키텍처 문제가 됩니다. 계층 구조, 오픈소스 스택, 클라우드 벤더, 그리고 그 모든 선택을 판정하는 지표까지 봅니다.
① 실제 로그와 숫자로 확인합니다. "왕복 4회"도, "초당 3,255토큰"도 원문에 있는 실측·계산값입니다.
② 전문 용어는 영어 그대로. speculative decoding을 억지로 번역하지 않고 뜻만 한국어로 풀어둡니다.
③ 그림 16개 전부 조작 가능. 노브를 밀어봐야 트레이드오프가 몸에 남습니다.
④ 장마다 세 줄 요약. 거기만 읽어도 넘어갈 수 있게.
들어가며
3장까지 우리가 만든 것은 "요청 하나에 추론 한 번, 응답 반환"이라는 전제 위에 서 있었습니다. 이 장은 그 전제가 깨지는 지점에서 시작합니다.
모델이 에이전트 안에 들어가면 제어 루프(control loop) 안에서 반복 호출됩니다. 검색하고, 중간 결과를 추론하고, 도구를 실행하고, 출력을 다듬어야 최종 답이 나옵니다. 사용자는 질문을 한 번 던졌는데 서빙 백엔드는 네 번 왕복합니다.
그러면 무엇이 달라지나요. 토큰 사용량이 늘고, 체인으로 이어진 호출마다 tail latency가 누적되고, 트래픽 패턴이 동적으로 변합니다. 서빙은 이제 "모델을 효율적으로 돌리는 일"을 넘어 오케스트레이션·메모리 관리·시스템 레벨 조정까지 감당해야 합니다.
- 에이전트 워크로드는 요청 한 건의 비용을 몇 배로 부풀립니다. 이 장의 나머지 전부가 그 부담을 감당하기 위한 장치입니다.
- 엔터프라이즈 서빙은 일곱 개의 층으로 나뉩니다. 층을 나누는 이유는 기술보다 조직에 가깝습니다 — 여러 팀이 서로 발목 잡지 않고 일하기 위해서.
- "직접 지을까 사서 쓸까"는 이분법이 아니라 스펙트럼입니다. 그 위 어디에 설지는 측정된 숫자가 정합니다.
읽는 방법
· 굵은 글씨만 따라가도 흐름이 이어집니다.
· 코드 상자에는 파일 경로나 출처가 주석으로 붙어 있습니다.
· 그림은 전부 슬라이더를 밀고 버튼을 눌러보는 용도입니다. 특히 극단으로 밀어봤을 때 무슨 일이 나는지가 핵심입니다.
· 4장은 실습이 여럿입니다. 실습을 건너뛰어도 본문은 완결되게 썼습니다.
이 장이 답하는 세 가지 질문
원문 도입부가 스스로에게 던지는 질문입니다. 다 읽고 나서 여기로 돌아와 보시면 좋습니다.
① 에이전트 워크플로우가 등장하면서 모델 서빙 요구사항이 근본적으로 어떻게 달라졌는가?
단일 요청-응답과 달리, 제어 루프 안에서 반복 호출되는 것이 서빙 시스템에 어떤 부담을 주는가? (1~5장)
② 엔터프라이즈 LLM 서빙 플랫폼의 계층형 아키텍처는 각 레이어가 어떤 역할과 트레이드오프를 가지며,
이를 오픈소스 스택(Kubernetes / Ray Serve)으로는 어떻게 구현하는가? (6~11장)
③ "직접 구축 vs 클라우드 벤더"를 스펙트럼으로 볼 때, 어떤 기준으로 위치를 정하고
그 타당성을 판단하려면 어떤 지표를 측정해야 하는가? (12~16장)
이 장에 나오는 이름들 — 지금 외울 필요는 없습니다
10개만 먼저 훑어두면 뒤가 훨씬 편합니다. "아 그거" 정도면 충분합니다.
Agent
아키텍처
RayService
6 옵션
TTFT · ITL
— Part 1. 에이전트 시대의 모델 서빙 —
01질문 하나, 왕복 네 번
한 줄 요약: 사용자는 한 번 물었는데 서빙 백엔드는 네 번 일했습니다. 이게 4장의 출발점입니다.
"토큰 사용량이 늘고 지연시간이 증폭된다"는 문장은 읽으면 넘어가집니다. 그런데 실제 로그를 보면 숫자가 눈에 박힙니다.
이 장에서는 Knowledge Agent에 질문 하나를 던졌을 때 실제로 찍힌 HTTP 요청 로그 4줄을 보고, 거기서 왜 tail latency가 체인마다 누적되는지를 확인합니다.
- Agent (에이전트)
- 목표를 이해하고 → 방법을 추론하고 → 도구를 호출하고 → 중간 결과로 적응·반복하며 → 사람 개입을 최소화해 결과를 내는 자율적인 LLM 기반 시스템.
- Control loop (제어 루프)
- 답이 나올 때까지 모델을 반복 호출하는 순환 구조. 요청 한 건이 여러 번의 추론으로 펼쳐지는 원인.
- Tail latency
- 느린 쪽 꼬리의 지연시간(p95·p99 등). 호출을 체인으로 이으면 각 단계의 꼬리가 더해져 눈에 띄게 커집니다.
- Autonomy (자율성)
- "무엇을 어떻게 할지"까지 시스템이 스스로 판단하는 성질. 규칙 기반 챗봇과의 본질적 차이.
- Embedding (임베딩)
- 텍스트를 의미를 담은 숫자 벡터로 바꾼 것. 검색·유사도 매칭에 씁니다.
비유 먼저 — 창구 한 번, 뒷방 네 번
민원 창구에 서류 한 장을 냈다고 해봅시다. 창구 직원은 서류를 받고 한 번 응대했습니다.
그런데 뒤에서는 이런 일이 벌어집니다. 담당자가 어떤 절차를 밟을지 먼저 정하고(계획), 서고에 가서 관련 문서를 찾아오고(검색), 요약본을 만들고, 그 요약본을 근거로 판단문을 씁니다.
민원인 입장에선 "한 번 냈다"입니다. 기관 입장에선 네 건의 작업입니다. 창구 수만 세면 이 기관의 부하를 절반도 못 봅니다. 서빙 지표도 똑같습니다.
작은 숫자로 한 번 — 실제 로그 4줄
"What is 5-level paging and how does it work?" 딱 한 질문에 실제로 찍힌 로그입니다.
INFO:planner:Creating plan for query: What is 5-level paging...
POST https://api.openai.com/v1/chat/completions // ① 플래닝
INFO:planner:Created plan: {'plan': ['generate_summary', 'generate_analysis'], ...}
POST https://api.openai.com/v1/embeddings // ② 쿼리 검색
INFO:rag_system:Found 5 relevant documents
POST https://api.openai.com/v1/chat/completions // ③ 요약
POST https://api.openai.com/v1/chat/completions // ④ 분석
| 호출 | 엔드포인트 | 횟수 | 무엇 때문에 |
|---|---|---|---|
| 플래닝 | chat/completions | 1 | 어떤 액션을 어떤 순서로 실행할지 LLM이 결정 |
| 쿼리 검색 | embeddings | 1 | 질문을 벡터로 바꿔 관련 문서 5개 검색 |
| 요약 | chat/completions | 1 | 검색된 문서를 컨텍스트로 요약 생성 |
| 분석 | chat/completions | 1 | 앞 요약을 컨텍스트로 심층 분석 생성 |
| 합계 | — | 4회 | 사용자가 던진 질문은 단 하나 |
여기서 하나만 기억한다면 — 이 네 번은 병렬이 아니라 직렬입니다. 플래닝이 끝나야 검색을 시작하고, 검색이 끝나야 요약을 시작합니다. 그래서 각 단계의 지연시간이 더해집니다. 평균이 아니라 합입니다.
요청 한 건이 만드는 왕복 — 직접 세어보기
↔ 슬라이더 · 버튼막대가 겹치지 않고 이어집니다. 직렬이라는 뜻이고, 그래서 지연이 더해집니다.
이 질문("What is X and how does it work?")은 코드의 폴백 규칙대로라면
"what"/"how"에 걸려 ['query_rag_with_context'] 1스텝으로 처리됐어야 합니다.
그런데 실제 LLM 플래너는 ['generate_summary', 'generate_analysis'] 2스텝을 골랐습니다.
이유까지 붙여서요 — "요약으로 개요를 먼저 주고, 분석으로 상세 설명을 이어가겠다".
LLM 기반 플래닝이 하드코딩된 휴리스틱과 다른 판단을 내린다는 걸 로그로 확인한 셈입니다.
process_query는 results[-1], 즉 마지막 액션의 결과만 화면에 찍습니다.
1단계 generate_summary의 출력 텍스트는 화면에 전혀 안 보였고, 2단계의 입력 재료로만 소비됐습니다.
사용자는 분석 결과 하나를 봤지만, 뒤에서는 2단계 추론이 일어난 것입니다.
- 에이전트에서는 사용자 상호작용 한 번이 여러 번의 LLM 호출로 펼쳐집니다. 실측 4회.
- 이 호출들은 직렬이라 지연시간이 더해집니다. 그래서 tail latency가 체인마다 누적됩니다.
- 그 결과 서빙은 "모델 실행"을 넘어 오케스트레이션·메모리 관리·시스템 조정까지 감당해야 합니다.
02에이전트를 뜯어보면 — 다섯 부품, 두 모델
한 줄 요약: LLM이 혼자 다 하는 게 아닙니다. LLM은 생각하고, RAG가 찾고, Planner가 순서를 정합니다.
맞는 부분도 있습니다. 이 샘플 에이전트의 액션은 실제로 "LLM 호출 + 특화된 프롬프트 템플릿"이 전부입니다. 그런데 어떤 프롬프트를 언제 쓸지 정하는 부품이 따로 있다는 게 핵심입니다.
이 장에서는 부품을 하나씩 꺼보며 "이게 없으면 무엇이 안 되는가"를 확인합니다. 3장에서 서빙 파일을 하나씩 꺼봤던 것과 같은 방식입니다.
- Orchestrator (오케스트레이터)
- 부품들을 조율하는 중앙 컨트롤러. 여기서는
Agent.process_query()가 그 역할. - Action (액션)
- 에이전트가 고를 수 있는 이산적(discrete)이고 재사용 가능한 행동. "셀 수 있게 딱 떨어지는" 유한한 선택지라는 뜻입니다.
- Action registry
- 호출 가능한 액션 목록을 관리하는 부품. 플래너는 이 목록 안에서만 계획을 짭니다.
- Facade (파사드)
- 여러 부품을 하나의 얇은 진입점 뒤로 숨기는 패턴.
agent.py가 그렇게 생겼습니다. - MCP (Model Context Protocol)
- LLM이 도구를 발견하고 · 구조화된 입력으로 호출하고 · 결과를 추론에 반영하는 일관된 인터페이스 표준.
비유 먼저 — 조사관 한 명이 아니라 팀 하나
보고서를 하나 써야 합니다. 혼자 다 하는 사람은 없습니다.
팀장이 "먼저 자료를 찾고, 요약하고, 그다음 비교 분석하자"고 순서를 잡습니다(Planner). 자료조사원이 서고에서 관련 문서를 뽑아옵니다(RAG). 작성자가 그 자료로 글을 씁니다(LLM Manager). 진행 담당이 순서대로 굴리고 앞 결과를 다음 사람에게 넘깁니다(ActionExecutor).
여기서 LLM은 팀장이자 작성자입니다. 두 역할 다 하지만, 자료조사는 못 합니다. 그건 임베딩 모델의 일입니다.
모델 두 개가 역할을 나눕니다
Knowledge Agent는 OpenAI 모델 두 개를 씁니다. 이름이 아니라 역할이 중요합니다.
| 모델 | 역할 | 로그에서 어떻게 보이나 |
|---|---|---|
text-embedding-3-small | Embedding | 텍스트를 의미 벡터로. POST /v1/embeddings — PDF 42청크 벡터화, 사용자 쿼리 벡터화 |
gpt-4.1-nano | LLM | 추론·계획·생성. POST /v1/chat/completions — 플래닝 1 + 요약 1 + 분석 1 |
사용자 질문 → Agent.process_query()
├─ Planner (gpt-4.1-nano) → 실행 계획 수립
└─ ActionExecutor 순차 실행
├─ RAGSystem (text-embedding-3-small) → 관련 문서 검색
└─ LLMManager (gpt-4.1-nano) → 각 액션별 응답 생성
액션은 네 개뿐입니다
플래너가 고를 수 있는 선택지는 딱 네 개입니다. 이게 "이산적 액션 집합"의 실체입니다.
query_rag_with_context // RAG 검색 + 답변
generate_profile_based_response // 사용자 프로필 반영 답변
generate_summary // 요약
generate_analysis // 심층 분석
액션 하나는 결국 프롬프트 템플릿 하나입니다. 분석 액션의 프롬프트를 보면 명확합니다.
def create_analysis_prompt(self, query: str, context: str) -> str:
prompt = f"""
You are an expert analyst. Please provide a detailed
analysis of the following question based on the
provided context.
Question: {query}
Context:
{context}
Please provide:
1. A comprehensive analysis
2. Key insights and findings
3. Relevant examples from the context
4. Any limitations or gaps in the available information
"""
return prompt
여기서 하나만 기억한다면 — 액션이 LLM 프롬프트에만 머물 필요는 없습니다. 도구 호출(웹 검색·API 질의·DB 명령), 시스템 연산(파일 관리·워크플로우 트리거), 사용자에게 안 보이는 중간 추론 단계도 전부 액션이 될 수 있습니다. 이 샘플이 의도적으로 LLM 호출만 쓰는 것뿐입니다.
Knowledge Agent 시스템 개요
위아래 두 덩어리로 갈립니다. 위쪽이 에이전트(Planner · Actions · RAG system · Knowledge docs), 아래쪽이 모델 서빙(LLM과 Embedding 두 모델)입니다.
핵심은 위쪽 네 부품이 전부 아래 두 모델을 부르려고 존재한다는 것입니다. Planner도 LLM을 부르고, RAG도 Embedding을 부르고, Actions도 LLM을 부릅니다. 그래서 사용자 요청 한 건이 서빙 호출 여러 건으로 펼쳐집니다 — 1장에서 로그로 확인한 그 4회입니다.
다섯 부품 — 하나씩 꺼보기
↔ 부품 클릭보라색 부품이 gpt-4.1-nano, 파란색이 text-embedding-3-small을 씁니다.
validate_action_prerequisites()는 len(self.rag_system.documents) > 0만 확인합니다.
build_knowledge_base()를 먼저 호출하지 않으면(문서가 0개면) 모든 액션이 에러 없이 조용히 스킵되고
results가 빈 리스트로 나옵니다. 그런데 process_query는
success: True에 final_response: "No response generated"를 반환합니다.
겉보기엔 성공, 실제로는 아무 일도 안 일어난 상태입니다. 프로덕션에서 제일 찾기 힘든 종류의 버그입니다.
· 플래너 응답 파싱이 실패하면 _create_fallback_plan()이 키워드 매칭으로 대체 계획을 만듭니다
(what/how→RAG 1스텝, summarize→RAG+요약 2스텝, analyze/compare→RAG+분석 2스텝).
· 액션마다 try/except가 걸려 있어 한 스텝이 실패해도 나머지 스텝은 계속 진행됩니다.
· process_query 전체가 try/except로 감싸져 있어 어떤 예외든
success: False dict로 변환됩니다 — 에이전트 프로세스 자체는 죽지 않습니다.
- 에이전트는 LLM 하나가 아니라 다섯 부품입니다 — Agent · Planner · RAG · Actions · LLM Manager.
- 모델도 두 개가 역할을 나눕니다 — 임베딩 모델은 찾고, LLM은 계획하고 생성합니다.
- 액션은 이산적 집합이고, 여기서는 프롬프트 템플릿 네 개입니다. 도구 호출로 확장하는 게 다음 단계입니다.
039단계 워크플로우 — 그리고 컨텍스트 체이닝
한 줄 요약: 두 번째 액션은 원본 문서를 다시 찾지 않습니다. 첫 번째 액션의 결과를 재료로 씁니다.
이게 그냥 구현 디테일 같지만, 서빙 비용과 지연시간을 직접 결정합니다. 앞 결과를 넘기면 검색 호출이 한 번으로 끝나고, 안 넘기면 액션 수만큼 늘어납니다.
이 장에서는 원문의 9단계 워크플로우를 따라가며, 로그에서 임베딩 호출이 한 번뿐이었던 이유를 확인합니다.
- Context chaining (컨텍스트 체이닝)
- 앞 액션의 결과를 다음 액션의 입력 컨텍스트로 넘기는 것. 여기서는 결과가 50자를 넘을 때만 넘깁니다.
- Workflow executor
- 계획에 따라 액션을 순서대로 실행하는 부품. 실패한 스텝은 에러 문자열만 남기고 다음으로 넘어갑니다.
- Sequential plan
- 액션들이 줄 세워진 계획. 병렬이 아니라서 지연시간이 합산됩니다.
- Grounding (그라운딩)
- 모델의 답을 실제 문서에 근거하게 만드는 것. RAG가 하려는 일이 이것입니다.
비유 먼저 — 릴레이인가, 각자 도서관 가기인가
세 사람이 순서대로 보고서를 다듬습니다. 두 가지 방식이 있습니다.
릴레이 — 첫 사람이 자료를 찾아 초안을 쓰고, 그 초안을 다음 사람에게 넘깁니다. 다음 사람은 도서관에 갈 필요가 없습니다. 빠르고 쌉니다. 대신 첫 사람이 놓친 자료는 영영 못 봅니다.
각자 도서관 — 세 사람이 각각 서고에 갑니다. 놓치는 게 적습니다. 대신 도서관 왕복이 세 번입니다.
Knowledge Agent는 릴레이 방식입니다. 그래서 로그에 임베딩 호출이 한 번뿐이었습니다.
9단계 — 원문 그림 4-2를 따라
질문: "데이터베이스 쿼리 최적화와 자료구조 최적화를 상세 비교해 주세요."
| 단계 | 무슨 일이 | 주체 |
|---|---|---|
| 1 | 사용자 질문 입력 | User |
| 2 | Agent가 Planner를 호출해 실행 계획 설계 요청 | Agent |
| 3 | Planner가 LLM을 호출해 3단계 계획 생성 | Planner → LLM |
| 4 | ActionExecutor가 순서대로 실행 시작 | ActionExecutor |
| 5 | query_rag_with_context — 시맨틱 검색 후 그 문서로 답변 생성 | RAG → LLM |
| 6 | generate_analysis — 질문 + 5단계 결과를 컨텍스트로 전달 | ActionExecutor |
| 7 | LLM이 심층 비교 분석 생성 | LLM |
| 8 | generate_summary — 직전 분석 결과를 요약 | LLM |
| 9 | Agent가 액션 출력을 종합해 최종 결과 반환 | Agent |
{
"plan": ["query_rag_with_context", "generate_analysis", "generate_summary"],
"reasoning": "First, retrieving relevant contextual information ensures
a comprehensive understanding...",
"estimated_steps": 3
}
체이닝이 실제로 일어났다는 증거
앞 장의 로그를 다시 보면, 2단계 generate_analysis 직전에 embeddings 호출이 없습니다.
chat/completions 딱 하나만 있습니다.
즉 generate_summary의 결과(50자 초과)가 context로 재사용되면서,
generate_analysis는 원본 문서를 다시 검색하지 않고 방금 만든 요약문을 입력 삼아 분석을 썼습니다.
에이전트가 사용자 질문에 응답하는 내부 워크플로우
(1)~(8) 번호를 따라가면 LLM serving 상자가 세 번 등장합니다. 계획할 때 한 번, 분석할 때 한 번, 최종 응답에 한 번.
왼쪽 아래 Embedding model → RAG system이 (5) 검색을 담당하고, 그 아래 세 상자(PDF · 인덱스 · 벡터 DB)가 RAG가 딛고 선 자료입니다.
오른쪽 세로줄이 액션 체인입니다 — Action 1의 결과가 Action 2로, Action 2의 결과가 Action 3으로 흘러갑니다. 이게 컨텍스트 체이닝의 그림 버전입니다.
9단계 워크플로우 — 재생해보기
▶ 재생 · 체이닝 토글주황 화살표가 외부 API 왕복입니다. 이게 늘어나면 지연도 비용도 같이 늡니다.
릴레이는 싸지만 정보 손실이 있습니다. generate_analysis가 본 것은
원본 문서가 아니라 요약본입니다. 요약 과정에서 빠진 디테일은 분석에도 없습니다.
"무엇을 다시 검색하고 무엇을 넘겨받을지"는 정확도와 비용 사이의 설계 선택입니다. 정답이 하나 있는 문제가 아닙니다.
- 계획은 LLM이 JSON으로 짜고, ActionExecutor가 순서대로 실행합니다.
- 앞 액션의 결과는 다음 액션의 컨텍스트로 체이닝됩니다 — 그래서 임베딩 호출이 한 번으로 끝났습니다.
- 체이닝은 비용을 줄이는 대신 정보를 잃습니다. 어느 쪽을 택할지가 설계입니다.
04RAG — 오픈북 시험
한 줄 요약: LLM 입장에서 RAG는 프롬프트가 길어진 것뿐입니다. 행렬에 뭘 주입하는 게 아닙니다.
아닙니다. 그런데 이 오해는 처음 배울 때 아주 흔합니다. 그리고 이 오해를 갖고 있으면 왜 컨텍스트 길이가 비용을 결정하는지를 영영 이해할 수 없습니다.
이 장에서는 RAG를 오프라인 인덱스 빌딩과 온라인 질의/검색 두 워크플로우로 갈라 보고, 청킹 크기라는 노브가 정밀도와 문맥 사이에서 무엇을 맞바꾸는지 확인합니다.
- RAG (Retrieval-Augmented Generation)
- 질문할 때 필요한 자료를 검색해 LLM에 같이 넣어주는 방법.
- Chunking (청킹)
- 큰 문서를 작은 덩어리로 나누는 것. 보통 1,000토큰 단위. 검색의 세분화 정도를 정의합니다.
- Vector database (벡터 DB)
- 청크 임베딩을 인덱싱해 저장하고 유사도 검색을 제공하는 저장소.
- Cosine similarity (코사인 유사도)
- 두 벡터가 같은 방향을 얼마나 보는지로 재는 유사도. 벡터 검색의 기본 지표.
- Context window (컨텍스트 윈도우)
- 모델이 한 번에 처리할 수 있는 입력+출력 토큰의 상한. 청킹이 필요한 근본 이유.
- Dilution (희석)
- 청크가 너무 커서 관련 없는 내용이 섞여 정밀도가 떨어지는 현상.
비유 먼저 — 오픈북 시험과 암기
RAG는 오픈북 시험입니다. 매 시험(요청)마다 책(외부 DB)에서 찾아 답안(prompt)에 첨부합니다. 책 내용이 바뀌면 다음 시험부터 바로 반영됩니다.
반대 방향이 Fine-tuning, 즉 암기입니다. 가중치에 지식을 내재화합니다. 빠르지만, 내용이 바뀌면 다시 외워야(재학습) 합니다.
그래서 최신 정보나 사내 문서는 RAG가 유리합니다. 벡터 DB만 갱신하면 모델 재학습 없이 지식이 바뀝니다.
LLM 입장에서 실제로 무슨 일이 일어나나
이 흐름을 한 번 보면 오해가 풀립니다. 중간에 "텍스트 합치기"라고 적힌 구간이 핵심입니다.
↓
[embedding model — RAG 전용, 예: text-embedding-3-small]
↓
query 벡터 (1536차원)
↓
[Vector DB — cosine similarity 검색]
↓
top-5 문서 (카탈로그 5행의 텍스트)
─────────────────────────────────────────
여기부터는 그냥 "텍스트 합치기" — 행렬 주입이 아님
prompt = f"""
{retrieved_doc_1_text}
{retrieved_doc_2_text}
...
Question: 회색 정장에 어울리는 셔츠 추천해줘
"""
─────────────────────────────────────────
↓
토큰화 → X (입력 임베딩 행렬, row가 늘어남)
↓
평소와 똑같은 self-attention
여기서 하나만 기억한다면 — 외부에서 검색했든 사람이 붙여넣었든, LLM은 그 둘을 구분하지 못합니다. 입력이 길어졌을 뿐입니다. 그래서 "검색된 문서가 많을수록 좋다"가 성립하지 않습니다 — 프리필 비용이 그만큼 붙습니다.
두 개의 워크플로우 — 오프라인과 온라인
| Index building (오프라인) | Query/retrieval (온라인) | |
|---|---|---|
| 언제 | 주기적으로 예약 실행 | 사용자가 물을 때마다 실시간 |
| 무엇을 | 문서 정제·파싱 → 청킹(~1,000토큰) → 청크별 임베딩 대량 사전 계산 → 벡터 DB 인덱싱 | 쿼리 임베딩 → 코사인 유사도로 최근접 검색 → 관련 청크 반환 → 쿼리+청크를 LLM에 |
| 성격 | 오프라인 배치 추론 | 온라인 추론 — 지연시간이 사용자에게 보임 |
원래 인덱스 빌딩은 주기적 배치입니다. 그런데 Knowledge Agent는 이식성을 위해
agent.py를 기동할 때마다 인메모리로 다시 빌드합니다.
그래서 테스트를 네 번 돌리면 42개 청크를 네 번 재임베딩합니다. 매번 과금됩니다.
데모니까 괜찮지만, 프로덕션에서 이러면 안 됩니다.
청킹은 단순한 전처리가 아닙니다
청킹은 검색의 세분화 정도를 정의합니다. 시스템이 한 번에 고려할 수 있는 텍스트 양이 여기서 정해집니다.
| Fine (작은 청크) | Coarse (큰 청크) | |
|---|---|---|
| 장점 | 검색 정밀도 ↑ — 딱 필요한 부분만 | 문맥 풍부 — 앞뒤 맥락이 살아 있음 |
| 단점 | 문맥 손실 — 잘린 부분이 답에 필요했을 수 있음 | 관련 없는 내용이 섞여 정밀도 ↓ (dilution) |
| 최적값 | 도메인과 LLM 컨텍스트 윈도우에 따라 달라집니다 — 정답이 하나 있는 문제가 아닙니다 | |
RAG의 두 워크플로우 — 오프라인과 온라인
점선 위가 (A) 인덱스 빌딩, 아래가 (B) 질의/검색입니다.
위쪽은 문서를 쪼개고(split) → 임베딩하고(embedded) → 벡터 DB에 넣습니다(index). 주기적으로 한 번 도는 배치 작업입니다.
아래쪽은 질문을 임베딩하고(embedded) → 검색하고(search) → 관련 조각을 받아(context) → LLM에 넣습니다. 질문할 때마다 매번 돕니다.
두 워크플로우가 Embedding model 상자를 공유한다는 점도 보세요 — 같은 모델로 재야 같은 공간에서 비교가 됩니다.
청킹과 top-k — 무엇을 맞바꾸나
↔ 슬라이더 3개막대가 빨간 선을 넘으면 컨텍스트 윈도우 초과입니다. 청크를 나누는 이유가 이것입니다.
원문도 못 박습니다 — 그림 4-3은 "기본" RAG 파이프라인입니다. 실제 운영 RAG에는 재랭킹(ranking), 중복 제거(deduplication), 다중 소스 검색 같은 복잡성이 더 붙습니다.
- LLM 입장에서 RAG는 프롬프트가 길어진 것입니다. 행렬 주입이 아니라 텍스트 합치기입니다.
- RAG는 오프라인 인덱스 빌딩과 온라인 질의/검색 두 워크플로우로 나뉩니다.
- 청킹 크기는 정밀도 ↔ 문맥의 노브이고, 최적값은 도메인과 컨텍스트 윈도우가 정합니다.
05CAG — 검색 대신 미리 펴놓기
한 줄 요약: 컨텍스트 윈도우가 100만 토큰이 되면 "몇 개만 골라 넣는다"는 전제 자체가 흔들립니다.
RAG는 강력하지만 대가가 있습니다. 검색 단계가 LLM 호출 앞에 끼어들고, 제한된 토큰 안에서 "가장 관련 있는 것"을 고르다 틀릴 수 있고, 임베딩·인덱스·벡터 DB를 유지해야 합니다.
이 장에서는 CAG가 그 세 가지를 어떻게 없애는지, 그리고 대신 무엇을 지불하는지를 봅니다.
- CAG (Cache-Augmented Generation)
- 질의 시점에 검색하는 대신, 지식을 미리 LLM의 KV 캐시에 프리로드해두고 그 캐시된 컨텍스트로 답하는 방식.
- KV cache
- 이미 처리한 토큰의 Key/Value 계산 결과를 저장해둔 것. 다시 계산하지 않으려고 들고 있습니다. (CH2 참고)
- Preload (프리로드)
- 쓰기 전에 미리 올려두는 것. 첫 비용은 크고, 이후 재사용마다 이득이 납니다.
- Prompt caching
- 반복되는 프롬프트 앞부분의 계산 결과를 재사용하는 기법. CAG와 같은 계열의 아이디어입니다.
비유 먼저 — 매번 서고에 갈 것인가, 책상에 펴놓을 것인가
RAG는 질문이 올 때마다 서고로 뜁니다. 서고가 아무리 크더라도 필요한 것만 가져오니 책상은 깨끗합니다. 대신 매번 왕복이 있고, 잘못 집어올 수도 있습니다.
CAG는 필요한 책을 미리 책상에 다 펴놓습니다. 질문이 오면 고개만 돌리면 됩니다. 대신 책상이 아주 커야 하고, 펴놓는 데 처음 한 번 시간이 듭니다.
어느 쪽이 나은지는 질문이 몇 번 오는가에 달렸습니다. 한 번이면 서고가 낫고, 백 번이면 책상이 낫습니다.
왜 지금 CAG가 나왔나
이유는 하나입니다. 컨텍스트 윈도우가 커졌기 때문입니다.
Claude Sonnet 4(2025년 8월 기준) 같은 모델이 100만 토큰 컨텍스트를 지원하게 되면서, "몇 개만 골라 넣는다"는 RAG의 전제 자체가 흔들립니다. 지식 베이스 전체(또는 상당 부분)를 통째로 넣을 수 있다면, 복잡한 검색 시스템 없이도 LLM의 고질적 한계(정적 지식·환각·도메인 갭)를 다룰 수 있다는 것입니다.
| RAG | CAG | |
|---|---|---|
| 언제 지식을 넣나 | 쿼리마다 동적으로 검색 | 한 번 캐시에 프리로드, 이후 재사용 |
| 강한 곳 | 동적 검색 · 최신성 | 반복 쿼리 · 고정 지식셋에서 지연시간 |
| 비용 | 매 쿼리마다 검색 지연 + 인덱스 유지 | 큰 컨텍스트 윈도우 → 메모리·연산 요구 ↑ |
| 개선하는 것 | 답변 품질 (지식 접지·최신성) | 서빙 효율 (지연·처리량·비용) |
RAG와 CAG의 구조 비교
위쪽 RAG에는 상자가 많습니다 — RAG system 아래에 지식 소스 · 임베딩 모델 · 인덱스 · 벡터 DB 네 개가 매달려 있습니다. 질문이 올 때마다 이 경로를 한 번씩 돕니다.
아래쪽 CAG에는 검색 상자가 하나도 없습니다. 사용자 질문이 곧바로 LLM으로 갑니다. 지식은 이미 Knowledge cache(=KV 캐시) 안에 들어와 있기 때문입니다.
점선 화살표 preload가 그 차이의 전부입니다 — 언제 지식을 넣느냐만 다릅니다.
CAG를 쓰는 에이전트 워크플로우
그림 4-2와 나란히 놓고 보시면 차이가 한눈에 들어옵니다. 단계가 8개에서 3개로 줄었습니다. Embedding model도, Vector DB도, 검색 화살표도 없습니다.
대신 오른쪽 위에 Knowledge and context cache가 붙었습니다. 설계가 단순해진 만큼 캐시를 채워두는 비용이 앞에 한 번 붙고, 큰 컨텍스트를 유지할 메모리가 필요해집니다.
RAG vs CAG — 몇 번 물으면 뒤집히나
↔ 슬라이더 · 버튼CAG의 첫 막대만 유난히 큽니다. 프리로드는 한 번이고, 그 뒤로는 거의 공짜입니다.
원문의 결론은 명확합니다 — RAG로 입력을 보강하고, CAG로 실행을 최적화하는 조합.
RAG는 답변 품질을 올리고, CAG는 서빙 효율을 올립니다. 개선하는 축이 아예 다릅니다.
큰 컨텍스트 + 캐시 관리의 메모리·연산 비용을 다루는 기법은 7장에서 이어집니다.
- RAG의 대가는 검색 지연 · 선택 오류 · 시스템 복잡도 세 가지입니다.
- CAG는 지식을 KV 캐시에 프리로드해 그 셋을 줄입니다. 대신 메모리·연산을 지불합니다.
- 둘은 경쟁이 아니라 조합입니다 — RAG는 품질, CAG는 효율을 개선합니다.
— Part 2. 엔터프라이즈 서빙의 계층 구조 —
06일곱 개의 층 — 그리고 그것이 조직도인 이유
한 줄 요약: "모델 실행"과 "엔터프라이즈 서빙"은 다른 문제입니다. 층을 나누는 진짜 이유는 조직에 있습니다.
지금까지 만든 것 — 단일 모델 서빙, 멀티모델 서빙(Triton 포함), 샘플 RAG 에이전트 — 은 전부 "모델을 호스팅하고 실행한다"는 좁은 의미의 서빙이었습니다.
실제 대규모 프로바이더는 그 위에 인증 · 과금 정책 · 리소스 관리 · 네트워킹 · 최적화 · 실험(A/B) · 관측성 · 온콜 지원까지 감당합니다. 이 장은 그것들이 어디에 얹히는지를 봅니다.
- Tenant (테넌트)
- 플랫폼을 쓰는 고객 단위. 쿼터·과금·라우팅이 전부 이 값 하나에 매달립니다.
- Rate limiting
- 단위 시간당 요청 수를 제한하는 것. 어뷰징 방어와 공정 사용을 위해.
- Capacity planning (용량 계획)
- 수요를 예측해 필요한 만큼만 준비하는 일. 과다 프로비저닝은 그대로 비용입니다.
- Preemption (선점)
- 저우선순위 작업을 중단시키고 자원을 회수하는 것. 중요 고객을 위해.
- Speculative decoding
- 빠른 draft 모델이 여러 토큰을 미리 만들고, 정확한 target 모델이 한꺼번에 검증하는 기법. (7장에서 상세히)
- Observability (관측성)
- 시스템 내부 상태를 밖에서 알 수 있게 만드는 것 — 메트릭·로그·트레이스.
비유 먼저 — 층이 나뉘는 건 배관 때문이 아닙니다
큰 건물이 층으로 나뉘는 이유는 구조공학 때문만이 아닙니다. 층마다 다른 조직이 들어가고, 층 경계가 곧 책임 경계이기 때문입니다.
원문은 이 어려움이 기술적인 것보다 조직적인 것에 가깝다고 말합니다. 서로 다른 책임을 가진 여러 팀이 병목이나 과도한 상호 의존 없이 하나의 진화하는 시스템에 동시에 기여할 수 있어야 하니까요.
그래서 좋은 서빙 아키텍처를 만든다는 건 "레이턴시를 줄이는 기술" 문제가 아니라 "어떤 레이어를 누가 소유하고, 경계를 어떻게 그어야 팀들이 서로 발목 잡지 않는가"라는 설계 문제이기도 합니다.
일곱 층, 각각의 난제
| 층 | 이름 | 핵심 난제 |
|---|---|---|
| 1 | Public API | 고동시성(수백만 연결) · 공정 사용과 수익화 · 저지연 글로벌 접근 · 보안 |
| 2 | Resource Management | 용량 계획 · 이기종 GPU 풀의 고가동률 · 고객 우선순위(쿼터·예약·선점) |
| 3 | Model Selection & Orchestration | 비용-품질 트레이드오프 · 로드 밸런싱 · 지연 민감 케이스 대응 |
| 4 | Distributed Serving | 단일 GPU 메모리 초과 · 멀티GPU/멀티노드 조율 · 캐시 인식 라우팅 |
| 5 | Core Inference | 모델별 의존성 차이 · load/unload가 만드는 지연 스파이크 · cold start · hot model 스케일링 |
| 6 | Model Optimization | 재학습 없이 성능·효율 개선 (양자화 6장 · speculative decoding·LMCache 7장 · 종단간 9장) |
| 7 | Model | 학습 파이프라인/외부 소스에서 운영으로 이동 · 기능·목적별 분류 · 추적과 버전 관리 |
작은 숫자로 한 번 — 3층이 왜 존재하나
3층(Model Selection)의 존재 이유는 한 문장으로 설명됩니다.
"1+1?" 에 거대한 Reasoning Model을 쓸 필요는 없습니다.
단순 질문 → Small Model
복잡한 Reasoning → Large Model
# 세 요소의 Trade-off
Quality · Latency · Cost
실제로 OpenAI가 기본값으로 gpt-4o-mini를 쓰는 것이 이 층의 판단입니다.
모든 작업에 최대 모델이 필요하지 않으므로, 쿼리를 이해해서 분류(triaging)해야 합니다.
4층 — 모델이 GPU 하나에 안 들어갈 때
Model Weight = 160 GB
GPU VRAM = 80 GB
# 그러면 최소한 여러 GPU로 나눠야 합니다.
GPU 0 + GPU 1
# 이 Layer가 관리하는 것
Multi-GPU · Multi-Node · KV Cache · Prompt Cache · Cache-aware Routing
실전 예시도 있습니다. KubeCon 2025 발표 기준 GPU 8개짜리 노드 1대면 노드 안에서 Tensor Parallel(TP) 8, 노드 2대면 거기에 Pipeline Parallel(PP) 2를 얹습니다. TP는 노드 안에서, PP는 노드 사이에서 자릅니다.
엔터프라이즈 모델 서빙 계층 아키텍처
원문이 이 절에서 제시하는 레퍼런스 아키텍처입니다. 위로 갈수록 사용자에 가깝고 아래로 갈수록 하드웨어에 가깝습니다.
층마다 안고 있는 것이 다릅니다 — 1층은 인증·과금·라우팅, 2층은 하드웨어 프로비전, 3층은 어떤 모델을 쓸지, 4층은 여러 GPU에 나누기, 5층은 실제 추론 엔진, 6층은 재학습 없는 최적화, 7층은 모델 자체입니다.
맨 아래 칩들(Voice · Reasoning · Video · Code …)이 모델을 기능과 목적으로 분류한 것입니다.
노드 1대(TP 8) vs 노드 2대(TP 8 · PP 2)
4층 Distributed serving이 실제로 무엇을 하는지 보여주는 그림입니다.
① GPU 8장짜리 노드 하나에서는 Tensor Parallel 8 — 한 층의 행렬을 8조각으로 갈라 8장이 동시에 계산합니다. 노드 안이라 GPU 사이 대역폭이 넓어서 가능합니다.
② 노드가 둘이 되면 거기에 Pipeline Parallel 2가 얹힙니다 — 층 묶음을 노드끼리 나눠 갖습니다. 노드 사이는 느리니 층 경계에서만 주고받게 자르는 것입니다.
일곱 개의 층 — 눌러서 들여다보기
↔ 층 클릭층마다 난제가 다릅니다. 그래서 소유하는 팀도 다릅니다.
새로운 모델과 기술이 계속 시스템 설계를 재편할 것입니다.
그래도 API · 오케스트레이션 · 추론 · 최적화 같은 관심사를 분리하는 계층형 패턴은 남습니다.
서로 다른 역할의 팀들이 독립적으로 혁신하면서도 하나의 플랫폼에 기여할 수 있게 하니까요.
- 엔터프라이즈 서빙은 일곱 층입니다 — Public API · Resource · Selection · Distributed · Core Inference · Optimization · Model.
- 층마다 난제가 다르고, 그래서 소유하는 팀도 다릅니다. 층 경계 = 책임 경계.
- 층을 나누는 목적은 팀들이 서로 발목 잡지 않고 동시에 기여하게 만드는 것입니다.
— Part 3. 오픈소스 스택으로 직접 짓기 —
07Public API — 인증 · 쿼터 · 오토스케일
한 줄 요약: 두 가지 인증 방식이 결국 같은 목적지로 수렴합니다 — tenant 값 하나.
생각보다 적습니다. FastAPI 데코레이터 하나, 의존성 함수 하나, YAML 두 장이면 고동시성 · 공정 사용 · 보안의 뼈대가 섭니다.
이 장에서는 그 코드를 보고, 왜 rate limit이 두 군데에 걸려 있는지(defense in depth)를 확인합니다.
- JWT (JSON Web Token)
- 서명된 토큰. 공개키로 서명을 검증하고 만료·수신자(audience)를 확인합니다. 클레임 안에 tenant가 들어 있습니다.
- Dependency injection (의존성 주입)
- FastAPI의
Depends(). 인증 같은 공통 로직을 선언적으로 끼워넣어 핸들러는 비즈니스 로직만 갖게 합니다. - HPA (HorizontalPodAutoscaler)
- 지표(CPU 등)를 보고 파드 수를 자동으로 늘리고 줄이는 Kubernetes 리소스.
- Ingress
- 클러스터 바깥에서 들어오는 HTTP 트래픽의 관문. Nginx·Envoy 같은 구현체가 붙습니다.
- Burst (버스트)
- 순간적으로 제한을 넘겨 허용하는 여유분.
limit-rps 50+burst ×5면 순간 최대 250건. - Defense in depth
- 같은 위협을 여러 층에서 중복 방어하는 패턴. 하나가 뚫려도 다음이 막습니다.
비유 먼저 — 정문과 창구
건물에 들어가려면 정문 경비를 지납니다. 경비는 신분증만 봅니다. 누가 몇 번 들어왔는지 세지 않고, "초당 이 문으로 50명까지"라는 거친 기준만 적용합니다. 값싸고 빠릅니다.
안에 들어가면 창구가 있습니다. 여기선 당신 회사가 이번 달에 몇 건까지 계약했는지를 봅니다. 정교하지만 사람이 붙어야 하니 비쌉니다.
둘 다 있는 이유는 명확합니다. 정문이 대량 어뷰징을 값싸게 걸러내고, 창구가 계약을 정교하게 강제합니다.
바닥은 Kubernetes입니다
이 설계의 근간은 Kubernetes입니다. 저자들이 고른 이유는 두 가지입니다.
① 핵심 기능 자체 — 컨테이너화된 애플리케이션의 배포·스케일링·관리 자동화.
② 거대한 생태계 — 그 위에 쌓인 메트릭·로깅·네트워킹·인가·하드웨어 관리.
이 둘 덕분에 Kubernetes는 AWS·GCP·Azure 같은 클라우드 프로바이더와 OpenAI·Anthropic 같은 파운데이션 모델 벤더 양쪽 모두의 백본으로 채택됐습니다.
리소스 관리 · 네트워킹 · 트래픽 라우팅과 로드 밸런싱 · 서비스 호스팅과 스케일링 · 서비스 메트릭과 모니터링.
다시 말해 레이어 1의 라우팅/네트워킹 부분과 레이어 2 전체를 실제 오픈소스 컴포넌트로 구현해주는 기반 계층입니다.
코드 — 엔드포인트는 이게 전부입니다
@app.post("/v1/chat/completions")
async def chat(req: ChatReq, idp=Depends(require_auth)):
await rate_limit(idp["tenant"])
# select model, route traffic based on tenant information
핸들러 본문에 인증 코드가 한 줄도 없습니다. Depends()가 require_auth를 선언적으로 끼워넣기 때문입니다.
핸들러는 "인증된 테넌트로 쿼터를 걸고 라우팅한다"는 비즈니스 로직만 갖습니다.
async def require_auth(api_key=Depends(verify_api_key), claims=Depends(verify_jwt)):
if not api_key and not claims:
raise HTTPException(401, "Missing API key or JWT")
tenant = claims.get("tenant") if claims else await rds.hget(f"keys:{api_key}", "tenant")
if not tenant: raise HTTPException(403, "Unknown tenant")
return {"tenant": tenant, "claims": claims, "api_key": api_key}
여기서 하나만 기억한다면 — 두 인증 경로가 같은 목적지로 수렴합니다.
· JWT 경로: Authorization: Bearer ...를 파싱해 RSA 공개키(JWK)로 서명 검증 + audience/만료 확인 → 클레임에서 tenant 추출
· API 키 경로: 키를 Redis(rds.hget)에서 조회해 tenant 역참조
이후 단계(쿼터 강제 · 트래픽 라우팅 · 모델 선택)가 전부 이 tenant 값 하나에 의존하기 때문입니다.
고동시성 — HPA
kind: HorizontalPodAutoscaler
metadata: { name: enterprise-model-api-hpa }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: enterprise-model-api }
minReplicas: 3
maxReplicas: 15
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
평균 CPU 사용률이 70%를 넘으면 Kubernetes가 인스턴스를 3개 → 최대 15개까지 수평 확장합니다.
공정 사용 — Ingress rate limit
metadata:
annotations:
kubernetes.io/ingress.class: nginx
# 초당 50개 요청 제한
nginx.ingress.kubernetes.io/limit-rps: "50"
nginx.ingress.kubernetes.io/limit-burst-multiplier: "5"
nginx.ingress.kubernetes.io/proxy-body-size: "8m"
초당 50건, 버스트는 5배(순간 최대 250건)까지 허용합니다.
오픈소스 스택으로 구현한 모델 서빙
맨 위 Kubernetes 띠가 이 그림의 바닥입니다 — 리전별 클러스터까지 여기서 관리합니다.
아래 세 상자가 요청이 지나는 순서입니다. Public API(Envoy/Nginx + FastAPI + OAuth) → Model selection & orchestration(설정 기반 선택 · Ray Serve 또는 직접 구현) → Model endpoints(SVC).
엔드포인트에서 갈라진 화살표가 아래 Pod 여섯 개로 내려갑니다. 그 Pod 하나를 열어본 것이 왼쪽 아래 상자입니다 — GPU + 최적화 + vLLM + FastAPI. 4장의 일곱 계층이 실제 오픈소스 부품으로 어떻게 채워지는지가 이 한 장에 들어 있습니다.
누가 누구를 관리하는가
같은 스택을 관리 관계로 다시 그린 것입니다. 실선은 요청이 흐르는 길, 점선은 Kubernetes가 "관리"하는 대상입니다.
Kubernetes에서 점선이 세 갈래로 뻗습니다 — FastAPI · Ray Serve · vLLM. 요청 경로에는 안 나타나지만 배포·스케일링·복구를 전부 이 층이 합니다.
Prometheus/Grafana도 요청 경로 밖에서 점선으로 붙습니다 — 관측성은 트래픽과 별개의 축이라는 뜻입니다.
부하가 올라가면 무슨 일이 — HPA와 rate limit
↔ 슬라이더 2개인그레스에서 잘린 요청은 파드에 부하를 주지 않습니다. 그래서 값싼 방어입니다.
- 인증은 JWT든 API 키든 결국
tenant하나로 수렴합니다. 그 뒤 모든 결정이 여기 매답니다. - 고동시성은 HPA(CPU 70%, 3~15 레플리카)가, 공정 사용은 Ingress rate limit(50rps, burst ×5)이 맡습니다.
- rate limit이 두 층에 걸린 건 실수가 아니라 defense in depth입니다 — 거친 방어 + 정교한 강제.
08모델 선택 — 카나리는 왜 테넌트보다 먼저인가
한 줄 요약: 순서 하나가 카나리 배포의 대표성을 결정합니다.
그 열 줄 안에 순서가 결과를 바꾸는 지점이 있습니다. 카나리 체크가 테넌트 오버라이드보다 먼저 오는 게 실수처럼 보이는데, 의도된 설계입니다.
이 장에서는 라우팅 정책 네 단계를 따라가고, speculative decoding을 언제 켜는지의 기준(1,024토큰)이 왜 그 숫자인지 봅니다.
- Canary deployment (카나리 배포)
- 새 버전에 트래픽 일부만 먼저 흘려 문제를 조기에 잡는 배포 방식.
- Draft model / Target model
- speculative decoding에서 빠르고 저렴한 쪽과 느리지만 정확한 쪽.
- Passthrough (패스스루)
- 중간에서 손대지 않고 그대로 흘려보내는 것. 여기서는 스트리밍 응답을 그대로 전달.
- Tenant override
- 특정 고객에게 전용 엔드포인트(전용 파인튜닝 모델, 전용 용량)를 지정하는 것.
- Alias (별칭)
- 모델 이름의 다른 표기를 실제 라우트로 이어주는 매핑.
비유 먼저 — 표본을 어디서 뽑는가
신제품을 시험 판매합니다. 전체 손님의 5%에게 새 레시피를 내놓기로 했습니다.
그런데 "단골은 기존 레시피 고정"이라는 규칙을 먼저 적용하면 어떻게 될까요. 새 레시피는 뜨내기 손님에게만 나갑니다. 그 반응으로 전체를 판단하면 틀립니다.
그래서 5% 추첨을 먼저 돌리고, 거기 안 걸린 사람에게 단골 규칙을 적용합니다. 코드의 순서가 정확히 그렇게 되어 있습니다.
코드 — 라우팅 정책 네 단계
cfg = load_routes()
# ① 모델/별칭 조회 — 없으면 404
route = cfg["models"].get(model) or cfg["aliases"].get(model)
if not route: raise HTTPException(404, f"Unknown model {model}")
# ② weighted canary — 테넌트 오버라이드보다 먼저!
if "canary" in route and random() < float(route["canary"]["weight"]):
return route["canary"]["url"]
# ③ tenant override
route_over = (route.get("tenants") or {}).get(tenant)
if route_over: return route_over["url"]
# ④ 기본 라우트로 폴백
return route["url"]
여기서 하나만 기억한다면 — ②가 ③보다 먼저인 이유는 카나리 샘플이 전체 트래픽에서 무작위로 뽑히게 하기 위해서입니다. 전용 라우팅을 가진 테넌트도 포함해서요. 그래야 카나리 배포가 특정 고객군에 편향되지 않고 대표성을 갖습니다.
모델 선택 — 1,024토큰이라는 기준선
async def chat(req: ChatReq, idp=Depends(require_auth)):
await rate_limit(idp["tenant"])
ep = choose_endpoint(req.model, idp["tenant"])
# basic classifier: use speculation for long outputs, else direct
if req.model.draft_enabled and req.max_new_tokens > 1024:
draft_ep = config.get_draft_endpoint(req.model)
gen = speculative_decode(req, endpoint_draft=draft_ep, endpoint_target=ep)
else:
gen = passthrough(ep) # simple pass-through stream
왜 하필 1,024일까요. speculative decoding은 두 모델을 동시에 돌리는 오버헤드가 있습니다. 생성할 토큰이 많을수록 draft 모델의 속도 이득이 그 오버헤드를 상쇄하고도 남습니다. 짧은 응답에는 오히려 손해라서, 출력 길이로 두 전략을 분기하는 게 합리적인 휴리스틱입니다.
세 군데 중 하나입니다 — Public API 안, 별도 미들웨어 서비스, 정적 라우팅 설정.
이 예시는 요구사항이 단순해서 Public API의 chat 엔드포인트 안에 두었습니다.
분류 규칙이 복잡해지면 별도 서비스로 빼는 게 보통입니다.
요청 1,000건을 흘려보내면 어디로 가나
↔ 슬라이더 · 버튼순서를 바꾸면 카나리 막대의 구성이 달라집니다. 총량이 아니라 누가 들어있느냐가 문제입니다.
- 라우팅은 모델 조회 → 카나리 → 테넌트 오버라이드 → 기본 네 단계입니다.
- 카나리가 테넌트보다 먼저인 건 의도입니다 — 표본의 대표성을 지키기 위해서.
- speculative decoding은 긴 출력에서만 이득입니다. 그래서
max_new_tokens > 1024로 분기합니다.
09Ray Serve — 데코레이터 한 줄이 YAML을 대체할 때
한 줄 요약: "레플리카 3개, 각각 GPU 1개씩"이 파이썬 데코레이터 한 줄로 끝납니다.
인스턴스 서빙의 확장성과 네트워크 관리가 부담스러울 때 꺼내는 카드가 Ray Serve입니다. 확장 가능하고 프레임워크에 구애받지 않는 모델 서빙 라이브러리입니다.
이 장에서는 3장의 ModelWorker와 Ray Serve의
@serve.deployment를 나란히 놓고, 무엇이 위임되는지를 봅니다.
- Deployment (Ray Serve)
- Ray Serve의 확장 단위. 데코레이터로 선언하면 Ray가 자원을 배정하고 배포합니다.
- Replica (레플리카)
- Deployment의 실행 인스턴스. 내부적으로는 Ray Actor입니다.
- AsyncLLMEngine
- vLLM의 비동기 엔진. 배칭·큐잉·스케줄링을 엔진이 알아서 합니다.
- DeploymentHandle
- 레플리카 사이의 RPC 통신 핸들. Ingress에서 LLMServer를 부를 때 씁니다.
- Prefill-Decode 분리
- 프리필과 디코드를 독립적으로 스케일링하고 단계별로 다른 GPU를 배정하는 방식.
비유 먼저 — 주문서와 계약서
3장에서 우리는 계약서를 직접 썼습니다. 프로세스를 몇 개 띄우고, 큐를 어떻게 잇고, 배치를 언제 자를지 전부요.
Ray Serve는 주문서 한 장입니다. "레플리카 3, GPU 1개씩"이라고 적어 내면 나머지는 알아서 됩니다.
주문서가 편한 만큼 세부 조정은 프레임워크가 허용하는 범위 안입니다. 이게 이 장 이후로 계속 나올 편의성 ↔ 통제권의 첫 등장입니다.
단일 모델 호스팅 — 코드
class QwenVLLM:
def __init__(self):
args = AsyncEngineArgs(
model="Qwen3-Thinking-2507",
tensor_parallel_size=1,
trust_remote_code=True,
max_num_batched_tokens=4096,
gpu_memory_utilization=0.9,
)
self.engine = AsyncLLMEngine.from_engine_args(args)
# HTTP 요청 진입점
async def __call__(self, http_request: Request) -> str:
body = await http_request.json()
return await self.generate_text(body["text"],
max_new_tokens=body.get("max_new_tokens", 256),
temperature=body.get("temperature", 0.7))
그리고 데코레이터 한 줄이 배포를 정합니다.
@serve.deployment(
name="qwen_vllm",
# declare the number of service instances for the model
num_replicas=3,
# needs a GPU for speed
ray_actor_options={"num_cpus": 0.5, "num_gpus": 1},
)
class QwenVLLM:
...
여기서 하나만 기억한다면 — 앞 장의 HPA YAML(minReplicas/maxReplicas/CPU 임계값)이 하던 일을 파이썬 데코레이터 한 줄이 대체합니다. 리소스 프로비저닝과 배포를 Ray가 처리합니다.
| 3장에서 직접 만든 것 | Ray Serve에서는 |
|---|---|
ModelWorker — 모델 한 번 로드, generate()로 처리 |
QwenVLLM.__init__ + __call__ — 개념적으로 동일한 패턴 |
WorkloadManager — 수동 배칭/큐잉을 직접 구현 |
vLLM AsyncLLMEngine에 통째로 위임 |
프로세스 기동·IPC — model_executor.py |
@serve.deployment 한 줄 — Ray가 자원 배정과 배포 담당 |
Ray Serve LLM — 분산 LLM 서빙에 특화된 층
vLLM 같은 추론 엔진을 Ray Serve의 배포 프리미티브 위에서 수평 확장시키는 층이 따로 있습니다. 구성 요소는 셋입니다.
| 구성 요소 | 역할 |
|---|---|
| OpenAiIngress | FastAPI 기반 진입점. OpenAI 호환 엔드포인트 제공, 요청 라우팅, 모델 멀티플렉싱(LoRA 어댑터 관리 포함) |
| LLMServer | 추론 엔진 인스턴스를 감싸는 배포 단위. 세 모드 — 독립형 · 배포 내 조율(데이터 병렬 어텐션) · 배포 간 조율(prefill-decode 분리) |
| Ray Serve Primitives | Deployment(확장 단위) · Replica(Ray Actor) · DeploymentHandle(레플리카 간 RPC)을 그대로 재사용 |
요청은 이렇게 흐릅니다 — 클라이언트 → OpenAiIngress(라우팅) → DeploymentHandle(RPC) → LLMServer Replica → vLLM 엔진(GPU) → 응답. 이때 같은 노드의 레플리카를 우선 라우팅해서 크로스노드 오버헤드를 줄이는 게 특징입니다.
Ingress : LLMServer = 2 : 1 비율이 권장됩니다.
target_ongoing_requests로 컴포넌트별 스케일 균형을 잡습니다.
Ingress는 가볍고 LLMServer는 무거우니, 앞단을 더 많이 두는 게 자연스럽습니다.
레플리카를 늘리면 무엇이 늘고 무엇이 안 느나
↔ 슬라이더 · 버튼GPU가 모자라면 레플리카는 Pending에 멈춥니다. 선언만으로 자원이 생기지는 않습니다.
@serve.deployment(num_replicas=3, num_gpus=1)한 줄이 HPA YAML이 하던 일을 대체합니다.- 수동 배칭/큐잉은 vLLM AsyncLLMEngine에 위임됩니다 — 3장에서 손으로 짠 부분입니다.
- 편해진 만큼 조정은 프레임워크가 허용하는 범위 안입니다. 편의성 ↔ 통제권의 첫 등장입니다.
10멀티플렉싱 — 우리가 손으로 짠 LRU를 프레임워크가
한 줄 요약: 3장의 ModelManager가 @serve.multiplexed 한 줄로 사라집니다.
OrderedDict로 LRU를 직접 짰는데, 그게 그렇게 흔한 문제였나?"네. 너무 흔해서 프레임워크가 기능으로 내장하고 있습니다. 이 장은 "직접 만들어본 사람이 프레임워크를 더 잘 쓴다"는 이 책의 주장을 가장 선명하게 보여주는 대목입니다.
3장 코드와 Ray Serve 기능을 한 줄씩 대응시켜 보고, lazy loading의 대가가 어디서 나타나는지 확인합니다.
- Model multiplexing
- 하나의 레플리카 풀에서 여러 모델을 서빙하는 것. 비슷한 입력 타입의 모델 그룹에 적합합니다.
- Lazy loading
- 미리 올리지 않고 요청이 올 때 로드하는 방식. 메모리를 아끼는 대신 첫 요청이 느립니다.
- LRU eviction
- 가장 오래 안 쓴 것부터 내보내는 캐시 교체 정책.
- Cold start
- 캐시에 없는 모델을 처음 부를 때 로딩 시간이 그대로 지연으로 드러나는 현상.
- Model registry
- model_id를 실제 모델 위치로 매핑하는 표. S3든 HF 리포든 어디든 가리킬 수 있습니다.
비유 먼저 — 책상 위에 몇 권을 올려둘 것인가
책이 세 권 필요한데 책상에는 두 권만 올라갑니다. 세 번째를 펴려면 가장 오래 안 본 책을 덮어야 합니다.
3장에서는 이 규칙을 우리가 직접 썼습니다.
OrderedDict를 쓰고, 쓸 때마다 move_to_end()하고, 넘치면 popitem(last=False)했습니다.
Ray Serve에서는 "책상에 두 권"이라고 적기만 하면 됩니다. 나머지는 프레임워크가 합니다.
대응표 — 한 줄씩 맞춰보기
| 직접 만든 것 (ch03/multi_model_serving) | Ray Serve의 대응 기능 |
|---|---|
ModelManager (OrderedDict LRU, max_models=2) |
@serve.multiplexed(max_num_models_per_replica=2) |
ModelEngine.create_worker() — 요청 시점에 없으면 새로 로드 |
load_model(model_id) — await self.load_model(...)로 요청마다 확인 |
config/models.json (model_id → 모델명 매핑) |
MODEL_REGISTRY dict (model_id → HF repo) |
server.py의 model_id 필드로 라우팅 |
HTTP 헤더 ray_serve_multiplexed_model_id |
코드 — 요청마다 모델을 고릅니다
num_replicas=2, # two instances/replicas
ray_actor_options={"num_cpus": 1}
)
class MultiModelService:
async def __call__(self, request: Request):
# Pick the model id for this request
model_id = serve.get_multiplexed_model_id()
# load model and run model inference
tokenizer, model = await self.load_model(model_id)
inputs = tokenizer(text, return_tensors="pt")
with torch.no_grad():
logits = model(**inputs).logits
probs = torch.softmax(logits, dim=-1).tolist()[0]
return JSONResponse({"model_id": model_id, "label_probs": probs})
단일 모델과 거의 같습니다. 차이는 하나 — 단일 모델은 __init__에서 미리 로드하는데,
멀티모델은 요청마다 await self.load_model(model_id)로 실시간 로드를 시도합니다.
이게 lazy loading의 대가입니다. 완화하는 방법이 데코레이터 하나입니다 —
load_model에 @serve.multiplexed를 달면 Ray Serve가 캐싱을 적용하고,
메모리 같은 자원이 부족할 때 모델 교체를 관리합니다.
# This model source could be S3 or any other storage
MODEL_REGISTRY = {
"distilbert-sst2": "distilbert-base-uncased-finetuned-sst-2-english",
"bert-sst2": "textattack/bert-base-uncased-SST-2",
"roberta-sst2": "roberta-base-openai-detector",
}
@serve.deployment(ray_actor_options={"num_cpus": 1})
class MultiModelService:
# enable multi model support and set model count per replica
@serve.multiplexed(max_num_models_per_replica=2)
async def load_model(self, model_id: str) -> Tuple[AutoTokenizer, torch.nn.Module]:
"""
Lazily load & cache a model per model_id on this replica.
Ray Serve will LRU-evict when cached models
exceed max_num_models_per_replica.
"""
repo = MODEL_REGISTRY[model_id]
tokenizer = await asyncio.to_thread(AutoTokenizer.from_pretrained, repo)
model = await asyncio.to_thread(
AutoModelForSequenceClassification.from_pretrained, repo)
model.eval()
return tokenizer, model
모델 지정은 헤더로
curl -s -X POST "http://127.0.0.1:8000/SentimentService" \
-H 'content-type: application/json' \
-H 'ray_serve_multiplexed_model_id: distilbert-sst2' \
-d '{"text":"I absolutely loved this!"}'
Ray가 백엔드에서 트래픽 라우팅 · 부하 분산 · 자원 관리를 담당합니다.
멀티플렉싱 — 요청 하나가 캐시를 만나는 순서
@serve.multiplexed가 내부에서 하는 판단을 플로우차트로 편 것입니다.
분기는 두 번뿐입니다. ① 캐시에 있나? 있으면 그대로 씁니다(cache hit). 없으면 ② 캐시가 꽉 찼나? 를 묻고, 꽉 찼으면 LRU 축출을 한 뒤 로드합니다.
3장에서 OrderedDict로 손수 짠 로직이 정확히 이 모양이었습니다. 이제는 데코레이터 인자 하나(max_num_models_per_replica)가 "꽉 찼나?"의 기준을 정합니다.
모델 세 개, 자리 두 개 — 캐시가 도는 걸 보기
▶ 재생 · 모델 버튼히트율이 낮으면 로딩 지연이 그대로 사용자 지연이 됩니다. 이게 cold start입니다.
원문의 결론입니다. 움직이는 요소가 많아 보이지만 대부분 이미 프로덕션에서 검증된 것들입니다.
인프라는 Kubernetes, 모델 호스팅은 Ray Serve 또는 Triton,
분산 LLM 서빙과 최적화는 vLLM. 여기에 가벼운 맞춤 미들웨어만 얹으면
완전한 엔터프라이즈급 솔루션을 직접 구축하는 것도 가능합니다.
@serve.multiplexed(max_num_models_per_replica=2)가 3장의ModelManager전체를 대체합니다.- 모델 지정은 HTTP 헤더
ray_serve_multiplexed_model_id로 하고, 라우팅은 Ray가 합니다. - lazy loading의 대가는 cold start입니다. 캐시 자리 수가 그 빈도를 정합니다.
11KubeRay와 RayService — kubectl apply 한 번으로
한 줄 요약: RayCluster는 클러스터만, RayService는 그 위의 Serve 앱까지 함께 관리합니다.
KubeRay라는 Operator가 필요합니다. 그리고 CRD를 고를 때 RayCluster냐 RayService냐라는 선택이 나옵니다.
이 장에서는 그 선택의 기준을 보고,
실제 vllm-service 매니페스트가 어떻게 생겼는지, 배포 후 무엇을 확인하는지를 봅니다.
- Operator (오퍼레이터)
- Kubernetes 위에서 특정 소프트웨어의 생명주기를 자동으로 관리하는 컨트롤러.
- CRD (Custom Resource Definition)
- Kubernetes에 새로운 리소스 종류를 추가하는 정의.
RayCluster같은 것들. - Head Node / Worker Node
- Ray 클러스터의 구조. Head는 Autoscaler · GCS · 드라이버를 추가로 돌리고, Worker는 오직 Task/Actor 실행만 합니다.
- GCS (Global Control Store)
- 클러스터 메타데이터를 관리하는 Head Node의 싱글톤 프로세스.
- In-place update
- 서비스를 내리지 않고 그 자리에서 갱신하는 것. RayService가 Serve 설정에 대해 해주는 일.
- AWQ
- 4비트 계열 양자화 방식. 가중치가 작아져 KV 캐시에 쓸 VRAM이 넉넉해집니다. (6장에서 상세히)
비유 먼저 — 건물만 지을 것인가, 입주까지 맡길 것인가
RayCluster는 건물을 지어줍니다. 입주(Serve 앱 배포)는 직접 해야 하고, 세입자가 나가면(파드가 죽으면) 다시 들이는 것도 직접입니다.
RayService는 건물 + 입주 관리입니다. 임대 조건을 바꾸면(모델 교체, 레플리카 수 변경) 관리인이 알아서 무중단으로 처리합니다.
목표가 "vLLM을 독립형으로 서빙해서 OpenAI 호환 엔드포인트를 노출"이면 RayService가 맞습니다.
Ray의 구조 — 세 층
| 층 | 무엇 |
|---|---|
| Ray Core | 기반. @ray.remote로 함수(Task)나 클래스(Actor)를 분산 실행 |
| Ray Serve | 학습된 모델을 HTTP 엔드포인트로 프로덕션 서빙 |
| Ray Data / Train / Tune / RLlib | 병렬 ETL · 분산 학습 · 하이퍼파라미터 튜닝 · 강화학습 |
클러스터는 Head Node 1개 + Worker Node N개입니다. Head는 워커와 같은 프로세스를 돌리면서 추가로 Autoscaler · GCS · 드라이버를 담당하고, Worker는 클러스터 관리 기능이 전혀 없는 순수 연산 노드입니다.
네 개의 CRD
| CRD | 용도 |
|---|---|
| RayCluster | Head/Worker 파드로 구성된 Ray 클러스터 자체의 생명주기 관리 (기본 단위) |
| RayJob | RayCluster를 만들어 단일 Job을 실행하고 완료되면 정리 (필요시 클러스터 자동 삭제) |
| RayService | RayCluster 위에 Ray Serve 애플리케이션을 얹어 운영 — 무중단 업그레이드, 헬스체크 기반 HA |
| RayCronJob | RayJob을 크론 스케줄로 반복 실행 |
RayCluster vs RayService — 무엇이 다른가
| 항목 | RayCluster | RayService |
|---|---|---|
| 역할 범위 | Ray 클러스터 자체만 관리 | 클러스터 생성 + 그 위의 Serve 앱까지 |
| 배포 방식 | helm install 또는 CR 직접 apply | 단일 매니페스트에 rayClusterConfig + serveConfigV2 |
| 워크로드 접속 | kubectl exec 또는 대시보드 포트포워드 후 ray job submit | Serve용 Service(-serve-svc)로 바로 HTTP |
| Serve 앱 | 없음 — serve run으로 수동 배포·갱신 | 자동 관리 — 설정만 바꾸면 무중단 in-place 업데이트 |
| 헬스체크/HA | k8s가 Serve 상태를 모름 | /-/routes 헬스체크로 k8s가 Serve 상태까지 파악, 장애 시 컨트롤러가 복구 |
| GPU 워커 설정 | 두 리소스가 차이 없음 — 그래서 난이도로 RayCluster를 고를 이유가 없습니다 | |
실제 매니페스트 — 물리 GPU 1장에 맞춘 버전
kind: RayService
metadata: { name: vllm-service, namespace: kuberay }
spec:
serveConfigV2: |
applications:
- name: llms
import_path: ray.serve.llm:build_openai_app
route_prefix: "/"
args:
llm_configs:
- model_loading_config:
model_id: qwen2.5-7b-instruct-awq
model_source: Qwen/Qwen2.5-7B-Instruct-AWQ
engine_kwargs:
dtype: auto
quantization: awq
max_model_len: 4096
gpu_memory_utilization: 0.85
deployment_config:
autoscaling_config:
min_replicas: 1
max_replicas: 1 # 물리 GPU 1장 고정
target_ongoing_requests: 16
max_ongoing_requests: 32
rayClusterConfig:
headGroupSpec: # num-gpus: "0", 이미지 rayproject/ray-llm:2.52.0-py311-cu128
workerGroupSpecs: # num-gpus: "1", nvidia.com/gpu: 1, HUGGING_FACE_HUB_TOKEN
· 모델: Qwen2.5-7B-Instruct-AWQ, quantization: awq 명시(AWQ 커널 강제), dtype: auto
· gpu_memory_utilization: 0.85 — AWQ라 가중치가 4~5GB뿐이라 KV 캐시용으로 넉넉히
· max_model_len: 4096 — VRAM 여유가 생겨 문서 예제(1024)보다 실용적인 길이로 상향
· nvidia.com/gpu: 4→1, max_replicas: 4→1 — 물리 GPU 1장에 맞게 축소
· worker CPU/메모리 32/32Gi → 8/24Gi — 모니터링 스택과 공존하려고
배포 후 무엇을 확인하나
kubectl get pods -n kuberay
NAME READY STATUS
vllm-service-g6qjt-gpu-group-worker-wh275 1/1 Running
vllm-service-g6qjt-head-ps2zr 1/1 Running
kubectl get rayservice -n kuberay
NAME SERVICE STATUS NUM SERVE ENDPOINTS
vllm-service Running 2
# applicationStatuses가 HEALTHY / RUNNING 이 될 때까지 대기
LLMServer:qwen2_5-7b-instruct-awq: Status: HEALTHY
Open Ai Ingress: Status: HEALTHY
그리고 NodePort 30005로 노출한 뒤 OpenAI 호환 엔드포인트를 그대로 호출합니다.
-H 'Content-Type: application/json' \
-d '{"model": "qwen2.5-7b-instruct-awq",
"messages": [{"role":"user","content":"안녕, 너는 어떤 모델이야?"}]}' | jq
"content": "안녕하세요! 저는 Alibaba Cloud에서 만든 Qwen이라는 언어 모델입니다..."
Ray Cluster 구조와 KubeRay 스택
위쪽이 Ray 클러스터 자체입니다. Head Node만 Autoscaler · GCS · Driver 세 싱글톤을 더 돌리고, Worker Node는 연산만 합니다 — 클러스터 관리 기능이 아예 없습니다.
Autoscaler가 리소스 요청을 감지해 워커를 늘리고, 노는 워커는 없앱니다.
아래쪽이 KubeRay — 이 Head/Worker를 Kubernetes 파드로 바꿔 관리합니다. 맨 아래 EKS·GKE·AKS·자체 K8s 어디에 올려도 위층은 그대로라는 것이 이 스택의 값어치입니다.
kubectl apply 이후 — 무엇이 순서대로 뜨나
▶ 재생 · 비교 토글AWQ 모델도 다운로드에 4~5GB, 최초 로딩에 몇 분이 걸립니다. cold start는 현실입니다.
- KubeRay는 Ray의 Head/Worker를 Kubernetes 파드로 관리하는 공식 Operator입니다.
- CRD는 넷 — RayCluster · RayJob · RayService · RayCronJob. 프로덕션 서빙은 RayService입니다.
- RayService는 무중단 in-place 업데이트와 헬스체크 기반 복구를 컨트롤러가 대신해 줍니다.
— Part 4. 클라우드 벤더로 짓기 —
12여섯 단계 — 택시부터 자가용 정비까지
한 줄 요약: 단계가 올라갈수록 자유도가 오르고, 정확히 같은 만큼 운영 부담도 오릅니다.
아닙니다. 원문이 스코프를 명확히 못 박습니다 — SageMaker 하우투가 아니라, 클라우드 벤더들이 서빙 옵션을 설계하는 근본 논리를 이해시키는 게 목적입니다.
이 논리를 알면 GCP Vertex AI든 Azure ML이든 같은 스펙트럼을 스스로 판단할 수 있게 됩니다.
- Bedrock
- 파운데이션 모델을 API로 제공하는 완전 관리형 서비스. 서버를 직접 운영하지 않습니다.
- DLC (Deep Learning Containers)
- 프레임워크별로 사전 빌드된 서빙 Docker 이미지. "bring-your-own-model"이지 "bring-your-own-code"는 아닙니다.
- LMI (Large Model Inference)
- DJL 기반의 대형 모델 추론 컨테이너. 내부 엔진으로 vLLM을 씁니다.
- Script mode
- 컨테이너는 벤더 것을 쓰되 entry-point 스크립트만 직접 작성하는 방식.
/invocations·/ping- SageMaker가 컨테이너에 요구하는 HTTP 계약. 포트 8080에서 POST/GET.
- ECR
- AWS의 컨테이너 이미지 저장소. 직접 만든 이미지를 여기 푸시합니다.
비유 먼저 — 자동차 여섯 단계
| 옵션 | 방식 | 비유 |
|---|---|---|
| 1 | Bedrock | 택시 — 목적지만 말합니다. 차량 관리는 신경 쓰지 않습니다. |
| 2 | SageMaker JumpStart | 렌터카 — 차종은 고를 수 있지만 준비된 목록 안에서. |
| 3 | Bring Your Own Model | 엔진은 내 것, 차량 플랫폼은 AWS — 제공되는 DLC 컨테이너를 씁니다. |
| 4 | Bring Your Own Code | 컨테이너는 AWS 것이지만 전처리·로딩·예측·후처리를 직접 씁니다. |
| 5 | Bring Your Own Serving Image | Docker 이미지까지 직접 — CUDA·Python·vLLM·라이브러리 전부 내 구성. |
| 6 | Build Your Own Infrastructure | 거의 모든 것을 직접 — EKS + GPU 노드 + vLLM/Triton/Ray Serve + Karpenter + Prometheus. |
1단계 Bedrock — 세 단계면 끝납니다
① 오레곤 리전(us-west-2)에서 Bedrock API 키 생성 → ② 모델 카탈로그에서 선택 → ③ 클라이언트 초기화 + 호출.
client = boto3.client(service_name="bedrock-runtime", region_name="us-west-2")
model_id = "amazon.nova-lite-v1:0"
messages = [{"role": "user", "content": [
{"text": "Hello! Can you tell me about Amazon Bedrock?"}]}]
response = client.converse(modelId=model_id, messages=messages)
과금은 서버 시간이 아니라 요청 수나 토큰 수 기준입니다. 프로비저닝할 서버도, 빌드할 컨테이너도 없습니다. 대신 못 하는 것도 셋입니다 — 인스턴스 타입 선택 불가, 컨테이너/코드 커스터마이징 불가, 새 모델을 Bedrock에 배포하는 것 자체가 불가능합니다.
2단계 JumpStart — 본질적 차이는 "어디서 도느냐"
| Bedrock | JumpStart | |
|---|---|---|
| 모델이 어디서 | AWS가 완전히 숨긴 인프라 | 여러분의 SageMaker 계정/인프라 |
| 과금 | 요청/토큰당 (서버 없음) | 시간당 인스턴스 요금 |
| 인프라 관리 | 거의 없음 | 인스턴스 타입(g5.48xlarge 등)을 직접 선택 |
"원클릭 배포"라고 불리지만, Bedrock과 달리 여러분 계정에 실제 엔드포인트가 뜹니다.
그래서 deploy()라는 단계가 새로 생깁니다.
3단계 DLC — 우리 3장 실습과 만나는 지점
model_id="meta-llama/Meta-Llama-3.1-8B-Instruct",
env={
"HF_TOKEN": "",
# set tensor parallel degree
"OPTION_TENSOR_PARALLEL_DEGREE": "4",
# specify serving loader as vLLM
"OPTION_SERVING_LOADER": "vllm",
# set max rolling batch size
"OPTION_MAX_ROLLING_BATCH_SIZE": "128",
}
)
predictor = model.deploy(instance_type="ml.g5.12xlarge", initial_instance_count=1, ...)
여기서 하나만 기억한다면 — 이 세 환경변수는 우리가 3장에서 손으로 만졌던 것과 같습니다.
OPTION_SERVING_LOADER=vllm은 vLLM 엔진, OPTION_MAX_ROLLING_BATCH_SIZE는 연속 배칭,
OPTION_TENSOR_PARALLEL_DEGREE는 텐서 병렬화입니다.
AWS 관리형 컨테이너 뒤에서 도는 엔진이 우리가 통합해본 바로 그 vLLM입니다.
· 프레임워크/드라이버/CUDA/Python/OS가 전부 이미지 태그에 고정됩니다.
· 모든 DLC는 포트 8080에서 /invocations(POST), /ping(GET)을 구현해야 합니다.
기본 타임아웃(예: 60초)도 적용됩니다. 사실상 SageMaker가 강제하는 미니 Public API 계약입니다.
· 입출력 포맷도 고정 — 커스텀 전처리/후처리가 필요하면 4단계로 가야 합니다.
4·5·6단계 — 무엇이 하나씩 내 책임이 되나
4단계(BYO Code): 컨테이너는 그대로 쓰되 model.py를 직접 씁니다 —
initialize()(모델 GPU 로드), run_inference()(배치 루프 + torch.no_grad()),
handle()(엔트리포인트). 3장의 ModelWorker와 구조가 거의 같습니다.
5단계(BYO Image): 컨테이너를 통째로 직접. 언어도 프레임워크도 자유입니다. SageMaker는 컨테이너를 "포트/경로 계약만 지키는 블랙박스"로만 취급합니다. 다만 분산 서빙(분산 KV 캐싱·프롬프트 캐싱·요청 라우팅)이 필요하면 이 옵션으로는 부족합니다.
6단계(BYO Infra): SageMaker의 엔드포인트 관리마저 벗어나 EKS 위에 직접 짓습니다. 4장 초반의 일곱 레이어 전체를 사실상 직접 소유하게 됩니다.
사용 편의성 기준 의사결정 트리
질문 다섯 개로 여섯 옵션이 갈립니다. 왼쪽으로 갈수록 편하고, 아래로 내려갈수록 통제권이 커집니다.
"LLM 출력만 필요한가?" → 예면 Bedrock에서 끝납니다.
아니면 "내 계정에 모델만 빨리 띄우면 되나?" → 예면 JumpStart.
이런 식으로 "AWS 기본 서빙 코드로 괜찮나", "서빙 코드만 고치면 되나", "인프라 전체를 관리해야 하나"까지 내려갑니다.
원문은 이 트리가 사용 편의성부터 시작하는 이유를 밝히면서도 곧바로 덧붙입니다 — "개발 속도는 여러 요인 중 하나일 뿐"이라고요.
스펙트럼을 밀어보기 — 누가 무엇을 책임지나
↔ 슬라이더6단계에서도 물리 GPU 하드웨어는 여전히 클라우드 것입니다. 그것만 파랗게 남습니다.
- 여섯 옵션은 완전 관리형 → 최대 커스터마이징 순으로 배열된 한 축입니다.
- 단계가 오를수록 AWS에 위임하던 레이어가 하나씩 내 책임으로 넘어옵니다.
- 목적은 SageMaker 하우투가 아니라 벤더들이 옵션을 설계하는 논리를 아는 것입니다.
13손익분기점 — 초당 3,255토큰
한 줄 요약: "언제 옮겨탈까"는 취향이 아니라 계산입니다.
토큰당 과금은 쓴 만큼 냅니다. 인스턴스 시간당 과금은 켜둔 만큼 냅니다. 이 둘은 어느 처리량에서 반드시 교차합니다.
이 장에서는 원문의 실제 사례(Qwen3 모델의 여정)를 따라 그 교차점을 직접 계산해보고, 계산이 왜 판단 방법론인지를 확인합니다.
- Break-even point (손익분기점)
- 두 가격 모델의 총비용이 같아지는 지점. 여기를 넘으면 선택이 뒤집힙니다.
- Pay-as-you-go
- 쓴 만큼 내는 과금. 유휴 시간에는 비용이 0이라는 게 최대 장점입니다.
- Utilization (가동률)
- 준비한 자원 중 실제로 쓰는 비율. 시간당 과금에서는 이게 곧 단가입니다.
- Decision tree (의사결정 트리)
- 질문을 순서대로 던져 선택지를 좁혀가는 구조. 원문 그림 4-8이 그것.
비유 먼저 — 택시 요금과 월 리스료
택시는 탄 만큼 냅니다. 하루 한 번 타면 압도적으로 쌉니다.
리스 차량은 세워둬도 월정액이 나갑니다. 그런데 하루 열 번 타면 택시비가 리스료를 넘습니다.
어느 쪽이 옳은지는 얼마나 타는가에만 달렸습니다. 취향이 아니라 산수입니다.
실제 사례 — Qwen3 모델의 여정
| 단계 | 옵션 | 과금 방식 |
|---|---|---|
| 초기(검증) | Option 1 (Bedrock) | 입력 100만 토큰당 $0.10 |
| 사용량 증가 | Option 2/3 (JumpStart/DLC, 자기 계정 호스팅) | 서버 인스턴스 시간당 $1.172 |
| 처리량↑·지연↓ 심화 | Option 4/5/6 | 커스터마이징 수준에 따라 |
작은 숫자로 한 번 — 교차점 계산
두 비용이 같아지는 처리량을 구합니다.
인스턴스 1시간 = $1.172
Bedrock 단가 = $0.10 / 1,000,000 토큰
$1.172 ÷ $0.10 × 1,000,000 = 11,720,000 토큰 / 시간
11,720,000 ÷ 3,600초 ≈ 3,255 토큰 / 초
즉 초당 약 3,255토큰 이상을 꾸준히 처리한다면, Bedrock보다 전용 인스턴스를 직접 호스팅하는 게 더 쌉니다. (출력 토큰 과금·인스턴스 유휴 시간 등은 단순화해서 제외한 근사치입니다.)
교차점을 직접 옮겨보기
↔ 슬라이더 3개Bedrock 선은 원점에서 시작합니다. 안 쓰면 0원입니다. 그게 초기 검증에 유리한 이유입니다.
많은 팀이 아이디어 검증은 가장 단순한 옵션(Bedrock)으로 시작하고,
프로젝트가 성숙하거나 요구사항이 커지면 더 커스터마이징된 옵션으로 옮겨갑니다.
의사결정 트리가 사용 편의성부터 시작하는 이유도 같습니다 — AWS 서비스를 많이 쓸수록 빨리 띄웁니다.
다만 원문은 곧바로 덧붙입니다: "개발 속도는 여러 요인 중 하나일 뿐, 운영/유지보수 비용도 똑같이 중요하다."
- 토큰당 과금과 시간당 과금은 반드시 어딘가에서 교차합니다. 그 지점이 판단 기준입니다.
- 원문 예시에서는 초당 약 3,255토큰이 그 지점이었습니다 ($1.172/h vs $0.10/1M).
- 가동률이 낮으면 교차점은 오른쪽으로 밀립니다 — 놀리는 GPU는 그대로 비용입니다.
— Part 5. 직접 지을까, 사서 쓸까 —
14Build or Buy — 스펙트럼 위 내 자리
한 줄 요약: 직접 짓지 않더라도 지을 줄 아는 것이 남습니다. 벤더의 노브를 해독할 수 있게 되니까요.
이 장이 그 질문에 답합니다. 원문 저자 둘은 10년 넘게 서빙 시스템을 만들고 유지해 왔고, 이것이 단순한 이분법이 아니라고 말합니다.
실제로 대부분의 팀은 중간에 있습니다 — 벤더 플랫폼을 쓰면서 커스텀 핸들러, 오토스케일링 신호, 라우팅 로직, 비용 제어를 얹습니다.
- SLO (Service Level Objective)
- 서비스가 지켜야 할 목표치. 여기서는 예측 지연시간과 처리량이 대표적입니다.
- Vendor lock-in
- 특정 벤더에 묶여 옮기기 어려워지는 상태. BYO를 택하는 흔한 이유 중 하나.
- Hybrid (하이브리드)
- 벤더 플랫폼 위에 표적 커스터마이징만 얹는 중간 지점. 실무의 기본값에 가깝습니다.
- 80/20 원칙
- 벤더 기본값을 80%는 그대로 쓰고, 깊은 통제가 필요한 20%만 정밀하게 교체하는 전략.
- Shared telemetry
- 어느 쪽에 있든 같은 형식으로 수집되는 관측 데이터. 자리를 옮겨도 비교가 가능해집니다.
비유 먼저 — 집을 안 지어도 도면은 읽을 줄 알아야
집을 직접 지을 일은 평생 없을지 모릅니다. 그래도 도면을 읽을 줄 알면 달라집니다.
분양 광고의 "특화 설계"가 실제로 뭔지 알아보고, 옵션 가격이 합당한지 따지고, 하자가 났을 때 어디를 봐야 하는지 압니다.
서빙도 같습니다. 원문의 표현으로는 — "기본 원칙부터 스택을 구축하는 방법을 잘 이해할수록, 관리형 옵션을 더 정확히 평가하고 숨겨진 트레이드오프를 발견할 수 있다."
직접 지을 줄 알면 생기는 네 가지
| 능력 | 이 스터디에서 실제로 있었던 일 |
|---|---|
| ① 벤더 기능 해독 | AWS LMI의 OPTION_SERVING_LOADER=vllm, OPTION_TENSOR_PARALLEL_DEGREE,
OPTION_MAX_ROLLING_BATCH_SIZE를 보고 "3장에서 만졌던 vLLM의 텐서 병렬화·연속 배칭"이라고 바로 알아봄.
@serve.multiplexed를 보고 "손으로 짠 ModelManager의 LRU"라고 즉시 대응시킴. |
| ② 트레이드오프 정량화 | Bedrock(토큰당)과 전용 인스턴스(시간당)의 손익분기점을 초당 3,255토큰으로 실제 계산. |
| ③ 커스터마이징 (80/20) | SageMaker DLC 실습이 정확히 이 패턴 — TorchServe 엔진·포트 8080 계약·기본 핸들러는 그대로 수용(80%)하고,
커스텀 모델(.mar) 주입과 config.properties 토큰 인증 딱 두 곳만 수술. |
| ④ 디버깅/트러블슈팅 | 벤더 솔루션은 블랙박스처럼 동작하고, 설정/내부 로직 문서가 부족하며, 로깅도 다 안 잡힙니다. 안쪽을 알아야 뚫고 들어갑니다. |
선택 전략 — 네 가지 규칙
원문 저자들이 고객·벤더와 논의할 때 실제로 쓰는 판단 기준입니다.
| 이럴 때 | 이렇게 |
|---|---|
| 벤더 유지 | SLO(예측 지연·처리량)가 충족되고, 비용이 허용 가능하며, 기능 속도와 일정이 가장 중요할 때 |
| 하이브리드 | 몇몇 모델 엔드포인트에만 특별한 배칭·라우팅·성능 튜닝·테넌트별 격리가 필요할 때 |
| BYO | 하드웨어·런타임·독립 네트워킹을 직접 제어해야 하거나, 서비스 비용이 높아 세밀한 튜닝이 필요할 때 |
| 다시 벤더로 | 볼륨이 낮은 속도로 안정되거나, 시스템 복잡성이 성과를 내지 못할 때 |
여기서 하나만 기억한다면 — 마지막 줄이 중요합니다. 되돌아가는 것도 전략입니다. 스펙트럼 위의 이동은 한 방향이 아닙니다.
내 상황을 넣으면 어디로 가나
↔ 토글 4개같은 팀도 분기마다 답이 달라집니다. 그래서 자리를 고정하지 말라는 것입니다.
원문의 결론입니다 — 안정적인 API와 공유 텔레메트리를 유지하고,
효과가 나는 부분을 커스터마이징하며,
제품·트래픽·제약이 변함에 따라 스펙트럼 상의 위치도 바뀔 것을 예상해야 합니다.
API가 안정적이어야 옮길 수 있고, 텔레메트리가 공유되어야 옮긴 결과를 비교할 수 있습니다.
- Build vs Buy는 이분법이 아니라 스펙트럼이고, 대부분의 팀은 중간에 있습니다.
- 직접 지을 줄 알면 벤더 기능 해독 · 트레이드오프 정량화 · 80/20 커스터마이징 · 디버깅이 가능해집니다.
- 판단은 SLO · 비용 · 특수 요구 · 복잡성 대비 성과 네 가지로 하고, 되돌아가는 것도 전략입니다.
— Part 6. 모든 선택을 판정하는 숫자 —
15지연시간 — E2E · TTFT · ITL
한 줄 요약: E2E = TTFT + ITL × (N − 1). 이 한 줄이 유스케이스별 우선순위를 전부 설명합니다.
나누지 않으면 무엇을 고쳐야 할지 모르기 때문입니다. "느리다"는 신고가 들어왔을 때 프리필이 느린 건지 디코드가 느린 건지 구분해야 손을 댈 수 있습니다.
이 장에서는 세 지표를 정의하고, 공식을 세우고, 유스케이스마다 어느 것이 최우선인지를 봅니다.
- E2E latency (종단 간 지연시간)
- 요청을 받은 시점부터 전체 응답 생성 완료까지. 필요하면 큐 대기·네트워크·라우팅·스케일링 오버헤드까지 포함합니다.
- TTFT (Time To First Token)
- 요청을 받은 시점부터 첫 토큰을 내놓기까지. 프리필 단계와 대응합니다. (CH2)
- ITL / TPOT
- 첫 토큰 이후 토큰 하나가 더 나오는 데 걸리는 시간. 디코드 단계와 대응하며 비교적 일정합니다.
- Prefill / Decode
- 입력 프롬프트 전체를 한 번에 처리하는 단계 / 토큰을 하나씩 순차 생성하는 단계.
- Autoregressive (자기회귀)
- 방금 만든 토큰을 다시 입력에 넣어 다음 토큰을 만드는 방식. 디코드가 순차인 이유.
비유 먼저 — 물이 나오기까지, 그리고 물이 나오는 속도
수도꼭지를 틉니다. 첫 물이 나올 때까지 잠깐 걸립니다 — 관에 물이 차야 하니까요. 이게 TTFT입니다.
물이 나오기 시작하면 일정한 속도로 흐릅니다. 이게 ITL입니다.
양동이를 다 채우는 데 걸린 전체 시간이 E2E입니다. 그래서 이렇게 됩니다.
# N = 총 생성 토큰 수. ITL이 일정하고 토크나이즈/디토크나이즈는 무시한 가정.
TTFT = 입력 전체 토큰화 + prefill + 첫 토큰 디코드 + 첫 토큰 디토크나이즈
≈ prefill + 첫 토큰 디코드
ITL = 새 토큰 하나 디코드 + 디토크나이즈 − 직전 디토크나이즈
≈ 새 토큰 하나 디코드
토크나이즈·디토크나이즈 시간이 무시되는 건 게으름이 아니라 실제로 모델 추론 대비 미미하기 때문입니다.
유스케이스별로 우선순위가 다릅니다
| 유스케이스 | 최우선 | 이유 |
|---|---|---|
| 에이전틱 워크플로우 | E2E | 다운스트림이 전체 출력에 의존합니다. generate_analysis는 generate_summary의
완성된 전체 출력이 있어야 시작합니다 — 첫 토큰만 빨리 나오는 건 의미가 없습니다. |
| 챗봇 (스트리밍) | TTFT | 사용자가 체감하는 응답성 그 자체. 첫 글자가 뜨는 순간 "반응했다"고 느낍니다. |
| 아주 긴 출력 | ITL | ITL이 높으면 "느릿느릿하다"고 체감됩니다. 첫 토큰이 빨라도 소용없습니다. |
여기서 하나만 기억한다면 — 1장의 Knowledge Agent가 정확히 첫 번째 케이스였습니다. 체이닝 구조라서 각 단계의 E2E가 다음 단계를 블로킹했습니다.
LLM 실행의 주요 지연시간 지표
세로가 시간입니다. 맨 왼쪽 긴 화살표가 End-to-end latency — 요청부터 마지막 토큰까지 전부입니다.
그 오른쪽 짧은 화살표가 Time to first token입니다. 여기에 무엇이 들어가는지 보세요 — Tokenization + Prefill + 첫 Decode + 첫 Detokenization. Prefill 상자가 유난히 큰 게 핵심입니다.
두 번째 토큰부터는 Decode + Detokenization만 반복되고, 그 간격이 Inter-token latency입니다. 계단처럼 오른쪽 아래로 내려가는 모양이 자기회귀 생성의 순차성을 그대로 보여줍니다.
세 지표를 직접 움직여보기
↔ 슬라이더 3개파란 조각이 프리필, 초록 띠가 디코드입니다. 어느 쪽이 더 넓은지가 곧 최적화 대상입니다.
원문이 미리 경고합니다 — 몇 가지 타협이 필요하기 때문에,
기법 선택은 유스케이스의 구체적 요구사항에 달렸습니다.
예를 들어 배치를 크게 묶으면 처리량과 ITL은 좋아지는데 TTFT는 나빠질 수 있습니다 — 묶이기를 기다려야 하니까요.
- E2E = TTFT + ITL × (N − 1). TTFT는 prefill, ITL은 decode에 대응합니다.
- 우선순위는 유스케이스가 정합니다 — 에이전트는 E2E, 챗봇은 TTFT, 긴 출력은 ITL.
- 셋을 동시에 개선하는 기법은 드뭅니다. 무엇을 포기할지 먼저 정해야 합니다.
16처리량 — 그리고 부풀릴 수 있다는 사실
한 줄 요약: TPS는 입력을 짧게 자르거나 배치를 잘 짜면 올라갑니다. 그래서 측정 조건을 물어야 합니다.
이 절이 은근히 경고하는 게 정확히 그것입니다. TPS는 인위적으로 부풀릴 수 있고, 부풀리는 방법이 둘 다 합법적인 최적화처럼 생겼습니다.
이 장에서는 두 처리량 지표의 한계를 보고, 마지막으로 성능 측정 모범 사례 아홉 가지로 이 장 전체를 닫습니다.
- RPS / RPM
- 초당/분당 요청 수. ML이 아닌 워크로드에서도 널리 쓰는 일반 지표.
- TPS (Tokens Per Second)
- 초당 생성되는 출력 토큰 수. 입력 토큰이나 입출력 합산은 포함하지 않습니다.
- Inflate (부풀리기)
- 측정 조건을 유리하게 만들어 지표를 실제보다 좋아 보이게 하는 것.
- Regression test (회귀 테스트)
- 새 변경이 기존 성능을 악화시키지 않았는지 확인하는 반복 테스트.
비유 먼저 — "시속 몇 km 나오나요"
같은 차라도 내리막에서 짐 없이 재면 숫자가 훨씬 잘 나옵니다. 거짓말은 아닙니다. 조건이 다를 뿐입니다.
그래서 연비를 비교할 때는 측정 조건을 표준화합니다. TPS에는 그런 표준이 사실상 없습니다. 그래서 조건을 물어야 합니다.
두 지표, 각각의 한계
| 지표 | 무엇을 재나 | 한계 |
|---|---|---|
| RPS / RPM | 일정 시간 동안 처리 가능한 요청 수 | 요청 패턴에 민감합니다. 긴 입력·긴 출력이 성능에 크게 영향을 주므로 입출력 길이와 동시 사용자 수에 좌우됩니다. → 트래픽 패턴이 다른 두 워크로드의 RPS 비교는 사실상 무의미합니다. |
| TPS | 초당 생성되는 출력 토큰 수 | 부풀릴 수 있습니다. 아래 두 가지 방법으로. |
① 입력을 짧게 자른다 — TTFT가 크게 줄고 요청당 작업 부하가 줄어듭니다. 그래서 TPS가 실제보다 높아 보입니다.
② 배치를 크게 또는 균일하게 짠다 — 배치 크기를 늘리거나 입출력 길이를 균일하게 만들면
GPU 활용도가 오르고 유휴 시간이 줄어 TPS가 개선됩니다. (이유는 6·7장에서)
둘 다 정당한 최적화처럼 생겼다는 게 함정입니다.
여기서 하나만 기억한다면 — TPS는 생성된 출력 토큰만 셉니다. 입력 토큰도, "입력+출력" 합산도 아닙니다. 이 구분을 놓치면 벤치마크를 잘못 읽습니다.
같은 시스템, 다른 숫자 — TPS를 부풀려보기
↔ 슬라이더 · 버튼TPS는 올라가는데 TTFT가 같이 나빠집니다. 하나만 자랑하면 나머지가 숨습니다.
성능 측정 모범 사례 아홉 가지
이 장에 나온 모든 지표를 실전에서 신뢰할 수 있게 쓰는 법으로 종합한 체크리스트입니다.
| # | 원칙 | 이 스터디에서의 대응 |
|---|---|---|
| 1 | 지연 vs 처리량 트레이드오프 파악 | 오프라인/배치는 처리량, 챗봇/실시간 에이전트는 지연이 핵심. → CH3의 비용 최적화 설계 vs 지연 최적화 설계 대비. |
| 2 | "충분히 좋은" 목표 설정 | 1초→0.5초가 체감상 의미 없다면 그 노력을 처리량/비용으로. → 손익분기점 3,255도 "여기까지는 안 옮겨도 된다"의 응용. |
| 3 | E2E를 TTFT/ITL로 분해 | 출력 길이가 제한된 시스템이면 TTFT가 ITL보다 중요할 수 있음. → 15장 공식을 실전에 적용. |
| 4 | 실제 트래픽 패턴 시뮬레이션 | 긴 프롬프트+짧은 답변과 그 반대는 완전히 다르게 동작. 트래픽은 절대 균일하지 않음(일/계절 변동, 버스트 → 큐잉 지연·실패). |
| 5 | 실험의 일관성 — 한 번에 한 노브 | 동시에 여러 개 바꾸면 효과를 분리할 수 없음. → 테스트 버그를 딱 한 줄만 고쳐 "11/11 통과"라는 인과를 확인. |
| 6 | 하드웨어 활용률 모니터링 | 병목이 모델 때문인지 하드웨어 한계인지 구분. → CH3에서 "모델은 0.24GiB인데
gpu_memory_utilization=0.9 기본값 때문에 14.7GB 점유"를 발견한 것이 이 사례. |
| 7 | 지표를 인위적으로 부풀리지 말 것 | 바로 위에서 다룬 TPS 조작 가능성의 재확인. |
| 8 | 프로덕션에서 지속 모니터링 | 배포 후에도 사용자 행동 변화로 인한 트래픽 급증/실패까지 계속 관찰. |
| 9 | 테스트 스위트 주기적 재실행 | 회귀 방지 · 스케일링 테스트(피크 시뮬레이션) · A/B 테스트(프로덕션 배포 전 데이터 기반 비교). |
- RPS/RPM은 요청 패턴에 민감해서, 트래픽 패턴이 다르면 비교 자체가 무의미합니다.
- TPS는 출력 토큰만 세고, 입력 단축·배치 최적화로 부풀릴 수 있습니다.
- 그래서 어느 쪽도 단독으로는 완전하지 않습니다. 조건을 밝히고 여러 지표를 함께 봐야 합니다.
마치며
이 장은 2·3장 위에 한 층을 더 올린 장이었습니다. 모델을 어떻게 실행하는가에서, 프로덕션에서 무엇이 더 필요한가로요.
- 에이전틱 서빙 — Knowledge Agent(OpenAI 모델 + Planner + Actions)로 서빙이 자율성을 어떻게 뒷받침하는지 보고, RAG vs CAG의 트레이드오프를 비교했습니다.
- 엔터프라이즈 아키텍처 — 독립적으로 진화 가능한 일곱 층과, 그것을 실제로 구현하는 Kubernetes + Ray Serve + vLLM 스택을 봤습니다.
- 빌드 vs 클라우드 — 커스터마이징이 커지는 여섯 옵션과 의사결정 트리. 핵심은 이분법이 아니라 스펙트럼이고, 대부분의 팀은 중간에서 균형점을 계속 옮깁니다.
- 성능 지표 표준화 — 지연시간과 처리량, 그리고 공정한 벤치마킹·현실적 트래픽 시뮬레이션· 하드웨어 활용률 추적·회귀/스케일링 테스트라는 모범 사례.
'AI > [LLMSO] Hands-On LLM 서빙과 최적화 스터디' 카테고리의 다른 글
| [2주차-LLMSO]CH3. Model Serving System Design A Deep Dive (2026.08.09) (0) | 2026.08.15 |
|---|---|
| [1주차-LLMSO]LLM 기초 동작 원리 (2026.08.02) (0) | 2026.08.08 |
| [1주차-LLMSO]CH2. Large Language Model Serving (2026.08.02) (0) | 2026.08.08 |
| [1주차-LLMSO]CH1. Introduction to Model Serving and Optimization (2026.08.02) (0) | 2026.08.08 |
| [1주차-LLMSO]트랜스포머 모델을 이해하기 위한 기초 수학 정리 (0) | 2026.08.07 |