DeepSeek-V4-Flash 304B를 RTX PRO 6000 두 장에 서빙하기 — 최신 vLLM이 곧 퇴보였던 이유
요약
DeepSeek-V4-Flash-0731의 NVFP4 양자화본(304B, 가중치 175.6GB)을 RTX PRO 6000 Blackwell 96GB 두 장에 올렸다. 기동 시도 10번이 전부 실패했는데 원인은 OOM이 아니라 vLLM nightly가 특정 날짜부터 우리 GPU(SM 12.0)용 커널을 통째로 잃어버린 회귀였다. 최신 태그를 버리고 이틀 전 빌드로 내려가자 우회 플래그 하나 없이 컨텍스트 250,000토큰까지 단번에 떴다. 이 글은 그 삽질의 기록이고, 부록에 같은 함정을 60초 만에 피하는 판별법을 남긴다.
대상과 장비
| 항목 | 값 |
|---|---|
| 모델 | MJPansa/DeepSeek-V4-Flash-0731-NVFP4 — 304B, NVFP4 양자화 |
| 가중치 | safetensors 48개, 총 175.6GB |
| GPU | NVIDIA RTX PRO 6000 Blackwell Max-Q 96GB × 2 (compute capability 12.0, SM120) |
| 엔진 | vLLM (공식 Docker 이미지, tensor parallel 2) |
VRAM 총 192GB에 가중치가 175.6GB — GPU당 여유가 8GB 남짓인 빠듯한 구성이다. 그래서 처음엔 "KV 캐시가 안 잡혀서 못 뜨면 어쩌나"가 걱정이었다. 결론부터 말하면 메모리는 문제가 아니었다.
최종 구성
docker run -d \
--name vllm-serve \
--restart unless-stopped \
--gpus all \
--ipc=host \
-p 8000:8000 \
-v /path/to/DeepSeek-V4-Flash-0731-NVFP4:/model:ro \
vllm/vllm-openai:nightly-6f91edf96d3f3272945809c04702380053bff4de \
--model /model \
--served-model-name DeepSeek-V4-Flash-0731-NVFP4 \
--port 8000 \
--trust-remote-code \
--tokenizer-mode deepseek_v4 \
--tensor-parallel-size 2 \
--max-model-len 250000 \
--max-num-seqs 8 \
--max-num-batched-tokens 4096 \
--gpu-memory-utilization 0.95 \
--kv-cache-dtype fp8 \
--block-size 256 \
--enforce-eager
핵심 플래그만 짚으면 — --kv-cache-dtype fp8은 튜닝 옵션이 아니라 이 모델의 필수 인자다(auto면 fp8_ds_mla layout only supports fp8 kv-cache assert로 즉사). --tokenizer-mode deepseek_v4·--block-size 256·--enforce-eager는 모델 카드의 테스트 구성 그대로. 그리고 이미지 태그가 latest도 최신 nightly도 아닌 커밋 고정 태그인 이유가 이 글의 본론이다.
삽질기
1막 — 기동 7연패, 그런데 OOM이 아니다
최신 nightly(당시 vLLM 0.26.1rc1.dev229)로 첫 기동. 가중치 로드 후 메모리 프로파일링 단계에서 죽는다. 로그를 뒤져도 OutOfMemory는 한 줄도 없고, 대신 이런 게 나온다:
RuntimeError: Assertion error (deepgemm .../apis/layout.hpp:60): Unknown SF transformation
DeepGEMM — DeepSeek이 만든 FP8 행렬곱 라이브러리 — 쪽 에러다. VLLM_USE_DEEP_GEMM=0으로 끄면 로드는 통과하는데, 이번엔 forward에서:
deepgemm .../apis/hyperconnection.hpp:56: Unsupported architecture
커널 백엔드를 하나씩 갈아끼우며 우회를 시도했다. cutlass로 폴백하면 RuntimeError: dispatch_scaled_mm, .../w8a8/cutlass/c3x/scaled_mm_helper.hpp:17, Triton으로 밀면 KeyError: 'float8_e8m0fnu'(체크포인트의 스케일 dtype을 Triton 커널이 몰라서). 벽 하나를 넘으면 다음 벽. 7번을 그렇게 부딪혔다.
2막 — 모델 카드 재현도 실패, 그리고 가설이 하나 깨진다
모델 카드를 다시 읽으니 테스트 명령이 우리와 꽤 달랐다 — --enforce-eager, --block-size 256 등. "eager 모드면 문제의 커널 경로를 안 탈지도?"라는 가설로 카드 구성을 충실 재현했다. 결과: 같은 지점에서 같은 에러. 문제의 GEMM은 CUDA graph 캡처가 아니라 forward 본체에서 호출되고 있었다 — eager 가설 반증.
여기서 관점을 바꿨다. 플래그로 안 되면 바이너리를 까보자.
3막 — 60초 심볼 조사가 수 시간의 플래그 실험을 이겼다
이미지에 번들된 DeepGEMM 공유 라이브러리의 심볼을 그냥 strings로 뽑아봤다. 결과가 모든 걸 설명했다:
47 sm100
43 sm90
(sm12x: 없음)
sm90(Hopper)·sm100(B200급 데이터센터 Blackwell) 커널만 있고, 우리 GPU인 sm120(워크스테이션 Blackwell) 커널이 하나도 없다. 우리가 막힌 바로 그 함수도 sm90_/sm100_ 두 벌뿐. 환경변수로 우회가 안 됐던 이유다 — 빌드 옵션이 아니라 소스 레벨 부재.
여기서 한 번 오판을 했다. DeepGEMM 공식 문서의 지원 대상이 "NVIDIA SM90 or SM100 architecture GPU"라서, "공식 이미지는 원래부터 SM120이 안 되는 거고, 모델 카드가 워크스테이션급(DGX Spark, SM121)에서 성공한 건 로컬 소스 빌드의 JIT 컴파일 덕"이라고 결론 내릴 뻔했다. 그럴듯했지만 틀렸다.
4막 — 진범: 이틀 사이의 의존성 교체
과거 nightly들을 날짜순으로 pull해서 같은 심볼 조사를 돌렸다(GPU도 모델 로드도 필요 없어서 태그당 확인은 사실상 1분). 이틀 전 빌드에서 반전:
46 sm100
42 sm120 ← !!
37 sm90
있다. 원인은 vLLM이 그 이틀 사이에 번들 DeepGEMM을 교체한 것이었다:
| vLLM nightly | 번들 DeepGEMM | sm120 커널 |
|---|---|---|
| 07-29 (dev77) 이전 | deepseek-ai 원본 a6b593d2 |
42종 존재 |
| 07-31 (dev181) 이후 | vllm-project 자체 포크 f5a76426 |
없음 (sm90/sm100만) |
원본 DeepGEMM의 디스패처에는 arch_major == 12 분기가 있는데, 포크에는 그 분기가 통째로 없고 else가 Unsupported architecture를 던진다. DeepSeek-V4 구현은 이 라이브러리를 세 곳에서 조건 없이 호출하므로, 07-31 이후의 모든 공식 이미지는 SM120에서 원천적으로 기동 불가였던 것.
5막 — 허무한 결말
07-29 태그로 바꾸자 우회 플래그 하나 없이 첫 시도(8k 컨텍스트)에 떴다. 8k→16k→32k→65k→131k→250k(모델 카드 상한)까지 배증 탐색 전 구간 성공, 실패 0회. 이분 탐색은 시작도 못 해봤다. gpu-memory-utilization 0.95에서 KV 캐시도 넉넉히 잡혔다. 수 시간의 사투가 태그 하나로 끝났다.
교훈
- "최신 = 개선"이 아니다. 의존성 교체가 낀 nightly에서는 최신이 곧 퇴보일 수 있다. 특히 소수 아키텍처(워크스테이션 Blackwell) 사용자는 회귀의 첫 피해자가 된다.
- 커널 계열 기동 실패는 플래그 실험 전에 바이너리 심볼 조사부터.
strings *.so | grep sm60초가 플래그 조합 실험 수 시간보다 정확했다. - 버전 문자열의 접미사를 읽어라. 모델 카드의 성공 버전
…dev191+g….d20260731에서.dYYYYMMDD는 로컬 빌드 표식이다 — "그 버전의 공식 이미지를 찾는" 삽질을 원천 차단해줬을 단서. - 에러 메시지의 파일 경로가 범인을 가리킨다.
deepgemm/.../hyperconnection.hpp— 어느 라이브러리의 어느 계층에서 죽는지가 로그에 이미 있었다. - OOM 걱정으로 시작한 작업의 진짜 벽이 커널 지원이었듯, 가설은 로그로 검증하고 버려라. 이번 건에서 세운 가설 중 둘(eager 우회, "공식 이미지는 원래 안 됨")이 실측으로 깨졌다.
부록 — 태그 채택 전 60초 판별법
SM120 계열 GPU에서 vLLM 이미지 태그를 올리기 전, GPU 없이 이것만 돌려보면 된다:
docker run --rm --entrypoint bash <태그> -c \
'strings /usr/local/lib/python3*/dist-packages/vllm/third_party/deep_gemm/_C.*.so \
| grep -oE "^sm[0-9]+_" | sort -u'
출력에 sm120_이 있으면 기동 가치가 있고, sm90_/sm100_뿐이면 그 태그는 탈락이다. upstream이 포크에 sm12x 커널을 다시 넣는 날까지, 이 한 줄이 태그 상향의 게이트다.
댓글
아직 댓글이 없습니다. 첫 댓글을 남겨보세요.