CH4. 프로덕션 LLM 서빙 - 정리

limes22·2026년 8월 15일

CH4-0. 프로덕션 LLM 서빙 — 개요와 용어 사전

이 시리즈의 구성

제목다루는 것
0개요와 용어 사전이 문서
1에이전틱 서빙Knowledge Agent, RAG, CAG
2엔터프라이즈 계층형 아키텍처7개 레이어와 각 레이어의 난제
3오픈소스 스택Kubernetes + Ray Serve + KubeRay 실습
4클라우드 벤더 6단계SageMaker 스펙트럼, Build vs Buy
5성능 지표Latency, Throughput, 측정 모범 사례
6마지막 3문제 답변학습 질문 풀이

3장에서 4장으로 — 무엇이 바뀌는가

세 줄 요약
1. 2~3장은 "모델을 어떻게 실행하는가"였고, 4장은 "실제 서비스를 어떻게 운영하는가"다.
2. 결정적 전환점은 모델이 요청당 한 번이 아니라 제어 루프 안에서 반복 호출된다는 것.
3. 그 순간 서빙은 추론 문제가 아니라 시스템 아키텍처 문제가 된다.

[그림 0-1] 전제의 전환

2~3장:  요청 1건 ──▶ 추론 1회 ──▶ 응답 1건
                (모델 = 독립적인 예측 엔진)

4장:    요청 1건 ──┬──▶ LLM 호출 (계획 수립)
                  ├──▶ embedding 호출 (검색)
                  ├──▶ LLM 호출 (1차 생성)
                  └──▶ LLM 호출 (2차 생성)
                (모델 = 제어 루프 안의 부품)

용어 사전

에이전트 관련

용어
agent목표를 이해하고, 방법을 추론하고, 도구를 골라 호출하고, 중간 결과에 적응해 반복하며, 사람 개입 없이 결과를 내놓는 자율 LLM 시스템
control loop최종 답이 나올 때까지 "생각 → 행동 → 관찰"을 반복하는 순환 구조
action에이전트가 호출할 수 있는 이산적(discrete) 작업 단위. 연속적이지 않고 서로 명확히 구분되는 유한 개수
planning사용자 질문을 어떤 action을 어떤 순서로 실행할지로 분해하는 단계
tool callingLLM이 도구를 고르고 구조화된 입력(보통 JSON)을 생성하면, 에이전트가 실제로 실행하고 결과를 다시 LLM에 넣는 4단계 패턴
MCPModel Context Protocol. 도구 발견·호출·결과 반영을 표준화한 프로토콜. 도구 정의를 에이전트 핵심 로직에서 분리
context chaining앞 action의 출력을 다음 action의 입력 context로 넘기는 것

검색·캐시 관련

용어
RAGRetrieval-Augmented Generation. 질의 시점에 외부 문서를 검색해 prompt에 붙여 넣는 방식
CAGCache-Augmented Generation. 지식을 미리 KV cache에 올려두고 검색 없이 바로 답하는 방식
embedding텍스트를 의미를 담은 숫자 벡터로 변환한 것
chunking문서를 검색 단위로 쪼개는 것. 보통 1,000 token 내외
vector databaseembedding 벡터를 저장하고 유사도 검색을 제공하는 저장소
cosine similarity두 벡터가 이루는 각도로 재는 유사도. 방향이 같으면 1
context windowLLM이 한 번에 처리할 수 있는 최대 token 수(입력 + 출력)
KV cache이미 계산한 Key/Value 중간값 저장소. 앞부분 재계산을 생략하게 해줌

서빙 인프라 관련

용어
prefill입력 프롬프트 전체를 한 번에 forward 시키는 첫 단계. compute-bound
decodeprefill 이후 token을 1개씩 이어 만드는 단계. memory-bound
speculative decoding빠르고 싼 draft model이 여러 token을 미리 만들고, 느리지만 정확한 target model이 한꺼번에 검증하는 기법
tensor parallel (TP)하나의 layer 연산을 여러 GPU로 쪼개는 병렬화. 같은 노드 안이 기본
pipeline parallel (PP)layer를 그룹으로 나눠 여러 노드에 배치하는 병렬화. 노드 간
multiplexing하나의 서비스 인스턴스가 여러 모델을 번갈아 서빙하는 것
canary새 버전으로 트래픽 일부만 흘려보내 검증하는 배포 방식
tenant하나의 플랫폼을 나눠 쓰는 고객/조직 단위
HPAHorizontal Pod Autoscaler. 지표를 보고 Kubernetes pod 수를 늘리고 줄이는 컨트롤러
CRDCustom Resource Definition. Kubernetes에 새 리소스 타입을 추가하는 방법
DLCDeep Learning Container. AWS가 프레임워크별로 미리 빌드해 둔 서빙용 Docker 이미지
LMILarge Model Inference. AWS의 대형 모델 추론 컨테이너. 내부에 vLLM 등을 탑재

지표 관련

용어
E2E latency요청 수신부터 전체 응답 완료까지의 시간. 큐 대기·네트워크·라우팅까지 포함 가능
TTFTTime To First Token. 첫 token이 나올 때까지. prefill 단계에 대응
ITL / TPOTInter-Token Latency / Time Per Output Token. 첫 token 이후 token 하나당 간격. decode 단계에 대응
RPS / RPM초당 / 분당 요청 수
TPSTokens Per Second. 출력 token만 셈. 입력이나 입출력 합산이 아님
SLO / SLAService Level Objective / Agreement. 서비스 수준 목표 / 협약

4장을 관통하는 한 문장

에이전트가 기본 패턴이 되면서, 사용자 요청 하나가 여러 번의 LLM 호출·검색·도구 실행을 촉발한다.
그래서 서빙은 모델을 효율적으로 실행하는 일을 넘어 오케스트레이션·메모리 관리·시스템 레벨 조정까지 감당해야 한다.

CH4-1. 질문 1개에 API 호출 4번

1장. 무엇이 달라졌나

세 줄 요약
1. 모델이 요청당 한 번이 아니라 제어 루프 안에서 반복 호출된다.
2. 그래서 사용자 상호작용 1회가 다중 LLM 호출 + 긴 context + 검색(RAG) + 캐시 재사용(CAG)을 촉발한다.
3. 결과는 셋 — token 사용량 증가, 체인 호출에서 tail latency 증폭, 동적인 트래픽 패턴.

전통적 어시스턴트 vs LLM 에이전트

전통적 (규칙 기반 챗봇)LLM 에이전트
워크플로우개발자가 미리 짜둠스스로 계획
지시 해석정해진 패턴 매칭자연어 해석
도구하드코딩된 호출스스로 선택·사용
적응없음피드백에 적응, 반복

차이의 본질은 자율성입니다 — "무엇을 어떻게 할지"까지 시스템이 판단합니다.

에이전트의 정의 (5가지 능력)

  1. 상위 수준 목표를 이해
  2. 달성 방법을 추론
  3. 외부 도구/데이터 소스를 선택·호출
  4. 중간 결과를 바탕으로 적응·반복
  5. 사람 개입 최소화(또는 없이) 최종 결과물 산출

실무에서는 에이전트 하나로 끝나지 않고, 여러 에이전트가 서로 쌓이거나 협업하거나 기능별로 특화된 네트워크/플랫폼 형태가 됩니다. 자율성은 커지지만 서빙 관점의 난도도 그만큼 올라갑니다.


2장. Knowledge Agent — 구조

세 줄 요약
1. PDF 폴더를 지식 베이스로 삼는 RAG 에이전트. 로컬 모델·DB 없이 OpenAI API + 인메모리.
2. 모델 2개가 역할 분담 — embedding용과 추론·생성용.
3. LLM이 혼자 다 하는 게 아니다. LLM은 생각, RAG는 자료 찾기, Planner는 순서 결정, Agent는 조정.

2-1. 모델 2개의 역할 분리

모델역할로그에 찍히는 것
text-embedding-3-small텍스트 → 의미 벡터. 시맨틱 검색용POST /v1/embeddings
gpt-4.1-nano추론·계획·자연어 생성POST /v1/chat/completions

2-2. 컴포넌트 4개

[그림 1-1] Knowledge Agent 구조

사용자 질문 → Agent.process_query()
              ├─ Planner (gpt-4.1-nano) ────────▶ 실행 계획 수립
              └─ ActionExecutor 순차 실행
                    ├─ RAGSystem (text-embedding-3-small) → 관련 문서 검색
                    └─ LLMManager (gpt-4.1-nano) ────────▶ 각 action별 응답 생성
컴포넌트하는 일
Agent (orchestrator)전체 조율. process_query()가 계획 → 실행 → 최종 응답까지 지휘
RAGSystemPDF 처리(청크 분할), embedding 생성, 벡터 검색
PlannerLLM으로 실행 계획 수립
ActionExecutor질의/요약/분석 등 구체적 작업 실행. 앞 결과를 다음 context로 체이닝
LLMManagerOpenAI API 호출과 token 관리

2-3. 사용 가능한 action 4개

  • query_rag_with_context — RAG 검색 + 답변
  • generate_profile_based_response — 사용자 프로필 반영 답변
  • generate_summary — 요약
  • generate_analysis — 심층 분석

각 action은 "LLM 호출 + 특화된 프롬프트 템플릿"으로 구현됩니다. 예를 들어 분석용 프롬프트는 질문과 context를 결합해 (1) 종합 분석 (2) 핵심 인사이트 (3) context에서 뽑은 예시 (4) 정보의 한계 — 네 가지를 요구하는 구조입니다.

주의: 이 샘플은 의도적으로 action을 전부 LLM 호출로만 만들었습니다. 실제 에이전트의 action은 여기 국한되지 않습니다 — 웹 검색, API 질의, DB 명령, 파일 관리, 외부 서비스 호출, 그리고 사용자에게 노출되지 않는 중간 추론 단계까지 포함됩니다.


3장. 실행 로그로 본 진짜 비용

세 줄 요약
1. 질문 1개 → API 호출 4회 (chat 3 + embedding 1).
2. LLM planner가 키워드 기반 fallback 규칙과 다른 판단을 내렸다 — 규칙보다 똑똑했다.
3. 2단계 action에서 embedding 호출이 없다 — context chaining이 실제로 일어났다는 증거.

3-1. 질문: "What is 5-level paging and how does it work?"

[그림 1-2] 실제 호출 순서 (조작 포인트: 계획 단계 수를 늘려보면 호출 수가 어떻게 늘어나는지)

기동 시  PDF 4개 → 42 chunk → [POST /v1/embeddings]  ← 인덱스 빌드

질문 입력
   │
   ├─ ① Planner.create_plan()          [POST /v1/chat/completions]
   │     → plan: ['generate_summary', 'generate_analysis']
   │
   ├─ ② action 1/2: generate_summary
   │     ├─ context 비어있음 → RAG 검색  [POST /v1/embeddings]
   │     │     → 관련 문서 5개 확보
   │     └─ 요약 생성                    [POST /v1/chat/completions]
   │
   └─ ③ action 2/2: generate_analysis
         └─ 분석 생성                    [POST /v1/chat/completions]
               ↑ embedding 호출이 없다! = 앞 요약을 context로 재사용
호출횟수
chat/completions (플래닝)1
embeddings (쿼리 검색)1
chat/completions (요약)1
chat/completions (분석)1
합계4회

3-2. 여기서 읽어야 할 것 3가지

① LLM 플래너가 규칙 기반보다 나은 판단을 했다.
_create_fallback_plan()의 키워드 규칙대로면 "what"/"how"에 매칭돼 ["query_rag_with_context"] 1스텝이었어야 합니다. 하지만 LLM 플래너가 살아있었기 때문에 generate_summary + generate_analysis 2스텝이라는 독자적 판단을 내렸습니다. "LLM 기반 플래닝이 규칙 기반보다 유연하다"를 로그로 확인한 셈입니다.

② context chaining이 실제로 작동했다.
2단계 직전에 embeddings 호출이 없습니다. 앞 action의 결과(50자 초과 조건 충족)가 context로 재사용되면서, generate_analysis는 원본 문서를 재검색하지 않고 방금 만든 요약문 자체를 입력으로 삼았습니다.

③ 사용자는 최종 결과만 봤다.
process_queryresults[-1]final_response로 반환합니다. 1단계 요약의 실제 텍스트는 화면에 전혀 안 나왔고, 내부적으로 2단계의 입력 재료로만 소비됐습니다. 겉보기엔 1번 답변이지만 실제로는 2단계 추론이 뒤에서 돌았습니다.

3-3. 방어적 설계와 조용한 실패 지점

패턴내용
fallback planLLM 응답 파싱 실패 시 키워드 매칭으로 대체 계획 생성 → 플래닝이 실패해도 멈추지 않음
action 단위 try/except한 스텝이 실패해도 에러 문자열만 남기고 나머지 진행
최상위 안전망process_query 전체가 try/except → 어떤 예외든 success: False dict로 변환. 프로세스가 죽지 않음
⚠️ 조용한 실패validate_action_prerequisites()len(documents) > 0만 확인. build_knowledge_base()를 안 부르면 모든 action이 에러 없이 스킵되고 success: True + "No response generated"가 반환됨

마지막 항목이 실무에서 제일 위험한 유형입니다. 실패가 실패처럼 안 보이는 실패입니다.


4장. Figure 4-2 — 9단계 워크플로우

세 줄 요약
1. 질문이 복잡해지면 계획이 3단계로 늘어난다.
2. RAG → 분석 → 요약 순으로, 각 단계가 앞 단계 출력을 먹는다.
3. 그림의 "LLM 서빙"은 같은 모델일 수도, 다른 모델일 수도 있다.

질문 예시: "데이터베이스 쿼리 최적화와 데이터 구조 최적화 간의 상세 비교를 작성해 주세요."

{
  "plan": ["query_rag_with_context", "generate_analysis", "generate_summary"],
  "reasoning": "First, retrieving relevant contextual information ensures ...",
  "estimated_steps": 3
}
[그림 1-3] 9단계 (조작 포인트: 각 화살표가 곧 API 왕복 1회)

 ① 사용자 질문
      ↓
 ② Agent → Planner 호출
      ↓
 ③ Planner → LLM 호출 ──────────▶ 3단계 실행 계획 반환
      ↓
 ④ ActionExecutor 순차 실행 시작
      ↓
 ⑤ [1단계] query_rag_with_context
      ├─ RAG 시맨틱 검색 → 관련 문서
      └─ 문서를 context로 LLM 답변 생성
      ↓
 ⑥ [2단계] generate_analysis ← ⑤의 결과를 context로
      ↓
 ⑦ LLM이 심층 비교 분석 생성
      ↓
 ⑧ [3단계] generate_summary ← ⑦의 결과를 요약
      ↓
 ⑨ Agent가 종합해 최종 결과 반환

5장. RAG — 오픈북 시험

세 줄 요약
1. LLM 입장에서 RAG는 prompt 텍스트가 길어진 것일 뿐이다. 행렬 주입이 아니다.
2. 워크플로우는 두 개 — 오프라인 인덱스 빌드, 온라인 질의/검색.
3. chunk 크기가 검색 정밀도와 문맥 풍부함 사이의 트레이드오프를 결정한다.

5-1. 흔한 오해부터 정리

RAG를 처음 접하면 "검색된 문서가 LLM 내부의 Q 행렬에 들어간다"거나 "RAG의 query와 attention의 Q가 같은 것"이라는 직관을 갖기 쉽습니다. 틀렸습니다.

[그림 1-4] RAG의 실제 데이터 흐름 (조작 포인트: top-k를 늘리면 X의 row가 늘어날 뿐)

사용자 query
     ↓
[embedding model — RAG 전용, LLM과 별개]
     ↓
query 벡터 (예: 1536차원)
     ↓
[Vector DB — cosine similarity 검색]
     ↓
top-k 문서 텍스트
     ↓
─────────────────────────────────────────
여기부터는 그냥 "텍스트 합치기" — 행렬 주입이 아님

prompt = f"""
{doc_1}
{doc_2}
...
Question: {query}
"""
─────────────────────────────────────────
     ↓
[LLM] → 토큰화 → X (입력 임베딩 행렬, row가 그만큼 늘어남)
     ↓
평소와 똑같은 self-attention
     ↓
응답

외부에서 검색했든 사람이 직접 붙여넣었든, LLM은 구분하지 못합니다.

비유하면 RAG는 오픈북 시험입니다. 매 시험(요청)마다 책(외부 DB)에서 찾아 답안(prompt)에 첨부합니다.
반대 방향이 fine-tuning — 암기, 즉 가중치에 지식을 내재화하는 방식입니다.

5-2. 워크플로우 2개

[그림 1-5] RAG의 두 워크플로우

[A] Index-building — 오프라인, 주기적 실행
    원시 문서(HTML/PDF) → 정제·파싱 → 일반 텍스트
        → chunking (~1,000 token)
        → embedding 벌크 계산 (오프라인 배치 추론)
        → Vector DB에 인덱싱

[B] Query/retrieval — 온라인, 실시간
    사용자 query → embedding
        → cosine similarity로 최근접 벡터 검색
        → 관련 chunk 반환
        → query + chunk를 LLM에 전달 → 응답

chunking은 단순 전처리가 아닙니다. 두 가지를 동시에 결정합니다.
1. 검색의 세분화 정도 — 시스템이 한 번에 고려할 수 있는 텍스트 양
2. 오프라인 배치 추론이 일어나는 지점 — 매번 문서 전체를 임베딩하는 대신 chunk별로 미리 계산

Knowledge Agent는 이걸 단순화해서 매번 기동할 때마다 인메모리로 다시 빌드합니다. 테스트를 4번 돌리면 42개 chunk를 4번 재임베딩합니다. 실무에서는 이 부분이 주기적 배치 파이프라인으로 분리됩니다.

5-3. chunk 크기 트레이드오프

[그림 1-6] Fine vs Coarse chunking (조작 포인트: chunk 크기를 200 ↔ 2000 token으로)

작은 chunk:  ■ ■ ■ ■ ■ ■ ■ ■   검색 정밀도 ↑ / 문맥 손실 위험
큰 chunk:    ■■■■   ■■■■        문맥 풍부 / 관련 없는 내용 섞여 정밀도 ↓ (dilution)

최적값은 도메인과 LLM의 context window에 따라 달라집니다. 정답이 없습니다.

5-4. 왜 쪼개야 하는가

LLM은 입력과 출력을 합산한 token 수에 상한이 있습니다. 이 공간이 context window입니다. 그래서 문서 전체가 아니라 관련성 높은 chunk 몇 개만 골라 보내야 합니다.


6장. CAG — 검색 대신 미리 올려두기

세 줄 요약
1. RAG의 3가지 대가 — 검색 지연, 선택 오류, 시스템 복잡도.
2. context window가 100만 token까지 커지면서 "몇 개만 골라 넣는다"는 전제 자체가 흔들렸다.
3. CAG는 지식을 미리 KV cache에 올려두고 검색 없이 답한다. 대신 메모리/연산 요구량이 늘어난다.

6-1. RAG의 한계 3가지

한계내용
추가 지연시간LLM 호출 전에 "query embedding → 유사도 검색" 단계가 끼어듦
선택 오류 위험제한된 token 안에서 "가장 관련 있는" 문서만 골라야 하는데, 잘못 고를 수 있음
시스템 복잡도embedding·인덱스·벡터 DB를 구축·유지해야 함

6-2. RAG vs CAG

[그림 1-7] 두 방식의 구조 차이 (조작 포인트: 쿼리 수를 1 → 100으로 늘려보기)

RAG:  query 1 ─▶ [검색] ─▶ LLM      ← 매 쿼리마다 검색 발생
      query 2 ─▶ [검색] ─▶ LLM
      query 3 ─▶ [검색] ─▶ LLM

CAG:  [지식 문서를 KV cache에 1회 로드]
      query 1 ─────────────▶ LLM     ← 검색 없음
      query 2 ─────────────▶ LLM
      query 3 ─────────────▶ LLM

CAG는 에이전트 설계 자체를 크게 단순화합니다 — embedding model, vector DB, 검색 로직이 통째로 사라집니다.

6-3. CAG의 대가

큰 context window + 캐시 관리는 메모리와 연산 요구량을 늘립니다. 지식이 커질수록 KV cache가 커지고, 그만큼 동시 처리 가능한 요청 수가 줄어듭니다. 이걸 최적화하는 기법이 7장 주제입니다.

6-4. 양자택일이 아니다

강점개선하는 축
RAG동적 검색, 최신성, 지식 갱신이 쉬움 (vector DB만 갱신)답변 품질 (지식 접지·최신성)
CAG반복 query·고정 지식셋에서 latency 절감서빙 효율 (지연시간·처리량·비용)

RAG로 입력을 보강하고, CAG로 실행을 최적화하는 조합이 현실적인 답입니다.


7장. 에이전트가 모델 서빙을 쓰는 방식

세 줄 요약
1. 에이전트는 LLM 하나가 아니라 여러 모델과 도구를 조율한다.
2. tool calling은 4단계 루프이고, 그 루프가 곧 반복 호출의 원천이다.
3. 이 모든 게 HTTP/gRPC API로 온디맨드 호출되므로, 저지연·저비용 서빙이 성공 조건이 된다.

7-1. 에이전트가 쓰는 것들

종류용도
LLM추론, 계획, 대화 생성
embedding model검색, 유사도 매칭
비전/음성 모델멀티모달 인식
태스크 전용 모델코드 생성, 분류, 요약
외부 도구API, DB, 서비스

7-2. tool calling 4단계

[그림 1-8] tool calling 루프 (조작 포인트: 이 루프가 몇 바퀴 도는지가 곧 서빙 부하)

① LLM이 요청 + 사용 가능한 도구를 놓고 추론
        ↓
② 도구 호출용 구조화 출력(JSON) 생성
        ↓
③ 에이전트가 도구를 실제로 실행
        ↓
④ 결과를 다시 LLM에 투입 ──┐
        ↑                   │
        └───── 작업이 끝날 때까지 반복 ─┘

MCP(Model Context Protocol)는 이 패턴을 표준화합니다 — 도구 발견, 구조화된 호출, 결과 반영을 일관된 인터페이스로 제공하고, 도구 정의를 에이전트 핵심 로직에서 분리해 ad-hoc 프롬프트 엔지니어링의 취약성을 줄입니다.


CH4-2. 엔터프라이즈 계층형 아키텍처 — 7개 레이어

1장. "모델 실행"과 "엔터프라이즈 서빙"은 다른 문제다

세 줄 요약
1. 2~4장 앞부분에서 만든 것은 전부 "모델을 호스팅하고 실행한다"는 좁은 의미의 서빙이었다.
2. 실제 프로바이더는 그 위에 인증·과금·리소스·네트워킹·최적화·실험·관측성·온콜을 더 얹어야 한다.
3. 그리고 이 아키텍처가 어려운 진짜 이유는 기술이 아니라 조직이다.

1-1. 추가로 감당해야 하는 것들

  • 인증(authentication)
  • 과금 정책(pricing)
  • 리소스 관리
  • 네트워킹
  • 최적화
  • 실험(A/B 테스트 등)
  • 관측성(observability)
  • 온콜 지원

1-2. 진짜 난제는 조직 설계다

책이 명시적으로 짚는 부분입니다.

  • 서로 다른 책임을 가진 여러 팀이, 병목이나 과도한 상호 의존 없이 하나의 진화하는 시스템에 동시에 기여할 수 있어야 함
  • 빠른 반복(iteration)과 안정성의 균형
  • 이 모든 걸 비용·신뢰성·사용자 경험이라는 제약 안에서

즉 서빙 아키텍처 설계는 "레이턴시를 줄이는 기술" 문제가 아니라,
"어떤 레이어를 누가 소유하고, 경계를 어디에 그어야 팀들이 서로 발목을 안 잡는가"라는 조직 설계 문제이기도 합니다.


2장. 전체 그림

[그림 2-1] 7개 레이어 (조작 포인트: 위에서 아래로 갈수록 추상화가 벗겨진다)

  ┌─────────────────────────────────────────────────┐
  │ 1. Public API                                   │  인증·과금·rate limit·라우팅
  ├─────────────────────────────────────────────────┤
  │ 2. Resource Management                          │  GPU/CPU/메모리/네트워킹 배분
  ├─────────────────────────────────────────────────┤
  │ 3. Model Selection & Orchestration              │  어떤 모델을 쓸지 결정
  ├─────────────────────────────────────────────────┤
  │ 4. Distributed Serving                          │  멀티GPU/노드 + 분산 캐싱
  ├─────────────────────────────────────────────────┤
  │ 5. Core Inference                               │  vLLM/Triton + CUDA 커널
  ├─────────────────────────────────────────────────┤
  │ 6. Model Optimization                           │  양자화·speculative decoding
  ├─────────────────────────────────────────────────┤
  │ 7. Model                                        │  실제 학습된 모델 공급·버전 관리
  └─────────────────────────────────────────────────┘

3장. 레이어 1 — Public API

세 줄 요약
1. 고객·개발자·내부 서비스가 만나는 외부 인터페이스.
2. 인증 → rate limit → tenant 식별 → 과금 → 라우팅 순으로 관문 역할.
3. 여기서 나온 tenant 값 하나에 이후 모든 레이어의 정책이 의존한다.

[그림 2-2] Public API 파이프라인

Internet
   ↓
Authentication  ← JWT 또는 API key
   ↓
Rate Limit
   ↓
Tenant 식별      ← ★ 이후 모든 정책의 기준점
   ↓
Billing
   ↓
Routing
   ↓
LLM
핵심 난제내용
High concurrency수백만 동시 연결 처리
Fair usage & monetizationquota 강제, 어뷰징 방지, 정확한 과금
Low-latency global access지역 라우팅·캐싱으로 최근접 리전 서빙
Security인증, tenant 격리, 네트워크 공격 방어

4장. 레이어 2 — Resource Management

세 줄 요약
1. CPU/GPU/메모리/디스크/네트워킹을 여러 리전에 걸쳐 배분한다.
2. 예산 검토와 비용 배분이 여기서 통합된다.
3. 질문은 단순하다 — 누구에게 GPU를 줄 것인가, 지금 GPU가 놀고 있지 않은가.

[그림 2-3] GPU 풀 관리 (조작 포인트: 이기종 GPU 비율을 바꿔보기)

GPU Cluster
├─ H100 × N
├─ H200 × N
├─ B200 × N
└─ L40S × N

질문:
- 누구에게 GPU를 줄 것인가?
- 몇 대가 필요한가?
- GPU가 놀고 있지는 않은가?
- 중요 고객 요청을 우선 처리할 것인가?
핵심 난제내용
Capacity planning수요 예측, 과다 프로비저닝 방지
GPU utilization데이터센터 전반의 이기종 GPU 풀 고가동률 유지
Customer prioritization중요 워크로드에 quota/예약을 강제, 저우선순위는 선점 가능하게

5장. 레이어 3 — Model Selection & Orchestration

세 줄 요약
1. 요청마다 어떤 모델을 쓸지 결정. 정확도·지연·비용의 균형점을 찾는 레이어.
2. 단일 모델 선택을 넘어 여러 모델을 동시에 조율한다 (speculative decoding, 모델 패밀리 간 라우팅).
3. "1+1?"에 거대한 reasoning model을 쓸 이유는 없다.

[그림 2-4] 라우팅 판단 (조작 포인트: 분류 임계값을 조정하면 비용/품질 곡선이 움직인다)

단순 질문 ──▶ Small Model     (싸고 빠름)
복잡한 추론 ──▶ Large Model    (비싸고 정확)

           Quality
             △
            ╱ ╲
           ╱   ╲
      Cost ─────  Latency      ← 셋 중 둘만 가져갈 수 있다
핵심 난제내용
Cost–quality trade-off모든 작업에 최대 모델이 필요하진 않음. 쿼리를 이해해서 triage해야 함
Load balancing모델 풀 전반에 트래픽 분산
Latency-sensitive use case더 작고 빠른 모델 또는 speculative decoding 적용

6장. 레이어 4 — Distributed Serving

세 줄 요약
1. 두 갈래 — 대형 모델을 위한 분산 호스팅, 중복 연산을 줄이는 분산 캐싱.
2. 모델이 GPU 한 장에 안 들어가면 쪼개야 한다.
3. cache-aware routing이 이 레이어의 숨은 핵심이다.

[그림 2-5] 모델이 GPU 하나에 안 들어갈 때

Model Weight = 160 GB
GPU VRAM     =  80 GB
                 ↓
        GPU 0 ─┬─ GPU 1        같은 노드 내 → Tensor Parallel (TP)
               │
        Node A ─── Node B      노드 간 → Pipeline Parallel (PP)

예: GPU 8장 노드 1대  → TP 8
    GPU 8장 노드 2대  → TP 8, PP 2

관리 대상: Multi-GPU, Multi-Node, KV Cache, Prompt Cache, Cache-aware Routing

핵심 난제내용
Hardware limitations모델 크기가 단일 GPU 메모리를 초과
Multi-GPU/node coordination요청 context를 효율적으로 공유하며 SLA 유지
Caching for efficiencyKV-cache 인식 라우팅으로 중복 추론 감소

7장. 레이어 5 — Core Inference

세 줄 요약
1. 모델 계산이 실제로 일어나는 곳.
2. 서빙 프레임워크 + 최적화 커널을 웹 엔드포인트로 노출한다.
3. 3장에서 상세히 다룬 영역이다.

[그림 2-6] Core Inference 스택

Application
     ↓
Orchestration
     ↓
vLLM / Triton / TensorRT-LLM / SGLang    ← 서빙 프레임워크
     ↓
FlashAttention / GEMM / PagedAttention   ← 최적화 커널
     ↓
CUDA Kernel
     ↓
GPU
핵심 난제내용
모델별 dependency와 framework가 제각각
model load/unload가 latency spike를 만든다cold start
hot model과 cold model의 트래픽 차이가 크다hot model scaling
모델 cache eviction 정책이 성능과 비용에 직결
보안·격리·observability가 단일 모델보다 복잡

8장. 레이어 6, 7 — Model Optimization / Model

세 줄 요약
1. 레이어 6은 재학습 없이 성능·효율을 높이는 기법을 적용한다.
2. 레이어 7은 학습된 모델을 서빙 시스템에 공급하고 분류·추적·버전 관리한다.
3. 새 모델과 기술이 계속 나와도, 이 계층 분리 패턴 자체는 유지된다.

레이어 6 — Model Optimization
양자화(6장), speculative decoding·LMCache(7장), 종단간 최적화(9장) 등이 여기에 해당합니다.

레이어 7 — Model

  • 모델을 내부 학습 파이프라인이나 외부 소스에서 운영 환경으로 이동
  • 기능(음성/추론/비디오)과 목적(샌드박스/실험/운영)에 따라 분류
  • 모델이 발전함에 따라 추적과 버전 관리

9장. 여기서 짚어둘 것 — 레이어를 나누는 대가

책이 명시적으로 다루지는 않지만, 실무에서 반드시 계산해야 하는 항목입니다.

레이어를 나눌 때마다 네트워크 홉이 하나씩 늘어납니다.

[그림 2-7] 레이어 분리의 비용 (조작 포인트: 홉 수를 늘려보며 latency budget 소진 확인)

Public API ──1ms──▶ Orchestration ──1ms──▶ Router ──1ms──▶ Serving ──1ms──▶ Engine
                                                              총 ~4ms 오버헤드

TTFT 목표가 200ms면 4ms는 2% — 무시 가능
TTFT 목표가 30ms면 4ms는 13% — 무시 불가

즉 계층형 아키텍처는 조직 확장성을 사기 위해 latency budget을 지불하는 거래입니다. 대규모 조직이 아니라면 레이어를 다 만들 필요가 없고, 만들지 않는 게 낫습니다. "레퍼런스 아키텍처"는 도달 목표지 시작점이 아닙니다.


CH4-3. 오픈소스 스택 — Kubernetes + Ray Serve + vLLM

1장. 왜 Kubernetes가 바닥인가

세 줄 요약
1. 이유는 두 가지 — 핵심 기능(배포/스케일링/관리 자동화)과 거대한 생태계.
2. Kubernetes 한 층이 레이어 1의 라우팅/네트워킹 부분과 레이어 2 전체를 실제 구현으로 채워준다.
3. 목표는 단일 정답 제시가 아니라, 직접 플랫폼을 만들 수 있는 출발점 제공이다.

Kubernetes + 생태계가 담당하는 것:

  • 리소스 관리
  • 네트워킹
  • 트래픽 라우팅 및 로드 밸런싱
  • 서비스 호스팅 및 스케일링
  • 서비스 메트릭 및 모니터링
[그림 3-1] 레이어 대응 관계

레이어 1 Public API      → FastAPI + Ingress(nginx) + JWT/API key
레이어 2 Resource Mgmt   → Kubernetes (HPA, node pool, scheduler)
레이어 3 Model Selection → 미들웨어 서비스 또는 API 안의 라우팅 로직
레이어 4 Distributed     → Ray Serve
레이어 5 Core Inference  → vLLM / Triton

2장. 레이어 1 구현 — Public API

세 줄 요약
1. 인증은 JWT와 API key 두 경로가 있지만, 둘 다 tenant 식별이라는 같은 목적지로 수렴한다.
2. rate limit이 두 곳에 이중으로 걸린다 — Ingress(거친 방어선)와 애플리케이션(정교한 quota).
3. HPA는 CPU 기준으로 파드를 3~15개로 자동 확장한다.

2-1. 엔드포인트와 인증 주입

@app.post("/v1/chat/completions")
async def chat(req: ChatReq, idp=Depends(require_auth)):
    await rate_limit(idp["tenant"])
    # tenant 정보로 모델 선택 / 트래픽 라우팅

FastAPI의 Depends()로 인증 로직을 선언적으로 주입해서, 라우트 핸들러는 비즈니스 로직에만 집중합니다.

2-2. 인증 — 두 경로, 하나의 목적지

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}
[그림 3-2] 인증 두 경로

JWT 경로:      Authorization: Bearer ...
                  → RSA 공개키(JWK)로 서명 검증 + audience/만료 확인
                  → 토큰 claim에서 바로 tenant 추출
                                                    ↘
                                                     tenant  ← 여기로 수렴
                                                    ↗
API key 경로:  key → Redis 조회(rds.hget) → tenant 역참조
  • 둘 다 없으면 401 (인증 정보 없음)
  • 있어도 tenant를 못 찾으면 403 (알 수 없는 tenant)

이후 단계(quota 강제, 라우팅, 모델 선택)가 전부 이 tenant 값 하나에 의존합니다.

2-3. HPA — 고동시성 대응

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
  scaleTargetRef: {kind: Deployment, name: enterprise-model-api}
  minReplicas: 3
  maxReplicas: 15
  metrics:
  - type: Resource
    resource: {name: cpu, target: {type: Utilization, averageUtilization: 70}}

여기서 짚어둘 것: 이 HPA는 API 서버용이라 CPU 기준이 맞습니다. 하지만 GPU 추론 replica에 이 방식을 그대로 쓰면 안 됩니다. GPU 워크로드에서 CPU 사용률은 부하를 반영하지 못하고, GPU는 CPU처럼 오버서브스크립션이 안 됩니다. 추론 replica는 큐 길이나 target_ongoing_requests 같은 요청 기반 지표로 스케일해야 합니다.

2-4. Ingress rate limiting

annotations:
  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건)까지 허용.

[그림 3-3] 이중 rate limit = defense in depth

Internet
   ↓
[Ingress limit-rps: 50]        ← 거친 방어선. 앱 도달 전에 DDoS성 트래픽을 값싸게 차단
   ↓
[app: rate_limit(tenant)]      ← 정교한 방어선. tenant별 계약(quota)을 정확히 강제
   ↓
LLM

3장. 레이어 3 구현 — Model Selection

세 줄 요약
1. 모델 선택 로직은 Public API 안, 별도 미들웨어, 정적 라우팅 설정 중 어디에나 둘 수 있다.
2. 이 예제는 출력 길이(1024 token) 로 speculative decoding 여부를 분기한다.
3. canary 판정이 tenant override보다 먼저 실행되는 순서에 의도가 있다.

3-1. 출력 길이로 전략 분기

ep = choose_endpoint(req.model, idp["tenant"])

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)

임계값 1024의 의미: speculative decoding은 두 모델을 동시에 돌리는 오버헤드가 있습니다. 생성 token이 많을수록 draft model의 속도 이득이 그 오버헤드를 상쇄하고도 남습니다. 짧은 응답에는 오히려 손해라서, 출력 길이로 분기하는 게 합리적인 휴리스틱입니다.

3-2. 라우팅 정책 4단계

def choose_endpoint(model: str, tenant: str):
    cfg = load_routes()
    route = cfg["models"].get(model) or cfg["aliases"].get(model)
    if not route: raise HTTPException(404, f"Unknown model {model}")

    if "canary" in route and random() < float(route["canary"]["weight"]):
        return route["canary"]["url"]                    # ← tenant보다 먼저!

    route_over = (route.get("tenants") or {}).get(tenant)
    if route_over: return route_over["url"]

    return route["url"]
[그림 3-4] 라우팅 우선순위 (조작 포인트: canary weight를 0 → 1로)

① 모델/alias 조회         없으면 404
        ↓
② canary 가중치 판정      random() < weight → 신규 버전으로
        ↓                 ★ 여기가 tenant override보다 먼저인 이유:
        ↓                   전체 트래픽에서 무작위 샘플링해야
        ↓                   특정 고객군에 편향되지 않은 대표성 확보
③ tenant override         전용 파인튜닝 모델/전용 용량
        ↓
④ 기본 라우트

4장. Ray Serve — 모델 호스팅

세 줄 요약
1. 3장에서 손으로 만든 것들을 Ray Serve는 데코레이터 한 줄로 대체한다.
2. 단일 모델은 @serve.deployment, 멀티 모델은 @serve.multiplexed.
3. 우리가 짠 ModelManager의 LRU 캐시가 프레임워크 기능으로 내장돼 있다.

4-1. 단일 모델 호스팅

@serve.deployment(
    name="qwen_vllm",
    num_replicas=3,
    ray_actor_options={"num_cpus": 0.5, "num_gpus": 1},
)
class QwenVLLM:
    def __init__(self):
        args = AsyncEngineArgs(
            model="Qwen3-Thinking-2507",
            tensor_parallel_size=1,
            max_num_batched_tokens=4096,
            gpu_memory_utilization=0.9,
        )
        self.engine = AsyncLLMEngine.from_engine_args(args)

    async def __call__(self, http_request):
        ...
[그림 3-5] Ray Serve 단일 모델 (조작 포인트: num_replicas를 1↔3으로)

HTTP Request
     ↓
Ray Serve
     ├─ Replica 1 → GPU 1
     ├─ Replica 2 → GPU 2
     └─ Replica 3 → GPU 3
          ↓
     vLLM AsyncLLMEngine
          ↓
         GPU

3장의 ModelWorker와 개념적으로 같은 패턴입니다 — 모델을 한 번 로드하고 요청을 처리. 다만 수동 배칭/큐잉을 직접 구현했던 부분을 AsyncLLMEngine에 통째로 위임합니다. 그리고 HPA YAML(minReplicas/maxReplicas/CPU 임계값)이 하던 일을 데코레이터 한 줄이 대신합니다.

4-2. 멀티 모델 호스팅 (multiplexing)

@serve.deployment(num_replicas=2, ray_actor_options={"num_cpus": 1})
class MultiModelService:
    async def __call__(self, request):
        model_id = serve.get_multiplexed_model_id()
        tokenizer, model = await self.load_model(model_id)
        ...

    @serve.multiplexed(max_num_models_per_replica=2)
    async def load_model(self, model_id: str):
        repo = MODEL_REGISTRY[model_id]
        ...

호출은 HTTP 헤더로:

curl -X POST "http://127.0.0.1:8000/SentimentService" \
  -H 'ray_serve_multiplexed_model_id: distilbert-sst2' \
  -d '{"text":"I absolutely loved this!"}'

4-3. 3장에서 직접 만든 것 ↔ 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() — 요청 시점 로드await self.load_model(model_id)
config/models.json (model_id → 모델명)MODEL_REGISTRY dict
server.pymodel_id 필드로 라우팅HTTP 헤더 ray_serve_multiplexed_model_id

핵심 차이: 단일 모델은 __init__에서 미리 로드하지만, 멀티 모델은 요청마다 다른 모델이 올 수 있어 실시간 로드를 시도합니다. 그 지연을 완화하는 게 @serve.multiplexed의 LRU 캐싱이고, max_num_models_per_replica를 넘으면 자동 축출합니다.


5장. Ray의 구조

세 줄 요약
1. Ray Cluster = Head Node 1개 + Worker Node N개.
2. Head Node만 클러스터 관리 싱글톤(Autoscaler, GCS, 드라이버)을 돌린다.
3. Ray Serve LLM은 그 위에 OpenAiIngress + LLMServer 두 컴포넌트를 얹는다.

5-1. Ray 생태계

컴포넌트역할
Ray Core기반 레이어. @ray.remote로 함수(Task)/클래스(Actor)를 분산 실행
Ray Data대규모 병렬 데이터 처리/ETL
Ray Train분산 학습 자동화
Ray Tune병렬 하이퍼파라미터 튜닝
Ray Serve모델을 HTTP 엔드포인트로 프로덕션 서빙
Ray RLlib강화학습

5-2. Ray Cluster

[그림 3-6] Ray Cluster 구조

┌──────────────── Head Node ────────────────┐
│  Worker Node와 동일한 프로세스             │
│  +  Autoscaler   (리소스 요청 감지, 노드 증감) │
│  +  GCS          (클러스터 메타데이터)      │
│  +  Ray Driver   (사용자 스크립트 실행)     │
└───────────────────┬───────────────────────┘
                    │
      ┌─────────────┼─────────────┐
      ▼             ▼             ▼
 Worker Node   Worker Node   Worker Node
 (Task/Actor 실행만, 클러스터 관리 기능 없음)

5-3. Ray Serve 개념 4개

개념
Deployment기본 단위. @serve.deployment로 정의, 여러 replica(각각 Ray Actor)로 확장
Application하나 이상의 Deployment로 구성된 업그레이드/롤백 단위
Ingress Deploymentserve.run()에 전달되는 최상위 Deployment. HTTP 진입점
DeploymentHandleDeployment 간 통신용 Python 네이티브 API

연결: 각 Deployment를 .bind()로 연결 → serve.run(ingress.bind(...))로 실행.

5-4. Ray Serve LLM

[그림 3-7] Ray Serve LLM 요청 흐름

Client
   ↓
OpenAiIngress          FastAPI 기반. OpenAI 호환 엔드포인트,
   ↓                    라우팅, 모델 multiplexing(LoRA 어댑터 포함)
DeploymentHandle (RPC)
   ↓
LLMServer Replica      ← 가능하면 같은 노드 우선 배치
   ↓                      (크로스노드 오버헤드 최소화)
vLLM 엔진 (GPU)
   ↓
Response

LLMServer의 3가지 운영 모드

  • 독립형(standalone) — replica마다 독립 처리
  • 배포 내 조율 — 데이터 병렬 attention (MoE expert 계층 조율)
  • 배포 간 조율 — prefill-decode 분리 (단계별 독립 스케일링, 다른 GPU 타입 배정 가능)

추가 기능

  • 커스텀 라우팅 — 캐시 지역성/세션 친화성 기반. 반복 프롬프트 워크로드에 유리
  • 오토스케일링 조율 — Ingress : LLMServer = 2 : 1 비율 권장, target_ongoing_requests로 균형 유지

6장. KubeRay — Kubernetes 위의 Ray

세 줄 요약
1. Ray head/worker를 Kubernetes pod으로 관리하는 공식 Operator.
2. CRD 4개 — RayCluster, RayJob, RayService, RayCronJob.
3. 프로덕션 모델 서빙에는 RayService가 정답이다. 고를 이유가 명확하다.

6-1. CRD 4개

CRD용도
RayClusterHead/Worker pod으로 구성된 Ray 클러스터 자체의 생명주기 관리
RayJobRayCluster를 만들어 단일 Job 실행 후 정리 (배치 작업용)
RayServiceRayCluster 위에 Ray Serve 앱을 얹어 운영. 무중단 업그레이드, 헬스체크 기반 HA
RayCronJobRayJob을 크론 스케줄로 반복 실행

6-2. RayCluster vs RayService

항목RayClusterRayService
역할 범위Ray 클러스터만클러스터 + Serve 앱까지
배포RayCluster CR단일 매니페스트에 rayClusterConfig + serveConfigV2
접속head pod에 exec, dashboard port-forward 후 ray job submitServe용 Service(-serve-svc)로 바로 HTTP
Serve 라이프사이클없음serve run으로 수동 배포·갱신자동. 설정만 바꾸면 무중단 in-place 업데이트
헬스체크/HARay 자체 기능만, k8s가 Serve 상태 모름/-/routes 기반 헬스체크로 k8s가 Serve 상태까지 파악
적합 용도배치 작업, 대화형 실험, Data/Train/Tune프로덕션 모델 서빙
GPU 워커 설정workerGroupSpec에 직접 기술동일

RayService를 고르는 이유 3가지
1. RayCluster만 쓰면 Serve 앱을 직접 serve run으로 올리고 pod이 죽으면 수동 재배포해야 함
2. RayService는 무중단 업데이트/헬스체크를 컨트롤러가 대신 — 모델 교체나 replica 변경이 kubectl apply 한 번
3. GPU 워커 스펙 작성 난이도가 동일하므로 RayCluster를 고를 이유가 없음


7장. 실습 — RayService로 vLLM 서빙

세 줄 요약
1. Qwen2.5-7B-Instruct-AWQ를 GPU 1장에 올려 OpenAI 호환 엔드포인트로 노출.
2. 부하 테스트 결과 p50~p99 편차가 거의 없었다 — 하지만 그 이유를 오해하면 안 된다.
3. GPU가 병목으로 예측 가능하게 동작한다는 것까지가 이 테스트가 증명한 범위다.

7-1. 설치

helm repo add kuberay https://ray-project.github.io/kuberay-helm/
helm repo update kuberay
kubectl create namespace kuberay-system
helm install kuberay-operator kuberay/kuberay-operator --version 1.6.0 -n kuberay-system

CRD는 helm install에 포함되어 자동 설치됩니다. 별도 kubectl apply -f crds/ 불필요.

kubectl get crd | grep ray.io
# rayclusters.ray.io / raycronjobs.ray.io / rayjobs.ray.io / rayservices.ray.io

7-2. 공식 예제 대비 조정한 부분

항목공식 예제조정값이유
모델Qwen2.5-7B-Instruct-AWQ + quantization: awq가중치 4~5GB로 축소
gpu_memory_utilization0.85AWQ라 가중치가 작아 KV cache에 넉넉히 배분
max_model_len10244096VRAM 여유 확보로 실용적 길이 상향
GPU 수41물리 GPU 1장
max_replicas41동일
worker CPU/메모리32 / 32Gi8 / 24Gi모니터링 스택과 공존

핵심 설정만:

serveConfigV2: |
  applications:
    - name: llms
      import_path: ray.serve.llm:build_openai_app
      args:
        llm_configs:
          - model_loading_config:
              model_id: qwen2.5-7b-instruct-awq
              model_source: Qwen/Qwen2.5-7B-Instruct-AWQ
            engine_kwargs:
              quantization: awq
              max_model_len: 4096
              gpu_memory_utilization: 0.85
            deployment_config:
              autoscaling_config:
                min_replicas: 1
                max_replicas: 1
                target_ongoing_requests: 16
              max_ongoing_requests: 32

7-3. 검증

kubectl get rayservice -n kuberay
# NAME           SERVICE STATUS   NUM SERVE ENDPOINTS
# vllm-service   Running          2

kubectl describe로 확인할 것:

  • LLMServer:qwen2_5-7b-instruct-awqHEALTHY
  • OpenAiIngressHEALTHY
  • Application Status → RUNNING

로그에서 Using executor class: RayDistributedExecutorStarted vLLM engine. 확인.

curl -s http://<node-ip>:30005/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"qwen2.5-7b-instruct-awq","messages":[{"role":"user","content":"안녕"}]}'

7-4. 부하 테스트 결과

파라미터: 동시 8, 5분, max_tokens=64, 고정 프롬프트, closed-loop

지표
총 요청1,177건
성공/실패1,177 / 0 (100%)
평균 지연시간2.05 s
p50 / p95 / p992.02 s / 2.10 s / 2.10 s
처리량3.92 req/s
GPU (부하 중)사용률 99%, 201.5 W, 66 °C
GPU (종료 후)즉시 idle (0%, 52 W)

산술 검증: 동시성 8 ÷ 평균 2.05s = 3.90 req/s ≈ 측정값 3.92. 일치합니다.

7-5. ⚠️ 이 결과를 오해하지 않기

p50~p99 편차가 0.08초밖에 안 되는 걸 보고 "시스템이 매우 안정적이다"로 읽으면 절반만 맞습니다.

[그림 3-8] closed-loop vs open-loop (조작 포인트: 부하 방식을 바꿔보기)

closed-loop (이 테스트):
    워커 8개가 각자 "응답 오면 즉시 다음 요청"
    → 동시성이 절대 8을 넘지 못함
    → 시스템이 느려지면 요청 발사 속도도 같이 느려짐
    → 큐가 쌓일 수가 없음 ⇒ 큐잉 지연을 측정할 수 없음

open-loop (포화점 측정용):
    응답과 무관하게 "초당 N건" 고정 발사
    → 처리 속도 < 발사 속도면 큐가 무한히 쌓임
    → 여기서 비로소 p99가 폭발하고 포화점이 보인다

즉 이 테스트가 증명한 것은 "동시성 8에서는 여유롭다"이지, "시스템의 한계가 어디인지"가 아닙니다. max_ongoing_requests: 32 설정이 실제로 어떻게 동작하는지는 동시성을 32 이상으로 올리거나 open-loop로 바꿔야 보입니다.

이건 5편의 "성능 측정 모범 사례"와 직결되는 지점이라 거기서 다시 다룹니다.

7-6. 모니터링 연동

구분내용다운타임
A. Prometheus 스크레이프PodMonitor 2개(head/worker) + operator ServiceMonitor없음
B. Grafana 대시보드kuberay 공식 5종을 ConfigMap으로 자동 로드없음
C. Grafana embeddingallow_embedding + 익명 Viewer 허용Grafana pod 재시작(수 초)
D. Ray Dashboard 연동head에 RAY_GRAFANA_HOST 등 env 추가vLLM 서빙 수 분 중단

D 단계는 head pod 재생성을 유발하므로 서빙이 끊깁니다. 무중단이 필요하면 처음 배포할 때 env를 함께 넣어야 합니다.

보안 주의: C의 익명 Viewer 허용은 해당 네트워크에 접근 가능한 누구나 로그인 없이 Grafana를 조회할 수 있게 만드는 정책 변화입니다. 사설망이라도 명시적으로 판단하고 넘어가야 합니다.


8장. 정리 — 나만의 서빙 스택은 어렵지 않다

역할도구
인프라 관리Kubernetes
모델 호스팅Ray Serve 또는 Triton Inference Server
분산 LLM 서빙 + 최적화vLLM
나머지가벼운 커스텀 미들웨어

여기 나온 라이브러리들은 전부 수년간 프로덕션에서 검증됐고, 참고할 구현체도 많습니다. 움직이는 부품이 많아 보이지만 각각은 이미 성숙한 도구입니다.


CH4-4. 클라우드 벤더 6단계 — 그리고 Build vs Buy

1장. 스펙트럼 전체 보기

세 줄 요약
1. AWS SageMaker를 예로 6단계. 완전 관리형 → 완전 직접 구축 순.
2. 목적은 SageMaker 사용법 튜토리얼이 아니라 벤더가 서빙 옵션을 설계하는 논리를 이해하는 것.
3. 이 논리를 알면 Vertex AI, Azure ML의 유사 스펙트럼도 스스로 판단할 수 있다.

단계방식자유도운영 부담
1Bedrock낮음매우 낮음
2SageMaker JumpStart조금 높음낮음
3Bring Your Own Model중간중간
4Bring Your Own Code높음높음
5Bring Your Own Serving Image매우 높음매우 높음
6Build Your Own Infrastructure최고최고
[그림 4-1] 자동차 비유 (조작 포인트: 아래로 갈수록 내가 직접 만지는 부품이 늘어난다)

1. Bedrock          🚕 택시        목적지만 말한다
2. JumpStart        🚗 렌터카      차종은 고르지만 준비된 것 중에서
3. BYO Model        🔧 엔진만 내 것, 차량 플랫폼은 AWS
4. BYO Code         ⚙️ 컨테이너는 AWS, 추론 코드는 내가
5. BYO Image        🏭 Docker 이미지까지 직접
6. BYO Infra        🏗️ EKS부터 전부 직접

2장. Option 1 — Bedrock (완전 관리형)

세 줄 요약
1. 서버가 없다. 요청/token 단위 과금.
2. 3단계면 끝 — API key 생성, 모델 선택, 클라이언트 초기화.
3. 대신 못 하는 게 3가지 명확하다.

os.environ["AWS_BEARER_TOKEN_BEDROCK"] = "..."
client = boto3.client(service_name="bedrock-runtime", region_name="us-west-2")

response = client.converse(
    modelId="amazon.nova-lite-v1:0",
    messages=[{"role": "user", "content": [{"text": "Hello!"}]}],
)

AWS 인프라가 눈에 안 보이게 처리하는 것: GPU 인스턴스 프로비저닝 → 모델 가중치 로딩 → 추론 실행 → 결과 반환

못 하는 것 3가지
1. 인스턴스 타입 선택 불가
2. 컨테이너/코드 커스터마이징 불가
3. 새 모델을 Bedrock에 배포하는 것 자체가 불가 — 자체 독점 모델이나 남들이 안 올려둔 파인튜닝 버전은 아예 못 씀

적합부적합
Bedrock챗봇/생성기를 인프라 관리 없이 빠르게 프로토타입, 사전학습 모델로 충분자체 커스텀 모델 배포, 정교한 추론 로직 필요

3장. Option 2 — JumpStart (원클릭 배포)

세 줄 요약
1. Bedrock과 결정적 차이 — 내 AWS 계정에 실제 엔드포인트가 뜬다.
2. 그래서 과금이 token당이 아니라 인스턴스 시간당으로 바뀐다.
3. 인스턴스는 내가 고르지만, 그 안에서 뭘 하는지는 여전히 AWS가 정한 대로다.

BedrockJumpStart
모델이 도는 곳AWS가 숨긴 인프라내 SageMaker 계정
과금요청/token당 (서버 없음)시간당 인스턴스 요금
인프라 관리거의 없음인스턴스 타입 직접 선택
model = JumpStartModel(
    model_id="huggingface-llm-mistral-7b-instruct",
    instance_type="ml.g5.2xlarge",
    env={"SAGEMAKER_MODEL_SERVER_WORKERS": "1", "SAGEMAKER_CONTAINER_LOG_LEVEL": "20"},
)
predictor = model.deploy(initial_instance_count=1, endpoint_name="my-mistral-endpoint")
response = predictor.predict({"inputs": "Hello, world!"})

deploy()라는 단계가 새로 생겼습니다. 인프라 설정을 지정하고, 그 결과 나온 predictor로 추론하는 2단계 구조입니다.

한계 6가지

  • 모델마다 파인튜닝/평가 지원 여부가 다름
  • 인스턴스 선택이 모델별로 고정 (아무거나 못 씀)
  • 전처리/배칭 등 기본 추론 동작 커스터마이징 불가
  • 요청 payload 스키마/content type 조정 불가
  • CUDA/PyTorch 버전 선택 불가
  • context 길이·배치 크기를 JumpStart가 이미 정해버려 최적화 적용이 어려움

4장. Option 3 — Bring Your Own Model (DLC)

세 줄 요약
1. "bring-your-own-model"이지 "bring-your-own-code"는 아니다. 서빙 코드는 안 짠다.
2. JumpStart는 컨테이너를 자동 선택해줬지만, DLC는 어떤 컨테이너를 쓸지 직접 지정한다.
3. 모든 DLC는 포트 8080에서 /invocations(POST)와 /ping(GET)을 구현해야 한다.

4-1. 예시 1 — PyTorch + TorchServe

baseimage = sagemaker.image_uris.retrieve(
    framework="pytorch", py_version="py310", image_scope="inference",
    version="2.0.1", instance_type="ml.g4dn.16xlarge",
)
model = Model(model_data=f"{output_path}/mnist.tar.gz", image_uri=baseimage, name="mnist")
predictor = model.deploy(instance_type="ml.g4dn.16xlarge", initial_instance_count=1, ...)

JumpStart에서 자동이던 이미지 검색 단계가 여기선 명시적입니다.

4-2. 예시 2 — LLM을 LMI + vLLM으로

model = DJLModel(
  model_id="meta-llama/Meta-Llama-3.1-8B-Instruct",
  env={
    "OPTION_TENSOR_PARALLEL_DEGREE": "4",
    "OPTION_SERVING_LOADER": "vllm",
    "OPTION_MAX_ROLLING_BATCH_SIZE": "128",
  }
)
predictor = model.deploy(instance_type="ml.g5.12xlarge", initial_instance_count=1, ...)
[그림 4-2] 3장에서 직접 만진 것 ↔ LMI 설정 대응

OPTION_SERVING_LOADER=vllm          → /generate_vllm 엔드포인트로 통합했던 그 vLLM
OPTION_MAX_ROLLING_BATCH_SIZE=128   → 로그로 관찰했던 continuous batching
OPTION_TENSOR_PARALLEL_DEGREE=4     → tensor_parallel_size (우리는 1로 GPU 1장)

AWS의 관리형 LMI 컨테이너 뒤에서 실제로 도는 엔진이 우리가 손으로 통합해본 것과 똑같은 vLLM입니다. 다르는 건 인프라 프로비저닝을 AWS가 해준다는 것뿐입니다.

4-3. 한계

한계내용
컨테이너 내부 통제 불가프레임워크/드라이버/CUDA/Python/OS가 전부 이미지 태그에 고정 (예: TF 2.19 + CUDA 12.2 + Ubuntu 22.04)
고정 포트/HTTP 계약8080 포트, /invocations(POST) + /ping(GET), 기본 타임아웃(예: 60초)
입출력 포맷 고정커스텀 전/후처리가 필요하면 Option 4로

이 HTTP 계약은 사실상 SageMaker가 강제하는 미니 Public API 레이어입니다. 2편의 레이어 1이 벤더 쪽에서는 이런 형태로 나타납니다.

4-4. 실습 — SageMaker 없이 DLC만 로컬에서

SageMaker 서비스를 안 켜고 ECR에서 이미지만 받아 로컬 GPU에서 재현할 수 있습니다.

항목
DLC 이미지pytorch-inference:2.6.0-gpu-py312-cu124-ubuntu22.04-sagemaker-v1.84
TorchServe0.12.0 (컨테이너 내장, 토큰 인증 기본 활성화)
호스트 GPU 드라이버595.84 (CUDA 13.2) → 이미지 CUDA 12.4와 호환
테스트 모델ResNet18 (TorchScript 변환 후 .mar 패키징)
docker pull 763104351884.dkr.ecr.us-west-2.amazonaws.com/pytorch-inference:2.6.0-gpu-py312-cu124-ubuntu22.04-sagemaker-v1.84

docker run -d --name sm-pytorch-local --gpus=1 -p 8080:8080 -p 8081:8081 <이미지>

curl -X POST http://localhost:8080/predictions/resnet18 -T cat1.jpg
# {"282": 0.318, "281": 0.287, "285": 0.171, ...}
# TIGER CAT / TABBY / EGYPTIAN CAT 상위 3개에 87%

필요한 것: AWS 계정(ECR pull 인증용, SageMaker 서비스는 불필요) + Docker + NVIDIA Container Toolkit

주의: CUDA/드라이버 버전이 이미지 태그에 고정돼 있어서, 로컬 GPU 드라이버와 이미지가 요구하는 CUDA가 안 맞으면 실행이 안 됩니다. aws ecr list-images로 실제 존재하는 태그를 먼저 조회하고 호환되는 걸 고르는 게 안전합니다.

이 실습이 확인한 것: model.deploy()가 클라우드에서 하던 일(포트 매핑, 헬스체크 /ping, 모델 로드)을 로컬에서 docker run으로 그대로 흉내낼 수 있다는 것. 즉 SageMaker의 마법은 대부분 오케스트레이션이지 컨테이너 자체가 아닙니다.


5장. Option 4 — Bring Your Own Code (Script Mode)

세 줄 요약
1. 컨테이너는 AWS 것을 쓰되, 모델 로딩과 추론 로직만 직접 짜서 끼워넣는다.
2. handle() 하나가 진입점. 3장의 ModelWorker 구조와 사실상 동일하다.
3. 대가는 명확 — 이제 코드를 직접 쓰고 유지보수해야 한다. Option 1~3은 코드가 필요 없었다.

5-1. 설정 + 코드

# serving.properties
engine=Python
option.tensor_parallel_degree=2
option.rolling_batch=vllm
option.s3url=s3://.../my-own-llm
# model.py — 3개 함수 구조
def initialize(properties):        # 모델을 GPU에 로드
    model = AutoModel.from_pretrained(model_id)
    model.to(device); model.eval()

def run_inference(input_texts, model, tokenizer):
    max_batch_size = 128
    for i in range(0, len(input_texts), max_batch_size):     # 수동 배칭 루프
        batch_dict = tokenizer(input_texts[i:i+max_batch_size],
                               max_length=512, padding=True, truncation=True,
                               return_tensors='pt').to("cuda")
        with torch.no_grad():
            outputs = model(**batch_dict)
    ...

def handle(inputs: Input) -> None:  # ★ 진입점
    if not model:
        model, tokenizer = initialize(inputs.get_properties())
    if inputs.is_empty():
        # Model server makes an empty call to warmup the model on startup
        return None
    data = inputs.get_as_json()
    res = run_inference(data["inputs"], model, tokenizer)
    return Output().add_as_json(res)

5-2. 3장 코드와의 대응

3장 single_model_llm_servingOption 4 model.py
ModelWorker.__init__self.model.to(device)initialize(properties)
generate_forward_batch()의 수동 배칭 루프run_inference()max_batch_size=128 루프
server.py/predict 핸들러handle() (FastAPI 데코레이터 대신 단일 함수 시그니처)

주목할 프로덕션 패턴 하나:

if inputs.is_empty():
    # 모델 서버가 부팅 시 빈 입력으로 한 번 호출해 워밍업
    return None

2장에서 개념으로만 봤던 cold start / 워밍업 전략이 코드 레벨에서 이렇게 구현됩니다.

5-3. 한계

  • 여전히 컨테이너에 구워진 프레임워크·Python/OS/CUDA·서빙 라이브러리에 종속
  • option.rolling_batch=vllm으로 vLLM을 켤 수는 있지만, 그 vLLM의 정확한 버전은 컨테이너가 정한 대로. 원하는 버전을 pip install할 자유는 없음
  • HTTP 인터페이스/요청 스키마는 여전히 못 바꿈 (8080, /invocations, /ping 계약 그대로)

6장. Option 5 — Bring Your Own Serving Image

세 줄 요약
1. 컨테이너 이미지를 통째로 직접 가져온다. 내부는 100% 사용자 책임.
2. SageMaker는 컨테이너를 "8080 포트에서 /ping/invocations를 지키는 블랙박스"로만 취급한다.
3. 분산 서빙(분산 KV 캐싱, 프롬프트 캐싱, 인스턴스 간 요청 라우팅)이 필요하면 이걸로도 부족하다.

할 수 있는 것

  • 어떤 언어든, 어떤 프레임워크든 (SageMaker 미지원 포함)
  • 커스텀 추론 코드, 시스템 의존성, 네트워크 설정까지 자유

책임지는 것

  • 컨테이너 빌드·유지보수·업데이트
  • SageMaker의 헬스체크/추론 엔드포인트 요구사항 준수
  • Dockerfile 작성, ECR 푸시, 테스트/디버깅

언제

  • 모델이 SageMaker 미지원 스택/서빙 프레임워크에 의존하거나, 최신 버전이 아직 없거나, 대규모 커스터마이징 필요
  • 시스템 레벨 최적화, 특정 리눅스 배포판 등 모델 코드를 넘는 통제가 필요
  • 한 컨테이너에 여러 프로세스를 묶고 싶을 때 (예: 전용 메트릭 수집 사이드카를 모델과 나란히)

7장. Option 6 — Build Your Own Infrastructure

세 줄 요약
1. Option 1~5는 전부 SageMaker의 엔드포인트 관리를 어느 정도 계속 쓴다. Option 6는 그것마저 벗어난다.
2. 7개 레이어 전체를 사실상 직접 소유하게 된다.
3. 그래도 맨땅은 아니다 — AWS 관리형 프리미티브는 계속 활용한다.

[그림 4-3] Option 5와 6의 경계

Option 5: [내 Docker 이미지] ← SageMaker가 배포·스케일링·엔드포인트 URL 관리
Option 6: [내 Docker 이미지] ← 내가 EKS 위에서 배포·스케일링·라우팅까지 전부

직접 조립하는 것: EKS + GPU Nodes + vLLM/Triton/Ray Serve/KServe + Karpenter + Prometheus + Gateway
떠안는 것: 트래픽 관리, 오토스케일링, 보안, 관측성, 비용 통제

계속 쓰는 AWS 프리미티브: EKS, ALB/NLB, ECR, S3, EFS/EBS, IAM/IRSA, Karpenter/Cluster Autoscaler, CloudWatch
Kubernetes 애드온: Prometheus/Grafana, OpenTelemetry, Fluent Bit, CNI/NetworkPolicy, Gatekeeper/Kyverno, Argo CD/Rollouts

언제 쓰나 (4가지 시나리오)

시나리오내용
서빙 런타임 완전 통제커널, 배칭, 토크나이제이션, 커스텀 사이드카, 비표준 API(gRPC, SSE) — 고정된 /invocations 계약으로는 안 되는 것들
공격적 비용/성능 튜닝스팟 GPU, GPU 공유(MIG/MPS), 모델 빈패킹, TPS 기반 커스텀 오토스케일링
엄격한 컴플라이언스프라이빗 클러스터, VPC 전용 egress, tenant별 격리, 커스텀 감사 추적
최신 하드웨어/특수 최적화speculative decoding, KV cache 샤딩, 고급 라우팅 — 매니지드에 아직 없는 것들

8장. Build or Buy — 이분법이 아니라 스펙트럼

세 줄 요약
1. 대부분의 팀은 중간에 있다 — 벤더 플랫폼 + 커스텀 핸들러/오토스케일링 신호/라우팅/비용 제어.
2. 직접 만드는 법을 알면 벤더를 안 쓰더라도 이득이 4가지 있다.
3. 성공적인 전략은 유연하다. 스펙트럼 상의 위치는 계속 바뀔 것을 예상해야 한다.

8-1. 직접 짓지 않더라도 아는 것이 도움이 되는 이유 4가지

① 벤더 기능 해독
OPTION_SERVING_LOADER=vllm, OPTION_MAX_ROLLING_BATCH_SIZE를 보고 "3장에서 만진 텐서 병렬화·continuous batching이구나"라고 즉시 알아보는 능력. @serve.multiplexed를 보고 "손으로 짠 ModelManager의 LRU 캐시네"라고 대응시키는 능력.

② 트레이드오프 정량화
Option 3 vs Option 4 같은 선택지 사이의 손익을 실제로 계산할 수 있는 능력.

③ 커스터마이징 (80/20 원칙)
벤더 기본값을 80% 그대로 쓰고, 통제가 필요한 20%만 외과수술 하듯 교체. DLC 로컬 실습이 정확히 이 패턴이었습니다 — TorchServe 엔진, 8080 계약, 기본 핸들러는 그대로 두고 딱 두 곳(커스텀 .mar 주입, config.properties 토큰 인증)만 수술.

④ 디버깅
벤더 솔루션은 블랙박스처럼 동작하고, 내부 로직 문서가 부족하며, 로깅도 다 안 잡힙니다.

8-2. 선택 전략 4가지

상황선택
SLO 충족 + 비용 허용 + 기능 속도가 최우선벤더 관리형 유지
일부 엔드포인트에만 특별한 배칭/라우팅/튜닝/tenant 격리 필요하이브리드
하드웨어·런타임·네트워킹 직접 통제 필요, 또는 비용이 높아 세밀 튜닝 필요BYO
볼륨이 낮은 수준에서 안정되거나, 시스템 복잡성이 성과를 못 냄벤더 관리형으로 복귀

마지막 항목이 중요합니다. 되돌아가는 것도 전략입니다.

8-3. 손익분기점 계산 — 그리고 그 계산의 함정

책의 예시:

Bedrock:      입력 100만 token당 $0.10
전용 인스턴스: 시간당 $1.172

$1.172/hr ÷ $0.10/1M token = 11.72M token/hr
11,720,000 ÷ 3,600 = 약 3,255 token/s

→ 초당 약 3,255 token 이상을 꾸준히 처리하면 직접 호스팅이 저렴

여기서 반드시 짚어야 할 것: 이 3,255라는 숫자를 그대로 의사결정에 쓰면 위험합니다. 세 가지 보정이 필요합니다.

보정 ①: 가동률(utilization)
인스턴스는 24시간 과금되지만 트래픽은 주야 편차가 있습니다. 평균 가동률이 30%라면 실질 손익분기점은 약 3배 높아집니다. "피크 시점 3,255 token/s"가 아니라 "24시간 평균 3,255 token/s"여야 합니다.

보정 ②: 출력 token 가격
이 계산은 입력 token 단가만 씁니다. 출력 token은 보통 입력의 3~5배 단가입니다. 출력이 많은 워크로드(생성 위주)라면 실질 손익분기점은 훨씬 낮아집니다 — 즉 직접 호스팅이 더 빨리 유리해집니다.

보정 ③: 운영 인건비
전용 인스턴스에는 시간당 $1.172 외에 온콜, 업그레이드, 장애 대응 비용이 붙습니다. 이건 계산식에 안 들어가지만 실제로는 가장 큰 항목일 수 있습니다.

[그림 4-4] 보정 방향 (조작 포인트: 가동률과 출력 비중을 바꿔보기)

기본 계산              3,255 token/s
  × 가동률 30% 보정  → 약 10,850 token/s  (직접 호스팅이 불리해짐)
  ÷ 출력 비중 보정   → 낮아짐            (직접 호스팅이 유리해짐)
  + 인건비           → 더 높아짐          (직접 호스팅이 불리해짐)

세 보정이 서로 반대 방향으로 작용하므로, 자기 워크로드의 실제 숫자를 넣어봐야만 답이 나옵니다. 책의 숫자는 계산 방법의 예시지 결론이 아닙니다.

8-4. 실제 여정 예시 (Qwen3)

단계옵션과금
초기(검증)Option 1 (Bedrock)입력 100만 token당 $0.10
사용량 증가Option 2/3 (JumpStart/DLC)인스턴스 시간당 $1.172
처리량↑·지연↓ 요구 심화Option 4/5/6커스터마이징 수준에 따라

CH4-5. 성능 지표 — 무엇을 재야 개선할 수 있는가

1장. 왜 지표가 부수적이지 않은가

세 줄 요약
1. 아키텍처의 우아함만으로는 부족하다. 성능이 실현 가능성·사용자 경험·비용을 결정한다.
2. 에이전트는 체인에서 지연을 증폭시키고, 엔터프라이즈는 확장성과 비용을 저울질하고, build vs cloud는 결국 "예산 내 SLO 충족 가능한가"로 귀결된다.
3. 그래서 지표는 부수적인 게 아니라 방향을 제시하는 원칙이다.

핵심 지표는 두 축입니다.

  • Latency (지연 시간) — 처리 및 응답 생성에 필요한 시간
  • Throughput (처리량) — 단위 시간당 처리되는 요청 수 또는 token 수

2장. Latency 지표 3개

세 줄 요약
1. TTFT는 prefill 단계에, ITL은 decode 단계에 대응한다.
2. E2E는 그 둘의 합에 시스템 오버헤드까지 얹은 것.
3. 어느 지표가 중요한지는 유스케이스가 정한다. 하나만 보면 반드시 틀린다.

2-1. 정의

지표정의대응 단계
E2E latency요청 수신부터 전체 응답 완료까지. 큐 대기, 네트워크, 라우팅, 스케일링 오버헤드 등 시스템 요인 포함 가능전체
TTFT요청 수신부터 첫 token 발행까지. 사용자가 체감하는 "응답 속도"prefill
ITL / TPOT첫 token 이후 각 token 생성 간격. 보통 비교적 일정decode

2-2. 그림으로

[그림 5-1] E2E 안의 TTFT와 ITL (조작 포인트: 입력 길이와 출력 길이를 따로 늘려보기)

요청 도착
   │
   ├─ tokenize (입력)
   ├─ ███████████ prefill           ┐
   ├─ █ decode (첫 token)           ├─ TTFT
   ├─ detokenize (첫 token)         ┘
   │
   ├─ █ decode → detokenize   ┐
   ├─ █ decode → detokenize   ├─ ITL × (N-1)
   ├─ █ decode → detokenize   │
   ├─ ...                     ┘
   ▼
전체 응답 완료

├───────────────── E2E latency ─────────────────┤

읽는 법:
  입력을 늘리면  → prefill 구간이 길어짐 → TTFT ↑ (ITL은 거의 그대로)
  출력을 늘리면  → decode 구간이 길어짐 → E2E ↑ (TTFT는 그대로)

2-3. 공식

가정: ITL이 일정, 총 생성 token N, 토크나이즈/디토크나이즈 시간은 무시(실제로도 추론 대비 미미).

E2E latency = TTFT + ITL × (N − 1)

TTFT = 입력 토큰화 + prefill + 첫 token decode + 첫 token detokenize
     ≈ prefill + 첫 token decode

ITL  = 새 token 1개 decode + detokenize
     ≈ 새 token 1개 decode

2-4. 유스케이스별 우선순위

유스케이스최우선 지표이유
에이전틱 워크플로우E2E다운스트림이 전체 출력에 의존. 중간에 첫 token만 빨리 나오는 건 무의미
챗봇 (스트리밍)TTFT응답성 그 자체
출력이 매우 긴 작업ITLITL이 높으면 "느릿느릿하다"고 체감

에이전틱 케이스를 구체적으로: 1편의 Knowledge Agent가 정확히 이 경우였습니다.
generate_analysisgenerate_summary완성된 전체 출력을 context로 받아야 시작할 수 있었습니다.
스트리밍이 아무 의미가 없고, 각 단계의 E2E가 다음 단계를 블로킹합니다.

중요: 모든 최적화가 세 지표를 동시에 개선하지 않습니다. 타협이 필요하고, 그래서 어떤 기법을 고를지는 유스케이스가 결정합니다.


3장. Throughput 지표 2개

세 줄 요약
1. RPS/RPM은 요청 패턴에 민감해서, 트래픽 패턴이 다른 두 워크로드를 직접 비교하면 무의미하다.
2. TPS는 출력 token만 센다. 입력이나 입출력 합산이 아니다.
3. TPS는 인위적으로 부풀릴 수 있다. 벤더 벤치마크를 볼 때 측정 조건을 의심해야 한다.

3-1. 정의

지표
RPS / RPM일정 시간 동안 처리 가능한 요청 수
TPS초당 생성되는 출력 token 수. 입력 token이나 입출력 합산 아님

3-2. 각각의 약점

RPS/RPM의 약점 — 요청 패턴에 민감

긴 입력과 긴 출력이 성능에 부정적 영향을 주는데, 요청 단위로만 세면 이 차이가 안 보입니다. 그래서 RPS/RPM은 입력 길이, 출력 길이, 동시 사용자 수에 크게 좌우됩니다.

[그림 5-2] 같은 RPS가 전혀 다른 부하일 수 있다

워크로드 A:  100 RPS × (입력 50 token, 출력 20 token)  = 초당 2,000 출력 token
워크로드 B:  100 RPS × (입력 4000 token, 출력 500 token) = 초당 50,000 출력 token
                                                            + prefill 부하 80배

→ 같은 "100 RPS"인데 GPU 부하는 자릿수가 다르다

서로 다른 트래픽 패턴을 가진 두 워크로드 간 RPS 비교는 공정하지 않습니다.

TPS의 약점 — 부풀릴 수 있다

TPS는 LLM 분야 표준 지표이고 token당 비용 계산에도 쓰이지만, 인위적으로 높일 방법이 있습니다.

조작 방법원리
입력 길이를 줄인다prefill이 짧아져 TTFT가 크게 감소 → 요청당 작업 부하 감소 → TPS가 실제보다 높게 보임
배치를 최적 구성한다배치 크기를 키우거나 입출력 길이를 균일하게 맞추면 GPU 유휴 시간이 줄어 TPS 개선

3-3. 실전 시사점

벤더가 발표하는 TPS 숫자를 볼 때 "어떤 조건에서 측정했는가"를 따져야 합니다. 입력을 짧게 잘랐는지, 배치를 인위적으로 최적 구성했는지에 따라 같은 모델도 TPS가 크게 달라 보입니다.

4편의 "직접 만드는 법을 알면 좋은 이유 ① 벤더 기능 해독"이 바로 여기서 실전에 쓰입니다. 밑바닥을 알아야 화려한 숫자 뒤의 측정 조건을 의심하고 검증할 수 있습니다.


4장. 측정 모범 사례 9가지

세 줄 요약
1. 목표를 먼저 정하고, E2E를 TTFT/ITL로 분해하고, 실제 트래픽을 그대로 흉내낸다.
2. 한 번에 노브 하나만 바꾸고, 하드웨어 활용률을 같이 본다.
3. 배포 후에도 계속 모니터링하고 주기적으로 재실행한다.

#원칙요점
1지연 vs 처리량 트레이드오프 파악오프라인/배치는 처리량, 챗봇/실시간 에이전트는 지연이 핵심
2"충분히 좋은" 목표 설정1초→0.5초가 체감상 의미 없다면 그 노력을 처리량/비용으로 돌린다
3E2E를 TTFT/ITL로 분해출력 길이가 제한된 시스템이면 TTFT가 ITL보다 중요할 수 있다
4실제 트래픽 패턴 시뮬레이션긴 프롬프트+짧은 답변과 짧은 프롬프트+긴 답변은 완전히 다르다. 트래픽은 절대 균일하지 않다
5한 번에 노브 하나만여러 개를 동시에 바꾸면 효과를 분리할 수 없다
6하드웨어 활용률 모니터링병목이 모델 때문인지 하드웨어 한계 때문인지 구분
7지표를 부풀리지 말 것3장의 TPS 조작 가능성
8프로덕션에서 지속 모니터링배포 후 사용자 행동 변화로 인한 급증/실패까지
9테스트 스위트 주기적 재실행회귀 방지, 스케일링 테스트, A/B 테스트

4-1. 원칙 6의 실제 사례

3장 실습에서 확인했던 것:

모델은 0.24 GiB인데 vLLM의 gpu_memory_utilization=0.9 기본값 때문에 14.7 GB를 점유한다.

nvidia-smi를 안 봤으면 "메모리가 왜 이렇게 많이 잡히지"를 영원히 몰랐을 상황입니다. 지표 없이 튜닝하면 추측만 하게 됩니다.

4-2. 원칙 4를 제대로 하려면 — closed-loop의 함정

3편의 RayService 부하 테스트를 다시 봅시다.

지표
p50 / p95 / p992.02 s / 2.10 s / 2.10 s
처리량3.92 req/s
실패0건

편차가 0.08초. "매우 안정적"으로 읽히지만, 부하 방식 때문에 그렇게 나온 것입니다.

[그림 5-3] closed-loop vs open-loop (조작 포인트: 발사 방식을 바꿔보기)

■ closed-loop (그 테스트가 쓴 방식)
    워커 8개가 각자 "응답 오면 즉시 다음 요청"

    발사 속도 ──▶ 처리 속도에 자동 연동
    시스템이 느려지면 발사도 느려짐
    → 동시성이 절대 8을 초과하지 않음
    → 큐가 쌓일 수 없음
    ⇒ 큐잉 지연도, 포화점도 측정 불가

■ open-loop (포화점 측정용)
    응답과 무관하게 "초당 N건" 고정 발사

    발사 속도 ──▶ 고정
    처리 속도 < 발사 속도가 되는 순간
    → 큐가 무한히 쌓임
    → p99가 폭발하고 실패가 나기 시작
    ⇒ 여기서 비로소 시스템의 한계가 보인다

그 테스트가 증명한 것: 동시성 8에서는 여유롭다. GPU가 병목이며 예측 가능한 속도로 소화한다.
증명하지 못한 것: 시스템의 한계는 어디인가. max_ongoing_requests: 32가 실제로 어떻게 작동하는가.

실무에서 SLO를 정하려면 후자가 필요합니다. 동시성을 32 이상으로 올리거나 open-loop로 바꿔야 보입니다.

4-3. 원칙 9 — 재실행의 세 목적

목적내용
회귀 방지새 업데이트가 지연/처리량을 악화시키지 않는지
스케일링 테스트피크 트래픽 시뮬레이션
A/B 테스트새 최적화 기법을 프로덕션 배포 전에 데이터로 비교

5장. 4장 전체 정리

세 줄 요약
1. 에이전트가 기본 패턴이 되면서 서빙 요구사항이 근본적으로 바뀌었다.
2. 그 요구를 감당하는 구조가 계층형 아키텍처이고, 오픈소스로도 클라우드로도 구현 가능하다.
3. 어느 쪽을 고르든 판단 근거는 측정된 지표다.

주제핵심
에이전틱 서빙질문 1개 → API 호출 4회. RAG(입력 보강) vs CAG(실행 최적화)는 조합 관계
엔터프라이즈 아키텍처7개 레이어. 기술 문제이기 이전에 조직 설계 문제
오픈소스 스택Kubernetes(인프라) + Ray Serve(호스팅) + vLLM(엔진)
Build vs Cloud이분법이 아니라 스펙트럼. 대부분은 중간에 있고, 위치는 계속 옮겨간다
성능 지표Latency 3개(E2E/TTFT/ITL) + Throughput 2개(RPS/TPS). 공정한 벤치마킹이 전제

CH4-6. 마지막 3문제 답변

Q1. 에이전틱 워크플로우가 서빙 요구사항을 근본적으로 어떻게 바꾸는가?

단일 요청-응답 구조와 달리, 제어 루프 안에서 반복 호출되는 것이 서빙 시스템에 어떤 부담을 주는가?

요약

바뀌는 건 "부하가 늘어난다"가 아닙니다. 부하의 성질이 바뀝니다. 다섯 가지로 나뉩니다.


① 호출 수 증폭 — 그리고 그건 시작일 뿐

1편의 실측: 질문 1개 → API 호출 4회 (chat 3 + embedding 1).

[그림 6-1] 증폭 (조작 포인트: 계획 단계 수를 2 → 5로)

사용자 요청 1건
    ↓
계획 LLM 호출        ×1
검색 embedding       ×k   (action 중 RAG를 쓰는 개수)
action별 LLM 호출    ×n
    ↓
총 백엔드 왕복 = 1 + k + n

Figure 4-2의 3단계 계획이면 호출이 더 늘어납니다. 그리고 실무의 에이전트는 이보다 훨씬 깊습니다 — 도구 호출 루프가 조건 만족까지 반복되고, 여러 에이전트가 계층으로 쌓이면 곱셈이 됩니다.

② tail latency 증폭 — 여기가 가장 중요합니다

책이 "체인된 호출 간 지연이 늘어난다"고만 쓴 부분인데, 산술을 보면 왜 심각한지 명확해집니다.

한 번의 호출이 p95를 넘길 확률이 5%라면, N번 연쇄에서 적어도 한 번 p95를 넘길 확률은:

1 − (1 − 0.05)^N

N = 1  →  5.0 %
N = 4  →  18.5 %
N = 10 →  40.1 %
N = 20 →  64.2 %
[그림 6-2] 왜 평균이 아니라 꼬리를 봐야 하는가

단일 요청:   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░   95%는 빠르고 5%만 느림
                                    → 대부분의 사용자는 만족

4단계 체인:  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░   18.5%가 한 번은 느린 구간을 만남
20단계 체인: ▓▓▓▓▓▓▓░░░░░░░░░░░░░   64%가 느림 ← 이게 "평균적 경험"이 됨

시사점: 에이전트 워크로드에서는 평균 latency를 개선하는 최적화보다 p99를 깎는 최적화가 압도적으로 가치 있습니다. 단일 요청 서빙이라면 반대일 수도 있는데, 여기선 아닙니다. 이건 최적화 우선순위를 정할 때 실제로 판단을 뒤집는 기준입니다.

③ 트래픽 패턴이 버스트로 바뀐다

단일 요청 서빙은 사용자 도착이 대략 독립적입니다(Poisson에 가까움). 에이전트는 다릅니다.

[그림 6-3] 도착 패턴 변화

단일 요청:  · · ·  ·  · ·   ·  · ·      독립적, 평활
에이전트:   ····      ····       ····    사용자 1명 = 상관된 호출 묶음
            └4개┘     └4개┘      └4개┘

같은 사용자 수여도 큐 길이의 분산이 훨씬 큽니다. 대기행렬 이론상 도착이 상관되면 같은 평균 이용률에서 대기 시간이 더 나빠집니다. 결과적으로 평균 GPU 사용률을 기준으로 용량을 잡으면 부족합니다.

④ 입력 token이 비선형으로 자란다

context chaining 때문입니다.

[그림 6-4] 단계가 진행될수록 입력이 커진다 (조작 포인트: 단계 수 증가)

1단계: 검색 문서(5개) + 질문           → 입력 X token
2단계: 1단계 출력 + 질문               → 입력 X' token
3단계: 2단계 출력 + ...                → 입력 X'' token
       ↑ 매 단계 앞 단계 출력이 입력에 얹힘

prefill은 compute-bound입니다. 입력이 커지면 TTFT가 직접 나빠지고, 이게 체인 전체에 누적됩니다. 그래서 에이전틱 워크로드에서는:

  • prefix caching(--enable-prefix-caching)의 가치가 급등합니다. 같은 system prompt와 도구 정의가 매 호출마다 반복 전송되기 때문입니다.
  • chunked prefill(--enable-chunked-prefill)이 필수에 가까워집니다. 긴 프롬프트 하나가 다른 사용자의 decode 스트림을 멈추게 두면 안 됩니다.
  • CAG가 매력적인 선택지가 됩니다. 반복 재전송되는 지식을 KV cache에 상주시키는 것이니까요.

⑤ SLO의 단위가 바뀐다

이게 실무에서 가장 자주 틀리는 지점입니다.

단일 요청 서빙에이전틱 서빙
SLO 단위호출 1건세션(사용자 요청) 1건
핵심 지표TTFTE2E
스트리밍 가치중간 단계는 거의 없음

"p99 TTFT 500ms 이내"라는 SLO를 만족해도, 4번 연쇄하면 사용자는 최악 2초를 기다립니다. 호출 단위 SLO가 전부 초록불인데 사용자 경험은 나쁠 수 있습니다. 세션 단위 추적(trace ID로 묶기)이 없으면 이걸 볼 수조차 없습니다.

추가로 챙겨야 하는 운영 항목 2가지

취소 전파(cancellation propagation)
사용자가 중간에 창을 닫아도, 이미 발사된 3단계 호출은 GPU를 계속 태웁니다. 체인 전체에 취소를 전파하는 메커니즘이 없으면 순수 낭비입니다.

stateful 라우팅
같은 세션의 후속 호출은 이전 KV cache가 있는 replica로 보내는 게 훨씬 효율적입니다. 3편에서 본 Ray Serve의 "캐시 지역성/세션 친화성 기반 커스텀 라우팅"이 정확히 이 요구에 대응합니다. 단일 요청 서빙에서는 라운드로빈으로 충분했지만, 에이전틱에서는 라우팅이 성능 요소가 됩니다.

GPU/CPU 격리 관점에서

에이전트가 늘리는 건 GPU 부하만이 아닙니다. 호출 1번마다 tokenize / HTTP 파싱 / JSON 직렬화 / detokenize라는 CPU 작업이 따라붙습니다. 4배 호출이면 CPU 부하도 4배입니다.

이 CPU 작업이 GPU 스텝 사이에 끼면 GPU bubble이 생깁니다. 3장에서 프로세스를 분리한 이유가 여기서 4배로 중요해집니다 — GPU가 배치 N을 돌리는 동안 CPU가 배치 N+1을 준비하는 오버랩이 없으면, 에이전트 워크로드에서 GPU 유휴 시간이 눈에 띄게 커집니다.


Q2. 계층형 아키텍처의 각 레이어는 어떤 역할과 트레이드오프를 가지며, 오픈소스 스택으로는 어떻게 구현하는가?

요약 표

레이어역할핵심 트레이드오프오픈소스 구현
1. Public API인증·과금·rate limit·라우팅보안·정확한 과금 ↔ 요청당 오버헤드FastAPI + nginx Ingress + JWT/API key + Redis
2. Resource Mgmt하드웨어 배분, 예산가동률 ↔ 여유 용량(버스트 대응)Kubernetes scheduler, HPA, node pool, Karpenter
3. Model Selection요청별 모델 결정비용 ↔ 품질 ↔ 지연 (셋 중 둘)라우팅 미들웨어, canary weight, speculative decoding 분기
4. Distributed Serving멀티GPU/노드 + 분산 캐싱병렬화 이득 ↔ 통신 비용Ray Serve (TP/PP, prefill-decode 분리, cache-aware routing)
5. Core Inference실제 모델 실행처리량 ↔ 지연 (배칭 크기)vLLM / Triton / TensorRT-LLM / SGLang
6. Model Optimization재학습 없는 성능 개선속도·메모리 ↔ 품질양자화(AWQ/GPTQ), speculative decoding, LMCache
7. Model모델 공급·분류·버전 관리최신성 ↔ 안정성모델 레지스트리, S3/HF Hub, 태깅

레이어별 상세

레이어 1 — Public API

역할은 tenant 식별이 전부라고 해도 과언이 아닙니다. JWT든 API key든 결국 tenant 하나로 수렴하고, 이후 모든 정책(quota, 라우팅, 모델 선택, 과금)이 이 값에 매달립니다.

트레이드오프는 관문의 두께입니다. 검증을 촘촘히 하면 안전하지만 요청당 지연이 붙습니다. 그래서 rate limit을 두 겹으로 나눕니다 — Ingress의 limit-rps가 값싼 거친 방어선, 애플리케이션의 rate_limit(tenant)가 정교한 방어선.

레이어 2 — Resource Management

트레이드오프의 본질은 가동률과 여유의 상충입니다. GPU를 빈틈없이 채우면 비용 효율이 좋지만 버스트가 오면 큐가 터집니다. Q1의 ③에서 봤듯 에이전틱 워크로드는 버스트가 심해서, 평균 사용률 기준 용량 산정이 특히 위험합니다.

레이어 3 — Model Selection

Quality / Latency / Cost 삼각형에서 둘만 가져갈 수 있습니다. 판단 근거는 요청 자체에서 뽑습니다 — 예제는 max_new_tokens > 1024로 speculative decoding을 분기했습니다.

여기서 canary 판정이 tenant override보다 먼저 오는 순서에 주목할 가치가 있습니다. 카나리 샘플을 전체 트래픽에서 무작위로 뽑아야 특정 고객군에 편향되지 않은 대표성을 얻기 때문입니다.

레이어 4 — Distributed Serving

트레이드오프는 병렬화 이득 vs 통신 비용입니다. TP는 같은 노드 안이 기본이고, 노드를 넘어가면 PP를 씁니다. GPU를 늘려도 All-Reduce/Reduce-Scatter 통신이 병목이 되면 성능이 선형으로 안 늘고, 어떤 지점부터는 오히려 손해입니다.

레이어 5 — Core Inference

이 레이어의 트레이드오프가 3장 전체의 주제였습니다 — batch를 키우면 throughput↑, TTFT↑, ITL↑. 그리고 여기서 나오는 cold start(모델 load/unload)가 위 레이어들의 스케일링 정책을 제약합니다.


오픈소스 구현 — 매핑과 주의점

[그림 6-5] 레이어 ↔ 컴포넌트 매핑 (조작 포인트: 어느 레이어를 생략할지 결정해보기)

┌──────────────────────────────────────────────────────┐
│ 1. Public API      → FastAPI + nginx Ingress + Redis │
├──────────────────────────────────────────────────────┤
│ 2. Resource Mgmt   → Kubernetes                      │  ← 이 한 층이
│ 1의 라우팅/네트워킹 → Kubernetes                      │     둘을 커버
├──────────────────────────────────────────────────────┤
│ 3. Model Selection → 라우팅 미들웨어 (직접 작성)      │
├──────────────────────────────────────────────────────┤
│ 4. Distributed     → Ray Serve (KubeRay로 K8s 통합)  │
├──────────────────────────────────────────────────────┤
│ 5. Core Inference  → vLLM / Triton                   │
└──────────────────────────────────────────────────────┘

Kubernetes가 바닥인 이유: 핵심 기능(배포/스케일링/관리 자동화)과 거대한 생태계(메트릭, 로깅, 네트워킹, 인가, 하드웨어 관리). 그래서 AWS/GCP/Azure와 OpenAI/Anthropic 양쪽 모두의 백본으로 채택됐습니다.

Ray Serve가 대체하는 것들

3장에서 손으로 만든 것Ray Serve
ModelWorker + 수동 배칭/큐잉@serve.deployment + AsyncLLMEngine 위임
ModelManager의 OrderedDict LRU@serve.multiplexed(max_num_models_per_replica=N)
HPA YAML의 replica/리소스 선언데코레이터 인자 num_replicas, ray_actor_options
model_id 필드 라우팅헤더 ray_serve_multiplexed_model_id

KubeRay 선택: 프로덕션 모델 서빙에는 RayService입니다. RayCluster만 쓰면 Serve 앱을 직접 serve run으로 올리고 pod이 죽으면 수동 재배포해야 하는데, GPU 워커 스펙 작성 난이도는 둘이 동일합니다. 즉 RayCluster를 고를 이유가 없습니다.

⚠️ 실무에서 걸리는 함정 3가지

① 오토스케일링 이중화
Ray Serve의 autoscaling_config와 Kubernetes HPA를 동시에 켜면 서로 싸웁니다. Ray가 replica를 늘리려는데 K8s가 pod을 줄이는 식입니다. 한쪽만 주체로 정해야 합니다. RayService에서는 Ray가 Serve replica를, KubeRay가 워커 pod을 담당하는 계층 구조로 정리하는 게 일반적입니다.

② GPU replica에 CPU 기반 HPA를 쓰면 안 됨
3편의 HPA 예제는 averageUtilization: 70 (CPU)인데, 이건 API 서버용이라 맞습니다. GPU 추론 replica에 같은 방식을 쓰면 부하를 전혀 반영하지 못합니다. GPU는 CPU처럼 오버서브스크립션도 안 됩니다. 추론 replica는 target_ongoing_requests 같은 요청 기반 지표로 스케일해야 합니다.

③ 레이어를 나눌 때마다 홉이 늘어난다
2편에서 짚은 부분입니다. 레이어 하나당 RPC 1~수 ms. TTFT 목표가 200ms면 무시 가능하지만 30ms면 무시 불가입니다.

계층형 아키텍처는 조직 확장성을 사기 위해 latency budget을 지불하는 거래입니다. 대규모 조직이 아니라면 7개 레이어를 다 만들 필요가 없고, 만들지 않는 게 낫습니다. 레퍼런스 아키텍처는 도달 목표지 시작점이 아닙니다.


Q3. "직접 구축 vs 클라우드 벤더"를 스펙트럼으로 볼 때, 어떤 기준으로 위치를 정하고 어떤 지표로 타당성을 판단하는가?

판단 기준 4개 — 그리고 각각의 실제 임계선

기준 ① 사용 편의성 (개발 속도)

AWS 컴포넌트를 많이 쓸수록 빨리 띄웁니다. 하지만 개발 속도는 여러 요인 중 하나일 뿐이고, 운영/유지보수 비용도 똑같이 중요합니다.

판단선: "이 모델이 3개월 뒤에도 필요한가?" 아니라면 Bedrock으로 끝내는 게 맞습니다. 검증 단계에서 인프라를 만드는 건 대부분 조기 최적화입니다.

기준 ② 비용 — 손익분기점 계산과 그 보정

책의 예시:

Bedrock:      입력 100만 token당 $0.10
전용 인스턴스: 시간당 $1.172

$1.172 ÷ $0.10/1M = 11.72M token/hr ÷ 3,600 = 약 3,255 token/s

이 숫자를 그대로 쓰면 안 됩니다. 세 가지 보정이 필요합니다.

보정방향이유
가동률손익분기점 상승인스턴스는 24h 과금, 트래픽은 주야 편차. 평균 가동률 30%면 실질 임계값이 약 3배
출력 token 단가손익분기점 하락계산이 입력 단가만 씀. 출력은 보통 3~5배 단가라 생성 위주 워크로드는 더 빨리 유리
운영 인건비손익분기점 상승온콜, 업그레이드, 장애 대응. 계산식에 없지만 실제로 가장 클 수 있음

핵심: "피크 3,255 token/s"가 아니라 "24시간 평균 3,255 token/s"여야 합니다. 세 보정이 서로 반대 방향이라, 자기 워크로드 숫자를 넣어봐야만 답이 나옵니다.

기준 ③ 커스터마이징 — "무엇이 막혔는가"로 판단

추상적으로 "유연성이 필요하다"가 아니라, 구체적으로 어느 옵션에서 막혔는지로 결정합니다.

[그림 6-6] 막힌 지점 = 필요한 옵션 (조작 포인트: 자기 요구사항을 여기 대입)

"내 모델이 카탈로그에 없다"           → Option 3 (DLC)
"CUDA/프레임워크 버전을 골라야 한다"    → Option 3
"전처리가 컨테이너 기본 동작으로 안 된다" → Option 4 (Script Mode)
"vLLM 특정 버전을 써야 한다"           → Option 5 (BYO Image)
"HTTP 계약 자체를 바꿔야 한다(gRPC/SSE)" → Option 5
"인스턴스 간 KV cache를 공유해야 한다"   → Option 6 (BYO Infra)
"스팟 GPU/MIG로 비용을 깎아야 한다"     → Option 6

기준 ④ 트래픽 규모 — 규모가 아니라 안정성

절대 규모보다 안정성이 중요합니다. 트래픽이 예측 가능하고 꾸준하면 전용 인스턴스가 유리하고(가동률이 높으니), 스파이키하면 서버리스가 유리합니다(유휴 과금이 없으니). 같은 월 총량이어도 분포에 따라 답이 반대가 됩니다.


선택 전략 4가지 (되돌아가기 포함)

상황선택
SLO 충족 + 비용 허용 + 기능 속도 최우선벤더 관리형 유지
일부 엔드포인트만 특별한 배칭/라우팅/tenant 격리 필요하이브리드
하드웨어·런타임 직접 통제 필요, 또는 비용 세밀 튜닝 필요BYO
볼륨이 낮은 수준에서 안정되거나, 복잡성이 성과를 못 냄벤더 관리형으로 복귀

마지막이 실무에서 가장 자주 빠뜨리는 선택지입니다. 되돌아가는 것도 전략이고, 그러려면 API 계약을 안정적으로 유지해둬야 합니다.


측정해야 하는 지표

Latency 3개

지표어떤 판단에 쓰나
E2E에이전틱 워크플로우의 SLO. 세션 단위로 측정
TTFT챗봇 UX. prefill 최적화(prefix caching, chunked prefill)의 효과 측정
ITL긴 출력의 체감 속도. decode 최적화(배칭, 양자화)의 효과 측정
E2E latency = TTFT + ITL × (N − 1)

Throughput 2개

지표주의점
RPS / RPM트래픽 패턴이 다르면 비교 무의미. 입력/출력 길이를 함께 명시해야 의미가 생김
TPS출력 token만 셈. 입력을 짧게 자르거나 배치를 균일하게 맞추면 부풀릴 수 있음

함께 봐야 하는 것: GPU/CPU/메모리 활용률, KV cache hit rate, queue time, OOM 발생 여부


측정 방법론 — 여기가 실제 함정입니다

지표를 아는 것보다 어떻게 재느냐가 결론을 바꿉니다.

① closed-loop로는 포화점을 못 찾는다

3편 부하 테스트를 다시 봅시다.

지표
p50 / p95 / p992.02 s / 2.10 s / 2.10 s
처리량3.92 req/s
실패0건

편차 0.08초를 "안정적"으로 읽으면 절반만 맞습니다. 워커 8개가 각자 "응답 오면 다음 요청"을 던지는 closed-loop 방식이라, 동시성이 8을 절대 넘지 못하고 따라서 큐가 쌓일 수 없습니다.

SLO를 정하려면 open-loop(응답과 무관하게 초당 N건 고정 발사)로 부하를 올려서 p99가 꺾이는 지점을 찾아야 합니다. 그 지점이 실제 용량이고, 그래야 "몇 대가 필요한가"를 답할 수 있습니다.

② 벤더 벤치마크는 조건을 먼저 확인한다

TPS는 입력을 짧게 자르거나 배치를 인위적으로 최적 구성하면 부풀릴 수 있습니다. 벤더 숫자를 볼 때 물어야 할 것: 입력/출력 길이는? 배치 구성은? 동시성은? closed-loop인가 open-loop인가?

밑바닥을 알아야 이 질문을 할 수 있습니다. 그리고 그게 "직접 만드는 법을 아는 것"의 실질적 가치입니다 — 직접 만들지 않더라도.

③ 한 번에 노브 하나만

여러 설정을 동시에 바꾸면 효과를 분리할 수 없습니다. 이건 원칙이라기보다 실패 경험에서 나오는 규칙입니다.


최종 판단 프레임

[그림 6-7] 의사결정 순서

1. 지금 SLO를 만족하는가?
     아니오 → 어느 지표가 미달인가? (E2E / TTFT / ITL / RPS)
              → 그 지표를 개선하는 기능이 현재 옵션에 있는가?
                 없다 → 한 단계 아래 옵션으로
     예   ↓
2. 비용이 허용 범위인가?
     아니오 → 손익분기점을 (가동률·출력비중·인건비 보정해서) 계산
              → 넘었다면 한 단계 아래 옵션으로
     예   ↓
3. 현재 위치 유지. 단, 트래픽이 변하면 1번부터 다시.

한 문장으로: 안정적인 API와 공유 텔레메트리를 유지하고, 효과가 있는 부분만 커스터마이징하고, 제품·트래픽·제약이 바뀌면 스펙트럼 상의 위치도 바뀔 것을 예상하는 것. 그게 유연한 전략입니다.

profile
GPU 인프라 / MLOps 엔지니어 오수진입니다. 쿠버네티스 기반 GPU 스케줄링(HAMi·KAI·Volcano)과 LLM 서빙 인프라를 다룹니다.

0개의 댓글