我把一个 3B 模型塞进了 Xinference,然后它干掉了 DeepSeek V3.2

我把一个 3B 模型塞进了 Xinference,然后它干掉了 DeepSeek V3.2
我最近干了件事。给 XorbitsAI Inference 提了三个 PR,把一个叫VibeThinker的小模型注册了进去。本来想着就是把一个普通模型加到内置注册表,写写 JSON、修修 Bug,平平无奇的日常贡献。结果我开始翻这个模型的论文。翻完之后,我整个人都不好了。一个 3B 参数的小模型,在 AIME26 数学竞赛上拿了 94.3 分。DeepSeek V3.2 是 94.2 分。你注意看。3B 对 671B。差了 200 多倍。它不是「接近」大模型。它是已经跟 DeepSeek V3.2、GLM-5、Gemini 3 Pro 坐在同一张桌子上了。而且更离谱的是,当你加上测试时扩展(CLR),这个数字会飙到97.1,直接反超 Gemini 3 Pro(91.7),跟 GLM-5(95.8)拉开差距。今天我打算把这件事从头捋一遍。为什么一个小到可以跑在笔记本上的模型,能在数学推理上干翻几百倍参数量的巨无霸?它的训练方法 SSP 到底做了什么?以及我是怎么把它注册进 Xinference、让所有人一键启动的。三个 PR,一条完整链路先说我自己干的活。PR #5085:模型注册[feat: register VibeThinker 1.5B/3B (transformers + vLLM)]这是核心 PR。在llm_family.json里为 VibeThinker 的 1.5B 和 3B 两个规模补全了注册信息。{"version":2,"context_length":131072,"model_name":"vibethinker","model_lang":["en","zh"],"model_ability":["chat","tools"],"model_description":"VibeThinker is a series of dense reasoning language models...","model_specs":[{"model_format":"pytorch","model_size_in_billions":"1_5","model_src":{"huggingface":{"quantizations":["none"],"model_id":"WeiboAI/VibeThinker-1.5B"},"modelscope":{"quantizations":["none"],"model_id":"WeiboAI/VibeThinker-1.5B"}}},{"model_format":"pytorch","model_size_in_billions":3,"model_src":{"huggingface":{"quantizations":["none"],"model_id":"WeiboAI/VibeThinker-3B"},"modelscope":{"quantizations":["none"],"model_id":"WeiboAI/VibeThinker-3B"}}}],"architectures":["Qwen2ForCausalLM"],"model_type":"qwen2","chat_template":"...","stop_token_ids":[151643,151644,151645],"tool_parser":"qwen"}每个字段的含义:model_format: "pytorch", 使用原生 PyTorch checkpoint,量化方案为none(全精度推理)model_size_in_billions, 支持字符串"1_5"(1.5B)和整数3(3B)两种表示model_src, 同时声明 HuggingFace 和 ModelScope 两个镜像源,国内用户可以从 ModelScope 加速下载architectures: ["Qwen2ForCausalLM"],架构声明为 Qwen2,因为这个模型基于 Qwen2 架构做后训练,推理引擎需要用 Qwen2 的方式加载chat_template, Jinja2 格式的对话模板,包含了完整的 tool calling 支持(tool_callXML 标签)同时把.inference_home/加进了.gitignore,避免本地开发目录被误提交。这个在 Xinference 开发场景下很常见,XINFERENCE_HOME会缓存模型权重和运行状态,动辄几十 GB。我为什么要做这个 PR?VibeThinker 是基于 Qwen2 架构的轻量推理模型,能在单张消费级 GPU(比如 RTX 4090)上跑。加入内置注册表后,用户不需要自己写自定义注册文件,直接:xinference launch --model-name vibethinker --size-in-billions3就能启动。用 vLLM 后端的话,推理速度非常快,单卡就能做高吞吐。PR #5086:DynamicCache 兼容性修复[fix: handle DynamicCache in get_batch_size_and_seq_len_from_kv_cache when HybridCache is importable]这是三个 PR 里最 tricky 的一个。修了一个 KV Cache 读取的兼容性 Bug,跟新版 transformers 的行为变更有关。问题根因:Xinference 里有个函数叫get_batch_size_and_seq_len_from_kv_cache,它的工作是从模型的 KV Cache 中反推当前的 batch size 和序列长度。这个信息对内存管理、批处理调度非常关键。原来的代码逻辑是这样的:# === 修复前的代码(简化版) ===defget_batch_size_and_seq_len_from_kv_cache(kv,xinf_model_obj):bs_idx=0# batch_size 在 shape 的第 0 维seq_len_idx=-2# seq_len 在 shape 的倒数第 2 维# 分支1:HybridCache(新版 transformers 的混合缓存)try:fromtransformersimportHybridCacheifisinstance(kv,HybridCache):returnkv.key_cache[0].shape[bs_idx],kv.get_seq_length()exceptImportError:# 分支2:DynamicCache / None(旧版 transformers)# ⚠️ 问题在这里:DynamicCache 的处理逻辑被嵌套在 except 块里ifkvisNone:return0,0ifhasattr(kv,"key_cache"):iflen(kv.key_cache)==0orkv.key_cache[0]isNone:return0,kv.get_seq_length()ifhasattr(kv,"get_seq_length")else0key=kv.key_cache[0]returnkey.shape[bs_idx],kv.get_seq_length()# 分支3:旧版 tuple 格式的 kv cache(兜底)returnkv[0][0].shape[bs_idx],kv[0][0].shape[seq_len_idx]+1Bug 的触发条件:当 transformers 版本 ≥ 4.57 时,HybridCache可以被成功导入。这时候:kv是一个普通的DynamicCache对象(不是HybridCache)isinstance(kv, HybridCache)返回Falsetry块没有异常,except ImportError整个被跳过代码直接掉到最后的kv[0][0].shape[...]但在新版 transformers 里,DynamicCache 不再支持下标访问(kv[0]),直接抛TypeError: 'DynamicCache' object is not subscriptable。整条链路就断了。Web UI 和 API 都无法正常获取运行中的模型信息。修复方案:# === 修复后的代码 ===defget_batch_size_and_seq_len_from_kv_cache(kv,xinf_model_obj):bs_idx=0seq_len_idx=-2# 分支1:HybridCachetry:fromtransformersimportHybridCacheifisinstance(kv,HybridCache):# 新增防御:key_cache 可能为空或第一层为 Noneiflen(kv.key_cache)==0orkv.key_cache[0]isNone:return0,0returnkv.key_cache[0].shape[bs_idx],kv.get_seq_length()exceptImportError:pass# 不再在 except 里处理 DynamicCache# 分支2:None(没有缓存)ifkvisNone:return0,0# 分支3:有 key_cache 属性的缓存对象(DynamicCache 旧接口)# ✅ 从 except 块中移出,独立判断ifhasattr(kv,"key_cache"):iflen(kv.key_cache)==0orkv.key_cache[0]isNone:return0,0key=kv.key_cache[0]return(key.shape[bs_idx],(kv.get_seq_length()ifhasattr(kv,"get_seq_length")elsekey.shape[seq_len_idx