페이지 목차
이 페이지에서 검색
로컬 LLM
이 페이지에 묻기
로컬 LLM · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.
생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.
LOCAL · LLM
로컬 LLM
생성 수정
로컬 모델 실행과 GPU·메모리 이해부터 서빙·RAG·도구 호출·업무형 앱 구현까지 차례로 살펴봅니다.
42개 문서 · 5일차 · 36 / 42
35차시 · LLM Serving 구축
지도 위치 : 5일차 · 프로젝트 3단계 · ③추론 엔진 + ④API 층 | 이전: 34차시 LLM 모델 선정 및 최적화 | 다음: 36차시 AI 기능 구현 키워드 : vLLM 기반 모델 서버 구축, API 구성 및 테스트
이 차시를 다 읽으면
vllm serve명령의 옵션마다 34차시 결정 기록의 어느 줄이 근거인지 연결할 수 있다.- 기동 로그에서 최대 동시성 줄을 찾아 33차시의 동시 사용자 요구와 대조할 수 있다.
- 바인딩 주소 와 API 키 가 각각 무엇을 막는지, 무엇은 막지 못하는지 말할 수 있다.
- 모델 서버를 systemd 또는 Docker 로 상시 실행하는 이유와 차이를 말할 수 있다.
/health·/v1/models· 채팅 호출 · 실패 시험 으로 서버를 검수할 수 있다.- 개발 중에는 Ollama로 바꿔 끼우는 방법과 그때 달라지는 점을 말할 수 있다.
0. 이 차시의 판별 축
| # | 축 | 왼쪽 (0) | 오른쪽 (1) | 이 축으로 갈리는 것 | 큰 축 (README 5장) |
|---|---|---|---|---|---|
| 1 | 몇 명에게 제공하나 | 1인 (Ollama) | 다수 동시 (vLLM) | 운영 엔진의 선택 | ⑤ 하나 ↔ 여럿 |
| 2 | 상주하는가 | 터미널에서 띄운 프로세스 | OS · 컨테이너 엔진이 관리하는 서비스 | 재부팅 · 장애 후 다시 뜨는가 | 이 차시의 축 (5절) |
| 3 | 누구에게 열려 있나 | 127.0.0.1 (이 장비 안에서만) | 0.0.0.0 (모든 인터페이스) | 모델 서버가 사내망 · 외부에 노출되는가 | ④ 로컬 ↔ 원격 |
| 4 | 계약인가 구현인가 | OpenAI 호환 API (포트) | vLLM · Ollama (어댑터) | 엔진을 바꿔도 앱은 그대로인가 | ② 추상 ↔ 구상 |
앞에서 이어집니다 — 3일차(17~20차시)에서 vLLM 서버를 띄우고 불러 보았습니다. 35차시는 같은 일을 운영할 수 있게 합니다. 차이는 세 가지입니다. 옵션마다 근거 가 있고(34차시), 아무나 부를 수 없고 (4절), 누가 보지 않아도 계속 떠 있습니다 (5절). 앞에서 배운 Ollama와 vLLM 비교(24차시) — 같은 엔진 층, 다른 목적(1인 실행 ↔ 다수 동시 서빙) — 에서 오른쪽 칸을 고른 결과입니다.
1. 3일차를 서버 구축 절차로 회수하기
| 단계 | 하는 일 | 회수하는 앞 차시 | 이 단계의 산출물 |
|---|---|---|---|
| ① 격리 | vLLM 전용 가상환경 (버전 고정) | 3 · 18차시 | 환경 설치 기록 (vllm --version 출력) |
| ② 명령 | 옵션마다 근거를 붙여 기동 명령 결정 | 19차시, 34차시 | 기동 명령 + 근거표 (2절) |
| ③ 로그 | 기동 로그에서 메모리 · 최대 동시성 확인 | 19 · 21차시 | 로그 발췌 (3절) |
| ④ 보안 | 바인딩 주소 · API 키 | 20차시 | 보안 설정표 (4절) |
| ⑤ 상주 | systemd 또는 Docker | — | 서비스 파일 (5절) |
| ⑥ 시험 | 정상 시험 + 실패 시험 | 20차시 | API 테스트 체크리스트 (7절) |
⚠️ ①을 건너뛰면 안 됩니다. 앞에서 배운 대로 vLLM은 특정 PyTorch 버전을 고정해서 설치합니다(18차시). 36차시 이후의 앱 코드와 같은 환경에 넣으면 의존성이 서로 밀어냅니다.
2. 기동 명령 결정 — 옵션마다 34차시 근거
vllm serve <34차시 결정 기록의 모델 저장소> \
--served-model-name munuibot \
--host 127.0.0.1 --port 8000 \
--max-model-len 4096 \
--gpu-memory-utilization 0.9 \
--max-num-seqs 16 \
--api-key "$VLLM_API_KEY" \
--enable-auto-tool-choice --tool-call-parser hermes
🔍 읽는 포인트 : 첫 줄의 모델 저장소, --max-model-len, (썼다면) --kv-cache-dtype은 34차시 DR-001에서 그대로 옮겨 온 값 입니다. 여기서 새로 정하는 값이 아닙니다.
| 옵션 | 값 (문의봇) | 근거 | 값이 틀리면 |
|---|---|---|---|
| 모델 저장소 | DR-001의 결정 | 34차시 매트릭스 | 평가하지 않은 모델이 운영됨 |
--served-model-name | munuibot | 앱이 부르는 이름을 실제 모델과 분리 (축 4) | 모델을 바꿀 때마다 앱 코드도 바꿔야 함 |
--max-model-len | 4096 | DR-001 · 평가셋 최장 메일 | 크면 동시성 ↓, 작으면 긴 메일 잘림 |
--gpu-memory-utilization | 0.9 | 34차시 예산표 (24GB × 0.9) | 같은 GPU에 다른 프로세스가 있으면 기동 실패 |
--max-num-seqs | 16 | 33차시 NFR-01 (동시 10명) 이상 여유 | 너무 작으면 11번째 요청부터 대기 |
--quantization | 대개 생략 | 사전 양자화 모델은 대개 자동 인식 | 인식됐는지 기동 로그로 확인 |
--kv-cache-dtype fp8 | DR-001에서 정했을 때만 | 34차시 예산표 C안 | 품질 영향 가능 → 39차시 재확인 |
--enable-auto-tool-choice --tool-call-parser hermes | 켬 | 38차시 주문 조회 도구 | 도구 호출이 글로만 나옴. 파서 이름은 모델별로 다르니 문서 확인 |
--host · --api-key | 4절 | 33차시 NFR-02 (보안) | 4절 |
💡 프리픽스 캐싱 — 문의봇의 모든 요청은 같은 시스템 프롬프트 로 시작합니다. 앞부분 KV를 재사용하면 TTFT가 줄어듭니다(21차시). --enable-prefix-caching은 최신 버전에서 기본으로 켜져 있을 수 있으니 기동 로그에서 확인 하고, 꺼져 있다면 켭니다.
⭐ 옵션 하나를 바꾸면 결정 기록과 어긋납니다. 명령을 고치면 DR-001도 고칩니다. 그래야 40차시에 "왜 이 설정인가"를 답할 수 있습니다.
3. 기동 로그 읽기 — 최대 동시성
기동 로그에서 볼 줄은 세 개입니다. 문구와 형식은 버전에 따라 다르니 비슷한 줄을 찾습니다.
... Loading weights took ... GiB ← 가중치 메모리
... KV cache ... GiB / # GPU blocks ... ← KV 캐시에 할당된 메모리
... Maximum concurrency for 4096 tokens per request: NN.NNx ← 최대 동시성
| 로그 줄 | 이걸로 아는 것 | 대조할 곳 |
|---|---|---|
| 가중치 메모리 | 양자화가 실제로 적용됐는가 | 34차시 매트릭스의 VRAM |
| KV 캐시 할당 | 예산표의 KV 칸이 실제로 얼마인가 | 34차시 예산표 2.3절 |
| Maximum concurrency | 요청마다 max-model-len을 꽉 채워도 동시에 몇 개가 KV 캐시에 들어가는가 | 33차시 NFR-01 (동시 10명) |
⭐ 판정은 한 줄입니다. 최대 동시성 ≥ 10 이면 다음 단계로, < 10 이면 34차시로 돌아가 예산표의 칸을 다시 나눕니다.
⚠️ 이 숫자를 잘못 읽는 두 가지
| ❌ 오해 | ⭕ 실제 |
|---|---|
| "12x니까 12명까지 빠르다 " | KV 캐시에 들어간다 는 뜻일 뿐. 빠른지는 39차시에 잽니다 (한 건의 시간 ↔ 전체 처리량) |
| "12x니까 13번째 요청은 거절된다" | 실제 요청은 대개 4096보다 짧아 더 들어갑니다. 넘치면 거절이 아니라 대기 · 선점(preemption) 이 일어납니다(21차시) |
⚠️ 요청이 하나도 없는데 nvidia-smi가 거의 꽉 차 보이는 것은 정상 입니다. --gpu-memory-utilization만큼 기동할 때 미리 확보 하기 때문입니다(19차시).
4. 보안 — 바인딩 주소와 API 키
4.1 바인딩 주소
바인딩 주소 (Bind Address) — [서버 설정]인데, [ 어느 네트워크 인터페이스로 들어오는 연결을 받을지 정한다 ]는 점에서 기계 안의 창구 번호를 정하는 포트 번호와 구별된다.
| 설정 | 누가 부를 수 있나 | 문의봇에서 언제 |
|---|---|---|
--host 127.0.0.1 | 이 장비 안의 프로그램만 | 앱 서버가 같은 장비 에 있을 때 ⭕ 기본값으로 삼기 |
--host <사내 IP> | 그 인터페이스로 오는 사내망 | 앱 서버가 다른 장비 에 있을 때. 방화벽으로 앱 서버만 허용 |
--host 0.0.0.0 | 모든 인터페이스 | 컨테이너 안 에서만 (5.2절). 맨 서버에서는 ⚠️ |
앞에서 배운 OLLAMA_HOST=0.0.0.0의 위험(5차시 4절) 과 같은 문제입니다. 0.0.0.0은 "이 장비의 모든 네트워크 창구로 들어오는 연결을 받는다"는 뜻이고, 127.0.0.1은 "이 장비 자신에게서 온 연결만 받는다"는 뜻입니다. 모델 서버는 앱 서버만 부르면 됩니다. 상담원 브라우저가 모델 서버를 직접 부를 이유는 없습니다.
[상담원 브라우저] ──▶ [앱 서버 :8080] ──▶ [vLLM 127.0.0.1:8000]
(37·38차시) (밖에서는 안 보임)
4.2 API 키 — 인증은 하지만 인가는 하지 않는다
--api-key를 주면 요청 헤더 Authorization: Bearer <키>가 맞지 않을 때 401 을 돌려줍니다. 이것이 인증 의 가장 단순한 형태입니다.
| 묻는 질문 | 문의봇에서 | |
|---|---|---|
| 인증 (Authentication, AuthN) | 너는 누구냐? — 부른 쪽의 신원을 확인 | API 키가 맞는가 → 우리 앱 서버다 |
| 인가 (Authorization, AuthZ) | 너는 이것을 해도 되나? — 확인된 신원에 허락된 일인지 판정 | 상담원 A가 이 주문을 볼 권한이 있는가 |
인증이 먼저, 인가가 그다음입니다. 신원을 확인했다고 무엇이든 해도 되는 것은 아닙니다.
| API 키가 막는 것 | API 키가 막지 못하는 것 |
|---|---|
| 키를 모르는 프로그램의 호출 | 키를 아는 앱 서버가 어떤 상담원을 대신해 무엇을 부르는지 |
| 실수로 열린 포트로 들어온 호출 | 상담원 A가 상담원 B의 고객 주문을 조회하는 것 → 인가(AuthZ) , 38차시 |
⚠️ 키를 명령줄에 그대로 쓰면 셸 기록과 프로세스 목록에 남습니다. 앞에서 배운 .env 규칙(20차시 3.3) — 실제 값은 코드 저장소(Git)에 올리지 않는다 — 대로 환경 파일 에 두고 읽게 합니다.
⚠️ 어떤 경로까지 키를 요구하는지(예: /health, /metrics)는 버전에 따라 다를 수 있습니다. 추측하지 말고 7절 시험 4~5번으로 직접 확인합니다.
5. 상시 실행 — systemd 또는 Docker
상시 실행 서비스 — [프로세스 운영 방식]인데, [ 터미널을 닫거나 서버가 재부팅되거나 프로세스가 죽어도 OS나 컨테이너 엔진이 다시 띄운다 ]는 점에서 사람이 터미널에서 직접 띄운 프로세스와 구별된다.
0절 축 2의 오른쪽 칸 입니다. 리눅스에서 이 일을 하는 관리자가 systemd 입니다 — 프로그램을 "서비스"로 등록해 두면 부팅할 때 띄우고, 죽으면 다시 띄우고, 로그를 모아 줍니다. 다른 길은 Docker 입니다 — 프로그램과 그 실행 환경(라이브러리 · 설정)을 이미지 라는 한 덩어리로 묶고, 그 이미지를 실행한 것( 컨테이너 )을 Docker 엔진이 관리합니다. Ollama는 Linux 설치 스크립트가 이미 systemd 서비스로 등록 해 주었습니다(5차시). vLLM은 직접 등록합니다.
5.1 systemd
# /etc/systemd/system/munuibot-vllm.service
[Unit]
Description=munuibot vLLM server
After=network-online.target
[Service]
User=llm
EnvironmentFile=/etc/munuibot/vllm.env
ExecStart=/opt/munuibot/vllm-venv/bin/vllm serve ${MODEL_REPO} --served-model-name munuibot --host 127.0.0.1 --port 8000 --max-model-len 4096 --api-key ${VLLM_API_KEY}
Restart=on-failure
[Install]
WantedBy=multi-user.target
🔍 읽는 포인트 : EnvironmentFile (키는 파일에서), ExecStart의 가상환경 안 vllm 경로 (1절 ①), Restart=on-failure (죽으면 다시). 등록은 sudo systemctl enable --now munuibot-vllm, 로그는 journalctl -u munuibot-vllm -f.
5.2 Docker
docker run -d --name munuibot-vllm --gpus all --ipc=host \
-p 127.0.0.1:8000:8000 \
-v ~/.cache/huggingface:/root/.cache/huggingface \
--env-file vllm.env \
--restart unless-stopped \
vllm/vllm-openai:<고정한 버전 태그> \
--model <모델 저장소> --served-model-name munuibot --max-model-len 4096
🔍 읽는 포인트 : -p 127.0.0.1:8000:8000 — 컨테이너는 호스트와 네트워크가 분리된 작은 별도 컴퓨터처럼 동작합니다. 그래서 컨테이너 안 의 서버가 127.0.0.1로만 열리면 컨테이너 밖(호스트)에서 들어오는 연결조차 받지 못하므로, 안쪽은 0.0.0.0으로 열어야 합니다. 대신 호스트 쪽 은 127.0.0.1에만 묶습니다. -v — 모델 파일(수 GB)을 컨테이너 밖 디스크에 두어 다시 받지 않게 합니다(축 ① 디스크 ↔ 메모리). 이미지 인자 형식과 권장 옵션은 이미지 버전의 공식 문서로 확인 합니다.
5.3 무엇을 고르나
| systemd (가상환경 직접) | Docker | |
|---|---|---|
| 장점 | 층이 하나 적음. 로그가 journalctl로 바로 | 환경 통째로 고정 · 이동 쉬움 |
| 단점 | 서버마다 설치 반복 | GPU를 컨테이너에 넘기는 설정 필요 |
| 어울리는 곳 | 장비 한 대, 하루 프로젝트 | 같은 구성을 여러 장비에 |
⚠️ 어느 쪽이든 버전을 고정 합니다. :latest(그때그때 가장 새 버전을 가리키는 이름표)만 쓰면, 어제와 오늘 받은 이미지가 다를 수 있고 문제가 생겼을 때 되돌아갈 대상이 없습니다.
6. 개발 중엔 Ollama로 바꿔 끼우기
앞에서 배운 20차시 의 핵심 — 앱은 OpenAI 호환 API(포트) 만 알고, 뒤가 Ollama인지 vLLM인지 모른다 — 을 프로젝트에서 씁니다. GPU가 하나뿐이라 vLLM이 메모리를 다 잡고 있을 때, 노트북에서 앱만 개발할 때 유용합니다.
설정 (앱의 .env) | 개발 (Ollama) | 운영 (vLLM) |
|---|---|---|
LLM_BASE_URL | http://localhost:11434/v1 | http://127.0.0.1:8000/v1 |
LLM_MODEL | qwen3:8b (또는 Modelfile로 만든 이름) | munuibot |
LLM_API_KEY | 아무 값 | 환경 파일의 실제 키 |
⚠️ 바꿔 끼워도 같지 않은 것
| 다른 점 | 왜 | 대응 |
|---|---|---|
| 컨텍스트 길이 | Ollama 기본 num_ctx는 작게 잡혀 있을 수 있어 긴 메일이 조용히 잘림 | Modelfile PARAMETER num_ctx 설정 (13차시) |
| 양자화 형식 | GGUF ↔ AWQ 등. 품질이 다를 수 있음 | 최종 평가는 운영 형식으로 (34차시 4.2) |
| 동시 처리 | Ollama는 1인 사용 중심 | 성능 측정은 vLLM에서만 (39차시) |
| 도구 호출 형식 | 엔진 · 모델마다 지원이 다름 | 38차시 통합 시험은 운영 엔진으로 |
⭐ README 7장 문장 3 — "표준 API로 층을 나누면 엔진을 갈아 끼울 수 있다" — 의 실제 쓰임입니다. 단, 갈아 끼울 수 있다는 것은 코드가 같다 는 뜻이지 결과가 같다 는 뜻이 아닙니다.
7. 실습 — 맡기고 검수하기
지시
문의봇의 vLLM 모델 서버를 systemd 서비스로 구축하고 API 시험을 해줘.
목표: 34차시 결정 기록(DR-001, 아래 첨부)대로 vLLM 서버를 상시 실행하고, 아래 시험표를 전부 실행.
맥락: Ubuntu, RTX 4090 24GB, vLLM 전용 가상환경 /opt/munuibot/vllm-venv. vLLM 공식 문서의 serve 옵션 페이지를 기준으로.
제약:
- 바인딩은 127.0.0.1, API 키는 /etc/munuibot/vllm.env 에서 읽게. 키 값을 보고서에 쓰지 마.
- 옵션마다 DR-001의 어느 항목이 근거인지 표로 달아. 근거 없는 옵션을 추가하지 마.
- sudo가 필요한 명령(서비스 등록 등)은 실행 전에 나에게 먼저 보여줘.
- 추측하지 말고 실행 명령과 출력 원문을 붙여.
보고 형식:
1) [옵션 | 값 | 근거(DR-001 항목)] 표
2) 기동 로그 중 가중치 메모리 · KV 캐시 · Maximum concurrency 줄 원문
3) [# | 시험 | 명령 | 기대 | 실제(출력 원문 요약) | 통과] 표
받을 증거
- 서비스 파일 전문,
systemctl status출력 - 기동 로그의 Maximum concurrency 줄 원문
- 시험표 (아래) 각 행의 실행 명령과 출력
API 테스트 체크리스트
| # | 시험 | 명령 (예) | 기대 |
|---|---|---|---|
| 1 | 살아 있나 | curl -i http://127.0.0.1:8000/health | 200 |
| 2 | 모델 이름 | curl -s .../v1/models -H "Authorization: Bearer $KEY" | munuibot이 목록에 |
| 3 | 채팅 한 번 | /v1/chat/completions, 짧은 문의 하나 | content 있음, finish_reason: "stop", usage 숫자 |
| 4 | 키 없이 | 3번에서 헤더 빼기 | 401 |
| 5 | 틀린 키 | 3번에서 키 바꾸기 | 401 |
| 6 | 스트리밍 | 3번 + "stream": true, curl -N | data: {...} 조각들, 끝에 data: [DONE] |
| 7 | 너무 긴 입력 | max-model-len을 넘는 메일 | 에러 응답 (조용히 잘리지 않음) |
| 8 | 다른 장비에서 | 다른 PC에서 curl http://<서버IP>:8000/health | 연결 거부 |
| 9 | 프로세스가 죽으면 | sudo systemctl kill -s SIGKILL munuibot-vllm 후 대기 | 다시 뜸 (Restart=on-failure) |
| 10 | 재부팅 후 | 재부팅 → 1번 | 200 |
내가 직접 볼 것
| 볼 것 | 이걸로 아는 것 / 왜 내가 보는가 |
|---|---|
| Maximum concurrency 숫자 ≥ 10 | 33차시 NFR-01을 KV 캐시가 감당하는가. 미달이면 34차시로 |
| 시험 4 · 5 · 8번 이 실패해야 할 때 실패하는가 | 정상 시험만 통과한 서버는 시험을 안 한 서버와 같습니다. "막혀야 할 것이 막히는가"를 보지 않으면, 문이 열려 있어도 모릅니다 |
| 시험 7번의 응답 | 긴 메일이 잘린 채 답이 나오면 가장 위험. 에러여야 앱이 대응합니다 |
| 보고서에 API 키 값 이 없는가 | 키가 보고서 · 로그에 찍히면 그 자체가 유출입니다 |
산출물 체크리스트 (35차시)
- vLLM 전용 가상환경 +
vllm --version기록 - 기동 명령 + 옵션 근거표 (DR-001 항목과 연결)
- 기동 로그 발췌 (가중치 · KV 캐시 · 최대 동시성)
- 보안 설정: 바인딩
127.0.0.1, API 키는 환경 파일 - systemd 서비스 파일 (또는 Docker 실행 명령, 버전 고정)
- API 테스트 체크리스트 10항목 결과
- 앱용
.env두 벌 (개발 Ollama / 운영 vLLM)
부록 A. 스스로 확인하기
| # | 질문 | 답이 있는 곳 |
|---|---|---|
| 1 | --max-model-len의 값은 어디서 와야 하는가? 크거나 작으면 무엇이 생기나? | 2 |
| 2 | --served-model-name을 따로 주는 이유는? 어느 축인가? | 2 |
| 3 | Maximum concurrency 숫자로 판정하는 것과 판정하지 못하는 것은? | 3 |
| 4 | 바인딩 주소를 유와 종차로 정의하면? | 4.1 |
| 5 | API 키는 인증 · 인가 중 무엇을 하는가? 막지 못하는 것은? | 4.2 |
| 6 | Docker에서 컨테이너 안은 0.0.0.0인데 호스트 쪽을 127.0.0.1로 묶는 이유는? | 5.2 |
| 7 | 개발용 Ollama로 바꿔 끼웠을 때 달라지는 네 가지는? | 6 |
| 8 | 실패해야 통과인 시험 세 가지는? | 7 |
부록 B. 자주 하는 실수
| 실수 | 바로잡기 |
|---|---|
"일단 --host 0.0.0.0으로 열고 나중에 막자" | ❌ 열린 순간 사내망 누구나 부를 수 있습니다. 127.0.0.1부터 |
| "API 키를 걸었으니 권한 문제는 끝" | ❌ 키는 인증 뿐. 상담원별 인가 는 앱 서버(38차시) |
| "기동 로그에 12x가 나왔으니 성능 요구 충족" | ❌ 메모리에 들어간다 는 뜻. 속도는 39차시에서 |
| "nvidia-smi가 꽉 찼다, 메모리 누수다" | ⚠️ --gpu-memory-utilization만큼 미리 확보 한 것. 정상 |
"터미널에서 vllm serve 띄워 두면 된다" | ❌ 터미널을 닫거나 재부팅하면 사라집니다. 서비스로 |
| "Ollama에서 되니 vLLM에서도 똑같다" | ⚠️ 코드는 같아도 결과는 다를 수 있습니다. 컨텍스트 · 형식 · 동시성 |
| "정상 호출이 되니 시험 끝" | ❌ 키 없이 · 틀린 키 · 다른 장비 · 긴 입력의 실패 시험 까지 |
부록 C. NotebookLM 복습 프롬프트
이 35차시 노트만 근거로 6문제를 내줘. 비전공자 기준으로.
"옵션마다 34차시 근거 연결", "Maximum concurrency 읽는 법", "바인딩 주소와 API 키가 막는 것과 못 막는 것", "실패 시험"은 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.
LangTailor