在五一假期的高铁上,顺手花十块钱把 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
2
async for chunk in llm.generate_stream(prompt, sampling_params):
print(chunk, end="", flush=True)

背后是 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
2
qwen35-0.8B-W4A16 (eager)   PASS 'Zaya. I am a 6th-year high school student.'
qwen35-0.8B-rtn (graph) FAIL: repeating

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 又在凌晨两点问我 “你想先做哪一个”,我会不会真的疯掉。