两块RTX 4090跑llama.cpp,最多能同时运行几个智能体?
我是一家中型非营利组织的CTO,我在本地运行一个Qwen技术栈,作为该工作的编码子代理——部分出于原则(我处理的相当一部分内容不应离开公司),部分因为批量代码工作的token费用在非营利预算上累积得很快。在三周时间里,我基准测试了四个模型和三种量化,以回答一个问题:在变得无用之前,实际上能同时运行多少个代理,在什么上下文下?
我之所以完整写出来,是因为我一直在评论中半回答这个问题。每当并发、量化选择或专家卸载被提及时,我最终都会凭记忆打出其中的片段,而回复总是某种版本的为什么——为什么用那个量化,为什么用那个槽位数量,为什么不直接加更多代理。评论线程中的片段不是回答这个问题的好方式,所以这里把整件事放在一个地方,包括数字和先产生错误数字的错误。
简短版本:我开始试图让一个122B的MoE变快,放弃了,最终落在27B上——不是因为27B更准确(可测量上并没有),而是因为其他一切都更好。然后我发现,超过某个点后添加代理根本买不到任何东西。
硬件: 2× RTX 4090(44.6 GiB可用),Threadripper TRX50,128 GB DDR5——重要的是,4条内存通道中只有2条被占用(2×64 GB),事实证明这关系重大。llama.cpp,Windows,全程q8_0 KV。
---
1. 为什么我没有保留122B
(上方图库中的图表1/4:测试的模型)
Qwen3.5-122B-A10B在UD-IQ4_XS下运行——18.75 tok/s解码,带专家卸载——但杀死它的数字是每次代理工具调用11.91秒,而27B是3.40秒。对于一个每次任务进行数十次调用的代理,每次3.5倍就是全部关键。
Qwen3.6-35B-A3B在原始解码上看起来是赢家(78 tok/s——它是一个3B活跃的MoE),并且每次调用时间最快。它也在5个执行代码任务中失败了1个,原因是滑动窗口错误,而27B是5/5。这就是“解码tok/s”是错误标题指标的全部原因,我改用每次调用秒数作为主要指标。
我也在Qwen3.8-Flash-Next(125B MoE,Qwen4架构预览)一发布时就试了它。 它需要一个未合并PR的构建,经过完整的调优扫描——专家放置、线程数、--poll、--cpu-strict、加载模式、llama.cpp自己的内存适配器——它最高达到23.7 tok/s。
上限不是GPU。在103.7 GiB的权重中,71.7 GiB是路由专家,26.8 GiB是一个2000万行的n-gram嵌入表,所以大部分驻留在系统RAM中,每个token都通过内存总线流式传输。在--n-cpu-moe 40下,我测量到14.1 GB/s的有效速度,而峰值是83.2 GB/s——因为四条通道中有两条是空的。一台带DDR4的单3090机器在绝对专家带宽上比我好。填充另外两条通道比我尝试的任何标志都更有价值,而且我无法以当前价格证明购买RAM是合理的。所以:搁置了,老实说,数字已发布。
---
2. 准确性:我无法区分这些量化
在并发数字之前,先说明使它们有意义的注意事项。
我的探针在长文档的15%、50%和85%深度处植入三个事实,有六行诱饵引用相同字段但针对错误地区,所以模型必须在数万个token中匹配标识符,而不是模式匹配标签。每个代理获得自己的文档,其中包含不重叠数字带中的自己的秘密,因此跨槽位泄漏是可检测的。
| 结果 | |
|---|---|
| 配置 | 24 |
| 低于1.0的召回率 | 0 |
| 错误地区答案 | 0 |
| 跨槽位泄漏 | 0 |
| 截断回复 | 0 |
| 完美回答的最大提示 | 251,557 tokens |
Q4_K_M、Q6_K_XL和Q8_K_XL在所有地方都得分1.00,包括九个代理同时运行时。
这是天花板上的平局,这意味着“无法区分”,而不是“相等”。 诚实的说法是:在长达251k token的长距离检索带干扰项下,这三种量化之间没有可测量的准确性差异。 这不是“Q4是无损的”。检索饱和了;需要跨植入事实综合的任务可能会在这里没有区分的地方区分它们。
所以量化选择归结为吞吐量、延迟和VRAM——而不是质量。
---
3. 并发结果
(上方图库中的图表2/4:并发扩展)
相同模型,每个代理相同64k上下文,三种量化在各自能容纳的最多槽位数下:
| 量化 | 代理@64k | 完成/分钟 | 中位等待 | 聚合预填充 |
|---|---|---|---|---|
| Q8_K_XL | 3 | 1.54 | 112s | 1,481 tok/s |
| Q6_K_XL | 5 | 1.63 | 168s | 1,582 tok/s |
| Q4_K_M | 9 | 1.52 | 330s | 1,572 tok/s |
将代理数量增加三倍,总吞吐量只改变了6%,而等待时间增加了三倍。
机制在最后一列:无论槽位数量如何,聚合预填充吞吐量恒定在大约1,500 tokens/秒。 这台机器有一个预填充预算。槽位分割它;它们不会倍增它。
这特定于这种工作负载形状,值得明确说明:代理提示巨大而回复短,所以预填充占主导。解码密集型工作负载会批处理得更好——解码受带宽限制,批处理将权重读取分摊到槽位上。不要将其概括为“并发对llama.cpp没有帮助”。
软上限和硬上限
- 软上限——64k下5个代理。 超过这个,添加槽位不再购买吞吐量,而是开始购买延迟。这不是失败,而是一个糟糕的交易:在9个代理时,你等待330秒才能获得在112秒时得到的相同1.5次完成/分钟。
- 硬上限——64k下9个代理,这是VRAM,不是计算。
(上方图库中的图表3/4:VRAM预算)
每个配置都落在44.6 GiB上限的3.5 GiB以内。较小的量化购买槽位,然后将节省直接花在KV缓存上:Q4相对于Q8释放了14 GiB权重,并将其中17.5 GiB交给缓存。硬上限是算术问题,不是调优问题。
单代理,巨大上下文
(上方图库中的图表4/4:上下文与延迟)
延迟与提示大小呈线性关系,量化之间几乎没有差异——拟合预填充率Q4 1,482 tok/s,Q6 1,346,Q8 1,340。10%的差距,而不是槽位数量可能暗示的3倍。Q8显示出更高的固定成本(12.8秒截距对比约5.5秒);每个量化n=6,所以将其视为观察,而不是发现。
我的操作点:UD-Q6_K_XL,5个并发代理,每个64k(总上下文327,680),41.1 GiB。 与Q4-at-9相同总工作量,每个代理等待时间减半,权重保真度更高,还有2.6 GiB更多余量。Q4购买的额外槽位只有在没有东西等待它们时才值得拥有。
---
4. KV缓存:q8_0,以及一个只是挂起的组合
在相同权重、65k上下文、代码针对隐藏测试执行下测试:
| K / V | 通过率 | 编辑保真度 | 长召回 | tok/s | 65k时VRAM |
|---|---|---|---|---|---|
| q8_0 / q8_0 | 1.00 | 1.00 | 1.00 | 30.1 | 29.9 GiB |
| f16 / f16 | 1.00 | 1.00 | 1.00 | 30.4 | 31.3 GiB |
没有可测量的质量差异,没有速度差异,便宜1.4 GiB。q8_0现在是我的标准。
陷阱: 混合的--cache-type-k f16 --cache-type-v q8_0从未完成长预填充——两次在900秒内没有答案,在一个小于匹配q8_0配置在32秒内回答的提示上。匹配的f16在24秒内回答了相同提示。匹配你的K和V类型。
---
5. 在Claude Code中作为子代理使用
首先值得知道的事情:Claude Code的model:字段只接受它自己的模型。 你不能将本地模型注册为Claude Code子代理。你可以做的是将其暴露为MCP服务器,这样它就变成编排器调用的工具。
我的是一个stdio MCP服务器,暴露local_ask_about_files、local_implement、local_edit_file、local_engine_status,指向127.0.0.1:8080上的27B。分工:
- 编排器计划和决定。
- 前沿子代理做需要一次就做对的工作。
- 本地Qwen回答“这些文件里有什么”,起草实现,并做批量编辑——高token、低风险的工作,否则会烧掉预算。
编辑保真度是决定这是否完全可用的指标。一个改写的区域会返回“找不到要替换的字符串”,这看起来像坏工具而不是更差的量化。在Q6加q8_0 KV下,它测量为1.00。
对于压力测试,我使用一个个人副项目——一个带Rust模拟服务器和薄UE5客户端的游戏。与日常工作无关;这是我为了乐趣扔给机器的东西,因为它按需产生真实的多文件、多语言代理工作。本周一个代表性的本地任务:在槽位4上生成10,127个token,速度29.8 tok/s,而其他槽位同时活跃。
---
6. 值得偷学的错误
每一个都首先产生了一个自信的错误数字:
1. 512 token的回复预算配合reasoning_effort: xhigh。 每个回复都在思考中途达到上限,所以被评分的“答案”是截断的推理。召回变成噪声,Q8看起来比Q4差。提高到2048;截断回复现在被排除而不是评错。
2. .NET的每主机2连接默认值。 我的“16个并发代理”测试工具悄悄地在一次运行2个。它通过了测试,因为我在PowerShell 7上验证(HttpClient,没有这种限制),然后部署到Windows PowerShell 5.1(HttpWebRequest,有限制)。运行空间是真实的;套接字不是。
3. 以4字符/token估算token。 实际比率是2.99,所以一个“39,599-token”的提示实际上大约是53,000。调用/tokenize。
4. 一个丢弃异常的错误处理器。 返回一个裸的Failed=true,丢弃了消息、HTTP状态和llama.cpp的响应体。大约40分钟的假设仅仅因为那个而存在。
5. general.file_type在Unsloth UD量化上撒谎。 我的Q6_K_XL文件在元数据中报告Q4_K_S。文件大小(同一模型上Q4_K_M / Q6_K_XL / Q8_K_XL分别为15.3 / 24.1 / 29.3 GiB)确认量化与文件名一致——枚举只是对动态混合没有值。不要基于该字段进行基准测试。
---
限制
单机、单工作负载形状、单模型家族。以上所有内容都是预填充主导的代理流量;聊天或长生成工作负载会以不同方式扩展,可能随槽位更好。Flash-Next数字来自未合并的PR构建,应在上游落地后重新测量。我的内存总线只有一半被占用,这限制了这里所有CPU卸载结果——如果你有四条通道填满,预期MoE数字会比我好。
如果某人想要数据点,我很乐意回答问题或运行特定配置。
―― 高票评论(帖子共 15 条讨论)――
[+10] u/__JockY__: 天哪,真是垃圾,蝙蝠侠。
[+9] u/Pixer---: 这些解码速度值看起来真的很慢。使用docker配合sglang/vllm至少能让你获得2倍的预填充和解码。我的2张5060ti在nvfp4下能获得约44tk/s,3张5060ti在FP8下不启用mtp能获得30 tk/s。这些数字来自sglang和htsglang的tp3。在llamacpp中,q8_k_xl我得到1k预填充,所以你在sglang上使用docker可以获得约4k预填充和65 tk/s解码。
你运行这些代理解决了什么问题?
[+2] u/Firm-Luck2062: 值得在并发表中添加一列:每个提示实际预填充了多少与重用了多少。代理提示大部分是固定前缀——系统、工具、历史——llama.cpp只有在请求落在缓存已经以该前缀开头的槽位上时才重用。更多槽位意味着代理在它们之间轮换,命中率下降,你已经支付的工作被重做。这会夸大你正在测量的确切聚合值,看起来很像固定预填充预算。
服务器日志给出每个请求的处理token与缓存token。如果缓存比例随着槽位数量增加而下降,上限是槽位亲和性而不是机器——值得在确定一个预填充预算之前,测试固定的代理到槽位路由,或者在--parallel 1下使用单独的服务器。
[+1] u/Toooooool: 你应该为Qwen3.8-27B找一个好的Q4,因为我在2x3090上得到约40T/s。
那个,而且是的,你应该切换回vllm,它更适合并发。
[+1] u/Liberaces_Isopod: 你表现严重不足。你可以用那套配置在Q3下运行qwen3.8 flash next。使用ik_llama,单张3090加cpu/ram卸载,我得到约35t/s。你应该能获得比那好得多的数字,因为你可以让更多专家在GPU上。
ik_llama项目是为cpu卸载量身定制的,在这方面比vanilla llama.cpp工作得好得多。
最好的做法是告诉你的Claude(或任何东西):“使用ik_llama,设置ji-farthing/Qwen3.8-Flash-Next-ik-llama-GGUF,带ram/cpu卸载。”
[+1] u/AI_spell: 解码死亡的软上限比最大槽位更重要。超过有用并发后的额外代理只是分担痛苦。
[+1] u/eihns: 我有3090 24gb和4090 24gb——在t/s上似乎没有太大区别