지식

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 sm 60초가 플래그 조합 실험 수 시간보다 정확했다.
  • 버전 문자열의 접미사를 읽어라. 모델 카드의 성공 버전 …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 커널을 다시 넣는 날까지, 이 한 줄이 태그 상향의 게이트다.

조회 0댓글 0

댓글

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.