목표와 현재 상태
동일한 single-node k3s와 RTX 4090에서 vLLM과 Ollama를 각각 단독 실행해 model 준비 시간, API 응답 시간, GPU 사용량을 비교한다.
| 구분 | 상태 | 확인 내용 |
|---|---|---|
| vLLM warm-cache 재기동 | 완료 | Pod Ready까지 188초 |
| vLLM API 5회 | 완료 | HTTP 200 5회 |
| vLLM·GPU metric | 완료 | 요청 종료 후 상태와 DCGM 시점값 확인 |
| Ollama 단독 배포·측정 | 완료 | HTTP 200 5회, native timing, GPU 적재와 DCGM 시점값 확인 |
| vLLM 복구 | 완료 | rollout 성공, /health와 /v1/models 정상 확인 |
| 최종 engine 선택 | 보류 | latency와 GPU memory 중 우선순위를 정한 뒤 결정 |
1. vLLM 최적화
기존 gemma4-model-cache PVC를 유지한 상태에서 Deployment를 재기동했다.
SECONDS=0
sudo k3s kubectl -n gemmaforge rollout restart deployment/gemma4-12b
sudo k3s kubectl -n gemmaforge rollout status \
deployment/gemma4-12b \
--timeout=40m
printf 'vLLM Pod Ready까지 %s초\n' "$SECONDS"vLLM warm-cache rollout과 Ready 시간
deployment.apps/gemma4-12b restarted Waiting for deployment "gemma4-12b" rollout to finish: 0 out of 1 new replicas have been updated... Waiting for deployment "gemma4-12b" rollout to finish: 0 out of 1 new replicas have been updated... Waiting for deployment "gemma4-12b" rollout to finish: 0 out of 1 new replicas have been updated... Waiting for deployment "gemma4-12b" rollout to finish: 0 of 1 updated replicas are available... deployment "gemma4-12b" successfully rolled out vLLM Pod Ready까지 188초
새 Pod의 생성 시각, Ready 전환 시각, imageID와 restart 수를 확인했다.
sudo k3s kubectl -n gemmaforge get pod \
-l app.kubernetes.io/name=gemma4-vllm \
-o json \
| jq '.items[]
| {
pod: .metadata.name,
created: .metadata.creationTimestamp,
ready_at: ([.status.conditions[]
| select(.type == "Ready")
| .lastTransitionTime][0]),
imageID: .status.containerStatuses[0].imageID,
restarts: .status.containerStatuses[0].restartCount
}'vLLM Pod 생성·Ready 시각과 imageID
{ "pod": "gemma4-12b-5d4d7c54d5-vqbs2", "created": "2026-07-20T04:08:39Z", "ready_at": "2026-07-20T04:11:40Z", "imageID": "docker.io/vllm/vllm-openai@sha256:251eba5cc7c12fed0b75da22a9240e582b1c9e39f6fbc064f86781b963bd814f", "restarts": 0 }{ "pod": "gemma4-12b-5d4d7c54d5-vqbs2", "created": "2026-07-20T04:08:39Z", "ready_at": "2026-07-20T04:11:40Z", "imageID": "docker.io/vllm/vllm-openai@sha256:251eba5cc7c12fed0b75da22a9240e582b1c9e39f6fbc064f86781b963bd814f", "restarts": 0 }
| 항목 | 값 | 해석 |
|---|---|---|
| 명령 실행 기준 Ready 시간 | 188초 | rollout restart부터 Ready 확인까지 약 3분 8초 |
| Kubernetes 시각 차이 | 181초 | created부터 ready_at까지 약 3분 1초 |
| 두 측정값 차이 | 7초 | restart 명령 처리와 상태 관찰 구간이 포함된 차이 |
| Pod | gemma4-12b-5d4d7c54d5-vqbs2 | 새 ReplicaSet에서 생성 |
| restart | 0 | container 재시작 없이 정상 기동 |
| imageID | sha256:251eba...814f | 이후 engine 비교에서 같은 vLLM artifact 식별값으로 사용 |
동일한 Pod 정보가 두 번 전달됐으며 두 결과는 완전히 같다. 결과 해석에는 한 Pod의 단일 기동 기록으로 사용한다.
2. vLLM API 기준선
2-1. Service 연결과 model 확인
Terminal A에서 Service를 WSL host의 8100에 연결했다.
sudo k3s kubectl -n gemmaforge port-forward \
service/gemma4-vllm \
8100:8000Terminal B에서 health와 model 목록을 확인했다.
curl -fsS http://127.0.0.1:8100/health
curl -fsS http://127.0.0.1:8100/v1/models | jqvLLM model 등록 정보
{ "object": "list", "data": [ { "id": "gemma-4-12b-q4", "object": "model", "created": 1784520975, "owned_by": "vllm", "root": "google/gemma-4-12B-it-qat-w4a16-ct", "parent": null, "max_model_len": 8192, "permission": [ { "id": "modelperm-a747f75d9bc97f35", "object": "model_permission", "created": 1784520975, "allow_create_engine": false, "allow_sampling": true, "allow_logprobs": true, "allow_search_indices": false, "allow_view": true, "allow_fine_tuning": false, "organization": "*", "group": null, "is_blocking": false } ] } ] }
| key | 값 |
|---|---|
id | gemma-4-12b-q4 |
owned_by | vllm |
root | google/gemma-4-12B-it-qat-w4a16-ct |
max_model_len | 8192 |
allow_sampling | true |
allow_view | true |
allow_fine_tuning | false |
2-2. 동일 prompt 5회
재기동 후 첫 요청과 이후 warm 요청을 구분하기 위해 같은 prompt를 5회 실행했다.
for i in $(seq 1 5); do
curl -sS \
-o "/tmp/gemmaforge-06-vllm-${i}.json" \
-w "engine=vllm run=${i} http=%{http_code} total=%{time_total}s\n" \
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의 장점을 세 가지로 요약해줘."
}
],
"max_tokens": 128,
"temperature": 0
}'
donevLLM 동일 prompt 5회 응답 시간
engine=vllm run=1 http=200 total=2.846273s engine=vllm run=2 http=200 total=1.643920s engine=vllm run=3 http=200 total=1.633117s engine=vllm run=4 http=200 total=1.634878s engine=vllm run=5 http=200 total=1.629088s
응답 파일에서 model, 종료 사유와 token 수를 추렸다.
jq -s '[
.[] |
{
model,
finish_reason: .choices[0].finish_reason,
prompt_tokens: .usage.prompt_tokens,
completion_tokens: .usage.completion_tokens
}
]' /tmp/gemmaforge-06-vllm-*.jsonvLLM 응답별 종료 사유와 token 사용량
[ { "model": "gemma-4-12b-q4", "finish_reason": "length", "prompt_tokens": 26, "completion_tokens": 128 }, { "model": "gemma-4-12b-q4", "finish_reason": "length", "prompt_tokens": 26, "completion_tokens": 128 }, { "model": "gemma-4-12b-q4", "finish_reason": "length", "prompt_tokens": 26, "completion_tokens": 128 }, { "model": "gemma-4-12b-q4", "finish_reason": "length", "prompt_tokens": 26, "completion_tokens": 128 }, { "model": "gemma-4-12b-q4", "finish_reason": "length", "prompt_tokens": 26, "completion_tokens": 128 } ]
| 구분 | 결과 |
|---|---|
| HTTP 성공 | 5/5, 모두 200 |
| run 1 | 2.846273초 |
| run 2~5 평균 | 1.635251초 |
| 전체 5회 평균 | 1.877455초 |
| run 1 / warm 평균 | 약 1.74배 |
| run 1 추가 지연 | warm 평균 대비 약 74.1% |
| 요청당 prompt token | 26 |
| 요청당 completion token | 128 |
| 종료 사유 | 5회 모두 length |
첫 요청은 이후 네 요청보다 느렸고, run 2부터는 약 1.63~1.64초 범위로 안정됐다. 다만 모든 응답이 max_tokens: 128에 도달해 finish_reason: length로 끝났으므로 완전한 문장 종료 시간이라기보다 128 token 생성 조건의 성능으로 해석해야 한다.
3. vLLM과 GPU metric
3-1. vLLM 요청 상태
API 요청을 마친 직후 vLLM metric을 확인했다.
curl -fsS http://127.0.0.1:8100/metrics \
| grep -E '^vllm:(num_requests_running|num_requests_waiting|kv_cache_usage_perc|prompt_tokens_total|generation_tokens_total)'vLLM 요청·대기열·KV cache 상태
vllm:num_requests_running{engine="0",model_name="gemma-4-12b-q4"} 0.0 vllm:num_requests_waiting{engine="0",model_name="gemma-4-12b-q4"} 0.0 vllm:num_requests_waiting_by_reason{engine="0",model_name="gemma-4-12b-q4",reason="capacity"} 0.0 vllm:num_requests_waiting_by_reason{engine="0",model_name="gemma-4-12b-q4",reason="deferred"} 0.0 vllm:kv_cache_usage_perc{engine="0",model_name="gemma-4-12b-q4"} 0.0 vllm:prompt_tokens_total{engine="0",model_name="gemma-4-12b-q4"} 130.0 vllm:generation_tokens_total{engine="0",model_name="gemma-4-12b-q4"} 640.0
| metric | 값 | 해석 |
|---|---|---|
num_requests_running | 0 | 측정 시점에 실행 중인 요청 없음 |
num_requests_waiting | 0 | 대기 요청 없음 |
waiting: capacity | 0 | 처리 용량 때문에 대기한 요청 없음 |
waiting: deferred | 0 | 지연 처리된 요청 없음 |
kv_cache_usage_perc | 0 | 요청 종료 후 KV cache 사용률 0% |
prompt_tokens_total | 130 | 26 × 5, 측정 요청과 일치 |
generation_tokens_total | 640 | 128 × 5, 측정 요청과 일치 |
3-2. Prometheus DCGM 시점값
Terminal C에서 Prometheus Service를 WSL host의 9090에 연결했다.
sudo k3s kubectl -n monitoring port-forward \
service/kube-prometheus-stack-prometheus \
9090:9090Terminal B에서 VRAM 사용량, GPU 사용률과 온도를 조회했다.
curl -fsSG http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=DCGM_FI_DEV_FB_USED' \
| jq '.data.result[] | {metric: .metric, value: .value[1]}'
curl -fsSG http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=DCGM_FI_DEV_GPU_UTIL' \
| jq '.data.result[] | {metric: .metric, value: .value[1]}'
curl -fsSG http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=DCGM_FI_DEV_GPU_TEMP' \
| jq '.data.result[] | {metric: .metric, value: .value[1]}'RTX 4090 VRAM·GPU 사용률·온도
{ "metric": { "UUID": "GPU-3652ba18-b78d-828d-47ce-34ace2c0632d", "__name__": "DCGM_FI_DEV_FB_USED", "container": "exporter", "device": "nvidia0", "endpoint": "metrics", "exported_container": "vllm", "exported_namespace": "gemmaforge", "exported_pod": "gemma4-12b-5d4d7c54d5-vqbs2", "gpu": "0", "hostname": "momindesktop", "instance": "10.42.0.186:9400", "job": "dcgm-exporter", "modelName": "NVIDIA GeForce RTX 4090", "namespace": "monitoring", "pci_bus_id": "00000000:01:00.0", "pod": "dcgm-exporter-f6wzn", "service": "dcgm-exporter" }, "value": "22080" } { "metric": { "UUID": "GPU-3652ba18-b78d-828d-47ce-34ace2c0632d", "__name__": "DCGM_FI_DEV_GPU_UTIL", "container": "exporter", "device": "nvidia0", "endpoint": "metrics", "exported_container": "vllm", "exported_namespace": "gemmaforge", "exported_pod": "gemma4-12b-5d4d7c54d5-vqbs2", "gpu": "0", "hostname": "momindesktop", "instance": "10.42.0.186:9400", "job": "dcgm-exporter", "modelName": "NVIDIA GeForce RTX 4090", "namespace": "monitoring", "pci_bus_id": "00000000:01:00.0", "pod": "dcgm-exporter-f6wzn", "service": "dcgm-exporter" }, "value": "4" } { "metric": { "UUID": "GPU-3652ba18-b78d-828d-47ce-34ace2c0632d", "__name__": "DCGM_FI_DEV_GPU_TEMP", "container": "exporter", "device": "nvidia0", "endpoint": "metrics", "exported_container": "vllm", "exported_namespace": "gemmaforge", "exported_pod": "gemma4-12b-5d4d7c54d5-vqbs2", "gpu": "0", "hostname": "momindesktop", "instance": "10.42.0.186:9400", "job": "dcgm-exporter", "modelName": "NVIDIA GeForce RTX 4090", "namespace": "monitoring", "pci_bus_id": "00000000:01:00.0", "pod": "dcgm-exporter-f6wzn", "service": "dcgm-exporter" }, "value": "47" }
| 항목 | 값 | 해석 |
|---|---|---|
| VRAM 사용량 | 22,080 MiB | RTX 4090 24,564 MiB의 약 89.9% |
| 계산상 잔여 VRAM | 약 2,484 MiB | 동일 GPU에 다른 대형 model을 함께 적재하기 어려운 수준 |
| GPU 사용률 | 4% | 요청이 끝난 뒤 조회한 시점값 |
| GPU 온도 | 47°C | 요청이 끝난 뒤 조회한 시점값 |
이 쿼리는 instant query이므로 22,080 MiB, 4%, 47°C를 실험 구간의 최댓값으로 해석하지 않는다. Ollama와 비교할 때도 같은 시점 또는 max_over_time을 사용해야 한다.
4. Ollama 첫 배포 실패와 vLLM GPU 해제
4-1. vLLM이 실행 중인 상태에서 발생한 Scheduling 실패
Ollama Deployment를 적용할 때 기존 vLLM Pod를 먼저 종료하지 않아 두 workload가 single-node의 같은 GPU와 memory를 동시에 요청했다. Scheduler는 Ollama Pod를 배치할 수 없다고 판단했다.
Ollama Pod 자원 부족 Scheduling 이벤트
Warning FailedScheduling 4m11s default-scheduler 0/1 nodes are available: 1 Insufficient memory, 1 Insufficient nvidia.com/gpu. no new claims to deallocate, preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.
| 항목 | 값 | 해석 |
|---|---|---|
| 가용 node | 0/1 | single-node에서 Ollama를 배치할 node가 없음 |
| memory | Insufficient memory | 당시 기존 workload를 포함한 요청량 기준으로 Ollama의 memory request를 수용하지 못함 |
| GPU | Insufficient nvidia.com/gpu | 단일 nvidia.com/gpu: 1이 vLLM에 이미 할당된 상태 |
| preemption | No preemption victims found | Scheduler가 축출해 자원을 확보할 수 있는 대상이 없다고 판단 |
| 결과 | Pending | image나 container 시작 이전의 Scheduling 단계에서 중단 |
이 실패는 Ollama 자체의 GPU runtime 기동 오류가 아니라 Kubernetes Scheduler의 자원 할당 실패다. 동일한 RTX 4090 한 장으로 engine을 비교하려면 vLLM과 Ollama를 동시에 실행하지 않고, 이전 engine의 Pod 삭제와 GPU 해제를 확인한 뒤 다음 engine을 배포해야 한다.
4-2. vLLM scale-down과 해제 직후 상태
실패를 확인한 뒤 vLLM Deployment를 0 replica로 축소하고 기존 Pod 삭제를 기다렸다. 다음은 사용자가 전달한 scale-down, Pod 상태와 nvidia-smi output이다.
vLLM Pod 삭제 후 Ollama Pending·GPU 상태
deployment.apps/gemma4-12b scaled pod/gemma4-12b-5d4d7c54d5-vqbs2 condition met NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES gemma4-ollama-77f4c88844-8j5p5 0/1 Pending 0 13m <none> <none> momindesktop <none> Mon Jul 20 14:40:16 2026 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 610.53 KMD Version: 610.74 CUDA UMD Version: 13.3 | +-----------------------------------------+------------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 NVIDIA GeForce RTX 4090 On | 00000000:01:00.0 On | Off | | 0% 50C P8 40W / 480W | 3505MiB / 24564MiB | 16% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ +-----------------------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=========================================================================================| | No running processes found | +-----------------------------------------------------------------------------------------+
| 항목 | 값 | 해석 |
|---|---|---|
| vLLM scale | deployment.apps/gemma4-12b scaled | vLLM Deployment를 0 replica로 변경 |
| vLLM Pod 삭제 | condition met | 기존 gemma4-12b-5d4d7c54d5-vqbs2 삭제 조건 충족 |
| Ollama Pod | 0/1 Pending | 이 캡처 시점에는 아직 node에 배치되지 않음 |
| Ollama node | <none> | Scheduling 완료 전이므로 실제 배치 node 없음 |
| nominated node | momindesktop | Scheduler가 후보 node로 기록했지만 실제 배치를 의미하지 않음 |
| GPU memory | 3,505 / 24,564 MiB | WSL·display·driver를 포함할 수 있는 host 시점값 |
| compute process | 없음 | 이 시점에는 nvidia-smi에서 vLLM compute process가 관찰되지 않음 |
| GPU 온도 | 50°C | 자원 해제 직후 host 시점값 |
nvidia-smi의 전체 memory 사용량만으로 vLLM process가 남았다고 판단하지 않는다. 전달된 process 표에는 실행 중인 compute process가 없으므로 vLLM container의 GPU process는 해제된 상태로 본다. 다만 Ollama Pod는 여전히 Pending이므로 이 결과만으로 Ollama가 정상 기동됐다고 처리하지 않는다. 다음 Scheduler 재시도에서 Pod가 배치되는지 rollout과 새 event를 확인해야 한다.
vLLM 기준선 중간 결론
| 판단 항목 | 현재 결과 |
|---|---|
| vLLM 기동 안정성 | 새 Pod가 restart 없이 Ready |
| warm-cache Pod Ready | 188초 |
| API 성공률 | 5/5, 100% |
| 첫 요청 | 2.846273초 |
| 이후 warm 요청 | 평균 1.635251초 |
| 응답 제한 | 5회 모두 128 token에 도달해 length 종료 |
| 요청 종료 후 대기열 | running 0, waiting 0 |
| GPU memory | 시점값 22,080 MiB |
| Ollama 최초 배포 | vLLM 점유 상태에서 Insufficient memory, Insufficient nvidia.com/gpu |
| vLLM 해제 후 상태 | vLLM Pod 삭제 완료, compute process 없음 |
| engine 선택 | 비교와 vLLM 복구 완료, 최종 우선순위 결정 전까지 보류 |
vLLM은 동일 prompt 다섯 번을 모두 정상 처리했고, 첫 요청 이후 응답 시간이 안정됐다. Ollama의 첫 배포 실패를 통해 single-node·single-GPU 환경에서는 두 engine의 Deployment가 동시에 nvidia.com/gpu: 1을 요청할 수 없다는 점도 확인했다. 이 중간 결론 이후 수행한 Ollama 측정 결과는 아래 7~11절에 이어서 기록한다.
Ollama 측정 계획
vLLM 종료와 GPU compute process 해제 이후 Ollama Pod의 재스케줄과 rollout 상태 확인부터 측정을 이어가는 계획이었다. 아래 명령의 실행 결과는 7절 이후에 기록되어 있다.
sudo k3s kubectl -n gemmaforge get pod \
-l app.kubernetes.io/name=gemma4-ollama \
-o wide
sudo k3s kubectl -n gemmaforge rollout status \
deployment/gemma4-ollama \
--timeout=20m계속 Pending이면 최신 event를 다시 확인한다.
sudo k3s kubectl -n gemmaforge describe pod \
-l app.kubernetes.io/name=gemma4-ollama
sudo k3s kubectl -n gemmaforge get events \
--sort-by=.lastTimestamp \
| tail -n 50Ollama가 Running·Ready로 전환된 뒤 다음 내용을 실행 순서에 맞춰 측정했다.
- Ollama server Ready 시간과 imageID
gemma4:12b-it-q4_K_Mpull 결과와 digest- OpenAI-compatible API와 native API 5회
- DCGM 시점값
- vLLM·Ollama 중간 비교
7. Ollama server 준비 시간과 image 확인
sudo k3s kubectl -n gemmaforge get \
pod,pvc,service \
-l app.kubernetes.io/part-of=gemmaforge \
-o wideOllama Pod·PVC·Service 배포 상태
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES pod/gemma4-ollama-77f4c88844-8j5p5 1/1 Running 0 17m 10.42.0.191 momindesktop <none> <none> NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES pod/gemma4-ollama-77f4c88844-8j5p5 1/1 Running 0 17m 10.42.0.191 momindesktop <none> <none> NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE VOLUMEMODE persistentvolumeclaim/ollama-model-cache Bound pvc-db2c6501-5d05-4725-bfee-836a9f3a6941 40Gi RWO local-path <unset> 17m Filesystem NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR service/gemma4-ollama ClusterIP 10.43.242.246 <none> 11434/TCP 17m app.kubernetes.io/name=gemma4-ollama service/gemma4-vllm ClusterIP 10.43.124.212 <none> 8000/TCP 5d1h app.kubernetes.io/name=gemma4-vllm
sudo k3s kubectl -n gemmaforge get pod \
-l app.kubernetes.io/name=gemma4-ollama \
-o json \
| jq '.items[]
| {
pod: .metadata.name,
created: .metadata.creationTimestamp,
ready_at: ([.status.conditions[]
| select(.type == "Ready")
| .lastTransitionTime][0]),
image: .spec.containers[0].image,
imageID: .status.containerStatuses[0].imageID,
restarts: .status.containerStatuses[0].restartCount
}'Ollama Pod Ready 시각과 imageID
{ "pod": "gemma4-ollama-77f4c88844-8j5p5", "created": "2026-07-20T05:27:06Z", "ready_at": "2026-07-20T05:41:23Z", "image": "ollama/ollama:0.32.1", "imageID": "docker.io/ollama/ollama@sha256:6345fbc18bd73a1e16404be681dbc6fd291a027cab43ed541abe78c4c81051b0", "restarts": 0 }
| 항목 | 값 | 해석 |
|---|---|---|
| Pod 상태 | 1/1 Running | Ollama container가 정상 기동하고 readiness를 통과 |
| Pod IP·node | 10.42.0.191, momindesktop | single-node k3s에 정상 배치 |
| PVC | ollama-model-cache, 40Gi, Bound | model cache 저장 공간 연결 완료 |
| Service | gemma4-ollama:11434 | cluster 내부 Ollama API endpoint 생성 |
| 생성→Ready | 857초 | 05:27:06부터 05:41:23까지 14분 17초 |
| image | ollama/ollama:0.32.1 | 비교에 사용한 Ollama version 고정 |
| imageID | sha256:6345f...51b0 | 실제 실행 image 식별값 |
| restart | 0 | container 재시작 없이 Ready |
857초에는 앞서 발생한 vLLM GPU 점유와 Scheduler 대기 시간이 포함되어 있으므로 순수한 Ollama container 시작 시간으로 해석하지 않는다.
8. Ollama API 접속과 model pull
Terminal A에서 Ollama Service를 WSL host의 8114에 연결했다.
sudo k3s kubectl -n gemmaforge port-forward \
service/gemma4-ollama \
8114:11434Terminal B에서 server version을 확인했다.
curl -fsS http://127.0.0.1:8114/api/version | jqOllama server version
{ "version": "0.32.1" }
SECONDS=0
curl -fsS http://127.0.0.1:8114/api/pull \
-H 'Content-Type: application/json' \
-d '{
"model": "gemma4:12b-it-q4_K_M",
"stream": false
}' | jq
printf 'Ollama model pull에 %s초\n' "$SECONDS"Ollama model pull 성공과 소요 시간
{ "status": "success" } Ollama model pull에 90초
curl -fsS http://127.0.0.1:8114/api/tags \
| jq '.models[]
| select(.name == "gemma4:12b-it-q4_K_M")
| {
name,
digest,
size,
format: .details.format,
family: .details.family,
parameter_size: .details.parameter_size,
quantization_level: .details.quantization_level
}'Ollama Gemma 4 artifact 정보
{ "name": "gemma4:12b-it-q4_K_M", "digest": "4eb23ef187e2c5462566d6a1d3bbbc2f1346d0b4327cbb66d58fffbcc9b2b05c", "size": 7556508396, "format": "gguf", "family": "gemma4", "parameter_size": "11.9B", "quantization_level": "Q4_K_M" }
| 항목 | 값 | 해석 |
|---|---|---|
| Ollama version | 0.32.1 | Deployment image tag와 server 응답이 일치 |
| pull 상태 | success | model artifact 다운로드 완료 |
| pull 시간 | 90초 | Ollama cache에 artifact를 저장한 실제 소요 시간 |
| model | gemma4:12b-it-q4_K_M | 이후 API 요청에 사용할 정확한 model 이름 |
| digest | 4eb23e...b05c | 비교 결과를 재현할 model artifact 식별값 |
| size | 7,556,508,396 bytes | registry에 기록된 artifact 크기 |
| format | gguf | vLLM Compressed Tensors checkpoint와 다른 형식 |
| parameter size | 11.9B | Gemma 4 12B 계열 |
| quantization | Q4_K_M | Ollama 비교에 사용한 Q4 quantization |
model pull과 /api/tags 조회가 모두 성공해 이후 요청은 정확한 artifact 이름과 digest를 기준으로 실행됐다.
9. Ollama OpenAI-compatible API 5회 측정
for i in $(seq 1 5); do
curl -sS \
-o "/tmp/gemmaforge-06-ollama-openai-${i}.json" \
-w "engine=ollama run=${i} http=%{http_code} total=%{time_total}s\n" \
http://127.0.0.1:8114/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "gemma4:12b-it-q4_K_M",
"messages": [
{
"role": "user",
"content": "Kubernetes의 장점을 세 가지로 요약해줘."
}
],
"max_tokens": 128,
"temperature": 0
}'
doneOllama OpenAI-compatible 요청 5회 응답 시간
engine=ollama run=1 http=200 total=57.345420s engine=ollama run=2 http=200 total=1.891809s engine=ollama run=3 http=200 total=1.877050s engine=ollama run=4 http=200 total=1.839423s engine=ollama run=5 http=200 total=1.854489s
jq -s '[
.[] |
{
model,
finish_reason: .choices[0].finish_reason,
prompt_tokens: .usage.prompt_tokens,
completion_tokens: .usage.completion_tokens
}
]' /tmp/gemmaforge-06-ollama-openai-*.jsonOllama OpenAI-compatible 응답 token과 종료 사유
[ { "model": "gemma4:12b-it-q4_K_M", "finish_reason": "length", "prompt_tokens": 29, "completion_tokens": 128 }, { "model": "gemma4:12b-it-q4_K_M", "finish_reason": "length", "prompt_tokens": 29, "completion_tokens": 128 }, { "model": "gemma4:12b-it-q4_K_M", "finish_reason": "length", "prompt_tokens": 29, "completion_tokens": 128 }, { "model": "gemma4:12b-it-q4_K_M", "finish_reason": "length", "prompt_tokens": 29, "completion_tokens": 128 }, { "model": "gemma4:12b-it-q4_K_M", "finish_reason": "length", "prompt_tokens": 29, "completion_tokens": 128 } ]
| 구분 | 결과 | 해석 |
|---|---|---|
| HTTP 성공 | 5/5, 모두 200 | OpenAI-compatible endpoint가 모든 요청을 정상 처리 |
| run 1 | 57.345420초 | 최초 model load와 초기화가 포함된 cold request |
| run 2~5 평균 | 1.865693초 | model 적재 후 warm request 평균 |
| warm 최소 | 1.839423초 | run 4 |
| warm 최대 | 1.891809초 | run 2 |
| run 1 / warm 평균 | 약 30.74배 | Ollama 최초 요청에서 큰 cold-start 비용 확인 |
| 요청당 prompt token | 29 | OpenAI-compatible 응답 기준 |
| 요청당 completion token | 128 | 설정한 max_tokens까지 생성 |
| 종료 사유 | 5회 모두 length | 완전한 문장 종료가 아니라 token 제한 도달 |
Ollama도 동일 OpenAI-compatible 요청을 모두 성공했지만 첫 요청은 warm 요청보다 크게 느렸다. vLLM의 첫 요청 2.846273초와 직접 비교할 때는 Ollama run 1에 model load가 포함됐다는 차이를 함께 고려해야 한다.
10. Ollama native timing 5회 측정
curl -fsS http://127.0.0.1:8114/api/generate \
-H 'Content-Type: application/json' \
-d '{
"model": "gemma4:12b-it-q4_K_M",
"keep_alive": 0
}' | jqOllama model unload 결과
{ "model": "gemma4:12b-it-q4_K_M", "created_at": "2026-07-20T05:58:42.872671496Z", "response": "", "done": true, "done_reason": "unload" }
for i in $(seq 1 5); do
curl -fsS http://127.0.0.1:8114/api/chat \
-H 'Content-Type: application/json' \
-d '{
"model": "gemma4:12b-it-q4_K_M",
"messages": [
{
"role": "user",
"content": "Kubernetes의 장점을 세 가지로 요약해줘."
}
],
"stream": false,
"think": false,
"keep_alive": -1,
"options": {
"temperature": 0,
"num_predict": 128,
"num_ctx": 8192
}
}' > "/tmp/gemmaforge-06-ollama-native-${i}.json"
jq -r --arg run "$i" '[
$run,
(.total_duration / 1000000000),
(.load_duration / 1000000000),
.prompt_eval_count,
.eval_count,
(
if .eval_duration > 0
then (.eval_count / (.eval_duration / 1000000000))
else null
end
),
.done_reason
] | @tsv' "/tmp/gemmaforge-06-ollama-native-${i}.json"
doneOllama native timing 1차 측정
1 5.595056236 3.897096722 26 128 80.10844680986887 length 2 1.884743813 0.265624247 26 128 82.60484038550646 length 3 1.903136533 0.26893033 26 128 80.23871016273414 length 4 1.836320315 0.260310605 26 128 83.21663500533757 length 5 1.895853446 0.268437175 26 128 80.75225775111271 length
같은 input을 한 번 더 실행해 warm 상태를 재측정했다.
for i in $(seq 1 5); do
curl -fsS http://127.0.0.1:8114/api/chat \
-H 'Content-Type: application/json' \
-d '{
"model": "gemma4:12b-it-q4_K_M",
"messages": [
{
"role": "user",
"content": "Kubernetes의 장점을 세 가지로 요약해줘."
}
],
"stream": false,
"think": false,
"keep_alive": -1,
"options": {
"temperature": 0,
"num_predict": 128,
"num_ctx": 8192
}
}' > "/tmp/gemmaforge-06-ollama-native-${i}.json"
jq -r --arg run "$i" '[
$run,
(.total_duration / 1000000000),
(.load_duration / 1000000000),
.prompt_eval_count,
.eval_count,
(
if .eval_duration > 0
then (.eval_count / (.eval_duration / 1000000000))
else null
end
),
.done_reason
] | @tsv' "/tmp/gemmaforge-06-ollama-native-${i}.json"
doneOllama native timing 2차 측정
1 1.962980886 0.248694302 26 128 83.70948101429731 length 2 1.816804906 0.25833833 26 128 83.8692007396215 length 3 1.814686849 0.254275455 26 128 83.8121978177399 length 4 1.818859392 0.258434544 26 128 83.83453943727375 length 5 1.80304672 0.249589024 26 128 84.09909501489474 length
| 측정 | run 1 total | run 1 load | 이후 total | generation 처리량 | 해석 |
|---|---|---|---|---|---|
| 1차 | 5.595056초 | 3.897097초 | run 2~5 평균 1.880014초 | 약 80.11~83.22 token/s | unload 직후 첫 요청에 model load 포함 |
| 2차 | 1.962981초 | 0.248694초 | 5회 평균 1.843276초 | 약 83.71~84.10 token/s | model이 warm한 상태에서 안정적으로 반복 |
native API는 OpenAI-compatible client 전체 시간과 달리 load_duration과 generation 처리량을 분리해 보여준다. 1차 run 1에서 총 5.595056초 중 약 3.897097초가 load에 사용됐고, 이후에는 약 1.8~1.9초와 80~84 token/s 범위로 안정됐다.
11. Ollama GPU 적재와 DCGM metric 확인
curl -fsS http://127.0.0.1:8114/api/ps \
| jq '.models[]
| {
name,
size,
size_vram,
context_length,
quantization_level: .details.quantization_level
}'Ollama API 기준 model GPU 적재 정보
{ "name": "gemma4:12b-it-q4_K_M", "size": 8373969878, "size_vram": 8373969878, "context_length": 8192, "quantization_level": "Q4_K_M" }
sudo k3s kubectl -n gemmaforge exec \
deployment/gemma4-ollama \
-- ollama psOllama CLI 기준 GPU 적재 상태
NAME ID SIZE PROCESSOR CONTEXT UNTIL gemma4:12b-it-q4_K_M 4eb23ef187e2 8.4 GB 100% GPU 8192 Forever
nvidia-smi \
--query-compute-apps=pid,process_name,used_memory \
--format=csv,noheader,nounitsnvidia-smi compute process 조회 결과
콘솔에 찍히지 않음.
curl -fsSG http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=DCGM_FI_DEV_FB_USED' \
| jq '.data.result[] | {metric: .metric, value: .value[1]}'Ollama DCGM framebuffer memory 시점값
{ "metric": { "UUID": "GPU-3652ba18-b78d-828d-47ce-34ace2c0632d", "__name__": "DCGM_FI_DEV_FB_USED", "container": "exporter", "device": "nvidia0", "endpoint": "metrics", "exported_container": "ollama", "exported_namespace": "gemmaforge", "exported_pod": "gemma4-ollama-77f4c88844-8j5p5", "gpu": "0", "hostname": "momindesktop", "instance": "10.42.0.186:9400", "job": "dcgm-exporter", "modelName": "NVIDIA GeForce RTX 4090", "namespace": "monitoring", "pci_bus_id": "00000000:01:00.0", "pod": "dcgm-exporter-f6wzn", "service": "dcgm-exporter" }, "value": "12353" }
curl -fsSG http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=DCGM_FI_DEV_GPU_UTIL' \
| jq '.data.result[] | {metric: .metric, value: .value[1]}'Ollama DCGM GPU 사용률 시점값
{ "metric": { "UUID": "GPU-3652ba18-b78d-828d-47ce-34ace2c0632d", "__name__": "DCGM_FI_DEV_GPU_UTIL", "container": "exporter", "device": "nvidia0", "endpoint": "metrics", "exported_container": "ollama", "exported_namespace": "gemmaforge", "exported_pod": "gemma4-ollama-77f4c88844-8j5p5", "gpu": "0", "hostname": "momindesktop", "instance": "10.42.0.186:9400", "job": "dcgm-exporter", "modelName": "NVIDIA GeForce RTX 4090", "namespace": "monitoring", "pci_bus_id": "00000000:01:00.0", "pod": "dcgm-exporter-f6wzn", "service": "dcgm-exporter" }, "value": "13" }
curl -fsSG http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=DCGM_FI_DEV_GPU_TEMP' \
| jq '.data.result[] | {metric: .metric, value: .value[1]}'Ollama DCGM GPU 온도 시점값
{ "metric": { "UUID": "GPU-3652ba18-b78d-828d-47ce-34ace2c0632d", "__name__": "DCGM_FI_DEV_GPU_TEMP", "container": "exporter", "device": "nvidia0", "endpoint": "metrics", "exported_container": "ollama", "exported_namespace": "gemmaforge", "exported_pod": "gemma4-ollama-77f4c88844-8j5p5", "gpu": "0", "hostname": "momindesktop", "instance": "10.42.0.186:9400", "job": "dcgm-exporter", "modelName": "NVIDIA GeForce RTX 4090", "namespace": "monitoring", "pci_bus_id": "00000000:01:00.0", "pod": "dcgm-exporter-f6wzn", "service": "dcgm-exporter" }, "value": "44" }
| 항목 | 값 | 해석 |
|---|---|---|
/api/ps model size | 8,373,969,878 bytes | 실행 중인 model이 차지하는 전체 크기 |
/api/ps VRAM size | 8,373,969,878 bytes | model 전체가 GPU memory에 적재됨 |
| Ollama processor | 100% GPU | CPU fallback 없이 RTX 4090 사용 |
| context length | 8192 | vLLM의 max_model_len과 동일하게 설정 |
| quantization | Q4_K_M | 비교 대상 artifact와 일치 |
host nvidia-smi process | 출력 없음 | WSL 환경의 host process 조회에서는 container process가 표시되지 않음 |
| DCGM framebuffer memory | 12,353 MiB | Ollama Pod label로 수집된 시점값 |
| DCGM GPU utilization | 13% | 요청 이후 instant query 시점값 |
| DCGM GPU temperature | 44°C | 요청 이후 instant query 시점값 |
Ollama API와 ollama ps가 모두 model의 100% GPU 적재를 보여주고 DCGM metric도 exported_container="ollama"로 수집됐다. 따라서 nvidia-smi --query-compute-apps에 출력이 없더라도 CPU fallback으로 판단하지 않는다. 세 DCGM 값은 instant query 결과이므로 실험 구간의 최댓값이 아니라 조회 시점 상태다.
vLLM·Ollama 측정 결과 요약
| 항목 | vLLM | Ollama | 해석 |
|---|---|---|---|
| 실제 artifact | google/gemma-4-12B-it-qat-w4a16-ct | gemma4:12b-it-q4_K_M, 4eb23e...b05c | model 계열과 Q4급은 같지만 artifact format과 bytes는 다름 |
| engine imageID | sha256:251eba...814f | sha256:6345fb...51b0 | 실제 실행 image 식별값 기록 |
| Pod Ready | 188초 | 857초 | Ollama 값에는 최초 Scheduling 실패와 대기 시간이 포함됨 |
| OpenAI run 1 | 2.846273초 | 57.345420초 | Ollama 최초 요청에는 model load·초기화 비용이 크게 반영 |
| OpenAI run 2~5 평균 | 1.635251초 | 1.865693초 | 이번 단일 요청 측정에서는 vLLM이 약 0.230442초 빠름 |
| HTTP 성공 | 5/5 | 5/5 | 두 engine 모두 기능 검증 통과 |
| completion token | 요청당 128 | 요청당 128 | 두 결과 모두 length로 종료 |
| GPU framebuffer memory | 시점값 22,080 MiB | 시점값 12,353 MiB | Ollama가 약 9,727 MiB 적게 관측됨 |
| GPU utilization | 시점값 4% | 시점값 13% | 요청 종료 후 서로 다른 instant query이므로 최고 사용률 비교가 아님 |
| GPU temperature | 시점값 47°C | 시점값 44°C | 요청 종료 후 시점값 |
| restart | 0 | 0 | 두 engine 모두 측정 구간에서 container restart 없음 |
| native generation | 별도 동등 지표 없음 | 약 80~84 token/s | Ollama native API에서만 별도로 기록 |
13. Ollama 종료와 vLLM 복구
curl -fsS http://127.0.0.1:8114/api/generate \
-H 'Content-Type: application/json' \
-d '{
"model": "gemma4:12b-it-q4_K_M",
"keep_alive": 0
}' | jqOllama model unload 결과
{ "model": "gemma4:12b-it-q4_K_M", "created_at": "2026-07-20T06:27:49.548229638Z", "response": "", "done": true, "done_reason": "unload" }
sudo k3s kubectl -n gemmaforge scale \
deployment/gemma4-ollama \
--replicas=0Ollama Deployment scale down 결과
deployment.apps/gemma4-ollama scaled
sudo k3s kubectl -n gemmaforge wait \
--for=delete \
pod \
-l app.kubernetes.io/name=gemma4-ollama \
--timeout=5mOllama Pod 삭제 대기 결과
출력 없음
sudo k3s kubectl -n gemmaforge get pods -o wideOllama 종료 후 gemmaforge Pod 조회 결과
No resources found in gemmaforge namespace
nvidia-smiOllama 종료 후 GPU 해제 상태
+-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 610.53 KMD Version: 610.74 CUDA UMD Version: 13.3 | +-----------------------------------------+------------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 NVIDIA GeForce RTX 4090 On | 00000000:01:00.0 On | Off | | 0% 50C P0 83W / 480W | 3647MiB / 24564MiB | 2% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ +-----------------------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=========================================================================================| | No running processes found | +-----------------------------------------------------------------------------------------+
sudo k3s kubectl -n gemmaforge scale \
deployment/gemma4-12b \
--replicas=1vLLM Deployment scale up 결과
deployment.apps/gemma4-12b scaled
sudo k3s kubectl -n gemmaforge rollout status \
deployment/gemma4-12b \
--timeout=40mvLLM rollout 완료 결과
Waiting for deployment "gemma4-12b" rollout to finish: 0 out of 1 new replicas have been updated... Waiting for deployment "gemma4-12b" rollout to finish: 0 of 1 updated replicas are available... deployment "gemma4-12b" successfully rolled out (3m 30s)
curl -fsS http://127.0.0.1:8100/healthvLLM health endpoint 확인 결과
출력 없음
curl -f가 오류 없이 종료됐으므로 응답 본문이 없는 /health endpoint가 정상 상태 코드를 반환한 것으로 판단한다.
curl -fsS http://127.0.0.1:8100/v1/models | jq복구된 vLLM model 목록
{ "object": "list", "data": [ { "id": "gemma-4-12b-q4", "object": "model", "created": 1784529432, "owned_by": "vllm", "root": "google/gemma-4-12B-it-qat-w4a16-ct", "parent": null, "max_model_len": 8192, "permission": [ { "id": "modelperm-821f0d52e6c3a0e8", "object": "model_permission", "created": 1784529432, "allow_create_engine": false, "allow_sampling": true, "allow_logprobs": true, "allow_search_indices": false, "allow_view": true, "allow_fine_tuning": false, "organization": "*", "group": null, "is_blocking": false } ] } ] }
| 확인 항목 | 결과 | 해석 |
|---|---|---|
| Ollama unload | done_reason: unload | 실행 중이던 model을 memory에서 해제 |
| Ollama Deployment | replicas=0 scale 성공 | Ollama serving workload 종료 |
| Ollama Pod | namespace에 Pod 없음 | GPU resource claim을 보유할 Pod가 제거됨 |
| GPU 상태 | 3,647 MiB / 24,564 MiB, compute process 없음 | Ollama 종료 후 GPU가 serving workload에서 해제됨 |
| vLLM Deployment | replicas=1 scale 성공 | 기존 serving engine 재기동 |
| vLLM rollout | 3분 30초, successfully rolled out | 새 vLLM Pod가 Ready 상태에 도달 |
/health | curl -f 성공, 본문 없음 | vLLM health endpoint 정상 |
/v1/models | gemma-4-12b-q4, max_model_len=8192 | 기존 model이 다시 등록되고 API가 정상 복구됨 |
현재까지의 해석
이번 단일 요청 기준에서는 vLLM의 warm 응답이 평균 1.635251초로 Ollama의 1.865693초보다 약 12.3% 짧았다. 반면 DCGM framebuffer memory 시점값은 Ollama가 12,353 MiB로 vLLM의 22,080 MiB보다 약 44.1% 적었다.
하지만 이 결과를 순수한 serving engine 성능 차이라고 단정하지 않는다. vLLM은 Google의 Compressed Tensors QAT checkpoint를 사용했고 Ollama는 GGUF Q4_K_M artifact를 사용했으며, Ollama Pod Ready 시간에는 Scheduling 실패가 포함됐다. GPU 값도 구간 최댓값이 아닌 서로 다른 시점의 instant query다.
현재 증거로는 “warm 단일 요청 latency는 vLLM이 더 짧고, 조회 시점 GPU memory는 Ollama가 더 적다”까지 판단할 수 있다. 비교를 마친 뒤 Ollama를 정상 종료하고 vLLM rollout, health endpoint, model 목록을 모두 확인했으므로 기존 serving 상태로의 원복도 완료됐다.
남은 결정
실행 작업은 모두 끝났다. 이제 latency, GPU memory, 운영 편의성 가운데 무엇을 우선할지 정하고 최종 serving engine과 결과 해석 범위를 한 문장으로 남기면 Step 6을 마무리할 수 있다.