페이지 목차
이 페이지에서 검색
로컬 LLM
이 페이지에 묻기
로컬 LLM · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.
생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.
LOCAL · LLM
로컬 LLM
생성 수정
로컬 모델 실행과 GPU·메모리 이해부터 서빙·RAG·도구 호출·업무형 앱 구현까지 차례로 살펴봅니다.
42개 문서 · 2일차 · 15 / 42
14차시 · LLM 추론 성능 측정
지도 위치 : 2일차 · ①하드웨어 × ③추론 엔진을 숫자로 읽기 | 이전: 13차시 LLM 추론 파라미터 최적화 | 다음: 15차시 모델별 최적화 비교 키워드 : Latency, TTFT, TPS, Throughput, GPU Utilization, VRAM 사용량
이 차시를 다 읽으면
- Latency · TTFT · TPOT · TPS · Throughput을 유와 종차로 정의하고, 타임라인 그림 위에 짚을 수 있다.
- TTFT와 TPS가 각각 Prefill과 Decode (2차시)에 대응한다는 것을 말할 수 있다.
- 한 건의 시간 ↔ 전체 처리량 축으로 "빨라졌다"는 말이 어느 쪽 이야기인지 가를 수 있다.
- Ollama 응답 통계(나노초)로 Prefill 속도와 생성 TPS를 계산 할 수 있다.
- 측정 규칙 다섯 가지를 대고, AI의 보고에서 "추정"과 "측정"을 구분 할 수 있다.
0. 이 차시의 판별 축
| # | 축 | 왼쪽 (0) | 오른쪽 (1) | 이 축으로 갈리는 것 | 큰 축 (README 5장) |
|---|---|---|---|---|---|
| 1 | ⭐ 누구의 시간인가 | 한 건의 시간 (Latency · TTFT · TPS) | 전체 처리량 (Throughput) | 사용자 체감 ↔ 서버 용량 | 한 건의 시간 ↔ 전체 처리량 (파생 축, 부모 ⑤) |
| 2 | 어느 단계인가 | Prefill (병렬) | Decode (순차) | TTFT ↔ TPS | 순차 ↔ 병렬 (파생 축) |
| 3 | 말인가 숫자인가 | 추정 ("약 40 tok/s일 겁니다") | 측정 (실행 출력 원문) | 믿어도 되는 보고 ↔ 아닌 보고 | — (README 9장 "추정을 측정처럼 말함") |
앞에서 이어집니다 — README 5.1의 경고 하나가 이 차시의 축 1입니다. " ⑥ 비동기는 더 빠른 것이 아니다. 일 한 건 에 걸리는 시간은 같고, 기다리는 시간에 다른 일을 해서 같은 시간에 처리하는 양(전체 처리량) 이 늘어날 뿐이다." 가게로 치면, 손님 한 명의 음식이 나오는 시간은 그대로인데 한 시간에 받는 손님 수가 늘어나는 것입니다. LLM 서빙에서 "빨라졌다"는 말도 이처럼 두 가지 전혀 다른 뜻 이 될 수 있습니다. 2차시 4절의 Prefill · Decode를 이번에는 숫자로 잽니다.
1. 지표 — 타임라인 위에 놓기
1.1 한 요청의 타임라인 ⭐
요청 보냄 첫 토큰 도착 마지막 토큰 도착
│ │ │
├─ (모델 로딩) ─┬─ Prefill ┤─ 토큰 ─ 토큰 ─ 토큰 ─ … ─ 토큰 ─ 토큰 ─ 토큰 ──────┤
│ 첫 요청만 │ 입력 전체 │◄─TPOT─► │
│ │ 한꺼번에 │ Decode: 한 토큰씩 │
│◄──── TTFT ────────────────►│ │
│◄─────────────────────────── Latency (E2E) ─────────────────────────────────────►│
1.2 정의
| 지표 | 정의 (유 + 종차) | 단위 | 대응 단계 |
|---|---|---|---|
| Latency (E2E) | [응답 시간]인데, [ 요청부터 마지막 토큰까지 전체 ]를 잰다는 점에서 TTFT와 구별된다 | 초 | 전체 |
| TTFT (Time To First Token) | [응답 시간]인데, [ 요청부터 첫 토큰이 도착할 때까지만 ] 잰다는 점에서 E2E와 구별된다 | 초 · 밀리초 | Prefill (+ 대기 · 로딩) |
| TPOT / ITL (토큰 간 시간) | [생성 속도 지표]인데, [ 토큰 하나가 나오는 데 걸리는 평균 시간 ]이라는 점에서 TPS의 역수에 해당한다 | 밀리초/토큰 | Decode |
| TPS (요청 하나 기준) | [생성 속도 지표]인데, [ 요청 하나의 출력 토큰 수 ÷ 생성 시간 ]이라는 점에서 서버 전체를 보는 Throughput과 구별된다 | 토큰/초 | Decode |
| Throughput (서버 전체) | [처리량 지표]인데, [ 모든 요청의 출력 토큰 합 ÷ 전체 경과 시간 ]이라는 점에서 한 요청만 보는 TPS와 구별된다 | 토큰/초, 요청/초 | 전체 |
TPOT ≈ (E2E − TTFT) ÷ (출력 토큰 수 − 1)
TPS ≈ 출력 토큰 수 ÷ 생성 시간
⭐ 2차시 4절의 표가 여기서 숫자를 얻습니다. "첫 응답이 늦다" = TTFT(Prefill) , "답이 느릿느릿 나온다" = TPS(Decode) . 손볼 곳이 다르다는 그 표입니다.
1.3 GPU 자원 지표
| 지표 | 정의 | 주의 |
|---|---|---|
| VRAM 사용량 | GPU 메모리 중 잡혀 있는 양 | 9차시 4.2 — 엔진이 미리 확보 한 몫 포함 |
| GPU Utilization | 일정 시간 동안 GPU가 무언가를 실행 중이었던 시간 비율 | ⚠️ "GPU 계산 능력을 몇 % 썼나"가 아님 . Decode처럼 메모리를 기다리는 작업도 높게 나올 수 있음 |
⚠️ 반례 — "GPU 사용률 100%니까 더 이상 빨라질 수 없다"는 틀린 추론입니다. 사용률은 바빴던 시간의 비율 이지 얼마나 효율적으로 바빴는가 가 아닙니다. 15차시에서 Decode가 메모리 병목이라는 것을 보면 이유가 분명해집니다.
2. ⭐ 한 건의 시간 ↔ 전체 처리량
2.1 두 질문은 다르다
| 한 건의 시간 | 전체 처리량 | |
|---|---|---|
| 묻는 사람 | 상담원 한 명 | 운영자 · 팀장 |
| 질문 | "내 문의 요약이 언제 뜨나?" | "10명이 동시에 쓰면 1분에 몇 건 처리하나?" |
| 지표 | TTFT, TPS, E2E | Throughput (토큰/초, 요청/초) |
| 늘리는 방법 | 가벼운 가중치(15차시), 짧은 입력 | 여러 요청을 함께 계산 (배치, 21차시) |
2.2 둘은 서로 당긴다
동시 요청이 늘면 GPU는 여러 요청을 한꺼번에 계산해 처리량이 늘지만 , 각 요청 입장에서는 남과 나눠 쓰느라 한 건의 시간이 늘 수 있습니다.
동시 요청 1개: 한 건 빠름 · 처리량 낮음
동시 요청 10개: 한 건 조금 느려짐 · 처리량 크게 높음 ← 서빙 엔진(vLLM)이 노리는 자리
동시 요청 과다: 한 건 많이 느려짐 · 처리량도 더 안 늚 · 메모리 부족으로 대기·선점
(방향만 보여주는 그림입니다. 실제 모양은 22차시에서 측정합니다.)
⭐ 그래서 목표는 한 문장에 두 축을 같이 적습니다. 예: "동시 10명에서 TTFT p95 < 1초" (SLO, 22 · 39차시). "빠르게"만 쓰면 어느 축인지 모릅니다.
앞으로 다시 만납니다 — 이 차시는 요청 하나씩 재는 법입니다. 동시 요청을 걸어 처리량을 재는 것은 22차시(Serving 성능 측정)에서 비동기 클라이언트 로 합니다.
💡 왜 비동기 클라이언트인지 미리 짚어 둡니다. 일은 무엇을 기다리느냐에 따라 두 종류로 나뉩니다.
| I/O-bound (입출력 대기형) | CPU-bound (계산형) | |
|---|---|---|
| 시간이 어디에 쓰이나 | 네트워크 응답 · 디스크 읽기를 기다리는 데 | 계산 장치가 실제로 계산하는 데 |
| 예 | 부하 테스트 클라이언트 — 요청을 보내고 LLM 서버의 답을 기다림 | LLM 서버 안의 행렬 계산 (GPU가 함) |
| 비동기가 돕는가 | ⭕ 기다리는 동안 다음 요청을 보낼 수 있음 → 동시 요청을 많이 걸 수 있음 | ❌ 기다림이 없으니 겹칠 것도 없음. 빨라지려면 병렬화 (README 5.3) |
Python에서는 async def로 정의한 함수가 이 "기다리는 동안 다른 일을 하는" 방식으로 돕니다. 클라이언트의 일은 거의 전부 I/O-bound이므로, 비동기로 짜면 컴퓨터 한 대로 수십 개의 동시 요청을 걸어 서버의 처리량 을 잴 수 있습니다. 반대로 서버 쪽 생성 속도(TPS)는 비동기로 바뀌지 않습니다.
3. Ollama 응답 통계로 계산하기
3.1 통계 필드
7차시에서 본 대로 Ollama 응답의 마지막 줄("done": true)에 통계가 붙습니다. ⚠️ 단위는 나노초 (10⁻⁹초)입니다.
| 필드 | 뜻 | 대응 |
|---|---|---|
total_duration | 요청 전체 | ≈ E2E (서버 기준) |
load_duration | 모델 로딩 | 첫 요청에서만 큼 → 워밍업으로 버림 |
prompt_eval_count | 입력(프롬프트) 토큰 수 | Prefill 양 |
prompt_eval_duration | 입력 처리 시간 | Prefill |
eval_count | 출력 토큰 수 | Decode 양 |
eval_duration | 출력 생성 시간 | Decode |
curl -s http://localhost:11434/api/generate -d '{
"model": "qwen3:8b",
"prompt": "다음 고객 문의를 세 줄로 요약해: ...",
"stream": false,
"options": {"temperature": 0, "num_predict": 200, "seed": 42}
}' | jq '{load_duration, prompt_eval_count, prompt_eval_duration, eval_count, eval_duration, total_duration}'
🔍 읽는 포인트 : "stream": false(한 번에 받기), options의 세 값이 측정 조건 고정 (4절), jq는 통계 필드만 골라 보여주는 도구.
3.2 계산
Prefill 속도 (토큰/초) = prompt_eval_count ÷ prompt_eval_duration × 1e9
생성 TPS (토큰/초) = eval_count ÷ eval_duration × 1e9
서버 기준 TTFT 어림 ≈ (load_duration + prompt_eval_duration) ÷ 1e9 초
예시(실측 아님): eval_count 200, eval_duration 4,000,000,000 → 200 ÷ 4e9 × 1e9 = 50 토큰/초.
⚠️ 주의 두 가지.
- 서버 기준 vs 사용자 기준 : 통계는 서버 안에서 잰 값입니다. 사용자가 느끼는 TTFT에는 네트워크 · 앱 처리 시간이 더해집니다. 사용자 기준은 스트리밍으로 받으며 클라이언트에서 첫 조각 도착 시각 을 잽니다(아래 코드).
- 같은 프롬프트를 반복하면 엔진이 앞부분 계산을 재사용해
prompt_eval_count가 작게 나올 수 있습니다. 매번 값을 확인하고, Prefill을 잴 때는 이 점을 기록합니다(21차시 프리픽스 캐싱과 같은 현상).
3.3 사용자 기준 TTFT 재기
import time, ollama
t0 = time.perf_counter()
ttft = None
for chunk in ollama.chat(model="qwen3:8b", messages=[{"role": "user", "content": PROMPT}],
stream=True, options={"temperature": 0, "num_predict": 200, "seed": 42}):
if ttft is None and chunk["message"]["content"]:
ttft = time.perf_counter() - t0 # 첫 조각 도착
last = chunk
e2e = time.perf_counter() - t0 # 마지막 조각 도착
🔍 읽는 포인트 : t0(보낸 시각) → ttft(첫 조각이 비어 있지 않을 때 ) → e2e(끝). stream=True여야 첫 조각을 잴 수 있습니다. ⚠️ Qwen3의 생각 모드가 켜져 있으면 첫 조각이 <think> 내용일 수 있으니, 생각 모드를 어떻게 설정했는지 도 측정 조건에 적습니다(버전 · 모델 카드 확인).
4. ⭐ 측정 규칙
| # | 규칙 | 지키지 않으면 |
|---|---|---|
| 1 | 워밍업 1회 버리기 | 첫 요청은 모델 로딩(load_duration)이 포함돼 몇 배 느리게 나옴 |
| 2 | 같은 프롬프트 · 같은 출력 길이 (num_predict/max_tokens 고정, temperature 0, seed 고정) | 출력이 길면 E2E가 늘어나는 게 당연. 비교가 무의미 |
| 3 | N회 반복 → 중앙값과 p95 | 한 번 잰 값은 우연. 평균은 튀는 값 하나에 끌려감 |
| 4 | 한 번에 변수 하나 (13차시 5절) | 무엇 때문에 달라졌는지 모름 |
| 5 | 측정 조건 기록 (엔진 · 버전 · 모델 태그 · 양자화 · num_ctx · 동시 수 · GPU) | 다음 주에 다시 재면 비교 불가 |
p95 — [통계 지표]인데, [ 100번 중 95번은 이보다 빨랐다는 경계값 ]이라는 점에서 가운데 값인 중앙값과 구별된다. 사용자가 "가끔 너무 느리다"고 느끼는 것은 평균이 아니라 p95 쪽입니다.
4.1 GPU 숫자 함께 기록하기
# 1초마다 사용률 · 메모리를 CSV로
nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv -l 1
측정을 돌리는 동안 다른 터미널에서 켜 두고, 결과를 측정표와 함께 저장합니다. 눈으로만 볼 때는 watch -n 1 nvidia-smi도 됩니다.
4.2 측정 결과표 양식
| 조건 ID | 엔진 · 버전 | 모델 · 양자화 | num_ctx | 입력 토큰 | 출력 토큰 | 반복 | TTFT 중앙값 / p95 | TPS 중앙값 | E2E 중앙값 | VRAM (도구) | GPU 사용률 | 비고 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| A-1 | ||||||||||||
| A-2 |
💡 숫자 칸은 직접 채웁니다. "VRAM (도구)" 칸에 어느 도구의 숫자인지(nvidia-smi / ollama ps)를 꼭 적습니다(9차시 4.2). 15차시 비교표가 이 양식을 그대로 씁니다.
5. AI의 "추정"과 "측정" 구분하기
README 9장의 위험 "추정을 측정처럼 말함"이 이 차시에서 가장 자주 일어납니다.
| AI의 보고 | 판정 | 이유 |
|---|---|---|
| "RTX 4090이면 8B Q4는 대략 ○○ tok/s 정도 나옵니다" | ❌ 추정 | 실행 출력이 없음. 기억이나 일반론 |
| "측정 결과 평균 ○○ tok/s입니다" (출력 없음) | ⚠️ 검증 불가 | 원문을 요구 |
| 명령 + 출력 원문(JSON 통계) + 계산 과정 + 조건 | ⭕ 측정 | 다시 계산해 볼 수 있음 |
| "최적화 후 2배 빨라졌습니다" | ⚠️ 어느 축? | TTFT인가 TPS인가 처리량인가, 바꾼 변수는 하나인가 |
⭐ 코드를 맡길 때 AI의 "완료했습니다"라는 말 대신 실행해 본 결과(빌드 · 테스트 통과 여부) 로 판정하듯, 성능에서도 AI의 "빨라졌습니다" 대신 측정표가 판정 합니다. 판정은 AI 바깥의 숫자가 합니다.
💡 이론 상한(15차시)처럼 계산으로 나온 값 은 추정이 아니라 계산값 입니다. 다만 "실측은 이보다 낮다"는 표시를 붙여 측정값과 섞이지 않게 합니다.
6. 실습 — 맡기고 검수하기
지시
목표: 문의봇 모델(qwen3:8b)의 한 건 성능을 측정 규칙에 따라 잰다.
맥락: Ollama API 문서에서 응답 통계 필드(이름, 단위)를 먼저 인용해. 설치된 Ollama 버전, GPU 이름, 모델 태그와 양자화(ollama show)를 기록.
조건: 문의 예시 3개(짧은/중간/긴 메일, 내가 줌). temperature 0, seed 42, num_predict 200, num_ctx 8192, 생각 모드 설정 명시.
작업:
1. 각 프롬프트마다 워밍업 1회 버리고 10회 반복.
2. 서버 통계로 Prefill 속도, 생성 TPS 계산. 스트리밍으로 클라이언트 기준 TTFT, E2E 측정.
3. 측정 중 nvidia-smi --query-gpu=utilization.gpu,memory.used --format=csv -l 1 을 따로 기록.
보고 형식: 노트 14차시 4.2 측정 결과표 양식 그대로. 중앙값과 p95를 함께.
추측하지 말고 실행 코드, 원본 측정 로그(10회 각각의 값), 계산 과정을 붙여.
받을 증거
- 버전 · GPU · 모델 정보 출력
- 10회 각각의 원본 값 (통계 JSON 또는 CSV), 워밍업 1회 값(버린 것도 별도로)
- 계산 과정,
nvidia-smi로그
내가 직접 볼 것
| 볼 것 | 이걸로 아는 것 |
|---|---|
워밍업 1회의 load_duration이 나머지보다 훨씬 큰가 | 규칙 1이 왜 필요한지 눈으로 확인 |
출력 토큰 수(eval_count)가 매번 같은가 | 규칙 2가 지켜졌는가. 다르면 TPS · E2E 비교가 흔들림 |
| 긴 메일일수록 TTFT는 늘고 TPS는 거의 그대로 인가 | TTFT = Prefill, TPS = Decode라는 2차시 구분이 숫자로 보임 |
| 원본 로그 하나를 골라 TPS를 직접 계산 해 보고서 값과 맞는가 | 나노초 단위 실수(1e9 누락) 같은 계산 오류 검수 |
prompt_eval_count가 반복할수록 줄어드는가 | 프롬프트 재사용 현상. Prefill 측정값 해석에 주의 |
부록 A. 스스로 확인하기
| # | 질문 | 답이 있는 곳 |
|---|---|---|
| 1 | TTFT · TPOT · TPS · Throughput을 타임라인 위에 짚으면? | 1.1, 1.2 |
| 2 | TTFT와 TPS는 각각 Prefill · Decode 중 어디에 대응하는가? | 1.2 |
| 3 | GPU 사용률 100%가 "더 빨라질 수 없음"을 뜻하지 않는 이유는? | 1.3 |
| 4 | 한 건의 시간과 전체 처리량은 왜 서로 당기는가? | 2.2 |
| 5 | 좋은 성능 목표 문장의 조건은? | 2.2 |
| 6 | Ollama 통계로 생성 TPS를 계산하는 식은? 단위 함정은? | 3.2 |
| 7 | 서버 기준 TTFT와 사용자 기준 TTFT는 무엇이 다른가? | 3.2, 3.3 |
| 8 | 측정 규칙 다섯 가지는? | 4 |
| 9 | 평균 대신 중앙값과 p95를 보는 이유는? | 4 |
| 10 | AI 보고가 "측정"이 되려면 무엇이 붙어 있어야 하는가? | 5 |
부록 B. 자주 하는 실수
| 실수 | 바로잡기 |
|---|---|
| 첫 요청 시간을 그대로 기록 | ❌ 모델 로딩 포함. 워밍업 버리기 |
eval_duration을 초로 읽음 | ❌ 나노초 . × 1e9 또는 ÷ 1e9 확인 |
| 출력 길이를 고정하지 않고 E2E 비교 | ❌ num_predict 고정 |
| "빨라졌다"만 보고 | ⚠️ TTFT · TPS · 처리량 중 어느 축 인지 |
| 한 번 재고 결론 | ❌ N회 반복, 중앙값 · p95 |
| GPU 사용률을 계산 효율로 해석 | ❌ 바빴던 시간 비율 일 뿐 |
| AI가 말한 tok/s를 측정값으로 기록 | ❌ 출력 원문 이 없으면 추정 |
부록 C. NotebookLM 복습 프롬프트
이 14차시 노트만 근거로 6문제를 내줘. 비전공자 기준으로.
"TTFT와 TPS가 Prefill·Decode 중 어디인가", "한 건의 시간 vs 전체 처리량", "Ollama 통계 나노초 계산", "측정 규칙"은 반드시 포함하고 계산 문제 1개를 넣어줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.
LangTailor