목표와 최종 결과

k3s single-node cluster에 Gemma 4 12B Q4 checkpoint를 vLLM Deployment로 실행했다. Pod는 RTX 4090 1개를 할당받아 Ready 1/1 상태가 되었고, model cache는 40Gi PVC에 유지했다. ClusterIP Service를 host 8100으로 port-forward하여 model 조회, chat completion, metrics를 검증했다.

항목최종 확인값
nodemomindesktop
GPU allocatable1
Podgemma4-12b-6c5cc87ffd-s7gxn, 1/1 Running, restart 0
PVCgemma4-model-cache, Bound, 40Gi, local-path
Servicegemma4-vllm, ClusterIP, 8000/TCP
port-forwardhost 8100 → Service 8000
modelgoogle/gemma-4-12B-it-qat-w4a16-ct
served modelgemma-4-12b-q4
max model length8192
vLLM0.24.0-583965f1
API/v1/models, /v1/chat/completions, /metrics 성공

실행 명령과 manifest는 내부 Runbook에 유지한다.

1. Kubernetes resource 결과

Pod는 GPU workload로 정상 기동했고 restart 없이 Ready 1/1 상태를 유지했다. model cache PVC는 local-path StorageClass에서 40Gi volume에 Bound됐고, Service는 Pod label app.kubernetes.io/name=gemma4-vllm을 선택해 cluster 내부 8000/TCP를 노출했다. Node가 광고한 GPU resource도 1개로 확인했다.

이 결과로 Device Plugin이 nvidia.com/gpu resource를 Node에 노출했고, GPU limit을 요청한 vLLM Pod가 scheduling되어 Ready 상태까지 도달한 것을 확인했다.

2. model endpoint 검증

ClusterIP Service와 WSL localhost의 차이

처음에는 Service port를 8100으로 열면 WSL에서 localhost:8100으로 접근할 수 있을 것으로 생각했지만 요청이 실패했다. 원인은 Kubernetes ClusterIP Service의 port가 WSL host의 loopback port를 직접 여는 것이 아니기 때문이다.

구분의미
WSL 127.0.0.1:8100현재 WSL 환경의 loopback 주소와 port
Service 8000/TCPKubernetes cluster network 내부에서 사용하는 가상 Service port
kubectl port-forwardWSL local port와 Kubernetes workload 사이에 임시 터널을 생성하는 명령

따라서 Service만 생성된 상태에서 WSL의 localhost:8100을 호출하면 해당 port를 수신하는 host process가 없어 연결되지 않는다. 다음 port-forward 명령을 실행한 동안에만 kubectl이 WSL의 127.0.0.1:8100을 수신하고 vLLM workload의 8000으로 요청을 전달한다.

sudo k3s kubectl port-forward service/gemma4-vllm 8100:8000

논리적인 접근 흐름은 다음과 같다.

WSL curl 요청: 127.0.0.1:8100
  → kubectl port-forward local listener
  → Kubernetes API의 port-forward 터널
  → Service가 선택한 vLLM Pod:8000

엄밀히 말하면 port-forward 요청이 일반 Service traffic처럼 ClusterIP data path를 통과하는 것은 아니다. service/gemma4-vllm의 selector와 port 정보로 대상 Pod를 정한 뒤 그 Pod로 터널링한다. 여기서 8100은 WSL에 임시로 여는 local port이고, 8000은 Kubernetes Service가 노출한 port다. 상시 외부 접근이 필요하면 임시 port-forward 대신 NodePort, Ingress, LoadBalancer 등의 노출 방식을 별도로 구성해야 한다.

host 8100으로 port-forward한 뒤 model 목록을 조회했다.

curl -fsS http://127.0.0.1:8100/v1/models

주요 key-value를 추리면 다음과 같다.

key해석
objectlistmodel 목록 응답
data[0].idgemma-4-12b-q4API 요청에 사용하는 served model 이름
data[0].rootgoogle/gemma-4-12B-it-qat-w4a16-ctDeployment가 load한 Hugging Face checkpoint
data[0].max_model_len8192최대 model context length
data[0].owned_byvllmmodel serving backend
data[0].permission[0].allow_samplingtruesampling 요청 허용

Deployment에 지정한 checkpoint, served model, context length가 API 응답과 일치했다.

3. chat completion 검증

3-1. Kubernetes GPU 서빙 확인

한 문장으로 Kubernetes GPU 서빙 결과를 요청했다.

curl -fsS http://127.0.0.1:8100/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "gemma-4-12b-q4",
    "messages": [
      {"role": "user", "content": "Kubernetes GPU 서빙 성공이라고 한 문장으로 답해줘."}
    ],
    "max_tokens": 64,
    "temperature": 0
  }' | jq

주요 key-value를 추리면 다음과 같다.

key해석
modelgemma-4-12b-q4요청한 served model과 일치
choices[0].message.contentKubernetes GPU 서빙 성공.기대한 한 문장 응답 반환
choices[0].finish_reasonstoptoken 제한이 아닌 정상 종료
usage.prompt_tokens28입력 prompt token 수
usage.completion_tokens8생성 token 수
usage.total_tokens36전체 사용 token 수
system_fingerprintvllm-0.24.0-583965f1응답을 생성한 vLLM build 식별값

요청한 문장이 반환됐고 finish_reason: stop으로 정상 종료됐다.

3-2. 응답 길이 제한 확인

너 누구야. 요청으로 최대 생성 길이 동작도 확인했다.

curl -fsS http://127.0.0.1:8100/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "gemma-4-12b-q4",
    "messages": [
      {"role": "user", "content": "너 누구야."}
    ],
    "max_tokens": 64,
    "temperature": 0
  }' | jq

주요 key-value를 추리면 다음과 같다.

key해석
modelgemma-4-12b-q4요청한 served model과 일치
choices[0].message.content마지막 문장이 무엇에서 중단생성 길이 제한으로 응답이 완결되지 않음
choices[0].finish_reasonlength설정한 최대 생성 token에 도달
usage.prompt_tokens17입력 prompt token 수
usage.completion_tokens64입력의 max_tokens: 64와 정확히 일치
usage.total_tokens81prompt와 completion을 합한 전체 token 수

두 번째 요청은 오류나 model 중단이 아니라 max_tokens: 64 제한에 정확히 도달해 끝났다. 긴 답변이 필요하면 max_tokens128 또는 256으로 높이고, 짧은 검증 응답은 prompt에서 한 문장으로 제한한다.

4. vLLM metrics 검증

두 chat 요청 후 vLLM metrics를 조회했다.

curl -fsS http://127.0.0.1:8100/metrics | grep -E '^vllm:' | head -n 30

확인한 핵심 값:

metric해석
vllm:num_requests_running0scrape 시점에 실행 중인 요청 없음
vllm:num_requests_waiting0대기 요청 없음
vllm:kv_cache_usage_perc0요청 종료 후 KV cache 사용 없음
vllm:prefix_cache_queries_total45두 요청의 prompt token 처리량
vllm:prefix_cache_hits_total0이번 두 prompt에서 prefix cache hit 없음
vllm:num_preemptions_total0GPU memory 압박에 의한 preemption 없음
vllm:prompt_tokens_total45첫 요청 28 + 두 번째 요청 17과 일치

API response의 prompt token 합계와 Prometheus counter가 일치해 요청 처리와 metrics 노출 경로를 함께 검증했다.