微调后的Qwen模型用vLLM部署,并发请求出现乱码,多数情况下问题不在模型权重,而在微调阶段产生的tokenizer文件没被vLLM正确使用。vLLM在启动时加载一次tokenizer,后续所有请求都共用这份配置;如果微调时新增过special token、改动过padding设置,或导出目录里的tokenizer_config.json、special_tokens_map.json、tokenizer.json与模型版本不一致,就会在批量解码时出现乱码。建议先做单请求复现,再检查tokenizer文件,最后调整vLLM加载方式,尽量避免一上来就重训或换模型。
并发乱码多为tokenizer配置与模型词表不一致,常见于微调后未重新保存tokenizer、special token ID冲突或vLLM加载时未显式指定tokenizer。处理方向是先单请求复现,再核对三份tokenizer文件,用微调脚本重新保存后显式加载。若单请求正常、仅并发乱码,需另行排查服务端状态共享问题,不能只改tokenizer配置。
先复现,区分“单请求乱码”和“仅并发乱码”
排查的第一步是确认乱码是否只在并发时出现。如果单请求已经乱码,说明tokenizer或模型权重本身有问题;如果单请求正常、并发才乱码,除了tokenizer配置,还要检查vLLM服务是否有状态共享或参数串扰。可以用下面这段脚本直接验证模型和tokenizer是否匹配:
from vllm import LLM, SamplingParams
from transformers import AutoTokenizer
model_path = '/path/to/finetuned-qwen'
tok = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
llm = LLM(model=model_path, tokenizer=model_path)
prompt = '请将下面句子翻译成英文:你好,世界。'
ids = tok(prompt)['input_ids']
out = llm.generate({'prompt_token_ids': ids}, SamplingParams(max_tokens=64))
print(tok.decode(out[0].outputs[0].token_ids))这段脚本用微调阶段的同款tokenizer编码后传入vLLM生成,绕过了vLLM对文本的二次tokenize。如果输出正常,说明模型和tokenizer本身完整,问题出在vLLM的加载配置;如果输出仍是乱码,需要回到微调工程复查tokenizer的保存过程。
核对微调后tokenizer的三处高发不一致点
微调后的tokenizer目录里,最容易出问题的文件是tokenizer_config.json、special_tokens_map.json和tokenizer.json。前两者记录special token的id与chat template,后者是Fast Tokenizer的完整映射表。三份文件只要有一份和模型权重对应不上,并发解码时就可能产生乱码。建议先做一次目录对比:
diff <(jq keys /path/to/original-qwen/tokenizer_config.json) <(jq keys /path/to/finetuned-qwen/tokenizer_config.json)
diff /path/to/original-qwen/special_tokens_map.json /path/to/finetuned-qwen/special_tokens_map.json重点看special token的id是否冲突、chat_template是否还沿用微调前版本、新增的特殊token是否真的写入词表。Qwen默认没有pad token,如果微调时把eos或某个特殊token设置成了pad,而vLLM加载时没有读到pad_token_id,批量推理的padding会错位,表现就是不同请求之间的生成结果混入无意义字符。
按顺序执行三步修复
第一步,用微调脚本同款方式重新加载并保存tokenizer,不要直接复制原模型的tokenizer目录。微调时如果调用过add_special_tokens,新增映射只存在于训练内存中,模型权重保存时不会自动写回tokenizer文件。正确的保存方式:
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained('/path/to/finetuned-qwen', trust_remote_code=True)
tok.save_pretrained('/path/to/finetuned-qwen')如果微调时有新增special token,保存前先补上add_special_tokens,再确认模型的embedding维度与tokenizer的vocab_size一致,否则vLLM启动时会报维度错误,那种情况先解决词表维度再回头看乱码。
第二步,vLLM启动时显式指定tokenizer路径,不要依赖模型目录里可能不完整的配置:
python -m vllm.entrypoints.openai.api_server \
`--model` /path/to/finetuned-qwen \
`--tokenizer` /path/to/finetuned-qwen \
`--trust-remote-code` \
`--served-model-name` qwen-finetuned参数名称以你部署的vLLM版本为准,核心是让tokenizer来源明确。trust_remote_code的设置要和微调阶段保持一致,避免远程代码版本不同导致特殊token解析结果不一致。
第三步,清理HuggingFace的tokenizer缓存再重启服务。Fast Tokenizer会把解析结果缓存在~/.cache/huggingface/tokenizers目录,修改tokenizer文件后重启服务仍可能命中旧缓存,导致每次请求都使用旧的token映射。清理这个目录不会影响模型权重,但需要重新加载一次tokenizer。
并发场景的验证清单
- 用本节开头的脚本做单请求验证,记录输出是否正常,作为基线。
- 启动vLLM服务后,用curl分别发少量和较多数量的并发请求,对比响应文本是否会随并发量变化出现乱码。
- 在服务端日志里打印乱码请求的prompt token_ids,确认是否混入预期之外的special token id。
- 用transformers对同一模型跑连续batch推理,对比结果;如果transformers正常而vLLM乱码,重点检查vLLM的tokenizer加载参数与版本行为。
- 如果以上步骤都正常但并发仍乱码,需要把排查范围扩大到请求缓存、负载均衡和客户端编码,而不是继续调tokenizer。