페이지 목차
이 페이지에서 검색
로컬 LLM
이 페이지에 묻기
로컬 LLM · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.
생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.
LOCAL · LLM
로컬 LLM
생성 수정
로컬 모델 실행과 GPU·메모리 이해부터 서빙·RAG·도구 호출·업무형 앱 구현까지 차례로 살펴봅니다.
42개 문서 · 3일차 · 23 / 42
22차시 · LLM Serving 성능 측정
지도 위치 : 3일차 · ③④층을 바깥에서 재는 자리 · 동시 요청이라는 변수 | 이전: 21차시 vLLM 추론 구조 이해 | 다음: 23차시 vLLM 서버 성능 최적화 키워드 : Latency, TTFT, TPS, Throughput, 동시 요청별 성능 측정
이 차시를 다 읽으면
- 14차시의 지표(Latency · TTFT · TPOT · TPS · Throughput)를 다시 정의하고, 동시성 이 더해지면 각각 어떻게 움직이는지 말할 수 있다.
- 동시성 1 · 2 · 4 · 8 · 16 · 32 단계 측정을 "고정할 것 / 바꿀 것"으로 설계할 수 있다.
- 처리량 – 지연 곡선에서 무릎점 을 찾아 운영점을 고를 수 있다.
- 부하 테스트 클라이언트가
async여야 하는 이유 를 I/O-bound로 설명할 수 있다. vllm bench serve또는 비동기 스크립트, 그리고/metrics로 무엇을 보는지 말할 수 있다.- SLO를 문장으로 먼저 정하고, 결과표로 통과 여부를 판정할 수 있다.
0. 이 차시의 판별 축
| # | 축 | 왼쪽 (0) | 오른쪽 (1) | 이 축으로 갈리는 것 | 큰 축 (README 5장) |
|---|---|---|---|---|---|
| 1 | 한 건의 시간인가, 전체 처리량인가 | Latency · TTFT · TPOT | Throughput | 무엇을 최적화하는가 | ⭐ 한 건의 시간 ↔ 전체 처리량 (파생 축, 부모 ⑤) |
| 2 | 클라이언트가 기다리는가 | 동기 (한 번에 하나) | 비동기 (동시에 여럿) | 부하를 제대로 걸 수 있는가 | ⑥ 동기 ↔ 비동기 (I/O-bound, 3.3) |
| 3 | 대표값인가, 꼬리인가 | 중앙값 (p50) | p95 · p99 | 상담원이 겪는 "가끔 느림" | 한 건의 시간 쪽을 더 쪼갬 (14차시 측정 규칙) |
앞에서 이어집니다 — 14차시는 요청 하나 를 쟀습니다. 이번에는 변수가 하나 늘어납니다. 동시에 몇 개인가. 21차시 3.3의 표("10명이면 처리량↑, 한 사람의 TPS는 조금↓")를 내 장비의 숫자 로 바꾸는 차시입니다.
1. 지표 — 14차시 회수 + 동시성
1.1 다섯 지표
| 지표 | 정의 | 한 건 / 전체 | 상담원의 체감 |
|---|---|---|---|
| E2E Latency | 요청 ~ 응답 완료 | 한 건 | 요약이 다 나오기까지 |
| TTFT | 요청 ~ 첫 토큰 | 한 건 | 답이 시작되기까지 (스트리밍이면 이게 체감의 대부분) |
| TPOT (ITL) | (E2E − TTFT) ÷ (출력 토큰 − 1) | 한 건 | 글자가 흘러나오는 속도 |
| TPS (요청 하나) | 출력 토큰 ÷ 생성 시간 | 한 건 | TPOT의 역수에 가까움 |
| Throughput (서버) | 모든 요청의 출력 토큰 합 ÷ 전체 경과 시간 (tok/s), 또는 req/s | 전체 | 상담원은 직접 못 느낌. 운영자 가 보는 값 |
1.2 동시성이 더해지면 — 경향
| 지표 | 동시성 1 → 32로 올리면 (경향) | 왜 (21차시) |
|---|---|---|
| Throughput | 오르다가 평평해짐 | 배칭으로 오르다가 GPU · KV 풀이 포화 |
| TPOT | 서서히 늘어남 | 한 스텝에 할 일이 많아짐 |
| TTFT | 처음엔 비슷하다가 어느 순간 급증 | 대기열이 생기면 첫 토큰을 받기 전에 기다림 |
| E2E | 늘어남 | 위 둘의 합 |
⚠️ 위 표는 경향 입니다. 내 GPU · 모델 · 설정의 숫자는 재야 압니다.
1.3 ⚠️ 두 가지 함정
| 함정 | 바로잡기 |
|---|---|
| 평균만 본다 | 평균은 가끔 아주 느린 요청을 숨깁니다. p95는 "20번에 한 번은 이보다 느리다"입니다. 상담원이 불평하는 것은 그 한 번입니다 |
| 동시성 = 초당 요청 수 라고 생각 | 다른 변수입니다. 동시성 = 동시에 진행 중인 요청 수(하나 끝나면 다음을 보내는 방식). 요청률 = 초당 도착하는 요청 수. 상담원 10명이 각자 답을 보고 다음을 보내는 문의봇은 동시성 ≤ 10 에 가깝습니다 |
2. 측정 설계 — 한 번에 하나만 바꾼다
2.1 고정할 것 · 바꿀 것
14차시의 측정 규칙(워밍업 버리기 · 같은 프롬프트 · 출력 길이 고정 · 반복 후 중앙값/p95 · 변수 하나만)에 서빙 조건을 더합니다.
| 구분 | 항목 | 이유 |
|---|---|---|
| 고정 | 모델 · 기동 명령 ( 원문 기록 ) | 19차시 옵션 하나만 달라도 결과가 바뀜 |
| 고정 | 입력 길이 · 프롬프트 세트 | 긴 입력은 TTFT를 키움 (Prefill) |
| 고정 | 출력 길이 (max_tokens 고정, temperature 0) | 모델이 일찍 멈추면 TPS가 흔들림. 출력 길이를 강제로 맞추는 엔진 옵션은 버전 확인 |
| 고정 | 같은 GPU에 다른 프로세스 없음 | 18차시 5.1 |
| 바꿈 | 동시성만 : 1 → 2 → 4 → 8 → 16 → 32 | 곡선을 그리기 위해 두 배씩 |
| 반복 | 단계마다 요청을 충분히 (예: 동시성 × 10건 이상), 단계별 2~3회 | 우연을 걸러냄 |
| 버림 | 단계마다 첫 요청(워밍업) | 첫 요청에는 준비 비용이 섞임 |
2.2 입력은 실제 문의에서
vllm bench serve의 무작위 입력도 쓸 수 있지만, 문의봇이라면 평가셋의 실제 문의(가명 처리) 와 실제 시스템 프롬프트 로 재는 것이 정직합니다. 공통 시스템 프롬프트가 있으면 프리픽스 캐시 (21차시 4절)가 TTFT를 줄이므로, 무작위 입력과 결과가 다릅니다. 어느 쪽으로 쟀는지 조건에 적습니다.
3. 도구
3.1 vLLM 서빙 벤치마크
vLLM에는 서빙 벤치마크가 들어 있습니다. 최신 버전은 vllm bench serve 명령이고, 구버전은 저장소의 benchmarks/benchmark_serving.py 스크립트입니다. 옵션 이름은 버전마다 다르니 vllm bench serve --help로 확인 합니다.
vllm bench serve \
--backend vllm --base-url http://127.0.0.1:8000 \
--model Qwen/Qwen3-8B --served-model-name munuibot \
--dataset-name random --random-input-len 1000 --random-output-len 200 \
--num-prompts 200 --max-concurrency 8
🔍 읽는 포인트
--max-concurrency 8— 바꿀 변수. 이 값만 1 · 2 · 4 · 8 · 16 · 32로 바꿔 여러 번 돌립니다.--random-input-len·--random-output-len— 고정할 입력 · 출력 길이.--num-prompts— 단계마다 보낼 요청 수.- 끝나면 요청 처리량, 출력 토큰 처리량, TTFT · TPOT · ITL의 평균 · 중앙값 · 꼬리값이 출력됩니다. 이 출력 원문 이 증거입니다.
3.2 직접 만드는 비동기 스크립트
실제 문의 · 실제 시스템 프롬프트로 재려면 짧은 스크립트가 편합니다.
import asyncio, os, time
from openai import AsyncOpenAI
client = AsyncOpenAI(base_url=os.environ["LLM_BASE_URL"], api_key=os.environ["LLM_API_KEY"])
async def one(messages):
t0, ttft, chunks = time.perf_counter(), None, 0
stream = await client.chat.completions.create(
model=os.environ["LLM_MODEL"], messages=messages,
temperature=0, max_tokens=200, stream=True)
async for c in stream:
if c.choices and c.choices[0].delta.content:
if ttft is None:
ttft = time.perf_counter() - t0 # 첫 조각이 온 시각 = TTFT
chunks += 1
return ttft, time.perf_counter() - t0, chunks
async def run(concurrency, all_messages):
sem = asyncio.Semaphore(concurrency) # 동시에 진행할 요청 수
async def guarded(m):
async with sem:
return await one(m)
t0 = time.perf_counter()
results = await asyncio.gather(*(guarded(m) for m in all_messages))
return results, time.perf_counter() - t0 # 전체 경과 시간 → 처리량
🔍 읽는 포인트
Semaphore(concurrency)— 동시성 손잡이. 이 숫자만 바꿉니다.stream=True+ 첫 조각 시각 — 클라이언트 쪽에서 본 TTFT .gather(...)— 요청들을 동시에 출발시킵니다. 마지막 줄의 전체 경과 시간으로 처리량 을 계산합니다.- ⚠️ 스트리밍 조각 수는 토큰 수와 정확히 같지 않을 수 있습니다. 정확한 출력 토큰 수는
usage로 받습니다(스트리밍에서 usage를 받는 옵션은 엔진별 지원 확인).
3.3 ⭐ 왜 클라이언트는 async여야 하나
먼저 작업을 무엇이 속도를 묶어 두는가 로 두 종류로 가릅니다.
| 용어 | 정의 |
|---|---|
| I/O-bound | [작업의 성격]인데, [ 시간 대부분을 네트워크 응답 · 디스크 읽기 같은 입출력(I/O)을 기다리는 데 쓴다 ]는 점에서 CPU-bound와 구별된다. 기다리는 동안 계산 장치는 논다 |
| CPU-bound | [작업의 성격]인데, [ 시간 대부분을 계산 장치가 쉬지 않고 계산하는 데 쓴다 ]는 점에서 I/O-bound와 구별된다. GPU 행렬 계산도 이쪽(계산 장치가 GPU일 뿐) |
async def / await — Python의 비동기 (README 축 ⑥, 기다리는 동안 멈춰 있지 않고 다른 일을 하는 방식) 문법입니다. async def로 만든 함수가 await에서 응답을 기다리게 되면, Python의 실행기( 이벤트 루프 )가 그 사이에 다른 함수를 돌립니다. 그래서 프로그램 하나가 응답을 기다리는 요청 수십 개를 동시에 걸어 둘 수 있습니다. 판단 기준은 한 줄입니다. 기다림이 많은 I/O-bound 일에는 async def가 효과가 크고, 계산이 꽉 찬 CPU-bound 일에는 효과가 없다. 놀고 있는 시간이 있어야 그 시간에 다른 일을 끼워 넣을 수 있기 때문입니다.
| 모델 서버의 생성 | 부하 테스트 클라이언트 | |
|---|---|---|
| 하는 일 | GPU에서 행렬 계산 | 요청을 보내고 응답을 기다림 |
| 성격 | 계산 그 자체 (17차시 3.1) | 전형적인 I/O-bound |
async가 도움이 되나 | ❌ 안 됨 | ⭕ 크게 됨 |
같은 단어(async)가 경계 양쪽에서 정반대 로 판정됩니다. 동기 클라이언트로 for 문을 돌리면 동시성 1만 재게 됩니다. 그 결과를 보고 "vLLM이 생각보다 처리량이 낮다"고 결론 내리면, 서버가 아니라 클라이언트가 한 번에 하나씩 보낸 것 입니다.
3.4 /metrics — 서버 쪽에서 보기
vLLM은 /metrics에 Prometheus 형식으로 내부 지표를 냅니다. Prometheus 는 서버들의 지표를 주기적으로 모아 저장 · 그래프로 보여 주는 모니터링 도구이고, 그 도구가 읽는 지표이름 값 줄 모양의 텍스트가 "Prometheus 형식"입니다. 도구 없이 curl로 읽어도 됩니다.
curl -s http://127.0.0.1:8000/metrics | grep -E "num_requests_(running|waiting)|cache_usage|time_to_first_token"
| 지표 (예 — 정확한 이름은 내 버전의 출력에서 확인) | 뜻 | 이렇게 읽는다 |
|---|---|---|
vllm:num_requests_running | 지금 생성 중인 요청 수 | 동시성을 올려도 이 값이 더 안 오르면 상한(--max-num-seqs 또는 KV 풀)에 닿은 것 |
vllm:num_requests_waiting | 대기열에 선 요청 수 | 0보다 계속 크면 무릎점을 넘은 것 |
| KV 캐시 사용률 | KV 블록 풀이 얼마나 찼나 | 100% 근처 + 대기 증가 → 21차시 5절의 선점 |
| TTFT 히스토그램 | 서버가 본 TTFT 분포 | 클라이언트 TTFT보다 짧음 (네트워크 · API 서버 시간 제외) |
⚠️ 19차시 3절대로 nvidia-smi의 memory.used는 미리 확보한 양이라 이 용도로 쓸 수 없습니다. 실사용률은 여기서 봅니다.
4. ⭐ 결과 곡선 — 무릎점
개념도입니다. 실측이 아닙니다. 모양만 보세요.
처리량 (출력 tok/s) TTFT p95
│ ●─────● │ ●
│ ● │ /
│ ● │ ●
│ ● │ ●────●────●────●─
│ ● │
└──┬───┬───┬───┬───┬───┬── 동시성 └──┬───┬───┬───┬───┬───┬── 동시성
1 2 4 8 16 32 1 2 4 8 16 32
↑ ↑
처리량이 더 안 오르기 시작 같은 자리에서 지연이 꺾여 오름 = 무릎점
무릎점 (Knee Point) — [성능 곡선 위의 지점]인데, [ 동시성을 더 올려도 처리량은 거의 늘지 않고 지연만 가파르게 늘기 시작한다 ]는 점에서 둘 다 함께 오르는 앞 구간과 구별된다.
무릎점을 넘으면 서버는 더 일하지 않고 줄만 길어집니다. 원인은 21차시의 두 상한, KV 블록 풀 과 한 스텝에 함께 도는 요청 수 , 그리고 GPU 계산의 포화입니다.
⭐ 운영점 = 무릎점 바로 앞에서 SLO를 만족하는 동시성. 문의봇이라면 "상담원 10명"이 그 앞에 있는지가 질문입니다. 뒤에 있다면 23차시로 갑니다.
5. SLO — "충분히 빠름"을 먼저 문장으로
SLO (Service Level Objective) — [운영 목표]인데, [ 측정할 지표 · 조건 · 문턱값으로 "충분히 빠름"을 미리 정한 문장 ]이라는 점에서 "빨라야 한다" 같은 막연한 요구와 구별된다.
문의봇 SLO 예시입니다. 아래 숫자는 목표값의 예 이지 측정값이 아닙니다. 팀이 정합니다.
| 항목 | SLO 예시 |
|---|---|
| 조건 | 동시 10명, 입력 = 평가셋 문의의 p95 길이, 출력 200토큰, 실제 시스템 프롬프트 |
| 첫 반응 | TTFT p95 < 1초 |
| 흐르는 속도 | TPOT p95 < 100ms (초당 10토큰 이상) |
| 안정성 | 에러 · 타임아웃 0건 |
⭐ README 7장 문장 2에서 "내 업무에 충분한 품질 "을 평가셋으로 먼저 정했듯, 충분한 속도 도 먼저 정합니다. SLO가 없으면 측정 결과를 보고 "괜찮은 것 같다"밖에 말할 수 없습니다. 23차시의 튜닝 루프가 이 문장에서 출발합니다.
6. 결과표 양식 — 직접 채웁니다
숫자를 지어내지 마세요. AI가 채운 칸은 출력 원문이 붙은 것만 믿습니다.
조건 기록
측정 일시: vLLM 버전: GPU: 모델:
기동 명령(원문):
입력: (실제 문의 / random, 길이 ___ 토큰) 출력: max_tokens ___ temperature: 0
요청 수/단계: ___ 반복: ___회 워밍업: 단계마다 1회 제외
클라이언트: (vllm bench serve / 직접 스크립트, 버전) 같은 GPU의 다른 프로세스: 없음
결과
| 동시성 | 출력 처리량 (tok/s) | 요청 처리량 (req/s) | TTFT p50 | TTFT p95 | TPOT p50 | TPOT p95 | E2E p95 | 에러 | 최대 대기 요청 | 최대 KV 사용률 | SLO |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | |||||||||||
| 2 | |||||||||||
| 4 | |||||||||||
| 8 | |||||||||||
| 16 | |||||||||||
| 32 |
앞으로 다시 만납니다 — 23차시는 이 표를 기준선 으로 놓고 손잡이를 하나씩 돌립니다. 24차시는 같은 표를 Ollama로 한 번 더 채웁니다. 5일차 39차시에서 프로젝트 서비스로 다시 잽니다.
7. 실습 — 맡기고 검수하기
지시
19차시의 vLLM 서버로 동시성별 성능을 측정해줘.
맥락: vLLM 공식 문서의 benchmark(vllm bench serve) 페이지와 metrics 페이지를 먼저 읽을 것.
내 버전에서 vllm bench serve --help 출력으로 옵션 이름을 확인하고 그대로 쓸 것.
제약:
- 서버 기동 옵션은 바꾸지 말고 기동 명령 원문을 기록.
- 동시성만 1, 2, 4, 8, 16, 32로 바꾸고 나머지(입력 길이, max_tokens 200, temperature 0, 요청 수)는 고정.
- 단계마다 워밍업 1회 제외, 2회 반복.
- 측정 중 /metrics의 실행 중 요청 수 · 대기 요청 수 · KV 캐시 사용률을 1초마다 기록.
- 숫자는 도구 출력에서만 옮길 것. 계산한 값은 계산식을 함께 적을 것.
할 일: 22차시 6절 양식의 조건 기록 + 결과표를 채우고, 무릎점으로 보이는 동시성을 근거와 함께 표시.
SLO: 동시 10명 기준 TTFT p95 < 1초 (동시성 8과 16 사이라면 10에서 한 번 더 측정).
추측하지 말고 실행 출력 원문을 붙여.
받을 증거
- 단계별 벤치마크 출력 원문 (6단계 × 2회)
/metrics기록 (시각 | 실행 중 | 대기 | KV 사용률)- 채워진 결과표와 조건 기록
내가 직접 볼 것
| 볼 것 | 이걸로 아는 것 / 왜 내가 보는가 |
|---|---|
| 결과표 숫자가 원문 어디서 왔는가 (아무 칸이나 골라 원문에서 찾아보기) | AI가 추정을 측정처럼 쓰지 않았는가 (README 9장) |
| 처리량이 평평해지기 시작하는 동시성 | 무릎점 (4절) |
같은 지점에서 대기 요청이 0보다 커지는가 | 무릎점의 원인이 대기열인지 |
| 동시성 1의 TPOT ↔ 동시성 16의 TPOT | 한 사람의 속도는 조금 느려지고 처리량은 오르는가 (21차시 3.3) |
| 동시성 10에서 TTFT p95 | SLO 통과 여부. 이 한 칸이 23차시로 갈지 정함 |
| 반복 2회의 차이 | 차이가 크면 측정이 흔들리는 것. 조건을 다시 봄 |
부록 A. 스스로 확인하기
| # | 질문 | 답이 있는 곳 |
|---|---|---|
| 1 | TPOT의 계산식은? TPS와는 어떤 관계인가? | 1.1 |
| 2 | 동시성을 올릴 때 처리량과 TTFT는 각각 어떻게 움직이는가? | 1.2 |
| 3 | 평균 대신 p95를 보는 이유는? | 1.3 |
| 4 | 동시성과 요청률은 어떻게 다른가? | 1.3 |
| 5 | 측정에서 고정할 것과 바꿀 것은? | 2.1 |
| 6 | I/O-bound와 CPU-bound를 유와 종차로 정의하면? 부하 테스트 클라이언트가 async여야 하는 이유는? 서버의 생성과 무엇이 다른가? | 3.3 |
| 7 | /metrics에서 대기 요청 수가 계속 0보다 크면 무슨 뜻인가? | 3.4 |
| 8 | 무릎점을 유와 종차로 정의하면? 운영점은 어디에 두는가? | 4 |
| 9 | SLO 없이 측정하면 무엇이 문제인가? | 5 |
부록 B. 자주 하는 실수
| 실수 | 바로잡기 |
|---|---|
동기 for 문으로 "동시 10명" 측정 | ❌ 동시성 1만 잼. 비동기 클라이언트 |
| 평균 TTFT만 보고 "1초 이내 OK" | ⚠️ p95가 SLO의 기준 |
| 동시성과 입력 길이를 같이 바꿈 | ❌ 무엇 때문에 변했는지 모름. 하나만 |
| 워밍업 포함 | ⚠️ 첫 요청의 준비 비용이 섞임 |
nvidia-smi 메모리로 KV 여유 판단 | ❌ 미리 확보한 양. /metrics의 KV 사용률 |
| "처리량이 오르니 동시성을 끝까지 올리자" | ❌ 무릎점 뒤에서는 줄만 길어짐 |
| 결과표에 출력 원문 없는 숫자 | ❌ 추정일 수 있음. 원문이 붙은 숫자만 |
부록 C. NotebookLM 복습 프롬프트
이 22차시 노트만 근거로 6문제를 내줘. 비전공자 기준으로.
"동시성을 올릴 때 지표들의 경향", "부하 테스트 클라이언트가 async인 이유", "무릎점", "SLO를 먼저 정하는 이유"는 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.
LangTailor