NVIDIA Nemotron-3-Nano-4B GGUF의 토크나이저 불일치 문제와 해결책
요약: NVIDIA의 Nemotron-3-Nano-4B GGUF는 tokenizer.ggml.pre = "pixtral"을 사용합니다. llama.cpp에서 pixtral은 Llama 3 사전 토크나이저를 선택하지만, Nemotron 자체 토크나이저는 Tekken 분할 규칙을 사용합니다. 그 결과: llama.cpp master (339f324) 환경에서 영어, 일본어, 아랍어 및 코드는 Hugging Face의 토크나이저와 정확하게 토큰화되지만, 힌디어는 19개 대신 30개의 토큰을 사용하고, 타밀어는 15개 대신 20개를, 태국어는 15개 대신 16개를 사용했으며, दी는 하나가 아닌 두 개의 토큰으로 처리되었습니다. 오류는 발생하지 않으며 모델은 로드되고 답변합니다. --override-kv tokenizer.ggml.pre=str:tekken을 사용하여 llama-server를 시작하거나 파일의 메타데이터를 tekken으로 재작성하면 모든 샘플이 Hugging Face와 일치하게 됩니다. 한 가지 함정은, 이를 확인하기 위한 명백한 도구인 llama-tokenize가 --override-kv 옵션을 허용하지만 무시한다는 것입니다. 따라서 이 수정 사항이 작동하지 않는 것처럼 보입니다. 상위 이슈: ggml-org/llama.cpp#30894에는 영향을 받은 다른 업로드 목록이 있습니다.
증상
master에서 339f324로 빌드된 일회용 LXC 컨테이너, nvidia/NVIDIA-Nemotron-3-Nano-4B-GGUF로부터 얻은 NVIDIA-Nemotron3-Nano-4B-Q4_K_M.gguf(2,837,072,864 bytes로 Hugging Face의 크기와 일치), 그리고 참조용으로 사용된 동일 모델의 nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16에서 가져온 tokenizer.json을 사용했습니다. GGUF의 토크나이저 메타데이터는 다음과 같습니다:
general.architecture = nemotron_h
tokenizer.ggml.model = gpt2
tokenizer.ggml.pre = pixtral
특수 토큰 없이 동일한 텍스트에 대한 토큰 ID를 Hugging Face tokenizers와 llama-server의 /tokenize를 사용하여 비교했습니다:
english HF 19 server 19 ==
hindi-one HF 1 server 2 != [4894, 2063]
hindi HF 19 server 30 !=
...
hindi-one은 दी이며, Hugging Face는 이를 [44213]으로 인코딩합니다. 일본어(13), 아랍어(5) 및 파이썬 코드 한 줄(12)은 정확하게 일치했습니다.
동일한 서버를 --override-kv tokenizer.ggml.pre=str:tekken 옵션과 함께 시작했을 때의 결과는 다음과 같습니다:
english HF 19 server 19 ==
hindi-one HF 1 server 1 == [44213]
hindi HF 19 server 19 ==
...
GGUF 메타데이터를 tekken으로 다시 작성한 복사본은 동일한 전체 일치를 보였습니다.
놓치기 쉬운 이유
빠른 확인으로는 모든 것이 괜찮아 보입니다. 파일이 로드되고, llama-server가 시작되며, 채팅이 작동하고, 대부분의 사람들이 테스트하는 언어인 영어는 토큰 대 토큰으로 동일합니다. 코드와 일본어도 같았습니다. 차이는 두 분할 규칙이 의견을 달리하는 스크립트, 즉 여기서는 인도 문자(Indic scripts)와 태국어에 국한되며, 유일한 증상은 해당 프롬프트가 더 많은 토큰을 사용하고 모델이 훈련에서 본 적 없는 방식으로 분할되어 보인다는 것입니다. 이것이 답변에 얼마나 영향을 미치는지 여기서는 측정되지 않았으며, 짧은 프롬프트의 경우 작을 수 있습니다.
또한 이는 사람들이 이미 예상하는 것 뒤에 숨어 있습니다: 대부분의 토크나이저에서 인도 문자는 영어보다 더 많은 토큰을 사용하므로, 힌디어의 높은 카운트는 이상해 보이지 않습니다. llama.cpp가 추가한 부분은 Hugging Face의 카운트 옆에서만 나타납니다.
그리고 수정 사항을 테스트하는 명백한 방법이 잘못된 답을 제시합니다. llama-tokenize는 정확히 이를 위한 llama.cpp의 도구이며, --override-kv를 불평 없이 수락합니다:
$ llama-tokenize -m NVIDIA-Nemotron3-Nano-4B-Q4_K_M.gguf -p "दी" --ids --log-disable --no-bos \
--override-kv tokenizer.ggml.pre=str:tekken
[4894, 2063]
여전히 두 개의 토큰이며, 종료 코드 0입니다. 이 소스(tools/tokenize/tokenize.cpp)는 오버라이드 처리가 전혀 되어 있지 않은 반면, --no-such-flag와 같은 알 수 없는 플래그는 오류와 함께 거부되므로, 오버라이드는 구문 분석된 후 버려집니다. 위에서 보여준 것처럼 llama-server의 동일한 플래그는 작동합니다.
실제로 무슨 일이 일어나고 있는가
모델이 변환될 때, llama.cpp의 컨버터는 고정된 테스트 문자열을 분할하는 방식을 해싱하여 토크나이저를 식별하고, 알려진 토크나이저 테이블에서 해당 해시 값을 찾아 GGUF에 기록할 pre 이름을 선택합니다. 보고서에 따르면, Nemotron 토크나이저의 해시는 Pixtral-12B와 동일하므로 컨버터는 pixtral을 기록합니다. src/llama-vocab.cpp에서 pixtral은 llama3과 그룹화되어 Llama 3 pre-tokenizer를 받게 되는데, 이는 Pixtral에 대해서는 올바릅니다. Nemotron의 tokenizer.json은 Tekken 분할 패턴을 사용하며, llama.cpp에는 이 패턴이 별도의 이름인 tekken으로 존재합니다. 두 패턴은 영어와 코드에서는 동일하게 작동하지만 위 스크립트(문자)에서는 다르게 작동합니다.
여기서 테스트는 컨버터의 해시를 볼 수 없지만, 결과가 그 설명을 정확히 뒷받침합니다. 파일의 다른 모든 부분은 변경되지 않았고, pre 이름만 오버라이드하거나 파일을 재작성하여 변경하면 모든 샘플이 일치하게 됩니다.
보고서는 로드 시 수정(Nemotron 아키텍처인 경우 pixtral을 Tekken으로 처리)을 제안하며, 이는 이미 게시된 GGUF에도 오류를 수정할 것입니다. 10월 21일 기준으로 이 문제는 답글이나 풀 리퀘스트가 없습니다.
해결 방법
로드 시: 모델을 실행하는 곳에 오버라이드를 추가합니다.
llama-server -m NVIDIA-Nemotron3-Nano-4B-Q4_K_M.gguf --override-kv tokenizer.ggml.pre=str:tekken
또는 파일을 한 번 재작성합니다. llama.cpp의 gguf-py에 포함된 스크립트를 사용합니다:
python gguf-py/gguf/scripts/gguf_new_metadata.py --pre-tokenizer tekken \
NVIDIA-Nemotron3-Nano-4B-Q4_K_M.gguf nemotron-tekken.gguf
이렇게 하면 새 파일이 작성되고 원본은 그대로 유지됩니다. 이 2.8 GB 파일의 경우 약 5초가 걸렸습니다.
위 이유에 대해서는 llama-tokenize가 아닌 llama-server의 /tokenize를 확인하세요.
llama.cpp를 번들링하는 다른 프론트엔드(Ollama, LM Studio 등)는 자체적인 메타데이터 오버라이드 전달 방식을 가지고 있으며, 이는 여기서 테스트되지 않았습니다. NVIDIA의 Nano 4B 업로드만 테스트되었습니다. 이 보고서에는 또한 NVIDIA, ggml-org, Unsloth, bartowski, LM Studio에서 Nemotron 3 및 3.5 GGUF를 포함하여 pixtral을 가지고 있는 경우와, Unsloth에서 tekken을 가지고 있는 업로드가 나열되어 있습니다.
툴킷의 check-gguf-pretokenizer.sh는 GGUF 헤더를 직접 읽고(모델 로드 없음, 이 파일 기준 0.05초 소요) 다음 조합에 플래그를 지정합니다:
WRONG m/NVIDIA-Nemotron3-Nano-4B-Q4_K_M.gguf: architecture nemotron_h, pre-tokenizer 'pixtral' (llama.cpp는 이를 Llama 3으로 분할함); 'tekken'이어야 함
ok m/nemo-tekken.gguf: architecture nemotron_h, pre-tokenizer 'tekken'
--server <url> --hf-tokenizer tokenizer.json을 사용하면 실행 중인 llama-server로 다국어 샘플을 전송하고 이를 Hugging Face의 토크나이저와 비교하여, 이 모델뿐만 아니라 모든 모델에 대해 이러한 불일치를 포착할 수 있습니다.
일반화 가능한 습관
실제로 사용할 언어로 로컬 모델을 테스트하되, 단순히 대화를 나누는 것이 아니라 참조 토크나이저를 사용하여 테스트해야 합니다. 토크나이저 불일치는 눈에 띄게 무언가를 망가뜨리지는 않지만, 모델이 보는 것을 변화시킵니다. 각 언어의 몇 문장에 대해 토큰 ID를 원래 tokenizer.json과 비교하는 것은 몇 분밖에 걸리지 않으며 문제를 해결할 수 있습니다.
그리고 검사에서 수정이 작동하지 않았다고 할 때, 검사기 자체를 확인해야 합니다. 여기서는 도구가 테스트 중인 옵션을 조용히 무시했습니다. 플래그가 구현되지 않은 것을 받아들이는 도구는 올바른 수정을 잘못된 것처럼 보이게 만들 것이며, 서버를 통해 동일한 작업을 두 번째 방식으로 시도하는 것이 이를 보여주었습니다.
Ollama에서는 모델 이름에서 Gemma 4 프롬프트 템플릿을 가져오는 등, 다른 메타데이터 필드가 모델을 잘못된 동작으로 유도하는 사례가 발생했습니다: 모델 이름에서 Gemma 4 템플릿을 가져오는 Ollama.
툴킷
이 게시물의 수정 사항은 툴킷에서 테스트되고 바로 실행할 수 있는 스크립트로 제공됩니다.
툴킷 보기 →