Pod를 강제로 삭제했을 때 Deployment가 스스로 복구하는지, 그리고 잘못된 설정을 배포한 뒤 이전 revision으로 되돌릴 수 있는지 확인했다.
Pod 삭제부터 rollout 완료까지 101초가 걸렸고 restart 없이 1/1 Running에 도달했다. 기동 로그의 Loading weights took 0.77 seconds는 모델을 다시 내려받지 않고 hostPath cache를 재사용했다는 뜻이다.
같은 로그에서 vLLM이 보고한 memory 배분을 확보했다. weight 1.95 GiB, KV cache 6.63 GiB, KV cache 용량 193,216 토큰이다. 이 값으로 나눈 토큰당 KV 비용이 약 36 KiB로, 모델 선정 단계에서 계산했던 값과 일치했다.
rollout 실패 감지와 rollout undo 복구도 확인했다. 다만 주입한 결함의 종류는 의도와 달랐다. args가 --옵션=값 결합 형식이라 지정한 index가 모델이 아닌 다음 옵션을 덮었고, 그 결과 모델 적재 실패가 아니라 argument 파싱 실패로 CrashLoopBackOff가 됐다.
replica 확장에서는 예상과 다른 실패 양상이 나왔다. --gpu-memory-utilization=0.60인 상태로 replica를 2로 늘리자 두 번째 Pod가 Pending이 되지 않고 정상적으로 노드에 배정된 뒤 Startup probe failed: connection refused를 반복했다. 스케줄러는 nvidia.com/gpu.shared slot 개수만 보고 GPU memory는 모르기 때문이다. 예산을 0.22로 낮춘 뒤에는 두 replica가 모두 1/1 Running이 됐다.
같은 조건으로 replica 1개와 2개의 응답 시간을 비교했다. 평균은 2.215초와 2.427초로 replica 1개 쪽이 빨랐고, 16건 전체 소요도 1개가 3.39초 짧았다. time-slicing으로 같은 GPU를 나눠 쓰므로 replica 확장이 처리량을 늘리지 못한다는 방향이 구조적 이유와 함께 확인됐다. 다만 각 구성을 한 번씩만 측정했으므로 차이의 크기와 응답 시간 분포 차이는 확정하지 않는다.
1. Pod 재생성과 복구 시간
복구 시간을 재려면 삭제 시각과 rollout 완료 시각을 함께 잡아야 한다.
OLD_POD=$(kubectl -n llm get pod -l app=vllm,version=ver-0 \ -o jsonpath='{.items[0].metadata.name}')echo "old=$OLD_POD"START=$(date +%s)kubectl -n llm delete pod "$OLD_POD"
Pod 삭제 결과
old=vllm-qwen-865f7b887b-jpl7gpod "vllm-qwen-865f7b887b-jpl7g" deleted from llm namespace
Waiting for deployment "vllm-qwen" rollout to finish: 0 of 1 updated replicas are available...deployment "vllm-qwen" successfully rolled outrecovery_seconds=101
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATESpod/vllm-qwen-865f7b887b-7lg62 1/1 Running 0 2m7s 10.42.0.58 gpu-k8s <none> <none>(APIServer pid=1) Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.(EngineCore pid=128) INFO 08-04 01:43:36 [core.py:116] Initializing a V1 LLM engine (v0.26.0) with config: model='Qwen/Qwen2.5-3B-Instruct-AWQ', ... quantization=auto_awq, ... max_seq_len=4096, ... served_model_name=qwen2.5-3b, enable_prefix_caching=True, enable_chunked_prefill=True, ...(EngineCore pid=128) INFO 08-04 01:43:37 [model_runner.py:284] Loading model from scratch...(EngineCore pid=128) Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.Loading safetensors checkpoint shards: 0% Completed | 0/1 [00:00<?, ?it/s]Loading safetensors checkpoint shards: 100% Completed | 1/1 [00:00<00:00, 1.32it/s](EngineCore pid=128) INFO 08-04 01:43:40 [default_loader.py:430] Loading weights took 0.77 seconds(EngineCore pid=128) INFO 08-04 01:43:42 [model_runner.py:305] Model loading took 1.95 GiB and 4.586994 seconds(EngineCore pid=128) INFO 08-04 01:43:46 [gpu_worker.py:560] Available KV cache memory: 6.63 GiB(EngineCore pid=128) INFO 08-04 01:43:46 [kv_cache_utils.py:2177] GPU KV cache size: 193,216 tokens(EngineCore pid=128) INFO 08-04 01:43:56 [core.py:347] init engine (profile, create kv cache, warmup model) took 14.06 s
key
값
해석
새 Pod
vllm-qwen-865f7b887b-7lg62
1/1 Running, restart 0
IP
10.42.0.58
이전 Pod와 다른 주소
ReplicaSet hash
865f7b887b
삭제한 Pod와 동일. 설정 변경 없이 보충만 발생
weight 적재
0.77초
다운로드 없이 로컬 cache에서 읽음
모델 적재 전체
4.59초
weight 읽기와 GPU 배치 포함
engine 초기화
14.06초
KV cache 생성과 warmup 포함
Loading weights took 0.77 seconds가 cache 재사용의 직접 근거다. 로그에 다운로드 진행 표시가 없고, 3B AWQ weight 약 2GiB를 네트워크로 받았다면 이 시간에 끝날 수 없다. HF Hub 경고는 인증 없이 접속했다는 안내일 뿐 실제 다운로드가 일어났다는 뜻이 아니다.
ReplicaSet hash가 삭제 전과 같은 865f7b887b인 것도 확인 포인트다. 설정이 바뀌지 않았으므로 새 ReplicaSet이 생기지 않고 기존 ReplicaSet이 replica만 보충했다.
1-2. 복구 시간의 구성
101초 중 로그에서 설명되는 구간은 일부다.
구간
시간
근거
weight 적재
0.77초
Loading weights took
모델 적재 전체
4.59초
Model loading took
engine 초기화
14.06초
init engine ... took
로그로 설명되는 합계
약 19초
위 세 구간
나머지
약 82초
Pod 종료, 스케줄링, 컨테이너 기동, probe 감지
복구 시간의 대부분이 모델 적재가 아니라 그 앞뒤에 있다. 모델이 3B AWQ로 작고 cache까지 재사용되기 때문에 적재 자체는 20초 안에 끝난다. 나머지 82초의 내역을 더 쪼개려면 Pod 이벤트 타임스탬프와 probe 설정을 함께 봐야 하며, 이번 측정에서는 확인하지 않았다.
1-3. vLLM이 보고한 memory 배분
같은 로그에 GPU memory 배분이 그대로 남는다.
gpu_worker의 memory 배분 로그
(EngineCore pid=128) INFO 08-04 01:43:46 [gpu_worker.py:857] Free memory on device (21.85/22.04 GiB) on startup. Desired GPU memory utilization is (0.4, 8.81 GiB). Actual usage is 1.95 GiB for weight, 0.16 GiB for peak activation, 0.06 GiB for non-torch memory, and 0.0 GiB for CUDAGraph memory. Replace gpu_memory_utilization config with `--kv-cache-memory=6965470413` (6.49 GiB) to fit into requested memory, or `--kv-cache-memory=20958795776` (19.52 GiB) to fully utilize gpu memory. Current kv cache memory in use is 6.63 GiB.
key
값
해석
장치 전체
22.04 GiB
기동 시점 인식된 GPU memory
기동 시 여유
21.85 GiB
다른 workload 없음
--gpu-memory-utilization 목표
0.4 → 8.81 GiB
이 instance에 허용한 예산
weight
1.95 GiB
AWQ 4-bit 3B 실측
peak activation
0.16 GiB
non-torch
0.06 GiB
KV cache
6.63 GiB
예산에서 위 항목을 뺀 나머지
KV cache 용량
193,216 토큰
네 항목의 합은 8.80 GiB로 목표 예산 8.81 GiB와 일치한다. --gpu-memory-utilization이 GPU 연산 사용률이 아니라 이 instance가 잡을 memory 예산이라는 점이 수치로 확인된다.
이 patch가 실제로 덮은 것은 모델 값이 아니라 index 1의 --served-model-name=qwen2.5-3b다. 결합 형식이므로 모델은 index 0 안에 옵션과 함께 들어 있고, 배열에 모델 값만 담긴 원소는 존재하지 않는다.
구분
값
의도한 결함
존재하지 않는 모델 이름
실제 주입된 결함
옵션 형식이 아닌 문자열 Qwen/does-not-exist
덮어쓴 값
--served-model-name=qwen2.5-3b
결과
모델 적재 실패가 아니라 argument 파싱 실패
args index는 배열 형식에 따라 가리키는 대상이 달라진다
--옵션 값 분리 형식이라면 모델 값이 index 1에 오지만, --옵션=값 결합 형식에서는 index 1이 그다음 옵션이다. 같은 index를 지정해도 어떤 배열이냐에 따라 전혀 다른 인자를 덮는다. patch 전에 조회한 배열을 그대로 세어 대상 index를 확인해야 한다.
결함의 종류는 달라졌지만 이 절의 목적은 충족했다. 확인하려던 것은 “잘못된 설정이 배포됐을 때 rollout이 실패로 감지되고 rollback으로 되돌아오는가”이며, 두 동작 모두 아래에서 관측된다. 다만 “존재하지 않는 모델을 지정했을 때 모델 적재 단계에서 어떻게 실패하는가”는 이번에 확인되지 않았다.
rollout 상태와 Pod, 로그를 확인했다.
kubectl -n llm rollout status deployment/vllm-qwen --timeout=5mkubectl -n llm get podskubectl -n llm logs deployment/vllm-qwen --tail=50
실패한 rollout과 컨테이너 로그
Waiting for deployment "vllm-qwen" rollout to finish: 0 of 1 updated replicas are available...error: timed out waiting for the conditionNAME READY STATUS RESTARTS AGEvllm-qwen-59559b485c-tx6lf 0/1 CrashLoopBackOff 4 (78s ago) 5m10sWARNING 08-04 01:49:44 [argparse_utils.py:257] With `vllm serve`, you should provide the model as a positional argument or in a config file instead of via the `--model` option. The `--model` option will be removed in a future version.usage: vllm [-h] [-v] {chat,complete,serve,launch,bench,collect-env,run-batch} ...vllm: error: unrecognized arguments: Qwen/does-not-exist
key
값
해석
rollout
error: timed out waiting for the condition
5분 안에 Ready 도달 실패
새 Pod
vllm-qwen-59559b485c-tx6lf
새 ReplicaSet hash. 설정 변경이 반영됨
상태
CrashLoopBackOff, restart 4
기동 즉시 종료가 반복
실패 원인
unrecognized arguments: Qwen/does-not-exist
argument 파싱 단계에서 종료
로그에 --model 옵션이 향후 제거 예정이라는 경고도 함께 나왔다. 현재 manifest는 결합 형식 --model=...을 쓰고 있으므로, 다음 수정에서 positional argument 형식으로 바꿀지 검토 대상이다.
재조회하면 상태 표기가 Running으로 보이기도 한다.
kubectl get po -n llm
재시작 주기 중의 상태
NAME READY STATUS RESTARTS AGEvllm-qwen-59559b485c-tx6lf 0/1 Running 5 (109s ago) 5m41s
key
값
해석
STATUS
Running
재시작 직후 프로세스가 살아 있는 순간
READY
0/1
probe 통과 전이므로 트래픽 대상이 아님
RESTARTS
5
앞선 조회보다 1 증가
CrashLoopBackOff와 Running은 같은 Pod의 서로 다른 순간이다. STATUS만 보고 정상으로 판단하면 안 되고 READY와 RESTARTS를 함께 봐야 한다. 여기서는 0/1과 증가하는 재시작 횟수가 실패를 가리킨다.
2-2. rollback
rollout undo로 직전 revision으로 되돌렸다.
kubectl -n llm rollout history deployment/vllm-qwenkubectl -n llm rollout undo deployment/vllm-qwenkubectl -n llm rollout status deployment/vllm-qwen --timeout=20m
revision 목록과 rollback 결과
deployment.apps/vllm-qwen REVISION CHANGE-CAUSE1 <none>2 <none>3 <none>Warning: resource deployments/vllm-qwen was previously managed with 'kubectl apply'. Rolling back will not update the kubectl.kubernetes.io/last-applied-configuration annotation, which may cause unexpected behavior on future 'kubectl apply' operations. Consider using 'kubectl apply' with your previous configuration file instead.deployment.apps/vllm-qwen rolled backWaiting for deployment "vllm-qwen" rollout to finish: 0 out of 1 new replicas have been updated...Waiting for deployment "vllm-qwen" rollout to finish: 0 out of 1 new replicas have been updated...Waiting for deployment "vllm-qwen" rollout to finish: 0 of 1 updated replicas are available...deployment "vllm-qwen" successfully rolled out
key
값
해석
revision 수
3
초기 배포, 이전 변경, 이번 결함 주입
CHANGE-CAUSE
전부 <none>
변경 사유가 기록되지 않음
rollback
rolled back
직전 revision으로 복귀
rollout
successfully rolled out
정상 Pod 재기동 완료
CHANGE-CAUSE가 모두 비어 있어 revision 번호만으로는 각 배포가 무엇을 바꾼 것인지 알 수 없다. 실패 상황에서 되돌릴 대상을 고를 때 판단 근거가 없다는 뜻이며, kubernetes.io/change-cause annotation을 기록하도록 배포 방식을 바꿀 여지가 있다.
경고 문구도 남았다. 이 Deployment는 원래 kubectl apply로 관리되는데 rollout undo는 last-applied-configuration annotation을 갱신하지 않는다. 이후 같은 manifest로 apply하면 rollback으로 되돌린 내용이 다시 덮일 수 있다. 선언형 관리와 명령형 rollback을 섞을 때 생기는 문제이므로, 실제 운영에서는 rollback 후 manifest 쪽도 함께 맞춰야 한다.
3. replica 확장
3-1. 확장 전 상태
확장 전에 현재 memory 예산과 실제 점유량을 확인했다.
kubectl -n llm get deployment vllm-qwen -o jsonpath='{.spec.template.spec.containers[0].args}{""}'
kubectl -n llm describe pod vllm-qwen-57758995fd-m5jz7 | grep -A10 Events
두 번째 Pod의 이벤트
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 4m2s default-scheduler Successfully assigned llm/vllm-qwen-57758995fd-m5jz7 to gpu-k8s Normal Pulled 4m kubelet spec.containers{vllm}: Successfully pulled image "vllm/vllm-openai:latest" in 1.059s (1.059s including waiting). Image size: 8914087113 bytes. Normal Pulled 2m47s kubelet spec.containers{vllm}: Successfully pulled image "vllm/vllm-openai:latest" in 1.056s (1.056s including waiting). Image size: 8914087113 bytes. Normal Pulling 81s (x3 over 4m1s) kubelet spec.containers{vllm}: Pulling image "vllm/vllm-openai:latest" Normal Created 80s (x3 over 4m) kubelet spec.containers{vllm}: Container created Normal Pulled 80s kubelet spec.containers{vllm}: Successfully pulled image "vllm/vllm-openai:latest" in 1.087s (1.088s including waiting). Image size: 8914087113 bytes. Normal Started 79s (x3 over 4m) kubelet spec.containers{vllm}: Container started Warning Unhealthy 12s (x21 over 3m52s) kubelet spec.containers{vllm}: Startup probe failed: Get "http://10.42.0.63:8000/health": dial tcp 10.42.0.63:8000: connect: connection refused
단계
결과
Scheduled
성공. 노드 배정에 문제 없음
이미지
3회 pull, 매번 약 1.06초
Created, Started
x3. 컨테이너는 계속 다시 만들어짐
Startup probe
connection refusedx21
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.status.allocatable.nvidia\.com/gpu\.shared}{""}{end}'
slot 잔량 확인
gpu-k8s 4
자원 부족이 Pending으로 나타나지 않는다
스케줄러는 nvidia.com/gpu.shared slot 개수만 센다. slot이 4이고 두 Pod가 각각 1을 요청하므로 스케줄링 단계에서는 아무 문제가 없고, Pod는 정상적으로 노드에 배정된다.
GPU memory는 스케줄러가 모르는 값이다. 부족은 컨테이너가 시작된 뒤 vLLM이 memory를 잡으려 할 때 드러나고, 프로세스가 listen하지 못해 Startup probe failed: connection refused로만 보인다.
따라서 time-slicing 환경에서 GPU memory 초과 배치는 Pending이 아니라 기동 실패 반복으로 나타난다. Pending만 자원 부족의 신호로 알고 있으면 원인을 컨테이너나 probe 설정에서 찾게 된다.
describe의 이벤트를 보지 않고 get pods만 봤다면 0/1과 재시작 횟수만 보여 원인을 알 수 없었다. slot 잔량이 4로 남아 있다는 점이 “slot이 아니라 memory 문제”라는 것을 가른다.
3-3. memory 예산을 낮춘 뒤 재확장
--gpu-memory-utilization을 낮춰 두 instance가 함께 들어갈 수 있게 했다. args가 --옵션=값 결합 형식이므로 index를 지정하지 않고 접두사로 찾아 배열 전체를 교체했다.
NEW_ARGS=$(kubectl -n llm get deployment vllm-qwen -o json | jq -c '[.spec.template.spec.containers[0].args[] | if startswith("--gpu-memory-utilization=") then "--gpu-memory-utilization=0.22" else . end]')echo "$NEW_ARGS"
0.22는 이 GPU를 slot 4개로 나눠 쓰는 것을 전제로 잡은 값이다. 장치 23,034 MiB를 4등분하면 instance당 약 5,758 MiB이고, 0.22는 그보다 낮은 약 5,067 MiB로 CUDA context 몫을 남겨둔 예산이다. 이번에는 2 replica만 띄웠으므로 여유가 더 남는다.
patch 전에 echo로 확인하는 이유가 여기서 드러난다. 다른 원소가 그대로이고 목표 원소만 바뀐 것을 눈으로 보고 적용할 수 있다.
NAME READY STATUS RESTARTS AGEvllm-qwen-854dfd9bfb-hx8x9 1/1 Running 0 10mvllm-qwen-854dfd9bfb-z6sqc 1/1 Running 0 8m57s
key
값
해석
Pod 수
2
둘 다 1/1 Running
restart
0
기동 실패 없음
ReplicaSet hash
854dfd9bfb
args 변경으로 새 ReplicaSet 생성
기동 간격
약 1분
순차 기동
57758995fd에서 854dfd9bfb로 hash가 바뀐 것이 args 변경이 반영됐다는 표시다. 같은 hash에서 replica만 늘렸다면 3-2의 실패가 반복됐을 것이다.
3-4. replica 1개와 2개 비교
확장 효과를 보려면 replica 수만 바꾸고 나머지 조건을 같게 둬야 한다. 동시 요청 16건, max_tokens=512, 같은 프롬프트로 두 구성에서 각각 측정했다.
for i in $(seq 1 16); do curl -s http://127.0.0.1:8000/v1/chat/completions -H "Content-Type: application/json" -d '{"model":"qwen2.5-3b","messages":[{"role":"user","content":"긴 설명을 해줘."}],"max_tokens":512}' -o /dev/null -w "%{time_total}" &donewait
replica를 2개로 늘렸는데 평균과 중앙값이 모두 느려졌고, 16건 전체 소요도 1개 쪽이 짧았다.
이 방향은 구조에서 예상되는 결과다. time-slicing은 같은 물리 GPU를 시간으로 나누므로 replica를 늘려도 연산 능력이 늘지 않는다. 여기에 두 instance를 오가는 전환 비용과 모델 가중치 중복 적재가 더해진다. vLLM은 이미 단일 instance 안에서 요청을 batching하므로 replica로 쪼개면 그 배칭 효율을 스스로 깎는 셈이기도 하다. replica 확장이 이 구성에서 처리량 레버가 아니라는 결론은 이 구조적 이유와 측정 방향이 함께 뒷받침한다.
다만 두 값의 차이 폭 자체는 단정할 수 없다.
주장
근거
판정
확장해도 처리량이 늘지 않는다
구조적 이유 + 측정 방향
확정
1개가 약 0.21초, 약 9% 빠르다
각 구성 1회 측정
미확정
같은 구성을 반복 측정하지 않았으므로 구성 내부의 흔들림 폭을 모른다. 그 폭이 0.21초보다 크다면 두 구성의 차이는 노이즈에 묻힌다. 차이의 크기를 쓰려면 구성을 번갈아 여러 회 측정해 회차 간 편차부터 확인해야 한다.
3-6. 분포 차이는 확정하지 않는다
응답 시간의 퍼진 정도는 두 구성에서 다르게 관측됐다.
구성
관측된 범위
최대 ÷ 최소
replica 1
0.82 ~ 4.35초
5.29배
replica 2
1.32 ~ 3.14초
2.37배
replica 1에서는 --max-num-seqs=16 상한과 동시 요청 수가 같아 16건이 한 배치에 묶이고, replica 2에서는 8건씩 나뉘어 배치가 작아진다. 배치 크기가 완료 시각의 퍼짐에 영향을 준다는 설명이 가능하다.
이 관측을 결과로 쓰지 않는다
세 가지 이유로 확정하지 않는다.
첫째, 최소와 최대는 표본 16개의 꼬리값이라 가장 크게 흔들리는 통계다. 각 구성에서 한 번씩만 측정했으므로 다시 재면 다른 값이 나올 수 있다.
둘째, curl 16개를 백그라운드로 띄우는 방식이라 클라이언트 쪽 프로세스 기동 스큐가 측정에 섞인다. 0.82초가 서버가 빨라서인지 그 프로세스가 먼저 떠서인지 이 데이터로는 구분되지 않는다.
셋째, 배치 크기 외에 응답 길이 차이만으로도 비슷한 퍼짐이 나올 수 있다. max_tokens는 상한일 뿐이고 실제 생성 길이는 요청마다 다르다.
확인하려면 클라이언트 스큐가 없는 부하 도구로 동시성을 고정하고, 각 구성을 번갈아 여러 회 측정한 뒤 vllm:time_to_first_token_seconds와 vllm:e2e_request_latency_seconds 분위수를 비교해야 한다.
4. 확인된 범위
항목
결과
Pod 삭제 후 자동 복구
확인, 101초
hostPath cache 재사용
확인, weight 적재 0.77초
복구 후 GPU 재할당
확인, 1/1 Running
KV cache 계산식 검증
확인, 토큰당 약 36 KiB로 예측과 일치
rollout 실패 감지
확인, timed out waiting for the condition
rollback 복구
확인, rollout undo 후 정상 기동
재시작 후 GPU 경로
확인, 드라이버 595.71.05, slot 4 재노출
GPU memory 초과 시 증상
확인, Pending이 아니라 startup probe 실패
memory 예산 조정 후 2 replica
확인, 0.22에서 둘 다 1/1 Running
replica 확장의 처리량 효과
확인, 늘지 않음. 방향만 확정
아래는 이번 측정에서 확인하지 않았다.
- 복구 시간 101초 중 82초의 세부 내역- 실패한 rollout 동안 기존 Pod가 트래픽을 계속 처리했는지- 존재하지 않는 모델 이름을 주입했을 때의 실패 양상- change-cause를 기록하는 배포 방식- replica 1개와 2개 차이의 크기. 각 구성 1회 측정이라 노이즈와 구분되지 않음- 응답 시간 분포 차이. 꼬리값 기반이고 클라이언트 기동 스큐가 섞여 있음- 지속 부하와 다른 동시성에서의 확장 효과
두 번째 항목은 이번 조회에서 Pod가 하나만 표시됐으나 그것이 배포 전략 때문인지 조회 시점 때문인지 구분하지 않았다. strategy와 Pod 이벤트를 함께 확인해야 답할 수 있다.