AI Pulse

两块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_XL31.54112s1,481 tok/s
Q6_K_XL51.63168s1,582 tok/s
Q4_K_M91.52330s1,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/s65k时VRAM
q8_0 / q8_01.001.001.0030.129.9 GiB
f16 / f161.001.001.0030.431.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_fileslocal_implementlocal_edit_filelocal_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上似乎没有太大区别

阅读原文
📚 相关主题 本地大模型性能测试

订阅 AI Pulse

每天 08:00 · 12:30 · 18:30 · 23:50 更新