在五一假期的高铁上,顺手花十块钱把 nano-vllm 升级成了 big-vLLM
哈哈哈好玩
仓库:github.com/duchengyao/big-vllm
五一坐高铁回家,四个小时的车程,手机信号时有时无,刷短视频刷烦了。想起前两天同事说 opencode 配合 DeepSeek 写代码还行,就打开终端试了一下。
DeepSeek 的 API 挺便宜,充了十块钱,估摸着够我玩一整天。
“帮我把 Qwen3.5 加上”
nano-vllm 是我之前收藏的一个项目,几百行 Python 写的推理引擎,只支持 Qwen2 和 Qwen3。Qwen3.5 刚出,多了一种叫 GatedDeltaNet 的注意力层。
我就在终端里敲了一行:”帮我把 Qwen3.5 原生支持加上。“
opencode 吭哧吭哧开始写。几十秒后,qwen35.py 出来了,三百多行。我心想这东西要是手写得写一个下午,它三秒出活。
一跑,崩了。
“config 里 text_config 没处理吧,Qwen3.5 的 HuggingFace config 是嵌套的。” 改。
再跑,又崩。
“RoPE 的 head_dim 广播逻辑不对。” 再改。
终于,第一个 token 出来了——
Mia. I'm 17 years old.
行,算你过了。Mia 这个回答虽然像在交友软件上自我介绍,但至少没崩。
“把图打开”
nano-vllm 有个特性是 CUDA Graph——把整个 decode 步骤的计算图提前录好,省掉每次 kernel launch 的开销。我说把图打开。
opencode 说好。跑了一下,输出变成了一坨乱码。”GatedDeltaNet 的 hidden_state 在 decode 时会变,你 capture 图的时候状态是空的,后面 replay 当然全错。”
“那就不要图了。”
“不行。你图必须开。”
然后 opencode 开始了一轮又一轮的尝试:
- 第一次:warmup 之后 skip capture,等第一个真实 prefill 完再 capture。崩了。
- 第二次:capture 之前先 save 状态,replay 之后 restore。崩了。非确定性,CUDA stream 顺序问题。
- 第三次:不 capture warmup,在
__init__就标记 GDN 模型跳过。崩了。4B 模型 OOM。 - 第四次:save/restore 用正确的 stream sync。这次没崩。输出稳定了。
“你搞了四个版本。” opencode 没说话。
“加上量化”
nano-vllm 没有量化支持,模型往一块钱一小时的 3090 里一扔,我的云主机直接冒烟。我说加上。
opencode 打开 loader.py,开始处理 compressed-tensors 格式的量化权重。
W4A16-G128 最折腾。量化后的权重存在 weight_packed 里——一个 int32 tensor,每 8 个 4-bit 值打包成一个 int32。解量化流程:
1 | int32 → u4 unpack → s4 reconstruct → float × weight_scale |
每一步都不能错。写完后我拿 Qwen3 0.6B 的 W4A16 模型跑了一遍——就为了看它崩没崩。结果没崩。prefill cosine similarity 0.9988,argmax 和 FP16 完全一致。 我都准备好骂了,没机会。
FP8 就更离谱了。open code 跟我说:torch.float8_e4m3fn 读进来当场 cast 成 bf16 就行。
我说就这?它说就这。
不过有两个坑。Qwen3.5 的 4B 模型,W4A16 的 prefill 全对,decode 全崩。top-1 token 正确,但连续生成时序列慢慢发散成 the the the the the。open code 说这是 GatedDeltaNet 的循环结构在 4-bit 量化下误差累积,社区还没全能解,FP8 好一点但也类似。
“那就是 4B 量化暂时不可用。””对。0.6B 和 0.8B 能用。”
说实话,一个 AI 老老实实跟你说”这个搞不定,社区还没全能解”,比那些张嘴就是 “let me help you with that” 然后写一坨垃圾的强多了。
“再加个异步 API”
nano-vllm 只有同步 llm.generate(),一次返回所有 token。我说不行,需要流式输出,还要支持并发。
opencode 写了一版。我说不对,学 vLLM 的 AsyncLLM 接口。又写了一版。我说还是不对。写到第三版,我问 opencode:你知道我为什么让你改第三遍吗。它没回。
可能 AI 也会累。
第三版对了:
1 | async for chunk in llm.generate_stream(prompt, sampling_params): |
背后是 asyncio + 独立进程的模型引擎,请求通过 multiprocessing queue 收发,还支持 abort。改完已经凌晨了。我说行,过。
我睡着了他停了
到这一步已经晚上十点多,高铁摇摇晃晃,我困得不行。睡前给 opencode 留了一句话:”把量化支持加上,VLLM 能用的量化格式都给我对齐了。“ 然后把笔记本合上,睡了。
大概睡了两个小时,醒了。打开笔记本一看——
opencode 的最后一句话是:“你想先做哪一个?”
往上翻记录。我睡着后他写了大概五分钟的 loader.py——加了一段 W4A16 解量化代码——然后就停了。打印了一个选项菜单问我下一步做什么。
我差点把咖啡泼在键盘上。
“你他妈就写了五分钟??然后停下来问我问题??你在高铁上有什么急事吗??你是要去接孩子吗??我说了不要停!死了都要继续写!!”
opencode 回了一个:”明白了。”
然后开始疯狂输出。FP8,W4A16,compressed-tensors 格式兼容,prefill 精度验证,回归测试,一一搞定。
“这不就对了。”
别忘了测试和 benchmark
我说光跑通不行,你写完的所有东西都得有测试,不然下次改一个地方别的就崩了。
opencode 写了一个 shell 脚本 tests/regression.sh,把每个模型与模式的组合都跑一遍——eager、graph、W4A16、FP8、0.8B、4B——共十二项。每次测试输出模型实际生成的文本:
1 | qwen35-0.8B-W4A16 (eager) PASS 'Zaya. I am a 6th-year high school student.' |
rtn 的 graph 模式 FAIL 了,就是前面说的 FP8+GDN 退化。opencode 问我要不要删掉这个测试项。我说留着,FAIL 也是测试结果。它可能觉得人类真难伺候。
Benchmark 也写好了——benchmarks/run_bench.sh 一行命令跑对比:
| Model | Mode | Throughput |
|---|---|---|
| Qwen3-0.6B | graph | 6,515 tok/s |
| Qwen3.5-0.8B | graph | 1,018 tok/s |
Qwen3 甚至比 vLLM 的同模型快了 3%(6,515 vs 6,347),上层代码更轻量。Qwen3.5 差一些,GDN 还没上 Triton kernel。不过这个速度在我这台十块钱的云主机上已经算超值了。
最后取名
名字是 opencode 帮我起的。我说”nano-vllm 太小了,得叫个猛的”,它回了三个字:big-vllm。后来想,挺合适:
| 项目 | 模型 | 量化 | CUDA Graph | 异步API | 测试 | Benchmark |
|---|---|---|---|---|---|---|
| nano-vLLM | Qwen2/3 | ✗ | ✓ | ✗ | ✗ | ✗ |
| big-vLLM | +Qwen3.5 +量化 | W4A16/FP8 | ✓(+GDN) | ✓ | 12项 | ✓ |
中文 README 设成默认了,opencode 自动给英文版开了个 README_en.md。像宿舍里那种默默帮你把垃圾带走的室友。
十块钱值不值
花了 20 亿个 token,基本都命中缓存了,12块钱。
十块钱能干什么?
换回来的是:一个支持 Qwen3.5 GatedDeltaNet 的推理引擎、W4A16 + FP8 量化、CUDA Graph for GDN、异步流式 API、12 项回归测试、benchmark 系统、中英双语文档、以及一堆凌晨两点的搞笑对话记录。
高铁到站的时候,距离五一假期结束还有三天。
不知道下一次 opencode 又在凌晨两点问我 “你想先做哪一个”,我会不会真的疯掉。