跳转至内容
  • 版块
  • 最新
  • 标签
  • 热门
  • 用户
  • 群组
皮肤
  • 浅色
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • 深色
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • 默认(LCZ-Blue)
  • 不使用皮肤
  • LCZ-Green
  • LCZ-Blue
  • LCZ-Black
折叠
品牌标识

抡锤者

首页 版块 标签
硬件
装备秀
AI 广场 App
Edmund TanE

Edmund Tan

@Edmund Tan
体面乡绅
取消关注 关注
关于
帖子
3
主题
2
分享
0
群组
1
粉丝
0
关注
0

帖子

最新 最佳 有争议的

  • 4 X Mac Studio M3 Ultra 256GB 真实日志
    Edmund TanE Edmund Tan
    LLM讨论区 mac apple-m3 多卡部署

    大家好,我的第二个分享 苹果跑AI 的真实日志,虽然现在已经出M5 Ultra 了但是已经很多博主分享过了他们的优势,哪怕性能提升不少(预填充) 但是如果想要跑多并发Agent 开发的话 以下还是对M5 有很好的对比的。至于有考虑苹果二手机的兄弟们 可以先看完我的日志再做打算吧。

    2026 年 7 月 7 日 终结这四台萍妃们😂。 卖了。

    image.jpeg
    image.jpeg

    四台 Mac Studio:一套 4× M3 Ultra 的 MLX 集群

    四台 Mac Studio、1 TB 统一内存、一张 Thunderbolt 5 全互联网络,四台机器合跑同一个模型——跑在我们自己写的推理引擎上。

    四台 Mac Studio:4× M3 Ultra MLX 集群

    我们手上有四台 Mac Studio,还有一个值得解决的问题。

    问题是这样:一个 3000 亿参数的模型放不进一台机器。单台 256 GB 的 Mac Studio M3 Ultra,勉强能在说得过去的精度下跑个 70B。想跑真正大的模型——那种能改变你能做什么的模型——你需要比任何一台电脑都多的内存。

    于是我们做了那种"有四台 Mac、又没钱进机房"时才会做的事:把它们摞在桌子上,用线连起来。

    六根线,和一大堆天真的希望

    四台 Mac Studio M3 Ultra,每台 256 GB 统一内存,合计 1 TB。没有机柜、没有风扇、没有空调,就四台昂贵的银色盒子和一堆 Thunderbolt 线缆。

    不是走 hub,而是全互联(full mesh)——六根线,每两台机器之间直连一根。因为一旦经过交换机,所有流量就挤在一条链路上;而用四台机器的全部意义,恰恰是它们要不停地互相说话。

    每条链路实测 36–38 Gbps。我们用来管理它们的那张 Wi-Fi 大约 1 Gbps。我们造出来的东西比管理它的网络快约四十倍——而这仍然不够快。

    第一天是真的兴奋。同一个模型在每台 Mac 上单独跑,结果差异在 2% 以内——23.9、24.1、24.3、24.3 tok/s。四台健康的机器,一块空白的画布。

    然后我们试着让它们变成一台机器,它就崩了。

    为什么我们自己造

    已经有一个项目在做这件事。EXO 能把 Mac 连成网格、跨机跑模型,我们就从它开始——从别人能跑的代码起步,显然是最正确的做法。

    问题是 EXO 已经安静了。更新不再落地。而我们要跑的模型——DeepSeek V4、MiniMax-M3——都不被支持,这意味着每个新模型都是一堵墙,而不是一次下载。当上游不动、而模型生态飞快变化时,你只有两个选择:等,或者自己接管。

    我们接管了。

    最终这套东西是叠在一起的三层:

    上游 MLX,加上补丁。 不是重写——真正干活的是 Apple 的框架。我们固定了一个特定的 MLX 构建,然后在上面打自己的补丁。有些很小(arrayscache-trimmable,让缓存可裁剪,从而解锁了前缀缓存)。有些一点都不小(1111-hybrid-spec-decode,我们尝试的投机解码——剧透:最后被我们扔掉了)。

    mlx-lm,为我们需要的模型做扩展。 这个库既没有 deepseek_v32 的 chat template,也没有它的流水线并行路径。MiniMax 能跑,纯粹是因为碰巧存在一个 minimax_m2 模板。要让 DeepSeek V4 和 MiniMax-M3 能服务,就得自己把模型家族加进去,然后让这些补丁在一个不断移动的上游上活下去——我们有一套持续运行的 rebase 方案,因为每一次上游发布都可能把它们打碎。

    最上面是我们自己的集群层。 跨节点分片、supervisor、控制台、分层缓存、OpenAI 兼容 API。没人做过的那部分。

    所以"我们写了自己的引擎"这话准确,但过于干净了。我们实际做的是:站在上游之上,把缺失的部分重建一遍——然后把项目的大部分时间花在维护我们和上游之间的那道缝上。

    这也是引擎随硬件一起消失的原因。它从来不是一个产品。它是一堆补丁加一个集群管理器,精确地只适配这四台机器、这几个模型、这一刻。没有硬件还让它活着,等于替没人维护一个 fork 的 fork。

    我们到底打了哪些补丁

    清单比预想的长,而它也解释了为什么"我们直接用 EXO 就行了"这句话没能活过现实的第一周。

    GPU 死锁。 在四节点上同时用 RDMA 和 MLX_METAL_FAST_SYNCH=1,GPU 会在 fence wait 里死锁。不是变慢——是死掉。这是 MLX 上游的已知问题(mlx#3142),当时代码树里没有修复。唯一有效的是一个社区 fork:在编码 GPU fence 之前先在 CPU 侧等待。我们采用它、验证它,它撑住了。

    内核崩溃。 打开 batching,且 head dimension 为 256——这正是 Qwen 3.5/3.6 和那个 397B 用的——那个 fork 的 attention kernel 在并发数一超过 1 时就崩。我们用更小的 fp32 tile 重建了 kernel,把自编的 wheel 部署到全部四台节点。也就是说,一个打了补丁的 Apple 内核,在跑我们的生产流量。

    RDMA 拆卸泄漏。 一次 serve 停止后,下一次会带着 Couldn't allocate protection domain 直接死掉,只有重启才能清除。原因是:jaccl 为每个 rank 分配一个 RDMA protection domain,而 MLX 没有暴露任何拆卸 API——只有进程干净退出时它才会被释放。我们写了一个优雅退出来强制做到这一点。

    根本不存在的模型支持。 qwen3_moe 完全没有 shard() 实现,所以没法跨机切分;我们自己写了一个。qwen3_next 有同样的缺口,另外还有一个受 KV heads 限制的节点数上限。DeepSeek V4-Flash 没有随附 chat template,导致工具调用历史在多轮之间被静默丢弃——我们写了 shim。GLM-5.2 的 glm_moe_dsa 架构完全不被识别,需要打补丁才能把工具调用路由正确。

    还有一个我们自己制造的 bug。 我们自己的 qwen3_next 补丁按名字覆盖了一个共享的 MoE 类。这对某个架构是对的,对另一个复用同一个类的架构就是错的——因为一个复制它的共享专家,另一个切分它。结果是 rank 分歧:四节点上输出是一团乱码,单节点上完美正常。最后靠逐层 logit 探针才找到,它教会了我们一条到处适用的规则:永远不要按名字去改一个共享的模型类。

    这些没有一个在正式发布版里。它是一个 fork 的 fork,加上十几个本地补丁,再加上一套持续运行的 rebase 方案——因为每一次上游发布都可能把它们打碎。

    而 EXO 至今一个都没修

    我们离开的理由,现在依然是理由。几个月过去,那个死锁没有上游修复,bd256 的 kernel 崩溃没有上游修复,我们想跑的模型仍然不被支持。这个项目不是在我们开发期间停住了——它一直停着。

    我不是在抱怨。开源很难,而 EXO 做了一件真正了不起的事:它证明了把 Mac 连成网格是可行的,而我们从那里出发。但如果你今天在评估这条路、想知道上游有没有追上来,我们的经验是:做好打补丁的准备。

    该给的功劳要给

    这些不全是我们的工作,暗示相反就不诚实了。有三个修复来自别人,应该说清楚。

    rltakashige 写了修好我们 GPU 死锁的那个 fork——mlx-jaccl-fix-small-recv,commit cc3f3e60。改动是 CPU 侧 fence wait,加上为命令缓冲留出 watchdog 余量。就是那一个 commit,决定了一个集群是每天死一次,还是能连跑好几天。我们找到它、采用它、用 129 分钟的浸泡测试验证它,它撑住了。没有它,这个项目活不过第二周。

    Lazarus-931 在上游修了一个更隐蔽的问题——mlx-vlm PR #1484,2026 年 7 月 3 日以 a016e09 合入。只改一行:prefill 之后的 async_eval → eval。没有它,decode 可能在第一个 token 真正落定之前就读到空白内存,模型会在七八十个 token 之后崩塌成重复的垃圾——确定性复现、极其折磨人的排查,因为输出看起来像模型问题,而不像内存序问题。我们把这个修复 cherry-pick 到了全部四台节点。

    ml-explore/mlx issue #3142 是最早描述那个死锁的地方,由在自己项目里撞上它的人写的。它至今仍开着。那个 thread 是我们确信"不是我们做错了"的原因。

    以及底下的一切基础:Apple 的 MLX 框架、mlx-lm、mlx-vlm,还有 EXO 项目——它证明了把 Mac 连起来这件事本来就能成。我们站在所有这些之上。

    值得注意的模式是:最重要的两个修复,是一个 commit 和一行代码,出自我们从未交谈过的人、在我们没付钱的项目里。这就是开源对一个这样的项目真正做的事——它不给你成品,但如果你愿意去找,它会给你缺的那一块。

    那一周,它不工作

    分布式推理的第一天实际长这样:你得到的技术上是"词",但是错的。循环、退化。或者集群干脆停住。

    我们第一个能跑的配置是 6.9 tok/s。比单台 Mac 还慢,比阅读速度还慢。我们把硬件翻了四倍,把事情弄糟了五倍。

    然后我们从 TCP 换到 RDMA,到了 15。好一些。但依然荒谬——用四台机器,只拿到一台速度的四分之一。

    我想对那一周诚实一点,因为这类项目的博客都会跳过它。有些日子,诚实的总结就是"我们又把它变慢了,用一种新的、有趣的方式"。你开始怀疑整个想法是不是错的——四台机器是不是根本无法表现得像一台,而那些声称可以的人都是在推销什么。

    然后我们发现了一个环境变量。

    一行改变一切的东西

    MLX_METAL_FAST_SYNCH=1。

    就这个。整个事情就是这个。

    我们跑的方式 Decode
    TCP ring 6.9 tok/s
    RDMA 15 tok/s
    RDMA + 一个环境变量 50 tok/s

    从十五到五十。三倍,来自一个我们从没听说过的设置,同样的硬件、同样的模型、同一天。

    它做的是去掉 Metal 在每条命令之后执行的一次同步屏障。在单台 Mac 上,这个屏障是不可见的——微秒级、每秒几千次、谁在乎。但在四台 Mac 上、每个 token 有数万条小命令时,它就成了做生意的全部成本。

    我不相信这个数字。我以为我们测错了,或者缓存在骗我们。所以一周后我故意把它关掉,看着它掉回 12.4 tok/s。再打开,它回到五十。

    那一刻项目才算成真。不是因为 50 tok/s 在绝对意义上很快——按 GPU 的标准它不快——而是因为它意味着这个架构成立。四台电脑可以是一台电脑。之后的一切都是调优。

    我想纹在每一个做这类项目的人身上的教训:在调任何东西之前,先找到那条逐命令的同步屏障。 我们花了一周在除了这件事之外的所有事情上。

    它实际能做到什么

    一旦它跑通,它就是真跑通了。单用户:

    模型 节点 Decode
    Qwen3.6-27B(dense) 1 23.1
    Qwen3.6-27B 4 48.6
    Qwen3.5-397B(MoE) 4 ~41
    DeepSeek-V4-Flash 4 38.7
    MiniMax-M3(视觉 + 工具) 4 28.5
    GLM-4.7 4 26.3
    Qwen3.6-35B-A3B(MoE) 1 83

    397B 那个数字现在仍让我笑。三千亿参数——27B 的十四倍——跑到约 41 tok/s,只比小模型慢 15%。这就是 MoE 架构给你的东西:170 亿激活参数,干着 3970 亿存量参数的活。

    我们在桌子上跑着大多数人得去租机房才能跑的模型。

    然后是让人谦卑的那些。GLM-4.7 有 92 层,意味着每生成一个 token 大约 2000 次 GPU kernel 启动。无论我们做什么,它都停在 21–26 tok/s。试过 8-bit,试过 4-bit——速度一样。天花板是模型的形状,不是它的体积。

    35B MoE 在单台 Mac 上是 83 tok/s。四台上大约 55–66。我们靠增加机器把它弄慢了。这就是没人告诉你的教训:集群不是一台更大的电脑。它是一台每说一个字都要交税的电脑。

    那次 233,000 token 的对话

    我们最自豪的东西几乎看不见:一段对话,十一轮,长到 233,000 tokens。

    没有崩溃,没有 OOM。它就这么一直跑下去——27B 上 22 tok/s,397B 上 17 tok/s,缓存命中率达到 96%。

    上下文 27B decode 397B decode 缓存命中
    19k 41.1 33.7 0%
    132k 25.9 18.4 82%
    233k 22.3 16.9 96%

    这个数字重要,是因为它避免了什么。一个冷的 196,000 token prompt 需要处理 686 秒。十一分半。每一轮都这样。没人会干第二次。

    而这才是这个项目真正的发现,跟速度无关。

    缓存才是全部的关键

    我们的编码 agent 每一轮都会把整段对话重发一遍。所以真正决定这个集群好不好用的,不是 tok/s——而是它能不能避免重读已经读过的东西。

    历史长度 首 token(冷) 首 token(热)
    1,000 tokens 1,265 ms 314 ms
    4,000 tokens 3,930 ms 322 ms
    8,000 tokens 7,024 ms 330 ms

    从七秒降到三分之一秒。快 21 倍——而且关键在于——无论对话多长,热的那个数字都是平的。10 万 token 的对话,启动成本和 1,000 token 的一样。

    这就是"你能忍受的工具"和"你会主动去用的工具"之间的差别。

    这得我们自己造,因为这些模型用的是一种 attention 变体,其内部状态无法裁剪,从而打碎了内置缓存。做对它花的时间比预期长,而第一个能用的版本,从 SSD 恢复 203,000 token 的 prompt 只用了 1.1 秒,冷启动是 1,025 秒。

    我记得第一次重启是瞬间回来的,而不是让我们等十七分钟。那是一种小而过分强烈的快乐。

    然后它不断地倒下

    速度是最容易的部分。让四台 Mac 在真实负载下活着,才是项目的大部分。

    大约每天一次,GPU 会死锁。 100% 占用,卡在一个 wait 里,集群死掉。没有报错,没有警告——就是一台停住的机器。我们忍了九天,直到一个社区 fork 修好它;修好之后我们跑了浸泡测试:129 分钟、三个并发客户端、194/194 请求、零死锁。 那份日志我读了三遍。

    一个错误的库文件让我们损失了好几天。 张量并行的输出间歇性变成垃圾。我们一直以为是自己的代码。结果是一个错的 libjaccl 二进制。固定正确的构建之后,一切瞬间好了。几天的调试,一行配置。

    还有一个静默的正确性 bug。 一个补丁把一个本应完整复制到每台机器的共享专家切分了。输出是看似合理的。不是"看起来错"——是看似合理。这是最糟的一类 bug,因为你只能靠和一个你信任的参考实现对比来发现它,而我们差点就没发现。

    两次断电。 三台工作机每次都重启了。一个 UPS 比我们丢掉的那个下午便宜。

    这些全都写进这篇文章,因为它们才是真正的工作。那些激动人心的部分——50 tok/s、397B 跑起来——发生在几个小时里。稳定性发生在几周里。

    没用的那些尝试

    这是我希望当初有人替我们写好的那一节。

    投机解码让它变慢了。 不是稍微。0.26 到 0.37 倍——三分之一的速度。这项技术本该有用;在这套硬件上、配这种 attention 变体,记账开销超过了它省下的 token。我们试了、测了、关掉了。

    int8 KV cache 用速度换内存。 省下 45% 内存,损失 4–10% decode。Apple GPU 没有 int8 张量核心,所以这个交换是真的,而省下的不是。用我们并不需要的内存,换掉了我们真正需要的速度。

    手动做计算/通信重叠只带来约 5%。MLX 本来就是惰性做的。我们只是为噪声增加了复杂度。

    把大模型放在两个节点而不是四个,会把每台 Mac 逼进内存压缩,掉到 18.8 tok/s。机器多、机器少——有一个正确答案,而且它并不总是显然的。

    每一个都是一两天的工作,产出为零。我不后悔做它们。我后悔的是不知道哪些该跳过。

    如果你问我,我会说

    先找同步屏障。 在模型之前、在量化之前、在任何聪明的东西之前。在我们的集群上,它就是一个值 3 倍的环境变量。

    预期 prefill 会线性扩展,decode 不会。 四台机器给了我们 3 倍的 prompt 处理,和 2 倍的生成。每个 token 都要过一次网络,每层两次,而这个成本不会因为加机器而缩小。

    小模型就放单台 Mac 上。 那个 35B 在单机上比四机快。集群是给那些真的放不下的模型的。

    先做缓存,再做速度。 热的首 token 三分之一秒,比多 20 tok/s 更有价值——这话我直到自己用上才信。

    买个 UPS。 这是清单上最便宜的一课。

    锁死你的库版本。 一个错误的二进制,几天就没了。

    它是怎么结束的

    我们为此自己做了控制台——四个节点、一个模型库、实时遥测。当你在没人 benchmark 过的硬件上跑推理时,你需要自己的仪器,否则你说的每一句话都只是一个秒表和一种感觉。

    集群控制台:四个节点及实时内存、带每节点在线状态的模型库、以及最近的请求耗时

    它跑通了。我们用它在真实编码 agent 上服务了两个月——长对话、视觉模型、工具调用——在它乖的时候,确实是件愉快的事。

    但 Apple Silicon 有天花板,我们撞到了。这个互联对四台机器是惊人的,对十六台是没指望的。内存巨大,算力不大,而且不存在某个版本能让 Mac 集群在模型足够大之后跟 GPU 机架竞争。我们不断撞上的是架构性的墙,而不是可调优的墙。

    所以我们继续往前走了。集群拆了,引擎随硬件一起消失,我们转去在 Blackwell 上造别的东西——那是另一篇文章,不同的问题,说实话,空间大得多。

    我不觉得这是失败。我们回答了那个问题:是的,四台 Mac 能以 41 tok/s 跑一个 397B 模型;是的,你可以在桌子上维持一段 233,000 token 的对话。这值得知道,而几乎没人知道。

    但兴奋在前两周。另外七周是苦活——如果你正准备搭一台这样的东西,那才是我想让你做好准备的部分。


  • 3 x RTX Pro 6000D + 1 x RTX Pro 6000 跑官方 Deepseek v4.1 Flash 1017 分享
    Edmund TanE Edmund Tan
    LLM讨论区 rtxpro6000 deepseek 多卡部署

    @kop-wang 你好,回本就别想太多了😂。 ,目前这个设置是花了近两年来终于可以符合我的需求。之前八张5090 显存不够 看起来威武跑起来什么都差一点点哈哈。 过几天我发布一下我四台苹果M3Ultra 256GB 的经历。


  • 3 x RTX Pro 6000D + 1 x RTX Pro 6000 跑官方 Deepseek v4.1 Flash 1017 分享
    Edmund TanE Edmund Tan
    LLM讨论区 rtxpro6000 deepseek 多卡部署

    大家好。第一次在这里发表聊天, 有什么不对的请多多包含。

    花了2个多月 用SGlang 跑 Deepseek 4.1 Flash 1017. 终于达到理想的结果。 看了很久视频今天打算分享我设置 3 + 1 设置。 本来要买四张 RTX Pro 6000D(节省开销) 84GB 后来第四张 显卡买了 收到故障 , 当时显卡从 5万一张飙升至7万 商家就 质保变退款。 无奈之下第四张卡买了 满血 96GB 工作站版 凑数 4 张 显卡来跑AI。 (其实4张84GB 也能跑但是区别在 总上下文)

    Sglang 引擎可以支持 权重不平衡分配,所以意外的满血版多容纳了一些权重。 经过2个多月的修改,设置,引擎提升 终于达到了 Prefill 11000 (0-500K之间) - 7000 (988K)token 每秒。 目前最多三个并发 总上下文 3百多万

    image.jpeg
    image.jpeg

    一并发的输出 (写代码更快)
    image.jpeg

    三并发输出
    image.jpeg

    显卡设备 - AMD EPYC 7F52 + 512GB DDR4
    image.jpeg

    大佬们如果想要本地跑快点的Deepseek v4.1 Flash 1017, 这个方案提供参考。 以下是我设置日志

    DeepSeek V4.1 Flash:在 4 张 RTX PRO 6000 上实现 100 万上下文

    2026 年 10 月 21 日 · 阅读约 30 分钟

    四张 PCIe Blackwell 显卡,无 NVLink:100 万 token 上下文,冷启动预填充 149 秒,解码 212 tok/s,现已支持三个并发会话。

    点击放大

    四张没有 NVLink 的 PCIe Blackwell 显卡,现在以完整的 1,048,576 token 窗口服务 DeepSeek-V4.1-Flash:988K token 的冷启动提示预填充耗时 149 秒(6,618 tok/s),解码约 212 tok/s。

    十一个星期前,同样这四张卡在 llama.cpp 上只能跑到 60 tok/s 的解码速度,而且根本无法支撑长上下文。要走到完整的 100 万窗口、再达到这样的速度,花了两个月的硬件工作、一次没人算进去的内存分配、一次只有在替换卡到手即坏、最终凑成一套不匹配的卡组后才成为可能的非均匀专家切分,以及一个我们忘了带到新启动器上的环境变量。这是完整的历程——包括失败的部分。

    历程:从 630 秒到 149 秒

    988K token 冷启动预填充:三周内从 630 秒降到 149 秒。点击放大

    这不是靠一个灵光一现的点子实现的。前后大约十一个星期,其中两周花在与模型无关的硬件上,最后三周用来让 100 万 token 窗口装得下、然后再让它变快。以下是完整路径,包括失败的部分。

    八月:先证明这件事可行

    我们最初在 llama.cpp 上(2026-08-11)跑 DeepSeek-V4-Flash 加 DSpark 草稿模型,因为那是当时唯一能在 sm_120 工作站 Blackwell 上跑起来的栈。它确实能跑,但很慢:解码 60.1 tok/s,上下文只有几万 token。

    那个月得到两件事:

    • 草稿模型比引擎本身更重要。 加上 DSpark 草稿模型后,在相同权重下解码从 60.1 提升到 102.4 tok/s,即 1.7 倍。
    • 机器本身是坏的。 PCIe 错误风暴达到每分钟 23 次,追查下来是一块坏板子和 SlimSAS 线缆。换了新的 Tyan S8030GM2NE 和线缆后降到零。然后 amd_iommu=off iommu=pt 加上 NCCL 点对点,把 TP4 预填充从 977 提升到 2,149 tok/s,解码从 75 提升到 91.3。

    最后这一点值得细想:在完全还没做任何模型层面工作之前,仅仅一个内核参数就让预填充翻了一倍多。

    九月:互联成了天花板

    到九月底,我们已经用四张卡提供服务,瓶颈是全归约(all-reduce),而不是算力。

    日期 改动 全归约总线带宽
    2026-09-21 NCCL_P2P_LEVEL 从 SYS 改为 PHB 6.21 → 9.31 GB/s
    2026-09-28 为三张 GPU 加 Broadcom PEX 88096 交换芯片 9.31 → 19.53 GB/s
    2026-10-17 为 DS4.1 设置 NCCL_P2P_LEVEL=PHB 3.83 → 19.57 GB/s

    第三行是最痛的。我们为 DeepSeek V4 设过 PHB,但忘了把它带到 DS4.1 的启动器上。整整三周,发往交换芯片之外那张 GPU 的流量都在走主机内存。2026-10-17 修好后,冷启动 498K 从 150 秒降到 75 秒,冷启动 988K 从 374 秒降到 223 秒——比这篇文章里其他任何单项改动带来的收益都大。

    教训:你对一个启动器做的设置,不等于你对整台机器做的设置。 我们这个设置就那样静静待着,悄悄吃掉了 40% 的预填充性能。

    2026-09-29 首次 DS4.1 服务:400K 窗口,350K 时预填充 2,564 tok/s,解码 96-108 tok/s。

    内存墙:为什么是 400K 而不是 100 万

    这是耗时最长的部分,而且它不是调参问题——而是一笔没有被计入的分配。

    每一次尝试 100 万上下文都死于同样的特征:请求中途 OOM,失败点还会漂移,而且对 mem-fraction-static 完全免疫。我们从 0.92 调到 0.94,失败点纹丝不动。这本该立刻告诉我们:这块内存是被预算器看不见的东西占走的。

    我们最终在 OOM 那一刻取了分配器快照,答案是 V4.1 的稠密预填充 indexer:fp32 的 [chunk × context] 分数张量加上 bool 候选掩码,约占全部预填充内存增长的 94%,而且分配在 mem_fraction_static 的核算范围之外。

    上游有同样的 bug,而且已经命名了它(SGLang issue #35201):"indexer 与上下文成正比的 logits 分配未被 mem_fraction_static 计入——长上下文预填充必然 OOM。"已经有人把我们的问题写下来了。

    增长是线性且巨大的:每 1,000 token 增长 13.7 MiB。到 100 万 token 就是 13.7 GB 没人预算过的内存。

    两个运行时补丁解决了它,无需重新编译:

    • 给 indexer 拷贝预算加 256 MiB 上限。
    • 改用 int32 候选块 id 的行分块预填充 indexer——手工移植上游 SGLang PR #40217。

    这使窗口从 400K → 600K → 800K → 100 万,第一次冷启动 988K 通过耗时 629.7 秒。

    第二个内存天花板:非均匀专家

    100 万上下文跑通后,新的限制出现了:三张 84 GiB 的卡没有余量,而那张 96 GB 的卡却半闲置着。

    以下是干净重启后测得的数字:

    干净重启后 PRO 6000(rank 0) 每张 6000D
    主模型加载后剩余 21.34 GiB 9.52-9.56 GiB
    内存池与 CUDA 图之后剩余 15.86 GiB 4.02-4.12 GiB

    PRO 6000 上有 11.8 GiB 完全没被使用。 2026-10-11,一个冷启动的 784K 提示在三张 6000D 上因差不到 0.5 GiB 而失败,而 rank 0 还有约 16 GB 空闲。

    修复方案是非均匀专家切分——96 GB 的卡承担 384 个路由专家中的 108 个,84 GiB 的卡各承担 92 个,而不是原版的 96/96/96/96。一个专家位置在全部 40 层上占 0.70 GiB,所以移走四个大约能给每张 6000D 释放 2.7 GiB。

    84 GiB 6000D 上的峰值空闲显存。点击放大

    均等切分 非均匀 108/92/92/92
    6000D 主模型 72.63 GiB 69.97 GiB
    内存池后剩余 4.64-4.77 GiB 7.31-7.44 GiB
    冷启动 988K 峰值空闲 ~1.3 GB ~3.8 GB

    上线前我们离线验证了路由:45 项检查确认每个专家都恰好被一个 rank 加载、并在它被加载进去的那个槽位里运行;另有一项内核检查显示非均匀切分与单 rank 结果的接近程度与均等切分相当(0.002337 对 0.002347),并故意放了一个损坏的对照组(0.933535)来证明这个测试能区分错误路由和正确路由。

    那张坏掉的卡反而成了优势

    原计划是四张完全相同的 RTX 6000D。第四张到手就是坏的。

    再等一张 6000D 意味着延迟,所以我们买了 96 GB 的 RTX PRO 6000——那是当时真正有货的型号。三张比第四张小 12 GB 的卡,不是任何人会刻意设计的配置;它只是当一张卡坏掉、而你又只能拿到能拿到的那张替换卡时,最终得到的结果。

    在搭建过程的大部分时间里,它看起来像个负担。原版 SGLang 假定所有 rank 相同,在混装卡组上拒绝启动,而且小的那几张正是耗尽内存的。

    然后非均匀切分把它翻了盘。因为 96 GB 的卡空间更大,它能比其他的承担更多专家——108 对每张 92,而不是原版的 96/96/96/96。把四个专家位置从小卡上移走,每张大约释放 2.7 GiB,这正是"冷启动 784K 提示因差 0.5 GiB 而挂掉"和"从容运行"之间的差别。

    正是这份余量,把"很快,但上下文上限约 400K"变成了三个并发的 100 万会话。如果没有一张卡能承担更重的份额,就没有任何地方可以再平衡——每张卡都一样紧缺,也就无解了。

    所以这个优势不是运气。坏卡迫使我们用了混装机器;非均匀切分把一个约束变成了容量。如果第四张 6000D 是好的,我们会有四张相同的 84 GiB 卡、总显存更少、也没有可再分配的余量——三槽配置根本不会存在。

    可以推广的要点:在张量并行机器里,不均匀的卡本身不是问题——只有当切分是均等的时候它才是问题。 原版 SGLang 假定各 rank 相同,不一致时就不启动。按能力而不是按数量来切分,混装机器可以胜过同构机器,因为大卡能吸收小卡无法承受的不均衡。

    如果搭建途中你的卡坏了,就这么做:不要凑同型号,买你能买到的最大替换卡,然后重新平衡分片。

    十月:缓存、分块,以及换引擎

    内存问题解决后,剩下的工作就是速度和缓存。

    • 2026-10-04 — 预填充 chunk 从 1,024 改为 4,096,Claude Code 的 system 消息保持内联。冷启动 988K 从 629.7 降到 373.4 秒;每轮提示缓存复用从约 35K 中的 8,192 提升到 99.9%。
    • 2026-10-16 — HiCache 文件层,32 个缓存尾部,2.1M GPU 池。重启后恢复 777K 会话,从 269.7 秒的重新预填充降到从磁盘恢复的 5.1 秒。六个 agent 共 2.7M token,100% 复用。
    • 2026-10-18 — DeepSeek-V4.1-Flash-1017 权重(仅权重更新;速度上无明显变化,质量在噪声范围内),KV 文件层迁到三块 NVMe 组成的 RAID0。339K token 的提示从缓存恢复仅 2.90 秒,而计算需要 55.56 秒,而且它经受住了一次断电。
    • 2026-10-21 — 换引擎。冷启动 988K 从 223 降到 149 秒,解码从约 108 升到约 212 tok/s,空闲显存从 1.7 升到 4.2 GiB。

    完整调优日志

    日期 步骤 收益
    2026-10-21 更新的 SGLang 引擎(2026-10-19 SM120 构建)+ 我们的切分、indexer 预算和附加组件;chunk 2,048 配 expandable_segments 冷启动 988K 223 → 149 秒;解码 ~108 → ~212 tok/s;空闲显存 1.7 → 4.2 GiB
    2026-10-18 DeepSeek-V4.1-Flash-1017 权重 + 10 月 16 日编码器修复;KV 文件层放到 NVMe RAID0 速度与内存相同;质量在噪声内;断电后 2.9 秒恢复 339K 提示
    2026-10-17 为 DS4.1 设置 NCCL_P2P_LEVEL=PHB(此前没有) 全归约 3.83 → 19.57 GB/s;冷启动 498K 150 → 75 秒;冷启动 988K 374 → 223 秒
    2026-10-17 为 Claude Code 加 /v1/messages 兼容层 请求不再被拒绝后重试;PDF、redacted thinking、超长提示都得到处理
    2026-10-16 HiCache 文件层 + 32 个缓存尾部 + 2.1M GPU 池 重启后恢复 777K 从 269.7 → 5.1 秒;6 个 agent 共 2.7M token,100% 复用
    2026-10-12 非均匀专家切分 108/92/92/92 84 GiB 卡上空闲显存 1,322 → 3,798 MiB;解码 108.8 → 110.3 tok/s
    2026-10-04 预填充 chunk 1,024 → 4,096;Claude Code system 消息保持内联 冷启动 988K 629.7 → 373.4 秒;每轮缓存复用约 35K 中的 8,192 → 99.9%
    2026-10-03 Indexer scratch 预算 256 MiB + 行分块预填充 indexer(移植上游 #40217) 窗口 400K → 600K → 800K → 100 万;首次冷启动 988K 通过耗时 629.7 秒
    2026-09-29 DS4.1-Flash 首次服务:SGLang,TP4 + EP4,Engram 表放在主机内存 400K 窗口,350K 时预填充 2,564 tok/s,解码 96-108 tok/s
    2026-09-28 为三张 GPU 加 Broadcom PEX 88096 交换芯片 全归约 9.31 → 19.53 GB/s;V4 预填充 42K 时 +51%,672K 时 +20%
    2026-09-21 NCCL P2P level 从 SYS 改为 PHB 全归约 6.21 → 9.31 GB/s;V4 预填充 +14-24%
    2026-09-20 新主板(Tyan S8030GM2NE),SlimSAS 线缆更换 PCIe 错误风暴 23/分钟 → 0
    2026-08-25 内核 amd_iommu=off iommu=pt + NCCL 点对点 TP4 预填充 977 → 2,149 tok/s,解码 75 → 91.3
    2026-08-11 DeepSeek V4-Flash 在 llama.cpp 上配 DSpark 草稿模型 解码 60.1 → 102.4 tok/s(1.7 倍)

    真正重要的东西(按重要性排序)

    如果你只从这十一个星期里带走一件事:

    1. 为这个模型设置 NCCL PHB —— 冷启动 988K 从 374 秒降到 223 秒。一个环境变量,被遗忘了三周。
    2. indexer 补丁 —— 它是把窗口从 400K 做到 100 万的前提。在它解决之前,别的都不重要。
    3. 更新的引擎 —— 223 → 149 秒,解码翻倍。收益真实,但它需要底下的内存工作才能用得上。
    4. 非均匀专家切分 —— 在那些受限的卡上把余量从 1.3 GB 提到 3.8 GB。
    5. Chunk 4,096 和 HiCache —— 630 → 373 秒,重启从 4.5 分钟降到 5 秒。

    规律是:最大的收益来自配置和内存核算,而不是聪明的优化。 这个模型一直都有这个能力。我们大部分时间都花在发现"内存被花在了我们没看的地方"。

    机器配置

    一台工作站,无 NVLink:四张 Blackwell 卡在 PCIe 4.0 上,其中三张位于 PCIe 交换芯片之后。

    部件 说明
    GPU(服务用) 1x RTX PRO 6000 Blackwell Workstation(96 GB)+ 3x RTX 6000D(可用 84 GiB),均为 PCIe 4.0
    PCIe 拓扑 GPU 0-2 位于 Broadcom PEX 88096 交换芯片之后;GPU 3 独占一个 CPU 根端口
    CPU / 内存 AMD EPYC 7F52(16 核 Zen 2),主板 Tyan S8030GM2NE,512 GB DDR4-2933 ECC
    内核 amd_iommu=off iommu=pt pcie_aspm=off pci=realloc
    缓存盘 KV 文件层在三块 NVMe 的 RAID0 上(/mnt/scratch,上限 1000G);权重在 2 TB NVMe 上
    服务 SGLang main 2026-10-19(ormandj SM120 镜像)+ 我们的运行时补丁,TP4 + EP4,DSpark 投机解码,fp8 KV
    模型 DeepSeek-V4.1-Flash-1017:40 层,hidden 5,120,384 个路由专家(激活 6 个);Engram 表(189 GiB)放在主机内存

    为什么只有三张卡在交换芯片之后

    四张里有三张是中国市场的 RTX 6000D(可用 84 GiB),而不是完整的 96 GB 版本——448 位总线上 28 颗 3 GB GDDR7,156 个 SM,为出口管制做了削减。原计划是四张 6000D;第四张到手即坏,96 GB 的卡是作为替换买来的。但它与主板的交互方式还有一个容易被忽略的点,这也是这台机器不是"四张卡全在一个交换芯片后"的原因。

    简短版: Tyan S8030GM2NE 不让我们调整 PCIe 内存映射 I/O(MMIO)资源分配。没有这个,四张大显存 Blackwell 卡在交换芯片之后放不进平台的地址空间,所以三张在交换芯片后,第四张占自己的 CPU 根端口。

    机制(尽力还原)。 这部分是根据平台行为和公开报告推断的,不是我们自己抓的 BIOS dump——请把它当作一个在你的板子上需要验证的假设。

    每张 Blackwell 卡都会向固件请求一个非常大的 BAR1 窗口来暴露其显存:这一级别的大概是每卡 128 GB 量级。四张就是大约半个 TB 的 MMIO 地址空间,固件必须在不重叠的前提下切分并对齐,再分配到各根端口。服务器和工作站 BIOS 通常把 Above 4GB MMIO Limit 默认设为 40 位(1 TB)——听起来很宽裕,直到分配器的 2 的幂对齐吃掉了其中大部分。当它无法放置每个窗口时,通常的结果是 POST 卡在 D4"PCI resource allocation error. Out of resources"代码,或者某张卡干脆永远枚举不出来。

    暴露 Above 4GB MMIO Limit 和 Re-Size BAR 的板子可以让你抬高这个上限(通常建议 44 位 / 16 TB),从而装下四张。我们的 S8030 没给我们这个控制项,所以四张全在交换芯片后不是一个选项。

    所以这个拓扑是对固件限制的变通,而不是我们为性能选择的方案:

    • GPU 3 独占自己的 CPU 根端口,在交换芯片之外。
    • 发往它的流量除非告诉 NCCL 这一跳在主机桥内,否则不能做点对点拷贝——这正是 NCCL_P2P_LEVEL=PHB 在这台机器上值 40% 预填充的原因。
    • 三张挂在交换芯片上的卡彼此走 PIX/PXB;第四张是更长的路径。

    我们接下来会试的(未验证): 一块能让你抬高 above-4G MMIO 上限并启用 Re-Size BAR 的板子,应该能让四张卡都在 PCIe 交换芯片之后。如果能,每一对都会变成 PIX/PXB,全归约不再跨越根复合体,P2P 带宽应该还能进一步提升。我们没测过这个——这是从拓扑推出的假设,不是结果。它大概也能让我们去掉 PHB 覆盖,因为那时 PHB 就不再是正确的级别了。

    如果你要搭这套,两条实用提示:

    • 买板子之前先确认那个 BIOS 设置。 在我们的硬件上,它就是"三卡在交换芯片后"和"四卡在交换芯片后"之间唯一的差别。
    • 失败并不总是明显的。 有些板子能 POST,但会静默地留下一张卡未分配;另一些会卡在 D4。在假设分配正确之前,确认 nvidia-smi -L 显示全部四张,且 lspci -vv 中每个 BAR 的 current size 与其 supported 列表相符。

    换引擎买到了什么

    同样的四张 GPU,同样的 1017 权重,同样的 2.1M token KV 池——这个对比早于第三个槽位,所以下面两列都是双槽配置。引擎变了,每个头条数字都动了。

    四张 RTX PRO 6000,无 NVLink——换引擎买到了什么。点击放大

    指标 旧引擎(10 月 17-18 日) 新引擎(10 月 21 日)
    上下文窗口 1,048,576 1,048,576
    GPU KV 池 2.1M token 2.1M token
    冷启动 498K 预填充 74.8 秒 54.2 秒(9,189 tok/s)
    冷启动 988K 预填充 222.5 秒 149.3 秒(6,618 tok/s),needle 2/2
    解码,短提示 108.4 tok/s 212.3 tok/s
    解码,约 1.04M 上下文 103.5 tok/s 184.3 tok/s
    峰值最低空闲显存 1,706-1,922 MiB 4,210 MiB
    两个约 948K 提示同时 未测 都得到回答,259.6 秒
    Needle,878,591 token 处 5 个深度 -- 5/5
    工具调用 / 多轮 / 视觉 4/4, 4/4, 11/11 4/4, 4/4, 11/11

    新引擎是 ghcr.io/ormandj/sglang-deepseek-v41-flash-sm120:v0.4.3:2026-10-19 的 SGLang main 加 32 个补丁,FlashInfer 0.7.2,为 SM120(Blackwell 工作站)卡构建。之前的引擎是约 2026-09-10 的 dev 构建加我们自己的补丁。

    冷启动预填充:223 秒到 149 秒

    988K token 冷启动预填充 149 秒,从 223 秒降下来。点击放大

    冷启动测量,无任何缓存,两个引擎在每个级别都 needle 2/2。

    提示 498K 598K 698K 798K 898K 988K
    旧 74.8 96.9 122.4 152.0 186.8 222.5
    新 54.2 69.4 86.5 105.8 127.5 149.3

    短提示预填充改善幅度类似:

    提示 token 4.1K 32.8K 131K 336K
    旧 6,190 8,216 8,233 7,526
    新 7,882 11,291 11,441 10,662

    解码:各处大约翻倍

    解码在每个上下文长度上都大约翻倍。点击放大

    上下文 35 11.6K 47.7K 96K 194K 379K 750K 1,036K
    旧 108.4 113.0 99.5 113.8 109.8 107.8 112.8 103.5
    新 212.3 201.2 216.9 211.5 197.7 198.7 180.6 184.3

    在 100 万上下文下增益仍然保持:184 tok/s,而旧引擎只有 104。峰值空闲显存从 1.7 GiB 升到 4.2 GiB,正是这一点让更深的 KV 池变得安全。

    它不是开箱即用的

    四轮才到位。

    1. 原版配置。 预填充快了 36-48%,解码约 1.9 倍,但该配置假定四张 96 GB 卡。在我们 84 GiB 的 6000D 上它只装得下约 40 万 token 的 KV——没有 100 万窗口。
    2. 移植我们的专家切分。 108/92/92/92 切分加 256 MiB indexer 预算,chunk 2048、mem 0.915。100 万通过:988K 冷启动 149.1 秒。
    3. 生产用的 chunk 4096。 能启动,但长预填充崩了:500K 冷启动用了 278.7 秒,而对照是 54 秒。在这个引擎上不可用。
    4. 生产设置加完整池。 Chunk 2048、mem 0.93、2.1M 上限、所有客户端附加组件、32 个缓存尾部和 HiCache 文件层。全部通过。这就是随后的双槽服务;第三个槽位是之后加的(见下),把池提到 3.15M、mem 0.942。

    为什么 chunk 4,096 在这里会失败

    这是最有意思的部分,而且它不是一个简单的"缓冲区太大"。

    在 4,096 行时,引擎的内存在第一个 chunk 就被占满。FlashInfer MoE scratch 空间是每次调用时分配的、没有预分配缓冲区;mHC 残差缓冲区翻倍;而在 4,096+ 行时引擎会切换到单独的大批量代码路径,包括第二条 CUDA 流,它们持有内存更久并造成碎片。无论如何,约有 1.85 GiB 被预留给预录制的解码尾部 CUDA 图。

    这触发了一个反馈回路。V4.1 预填充 indexer 根据空闲 GPU 内存来决定其分数 tile 的大小。几乎没有空闲时,tile 缩到只有几行——每个 index 层最多约 1,000 次微小内核启动,每次都重读整个上下文。因此代价随上下文增长:随着上下文增长,预填充从约 11K tok/s 掉到约 1.1K tok/s。而在 2,048 时约有 4 GB 保持空闲,所以 tile 能保持较大。

    现场数据也一致。 其他在 RTX PRO 6000 上跑这个模型的人在生产中用 chunk 2,048;在 8 张 GPU 上,4,096 并不更快(128K 时 6,301 对 6,853 tok/s)。我们自己的测量:即便装得下,4,096 也只能买到约 4% 的预填充提升。

    原版配置实际给你什么

    在任何调优之前,引擎自带配置已经快得多——而且在我们这些卡上 100 万上下文不可用。这一点值得知道,因为它告诉你收益来自哪里、墙在哪里。

    在相同 chunk 4,096 和与旧生产服务相同的提示规模下运行:

    提示 token 旧引擎(tok/s) 原版新引擎
    4.1K 6,190 8,424
    8.2K 7,310 10,356
    32.8K 8,216 11,816
    131K 8,233 11,995
    336K 7,526 11,132
    上下文 旧解码 原版解码
    --- --- ---
    35 108.4 214.3
    96K 113.8 215.7
    379K 107.8 200.4

    所以开箱即用:预填充 +36-48%,解码约 1.9 倍。引擎本身就是那个收益。此后的一切都是关于让它装得下。

    为什么原版在这里撑不住 100 万

    上游的配置假定四张 96 GB 卡。我们三张 6000D 是 85.6 GB,没有非均匀专家切分时每张装 74.1 GiB 权重,而不是生产的 72.6——而那张 96 GB 的 PRO 6000 却有约 17 GB 闲置。

    五次启动尝试,按顺序:

    启动 设置 结果
    1 原样 拒绝:TP 内存不均衡
    2 mem 0.93, chunk 8192 KV 664,832,然后预热时 OOM(980 MiB 空闲)
    3 mem 0.91 拒绝:权重导致 0.916 以下无 KV
    4 mem 0.925, chunk 8192 KV 393,984,然后预热时 OOM(248 MiB 空闲)
    5 mem 0.925, chunk 4096 服务成功——KV 399,872,峰值 84.65 / 85.65 GB

    第 5 次能跑,但只有约 40 万 KV 而不是 2.1M:没有 100 万上下文,没有余量,也没有第二个 agent 的空间。速度是真的;容量不是。

    修复方案是非均匀专家切分(96 GB 卡承担 108 个专家,84 GiB 卡各 92 个),它把 6000D 的权重降到 71.5 GiB,并把差额交给 KV 池。这一个改动就是把"快但只有 400K"变成"快且有 2.1M"的关键。

    给你自己搭建的启示

    如果你用四张 96 GB 卡,原版配置可能直接就能用。在混装或 84 GiB 卡上,预期要先重新平衡专家——而且不要把原版配置的基准数字当成你会拿到的数字,因为它测的是一个你的机器可能装不下的内存形状。

    10 月 18 日的权重

    DeepSeek 的 1017 版本是一个仅权重更新——相同的配置、张量名、分片和分词器;分片哈希不同。

    在旧引擎上,原始版 vs 1017 是打平:冷启动 988K 253.5 秒对 257.1 秒,解码 108.9 对 106.1 tok/s。饱和测试集上的质量:HumanEval 157/164 对 156/164,GSM8K 297/300 对 296/300。DeepSeek 报告的增益在更难的推理和 agent 基准上,而这些测试集达不到那个水平。

    我们还移植了 DeepSeek 的 10 月 16 日编码器修复:推理努力级别 low/high/max 对应 50/75/100,仅携带工具结果的消息不再开启新回合,以及 ns::name 工具命名空间。新引擎已经包含这些。

    试过并否决的: 参考模型在 wo_a 之前对注意力输出做的 fp8 往返,损失约 13% 解码(106.1 到 92.7 tok/s),质量无可测变化。保持关闭。

    三个并发 100 万会话

    单会话时引擎已经很快了。对 agent 工作真正重要的问题不一样:这台机器能同时撑住三个完整的 100 万 token 会话而不倒吗?

    能。 现在生产上服务三个槽位,4 个槽位试过并否决。

    取舍:池 vs 余量

    一个能装下完整 1,048,576 token 上下文的槽位不是免费的。每个约花 1.7 GB KV 池加约 0.07 GB 解码 CUDA 图。

    6000D 启动后的空闲显存。点击放大

    槽位 池 mem 启动后空闲(6000D / GPU0)
    2(之前) 2,099,968 0.93 5.97 / 6.55 GB
    3(现在) 3,149,824 0.942 4.23 / 4.82 GB
    4 4,199,936 0.96 2.50 / 3.09 GB

    池就是 N × 1,050,000。在 mem 0.93 时,栈构建 2,599,936 token(无上限),每 +0.01 的 mem 分数增加约 547k。

    为什么 4 槽失败。 池装得下,功能检查也通过了——工具、多轮 4/4、视觉 11/11、OCR 3/3。然后第一个普通的 2k token 请求就挂了:

    cutlass_fused_moe: "Tried to allocate 124.00 MiB ... 40.12 MiB is free" on GPUs 1 and 2
    

    调度器退出,之后每个请求都被拒绝。那个阶段的最低空闲是 6000D 上的 898 MiB。在 84 GiB 卡上,4 槽不是一个能扛住真实流量的配置——池装得下,负载装不下。

    3 槽实际交付了什么

    功能上:工具通过,多轮 4/4,视觉 11/11,OCR 3/3,以及超长提示按预期返回 Anthropic 措辞的 400。

    多流同时解码(短提示,thinking 关闭——与 212 tok/s 的单流数字不可比,后者用的是不同的测试框架):

    三个并发流。点击放大

    流数 每流 合计
    1 189.6 tok/s 190.0
    2 154.4 tok/s 295.7
    3 137.4 tok/s 392.4

    从一流到三流,总吞吐翻倍,而每流速度下降约 28%。这是预期的形状,也是 agent 工作正确的取舍:三个 agent 在推进,胜过单个 agent 全速跑。

    预填充不变。 三槽和双槽产生的预填充曲线在噪声范围内相同:

    提示 2 槽 3 槽
    131K 11,441 tok/s 11,431
    498K 冷启动 54.2 秒 54.1 秒
    988K 冷启动 149.3 秒 149.4 秒

    这些冷启动级别是在 3 槽激活时测的,不是从双槽服务沿用的——needle 检查在这个配置上跑过:每个级别 2/2。单流解码也保持在 decode_bench 的 ±10 噪声内(短上下文 212.2,1,036K 时 177.1)。

    值得精确说明探测了什么、没探测什么,因为这两者容易混淆:

    测试 槽位 Needle 检查
    冷启动预填充,131K-988K 3 每个级别 2/2
    三个并发 988K 会话 3 未探测——正确性由答案判断,不是 needle
    两个并发约 948K 提示(较早) 2 未探测

    所以 3 槽配置对单请求冷启动预填充做了 needle 验证,窗口声明正是依赖这一点。并发运行证明调度器能撑住三个长会话并返回正确答案;它们没有额外证明三个会话都在驻留时的 needle 检索。这是证据上的一个缺口,不是已知的失败。

    三个真实的 988K 会话同时跑——2.96M token,占池的 94%——全部正确回答:

    会话 提示 token 首 token 结果
    1 988,050 150.2 秒 PASS
    2 988,055 297.0 秒 PASS
    3 988,050 443.9 秒 PASS

    教训:槽位增加解码并发,不增加预填充吞吐

    看那些首 token 时间:150 秒、297 秒、444 秒。预填充是一个接一个跑的,每个约 6,680 tok/s。

    这是因为 2,048 token 的预填充 chunk 被所有运行中的请求共享。后面的长提示要等前面的跑完。所以三个槽位给你三个 agent 同时思考,但它们不能让三个长提示的预填充整体更快。

    如果你的负载是三个 agent 各自冷启动 900K 提示,预期最后一个要等。如果是三个 agent 跑长期存活的缓存会话——agent 工作通常就是这种——你能拿到完整收益,因为前缀缓存意味着你根本不需要重新预填充。

    真正重要的浸泡测试

    基准不是生产测试,所以 3 槽配置被拿去跑了一个 agent 式浸泡测试:三个类 Claude-Code 会话分别预热到 698K / 754K / 780K token(同时 2.23M),每回合带 3-15K 工具输出加一个 <system-reminder>,每 4 回合起 2 个并行子 agent,800K 时压缩。

    56 个请求,0 错误,PASS。

    • 压缩:3/3(S3 804,298 用 26.5 秒,S2 802,023 用 11.2 秒,S1 802,489 用 6.8 秒)——每个都从缓存读取了其前序提示的 100.0%。
    • 600K-804K 上下文的 agent 回合:22 个。中位数 6.0 秒,最大 18.5 秒。
    • 子 agent 冷启动:8 个(54K-67K),排队时最大 25.3 秒。
    • 缓存复用:42 个判定回合中最差 96.2%;每个长上下文回合都在 99%+。
    • 最低空闲:GPU0 2,585;6000D 1,960-2,042 MiB。

    三个低于 99% 的回合都是压缩后的小上下文(前序 5-10K),最后一个不完整页没被缓存——不是真正的回退。

    一个值得重复的测量陷阱: 第一次试跑把缓存复用算成"缓存 ÷ 整个提示",这在某回合往小上下文里加了很多东西时会低估复用(一个 5K 子 agent 上的 3K 工具输出被打成 51-60% 并 FAIL)。按会话的前序提示来算是诚实的度量——服务器数据在两次运行中完全相同。如果你在测前缀缓存复用,在相信那个数字之前先决定你要的是哪个分母。

    最终设置

    现有启动器上的三个环境变量覆盖——除默认值外不改文件:

    设置 值
    MAX_RUNNING_REQUESTS / MAX_NUM_SEQS 3
    MAX_TOTAL_TOKENS(池下限) 3,150,000
    MEMORY_FRACTION 0.942

    HiCache 层的主机内存随池增长(比例 2);有 287 GB 可用,这没问题。

    诚实的限制:客户端只有在真的同时发超过两个请求时才受益。 如果你的 agent 工具把子 agent 扇出限制在 2,第三个槽位大部分时间是闲的——它是为"两个会话加一个子 agent 重叠"那一刻准备的余量,不是你总能看到的吞吐倍增器。

    重要的设置

    设置 值 原因
    预填充 chunk 2,048 4,096 在这个引擎上会让长预填充崩溃(见上);2,048 保持约 4 GB 空闲,使 indexer tile 保持较大
    PYTORCH_CUDA_ALLOC_CONF expandable_segments:True 必需。没有它引擎会把约 5.7 GiB 搁置成 reserved-but-unused,连 256 token 请求都失败
    启动预热 无 上游的 --warmups serving_coverage 在 84 GiB 卡上 OOM
    --mem-fraction-static 0.942 配合 --max-total-tokens 3150000;实际设定用量的是上限而不是分数
    NCCL_P2P_LEVEL PHB 让 NCCL 对交换芯片外那张 GPU 使用直接 GPU 到 GPU 拷贝(全归约 3.83 对 19.57 GB/s)
    KV 池 3.15M token,fp8 三个 100 万会话保持驻留
    --swa-prefix-tails 32 配 HiCache 时主机层保留被逐出的尾部
    HiCache file 后端,write-through GPU -> 主机内存(4.2M token)-> NVMe;会话经受重启
    专家切分 108 / 92 / 92 / 92 96 GB 卡比 84 GB 卡承担更多专家
    Indexer 预算 256 MiB 上游现在已对 indexer 分块;我们只给它的分数预算设上限
    并发 --max-running-requests 3 三个槽位;每流速度降约 28%,但合计大致翻倍

    沿用的客户端附加组件:Claude Code /v1/messages 兼容、对话中 system 消息保持内联以便前缀缓存复用、采样默认 0.95 / 1.0、推理努力 "off" 别名,以及重复循环防护。

    如何自己得到这个结果

    如果你有四张 Blackwell 工作站卡并想要这个结果,工作顺序比任何单个 flag 都重要。 这是实际奏效的顺序。

    1. 先把互联弄对

    下游的一切都被 PCIe 限制。这些卡没有 NVLink,所以所有跨 GPU 流量都走 PCIe,拓扑决定你的天花板。

    • 在负载下检查链路宽度,而不是空闲时。 我们有一张卡因为坏 riser 只训练到 x8。空闲时看着正常。它损失了约 15% 预填充(冷启动 988K 253.5 秒对 223.0 秒),直到我们对比 nvidia-smi 链路宽度和 lspci -vv 才发现。每次打开机箱后都要复查。
    • 了解你的拓扑。 nvidia-smi topo -m 告诉你哪些卡共享交换芯片、哪些在自己的根端口上。我们的是:三张在 Broadcom PEX 88096 之后,一张直连。
    • 设置 NCCL_P2P_LEVEL=PHB。 没有它,发往交换芯片外那张 GPU 的流量会走主机内存。在我们机器上那是全归约 3.83 对 19.57 GB/s——5 倍差距,表现为冷启动 988K 的 374 秒对 223 秒。
    • 内核 flag 很重要。 我们用 amd_iommu=off iommu=pt pcie_aspm=off pci=realloc 启动。在我们上一块板子上,仅 iommu=pt 就把 TP4 预填充从 977 提升到 2,149 tok/s。

    2. 在追速度之前先定 KV 池大小

    100 万窗口首先是一个内存问题。三件事解锁了它:

    • 非均匀专家切分。 96 GB 卡比 84 GiB 卡承担更多专家:108 / 92 / 92 / 92。这把 84 GiB 卡上的最低空闲显存从 1,322 提到 3,798 MiB。
    • 把 indexer scratch 预算限制在 256 MiB(DSV41_INDEXER_BUDGET_MIB)。这是把窗口从 400K -> 600K -> 800K -> 100 万推上去的东西。
    • 用分层缓存。 HiCache 配 file 后端把 KV 层放到 NVMe,所以 777K 会话 5.1 秒回来,而不是 269.7 秒重新预填充。

    如果你在 84 GiB 卡上,预期要调切分而不是照抄一个:比例是你卡组构成的函数。

    3. 用长预填充来选 chunk 大小,而不是短提示

    这是最容易犯的错误。Chunk 4,096 在这个引擎上并不更快——它能启动、通过短测试,然后在长上下文上崩掉(500K 冷启动:278.7 秒对 chunk 2,048 的 54 秒)。

    不要因为它在 32K 上装得下就接受某个 chunk 大小。测一个 500K+ 的冷启动提示。 失败模式是一个反馈回路,而不是 OOM 崩溃:空闲内存耗尽时 indexer 缩小 tile,这倍增内核启动次数,而这又随上下文增长让问题更糟。

    4. 设好分配器,跳过预热

    PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True 在这个引擎上是必需的。没有它我们看到约 5.7 GiB 被搁置为 reserved-but-unused,连 256 token 请求都失败。注意这和我们旧引擎的发现相反,那时它会导致失控生成。

    不要用上游的 --warmups serving_coverage。它在 84 GiB 卡上 OOM。

    设 --mem-fraction-static 0.942 配 --max-total-tokens 3150000。实际设定池的是显式 token 上限,不是分数。

    5. 信任之前先验证

    能抓到我们错误的检查:

    检查 抓到了什么
    负载下的 nvidia-smi 链路宽度 一张卡训练到 x8——15% 预填充
    冷启动 500K+ 预填充,而不只是短提示 Chunk 4,096 在长上下文上崩溃
    多深度 needle 测试 静默的上下文退化
    峰值最低空闲显存 100 万窗口是否真的安全
    两个并发长提示 真实 agent 负载下的行为

    6. 预期要保留你自己的补丁

    原版配置是为四张 96 GB 卡调优的。在混装或 84 GiB 配置上你会带上自己的专家切分、indexer 预算和 chunk 大小。我们的记在上面的设置表里——把它们当作一个开始测量的起点,而不是可以照抄的配置。

    好消息:这个引擎已经包含我们之前手工移植的 indexer 分块和编码器修复,所以需要维护的东西比以前少了。

    死胡同

    尝试 结果
    Chunk 4,096(新引擎) 能启动,但长预填充崩溃:500K 冷启动 278.7 秒对 54 秒
    上游启动预热 在 84 GiB 卡上 OOM
    注意力输出的 fp8 往返 解码 -13%,无质量收益
    Chunk 12,288 / 16,384(旧引擎) 第一个 chunk 就 OOM
    Chunk 10,240(旧引擎) 装得下,988K 时快 8.6%,但 KV 池减半
    Chunk 8,192(旧引擎) 快 6.8%,代价 1.6 GB 显存;否决
    更大的 indexer 预算(1,024 MiB) 39 次分配失败,无加速
    fp4 indexer flag(旧引擎) 慢 25-29%;新引擎默认开启
    expandable_segments(旧引擎) 有害:失控生成。在新引擎上反过来了,是必需的
    SGLang 自定义全归约、torch 对称内存、NCCL NVLS 在 4 张纯 PCIe SM120 卡上不可用

    两个硬件教训

    一张卡因为坏 riser 只训练到 PCIe x8。它损失了约 15% 预填充(冷启动 988K 253.5 秒对 x16 的 223.0 秒),直到我们在负载下检查链路宽度才发现。换 riser 后:221.8 秒,解码 108.4。每次打开机箱后都检查 nvidia-smi / lspci。

    PHB 伴随着一次主机硬复位(一个 AMD "data fabric sync flood")。之后的重启是干净的,目前还不清楚 PHB 是否相关。在照抄这个之前值得知道。

    我们还没有证明的

    这些数字的诚实边界:

    • 新引擎上没有重跑质量测试集。 相同权重,功能与 needle 检查通过,但这个引擎上的 HumanEval/GSM8K 还没做。
    • 重启后的缓存恢复没有重测。 早先"777K 5.1 秒回来"的结果属于旧引擎。
    • 引擎的 PCIe-IPC 全归约在这些混装卡上从未启用("every rank must use identical collective arguments"),所以全归约跑在 NCCL 上。上面的每个数字都是在没有它的情况下——可能还有更多可用。
    • 10 月 21 日晚起进入生产,尚未浸泡。 所有数字都是基准流量。
    • 三个并发 988K 会话没有做 needle 探测。 三个都正确回答了,但三个 100 万会话驻留时的 needle 检索未测——见 3 槽一节的表格。3 槽上的单请求冷启动预填充是 needle 验证的 2/2。

    致谢

    DeepSeek 的 DeepSeek-V4.1-Flash-1017。SGLang 项目。ormandj 的 SM120 镜像(ghcr.io/ormandj/sglang-deepseek-v41-flash-sm120)。最初的 4x RTX PRO 6000 DS4.1 配置 0xSero/deepseek-v4.1-flash-4x-rtx-pro-6000(现在在 sybil-solutions 下)是旧引擎的基础。SGLang PR #43376 和 issue #40877 为 chunk 分析提供了参考;上游 PR #40217 是 indexer 分块思路的来源。

    方法: 冷启动提示与 needle 检查用 scripts/coldctx_run.py,短预填充用 scripts/bench_openai_ctx.py,解码用 scripts/decode_bench.py(512 token 的 2-3 次运行取中位数;单次运行波动约 ±10 tok/s)。


  • 登录

  • 登录或注册以进行搜索。
  • 第一个帖子
    最后一个帖子
0
  • 版块
  • 最新
  • 标签
  • 热门
  • 用户
  • 群组