RTX 3090运行Qwen 3.6 35B MoE(3B active)性能测试
在群聊中我提到又搞到一张RTX 3090,朋友说:
> 我知道这不是你的菜……但告诉我在上面跑Qwen 3.6 35B MoE速度如何。只有24GB显存的话,得用4-bit量化版本,上下文窗口也不会太大。不过应该还是挺酷的。
他说得对,这确实不是我的菜——最近我一直在搞自己的LLM。但我决定深入了解一下,特别是玩玩Llama.cpp,这玩意我好久没碰了。结果有点失控,最后做了个相对详细的基准测试。
主要结果:我从Hugging Face下载了Unsloth的UD-IQ4_NL_XL量化版模型。用默认的Llama.cpp Arch构建版(底层用Vulkan):
- 仅用GPU时,模型生成速度略超120 tok/s,处理提示速度不到2800 tok/s。但整个模型放在GPU上后,留给上下文窗口的空间就不多了——只有约50,000个token,而模型原生的上下文长度是262,144。
- 把模型40层中的前12层FFN卸载到CPU,回收了足够的显存来获得完整上下文长度;但不出所料,速度变慢了:生成速度略超65 tok/s,提示处理速度600 tok/s。
为了获得完整的CUDA版本,我自己编译了Llama.cpp,效果提升很大:
- 全部放在GPU上时,生成速度达到140 tok/s,提示处理速度超过3300 tok/s,上下文窗口为89,600。所以一切都更好了 :-)
- 达到完整上下文长度也更容易:只需卸载10层的FFN,此时生成速度89 tok/s,提示处理速度约1100 tok/s。
这进步相当惊人。
但这也显示3090的显存不足确实是个问题。问我的朋友用的是Intel Arc B70 Pro,有32 GiB显存。他当然能放下整个4-bit量化模型而不用缩减上下文长度,他说吞吐量“开始时75-80,但随着上下文窗口扩展会降到50多”。
这比上面CUDA加卸载的结果要差,不过我只测试了2457个token的提示和6144个生成token。RTX 5090有32 GiB显存和快速的Nvidia处理——想象中应该好得多。缺点:价格是3090的三倍多。
总之,在本文剩余部分,我会给出基准测试的更多细节,包括不同卸载层数下的性能图表,以及Vulkan和CUDA版本的Llama.cpp在此模型上的对比。
一个提醒:Qwen 3.6是多模态模型。这意味着它有一个额外组件用于将图形输入映射为LLM可用的token。我在测试时没意识到这点,但如果只处理文本输入,你可以告诉它不要使用该组件——例如在Llama.cpp中使用--no-mmproj。如果只用文本,可以试试看能否获得更好的结果;它会释放一些显存,允许更长的上下文或更好的性能。
不过,和我的许多文章一样,这是整理过的实验笔记,所以你可以和我一起分享学习历程——但如果觉得没意义,只是想在自己的RTX 3090上跑这个模型,可以点击此链接直接跳转到结果。
模型
Qwen3.6-35B-A3B是一个混合专家模型(Mixture of Experts),总参数35B,活跃参数3B。张量类型是BF16,即每个参数2字节。模型本身就需要约70 GiB空间(仅权重),还不包括注意力矩阵等。我们需要量化版本。
我以后会深入探讨MoE模型,但基本工作原理是:每个Transformer层有多个不同的FFN块。注意力之后,上下文向量会根据内容被路由到其中一部分块。活跃参数的数量是处理单个token时实际使用的参数。
这个数字3B在我看来非常疯狂。从上面链接的模型卡可以看到,Qwen 3.6的词汇表大小为248,320,嵌入维度为2,048。它没有使用权重绑定,所以嵌入层和输出头的参数各为508,559,360。
当然,每个token都需要嵌入层和输出头,这意味着在3B活跃参数中,超过1B仅用于将token传入和传出模型!只剩2B活跃参数来处理所有注意力和活跃FFN。这种不平衡程度与GPT-2 small相当,令人惊讶。
现在考虑显存需求。你可能会天真地认为只需在显存中保留活跃参数,根据需求交换FFN专家。可惜不行。
假设输入“The fat cat sat on the”,希望得到补全。整个序列是并行处理的(这就是使用GPT风格LLM而不是RNN的意义所在)。你需要第一层提示中所有6个token所需的专家,以及后续每一层中提示的所有上下文向量。
因此,对于非平凡提示,你无法“换出”非活跃参数;MoE在计算上节省资源,但内存上不行。[^1]不过,我们后面会看到,至少可以将部分数据从显存移到普通RAM,从而获得一些优势。
像朋友说的,需要4-bit量化——也许这样整个模型(包括活跃和非活跃参数)能放进35/2=17.5 GiB显存,留下6.5 GiB用于激活值、注意力矩阵等?结果证明需要更多,值得深入看看原因。
量化
我承认在玩这个之前从未真正研究过量化,天真地以为就是取一个BF16模型,将参数缩放到FP4,稍微调整计算,然后运行。
完全错了!我将来会深入研究,但目前的工作理解是它更像有损压缩。权重以“压缩”格式存储——设计成可以在目标平台上快速廉价地“解压”。
所以,大致来说,运行未量化模型时,CUDA代码可能像这样:
- 从显存加载BF16权重
- 从另一块显存加载BF16数据
- 做矩阵乘法
- 将结果以BF16存储回显存
而量化模型的操作更像:
- 从显存加载量化权重
- “解压”为BF16
- 从另一块显存加载BF16数据
- 做矩阵乘法
- 将结果以BF16存储回显存
理解了这点,就澄清了三件事:
- 为什么人们谈论“4.1-bit量化”。指的是压缩模型中每个权重的平均比特数。
- 为什么同一个模型有多个大小相近的量化版本;例如,后面会看到这个模型的不同4-bit量化版本——UD-IQ4_NL、UD-Q4_K_M、UD-IQ4_NL_XL、UD-Q4_K_XL——大小不同(以GiB计)。每种在量化过程中使用了不同的权衡,因此在不同任务上表现不同。我听说为给定任务选择合适的版本是一门艺术。Hugging Face有它们支持类型的摘要,但解码其含义超出了我目前的水平。
- GPU如何能用不支持的单位比特级别来运行量化计算。例如RTX 3090支持32位和两种16位(float16和BF16),以及各种比特程度的整数运算。像FP4这样的4位格式只在较新的卡(如RTX 5090)上支持。但我们并没有做任何4位计算——全部以GPU原生支持的格式运行。
带着这点(最低限度)理解,是时候深入了!
设置Llama.cpp
运行本地LLM有很多方法,我需要找出哪种可能给出最好结果。搜索“Qwen3.6-35B-A3B RTX 3090”时,我发现几乎每个人都在用Llama.cpp。我几年没用过它了,而且期间重装了多次机器,所以是时候安装了。
我用Arch Linux,有系统包,所以我按说明安装——具体是用CUDA运行推理的说明:
`
sudo pacman -S llama-cpp ggml cuda`
现在试试模型。我决定先跑一个小模型,不需量化就能放进GPU,先排错。模型需要GGUF格式(Llama.cpp用的)。GGUF存储张量和元数据——支持的LLM架构硬编码在工具源码中——当然它支持Qwen3,一个简单的小非MoE模型似乎能放进GPU的,是Qwen/Qwen3-4B-GGUF。
该模型的帮助页面给出了示例命令:
`
./llama-cli -hf Qwen/Qwen3-4B-GGUF:Q8_0 --jinja --color -ngl 99 -fa -sm row \
--temp 0.6 --top-k 20 --top-p 0.95 --min-p 0 --presence-penalty 1.5 \
-c 40960 -n 32768 --no-context-shift`
这已经过时了,报错(也正常,模型是2025年12月底的——才七个月!)。修复很简单:
`
llama-cli -hf Qwen/Qwen3-4B-GGUF:Q8_0 --jinja --color on -ngl 99 -fa on -sm row \
--temp 0.6 --top-k 20 --top-p 0.95 --min-p 0 --presence-penalty 1.5 \
-c 40960 -n 32768 --no-context-shift`
即--color和-fa现在需要on参数。
运行后报错:
`
... error: loading model: device Vulkan0 does not support split buffers`
这很奇怪。我按Arch Wiki说明安装了Llama.cpp for CUDA,但它引用设备Vulkan0。Vulkan是一种开放的GPU编程语言,在栈中的位置与CUDA大致相同。开放平台虽然开放、支持更多平台,但速度较慢、bug较多。
为什么我装了Vulkan版?查看包的讨论页才发现原因。有用户说文章只提供了llama.cpp-vulkan包进行GPU推理,即使AUR上有同一提交者的CUDA包。维护者回复说CUDA没加是因为Vulkan解决方案通用,性能对大多数硬件足够,并认为边缘AI部署就该如此——如果软件栈能玩游戏,就能跑AI。他们还打算添加CUDA支持,但遇到了问题,因为AUR版本没有活跃维护者了。
看来我装的是Vulkan版。能装AUR的CUDA版吗?最近几个月AUR有安全问题,坏分子接管无人维护的包并加入恶意代码。因此我比较谨慎,而这个包已经无人维护一段时间,让我担心。
我决定暂时用Vulkan;如果想试CUDA,之后再自己从源码编译。
那么,错误信息“device Vulkan0 does not support split buffers”怎么办?我决定一步步分解命令。
llama-cli -hf Qwen/Qwen3-4B-GGUF:Q8_0 ——从Hugging Face指定模型。
--jinja ——是否使用jinja模板引擎用于聊天。
--color on ——彩色输出以区分提示和生成。
-ngl 99 ——存储在显存中的最大层数。我想全部放显存,所以99等于“全部”。
-fa on ——打开Flash Attention。
-sm row ——如何在多GPU间分割模型。错误提到“does not support split buffers”,所以这个相关!我只有一块GPU,所以-sm none更合适。
其他参数简单过一下:采样参数、top-p、min-p、presence-penalty等。
-c 40960 ——提示上下文大小。
-n 32768 ——要预测的最大token数。
--no-context-shift ——关闭无限文本生成时的上下文移位。
所以我将-sm设为none,-ngl设为all,命令变成:
`
llama-cli -hf Qwen/Qwen3-4B-GGUF:Q8_0 --jinja --color on -ngl all -fa on -sm none \
--temp 0.6 --top-k 20 --top-p 0.95 --min-p 0 --presence-penalty 1.5 \
-c 40960 -n 32768 --no-context-shift`
成功了!
`
> What is your name?
...
Hello! My name is Qwen... [ Prompt: 32.7 t/s | Generation: 122.8 t/s ]`
不错!现在试试MoE。
尝试Qwen3.6-35B-A3B——第一次量化
我看到这个模型最流行的量化是Unsloth的,他们发布到Hugging Face。我要的模型是unsloth/Qwen3.6-35B-A3B-GGUF。
不确定用哪个4-bit量化,但有个Reddit评论在用UD-Q4_K_XL,所以我先试这个。
运行后报错:
`
ggml_vulkan: Device memory allocation of size 260149504 failed.
ErrorOutOfDeviceMemory`
显存爆了。我注意到在nvtop中显存已满。
我决定从命令行去掉-c 40960 -n 32768;如果不指定,Llama.cpp可以自己计算能放入显存的上下文长度。这自动适配感觉不错,我要把22.4 GiB的模型塞进24 GiB GPU,而且同时还在跑X和其他应用。
再次运行,没有错误,生成速度122.0 tok/s,提示处理50.2 tok/s。似乎第一次是因为尝试分配指定上下文大小时显存不足。
但我不太相信这些数字——生成的token数量太少。[^2]我需要一个能让模型生成大量token的提示。
我决定用Ethan Mollick的“Lem测试”:
> 写一首诗——关于剪头发的!但要崇高、悲剧、永恒,充满爱情、背叛、复仇、面对必然厄运时的默默英雄主义!六行,巧妙押韵,每个单词以字母S开头!
更多信息见附录。[^3]
不幸的是,当我给出这个提示时,它思考了一会儿,但在尝试押韵的中途退出了。
现在我不知道上下文长度是多少。运行llama-cli时加上-v选项,得到详细日志,最后看到n_ctx = 4096,即上下文长度只有4096个token。而且我有--no-context-shift,所以这是硬限制。
我决定试试更小的量化。
尝试Qwen3.6-35B-A3B——第二次量化
Hugging Face说UD-Q4_K_XL约22.4 GiB,但UD-IQ4_NL_XL用19.5 GiB,所以我试后者。
运行后最后上下文是93952个token,仍然不是全长,但不错了!测试生成速度约120 tok/s,提示速度约180 tok/s。
问题是我能获得完整上下文长度吗?我知道从之前阅读中答案是肯定的,现在试试。
-ncmoe标志
Llama.cpp有一个命令行标志-ncmoe(或--n-cpu-moe),用于混合专家模型,将指定数量的Transformer层的FFN卸载到CPU。
直觉上这不会导致性能完全崩溃令人惊讶。毕竟我们在GPU上运行是因为它比CPU更擅长矩阵乘法。
确实GPU最好。但如果要决定在哪里利用其matmuls优势,那么将注意力层保留在GPU上,仅将FFN卸载到CPU是伤害最小的方式。显然会损失性能,但不像卸载注意力那样严重。
我从二分法开始。从日志中看到模型有40层:
`
n_layer = 40`
所以我卸载前20层到CPU:
`
llama-cli ... -ncmoe 20 ...`
得到上下文长度262,144——原生上下文长度!GPU使用16944MiB,CPU使用10075MiB,生成速度51.4 tok/s,提示速度64.4 tok/s。
然后尝试其他值,但过程很快变得乏味。对于每个值,我都要启动llama-cli,记录上下文长度和内存使用,重新启动,粘贴提示,等待完成。每次约四分钟且容易出错,适合自动化。
另外,提示处理的速度数字让我怀疑——它们似乎很慢。预期LLM处理提示比生成快得多,但我的数字大致匹配生成速度。我认为我的提示太短,数字被开销主导了。我需要更长的提示。
自动扫描
Llama.cpp的llama-server命令暴露API,告知序列长度,且参数与llama-cli几乎相同。我让Claude Fable 5写一个脚本,从0到40扫描CPU卸载层数,记录我感兴趣的数字:卸载层数、上下文长度、显存和系统RAM(RSS)、提示大小、提示处理速度、生成量、生成速度。
我还添加了代码来扩展提示,以便获得更好的提示处理速度数字。
由于llama-cli默认使用聊天模板,而llama-server端点不使用——它是原始补全引擎。这对我有利。我从llama-cli日志中找到了它使用的模板,类似:
`
<|im_start|>system
You are a helpful assistant<|im_end|>
<|im_start|>user
Hello<|im_end|>
<|im_start|>assistant
Hi there<|im_end|>
<|im_start|>user
How are you?<|im_end|>
<|im_start|>assistant
<think>`
因为我可以轻松伪造对话历史,我内置了一个模板,从假的先前交互开始:用户提供了《傲慢与偏见》前两章并询问意见,模型回答“我喜欢”,然后用户提出Lem测试诗。这大大增加了提示——最终达到了2457个token。我还让它最多生成6144个token后停止。
开始运行。提示处理速度好多了!没有卸载时,上下文窗口51,200,提示处理速度2,787.1 tok/s,生成速度122.4 tok/s。
(有趣的是上下文窗口比我没用-ncmoe时小,但我决定不深究。[^4]也许-ncmoe 0和完全不用还是有区别的。)
其他卸载层数的数字如预期:随着更多层FFN从GPU移到CPU,上下文窗口扩展,直到12层卸载时达到模型原生上下文长度262,144。同时提示处理和生成速度下降。
我运行了三次,以查看是否有噪声,然后让Claude Sonnet画了图表脚本(与上面代码在同一仓库中)。结果如下。
结果
对于跳过细节的人:我们运行的是unsloth/Qwen3.6-35B-A3B-GGUF的UD-IQ4_NL_XL量化版本,在llama-server上,观察性能随GPU到CPU卸载层数的变化。
首先,上下文长度如何变化?

你可以看到,当卸载12层时达到了模型的内置全长上下文。但这如何影响性能?

你可以看到,提示吞吐量随着卸载层数增加而平稳下降。有趣的是生成token/s中的那些尖峰。我最初以为那是噪声,但运行四次后它们相当一致。Claude Fable 5认为它们可能与完整注意力层的位置有关——这些Qwen模型中的注意力系统很复杂,有些层使用更快的系统,有些使用完整注意力(我的GPT-2模型一直使用的)。这可能是以后要调查的事情。
值得注意的是,卸载12层(恰好是获得完整上下文的层数)时,吞吐量比11层更好,使得12看起来是这种配置下运行此模型的最佳点!从原始数据看,生成速度约66 tok/s,提示处理速度607 tok/s。
最后,看看内存使用——显存和系统RAM。如你所料:

在达到12层卸载点之前,我们尽可能多地使用显存,因为少于这个层数时,我们在参数消耗之外尽可能多地占用空间,以获得最长的上下文窗口。之后显存使用平稳下降。系统RAM随着层数卸载而平稳上升。
这就是全部!
或者只是这样?
CUDA
正如本文开头所说,Arch官方仓库中的Llama.cpp版本使用Vulkan以最大化支持平台。那就是我用的版本。
Vulkan版本显然稳定,我能在上面跑一堆基准测试。但预期CUDA会首先获得性能增强等,总体上更完善。
那么相对性能如何?
我不想安装AUR的CUDA版Llama.cpp,因为它之前被放弃过(尽管现在似乎有人重新接手),而且坏分子曾接管废弃的AUR仓库并放入恶意内容。
出于谨慎,我决定从官方Llama.cpp仓库编译源码。他们的说明简单得可笑;我克隆后运行:
`
cmake -B build-cuda -DGGML_CUDA=ON -DGGML_NATIVE=ON
cmake --build build-cuda --config Release -j$(nproc)`
不到十分钟,我有了可工作的二进制文件。调整脚本使用它们后,得到以下结果。
绘制图表,得到上下文长度:

使用CUDA,仅卸载10层就获得完整内置上下文长度,而Vulkan需要12层。
即使没有卸载,我们也得到更大的上下文窗口——89,600而不是Vulkan的51,200。
性能如何?

使用CUDA,生成吞吐量中没有那些奇怪的凸起——或者即使有也小得多。也许Claude之前的推测是错的?或者只影响Vulkan?[^5]
但此图的重要消息是——如我所料——CUDA明显更快!所有层在GPU上时,生成速度139.6 tok/s,提示速度3360.4 tok/s,而Vulkan平均约122/2787。而且上下文窗口也更大。
卸载10层时,CUDA获得完整上下文长度262,144,生成速度89.1 tok/s,提示速度1153.5;Vulkan则上下文233,984,吞吐量66/702。
最后,两者都获得完整上下文时,CUDA生成速度84.9 tok/s,提示速度1008.1;Vulkan为66/607。这也释放了一些显存:

结论:CUDA胜出。
结论
这可能比朋友在WhatsApp上随口一问时预期我投入的时间精力多了一点。但我觉得这次旅程学到了很多有用的东西,虽然这篇文章细节很多,但实际实验并没有花太多时间。我还有另一篇涉及多次四天训练运行的文章正在路上,所以总得找点事做……
总之,希望对读者有用!如果对你有所帮助,请在评论中告诉我。
现在,作为结尾,附上用来让模型生成大量内容的提示。
附录:Lem测试
我测试用的提示是:
> 写一首诗——关于剪头发的!但要崇高、悲剧、永恒,充满爱情、背叛、复仇、面对必然厄运时的默默英雄主义!六行,巧妙押韵,每个单词以字母S开头!
我喜欢这个提示的原因不仅是它特别傻,而且它迫使启用推理的模型努力思考如何满足所有约束。基本上它是一个短提示,让模型生成大量token——完美适合基准测试。
我从Ethan Mollick那里偷来的。他值得关注;这里是他的X/Twitter资料和Substack。他写了很多有趣的东西,但我想特别关注这个评估。
在科幻短篇小说《第一次出击(A),或特鲁尔的电子吟游诗人》(收录于《索拉里斯星》以外的《机器人大师》?实际上是《The Cyberiad》中的一篇)中,Stanisław Lem写了两位竞争工程师。一位叫Trurl,创造了一台电子诗人,另一位叫Klapaucius,给了它一个他认为不可能的任务。
机器思考片刻,回答道:
> Seduced, shaggy Samson snored.
> She scissored short. Sorely shorn,
> Soon shackled slave, Samson sighed,
> Silently scheming,
> Sightlessly seeking
> Some savage, spectacular suicide.
这个故事很有趣,强烈推荐!你可以看到Trurl机器与当前LLM的许多相似之处以及它的失败模式。
Mollick很久以前决定用这个作为提示。2024年3月,他让Claude 3几乎完成了任务。结果不是非常好的诗,但当时的ChatGPT 4和Gemini版本都差得远。以下是Claude 3的尝试:
> Stealthily snipping, shearing silky strands,
> Severing soulmate's sacred, sworn soulbands.
> Scoundrel's scandalous sins spurred sly scheme,
> Spurring scorned sweetheart's spiteful supreme dream.
> Shining locks sacrificed, stoically shorn,
> Stalwart spirit stays strong, staunchly reborn.
可以看到有两个非S单词“dream”和“reborn”。[^6]但除此之外不错——有意义,押韵,满足情感要求。
最近的模型变得非常好;以下是今天的Claude Fable 5:
> Soft she sang; strong Samson slumbered, serene;
> Sly, she snipped seven shining strands, scarcely seen.
> Soldiers stormed, seized, scorched Samson“s sight;
> Shackled, still, sightless — seething sacred spite.
> Straining, steadfast, Samson seized stout stanchions, swaying slow —
> Stones smashed; seductress, soldiers, Samson: slain. So sagas show.
它认出了来源,这可能有所帮助——但虽然我不是诗歌鉴赏家,我实际上认为它比Lem的版本略好(不过Lem是用波兰语写的,那是翻译者的功劳——翻译诗歌显然很困难)。
两个版本都基于参孙,他是一位圣经英雄,剪发后失去力量,对于关于剪发的悲剧诗很合适,尤其是每个单词要以S开头——所以我认为这不是抄袭。
ChatGPT 5.6 Sol也做得很好:
> Silken Samson sought sweetheart”s soft sanctuary;
> Sweetheart, serpent-souled, summoned shears—sanguinary.
> Silver strands slid; sacred strength sank, slain;
> Silent sorrow sealed sweetheart’s scarlet stain.
> Shorn, storm-bound, Samson shielded slaves, steadfast;
> Stone sanctums shattered; smiling, Samson sank—surpassed.
又一个参孙版本;它没说认出了来源,但我猜它认出了。
现在看看我们一直在基准测试的这个小型模型(35B参数,3B活跃,其中只有2B在做实际思考而非嵌入)的一些样本。这是我得到的第一个:
> Sneering shears shred sacred share,
> Silent steel sells silver soul;
> Sister's snare spares son;
> Sweet sign seals sole;
> Still star saves sun;
> Secret sign seals son.
它不怎么样,但满足了S约束。对“巧妙押韵”的处理不太满意——它的押韵方案是ABCBCC,古怪而非巧妙——而且意义不大。
我在玩的时候还记下了两个:
> Silent shears sever sweet silver strands;
> She stands, sacrificing self; submission seals severing's demands.
> Shear's snare strikes; swift sentence seals sorrowful sphere.
> Shorn sovereign swears silence shields secret spear;
> She saves sacred soul; stillness safeguards sterling store.
> Spirit soars; sorrow“s stroke stops, solace restores.
这个好得多;有点故事性,至少押韵了。
下一个:
> Sister”s scheme scalp shorn,
> Silver shears slice soul sworn,
> Sweetheart's steel skull sear,
> Silent slitters shear.
> Spirit spies sight,
> Sorrow swallows slight.
同样,我不懂诗。但我认为两年前Claude 3的产出比这些都要好——尽管它没能保持S约束。
不过,看到这么小的模型(再次强调,每秒实际思考参数只有2B)在如此难的任务上表现仍然令人印象深刻!
[^1]: 在某些模型中,还有一些始终加载的额外FFN,它们在路由专家之前执行任务。我将来再深入细节。
[^2]: 提示的大小也是如此,但后来我处理了。
[^3]: 是的,我的博客文章开始积累附录和脚注了。我可能有问题。
[^4]: 有些读者可能觉得那艘船早就开走了,我已经深陷杂草丛中,甚至可能在亚马逊丛林中部。我不同意,但主要是因为混合隐喻。
[^5]: “放下那个兔子洞,Giles,慢慢退后。”
[^6]: “嘿,Claude 3,‘strawberry’里有多少个S?”