목표와 현재 결과

GPU 경로가 이어진 노드 위에 vLLM을 올리고, --gpu-memory-utilization이 실제로 무엇을 제어하는지 실측했다.

먼저 작은 공개 모델로 경로만 검증한 뒤, L4 한 장을 나눠 쓰는 기준 모델로 Qwen/Qwen2.5-3B-Instruct-AWQ를 올렸다. Deployment는 1/1 Ready, /health 200까지 확인했다.

핵심 측정은 --gpu-memory-utilization0.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를 거쳐 호출했다.

kubectl -n llm port-forward deployment/vllm-qwen 8000:8000
curl -s http://127.0.0.1:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"...","messages":[{"role":"user","content":"안녕"}]}'
확인 항목결과
Pod 기동성공
OpenAI 호환 Chat Completions응답 성공

1-1. 첫 모델 시도가 gated model 인증으로 막혔다

처음 고른 모델은 Hugging Face에서 접근 승인이 필요한 gated model이었다. 토큰 없이 받으려다 401로 실패했다.

경로를 검증하는 단계에서 인증까지 변수로 끌어들인 것이 문제였다. 실패했을 때 원인이 GPU 경로인지 토큰인지 모델 접근 권한인지 갈리지 않는다.

공개 모델로 바꿔 인증 변수를 없앴다. 이 단계의 목적은 모델 성능이 아니라 driver부터 API 응답까지가 이어지는가 하나뿐이므로, 가장 작고 조건이 적은 모델이 맞다.

여기가 통과하면서 01의 계층 연결이 실제 서빙으로 확인됐다.

2. 기준 모델 Deployment 구성

경로가 확인됐으니 실제로 쓸 모델로 바꾼다. 24GB를 넷이 나눠 쓴다는 전제에서 파라미터를 최대한 키운 선택이 3B AWQ다.

manifest는 노드의 작업 디렉터리에서 관리한다.

~/k3s-gpu-cluster/engine/vllm.yaml
설정
namespacellm
Deploymentvllm-qwen
imagevllm/vllm-openai:latest
modelQwen/Qwen2.5-3B-Instruct-AWQ
served model nameqwen2.5-3b
GPU resourcenvidia.com/gpu.shared: 1
--gpu-memory-utilization0.40
--max-model-len4096
--max-num-seqs16
model cachehostPath /opt/hf-cache
shared memorymemory emptyDir 2Gi
Pod labelapp=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,pod
kubectl -n llm logs deployment/vllm-qwen
key확인값해석
Deployment1/1 Ready
PodRunning, restart 0기동 재시도 없음
vLLM0.26.0
모델 아키텍처Qwen2ForCausalLM
quantizationauto_awqAWQ 4-bit가 인식됨
/health200

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
구간첫 번째 EngineCore두 번째 EngineCore합계
0.204,792 MiB4,648 MiB9,440 MiB
0.404,792 MiB9,184 MiB13,976 MiB
변화0 MiB+4,536 MiB+4,536 MiB
파생
장치 전체23,034 MiB
0.40 이론값9,214 MiB
실제 두 번째 EngineCore9,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으로 장치 용량을 넘는다.

0.40 × 4 = 1.60   →  불가능
0.22 × 4 = 0.88   →  4분할 계산의 출발점

그런데 이 상태로 replica를 늘려도 스케줄러는 막지 않는다. slot이 넷 남아 있기 때문이다. 실제로 어떤 실패가 나타나는지는 05에서 확인한다.

5. 확인된 범위

항목결과
공개 모델 경로 검증확인, Chat Completions 응답 성공
3B AWQ Deployment확인, 1/1 Ready, restart 0
AWQ 인식확인, auto_awq
/health확인, 200
--gpu-memory-utilization의 의미확인, 장치 메모리 대비 예산 비율
설정값과 실측 일치확인, 이론값과 0.3% 차이

아래는 이 단계에서 확인하지 않았다.

- 3B AWQ의 /v1/models와 Chat Completions 응답
- 예산 안에서 weight와 KV cache가 각각 얼마를 쓰는지
- 네 인스턴스를 실제로 올렸을 때의 동작
- Deployment 구성과 기동 확인 명령의 원문 출력

두 번째 항목은 05에서 vLLM 기동 로그의 memory 배분을 읽으며 확인한다. 예산 8.81 GiB 중 weight가 1.95 GiB, 나머지 대부분이 KV cache로 간다.