GPU 경로가 이어진 노드 위에 vLLM을 올리고, --gpu-memory-utilization이 실제로 무엇을 제어하는지 실측했다.
먼저 작은 공개 모델로 경로만 검증한 뒤, L4 한 장을 나눠 쓰는 기준 모델로 Qwen/Qwen2.5-3B-Instruct-AWQ를 올렸다. Deployment는 1/1 Ready, /health 200까지 확인했다.
핵심 측정은 --gpu-memory-utilization을 0.20에서 0.40으로 올렸을 때다. 해당 EngineCore의 GPU memory가 4,648 MiB에서 9,184 MiB로 늘었고, 이는 장치 23,034 MiB의 40%인 9,214 MiB와 거의 일치한다. 이 값은 GPU 연산 사용률이 아니라 이 인스턴스가 잡을 메모리 예산이다.
01에서 확인한 gpu.shared: 4가 메모리를 나누지 않으므로, 4분할을 강제하는 유일한 수단이 이 옵션이다. 그런데 이번에 쓴 0.40은 4분할용 값이 아니다. 단일 인스턴스의 KV cache를 늘리려고 올린 값이고, 이 상태로 replica를 늘리면 어떻게 되는지는 05에서 드러난다.
이 장의 증거 수준
memory 실측은 nvidia-smi process table 원문을 남겼다. Deployment 구성과 기동 확인은 값만 기록했고 명령 원문 출력은 남기지 않았다.
1. 공개 모델로 경로부터 검증
기준 모델을 바로 올리지 않는다. 모델이 크면 실패했을 때 원인이 GPU 경로인지 모델 자체인지 구분하기 어렵다. 작은 공개 모델로 경로만 먼저 확인한다.
Qwen/Qwen2.5-0.5B-Instruct로 Deployment를 만들고 port-forward를 거쳐 호출했다.
처음 고른 모델은 Hugging Face에서 접근 승인이 필요한 gated model이었다. 토큰 없이 받으려다 401로 실패했다.
경로를 검증하는 단계에서 인증까지 변수로 끌어들인 것이 문제였다. 실패했을 때 원인이 GPU 경로인지 토큰인지 모델 접근 권한인지 갈리지 않는다.
공개 모델로 바꿔 인증 변수를 없앴다. 이 단계의 목적은 모델 성능이 아니라 driver부터 API 응답까지가 이어지는가 하나뿐이므로, 가장 작고 조건이 적은 모델이 맞다.
여기가 통과하면서 01의 계층 연결이 실제 서빙으로 확인됐다.
2. 기준 모델 Deployment 구성
경로가 확인됐으니 실제로 쓸 모델로 바꾼다. 24GB를 넷이 나눠 쓴다는 전제에서 파라미터를 최대한 키운 선택이 3B AWQ다.
manifest는 노드의 작업 디렉터리에서 관리한다.
~/k3s-gpu-cluster/engine/vllm.yaml
설정
값
namespace
llm
Deployment
vllm-qwen
image
vllm/vllm-openai:latest
model
Qwen/Qwen2.5-3B-Instruct-AWQ
served model name
qwen2.5-3b
GPU resource
nvidia.com/gpu.shared: 1
--gpu-memory-utilization
0.40
--max-model-len
4096
--max-num-seqs
16
model cache
hostPath /opt/hf-cache
shared memory
memory emptyDir 2Gi
Pod label
app=vllm, version=ver-0
자원 요청이 nvidia.com/gpu.shared: 1인 것에 주의한다. 01에서 Device Plugin이 광고한 이름이 gpu.shared이므로 nvidia.com/gpu로 요청하면 이 노드에서 스케줄되지 않는다.
model cache를 hostPath로 둔 것은 Pod가 재생성돼도 weight를 다시 내려받지 않기 위해서다. 이 선택의 효과는 05에서 복구 시간을 잴 때 숫자로 나온다.
--max-num-seqs=16은 동시에 처리할 시퀀스 수의 상한이다. 이 값이 04의 부하 시험에서 병목으로 드러난다.
3. 기동 확인
kubectl -n llm get deployment,podkubectl -n llm logs deployment/vllm-qwen
key
확인값
해석
Deployment
1/1 Ready
Pod
Running, restart 0
기동 재시도 없음
vLLM
0.26.0
모델 아키텍처
Qwen2ForCausalLM
quantization
auto_awq
AWQ 4-bit가 인식됨
/health
200
auto_awq 인식이 확인 포인트다. AWQ 체크포인트를 올려도 엔진이 양자화를 인식하지 못하면 weight가 그대로 적재되어 메모리 계산이 어긋난다.
--model= 형식은 제거 예정이다
기동 로그에 --model 옵션이 향후 제거된다는 경고가 함께 나온다. 현재 manifest는 --model=... 결합 형식을 쓰고 있어 지금은 동작하지만, 다음 수정에서 positional argument 형식으로 옮기는 것이 좋다.
옵션 형식을 바꿀 때는 vllm serve --help로 실제 형식을 확인하고 바꾼다. 이 manifest의 args 형식 때문에 05에서 patch 대상 index를 잘못 짚는 일이 실제로 발생한다.
이 시점에서 확인된 것은 엔진이 떴다는 것까지다. 3B AWQ의 /v1/models와 Chat Completions 응답은 03에서 Service를 만든 뒤에 검증한다.
4. GPU memory 예산 실측
--gpu-memory-utilization이라는 이름은 GPU 사용률처럼 읽힌다. 실제로 무엇을 제어하는지 값을 바꿔가며 확인했다.
이 측정 시점에는 이전 Deployment vllm-blue가 아직 함께 떠 있었다. 그래서 process table에 EngineCore가 둘 보인다. 두 프로세스가 같은 GPU를 공유하는 상황이라 오히려 비교에 편했다. 값을 바꾼 쪽만 변하는지 볼 수 있기 때문이다.
nvidia-smi
변경 전 — 0.20
| 0 N/A N/A 5124 C VLLM::EngineCore 4792MiB || 0 N/A N/A 7596 C VLLM::EngineCore 4648MiB |
변경 후 — 0.40
| 0 N/A N/A 5124 C VLLM::EngineCore 4792MiB || 0 N/A N/A 10452 C VLLM::EngineCore 9184MiB |
구간
첫 번째 EngineCore
두 번째 EngineCore
합계
0.20
4,792 MiB
4,648 MiB
9,440 MiB
0.40
4,792 MiB
9,184 MiB
13,976 MiB
변화
0 MiB
+4,536 MiB
+4,536 MiB
파생
값
장치 전체
23,034 MiB
0.40 이론값
9,214 MiB
실제 두 번째 EngineCore
9,184 MiB
이론값과의 차이
30 MiB, 약 0.3%
두 프로세스 합계 점유율
41.0% → 60.7%
4-1. 판독
설정값이 그대로 메모리 예산이 됐다.0.40 × 23,034 = 9,214 MiB이고 실측이 9,184 MiB다. 차이는 0.3%로, 이 옵션이 장치 전체 메모리에 대한 비율로 예산을 잡는다는 것이 수치로 확인된다.
바뀐 쪽만 바뀌었다는 것도 근거다. 첫 번째 EngineCore는 4,792 MiB 그대로이고 두 번째만 늘었다. PID도 7596에서 10452로 바뀌었으므로, argument 변경이 그 Deployment의 EngineCore만 재생성했다는 해석이 성립한다.
오해
실제
GPU 연산을 40% 쓴다
연산과 무관하다. 메모리 예산이다
나머지 60%는 다른 Pod 몫으로 남는다
남는 것은 맞지만 아무도 지켜주지 않는다
값을 안 주면 알아서 나눠 쓴다
기본값이면 첫 인스턴스가 대부분을 점유한다
세 번째가 이 프로젝트의 설계 제약으로 이어진다. 01에서 본 대로 gpu.shared: 4는 메모리를 나누지 않으므로, 넷이 함께 올라가게 하려면 각 인스턴스가 스스로 예산을 지켜야 한다. 그 강제 수단이 이 옵션 하나뿐이다.
4-2. 0.40은 4분할용 값이 아니다
이번에 쓴 0.40은 단일 인스턴스의 KV cache를 늘리려고 올린 값이다. 넷이 동시에 이 값을 쓰면 0.40 × 4 = 1.6으로 장치 용량을 넘는다.