예약은 만석, 손님은 없음: nvidia-smi 0%의 정체

VRAM은 거의 꽉 찼는데 GPU-Util은 0%. 고장이 아니라 서빙 엔진이 KV 풀을 미리 예약해 둔 상태입니다. 모델 적재·추론·사전 예약을 눌러보며 메모리 점유와 연산 사용률이 별개임을 확인합니다. 단순화한 시뮬레이션이며 실제 GPU 측정값이 아닙니다.

nvidia-smi를 보면 메모리는 거의 꽉 찼는데 GPU 사용률(GPU-Util)은 0%인 순간이 있습니다. 이 조합만으로 고장이라고 볼 수는 없습니다. 결론부터 말하면 메모리 점유(VRAM)와 연산 사용률은 서로 다른 지표입니다. 메모리에 뭔가 올라가 있다고 GPU가 계산하고 있는 건 아닙니다.

왜 메모리가 차나

세 가지가 VRAM을 차지합니다.

  • 가중치: 모델 파라미터. 대략 파라미터 수 × dtype 바이트입니다. 7B 모델을 fp16(2바이트)로 올리면 약 14GB이고, 이건 모델이 올라가 있는 동안 계속 자리를 차지합니다.
  • KV 캐시: 생성 중 토큰마다 층별로 K·V를 저장한 것. 요청이 길고 배치가 클수록 커집니다. (동일한 층수·토큰수·헤드 차원인 MHA와 비교하면, GQA/MQA 모델은 KV 헤드 수가 적어 이보다 작습니다.)
  • 사전 예약: vLLM 같은 서빙 엔진은 설정된 GPU 메모리 예산(gpu_memory_utilization) 안에서 가중치와 실행에 필요한 비-KV 메모리를 뺀 나머지를 KV 캐시 풀로 미리 할당합니다. 그래서 요청이 하나도 없어도 메모리는 이미 차 있는 것처럼 보입니다.

KV 캐시가 얼마나 큰지는 계산해 볼 수 있습니다

"요청이 길고 배치가 클수록 커집니다"는 감으로 하는 말이 아닙니다. 식이 정해져 있습니다.

토큰 1개당 KV 바이트
  = 2(K와 V) × 층 수 × KV 헤드 수 × 헤드 차원 × dtype 바이트

Llama 3 8B의 공개된 config.json 값을 넣어 보겠습니다. 층 32개, KV 헤드 8개(num_key_value_heads), 헤드 차원 128(hidden_size 4096 ÷ 어텐션 헤드 32개), bf16이므로 2바이트입니다.

2 × 32 × 8 × 128 × 2 = 131,072 바이트 = 토큰 1개당 128 KiB

토큰 하나에 128KiB입니다. 이 숫자를 늘려 보면 왜 KV 풀이 GB 단위가 되는지 바로 보입니다.

상황KV 캐시
1,024토큰 요청 1건0.12 GiB
4,096토큰 요청 1건0.5 GiB
8,192토큰 요청 1건1 GiB
8,192토큰 요청 32건 동시32 GiB

같은 모델의 가중치가 bf16 기준 약 15GiB(80.3억 파라미터 × 2바이트)라는 걸 감안하면, 긴 요청 몇십 개만 겹쳐도 KV 캐시가 가중치보다 커집니다. 서빙 엔진이 KV 풀을 미리 크게 잡아 두는 이유가 여기 있습니다.

GQA가 하는 일도 이 식에 들어 있습니다

식에서 KV 헤드 수만 8에서 32(어텐션 헤드와 같은 수)로 바꿔 보면 토큰당 512 KiB, 정확히 4배가 됩니다. Llama 3 8B가 KV 헤드를 8개만 두는 GQA 구조인 덕분에 KV 캐시가 4분의 1로 줄어든 것입니다. 위 표의 32GiB가 128GiB가 되는 차이라 생각하면 체감이 됩니다.

왜 사용률은 0인가

nvidia-smi의 GPU-Util은 직전 샘플 구간 동안 GPU 커널이 실행된 시간의 비율입니다. 즉 "지금 계산하고 있는가"를 보는 값이지, 메모리를 얼마나 썼는지를 보는 값이 아닙니다. 모델을 올려두기만 하고 아무 요청도 처리하지 않으면 커널이 돌지 않으니 0%가 됩니다.

흔한 오해 — 100%면 GPU를 다 쓰고 있다?

0%를 오해하는 사람만큼이나 100%를 오해하는 사람도 많습니다. NVIDIA의 NVML 문서는 이 값을 이렇게 정의합니다.

Percent of time over the past sample period during which one or more kernels was executing on the GPU. — NVML nvmlUtilization_t

"커널이 하나 이상 실행된 시간의 비율"입니다. 몇 개가 도는지, GPU의 연산 장치를 얼마나 채웠는지는 이 숫자에 들어 있지 않습니다. 그래서 아주 작은 커널 하나가 쉬지 않고 돌기만 해도 100%로 표시됩니다. GPU의 계산 능력은 거의 놀고 있는데도요.

정리하면 이 지표는 이렇게 읽어야 합니다.

이 값이 말해 주는 것말해 주지 않는 것
GPU가 바빴는지 놀았는지얼마나 꽉 채워 계산했는지
커널이 돈 시간의 비율연산 장치 점유율, 실제 처리량(FLOPS)
샘플 구간(1초~1/6초) 평균그 구간보다 짧은 순간의 변화

"사용률이 100%인데 왜 이렇게 느리죠?"라는 질문이 자주 나오는 이유가 이것입니다. 100%는 바쁨의 증거이지 효율의 증거가 아닙니다. 처리량이 목표에 못 미친다면 사용률 대신 초당 토큰 수와 배치 크기를 봐야 합니다.

참고로 같은 구조체의 memory 값도 이름과 달리 "메모리를 몇 % 썼는지"가 아니라 **"메모리를 읽거나 쓴 시간의 비율"**입니다. 점유량은 memory.used로 따로 봐야 합니다.

실제 서버에서는 세 화면을 함께 봅니다

한 번의 nvidia-smi 화면만 보고 결론을 내리기보다 시간 흐름과 프로세스를 함께 확인해야 합니다.

# 1초마다 GPU 사용률과 메모리 사용량을 함께 기록
nvidia-smi \
  --query-gpu=timestamp,utilization.gpu,memory.used,memory.total \
  --format=csv -l 1

# 어떤 프로세스가 GPU 메모리를 잡고 있는지 확인
nvidia-smi pmon -s um

메모리는 계속 높고 요청할 때만 사용률이 솟는다면 모델과 예약 풀이 대기하는 정상적인 서빙 패턴일 수 있습니다. 반대로 요청이 있는데도 사용률이 계속 낮다면 CPU 전처리, 디스크·네트워크 입력, 작은 배치, 요청 스케줄링을 함께 확인합니다. 모르는 프로세스가 메모리를 차지한다면 먼저 그 프로세스의 소유자와 작업을 확인해야 합니다.

사용률은 샘플 구간의 비율이므로 아주 짧은 커널 실행은 관측 시점 사이에 지나가 보이지 않을 수 있습니다. 한 번의 0%보다 여러 초의 흐름, 처리량과 요청 지연을 같이 보는 편이 안전합니다.

숫자를 두 번 세던 버그

데모를 처음 만들 때 메모리 막대를 이렇게 그렸습니다. 가중치 + 사전 예약한 KV 풀 + 지금 쓰는 KV. 그럴듯해 보였는데 합계가 카드 용량을 넘어갔습니다.

당연합니다. 지금 쓰는 KV 는 이미 예약한 풀 안에 들어 있습니다. 따로 더할 게 아니라 풀 안에서 찬 부분과 빈 부분으로 나눠야 했습니다. 그래서 지금은 풀을 하나의 칸으로 두고 그 안이 채워지는 걸 보여줍니다. 사전 예약이 왜 "미리 잡아 두는" 것인지가 오히려 이 그림에서 더 잘 보입니다.

넘칠 때 값을 잘라 맞추고 싶은 유혹도 있었는데, 그러면 설정이 안 맞는다는 사실 자체가 화면에서 사라집니다. 지금은 넘치면 넘친 채로 표시합니다.

모델을 올렸다 내려 보세요

모델 프리셋: 7B · fp16 · 24GB GPU

적재 · 추론 · 예약을 눌러 두 지표를 비교하세요

대기(idle)
VRAM 메모리21.6 / 24 GB · 점유율 90%거의 가득
가중치 14.0 사용 중 KV 0.0 빈 KV 풀 7.6 빈 공간 2.4
GPU 사용률 (GPU-Util)커널 실행 시간 비율 · 메모리 점유율과 다른 지표
0%

프레임워크가 KV 풀을 미리 확보해 메모리는 거의 가득 찼지만, GPU-Util은 여전히 0% — 예약은 만석인데 손님은 없는 상태입니다.

단순화한 시뮬레이션이며 실제 GPU 측정값이 아닙니다. GPU-Util(NVML)은 샘플 구간에 커널이 실행된 시간 비율이라, 단순 메모리 전송만으로는 오른다는 보장이 없고 추론 92%도 설명용 부하 예시입니다. KV 캐시는 GQA/MQA에서 KV 헤드 수로 줄며, vLLM의 gpu_memory_utilization(예: 0.9, 기본값 단정 아님)은 가중치에 더하는 게 아니라 전체 목표 상한으로 시작 시 설정합니다.

참고: NVIDIA NVML(GPU-Util 정의), vLLM 메모리 설정, Hugging Face KV 캐시

데모만 따로 보려면 전체 화면으로 열기.

첫 화면은 이 현상을 바로 보여주기 위해 모델 적재와 사전 예약이 켜진 상태입니다. 처음부터 비교하려면 ‘모델 내리기’와 ‘사전 예약 끄기’를 차례로 눌러 보세요.

  • 모델 적재: 가중치가 VRAM을 차지합니다. 이때 GPU-Util은 데모에서는 0으로 단순화했습니다 (실제로는 초기화·변환 커널이 실행되면 일시적으로 오를 수 있지만, 단순 메모리 전송만으로 오른다는 보장은 없습니다).
  • 사전 예약 켜기: 프레임워크가 KV 풀을 미리 확보합니다. 메모리는 거의 가득 차지만 사용률은 여전히 0% — 예약은 만석인데 손님은 없는 상태입니다.
  • 추론 요청: 그제서야 커널이 돌아 사용률이 치솟고, 사용 중 KV가 늘어납니다. 끝나면 다시 0%로 내려갑니다.

예약을 켠 상태에서는 요청이 끝나도 그 블록은 풀에 반환될 뿐, 미리 확보한 풀 자체는 유지되어 메모리 점유는 그대로입니다.

정리

서빙 엔진이 의도적으로 KV 풀을 예약했다면 높은 메모리 점유는 정상일 수 있습니다. 반대로 다른 프로세스 점유, 멈춘 작업, OOM 직전 상태일 수도 있으니, "메모리 사용량"과 "연산 사용률"을 따로 봐야 병목이 어디인지 알 수 있습니다. 놀고 있는데 메모리만 찬 거라면 모델·예약이 자리를 잡은 것이고, 사용률이 낮은데 처리량이 안 나온다면 배치·요청 스케줄링을 봐야 합니다.

무엇이 측정값이고 무엇이 계산인지

  • 데모의 숫자 — 원리를 분리해 보여주기 위한 단순화한 시뮬레이션이며, 실제 GPU에서 측정한 값이 아닙니다. 추론 사용률(예: 92%)은 설명용 부하 예시입니다.
  • KV 캐시 표 — 공개된 config.json 값을 식에 넣은 계산 결과입니다. 제가 GPU에서 잰 값이 아닙니다. 실제 vLLM은 non-Torch 메모리·peak activation· CUDA Graph 여유분까지 시작 시 프로파일링해 KV 풀을 정하므로, 실제 점유량은 이 표보다 큽니다.
  • NVML 정의 — NVIDIA 공식 문서의 정의를 그대로 인용했습니다.

계산에 쓴 가정도 밝혀 둡니다. 양자화하지 않은 bf16 KV 캐시, 텐서 병렬 없음, 접두어 공유나 페이지 단위 낭비 없음을 전제로 한 이론값입니다. 실제 서빙에서는 KV 양자화로 줄어들 수도, 페이지 단위 할당으로 늘어날 수도 있습니다.

확인한 자료

#GPU#LLM#추론#vLLM#인터랙티브 과학

관련 글