[CloudNeta] Hands-On LLM Serving 1주차 part 1 - 모델 서빙 기본
이번에도 기회가 닿아 CloudNet@ 를 통해 모델 서빙 및 최적화에 대한 스터디에 참석할 수 있게 되었습니다.
다시금 감사의 인사를 드립니다.
본 스터디의 전체 목적은 LLM 서비스 제공 및 최적화에 대한 지식에 대한 학습 및 실습입니다. 이번 주차의 내용은 세 편으로 나누어 연재합니다.
- part 1 (이 글): 모델과 모델 서빙이란 무엇인지, 개념을 잡습니다.
- part 2: 인공지능 개요와 LLM 기초. part 3을 이해하는 데 필요한 최소한만 다룹니다.
- part 3: LLM 서빙. 토큰 생성 과정, 그리고 LLM 서빙이 왜 독특한 문제인지를 다룹니다.
스터디에서 추천받고, 읽기 계획중인 도서를 공개합니다.
들어가며
이번 스터디의 최종목적에 도달하기 위해 아래 질문을 먼저 살펴봅시다. 모델, 모델 서빙, 모델 서빙 최적화 는 무엇을 말하는 것일까요?
본문에서 계속 쓰는 용어들입니다. 지금은 직관 수준으로만 잡고 넘어가도 충분합니다. LLM 내부 구조와 관련된 용어(토큰, 트랜스포머, 어텐션 등)는 part 2에서 제대로 다룹니다.
- 학습(training): 모델 안의 숫자들을 데이터에 맞게 조금씩 조정해 나가는 과정입니다.
- 추론(inference): 학습이 끝난 모델에 입력을 넣어 출력을 받는 것입니다. 서빙이 실제로 처리하는 작업이 바로 이것입니다.
- 가중치(weight) · 편향(bias): 학습으로 조정되는 숫자들입니다.
y = ax + b의a와b에 해당하며, 이 숫자들을 통칭해 **파라미터(parameter)**라고 부릅니다. "GPT-2 124M" 할 때의 124M이 파라미터 개수입니다. - 텐서(tensor): 그 숫자들을 담는 다차원 배열입니다. shape(모양) 정보가 붙은 숫자 배열로 이해하면 충분합니다.
- 임베딩(embedding): 텍스트 같은 입력을 모델이 계산할 수 있는 숫자 벡터로 바꿔놓은 표현입니다.
또한 인공지능 전반과 LLM 자체에 대한 기초는 part 2에서 별도로 다루기로 하고, 이 글에서는 "서빙의 대상"으로서의 모델이 무엇인지에 집중합니다.
모델이란?
모델은 데이터·구조·실행 코드가 합쳐진 실행가능한 소프트웨어입니다.
모델의 구성요소
모델은 모델 데이터, 모델 아키텍처, 그리고 모델 실행코드로 구성됩니다. 정말 러프한 비유를 들자면 아래와 같다고 할 수 있습니다.
모델 = 코드(아키텍처) + 코드가 만들어낸 상태(데이터) + 그 둘을 조립해 실행하는 부트스트랩(실행 코드)
모델 데이터
엄밀히 말하자면 학습으로 얻게 된 weight, bias, configuration 을 의미합니다. 파트 2에서 보다 자세히 살펴볼 배치 크기, 입력/출력 텐서, 라벨, 임베딩 등의 실행 메타데이터도 여기에 포함됩니다.
PyTorch에서는 학습으로 얻은 weight와 bias를 state_dict라는 "이름 → 텐서" 딕셔너리로 관리하며, 파일로 저장할 때도 이것을 저장합니다[1].
# 가중치와 편향이 사용하고자 하는 모델 객체의 state_dict에 저장됩니다.
torch.save(model.state_dict(), "model_weights.pt")
pytorch_model.bin를 예시로 살펴봅시다
PyTorch에서는 Weight, Bias를 state_dict 에 저장하죠. 후술할 GPT-2의 경우 pytorch_model.bin 파일은 pickle 로 직렬화 되어있는데, 이를 열고 객체를 확인해보면 아래와 같은 값을 갖고있습니다.
import torch
# GPT-2의 pytorch_model.bin은 순수 텐서 딕셔너리라 weights_only=True로 안전하게 로드됩니다
sd = torch.load("pytorch_model.bin", weights_only=True)
print(type(sd))
print(len(sd))
# 앞쪽 몇 개만 이름·shape·dtype 확인
for name, tensor in list(sd.items())[:10]:
print(f"{name:40s} {tuple(tensor.shape)} {tensor.dtype}")
# 총 파라미터 수
print(f"total params: {sum(t.numel() for t in sd.values()):,}")
결과는 아래와 같습니다:
> python info.py
<class 'collections.OrderedDict'>
160
wte.weight (50257, 768) torch.float32
wpe.weight (1024, 768) torch.float32
h.0.ln_1.weight (768,) torch.float32
h.0.ln_1.bias (768,) torch.float32
h.0.attn.bias (1, 1, 1024, 1024) torch.float32
h.0.attn.c_attn.weight (768, 2304) torch.float32
h.0.attn.c_attn.bias (2304,) torch.float32
h.0.attn.c_proj.weight (768, 768) torch.float32
h.0.attn.c_proj.bias (768,) torch.float32
h.0.ln_2.weight (768,) torch.float32
total params: 137,022,720
여기서 <class 'collections.OrderedDict'>는 허깅페이스의 pickle 스캔 결과("Detected Pickle imports: collections.OrderedDict, ...")와 그대로 일치합니다.
참고로 총 파라미터가 흔히 알려진 124M이 아닌 137M인 이유는, 학습된 파라미터가 아닌 인과 마스크 버퍼(h.*.attn.bias, 블록당 1,048,576개 × 12)가 체크포인트에 함께 저장되어 있기 때문입니다.
모델 아키텍처
레이어 구조, 연결 방식, 연산을 정의합니다. 입력이 어떤 계산경로를 지나 출력으로 변환되는지를 결정합니다. 말하자면 모델의 내부 설계도입니다. 레이어가 몇 개인지, 어떤 종류인지, 서로 어떻게 연결되어 어떤 순서로 연산하는지, 입력과 출력이 어떤 구조인지가 여기서 정해집니다. 코드로 보면 아키텍처는 곧 모델 클래스 정의입니다.
LLM의 아키텍처는 Embedding → Transformer Block(Attention + FFN) → Output head 형태가 됩니다. 각 구성요소는 파트 2에서 후술하겠습니다.
아래 코드는 PyTorch 공식 튜토리얼(Saving and Loading Models)의 예시 모델입니다[1:1].
import torch.nn as nn
import torch.nn.functional as F
# LeNet과 유사한 고전적인 이미지 분류 CNN 모델 정의입니다.
class TheModelClass(nn.Module):
def __init__(self):
super(TheModelClass, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
눈여겨볼 점은 두 가지입니다.
__init__ 에 대해
"이런 모양의 텐서들이 필요하다"고 선언하는 코드입니다. 예를 들어 nn.Conv2d(3, 6, 5)는 (6, 3, 5, 5) 모양의 weight와 (6,) 모양의 bias 텐서를 만들며, 인스턴스화 직후에는 랜덤 숫자로 채워져 있습니다. 학습이 하는 일은 이 숫자들을 쓸모 있는 값으로 바꾸는 것이고, 그 결과물이 앞서 본 state_dict입니다.
self.conv1 = nn.Conv2d(3, 6, 5) # 입력 채널 3(RGB), 출력 채널 6, 커널 크기 5x5
self.pool = nn.MaxPool2d(2, 2) # 2x2 맥스풀링, stride=2 → 크기 절반으로 축소
self.conv2 = nn.Conv2d(6, 16, 5) # 입력 채널 6, 출력 채널 16, 커널 5x5
self.fc1 = nn.Linear(16*5*5, 120) # 완전연결층: 400 → 120
self.fc2 = nn.Linear(120, 84) # 120 → 84
self.fc3 = nn.Linear(84, 10) # 84 → 10 (클래스 개수, 예: CIFAR-10)
forward 메소드에 대해
데이터의 흐름을 정의합니다. 위의 텐서들을 어떤 순서로 통과할지(계산 경로)를 정의합니다.
x = self.pool(F.relu(self.conv1(x))) # conv1 → ReLU → 풀링
x = self.pool(F.relu(self.conv2(x))) # conv2 → ReLU → 풀링
x = x.view(-1, 16 * 5 * 5) # 4차원 텐서를 1차원으로 평탄화(flatten)
x = F.relu(self.fc1(x)) # 완전연결층 + ReLU
x = F.relu(self.fc2(x)) # 완전연결층 + ReLU
x = self.fc3(x) # 최종 출력층 (활성화 함수 없음, logits 반환)
return x
입력값 32x32로 보는 예시
입력이 32×32 이미지라면 텐서가 아래처럼 변형되며 흘러갑니다.
┌──────────────┬────────────────────┐
│ 단계 │ 텐서 크기 │
├──────────────┼────────────────────┤
│ 입력 │ (batch, 3, 32, 32) │
├──────────────┼────────────────────┤
│ conv1 → pool │ (batch, 6, 14, 14) │
├──────────────┼────────────────────┤
│ conv2 → pool │ (batch, 16, 5, 5) │
├──────────────┼────────────────────┤
│ flatten │ (batch, 400) │
├──────────────┼────────────────────┤
│ fc1 │ (batch, 120) │
├──────────────┼────────────────────┤
│ fc2 │ (batch, 84) │
├──────────────┼────────────────────┤
│ fc3 (출력) │ (batch, 10) │
└──────────────┴────────────────────┘
모델 실행코드
말 그대로 모델을 실행하는 코드입니다. 아래 순서대로 구동되지요.
- 아키텍처를 초기화하고
- 가중치(weight)를 불러오고
- 추론(inference)을 실행하는 코드를 구성합니다
- 입력값이 들어왔을 때 예상된 예측값을 리턴합니다
PyTorch 코드로는 아래 네 줄입니다[1:2].
# 1. 모델 구조를 생성합니다 (이 시점의 텐서들은 아직 랜덤 숫자입니다)
model = TheModelClass(*args, **kwargs)
# 2. 저장해둔 state_dict를 읽어 학습된 숫자로 덮어씁니다
model.load_state_dict(torch.load("model_weights.pt", weights_only=True))
# 3. 추론 모드로 전환합니다
model.eval()
# 4. 입력을 전달해 예측 결과를 얻습니다
pred = model(inputs)
model.eval()은 dropout, batch normalization처럼 학습 때와 추론 때 동작이 달라지는 레이어를 추론 모드로 바꾸는 스위치입니다. 이를 빠뜨리면 추론 결과가 일관되지 않게 나올 수 있어, 공식 튜토리얼에서도 특히 강조하는 부분입니다[1:3].
아키텍처와 데이터를 분리하는 이유
아키텍처와 가중치(weight)를 분리하면 버전변경, 부분 로딩, 파인튜닝, 레이어 추가 등의 운영 시나리오에 유리합니다. 예를 들어 새 모델 구조와 기존 checkpoint 사이에 일부 key가 맞지 않아도 호환되는 파라미터만 선택적으로 로드할 수 있습니다.
가령 아래와 같이 파일 구조가 구성될 수 있지요.
# 설정(Config) 파일
config.json : 모델 아키텍처 정보 (레이어 수, hidden size, 어텐션 헤드 수, activation 함수 등). 이 파일만으로 모델 구조를 재구성 가능
generation_config.json : 텍스트 생성 시 기본 파라미터 (max_length, temperature, top_p 등)
# 가중치(Weight) 파일
pytorch_model.bin : PyTorch의 전통적인 가중치 저장 포맷 (pickle 기반). state_dict()를 직렬화한 것
model.safetensors : 최신 표준 포맷. pickle 대신 안전한 직렬화 방식 사용 (임의 코드 실행 위험 없음, 로딩 속도도 더 빠름)
.pt / .pth : PyTorch에서 torch.save()로 저장한 파일 (전체 모델 또는 state_dict)
...
# 토크나이저 관련 파일 (NLP/LLM 모델일 경우)
tokenizer.json : Fast tokenizer용 통합 파일 (vocab + merge rules + 설정 모두 포함)
tokenizer_config.json: 토크나이저 클래스, special token 설정 등
vocab.txt / vocab.json : 단어(토큰) 사전
...
기재된 상세 내용은 다음장의 내용에서 보다 상세히 풀어나가도록 하겠습니다.
모델의 생명주기
그렇다면 모델은 학습부터 배포까지의 사이클을 통해 어떤 생명주기를 가지는지 살펴보겠습니다.
---
title: 모델의 생명주기
---
flowchart LR
pt1["데이터 수집"]
pt2["학습·파인튜닝"]
pt3["평가"]
pt4["배포"]
pt5["서빙"]
pt6["모니터링·최적화"]
pt7["재학습"]
pt1 --> pt2
pt2 --> pt3
pt3 --> pt4
pt4 --> pt5
pt5 --> pt6
pt6 --> pt7
pt7 --> pt2| 단계 | 역할 |
|---|---|
| 데이터 수집 | 로그, 문서, 센서, 사용자 데이터 수집 |
| 학습 | 데이터에서 패턴 학습 |
| 평가 | 정확도, Loss, Benchmark 검증 |
| 배포 | 모델을 버전 있는 소프트웨어 산출물로 패키징 |
| 서빙 | API를 통해 실제 요청 처리 |
| 최적화 | 속도, 비용, 안정성 개선 |
| 재학습 | 운영 데이터를 다시 학습에 반영 |
모델의 생명주기는 데이터 수집, 학습, 평가, 배포, 모니터링 그리고 재학습으로 이어집니다.
이번 스터디에서 중점적으로 살펴볼 모델 서빙은 학습된 모델이 실제 요청을 받아 예측 결과를 반환하는 운영 단계라 할 수 있습니다.
그러므로 모델 서빙의 주요 평가기준은 지연시간, 처리량, 안정성, 비용, 보안, 모니터링이 핵심이 됩니다.
모델 서빙과 모델 서빙 방안
여기서의 모델 서빙방식은 ML 모델을 서빙하기 위한 방안을 의미합니다. 프론티어 모델 서빙과는 다소 거리가 있으므로 참고해주세요.
모델 서빙이란?
모델 서빙은 학습이 끝난 모델을 실제 서비스로 전달하는 것을 말합니다. 입력을 수신하고, 모델을 실행한 후 예측결과를 반환하는 과정이죠. 이 관점에서의 최적화는 빠르고 안정적이며 저렴하게 모델을 운영하는 방법을 일컫습니다.
또한 모델 서빙은 LLM 서빙보다 넓은 개념입니다. 모델 서빙 대상은 LLM 말고도 비전 모델, 추천 모델, 음성 인식 모델 등이 모두 포함됩니다. 다만 LLM은 토큰을 하나씩 생성하는 자기회귀(autoregressive) 구조 탓에 일반 모델 서빙과는 질적으로 다른 문제를 갖게 되고, 그래서 LLM 서빙이 별도의 분야로 다뤄집니다. 이 이야기는 part 3에서 본격적으로 다룹니다.
---
title: 모델의 서빙 과정
---
flowchart LR
pt1["클라이언트 or 애플리케이션"]
pt2["모델 서빙 API"]
pt3["모델 로딩 및 관리"]
pt4["추론 백엔드"]
pt5["리소스(CPU, GPU, NPU, etc.)"]
pt1 --> |HTTP·gRPC 요청| pt2
pt2 --> |응답| pt1
pt2 --> pt3
pt3 --> pt4
pt4 --> |추론 결과| pt2
pt4 --> pt5
pt5 --> pt4이를테면 챗봇 앱이 AWS의 SageMaker 추론 엔드포인트에 배포된 모델 서빙, 그 중 LLM을 서빙하는 예시를 살펴봅시다.
- 챗봇 애플리케이션이 SageMaker 추론 엔드포인트에 요청을 보냅니다
- SageMaker는 요청을 해당 엔드포인트에서 실행 중인 DJL Serving으로 전달합니다. DJL Serving은 Java 기반 모델 서버로서 요청을 받아 추론 작업을 관리합니다
- DJL Serving이 vLLM에 추론을 요청합니다. vLLM은 LLM을 GPU에서 효율적으로 실행하는 추론 엔진으로, 모델을 실행해 토큰을 생성합니다
- 생성된 추론 결과는 요청이 들어온 순서대로 반환됩니다
이렇듯 LLM을 서빙할 때는 특히 토큰 생성방식이나 KV Cache, attention 비용, batching, decoding, GPU memory 병목 같은 LLM 고유 특성을 알아야 운영 비용과 성능을 제대로 제어할 수 있습니다.
모델 서빙방안에 대해
이어서 ML 모델의 서빙방식에 대해 살펴봅시다.
ML 프로덕션 서빙의 표준 패턴은 모델을 컨테이너 이미지에 패키징하고 Kubernetes 같은 오케스트레이션으로 배포·관리하는 것입니다. 대표적인 서빙 방법으로는 KServe[2]와 SageMaker AI[3]가 있습니다. 컨테이너 안에서는 TorchServe, TF Serving, NVIDIA Triton 같은 추론 서버가 돌면서 HTTP/gRPC API로 요청을 받습니다. 즉 표의 두 행은 "컨테이너(=추론 서버 인스턴스) 하나당 모델을 몇 개 올리느냐" 의 차이입니다.
| 방식 | 핵심 특징 | 적합한 상황 |
|---|---|---|
| On-device | 단말 안에서 직접 실행 | 초저지연, 오프라인, 개인정보 |
| Single-model | 컨테이너 하나에 모델 하나 | 높은 성능, 독립적 확장 |
| Multi-model | 컨테이너 하나에 여러 모델 | 모델 수가 많고 트래픽이 낮거나 불규칙 |
| Serving platform | 여러 앱·모델·워크플로 통합 | 대규모 조직, 복잡한 AI 서비스 |
모델 서빙 방안 - 온디바이스 서빙(On-Device Serving)
온디바이스 서빙은 모델을 스마트폰, 드론, 카메라, 로봇 등에서 직접 실행하는 방식입니다.
구성
온디바이스 서빙의 구성은 아래와 같습니다.

- 모델 런타임(Model Runtime): 다양한 하드웨어(스마트폰, 드론 등)와 OS(iOS, Android, Linux)에서 모델을 효율적으로 실행하도록 추상화하는 소프트웨어 계층입니다. GPU 같은 전용 하드웨어로 연산을 넘기는 "delegate" 기능도 지원하며 대표적으로 LiteRT, ONNX Runtime(ORT), Core ML 등이 있습니다
- 모델 래퍼(Model Wrapper): 개발자가 직접 구현하는 컴포넌트로, 입력 전처리 → 모델 로딩 → 실행 → 출력 후처리 등 런타임과의 상호작용을 캡슐화해 앱 로직이 쉽게 모델을 호출할 수 있게 합니다
온디바이스 앱은 아래 흐름으로 호출합니다.
- 앱 로직에서 모델 래퍼 호출
- (데이터 변환 후) 모델 런타임 실행
- 로컬 하드웨어에서 추론
주요 활용방안과 제약사항
온디바이스 서빙은 이렇듯 아래와 같은 형태에서 적합합니다.
- 프라이버시 우선 워크로드: 생체 인증, 건강 데이터 등 원본 데이터가 기기 밖으로 나가면 안 되는 경우
- 초저지연 애플리케이션: 제스처 인식, 로봇 제어, AR/VR, 실시간 오디오 처리 등 밀리초 단위가 중요한 경우
- 연결이 불안정하거나 끊기는 환경: 산업 장비, 원격 센서, 차량, 드론 등
- IoT/스마트시티/로봇공학: 특정 작업에 최적화된 저전력 모델이 필요한 경우
다만 온디바이스 서빙은 아래 제약사항과 트레이드오프가 있습니다.
- 연산·저장 공간 제약: CPU/GPU/메모리가 제한적이라 대형모델 구동이 어렵습니다
- 전력 소모: 모델 구동은 배터리를 빠르게 소모합니다
- 업데이트/유지보수의 어려움: 모델 개선 시 모든 기기에 개별적으로 업데이트를 배포해야 합니다
- 하드웨어 지원의 불일치: 기기마다 NPU 지원 여부가 다릅니다(예: iPhone Neural Engine 최적화 모델이 Android Snapdragon에선 비효율적일 수 있음).
모델 서빙 방안 - 단일 모델 서비스
가장 기본적인 서빙 방안입니다. 각 모델(그리고 각 모델 버전)을 독립된 웹 서비스로 배포하여 HTTP/gRPC로 예측 API를 노출하는 방식입니다. 표준 마이크로서비스 구조(컨테이너화)를 따르며, API가 요청을 받아 백엔드 워커(컨테이너)로 라우팅합니다.

구성
단일 모델 서빙 컨테이너는 아래와 같은 구성을 가집니다.
- API Server: HTTP/gRPC로 추론 기능을 외부에 노출
- Model Management: 모델 다운로드 → 로컬 저장소 추출 → 추론 백엔드에 로딩 (신규 모델을 감지해 자동 갱신)
- Inference Backend: 실제 모델 실행 담당 (TensorFlow Serving, TorchServe, vLLM, TensorRT-LLM 등 활용)
라우팅 전략
모델 서빙은 통상 API처럼 라운드 로빈으로 처리하기엔 한계점이 있습니다. 모델의 요청에 따른 처리시간이 모두 다르기 때문입니다. 예를들어 클라이언트가 5,000토큰 LLM 요청한 것과 100토큰 요청한 것은 결과가 다르게 올 것이고, 고해상도 이미지/저해상도 이미지도 마찬가지입니다.
따라서 보다 정교한 전략이 필요합니다.
- 가중 라운드로빈: 성능 좋은 서버에 더 많은 요청 할당
- 최소 연결 수(Least Connections): 활성 연결이 가장 적은 서버로 라우팅
- 최소 응답시간(Least Response Time): 응답이 가장 빠른 서버로 라우팅
- 동적 로드 밸런싱: CPU/GPU/메모리/큐 길이 등 실시간 지표 기반 분배
스케일링 방식
통상의 스케일링 방식과 같이 수직/수평 확장이 존재합니다
- 수평 확장: 트래픽 증가 시 인스턴스(컨테이너)를 여러 대 추가. 보통 오토스케일링(예: Kubernetes HPA)으로 CPU/메모리/지연시간/요청 수 등을 모니터링해 자동 조정하죠
- 수직 확장:
GPT-4,Llama-2-70B처럼 모델이 너무 커서 GPU 1개 메모리로는 구동이 어려울 때, 더 강력한 GPU(H100, A100 등)를 쓰거나 여러 GPU/머신에 분산하여 배포합니다- vLLM 같은 프레임워크는
--tensor-parallel-size옵션으로 더 큰 GPU 또는 여러 GPU에 모델을 분할합니다.
- vLLM 같은 프레임워크는
핵심 운영원칙
여러 머신에 분산하여 배치하기(inter-node) 보다는, 한 머신에 여러 GPU(intra-node)로 구성하는 것을 추천합니다. 머신 간 분산이 이루어진다면 네트워크 오버헤드와 동기화 복잡도라는 추가요소가 들어가 지연시간이 길어지기 때문입니다. 대표적인 예시로, NVIDIA의 NVLink 구성과 IB/RoCEv2 구성은 최대 18배 가량의 스루풋 차이가 납니다[4][5].
다만, 워크로드를 어떻게 꾸려야 할지 고민해야하는 입장에서는 Bin packing 스케줄링 방식을 사용할 수 있습니다. 자원은 유한하고, 상황에 맞게 커스텀 스케줄러를 구성해야할 수도 있지만, 이런 스케줄링 방식도 있다는 건 인지해야 할 것 같습니다.
가장 기본적 서빙 방안으로 불리는 이유
0 to 1을 수행하기 좋습니다.
- 리소스 경쟁이 없으니 매번 최고의 성능/최저 지연시간을 제공합니다
- 모델별로 독립적 확장이 가능하며
- 로그/지표/업데이트가 격리되어 있어 배포·디버깅이 쉽고
- 한 모델이 죽어도 다른 모델에 영향이 가지 않습니다
- 모델별 하드웨어 최적화로 비용 통제 또한 용이합니다
하지만, 자원 효율성과 비용측면에서 약점이 있습니다.
예를 들어 고객 100명이 각자 모델 10개씩 배포하는 에이전트 플랫폼이면 총 1,000개의 개별 서비스를 운영해야 합니다.
그렇다보니 유지보수/패치/모니터링 부담이 압도적이고, 실제로 안 쓰이는 모델도 자원을 낭비하게 됩니다.
이러한 한계로 인해 멀티 모델 서비스를 구성하게 됩니다.
모델 서빙 방안 - 멀티 모델 서비스
이어서 멀티모델 서비스입니다. 이는 하나의 서빙 컨테이너에 여러 모델을 함께 호스팅합니다. 리소스는 모델끼리 공유하고 트래픽에 따라 모델을 로드/언로드 하는 방식입니다. 위에서 살펴본 가성비를 극대화하는 방식입니다.
- 모든 모델을 항상 GPU에 올리지 않고, 요청이 오면 동적으로 로드합니다.
- "고객 100명 × 모델 10개 = 1,000개 모델" 에이전트 플랫폼 예시:
- 단일 모델 서비스처럼 1,000개를 다 GPU/메모리에 올리는 대신, 요청이 들어올 때만 모델을 로드하고, 비활성 상태이거나 메모리가 필요하면 언로드하는 방식으로 인프라 비용을 크게 절감.
구성
멀티-모델 서빙 컨테이너는 아래 구성을 가집니다.
-
Model Server Inference Backend: 프레임워크(TensorFlow, ONNX, PyTorch 등)가 서로 달라도 통합된 예측 API로 처리하는 "블랙박스" 역할. 내부적으로 각 프레임워크별 백엔드를 여러 개 갖고 있음. 대표 오픈소스 예시: NVIDIA Triton Inference Server
-
Model Cache Management:
- 모델 저장소에서 모델을 다운로드해 추론 백엔드에 로드
- LRU(Least Recently Used) 캐시로 자원 사용량이 높으면 가장 안 쓰인 모델을 언로드해 메모리 확보

멀티 모델 서비스의 어려움과 해결책
멀티모델 서비스는 아래와 같은 문제를 가집니다:
- 라우팅 문제: 모델이 아무 컨테이너에나 있을 수 있어서, 이미 해당 모델이 로드된 컨테이너로 요청을 보내야 함. 안 그러면 콜드 스타트(모델 로딩 대기)나 모델 스와핑(기존 모델 언로드 후 새 모델 로드)으로 지연이 생김
- 모델별 스케일링 문제: 모델마다 트래픽이 달라서, 인기 있는("hot") 모델은 더 많은 인스턴스가 필요함

이를 해결하기 위해 라우팅 영역으로 해결방안을 모색하기도 합니다. Envoy AI Gateway , LiteLLM 등이 그 예시입니다.
- 모델 A의 replica가 2라면 → 라우터가 컨테이너 C1, C3에 모델 A를 배치하고 → 이후 모델 A 요청을 C1/C3에 균등 분배.
- 트래픽 변화에 따라 replica 수를 실시간 조정하고, 새 replica를 어느 컨테이너에 넣을지도 라우터가 결정(일종의 bin-packing 문제)
- 실제 예시: Envoy AI Gateway는 이런 식의 구성을 가집니다

멀티 모델 서비스가 어려운 상황
- 모델이 너무 커서 GPU 하나에 여러 모델을 공유할 여지가 없거나, 언로드가 빈번해 콜드 스타트 오버헤드가 큰 경우
- 개별 모델의 트래픽이 많고 저지연이 필요해서 항상 로드된 상태여야 하는 경우 → 이땐 단일 모델 서비스가 더 나음
- 모델별로 보안 정책이 다른 경우
- 캐시 관리, 모델별 라우팅/스케일링, 프레임워크 호환성, 의존성 충돌 등으로 운영 복잡도가 높은 경우
단일 모델 서비스 vs 멀티 모델 서비스
둘은 서로 상호 보완적입니다. 실무에서는 두 방식을 조합해 서로 다른 서빙 케이스를 하나의 플랫폼 안에서 처리하며, 이것이 다음 주제인 모델 서빙 플랫폼(Model Serving Platform)으로 이어집니다.
모델 서빙 방안 - 서빙 플랫폼 운영
비즈니스가 복잡해지면 단일 모델 서비스, 멀티 모델 서비스를 나열하는 것 만으로는 부족해집니다.
- 여러 모델의 협업이 필요한 태스크 증가: Siri 같은 음성 비서는 음성 인식 + NLP + 추천 모델 + 음성 합성(TTS)을 하나의 명령 처리를 위해 함께 작동시켜야 합니다
- 자원 최적화의 복잡도 증가: 플랫폼 위에 앱이 늘어날수록 GPU/CPU/메모리를 여러 모델에 걸쳐 효율적으로 배분하면서도 확장성, 비용 효율성, 자원 경합 최소화를 모두 챙겨야 합니다
이런 경우라면 모델 서빙 플랫폼 자체를 고려해야합니다.
구성
- 게이트웨이: 외부 요청의 진입점입니다
- 라우팅: 어떤 모델을 어느 서빙 그룹에서 처리할지 선택합니다
- 그래프 실행(Graph Execution) 컴포넌트: 한 AI 요청이 여러 모델을 순차/병렬로 사용 지원을 수행합니다
- 리소스 그룹: 앱마다 리소스(CPU, GPU, 메모리 총량)한도를 분리합니다
- 예측 워크로드별로 별도 구성을 해서
- 비즈니스 요구사항에 맞게 쿼터를 할당받도록 구성합니다.

실행과정은 아래와 같습니다.
- 요청 → Gateway → 그래프 실행 엔진(다단계 워크플로우 실행) → 각 단계마다 라우팅 컴포넌트 호출 → 해당 리소스 그룹의 적절한 단일/멀티 모델 서비스로 전달
- 실제 플랫폼에는 여기에 더해,
- 접근 제어
- 보안
- metric/monitoring
- deployment/DevOps 통합이 추가될 것입니다.
종합선물세트네요!
모델 서빙 플랫폼 예시
이런 플랫폼을 구성하는 예시로 아래와 같은 방안들이 있습니다. 대체로 이런 흐름을 가집니다.
- 앞서 잠시 언급한 KServe(구 KFServing)도 k8s 네이티브, 오토스케일링, 멀티 프레임워크를 지원하는 플랫폼입니다.
- Ray Serve는 Ray를 기반으로 한 다단계 추론 그래프 구성에 강점을 가집니다.
- MLFlow 는 모델 레지스트리와 연동된 서빙, 실험 추적까지 통합관리를 수행합니다.
모델 서빙을 살펴봐야 하는 이유?
위에서 다양한 모델의 서빙 방안들을 살펴보았는데요. 이러한 특성들로 인해 클라우드 벤더가 Managed endpoint를 제공한다 하더라도, 서빙하는 근본을 이해하지 못하면 최적의 결정을 내리기 어렵습니다. 이를테면:
- 어떤 모델을 어떤 인프라에 배포할지
- latency와 throughput 목표를 어떻게 맞출지
- GPU/CPU/memory 비용을 어떻게 통제할지
- autoscaling과 routing을 어떻게 설계할지
- 모니터링, 보안, 장애 대응을 어떻게 구성할지
에 대해 결정하기 어렵죠.
확실한 것은 지향하는 목표에 따라 정답이 있기 때문에 이를 저울질할 수 있어야 한다는 점입니다. 그러므로 서빙의 원리를 어떻게 되는 지 안다면 아래 장점을 확보하는 셈입니다.
- 모델 크기, 트래픽 패턴, 보안 요구사항, 비용 구조, 지연 시간 목표에 따라 다른 설계를 할 수 있습니다
- 새 기술이 나왔을 때 장단점을 파악하고, 특정 프레임워크/벤더에 종속되지 않은 채 비즈니스에 유리한 선택을 할 수 있습니다
다음 편 예고
이번 편에서는 모델이 무엇이고, 어떤 방식으로 서빙하며, 어떤 도구가 있는지 다뤘습니다. part 2에서는 LLM 서빙을 이해하기 위한 최소한의 LLM 기초(토큰, 트랜스포머, 텍스트 생성 과정)를 잡고, part 3에서 LLM 서빙이 왜 일반 모델 서빙과 다른 문제인지를 본격적으로 다루겠습니다.
다음 편: 1주차 part 2 - LLM 기초
감사합니다.
이 절의 예시 코드는 PyTorch 공식 튜토리얼 Saving and Loading Models에서 가져왔습니다. ↩︎ ↩︎ ↩︎ ↩︎
Kubernetes 위에서 모델 서빙을 표준화해주는 오픈소스 추론 플랫폼입니다.
InferenceService라는 CRD(Custom Resource Definition) 하나로 그 조합을 모델 서빙용으로 추상화합니다. YAML에 "S3의 이 경로에 PyTorch 모델이 있다"고 선언하면, 알아서 추론 서버 컨테이너(Triton, TorchServe 등)를 골라 띄우고, HTTP/gRPC 엔드포인트를 만들고, 오토스케일링까지 붙여줍니다. 이로 인해 CNCF 인큐베이팅 프로젝트가 되었습니다. 참고 링크 ↩︎AWS에서 제공하는 Fully managed ML 서비스 입니다. 후술할 모델 아티팩트를 S3 에 업로드하고 엔드포인트를 만들기만 하면 AWS에서 호스팅·확장·패치를 관리합니다. 참고 링크 ↩︎
Kubernetes Bin Packing 스케줄링 (CloudNet@ 스터디 영상). NVLink(H100 기준 900GB/s)와 IB/RoCEv2(400Gbps ≒ 50GB/s)의 대역폭 차이는 NVIDIA H100 제품 페이지 참고. ↩︎
EC2 P5en 인스턴스 등의 릴리즈를 확인하고, 운영 원칙에 맞게 준비된 컴퓨팅 자원이 있는지 확인할 필요도 있겠습니다. ↩︎