본문으로 건너뛰기
페이지 목차
이 페이지에서 검색

로컬 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 사용량

이 차시를 다 읽으면

  1. Latency · TTFT · TPOT · TPS · Throughput을 유와 종차로 정의하고, 타임라인 그림 위에 짚을 수 있다.
  2. TTFT와 TPS가 각각 Prefill과 Decode (2차시)에 대응한다는 것을 말할 수 있다.
  3. 한 건의 시간 ↔ 전체 처리량 축으로 "빨라졌다"는 말이 어느 쪽 이야기인지 가를 수 있다.
  4. Ollama 응답 통계(나노초)로 Prefill 속도와 생성 TPS를 계산 할 수 있다.
  5. 측정 규칙 다섯 가지를 대고, 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, E2EThroughput (토큰/초, 요청/초)
늘리는 방법가벼운 가중치(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가 늘어나는 게 당연. 비교가 무의미
3N회 반복 → 중앙값과 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 중앙값 / p95TPS 중앙값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. 스스로 확인하기

#질문답이 있는 곳
1TTFT · TPOT · TPS · Throughput을 타임라인 위에 짚으면?1.1, 1.2
2TTFT와 TPS는 각각 Prefill · Decode 중 어디에 대응하는가?1.2
3GPU 사용률 100%가 "더 빨라질 수 없음"을 뜻하지 않는 이유는?1.3
4한 건의 시간과 전체 처리량은 왜 서로 당기는가?2.2
5좋은 성능 목표 문장의 조건은?2.2
6Ollama 통계로 생성 TPS를 계산하는 식은? 단위 함정은?3.2
7서버 기준 TTFT와 사용자 기준 TTFT는 무엇이 다른가?3.2, 3.3
8측정 규칙 다섯 가지는?4
9평균 대신 중앙값과 p95를 보는 이유는?4
10AI 보고가 "측정"이 되려면 무엇이 붙어 있어야 하는가?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개를 넣어줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.