들어가며
사례 1. transformers v5가 망가뜨린 서빙 — Dolphin-Mistral-24B
환경 개요
| 항목 | 내용 |
|---|---|
| 모델 | dphn/Dolphin-Mistral-24B-Venice-Edition |
| 아키텍처 | mistral3 (Mistral-Small-3.1-24B 기반, Pixtral 비전 포함) |
| GPU | H100 NVL × 1, tp=1 |
| vLLM | 0.25.1 |
| transformers | 5.12.1 |
에러 1: is_fast AttributeError — tokenizer 백엔드 교체의 여파
AttributeError: 'CachedMistralCommonBackend' object has no attribute 'is_fast'1
vLLM은 tokenizer를 초기화할 때 is_fast 속성으로 fast/slow tokenizer 분기를 결정합니다. 그런데 transformers v5.x에서 mistral3 계열 모델의 tokenizer 백엔드가 CachedMistralCommonBackend로 교체됐습니다. 이 백엔드는 Mistral 공식 mistral_common 라이브러리 위에서 동작하는 독립 구현체라 HuggingFace tokenizer 클래스 계층을 상속하지 않고, 결과적으로 is_fast 속성 자체가 없습니다.
여기서 중요한 점은 transformers 버전을 내려서 해결할 수 없다는 겁니다. vLLM 0.24.0부터 transformers v4를 공식 차단했기 때문에, v4로 내리면 vLLM 자체가 기동을 거부합니다. v5 어느 버전을 써도 mistral3 tokenizer 백엔드 문제는 동일하게 재현됩니다.
해결: --tokenizer_mode mistral --config_format mistral 두 플래그를 추가하면 vLLM이 tokenizer를 HuggingFace 경로가 아닌 mistral_common 전용 경로로 초기화합니다. HF 모델카드에는 없는 옵션이지만, transformers v5 환경에서 mistral3 아키텍처를 올릴 때는 사실상 필수입니다.
에러 2: model weights not found — 포맷 불일치
에러 1을 잡고 재기동하니 이번엔 No model weights found. Checked formats: consolidated.safetensors, consolidated.00.pth가 났습니다.
HuggingFace 모델카드에는 --load_format mistral 옵션이 명시되어 있는데, 이건 Mistral 공식 릴리즈 포맷(consolidated.safetensors 등)만 탐색하는 옵션입니다. 문제는 Dolphin-Mistral-24B가 HuggingFace 표준 샤딩 포맷(model-00001-of-00010.safetensors)으로 배포된 모델이라는 점입니다. --load_format mistral은 이 파일 패턴을 인식하지 못합니다.
해결: --load_format mistral을 제거하면 vLLM 기본 HuggingFace 로더가 샤딩된 safetensors를 정상 인식합니다.
정리하면 HF 모델카드 대비 빼야 할 옵션과 넣어야 할 옵션이 정반대였던 게 이 케이스의 핵심입니다. 모델카드는 Mistral 공식 체크포인트 기준으로 작성되어 있었는데, 실제로 받은 건 HF Hub 재배포 버전이었기 때문입니다.
사례 2. 커스텀 Tool Parser가 vLLM 버전업을 두 단계나 건너뛴 결과
두 번째는 이미 서빙 중이던 멀티모달 모델(Midm-K-200-multimodal)에 Tool Calling 기능을 추가하려다 만난 문제입니다. Midm 모델은 OpenAI 호환 tool_call 포맷이 아니라 자체 <tool_call>... 포맷을 쓰는데, vLLM 기본 내장 tool-parser 목록(hermes, llama3, mistral 등) 어디에도 이 포맷을 지원하는 게 없었습니다.
커스텀 파서를 가져왔지만 인자가 반영되지 않는다
Midm 공식 튜토리얼에 있던 IBM Granite 파서 기반 커스텀 코드를 가져와 서빙 커맨드에 세 옵션(--enable-auto-tool-choice, --tool-parser-plugin, --tool-call-parser)을 추가했습니다. 그런데 배포 후 확인해보니 옵션이 전혀 반영되지 않았습니다.
원인은 저희 서빙 이미지 내부에서 vLLM 실행을 감싸는 쉘 스크립트가 추가 인자($@)를 vllm 프로세스로 전달하지 않는 구조였기 때문입니다. 스크립트 내부의 실행 라인을 sed로 직접 찾아 옵션을 삽입하는 방식으로 우회했는데, 이 과정에서 작은따옴표로 감싸지 않으면 $PORT 같은 변수가 스크립트 실행 전에 조기 확장되어 버리는 함정도 있었습니다.
AnyTokenizer가 사라졌다 — vLLM 0.16.0의 조용한 이름 변경
인자 문제를 풀고 재배포하니 이번엔 ImportError: cannot import name AnyTokenizer from vllm.transformers_utils.tokenizer로 죽었습니다. 원본 파서 코드가 참조한 튜토리얼은 vLLM 0.11.0 기준이었는데, 저희가 쓰던 이미지는 0.16.0이었습니다. 그사이 해당 모듈이 deprecation shim으로 바뀌어 AnyTokenizer는 완전히 사라지고, 새 위치(vllm.tokenizers.TokenizerLike)로 이름까지 바뀌어 있었습니다.
tool_parsers 네임스페이스 전체 이전 — 한 번 더
이걸 고치고 재배포했더니 이번엔 ModuleNotFoundError: No module named vllm.entrypoints.openai.tool_parsers. vllm.entrypoints.openai.tool_parsers.* 네임스페이스 전체가 vllm.tool_parsers.*로 옮겨간 것이었습니다.
이때 정상 파드는 이미 크래시루프 상태라 kubectl exec가 먹히지 않았는데, GPU 없는 1회성 디버그 파드를 --command 플래그로 ENTRYPOINT를 덮어써서 띄우고, 그 안에서 새 네임스페이스 구조를 직접 열어봤습니다. 문서화된 마이그레이션 가이드가 없는 내부 리팩터링일수록, 소스를 직접 열어보는 게 가장 빠른 길이었습니다.
세 문제 모두 고치고 나니 실제 파서 코드에서 바뀐 건 상단 import 세 줄뿐이었습니다.
# 기존
from vllm.transformers_utils.tokenizer import AnyTokenizer
from vllm.entrypoints.openai.tool_parsers.abstract_tool_parser import ToolParserManager
from vllm.entrypoints.openai.tool_parsers.granite_20b_fc_tool_parser import Granite20bFCToolParser
# 수정
from vllm.tokenizers import TokenizerLike as AnyTokenizer
from vllm.tool_parsers import ToolParserManager
from vllm.tool_parsers.granite_20b_fc_tool_parser import Granite20bFCToolParser
최종적으로는 실제 호출에서 tool_calls가 정상 구조화되어 반환되는 것까지 확인했습니다.
마치며
두 사례 모두 공통점이 있습니다. 서빙 프레임워크(vLLM)와 모델/코드가 서로 다른 시점의 버전을 전제로 만들어졌을 때, 그 간극에서 문제가 생긴다는 점입니다. 모델카드나 튜토리얼은 특정 시점 기준으로 작성되고, 그 이후 프레임워크가 계속 리팩터링되면서 옵션명이나 모듈 경로가 조용히 바뀝니다. 문서화가 안 된 변경일수록 추측보다는 실제 배포된 이미지 안에 직접 들어가서 확인하는 편이 빨랐습니다.
셀프서빙 환경에서 새 모델을 처음 올리실 때도 마찬가지일 겁니다. 모델카드에 적힌 옵션이 그대로 안 먹힐 수 있고, 에러 메시지 하나가 실은 프레임워크 버전 차이 때문일 수 있습니다. 막히면 "왜 안 되지"보다 "지금 쓰는 버전에서는 뭐가 바뀌었지"부터 확인해보시길 권합니다.
읽어주셔서 감사합니다!