devlos
Devlos Archive
devlos
전체 방문자
오늘
어제
08-15 12:26

최근 글

  • 분류 전체보기 (122) N
    • 프로젝트 (1)
    • MSA 설계 & 도메인주도 설계 (9)
    • AI (6) N
      • [LLMSO] Hands-On LLM 서빙과 최적.. (6) N
    • 클라우드 컴퓨팅 & NoSQL (96)
      • [K8S Deploy] K8S 디플로이 스터디 (8)
      • [Cilium Study] 실리움 스터디 (8)
      • [KANS] 쿠버네티스 네트워크 심화 스터디 (12)
      • [T101] 테라폼 4기 스터디 (8)
      • [CICD] CICD 맛보기 스터디 (3)
      • [T101] 테라폼 기초 입문 스터디 (6)
      • [AEWS] Amazon EKS 워크숍 스터디 (7)
      • [PKOS] 쿠버네티스 실무 실습 스터디 (7)
      • Kubernetes (13)
      • Docker (7)
      • Redis (1)
      • Jenkins (3)
      • Terraform (1)
      • Ansible (4)
      • Kafka (1)
    • 프로그래밍 (7)
      • Spring Boot (5)
      • Broker (1)
    • 성능과 튜닝 (1)
    • ALM (0)
    • 기타 (2)

인기 글

태그

  • CloudNet@
  • 쿠버네티스
  • docker
  • kOps
  • Kubernetes
  • t101 4기
  • 데브옵스
  • PKOS
  • DevOps
  • 테라폼
  • MSA
  • cilium
  • ansible
  • 쿠버네티스 스터디
  • 도커

티스토리

최근 댓글

hELLO · Designed By 정상우.
devlos

Devlos Archive

[2주차-LLMSO]CH4. Model Serving Best Practices (2026.08.09)
AI/[LLMSO] Hands-On LLM 서빙과 최적화 스터디

[2주차-LLMSO]CH4. Model Serving Best Practices (2026.08.09)

2026. 8. 15. 00:26
반응형
CH4 모델 서빙 베스트 프랙티스 — 인터랙티브 학습자료
CH4 · Serving Best Practices 들어가며 1 왕복 네 번 2 에이전트 부품 3 9단계 워크플로우 4 RAG 5 CAG 6 일곱 개의 층 7 Public API 8 모델 선택 9 Ray Serve 10 멀티플렉싱 11 KubeRay 12 여섯 단계 13 손익분기점 14 Build or Buy 15 지연시간 16 처리량 마치며
LLM 서빙 스터디 2주차 · CH4 학습자료

질문은 하나였는데
왕복은 네 번이었습니다.

Model Serving Best Practices — 2·3장이 "모델을 어떻게 실행하는가"였다면, 이 장은 "프로덕션에서 무엇이 더 필요한가"입니다.

에이전트가 등장하면서 요청 한 건의 의미가 달라졌고, 그래서 서빙은 순수 추론 문제가 아니라 시스템 아키텍처 문제가 됩니다. 계층 구조, 오픈소스 스택, 클라우드 벤더, 그리고 그 모든 선택을 판정하는 지표까지 봅니다.

이 자료가 지키는 것

① 실제 로그와 숫자로 확인합니다. "왕복 4회"도, "초당 3,255토큰"도 원문에 있는 실측·계산값입니다.
② 전문 용어는 영어 그대로. speculative decoding을 억지로 번역하지 않고 뜻만 한국어로 풀어둡니다.
③ 그림 16개 전부 조작 가능. 노브를 밀어봐야 트레이드오프가 몸에 남습니다.
④ 장마다 세 줄 요약. 거기만 읽어도 넘어갈 수 있게.

들어가며

읽는 시간 약 55분 대상 서빙을 실제로 운영해야 하는 사람 사전 지식 CH2·CH3 정도면 충분 그림 16개 · 전부 조작 가능

3장까지 우리가 만든 것은 "요청 하나에 추론 한 번, 응답 반환"이라는 전제 위에 서 있었습니다. 이 장은 그 전제가 깨지는 지점에서 시작합니다.

모델이 에이전트 안에 들어가면 제어 루프(control loop) 안에서 반복 호출됩니다. 검색하고, 중간 결과를 추론하고, 도구를 실행하고, 출력을 다듬어야 최종 답이 나옵니다. 사용자는 질문을 한 번 던졌는데 서빙 백엔드는 네 번 왕복합니다.

그러면 무엇이 달라지나요. 토큰 사용량이 늘고, 체인으로 이어진 호출마다 tail latency가 누적되고, 트래픽 패턴이 동적으로 변합니다. 서빙은 이제 "모델을 효율적으로 돌리는 일"을 넘어 오케스트레이션·메모리 관리·시스템 레벨 조정까지 감당해야 합니다.

결론부터 세 줄
  1. 에이전트 워크로드는 요청 한 건의 비용을 몇 배로 부풀립니다. 이 장의 나머지 전부가 그 부담을 감당하기 위한 장치입니다.
  2. 엔터프라이즈 서빙은 일곱 개의 층으로 나뉩니다. 층을 나누는 이유는 기술보다 조직에 가깝습니다 — 여러 팀이 서로 발목 잡지 않고 일하기 위해서.
  3. "직접 지을까 사서 쓸까"는 이분법이 아니라 스펙트럼입니다. 그 위 어디에 설지는 측정된 숫자가 정합니다.

읽는 방법

· 굵은 글씨만 따라가도 흐름이 이어집니다.

· 코드 상자에는 파일 경로나 출처가 주석으로 붙어 있습니다.

· 그림은 전부 슬라이더를 밀고 버튼을 눌러보는 용도입니다. 특히 극단으로 밀어봤을 때 무슨 일이 나는지가 핵심입니다.

· 4장은 실습이 여럿입니다. 실습을 건너뛰어도 본문은 완결되게 썼습니다.

이 장이 답하는 세 가지 질문

원문 도입부가 스스로에게 던지는 질문입니다. 다 읽고 나서 여기로 돌아와 보시면 좋습니다.

챕터를 여는 질문 세 개

① 에이전트 워크플로우가 등장하면서 모델 서빙 요구사항이 근본적으로 어떻게 달라졌는가? 단일 요청-응답과 달리, 제어 루프 안에서 반복 호출되는 것이 서빙 시스템에 어떤 부담을 주는가? (1~5장)
② 엔터프라이즈 LLM 서빙 플랫폼의 계층형 아키텍처는 각 레이어가 어떤 역할과 트레이드오프를 가지며, 이를 오픈소스 스택(Kubernetes / Ray Serve)으로는 어떻게 구현하는가? (6~11장)
③ "직접 구축 vs 클라우드 벤더"를 스펙트럼으로 볼 때, 어떤 기준으로 위치를 정하고 그 타당성을 판단하려면 어떤 지표를 측정해야 하는가? (12~16장)

이 장에 나오는 이름들 — 지금 외울 필요는 없습니다

10개만 먼저 훑어두면 뒤가 훨씬 편합니다. "아 그거" 정도면 충분합니다.

Knowledge
Agent
샘플 에이전트
PDF를 질의·분석하는 데모. 플래닝 → 검색 → 생성이라는 에이전트의 기본 패턴을 그대로 보여줍니다.
Planner
계획 수립
LLM에게 "어떤 액션을 어떤 순서로"를 JSON으로 짜게 시키는 부품. 규칙 기반 폴백도 갖고 있습니다.
RAG
오픈북 시험
질의 시점에 외부 문서를 검색해 프롬프트에 붙입니다. LLM 입장에선 그냥 입력이 길어진 것.
CAG
미리 펴놓기
지식을 KV 캐시에 프리로드해두고 재사용. 검색 지연이 사라지는 대신 메모리를 씁니다.
계층형
아키텍처
그림 4-6
Public API부터 Model까지 일곱 개 층. 각 층의 난제가 다르고, 소유하는 팀도 다릅니다.
Kubernetes
바닥
오픈소스 스택의 근간. 레이어 1의 라우팅과 레이어 2 전체를 사실상 이 한 층이 구현합니다.
Ray Serve
모델 호스팅
데코레이터 한 줄로 레플리카 수와 GPU 배정이 끝납니다. 3장에서 손으로 짠 것의 프레임워크판.
KubeRay ·
RayService
운영
Ray 클러스터를 Kubernetes 파드로 관리하는 Operator. 무중단 업데이트와 헬스체크를 대신해 줍니다.
SageMaker
6 옵션
스펙트럼
Bedrock부터 직접 인프라 구축까지. 옵션이 올라갈수록 자유도와 운영 부담이 같이 올라갑니다.
E2E ·
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?" 딱 한 질문에 실제로 찍힌 로그입니다.

# python agent.py — 질문 한 건에 실제로 발생한 API 호출
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/completions1어떤 액션을 어떤 순서로 실행할지 LLM이 결정
쿼리 검색embeddings1질문을 벡터로 바꿔 관련 문서 5개 검색
요약chat/completions1검색된 문서를 컨텍스트로 요약 생성
분석chat/completions1앞 요약을 컨텍스트로 심층 분석 생성
합계—4회사용자가 던진 질문은 단 하나

여기서 하나만 기억한다면 — 이 네 번은 병렬이 아니라 직렬입니다. 플래닝이 끝나야 검색을 시작하고, 검색이 끝나야 요약을 시작합니다. 그래서 각 단계의 지연시간이 더해집니다. 평균이 아니라 합입니다.

그림 1

요청 한 건이 만드는 왕복 — 직접 세어보기

↔ 슬라이더 · 버튼
해볼 것 — ① 액션 수를 1→3으로 밀어보세요. 호출 횟수와 총 지연시간이 어떻게 늘어나는지 보세요. ② 플래닝 끄기를 눌러보세요. 호출이 한 번 줄고 시작이 빨라집니다 — 대신 다단계 추론이 사라집니다. ③ 컨텍스트 체이닝을 꺼보세요. 액션마다 검색을 다시 하느라 임베딩 호출이 액션 수만큼 늘어납니다.
관찰 포인트

막대가 겹치지 않고 이어집니다. 직렬이라는 뜻이고, 그래서 지연이 더해집니다.

플래너가 규칙보다 똑똑했던 순간

이 질문("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단계 추론이 일어난 것입니다.

1장 세 줄 요약
  1. 에이전트에서는 사용자 상호작용 한 번이 여러 번의 LLM 호출로 펼쳐집니다. 실측 4회.
  2. 이 호출들은 직렬이라 지연시간이 더해집니다. 그래서 tail latency가 체인마다 누적됩니다.
  3. 그 결과 서빙은 "모델 실행"을 넘어 오케스트레이션·메모리 관리·시스템 조정까지 감당해야 합니다.

02에이전트를 뜯어보면 — 다섯 부품, 두 모델

한 줄 요약: LLM이 혼자 다 하는 게 아닙니다. LLM은 생각하고, RAG가 찾고, Planner가 순서를 정합니다.

왜 이걸 배우나
"에이전트라는 게 결국 LLM에 프롬프트 잘 넣는 거 아닌가?"

맞는 부분도 있습니다. 이 샘플 에이전트의 액션은 실제로 "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-smallEmbedding 텍스트를 의미 벡터로. POST /v1/embeddings — PDF 42청크 벡터화, 사용자 쿼리 벡터화
gpt-4.1-nanoLLM 추론·계획·생성. 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) → 각 액션별 응답 생성

액션은 네 개뿐입니다

플래너가 고를 수 있는 선택지는 딱 네 개입니다. 이게 "이산적 액션 집합"의 실체입니다.

# planner.py — available_actions
query_rag_with_context     // RAG 검색 + 답변
generate_profile_based_response  // 사용자 프로필 반영 답변
generate_summary          // 요약
generate_analysis         // 심층 분석

액션 하나는 결국 프롬프트 템플릿 하나입니다. 분석 액션의 프롬프트를 보면 명확합니다.

# actions.py — create_analysis_prompt()
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 호출만 쓰는 것뿐입니다.

원문 그림 4-1

Knowledge Agent 시스템 개요

이 그림이 말하는 것

위아래 두 덩어리로 갈립니다. 위쪽이 에이전트(Planner · Actions · RAG system · Knowledge docs), 아래쪽이 모델 서빙(LLM과 Embedding 두 모델)입니다.
핵심은 위쪽 네 부품이 전부 아래 두 모델을 부르려고 존재한다는 것입니다. Planner도 LLM을 부르고, RAG도 Embedding을 부르고, Actions도 LLM을 부릅니다. 그래서 사용자 요청 한 건이 서빙 호출 여러 건으로 펼쳐집니다 — 1장에서 로그로 확인한 그 4회입니다.

그림 2

다섯 부품 — 하나씩 꺼보기

↔ 부품 클릭
해볼 것 — ① 부품을 클릭해서 꺼보세요. 그 부품이 빠지면 무엇이 죽는지 오른쪽에 뜹니다. ② Planner만 꺼보세요. 답은 여전히 나오지만 다단계 추론이 통째로 사라집니다. ③ RAG를 꺼보세요. LLM이 자기 파라미터 기억만으로 답합니다 — 환각 위험이 여기서 생깁니다.
관찰 포인트

보라색 부품이 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로 변환됩니다 — 에이전트 프로세스 자체는 죽지 않습니다.

2장 세 줄 요약
  1. 에이전트는 LLM 하나가 아니라 다섯 부품입니다 — Agent · Planner · RAG · Actions · LLM Manager.
  2. 모델도 두 개가 역할을 나눕니다 — 임베딩 모델은 찾고, LLM은 계획하고 생성합니다.
  3. 액션은 이산적 집합이고, 여기서는 프롬프트 템플릿 네 개입니다. 도구 호출로 확장하는 게 다음 단계입니다.

039단계 워크플로우 — 그리고 컨텍스트 체이닝

한 줄 요약: 두 번째 액션은 원본 문서를 다시 찾지 않습니다. 첫 번째 액션의 결과를 재료로 씁니다.

왜 이걸 배우나
"액션을 순서대로 실행한다는데, 그 사이에 데이터는 어떻게 흘러가지?"

이게 그냥 구현 디테일 같지만, 서빙 비용과 지연시간을 직접 결정합니다. 앞 결과를 넘기면 검색 호출이 한 번으로 끝나고, 안 넘기면 액션 수만큼 늘어납니다.

이 장에서는 원문의 9단계 워크플로우를 따라가며, 로그에서 임베딩 호출이 한 번뿐이었던 이유를 확인합니다.

이 장에 나오는 낯선 말
Context chaining (컨텍스트 체이닝)
앞 액션의 결과를 다음 액션의 입력 컨텍스트로 넘기는 것. 여기서는 결과가 50자를 넘을 때만 넘깁니다.
Workflow executor
계획에 따라 액션을 순서대로 실행하는 부품. 실패한 스텝은 에러 문자열만 남기고 다음으로 넘어갑니다.
Sequential plan
액션들이 줄 세워진 계획. 병렬이 아니라서 지연시간이 합산됩니다.
Grounding (그라운딩)
모델의 답을 실제 문서에 근거하게 만드는 것. RAG가 하려는 일이 이것입니다.

비유 먼저 — 릴레이인가, 각자 도서관 가기인가

세 사람이 순서대로 보고서를 다듬습니다. 두 가지 방식이 있습니다.

릴레이 — 첫 사람이 자료를 찾아 초안을 쓰고, 그 초안을 다음 사람에게 넘깁니다. 다음 사람은 도서관에 갈 필요가 없습니다. 빠르고 쌉니다. 대신 첫 사람이 놓친 자료는 영영 못 봅니다.

각자 도서관 — 세 사람이 각각 서고에 갑니다. 놓치는 게 적습니다. 대신 도서관 왕복이 세 번입니다.

Knowledge Agent는 릴레이 방식입니다. 그래서 로그에 임베딩 호출이 한 번뿐이었습니다.

9단계 — 원문 그림 4-2를 따라

질문: "데이터베이스 쿼리 최적화와 자료구조 최적화를 상세 비교해 주세요."

단계무슨 일이주체
1사용자 질문 입력User
2Agent가 Planner를 호출해 실행 계획 설계 요청Agent
3Planner가 LLM을 호출해 3단계 계획 생성Planner → LLM
4ActionExecutor가 순서대로 실행 시작ActionExecutor
5query_rag_with_context — 시맨틱 검색 후 그 문서로 답변 생성RAG → LLM
6generate_analysis — 질문 + 5단계 결과를 컨텍스트로 전달ActionExecutor
7LLM이 심층 비교 분석 생성LLM
8generate_summary — 직전 분석 결과를 요약LLM
9Agent가 액션 출력을 종합해 최종 결과 반환Agent
# 3단계에서 LLM이 실제로 뱉은 계획 (JSON)
{
  "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는 원본 문서를 다시 검색하지 않고 방금 만든 요약문을 입력 삼아 분석을 썼습니다.

원문 그림 4-2

에이전트가 사용자 질문에 응답하는 내부 워크플로우

이 그림이 말하는 것

(1)~(8) 번호를 따라가면 LLM serving 상자가 세 번 등장합니다. 계획할 때 한 번, 분석할 때 한 번, 최종 응답에 한 번.
왼쪽 아래 Embedding model → RAG system이 (5) 검색을 담당하고, 그 아래 세 상자(PDF · 인덱스 · 벡터 DB)가 RAG가 딛고 선 자료입니다.
오른쪽 세로줄이 액션 체인입니다 — Action 1의 결과가 Action 2로, Action 2의 결과가 Action 3으로 흘러갑니다. 이게 컨텍스트 체이닝의 그림 버전입니다.

그림 3

9단계 워크플로우 — 재생해보기

▶ 재생 · 체이닝 토글
해볼 것 — ① 재생을 눌러 9단계가 흘러가는 걸 보세요. 어느 단계에서 외부 호출이 나가는지 표시됩니다. ② 체이닝 끄기를 누르고 다시 재생해보세요. 검색 화살표가 액션마다 생깁니다. ③ 오른쪽 호출 집계가 어떻게 달라지는지 비교해보세요.
관찰 포인트

주황 화살표가 외부 API 왕복입니다. 이게 늘어나면 지연도 비용도 같이 늡니다.

체이닝의 대가

릴레이는 싸지만 정보 손실이 있습니다. generate_analysis가 본 것은 원본 문서가 아니라 요약본입니다. 요약 과정에서 빠진 디테일은 분석에도 없습니다.
"무엇을 다시 검색하고 무엇을 넘겨받을지"는 정확도와 비용 사이의 설계 선택입니다. 정답이 하나 있는 문제가 아닙니다.

3장 세 줄 요약
  1. 계획은 LLM이 JSON으로 짜고, ActionExecutor가 순서대로 실행합니다.
  2. 앞 액션의 결과는 다음 액션의 컨텍스트로 체이닝됩니다 — 그래서 임베딩 호출이 한 번으로 끝났습니다.
  3. 체이닝은 비용을 줄이는 대신 정보를 잃습니다. 어느 쪽을 택할지가 설계입니다.

04RAG — 오픈북 시험

한 줄 요약: LLM 입장에서 RAG는 프롬프트가 길어진 것뿐입니다. 행렬에 뭘 주입하는 게 아닙니다.

왜 이걸 배우나
"검색된 문서가 LLM 내부의 Q 행렬에 들어가는 건가? RAG의 query랑 attention의 Q는 같은 건가?"

아닙니다. 그런데 이 오해는 처음 배울 때 아주 흔합니다. 그리고 이 오해를 갖고 있으면 왜 컨텍스트 길이가 비용을 결정하는지를 영영 이해할 수 없습니다.

이 장에서는 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 입장에서 실제로 무슨 일이 일어나나

이 흐름을 한 번 보면 오해가 풀립니다. 중간에 "텍스트 합치기"라고 적힌 구간이 핵심입니다.

사용자 query: "회색 정장에 어울리는 셔츠 추천해줘"
                  ↓
   [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 컨텍스트 윈도우에 따라 달라집니다 — 정답이 하나 있는 문제가 아닙니다
원문 그림 4-3

RAG의 두 워크플로우 — 오프라인과 온라인

이 그림이 말하는 것

점선 위가 (A) 인덱스 빌딩, 아래가 (B) 질의/검색입니다.
위쪽은 문서를 쪼개고(split) → 임베딩하고(embedded) → 벡터 DB에 넣습니다(index). 주기적으로 한 번 도는 배치 작업입니다.
아래쪽은 질문을 임베딩하고(embedded) → 검색하고(search) → 관련 조각을 받아(context) → LLM에 넣습니다. 질문할 때마다 매번 돕니다.
두 워크플로우가 Embedding model 상자를 공유한다는 점도 보세요 — 같은 모델로 재야 같은 공간에서 비교가 됩니다.

그림 4

청킹과 top-k — 무엇을 맞바꾸나

↔ 슬라이더 3개
해볼 것 — ① 청크 크기를 왼쪽 끝까지 미세요. 정밀도는 올라가는데 문맥 보존이 떨어집니다. ② 오른쪽 끝까지 미세요. 이번엔 희석이 올라갑니다. ③ top-k를 늘려보세요. 답에 필요한 조각을 담을 확률은 오르지만 프롬프트 토큰이 같이 폭증합니다.
관찰 포인트

막대가 빨간 선을 넘으면 컨텍스트 윈도우 초과입니다. 청크를 나누는 이유가 이것입니다.

현실의 RAG는 이보다 복잡합니다

원문도 못 박습니다 — 그림 4-3은 "기본" RAG 파이프라인입니다. 실제 운영 RAG에는 재랭킹(ranking), 중복 제거(deduplication), 다중 소스 검색 같은 복잡성이 더 붙습니다.

4장 세 줄 요약
  1. LLM 입장에서 RAG는 프롬프트가 길어진 것입니다. 행렬 주입이 아니라 텍스트 합치기입니다.
  2. RAG는 오프라인 인덱스 빌딩과 온라인 질의/검색 두 워크플로우로 나뉩니다.
  3. 청킹 크기는 정밀도 ↔ 문맥의 노브이고, 최적값은 도메인과 컨텍스트 윈도우가 정합니다.

05CAG — 검색 대신 미리 펴놓기

한 줄 요약: 컨텍스트 윈도우가 100만 토큰이 되면 "몇 개만 골라 넣는다"는 전제 자체가 흔들립니다.

왜 이걸 배우나
"RAG가 정답이라면서 왜 또 다른 게 나오지?"

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의 고질적 한계(정적 지식·환각·도메인 갭)를 다룰 수 있다는 것입니다.

RAGCAG
언제 지식을 넣나쿼리마다 동적으로 검색한 번 캐시에 프리로드, 이후 재사용
강한 곳동적 검색 · 최신성반복 쿼리 · 고정 지식셋에서 지연시간
비용매 쿼리마다 검색 지연 + 인덱스 유지큰 컨텍스트 윈도우 → 메모리·연산 요구 ↑
개선하는 것답변 품질 (지식 접지·최신성)서빙 효율 (지연·처리량·비용)
원문 그림 4-4

RAG와 CAG의 구조 비교

이 그림이 말하는 것

위쪽 RAG에는 상자가 많습니다 — RAG system 아래에 지식 소스 · 임베딩 모델 · 인덱스 · 벡터 DB 네 개가 매달려 있습니다. 질문이 올 때마다 이 경로를 한 번씩 돕니다.
아래쪽 CAG에는 검색 상자가 하나도 없습니다. 사용자 질문이 곧바로 LLM으로 갑니다. 지식은 이미 Knowledge cache(=KV 캐시) 안에 들어와 있기 때문입니다.
점선 화살표 preload가 그 차이의 전부입니다 — 언제 지식을 넣느냐만 다릅니다.

원문 그림 4-5

CAG를 쓰는 에이전트 워크플로우

이 그림이 말하는 것

그림 4-2와 나란히 놓고 보시면 차이가 한눈에 들어옵니다. 단계가 8개에서 3개로 줄었습니다. Embedding model도, Vector DB도, 검색 화살표도 없습니다.
대신 오른쪽 위에 Knowledge and context cache가 붙었습니다. 설계가 단순해진 만큼 캐시를 채워두는 비용이 앞에 한 번 붙고, 큰 컨텍스트를 유지할 메모리가 필요해집니다.

그림 5

RAG vs CAG — 몇 번 물으면 뒤집히나

↔ 슬라이더 · 버튼
해볼 것 — ① 질의 횟수를 1에서 천천히 올려보세요. 두 막대가 교차하는 지점이 손익분기점입니다. ② 지식 크기를 키워보세요. CAG의 프리로드 비용이 커지면서 교차점이 오른쪽으로 밀립니다. ③ 지식이 바뀜을 눌러보세요. CAG는 캐시를 다시 지어야 합니다 — 최신성이 왜 RAG의 강점인지 보입니다.
관찰 포인트

CAG의 첫 막대만 유난히 큽니다. 프리로드는 한 번이고, 그 뒤로는 거의 공짜입니다.

양자택일이 아닙니다

원문의 결론은 명확합니다 — RAG로 입력을 보강하고, CAG로 실행을 최적화하는 조합.
RAG는 답변 품질을 올리고, CAG는 서빙 효율을 올립니다. 개선하는 축이 아예 다릅니다. 큰 컨텍스트 + 캐시 관리의 메모리·연산 비용을 다루는 기법은 7장에서 이어집니다.

5장 세 줄 요약
  1. RAG의 대가는 검색 지연 · 선택 오류 · 시스템 복잡도 세 가지입니다.
  2. CAG는 지식을 KV 캐시에 프리로드해 그 셋을 줄입니다. 대신 메모리·연산을 지불합니다.
  3. 둘은 경쟁이 아니라 조합입니다 — RAG는 품질, CAG는 효율을 개선합니다.

— Part 2. 엔터프라이즈 서빙의 계층 구조 —

06일곱 개의 층 — 그리고 그것이 조직도인 이유

한 줄 요약: "모델 실행"과 "엔터프라이즈 서빙"은 다른 문제입니다. 층을 나누는 진짜 이유는 조직에 있습니다.

왜 이걸 배우나
"3장까지 만든 걸로 서빙은 끝난 거 아닌가?"

지금까지 만든 것 — 단일 모델 서빙, 멀티모델 서빙(Triton 포함), 샘플 RAG 에이전트 — 은 전부 "모델을 호스팅하고 실행한다"는 좁은 의미의 서빙이었습니다.

실제 대규모 프로바이더는 그 위에 인증 · 과금 정책 · 리소스 관리 · 네트워킹 · 최적화 · 실험(A/B) · 관측성 · 온콜 지원까지 감당합니다. 이 장은 그것들이 어디에 얹히는지를 봅니다.

이 장에 나오는 낯선 말
Tenant (테넌트)
플랫폼을 쓰는 고객 단위. 쿼터·과금·라우팅이 전부 이 값 하나에 매달립니다.
Rate limiting
단위 시간당 요청 수를 제한하는 것. 어뷰징 방어와 공정 사용을 위해.
Capacity planning (용량 계획)
수요를 예측해 필요한 만큼만 준비하는 일. 과다 프로비저닝은 그대로 비용입니다.
Preemption (선점)
저우선순위 작업을 중단시키고 자원을 회수하는 것. 중요 고객을 위해.
Speculative decoding
빠른 draft 모델이 여러 토큰을 미리 만들고, 정확한 target 모델이 한꺼번에 검증하는 기법. (7장에서 상세히)
Observability (관측성)
시스템 내부 상태를 밖에서 알 수 있게 만드는 것 — 메트릭·로그·트레이스.

비유 먼저 — 층이 나뉘는 건 배관 때문이 아닙니다

큰 건물이 층으로 나뉘는 이유는 구조공학 때문만이 아닙니다. 층마다 다른 조직이 들어가고, 층 경계가 곧 책임 경계이기 때문입니다.

원문은 이 어려움이 기술적인 것보다 조직적인 것에 가깝다고 말합니다. 서로 다른 책임을 가진 여러 팀이 병목이나 과도한 상호 의존 없이 하나의 진화하는 시스템에 동시에 기여할 수 있어야 하니까요.

그래서 좋은 서빙 아키텍처를 만든다는 건 "레이턴시를 줄이는 기술" 문제가 아니라 "어떤 레이어를 누가 소유하고, 경계를 어떻게 그어야 팀들이 서로 발목 잡지 않는가"라는 설계 문제이기도 합니다.

일곱 층, 각각의 난제

층이름핵심 난제
1Public API 고동시성(수백만 연결) · 공정 사용과 수익화 · 저지연 글로벌 접근 · 보안
2Resource Management 용량 계획 · 이기종 GPU 풀의 고가동률 · 고객 우선순위(쿼터·예약·선점)
3Model Selection & Orchestration 비용-품질 트레이드오프 · 로드 밸런싱 · 지연 민감 케이스 대응
4Distributed Serving 단일 GPU 메모리 초과 · 멀티GPU/멀티노드 조율 · 캐시 인식 라우팅
5Core Inference 모델별 의존성 차이 · load/unload가 만드는 지연 스파이크 · cold start · hot model 스케일링
6Model Optimization 재학습 없이 성능·효율 개선 (양자화 6장 · speculative decoding·LMCache 7장 · 종단간 9장)
7Model 학습 파이프라인/외부 소스에서 운영으로 이동 · 기능·목적별 분류 · 추적과 버전 관리

작은 숫자로 한 번 — 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 하나에 안 들어갈 때

# 모델이 커지면 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는 노드 사이에서 자릅니다.

원문 그림 4-6

엔터프라이즈 모델 서빙 계층 아키텍처

이 그림이 말하는 것

원문이 이 절에서 제시하는 레퍼런스 아키텍처입니다. 위로 갈수록 사용자에 가깝고 아래로 갈수록 하드웨어에 가깝습니다.
층마다 안고 있는 것이 다릅니다 — 1층은 인증·과금·라우팅, 2층은 하드웨어 프로비전, 3층은 어떤 모델을 쓸지, 4층은 여러 GPU에 나누기, 5층은 실제 추론 엔진, 6층은 재학습 없는 최적화, 7층은 모델 자체입니다.
맨 아래 칩들(Voice · Reasoning · Video · Code …)이 모델을 기능과 목적으로 분류한 것입니다.

KubeCon 2025 발표 도식

노드 1대(TP 8) vs 노드 2대(TP 8 · PP 2)

이 그림이 말하는 것

4층 Distributed serving이 실제로 무엇을 하는지 보여주는 그림입니다.
① GPU 8장짜리 노드 하나에서는 Tensor Parallel 8 — 한 층의 행렬을 8조각으로 갈라 8장이 동시에 계산합니다. 노드 안이라 GPU 사이 대역폭이 넓어서 가능합니다.
② 노드가 둘이 되면 거기에 Pipeline Parallel 2가 얹힙니다 — 층 묶음을 노드끼리 나눠 갖습니다. 노드 사이는 느리니 층 경계에서만 주고받게 자르는 것입니다.

그림 6

일곱 개의 층 — 눌러서 들여다보기

↔ 층 클릭
해볼 것 — ① 층을 클릭하면 역할과 난제, 그리고 어느 장에서 다루는지가 뜹니다. ② 요청 흘려보내기를 눌러보세요. 요청 하나가 위에서 아래로 통과하는 경로가 보입니다. ③ Kubernetes가 덮는 범위를 눌러보세요 — 다음 장에서 왜 K8s부터 시작하는지가 보입니다.
관찰 포인트

층마다 난제가 다릅니다. 그래서 소유하는 팀도 다릅니다.

이 구조가 오래 가는 이유

새로운 모델과 기술이 계속 시스템 설계를 재편할 것입니다. 그래도 API · 오케스트레이션 · 추론 · 최적화 같은 관심사를 분리하는 계층형 패턴은 남습니다.
서로 다른 역할의 팀들이 독립적으로 혁신하면서도 하나의 플랫폼에 기여할 수 있게 하니까요.

6장 세 줄 요약
  1. 엔터프라이즈 서빙은 일곱 층입니다 — Public API · Resource · Selection · Distributed · Core Inference · Optimization · Model.
  2. 층마다 난제가 다르고, 그래서 소유하는 팀도 다릅니다. 층 경계 = 책임 경계.
  3. 층을 나누는 목적은 팀들이 서로 발목 잡지 않고 동시에 기여하게 만드는 것입니다.

— Part 3. 오픈소스 스택으로 직접 짓기 —

07Public API — 인증 · 쿼터 · 오토스케일

한 줄 요약: 두 가지 인증 방식이 결국 같은 목적지로 수렴합니다 — tenant 값 하나.

왜 이걸 배우나
"1층의 난제 네 개를 실제 코드로 옮기면 몇 줄이나 되지?"

생각보다 적습니다. 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 같은 파운데이션 모델 벤더 양쪽 모두의 백본으로 채택됐습니다.

K8s 한 층이 덮는 범위

리소스 관리 · 네트워킹 · 트래픽 라우팅과 로드 밸런싱 · 서비스 호스팅과 스케일링 · 서비스 메트릭과 모니터링.
다시 말해 레이어 1의 라우팅/네트워킹 부분과 레이어 2 전체를 실제 오픈소스 컴포넌트로 구현해주는 기반 계층입니다.

코드 — 엔드포인트는 이게 전부입니다

# FastAPI 채팅 엔드포인트 + 인증 의존성 주입
@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를 선언적으로 끼워넣기 때문입니다. 핸들러는 "인증된 테넌트로 쿼터를 걸고 라우팅한다"는 비즈니스 로직만 갖습니다.

# authorize request either with JWT or api_key
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

apiVersion: autoscaling/v2
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

kind: Ingress
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건)까지 허용합니다.

원문 그림 4-7

오픈소스 스택으로 구현한 모델 서빙

이 그림이 말하는 것

맨 위 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도 요청 경로 밖에서 점선으로 붙습니다 — 관측성은 트래픽과 별개의 축이라는 뜻입니다.

그림 7

부하가 올라가면 무슨 일이 — HPA와 rate limit

↔ 슬라이더 2개
해볼 것 — ① 들어오는 부하를 올려보세요. 레플리카가 3에서 15까지 자동으로 늘어납니다. ② 부하를 초당 250건 위로 밀어보세요. 인그레스가 버스트 한도에서 잘라냅니다 — 앱까지 오지도 못합니다. ③ 테넌트 쿼터를 줄여보세요. 인그레스는 통과했는데 앱에서 다시 걸립니다. 이게 이중 방어입니다.
관찰 포인트

인그레스에서 잘린 요청은 파드에 부하를 주지 않습니다. 그래서 값싼 방어입니다.

7장 세 줄 요약
  1. 인증은 JWT든 API 키든 결국 tenant 하나로 수렴합니다. 그 뒤 모든 결정이 여기 매답니다.
  2. 고동시성은 HPA(CPU 70%, 3~15 레플리카)가, 공정 사용은 Ingress rate limit(50rps, burst ×5)이 맡습니다.
  3. rate limit이 두 층에 걸린 건 실수가 아니라 defense in depth입니다 — 거친 방어 + 정교한 강제.

08모델 선택 — 카나리는 왜 테넌트보다 먼저인가

한 줄 요약: 순서 하나가 카나리 배포의 대표성을 결정합니다.

왜 이걸 배우나
"라우팅 함수가 열 줄인데, 여기서 뭘 배우지?"

그 열 줄 안에 순서가 결과를 바꾸는 지점이 있습니다. 카나리 체크가 테넌트 오버라이드보다 먼저 오는 게 실수처럼 보이는데, 의도된 설계입니다.

이 장에서는 라우팅 정책 네 단계를 따라가고, speculative decoding을 언제 켜는지의 기준(1,024토큰)이 왜 그 숫자인지 봅니다.

이 장에 나오는 낯선 말
Canary deployment (카나리 배포)
새 버전에 트래픽 일부만 먼저 흘려 문제를 조기에 잡는 배포 방식.
Draft model / Target model
speculative decoding에서 빠르고 저렴한 쪽과 느리지만 정확한 쪽.
Passthrough (패스스루)
중간에서 손대지 않고 그대로 흘려보내는 것. 여기서는 스트리밍 응답을 그대로 전달.
Tenant override
특정 고객에게 전용 엔드포인트(전용 파인튜닝 모델, 전용 용량)를 지정하는 것.
Alias (별칭)
모델 이름의 다른 표기를 실제 라우트로 이어주는 매핑.

비유 먼저 — 표본을 어디서 뽑는가

신제품을 시험 판매합니다. 전체 손님의 5%에게 새 레시피를 내놓기로 했습니다.

그런데 "단골은 기존 레시피 고정"이라는 규칙을 먼저 적용하면 어떻게 될까요. 새 레시피는 뜨내기 손님에게만 나갑니다. 그 반응으로 전체를 판단하면 틀립니다.

그래서 5% 추첨을 먼저 돌리고, 거기 안 걸린 사람에게 단골 규칙을 적용합니다. 코드의 순서가 정확히 그렇게 되어 있습니다.

코드 — 라우팅 정책 네 단계

def choose_endpoint(model: str, tenant: str):
    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토큰이라는 기준선

@app.post("/v1/chat/completions")
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 엔드포인트 안에 두었습니다. 분류 규칙이 복잡해지면 별도 서비스로 빼는 게 보통입니다.

그림 8

요청 1,000건을 흘려보내면 어디로 가나

↔ 슬라이더 · 버튼
해볼 것 — ① 카나리 비중을 올려보세요. 전용 테넌트 요청도 같은 비율로 카나리에 빨려 들어갑니다. ② 순서 바꾸기를 눌러 테넌트 체크를 먼저 하게 해보세요. 카나리 표본에서 전용 테넌트가 통째로 빠집니다 — 편향입니다. ③ 출력 길이를 1,024 위아래로 밀어 speculative 분기가 켜지고 꺼지는 걸 보세요.
관찰 포인트

순서를 바꾸면 카나리 막대의 구성이 달라집니다. 총량이 아니라 누가 들어있느냐가 문제입니다.

8장 세 줄 요약
  1. 라우팅은 모델 조회 → 카나리 → 테넌트 오버라이드 → 기본 네 단계입니다.
  2. 카나리가 테넌트보다 먼저인 건 의도입니다 — 표본의 대표성을 지키기 위해서.
  3. speculative decoding은 긴 출력에서만 이득입니다. 그래서 max_new_tokens > 1024로 분기합니다.

09Ray Serve — 데코레이터 한 줄이 YAML을 대체할 때

한 줄 요약: "레플리카 3개, 각각 GPU 1개씩"이 파이썬 데코레이터 한 줄로 끝납니다.

왜 이걸 배우나
"3장에서 손으로 만든 모델 호스팅 레이어를 프레임워크로 바꾸면 뭐가 달라지지?"

인스턴스 서빙의 확장성과 네트워크 관리가 부담스러울 때 꺼내는 카드가 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개씩"이라고 적어 내면 나머지는 알아서 됩니다.

주문서가 편한 만큼 세부 조정은 프레임워크가 허용하는 범위 안입니다. 이게 이 장 이후로 계속 나올 편의성 ↔ 통제권의 첫 등장입니다.

단일 모델 호스팅 — 코드

# vLLM의 AsyncLLMEngine을 한 번 초기화
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))

그리고 데코레이터 한 줄이 배포를 정합니다.

# Create Ray Serve instance for hosting Qwen3 model
@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는 무거우니, 앞단을 더 많이 두는 게 자연스럽습니다.

그림 9

레플리카를 늘리면 무엇이 늘고 무엇이 안 느나

↔ 슬라이더 · 버튼
해볼 것 — ① 레플리카 수를 올려보세요. 처리량이 올라가지만 가용 GPU를 넘으면 멈춥니다. ② 동시 요청을 늘려보세요. 레플리카당 대기열이 쌓이면서 지연시간이 올라갑니다. ③ Ingress 2:1 버튼을 눌러 앞단 병목이 풀리는 걸 보세요.
관찰 포인트

GPU가 모자라면 레플리카는 Pending에 멈춥니다. 선언만으로 자원이 생기지는 않습니다.

9장 세 줄 요약
  1. @serve.deployment(num_replicas=3, num_gpus=1) 한 줄이 HPA YAML이 하던 일을 대체합니다.
  2. 수동 배칭/큐잉은 vLLM AsyncLLMEngine에 위임됩니다 — 3장에서 손으로 짠 부분입니다.
  3. 편해진 만큼 조정은 프레임워크가 허용하는 범위 안입니다. 편의성 ↔ 통제권의 첫 등장입니다.

10멀티플렉싱 — 우리가 손으로 짠 LRU를 프레임워크가

한 줄 요약: 3장의 ModelManager가 @serve.multiplexed 한 줄로 사라집니다.

왜 이걸 배우나
"3장에서 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

코드 — 요청마다 모델을 고릅니다

@serve.deployment(
    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가 캐싱을 적용하고, 메모리 같은 자원이 부족할 때 모델 교체를 관리합니다.

# Example registry: map model_id -> HF repo
# 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

모델 지정은 헤더로

# 3장에서는 body에 model_id를 넣었습니다. Ray는 헤더로 지정합니다.
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)가 "꽉 찼나?"의 기준을 정합니다.

그림 10

모델 세 개, 자리 두 개 — 캐시가 도는 걸 보기

▶ 재생 · 모델 버튼
해볼 것 — ① 모델 버튼을 눌러 요청을 보내보세요. 캐시에 있으면 즉시, 없으면 로딩 막대가 뜹니다. ② A → B → C 순으로 눌러보세요. C를 넣을 때 A가 밀려납니다(LRU). ③ 레플리카당 자리를 3으로 올리면 축출이 사라집니다 — 메모리를 더 쓰는 대신.
관찰 포인트

히트율이 낮으면 로딩 지연이 그대로 사용자 지연이 됩니다. 이게 cold start입니다.

"나만의 서빙 스택을 만드는 건 어렵지 않습니다"

원문의 결론입니다. 움직이는 요소가 많아 보이지만 대부분 이미 프로덕션에서 검증된 것들입니다.
인프라는 Kubernetes, 모델 호스팅은 Ray Serve 또는 Triton, 분산 LLM 서빙과 최적화는 vLLM. 여기에 가벼운 맞춤 미들웨어만 얹으면 완전한 엔터프라이즈급 솔루션을 직접 구축하는 것도 가능합니다.

10장 세 줄 요약
  1. @serve.multiplexed(max_num_models_per_replica=2)가 3장의 ModelManager 전체를 대체합니다.
  2. 모델 지정은 HTTP 헤더 ray_serve_multiplexed_model_id로 하고, 라우팅은 Ray가 합니다.
  3. lazy loading의 대가는 cold start입니다. 캐시 자리 수가 그 빈도를 정합니다.

11KubeRay와 RayService — kubectl apply 한 번으로

한 줄 요약: RayCluster는 클러스터만, RayService는 그 위의 Serve 앱까지 함께 관리합니다.

왜 이걸 배우나
"Ray는 알겠는데, 그걸 Kubernetes 위에서 굴리려면 뭐가 더 필요하지?"

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용도
RayClusterHead/Worker 파드로 구성된 Ray 클러스터 자체의 생명주기 관리 (기본 단위)
RayJobRayCluster를 만들어 단일 Job을 실행하고 완료되면 정리 (필요시 클러스터 자동 삭제)
RayServiceRayCluster 위에 Ray Serve 애플리케이션을 얹어 운영 — 무중단 업그레이드, 헬스체크 기반 HA
RayCronJobRayJob을 크론 스케줄로 반복 실행

RayCluster vs RayService — 무엇이 다른가

항목RayClusterRayService
역할 범위Ray 클러스터 자체만 관리클러스터 생성 + 그 위의 Serve 앱까지
배포 방식helm install 또는 CR 직접 apply단일 매니페스트에 rayClusterConfig + serveConfigV2
워크로드 접속kubectl exec 또는 대시보드 포트포워드 후 ray job submitServe용 Service(-serve-svc)로 바로 HTTP
Serve 앱없음 — serve run으로 수동 배포·갱신자동 관리 — 설정만 바꾸면 무중단 in-place 업데이트
헬스체크/HAk8s가 Serve 상태를 모름/-/routes 헬스체크로 k8s가 Serve 상태까지 파악, 장애 시 컨트롤러가 복구
GPU 워커 설정두 리소스가 차이 없음 — 그래서 난이도로 RayCluster를 고를 이유가 없습니다

실제 매니페스트 — 물리 GPU 1장에 맞춘 버전

apiVersion: ray.io/v1
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 apply -f vllm-service.yaml

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 호환 엔드포인트를 그대로 호출합니다.

curl -s http://192.168.254.150:30005/v1/chat/completions \
  -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 어디에 올려도 위층은 그대로라는 것이 이 스택의 값어치입니다.

그림 11

kubectl apply 이후 — 무엇이 순서대로 뜨나

▶ 재생 · 비교 토글
해볼 것 — ① 재생을 눌러 Head → Worker → 모델 다운로드 → Serve 앱 HEALTHY까지 따라가 보세요. ② RayCluster로 전환을 눌러보세요. Serve 앱 단계에서 사람이 개입해야 하는 지점이 뜹니다. ③ 파드 죽이기를 눌러보세요. RayService는 컨트롤러가 복구하고, RayCluster는 멈춰 있습니다.
관찰 포인트

AWQ 모델도 다운로드에 4~5GB, 최초 로딩에 몇 분이 걸립니다. cold start는 현실입니다.

11장 세 줄 요약
  1. KubeRay는 Ray의 Head/Worker를 Kubernetes 파드로 관리하는 공식 Operator입니다.
  2. CRD는 넷 — RayCluster · RayJob · RayService · RayCronJob. 프로덕션 서빙은 RayService입니다.
  3. RayService는 무중단 in-place 업데이트와 헬스체크 기반 복구를 컨트롤러가 대신해 줍니다.

— Part 4. 클라우드 벤더로 짓기 —

12여섯 단계 — 택시부터 자가용 정비까지

한 줄 요약: 단계가 올라갈수록 자유도가 오르고, 정확히 같은 만큼 운영 부담도 오릅니다.

왜 이걸 배우나
"SageMaker 쓰는 법을 배우는 건가?"

아닙니다. 원문이 스코프를 명확히 못 박습니다 — 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의 컨테이너 이미지 저장소. 직접 만든 이미지를 여기 푸시합니다.

비유 먼저 — 자동차 여섯 단계

옵션방식비유
1Bedrock택시 — 목적지만 말합니다. 차량 관리는 신경 쓰지 않습니다.
2SageMaker JumpStart렌터카 — 차종은 고를 수 있지만 준비된 목록 안에서.
3Bring Your Own Model엔진은 내 것, 차량 플랫폼은 AWS — 제공되는 DLC 컨테이너를 씁니다.
4Bring Your Own Code컨테이너는 AWS 것이지만 전처리·로딩·예측·후처리를 직접 씁니다.
5Bring Your Own Serving ImageDocker 이미지까지 직접 — CUDA·Python·vLLM·라이브러리 전부 내 구성.
6Build Your Own Infrastructure거의 모든 것을 직접 — EKS + GPU 노드 + vLLM/Triton/Ray Serve + Karpenter + Prometheus.

1단계 Bedrock — 세 단계면 끝납니다

① 오레곤 리전(us-west-2)에서 Bedrock API 키 생성 → ② 모델 카탈로그에서 선택 → ③ 클라이언트 초기화 + 호출.

os.environ["AWS_BEARER_TOKEN_BEDROCK"] = "..."

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 — 본질적 차이는 "어디서 도느냐"

BedrockJumpStart
모델이 어디서AWS가 완전히 숨긴 인프라여러분의 SageMaker 계정/인프라
과금요청/토큰당 (서버 없음)시간당 인스턴스 요금
인프라 관리거의 없음인스턴스 타입(g5.48xlarge 등)을 직접 선택

"원클릭 배포"라고 불리지만, Bedrock과 달리 여러분 계정에 실제 엔드포인트가 뜹니다. 그래서 deploy()라는 단계가 새로 생깁니다.

3단계 DLC — 우리 3장 실습과 만나는 지점

model = DJLModel(
  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입니다.

3단계의 한계 — 고정된 계약

· 프레임워크/드라이버/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장 초반의 일곱 레이어 전체를 사실상 직접 소유하게 됩니다.

원문 그림 4-8

사용 편의성 기준 의사결정 트리

이 그림이 말하는 것

질문 다섯 개로 여섯 옵션이 갈립니다. 왼쪽으로 갈수록 편하고, 아래로 내려갈수록 통제권이 커집니다.
"LLM 출력만 필요한가?" → 예면 Bedrock에서 끝납니다.
아니면 "내 계정에 모델만 빨리 띄우면 되나?" → 예면 JumpStart.
이런 식으로 "AWS 기본 서빙 코드로 괜찮나", "서빙 코드만 고치면 되나", "인프라 전체를 관리해야 하나"까지 내려갑니다.
원문은 이 트리가 사용 편의성부터 시작하는 이유를 밝히면서도 곧바로 덧붙입니다 — "개발 속도는 여러 요인 중 하나일 뿐"이라고요.

그림 12

스펙트럼을 밀어보기 — 누가 무엇을 책임지나

↔ 슬라이더
해볼 것 — ① 슬라이더를 1에서 6까지 밀어보세요. 파란 칸(AWS 책임)이 하나씩 주황(내 책임)으로 넘어옵니다. ② 6단계에서 파란 칸이 몇 개 남았는지 세어보세요. ③ 오른쪽의 자유도 / 운영 부담 두 막대가 같이 올라간다는 걸 확인하세요.
관찰 포인트

6단계에서도 물리 GPU 하드웨어는 여전히 클라우드 것입니다. 그것만 파랗게 남습니다.

12장 세 줄 요약
  1. 여섯 옵션은 완전 관리형 → 최대 커스터마이징 순으로 배열된 한 축입니다.
  2. 단계가 오를수록 AWS에 위임하던 레이어가 하나씩 내 책임으로 넘어옵니다.
  3. 목적은 SageMaker 하우투가 아니라 벤더들이 옵션을 설계하는 논리를 아는 것입니다.

13손익분기점 — 초당 3,255토큰

한 줄 요약: "언제 옮겨탈까"는 취향이 아니라 계산입니다.

왜 이걸 배우나
"Bedrock이 편한 건 알겠는데, 언제까지 편해도 되지?"

토큰당 과금은 쓴 만큼 냅니다. 인스턴스 시간당 과금은 켜둔 만큼 냅니다. 이 둘은 어느 처리량에서 반드시 교차합니다.

이 장에서는 원문의 실제 사례(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보다 전용 인스턴스를 직접 호스팅하는 게 더 쌉니다. (출력 토큰 과금·인스턴스 유휴 시간 등은 단순화해서 제외한 근사치입니다.)

그림 13

교차점을 직접 옮겨보기

↔ 슬라이더 3개
해볼 것 — ① 처리량을 밀어 두 선이 교차하는 지점을 찾아보세요. 기본값에서 3,255 근처입니다. ② 가동률을 낮춰보세요. 인스턴스는 놀아도 돈이 나가서 교차점이 오른쪽으로 밀립니다. ③ 인스턴스 단가를 올려보세요 — 더 좋은 GPU를 쓰면 손익분기점이 어떻게 되는지.
관찰 포인트

Bedrock 선은 원점에서 시작합니다. 안 쓰면 0원입니다. 그게 초기 검증에 유리한 이유입니다.

흔한 패턴 — 단순하게 시작해서 성숙하며 옮겨간다

많은 팀이 아이디어 검증은 가장 단순한 옵션(Bedrock)으로 시작하고, 프로젝트가 성숙하거나 요구사항이 커지면 더 커스터마이징된 옵션으로 옮겨갑니다.
의사결정 트리가 사용 편의성부터 시작하는 이유도 같습니다 — AWS 서비스를 많이 쓸수록 빨리 띄웁니다. 다만 원문은 곧바로 덧붙입니다: "개발 속도는 여러 요인 중 하나일 뿐, 운영/유지보수 비용도 똑같이 중요하다."

13장 세 줄 요약
  1. 토큰당 과금과 시간당 과금은 반드시 어딘가에서 교차합니다. 그 지점이 판단 기준입니다.
  2. 원문 예시에서는 초당 약 3,255토큰이 그 지점이었습니다 ($1.172/h vs $0.10/1M).
  3. 가동률이 낮으면 교차점은 오른쪽으로 밀립니다 — 놀리는 GPU는 그대로 비용입니다.

— Part 5. 직접 지을까, 사서 쓸까 —

14Build or Buy — 스펙트럼 위 내 자리

한 줄 요약: 직접 짓지 않더라도 지을 줄 아는 것이 남습니다. 벤더의 노브를 해독할 수 있게 되니까요.

왜 이걸 배우나
"결국 클라우드 쓸 건데 3장을 왜 그렇게 힘들게 배웠지?"

이 장이 그 질문에 답합니다. 원문 저자 둘은 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 하드웨어·런타임·독립 네트워킹을 직접 제어해야 하거나, 서비스 비용이 높아 세밀한 튜닝이 필요할 때
다시 벤더로 볼륨이 낮은 속도로 안정되거나, 시스템 복잡성이 성과를 내지 못할 때

여기서 하나만 기억한다면 — 마지막 줄이 중요합니다. 되돌아가는 것도 전략입니다. 스펙트럼 위의 이동은 한 방향이 아닙니다.

그림 14

내 상황을 넣으면 어디로 가나

↔ 토글 4개
해볼 것 — ① 네 가지 조건을 켜고 꺼보세요. 오른쪽에 권장 위치가 바뀝니다. ② "SLO 충족 + 비용 허용"만 켜보세요 — 벤더 유지가 나옵니다. 아무것도 안 해도 되는 상태입니다. ③ "복잡성이 성과를 못 냄"을 켜보세요. 화살표가 왼쪽으로 돕니다.
관찰 포인트

같은 팀도 분기마다 답이 달라집니다. 그래서 자리를 고정하지 말라는 것입니다.

성공적인 전략은 유연합니다

원문의 결론입니다 — 안정적인 API와 공유 텔레메트리를 유지하고, 효과가 나는 부분을 커스터마이징하며, 제품·트래픽·제약이 변함에 따라 스펙트럼 상의 위치도 바뀔 것을 예상해야 합니다.
API가 안정적이어야 옮길 수 있고, 텔레메트리가 공유되어야 옮긴 결과를 비교할 수 있습니다.

14장 세 줄 요약
  1. Build vs Buy는 이분법이 아니라 스펙트럼이고, 대부분의 팀은 중간에 있습니다.
  2. 직접 지을 줄 알면 벤더 기능 해독 · 트레이드오프 정량화 · 80/20 커스터마이징 · 디버깅이 가능해집니다.
  3. 판단은 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입니다. 그래서 이렇게 됩니다.

E2E latency = TTFT + ITL × (N − 1)
# N = 총 생성 토큰 수. ITL이 일정하고 토크나이즈/디토크나이즈는 무시한 가정.

TTFT = 입력 전체 토큰화 + prefill + 첫 토큰 디코드 + 첫 토큰 디토크나이즈
      ≈ prefill + 첫 토큰 디코드

ITL  = 새 토큰 하나 디코드 + 디토크나이즈 − 직전 디토크나이즈
      ≈ 새 토큰 하나 디코드

토크나이즈·디토크나이즈 시간이 무시되는 건 게으름이 아니라 실제로 모델 추론 대비 미미하기 때문입니다.

유스케이스별로 우선순위가 다릅니다

유스케이스최우선이유
에이전틱 워크플로우E2E 다운스트림이 전체 출력에 의존합니다. generate_analysis는 generate_summary의 완성된 전체 출력이 있어야 시작합니다 — 첫 토큰만 빨리 나오는 건 의미가 없습니다.
챗봇 (스트리밍)TTFT 사용자가 체감하는 응답성 그 자체. 첫 글자가 뜨는 순간 "반응했다"고 느낍니다.
아주 긴 출력ITL ITL이 높으면 "느릿느릿하다"고 체감됩니다. 첫 토큰이 빨라도 소용없습니다.

여기서 하나만 기억한다면 — 1장의 Knowledge Agent가 정확히 첫 번째 케이스였습니다. 체이닝 구조라서 각 단계의 E2E가 다음 단계를 블로킹했습니다.

원문 그림 4-9

LLM 실행의 주요 지연시간 지표

이 그림이 말하는 것

세로가 시간입니다. 맨 왼쪽 긴 화살표가 End-to-end latency — 요청부터 마지막 토큰까지 전부입니다.
그 오른쪽 짧은 화살표가 Time to first token입니다. 여기에 무엇이 들어가는지 보세요 — Tokenization + Prefill + 첫 Decode + 첫 Detokenization. Prefill 상자가 유난히 큰 게 핵심입니다.
두 번째 토큰부터는 Decode + Detokenization만 반복되고, 그 간격이 Inter-token latency입니다. 계단처럼 오른쪽 아래로 내려가는 모양이 자기회귀 생성의 순차성을 그대로 보여줍니다.

그림 15

세 지표를 직접 움직여보기

↔ 슬라이더 3개
해볼 것 — ① TTFT만 줄여보세요. 챗봇 만족도는 크게 오르는데 E2E는 조금밖에 안 줄어듭니다. ② ITL을 줄여보세요. 토큰이 많을수록 E2E가 크게 줄어듭니다 — (N−1)이 곱해지니까요. ③ 토큰 수를 늘려보세요. 어느 지점부터 ITL이 지배하기 시작하는지 보세요.
관찰 포인트

파란 조각이 프리필, 초록 띠가 디코드입니다. 어느 쪽이 더 넓은지가 곧 최적화 대상입니다.

모든 최적화가 셋 다 좋아지게 하지는 않습니다

원문이 미리 경고합니다 — 몇 가지 타협이 필요하기 때문에, 기법 선택은 유스케이스의 구체적 요구사항에 달렸습니다.
예를 들어 배치를 크게 묶으면 처리량과 ITL은 좋아지는데 TTFT는 나빠질 수 있습니다 — 묶이기를 기다려야 하니까요.

15장 세 줄 요약
  1. E2E = TTFT + ITL × (N − 1). TTFT는 prefill, ITL은 decode에 대응합니다.
  2. 우선순위는 유스케이스가 정합니다 — 에이전트는 E2E, 챗봇은 TTFT, 긴 출력은 ITL.
  3. 셋을 동시에 개선하는 기법은 드뭅니다. 무엇을 포기할지 먼저 정해야 합니다.

16처리량 — 그리고 부풀릴 수 있다는 사실

한 줄 요약: TPS는 입력을 짧게 자르거나 배치를 잘 짜면 올라갑니다. 그래서 측정 조건을 물어야 합니다.

왜 이걸 배우나
"벤더가 발표한 TPS 숫자, 그대로 믿어도 되나?"

이 절이 은근히 경고하는 게 정확히 그것입니다. TPS는 인위적으로 부풀릴 수 있고, 부풀리는 방법이 둘 다 합법적인 최적화처럼 생겼습니다.

이 장에서는 두 처리량 지표의 한계를 보고, 마지막으로 성능 측정 모범 사례 아홉 가지로 이 장 전체를 닫습니다.

이 장에 나오는 낯선 말
RPS / RPM
초당/분당 요청 수. ML이 아닌 워크로드에서도 널리 쓰는 일반 지표.
TPS (Tokens Per Second)
초당 생성되는 출력 토큰 수. 입력 토큰이나 입출력 합산은 포함하지 않습니다.
Inflate (부풀리기)
측정 조건을 유리하게 만들어 지표를 실제보다 좋아 보이게 하는 것.
Regression test (회귀 테스트)
새 변경이 기존 성능을 악화시키지 않았는지 확인하는 반복 테스트.

비유 먼저 — "시속 몇 km 나오나요"

같은 차라도 내리막에서 짐 없이 재면 숫자가 훨씬 잘 나옵니다. 거짓말은 아닙니다. 조건이 다를 뿐입니다.

그래서 연비를 비교할 때는 측정 조건을 표준화합니다. TPS에는 그런 표준이 사실상 없습니다. 그래서 조건을 물어야 합니다.

두 지표, 각각의 한계

지표무엇을 재나한계
RPS / RPM일정 시간 동안 처리 가능한 요청 수 요청 패턴에 민감합니다. 긴 입력·긴 출력이 성능에 크게 영향을 주므로 입출력 길이와 동시 사용자 수에 좌우됩니다. → 트래픽 패턴이 다른 두 워크로드의 RPS 비교는 사실상 무의미합니다.
TPS초당 생성되는 출력 토큰 수 부풀릴 수 있습니다. 아래 두 가지 방법으로.
TPS를 부풀리는 두 가지 방법

① 입력을 짧게 자른다 — TTFT가 크게 줄고 요청당 작업 부하가 줄어듭니다. 그래서 TPS가 실제보다 높아 보입니다.
② 배치를 크게 또는 균일하게 짠다 — 배치 크기를 늘리거나 입출력 길이를 균일하게 만들면 GPU 활용도가 오르고 유휴 시간이 줄어 TPS가 개선됩니다. (이유는 6·7장에서)
둘 다 정당한 최적화처럼 생겼다는 게 함정입니다.

여기서 하나만 기억한다면 — TPS는 생성된 출력 토큰만 셉니다. 입력 토큰도, "입력+출력" 합산도 아닙니다. 이 구분을 놓치면 벤치마크를 잘못 읽습니다.

그림 16

같은 시스템, 다른 숫자 — TPS를 부풀려보기

↔ 슬라이더 · 버튼
해볼 것 — ① 입력 길이를 줄여보세요. 모델은 그대로인데 TPS 숫자가 올라갑니다. ② 배치 크기를 키우고 길이 균일화를 켜보세요. 한 번 더 올라갑니다. ③ 정직한 조건 버튼을 눌러 실제 트래픽에 가까운 값으로 되돌려보세요 — 차이가 몇 배인지 확인하세요.
관찰 포인트

TPS는 올라가는데 TTFT가 같이 나빠집니다. 하나만 자랑하면 나머지가 숨습니다.

성능 측정 모범 사례 아홉 가지

이 장에 나온 모든 지표를 실전에서 신뢰할 수 있게 쓰는 법으로 종합한 체크리스트입니다.

#원칙이 스터디에서의 대응
1지연 vs 처리량 트레이드오프 파악 오프라인/배치는 처리량, 챗봇/실시간 에이전트는 지연이 핵심. → CH3의 비용 최적화 설계 vs 지연 최적화 설계 대비.
2"충분히 좋은" 목표 설정 1초→0.5초가 체감상 의미 없다면 그 노력을 처리량/비용으로. → 손익분기점 3,255도 "여기까지는 안 옮겨도 된다"의 응용.
3E2E를 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 테스트(프로덕션 배포 전 데이터 기반 비교).
16장 세 줄 요약
  1. RPS/RPM은 요청 패턴에 민감해서, 트래픽 패턴이 다르면 비교 자체가 무의미합니다.
  2. TPS는 출력 토큰만 세고, 입력 단축·배치 최적화로 부풀릴 수 있습니다.
  3. 그래서 어느 쪽도 단독으로는 완전하지 않습니다. 조건을 밝히고 여러 지표를 함께 봐야 합니다.

마치며

이 장은 2·3장 위에 한 층을 더 올린 장이었습니다. 모델을 어떻게 실행하는가에서, 프로덕션에서 무엇이 더 필요한가로요.

네 갈래로 정리하면
  1. 에이전틱 서빙 — Knowledge Agent(OpenAI 모델 + Planner + Actions)로 서빙이 자율성을 어떻게 뒷받침하는지 보고, RAG vs CAG의 트레이드오프를 비교했습니다.
  2. 엔터프라이즈 아키텍처 — 독립적으로 진화 가능한 일곱 층과, 그것을 실제로 구현하는 Kubernetes + Ray Serve + vLLM 스택을 봤습니다.
  3. 빌드 vs 클라우드 — 커스터마이징이 커지는 여섯 옵션과 의사결정 트리. 핵심은 이분법이 아니라 스펙트럼이고, 대부분의 팀은 중간에서 균형점을 계속 옮깁니다.
  4. 성능 지표 표준화 — 지연시간과 처리량, 그리고 공정한 벤치마킹·현실적 트래픽 시뮬레이션· 하드웨어 활용률 추적·회귀/스케일링 테스트라는 모범 사례.

LLM 서빙 스터디 2주차 · CH4 — Model Serving Best Practices

본문의 코드·설정·수치는 스터디 원문 자료에서 인용했습니다. 그림 16개는 전부 조작 가능합니다.

반응형

'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
    devlos
    devlos
    안녕하세요, Devlos 입니다. 새로 공부 중인 지식들을 공유하고, 명확히 이해하고자 블로그를 개설했습니다 :) 여러 DEVELOPER 분들과 자유롭게 지식을 공유하고 싶어요! 방문해 주셔서 감사합니다 😀 - DEVLOS -

    티스토리툴바