페이지 목차
이 페이지에서 검색
로컬 LLM
이 페이지에 묻기
로컬 LLM · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.
생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.
LOCAL · LLM
로컬 LLM
생성 수정
로컬 모델 실행과 GPU·메모리 이해부터 서빙·RAG·도구 호출·업무형 앱 구현까지 차례로 살펴봅니다.
42개 문서 · 5일차 · 40 / 42
39차시 · 성능 테스트 및 최적화
지도 위치 : 5일차 · 프로젝트 7단계 · ①~⑤ 다섯 층 전체를 숫자로 | 이전: 38차시 Function Calling 및 서비스 통합 | 다음: 40차시 프로젝트 발표 및 평가 키워드 : 응답속도, TPS, Throughput, GPU 메모리 측정 및 최적화
이 차시를 다 읽으면
- 33차시의 성능 목표(SLO)를 시나리오 · 동시성 단계 · 지표 로 이루어진 측정 계획으로 바꿀 수 있다.
- 동시성 단계별 부하 테스트 표를 채우고, SLO가 깨지는 지점을 찾을 수 있다.
- 느릴 때 Prefill인가 Decode인가 → KV 캐시 부족인가 → 앱 서버인가 의 순서로 병목을 판정할 수 있다.
- 최적화를 한 번에 하나씩 적용하고 before/after 표로 판정할 수 있다.
- 최적화 후 평가셋을 다시 돌려 품질 회귀를 확인해야 하는 변경을 고를 수 있다.
- 숫자마다 측정 조건 을 함께 기록할 수 있다.
0. 이 차시의 판별 축
| # | 축 | 왼쪽 (0) | 오른쪽 (1) | 이 축으로 갈리는 것 | 큰 축 (README 5장) |
|---|---|---|---|---|---|
| 1 | 한 건의 시간 ↔ 전체 처리량 | TTFT · TPOT · E2E (상담원 한 명의 체감) | Throughput · req/s (서버 전체) | 동시성을 올릴 때 무엇이 오르고 무엇이 나빠지나 | 한 건의 시간 ↔ 전체 처리량 (⑤ 파생) |
| 2 | 어디가 느린가 | 계산 병목 (Prefill → TTFT) | 메모리 병목 (Decode → TPOT) | 손볼 곳이 달라짐 | 계산 병목 ↔ 메모리 병목 (① 파생) |
| 3 | 어느 구간인가 | 모델 서버 직접 | 앱 서버 경유 | 느린 것이 모델인가 앱인가 | 이 차시의 축 (3절 구간별 측정) |
| 4 | 빠른가 ↔ 맞는가 | 성능 지표 | 품질 지표 (평가셋) | 최적화가 품질을 깎았나 | README 7장 문장 2 (트레이드오프) |
앞에서 이어집니다 — 14차시 (측정 규칙: 워밍업, 고정 출력 길이, 중앙값 · p95, 변수 하나), 22차시 (동시 요청 부하 측정), 23차시 (vLLM 최적화 옵션)를 프로젝트 하나에 적용합니다. 다른 점은 통과선이 있다 는 것입니다. 3일차에는 "몇이 나왔다"로 끝났지만, 39차시는 33차시 NFR-01 — "동시 10명 TTFT p95 < 1초" — 을 넘었는가 못 넘었는가 를 판정합니다.
1. 측정 계획 — SLO를 시험으로 바꾸기
1.1 지표 회수 (14 · 22차시)
| 지표 | 정의 | 축 1의 어느 쪽 | 문의봇에서 누가 느끼나 |
|---|---|---|---|
| TTFT | 요청 ~ 첫 토큰 | 한 건 | 상담원: "누르고 얼마나 기다리나" |
| TPOT | (E2E − TTFT) ÷ (출력 토큰 − 1) | 한 건 | 상담원: "초안이 흘러나오는 속도" |
| E2E Latency | 요청 ~ 응답 완료 | 한 건 | 결과 카드가 뜰 때까지 |
| TPS (요청 하나) | 출력 토큰 ÷ 생성 시간 | 한 건 | — |
| Throughput | 모든 요청의 출력 토큰 합 ÷ 전체 경과 시간 | 전체 | 팀 전체가 하루에 처리하는 양 |
| req/s | 완료 요청 수 ÷ 전체 경과 시간 | 전체 | — |
| GPU 메모리 · KV 캐시 사용률 · 대기 요청 수 | nvidia-smi, /metrics | 자원 | 병목 판정 (3절) |
1.2 시나리오 — 부하의 모양
문의봇의 요청은 모양이 다릅니다. 한 가지 프롬프트로만 재면 실제와 다릅니다.
| 시나리오 | 무엇 | 입력 / 출력 | 주로 걸리는 쪽 |
|---|---|---|---|
| S1 결과 카드 | /api/triage (36차시) | 길다 / 짧다 (JSON) | Prefill → TTFT |
| S2 답변 초안 | /api/reply 스트리밍 (37차시) | 길다 / 길다 | Decode → TPOT |
| S3 후속 요청 3턴째 | 기록 포함 /api/reply | 더 길다 / 중간 | Prefill (37차시 3.3) |
| S4 도구 포함 | get_order 거치는 처리 (38차시) | 모델 호출 2회 + 도구 | 앱 · 도구 구간 |
💡 입력은 33차시 평가셋의 실제 메일(가명) 에서 뽑습니다. 짧은 "안녕하세요" 프롬프트로 잰 숫자는 문의봇의 숫자가 아닙니다.
1.3 측정 조건 기록
[측정 ID] P-003
[날짜] 2026-12-06 15:20
[장비] RTX 4090 24GB / 드라이버 ___ / CUDA ___
[엔진] vLLM ___ (vllm --version 출력)
[모델 · 양자화] DR-001의 값
[기동 옵션] (35차시 명령 전체를 그대로 붙임)
[프롬프트 버전] triage.v2, reply.v1
[시나리오] S1:S2:S3 = 5:4:1, 입력 토큰 중앙값 ___ / 최대 ___
[생성 설정] temperature 0, max_tokens S1=300 S2=500
[동시성 · 요청 수] 10 동시, 200 요청, 워밍업 10 요청 제외
[측정 구간] vLLM 직접 / 앱 경유
[도구 · 명령] (실행한 명령 원문)
⚠️ 조건이 없는 숫자는 비교할 수 없습니다. "빨라졌습니다"만으로는 아무 정보도 되지 않습니다. 무엇이, 어떤 조건에서, 얼마에서 얼마로 바뀌었는지가 함께 있어야 숫자가 됩니다. "어떤 조건에서"가 바로 이 기록입니다. 40차시 발표에서 숫자를 말할 때 이 블록이 함께 갑니다.
2. 동시성 단계별 부하 테스트
2.1 왜 단계별인가
동시 10명만 재면 "통과/실패"만 압니다. 1 → 2 → 5 → 10 → 15 → 20으로 올리면 어디서 깨지는지 , 즉 여유가 얼마인지 압니다. 상담원이 12명으로 늘면 괜찮은가? 그 답이 15 · 20 줄에 있습니다.
2.2 ⭐ 부하 테스트 표 — 직접 채우는 표
시나리오 혼합(1.2절)으로, 측정 구간은 vLLM 직접 과 앱 경유 두 벌을 채웁니다.
| 동시성 | TTFT p50 | TTFT p95 | TPOT p95 | E2E p95 | Throughput (출력 tok/s) | req/s | 오류 | KV 사용률 최대 | 대기 요청 최대 | 선점 경고 | SLO 통과? |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | |||||||||||
| 2 | |||||||||||
| 5 | |||||||||||
| 10 (SLO) | |||||||||||
| 15 | |||||||||||
| 20 |
2.3 표를 읽는 법
| 보이는 모양 | 뜻 | 배운 곳 |
|---|---|---|
| 동시성을 올리면 Throughput이 오르다가 평탄 해짐 | 연속 배칭이 GPU를 채웠다. 그 뒤로는 줄만 길어짐 | 21 · 22차시 |
| Throughput이 오르는 동안 TTFT · TPOT도 조금씩 늘어남 | 정상. 한 건의 시간을 조금 내주고 처리량을 산 것 | 축 1 |
| 어느 지점부터 TTFT p95가 급격히 뜀 | 대기열이 생겼다 → 3절 ② | 21차시 |
| 오류가 0이 아님 | 타임아웃 · 컨텍스트 초과 · 앱 예외 → 로그부터 | 35 · 37차시 |
⭐ 첫 줄이 이 차시의 핵심입니다. 앞에서 배운 "비동기는 빠른 게 아니다. 한 건의 시간은 같고 처리량이 늘어난다"(README 5.1 축 ⑥) 와 같은 구조입니다. vLLM의 연속 배칭도 한 건을 빠르게 하는 기술이 아니라 처리량을 늘리는 기술 입니다(21 · 22차시, README 5.3 "한 건의 시간 ↔ 전체 처리량").
2.4 무엇으로 재나
| 구간 | 도구 | 비고 |
|---|---|---|
| vLLM 직접 | vLLM의 서빙 벤치마크 (최신은 vllm bench serve, 구버전은 benchmarks/benchmark_serving.py) 또는 AsyncOpenAI 동시 요청 스크립트 | 이름 · 옵션은 버전 확인 |
| 앱 경유 | httpx 비동기 스크립트로 /api/triage · /api/reply(SSE) 호출 | SSE 첫 data: 도착 = TTFT |
| GPU | nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv -l 1 | 메모리는 거의 일정 (기동 때 확보, 35차시 3절) |
| KV · 대기열 | /metrics의 실행 중 · 대기 요청 수, KV 캐시 사용률, 선점 수 | 지표 이름은 버전마다 다름 |
⚠️ nvidia-smi의 memory.used로 KV 캐시가 꽉 찼는지 알 수 없습니다. vLLM은 기동 때 이미 확보해 두었기 때문입니다. KV가 얼마나 차 있는지는 /metrics 에서 봅니다.
3. ⭐ 병목 판정 순서
SLO를 못 넘었을 때 "모델이 느리다"고 결론 내리면 대개 틀립니다. 요청 한 건은 브라우저 → 앱 서버 → 모델 서버 → (도구) → 다시 앱 서버를 지나므로, 구간마다 시간을 따로 찍어야 어디서 느린지 압니다. 전체 시간 하나만 보면 범인을 모델로 몰기 쉽습니다. 아래 순서로 좁힙니다.
3.1 세 질문
| 순서 | 질문 | 무엇을 보나 | 판정 |
|---|---|---|---|
| ① | TTFT가 넘었나, TPOT가 넘었나? | 2.2절 표의 TTFT p95 · TPOT p95 | TTFT 쪽 → Prefill 또는 줄서기 / TPOT 쪽 → Decode |
| ② | KV 캐시가 모자라 줄을 서나? | /metrics 대기 요청 수 > 0, KV 사용률 ≈ 100%, 선점 경고 로그 | 그렇다면 TTFT가 늘어난 이유는 계산이 아니라 대기 |
| ③ | 앱 서버 구간인가? | 같은 시나리오의 vLLM 직접 vs 앱 경유 차이 | 직접은 통과, 경유는 실패 → 앱 쪽 |
3.2 판정별 손볼 곳
| 판정 | 손볼 곳 (배운 차시) | 치르는 대가 |
|---|---|---|
| Prefill (TTFT ↑, 대기 0, 입력이 김) | 시스템 프롬프트 · 예시 줄이기(36), 대화 기록 줄이기(37차시 3.3), 프리픽스 캐싱 확인(21), --max-num-batched-tokens 조정(23) | 프롬프트를 줄이면 품질 변화 가능 |
| Decode (TPOT ↑) | max_tokens 줄이기, 가중치 양자화 (34), 더 작은 모델 | 초안 잘림 · 품질 변화 |
| KV 부족 (대기 > 0, KV ≈ 100%, 선점) | --max-model-len ↓, --kv-cache-dtype fp8, 가중치 양자화로 KV 자리 확보, --max-num-seqs 조정(23) | 긴 메일 잘림 · 품질 변화 가능 |
| 앱 서버 (직접은 통과, 경유는 실패) | async def로 LLM 호출(36차시 5절 — 기다리는 동안 다른 요청을 처리), 클라이언트를 매 요청 새로 만들지 않기, 프록시 버퍼링 끄기(37차시 2.3), 도구 타임아웃(38) | 거의 없음 |
💡 앱 서버가 병목이면 모델 쪽을 손대지 않습니다. 대가가 "거의 없음"인 칸부터 고칩니다. 36차시 4.3절의 "원인이 지시 · 설정 쪽에 있으면 모델 교체는 답이 아니다"의 성능판입니다.
⚠️ Decode의 이론 상한은 34차시 4.2에서 계산했습니다(대역폭 ÷ 가중치 크기). 동시성 1에서 잰 TPS가 이 상한을 넘으면 측정이 잘못된 것이고, 상한에 크게 못 미치면 엔진 설정을 의심합니다(개발용 Ollama로 쟀다면 ollama ps의 CPU/GPU 분할부터, 12차시).
4. 최적화 — before / after
4.1 규칙
| 규칙 | 이유 |
|---|---|
| 한 번에 하나만 바꿈 | 두 개를 바꾸면 무엇 덕인지 모름 (14차시) |
| 1.3절의 조건을 그대로 두고 바뀐 한 줄만 기록 | 측정 ID로 before/after를 짝지음 |
| 바꾼 옵션은 35차시 기동 명령 · 34차시 DR-001 에도 반영 | 40차시에 "왜 이 설정인가"를 답하려면 |
| 성능이 올라도 품질 회귀 확인 전에는 채택하지 않음 | 4.3절 |
4.2 before / after 표 — 직접 채우는 표
| # | 변경 (하나만) | 동시 10 TTFT p95 | TPOT p95 | Throughput | 최대 동시성 (기동 로그) | 분류 정확도 | 주문번호 정확도 | 지어낸 값 | 채택? |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 기준선 (35차시 설정) | — | |||||||
| 1 | 프리픽스 캐싱 켜짐 확인 · 켜기 | ||||||||
| 2 | 시스템 프롬프트 단축 (triage.v3) | ||||||||
| 3 | max_tokens 조정 | ||||||||
| 4 | --kv-cache-dtype fp8 | ||||||||
| 5 | --max-model-len 조정 | ||||||||
| 6 | (필요하면 34차시로) 가중치 양자화 변경 |
4.3 ⚠️ 품질 회귀 확인 — 평가셋을 다시 돌려야 하는 변경
| 변경 | 품질에 영향? | 다시 할 것 |
|---|---|---|
| 프리픽스 캐싱 | 계산 결과를 재사용할 뿐 — 이론상 출력은 같음 | 평가셋 가볍게 (일부) |
--max-num-seqs · --max-num-batched-tokens | 스케줄링만 바뀜. 수치상 미세한 차이는 가능 | 평가셋 가볍게 |
--kv-cache-dtype fp8 | ⚠️ 앞 토큰들의 K · V 정밀도가 낮아짐 | 평가셋 전체 |
| 가중치 양자화 변경 | ⚠️ 모델 자체가 달라짐 (34차시) | 평가셋 전체 + 34차시 매트릭스 갱신 |
| 프롬프트 단축 | ⚠️ 지시 내용이 바뀜 (36차시) | 평가셋 전체 |
--max-model-len ↓ | 긴 메일이 잘림 | 평가셋 긴 메일 묶음 |
max_tokens ↓ | 초안이 잘림 | finish_reason: length 비율 확인 |
⭐ README 7장 문장 2 — "품질 · 속도 · 메모리는 셋 다 가질 수 없다. 측정해서 고른다." — 를 실제로 하는 자리가 이 표입니다. TTFT가 30% 줄었는데 주문번호 정확도가 하한(95%) 아래로 떨어졌다면, 그 최적화는 채택하지 않습니다. 34차시의 순서(품질 하한이 먼저)가 최적화에서도 그대로입니다.
💡 회귀(Regression) — 예전에 잘 되던 것이 새 변경 때문에 다시 망가지는 현상 — 가 LLM 서비스에서는 "설정 하나 바꿨더니 분류 정확도가 떨어졌다" 로 나타납니다. 사람은 안 건드린 곳을 다시 시험하지 않으니, 프롬프트 · 설정이 바뀔 때마다 평가셋 실행이 자동으로 돌게 하는 것이 다음 단계입니다. 코드가 바뀔 때마다 자동으로 시험을 돌리는 이런 장치를 CI (지속적 통합)라고 부릅니다(40차시 개선사항).
5. 실습 — 맡기고 검수하기
지시
문의봇의 성능 테스트와 최적화를 해줘.
목표: 1.2절 시나리오 혼합으로 동시성 1/2/5/10/15/20 부하 테스트를 vLLM 직접과 앱 경유 두 구간에서 실행하고,
33차시 SLO(동시 10명 TTFT p95 < 1초, 오류 0건) 통과 여부를 판정해.
실패하면 3.1절 순서(TTFT/TPOT → KV·대기열 → 앱 구간)로 병목을 판정하고, 4.2절 변경을 하나씩 적용해.
맥락: 35차시 vLLM 서비스(기동 옵션 첨부), 37·38차시 앱 서버. 입력은 eval_v1.jsonl의 메일에서 뽑아.
vLLM 벤치마크 도구는 설치된 버전의 문서 기준으로.
제약:
- 워밍업 요청은 버리고, temperature 0, max_tokens 고정. 1.3절 측정 조건 블록을 측정마다 남겨.
- 한 번에 옵션 하나만 바꿔. 서비스 재시작이 필요하면 바꿀 옵션을 먼저 보여줘.
- kv-cache-dtype, 양자화, 프롬프트, max-model-len을 바꾸면 반드시 평가셋을 다시 돌려 품질 칸을 채워.
- 성능이 올라도 품질 하한(분류 90%, 주문번호 95%, 지어낸 값 0건)을 깨면 '미채택'으로 표시해.
- 추측하지 말고 실행 명령, 벤치마크 출력 원문, /metrics 발췌를 붙여.
보고 형식:
1) 2.2절 부하 테스트 표 (직접 / 앱 경유 두 벌)
2) 병목 판정: [질문 | 본 지표 | 값 | 판정]
3) 4.2절 before/after 표 + 각 행의 측정 ID
받을 증거
- 측정마다 측정 조건 블록 (1.3절)과 실행 명령 · 출력 원문
/metrics발췌 (대기 요청 수 · KV 사용률 · 선점),nvidia-smi로그- before/after 각 행의 평가셋 실행 결과
내가 직접 볼 것
| 볼 것 | 이걸로 아는 것 / 왜 내가 보는가 |
|---|---|
| 동시성 10 줄의 TTFT p95 와 SLO | 판정은 내가 합니다. "대체로 빠릅니다"는 판정이 아닙니다 |
| Throughput이 평탄해지는 동시성 | 상담원이 늘 때 장비를 늘려야 하는 지점 |
| 직접 vs 앱 경유의 차이 | 차이가 크면 모델을 건드릴 필요가 없습니다 |
| 채택한 변경마다 품질 칸이 채워져 있는가 | 빈칸이면 품질 회귀를 모른 채 채택한 것 |
| 측정 조건 블록의 vLLM 버전 · 기동 옵션 | 40차시 발표에서 "어떤 조건에서의 숫자인가"를 답해야 합니다 |
| 상담원 화면에서 직접 10명 흉내 (창 여러 개) | 숫자와 체감이 크게 다르면 측정 시나리오가 실제와 다른 것 |
산출물 체크리스트 (39차시)
- 측정 계획 (시나리오 S1~S4, 혼합 비율, 입력 출처)
- 측정 조건 블록 (측정 ID별)
- 부하 테스트 표 (동시성 6단계 × 직접 / 앱 경유)
- SLO 판정 (NFR-01 통과 / 미달, 깨지는 동시성)
- 병목 판정표 (3.1절 세 질문)
- before/after 표 (한 행 한 변경, 품질 칸 포함)
- 갱신된 기동 명령 · DR-001 (채택한 변경 반영)
부록 A. 스스로 확인하기
| # | 질문 | 답이 있는 곳 |
|---|---|---|
| 1 | TTFT와 TPOT는 상담원에게 각각 어떤 체감인가? | 1.1 |
| 2 | 짧은 인사 프롬프트 하나로만 측정하면 안 되는 이유는? | 1.2 |
| 3 | 측정 조건 블록에 꼭 들어갈 항목 다섯 가지는? | 1.3 |
| 4 | 동시성을 단계별로 올리는 이유는? | 2.1 |
| 5 | Throughput이 평탄해진 뒤 동시성을 더 올리면 무엇이 생기나? | 2.3 |
| 6 | KV 캐시가 찼는지를 nvidia-smi로 알 수 없는 이유는? | 2.4 |
| 7 | 병목 판정의 세 질문을 순서대로 말하면? | 3.1 |
| 8 | 앱 서버가 병목일 때 손볼 곳과 대가는? | 3.2 |
| 9 | 평가셋을 전체 다시 돌려야 하는 변경 세 가지는? | 4.3 |
| 10 | 성능이 올랐는데 채택하지 않는 경우는? | 4.3 |
부록 B. 자주 하는 실수
| 실수 | 바로잡기 |
|---|---|
| "동시 10명만 재면 된다" | ⚠️ 통과 여부만 압니다. 단계별로 올려 여유를 봄 |
| "nvidia-smi가 꽉 찼으니 KV 부족" | ❌ 기동 때 확보한 것. KV 사용률은 /metrics |
| "느리니까 모델을 더 작게" | ❌ 먼저 구간 판정 . 앱 서버가 원인이면 모델을 건드릴 이유가 없음 |
| "옵션 세 개를 바꿨더니 빨라졌다" | ❌ 무엇 덕인지 모름. 하나씩 |
| "kv fp8로 빨라졌으니 채택" | ⚠️ 평가셋 전체 재실행 후에 |
| "평균 TTFT 0.6초, 통과" | ⚠️ SLO는 p95 . 평균은 느린 상담원을 숨깁니다 |
| "숫자만 표에 적으면 된다" | ❌ 측정 조건 이 없으면 비교도 발표도 못 합니다 |
부록 C. NotebookLM 복습 프롬프트
이 39차시 노트만 근거로 6문제를 내줘. 비전공자 기준으로.
"한 건의 시간과 전체 처리량", "병목 판정의 세 질문", "품질 회귀 확인이 필요한 변경", "측정 조건 기록"은 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.
LangTailor