【生命科学 Agent】推理平台(二)

有限 GPU 算力下,如何为生命科学 Agent 搭一套推理平台(二):选型与 Trade-off

系列导读:这是系列的第二篇。

  • 第一篇:回顾:负载的五个结构性特征如何否决掉标准答案,以及架构的四个基本决定(受理与执行解耦 / 按模型隔离队列 / 积压驱动弹性 / 弹性层与资源层正交)。
  • 第二篇(本文):这些决定落地到具体技术栈时怎么选,为什么最终是"消息队列 + Lag 驱动弹性 + GPU 虚拟化"这套组合;以及三个互相打架的核心矛盾如何折中。
  • 第三篇:平台与 Agent 怎么对话,以及怎么度量"系统到底有没有变好"。

上一篇我们留了个尾巴:架构被四个决定锚定之后,还剩两个问题没答------用什么实现排队?用什么实现弹性?GPU 到底怎么分?

这三个问题各自都有若干"看起来很香"的成熟方案,而我们的判断是:大部分方案不是不好,是它们优化的目标和我们不一样。这一篇把选型过程和取舍逻辑摊开讲。

一、先立四条评判标准

选型之前先定标尺,否则会陷入"大家都有道理"。我们用的是这四条:

标准 具体追问
① 扩缩容信号是否贴近真实负载 CPU / 内存使用率在 GPU 推理场景下失灵------机器很闲但队列很长的时候,它能感知吗?
② 能否缩到零 这是 GPU 场景最硬的省钱手段。K8s 原生 HPA 不支持缩到 0 副本,很多方案在这里就出局了
③ 能否做多租户与多模型的隔离 能不能按模型、按租户设配额?一个租户的批量任务能不能拖垮另一个租户的交互式请求?
④ 生态成熟度与运维成本 团队愿不愿意长期维护?出问题时有没有社区经验可查?

特别说明一下 ②:K8s 原生 HPA 是不能缩到零副本的(最小副本数为 1)。对 GPU 这种昂贵资源,这个限制几乎是致命的------一个每天被调用 20 次的工具,为了不违反这个限制要常驻一张卡,利用率可能不到 10%。

二、链路选型:为什么不是 vLLM,也不是 Slurm

把候选方案按上面四条标准过一遍:

方案 核心失败点 结论
同步 HTTP + 基于 CPU 的 HPA 违反 ①(CPU 完全反映不了 GPU 排队)、违反 ②(缩不到 0)、长任务必然超时 出局
Serverless GPU(Knative 类) 满足 ②,但冷启动对分钟级加载的蛋白模型不可接受;GPU 生态成熟度不足 暂不用,长期观察
纯批处理离线化(Airflow / Argo) 不满足交互式响应,Agent 会话会失去意义 作为旁路长期建设
自研队列 + KEDA 可行,但等于自己承担队列的可靠性、持久化与吞吐 备选
消息队列(Kafka)+ Lag 驱动弹性 + GPU 虚拟化 四条标准全部满足 主链路采用

这里有两个读者常问的问题,值得单独回答。

为什么不用 vLLM / Triton 这套?

因为它们解决的不是我们这一层的问题。

vLLM 的 PagedAttention、Triton 的动态批处理、continuous batching,优化的是"大量短请求如何在一张卡上摊薄开销"------它假设请求是可抢占、可合并、尺寸相近的。而我们的负载里,一个 RFDiffusion 请求会独占一张卡跑 15 分钟,它没有"批处理"的空间:你没法把两个 15 分钟的扩散采样塞进同一个 batch slot 里省时间。

但要诚实补充一句:这不代表它们没用,只是位置不同 。它们属于"单卡内的执行效率层",我们这套属于"跨卡的任务调度层"。两层是正交的:对于可批处理的模型(Embedding 类、部分结构预测),在 worker 内部引入动态批处理仍然是有效的优化手段;未来模型数量增多后,把部分模型迁移到 Triton 统一执行引擎也是合理的演进方向。只是它替代不了上层那套排队与弹性机制。

为什么不用 Slurm 那套?

Slurm / LSF 是极其成熟的资源管理器,backfill 算法、公平份额(fairshare)、优先级衰减都做得非常好。问题在于它们的交互契约:作业提交后你去查状态、去看日志、去文件系统取结果,整个语义是面向"人提交作业后离线等待"设计的。

Agent 需要的是:返回一个结构体、任务失败时返回结构化错误原因、能被告知"前面还有 8 个任務,预计等 4 分钟"。这些不是 Slurm 不能做,而是不在它的核心语义里,硬搬过来要在外面包一层厚厚的服务层------那就等于我们自建一个平台,只是底层换了批调度器。

结论:用消息队列做缓冲与在线式受理,用积压驱动弹性取代传统的 CPU 指标弹性,用专用的 GPU 虚拟化层管卡内分配。

三、GPU 资源层选型:一张卡究竟怎么分

这是本项目中收益最直接、也最容易踩坑的一层。候选方案四条:

方案 隔离机制 优点 致命缺陷
整卡独占 物理 零干扰、性能确定性最好 小模型浪费严重,8GB 的模型占 80GB 的卡 = 利用率天花板 10%
时间片共享(Time-slicing) 无 实现最简单,GPU Operator 直接提供 没有显存隔离------所有容器都"看得见"整张卡的显存,一个实例超额分配会把同卡邻居全部拖下水 OOM
MIG 硬件分区 硬件级(SM / L2 / 显存带宽全隔离) 隔离最强、性能可预期 只有部分卡型支持(A100 / H100 / A30 等),L40S、消费卡不支持;分区规格固定、无法动态调整算力比例;一张卡最多 7 个实例
HAMi(GPU 虚拟化 + 配额调度) 软件级拦截 显存硬隔离、算力按比例限流、跨厂商纳管、规格可动态调整 算力限制是尽力而为的软限制(见下文 caveat)

我们的选择是:HAMi 作为统一管理层,MIG 作为补充。大模型走整卡独占,中小模型走配额共享。

判断一个模型该用哪种方式,按下面这棵树走一遍基本就能定:
#mermaid-svg-Bh2f3bMcPfI1pZs0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Bh2f3bMcPfI1pZs0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .error-icon{fill:#552222;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .marker.cross{stroke:#333333;}#mermaid-svg-Bh2f3bMcPfI1pZs0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Bh2f3bMcPfI1pZs0 p{margin:0;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .cluster-label text{fill:#333;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .cluster-label span{color:#333;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .cluster-label span p{background-color:transparent;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .label text,#mermaid-svg-Bh2f3bMcPfI1pZs0 span{fill:#333;color:#333;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .node rect,#mermaid-svg-Bh2f3bMcPfI1pZs0 .node circle,#mermaid-svg-Bh2f3bMcPfI1pZs0 .node ellipse,#mermaid-svg-Bh2f3bMcPfI1pZs0 .node polygon,#mermaid-svg-Bh2f3bMcPfI1pZs0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .rough-node .label text,#mermaid-svg-Bh2f3bMcPfI1pZs0 .node .label text,#mermaid-svg-Bh2f3bMcPfI1pZs0 .image-shape .label,#mermaid-svg-Bh2f3bMcPfI1pZs0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .rough-node .label,#mermaid-svg-Bh2f3bMcPfI1pZs0 .node .label,#mermaid-svg-Bh2f3bMcPfI1pZs0 .image-shape .label,#mermaid-svg-Bh2f3bMcPfI1pZs0 .icon-shape .label{text-align:center;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .node.clickable{cursor:pointer;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .arrowheadPath{fill:#333333;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Bh2f3bMcPfI1pZs0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bh2f3bMcPfI1pZs0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Bh2f3bMcPfI1pZs0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .cluster text{fill:#333;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .cluster span{color:#333;}#mermaid-svg-Bh2f3bMcPfI1pZs0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Bh2f3bMcPfI1pZs0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .icon-shape,#mermaid-svg-Bh2f3bMcPfI1pZs0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .icon-shape p,#mermaid-svg-Bh2f3bMcPfI1pZs0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .icon-shape .label rect,#mermaid-svg-Bh2f3bMcPfI1pZs0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bh2f3bMcPfI1pZs0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Bh2f3bMcPfI1pZs0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Bh2f3bMcPfI1pZs0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
新模型接入

先取实测显存峰值
显存需求接近单卡容量

或依赖多卡 / NVLink 拓扑?
整卡独占
需要硬件级强隔离

且卡型支持 MIG?
MIG 硬件分区
时延敏感

且负载形态无规律?
虚拟化配额共享

显存按实测峰值 + 10~20% 余量

至于为什么"有没有显存隔离"这件事如此关键,看一张对比图就明白了:时间片共享下,一个实例的超额申请会把同卡所有邻居一起拖死;而配额共享把失败限制在它自己的容器内。
#mermaid-svg-VajeyT8uwM0FIarz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VajeyT8uwM0FIarz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VajeyT8uwM0FIarz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VajeyT8uwM0FIarz .error-icon{fill:#552222;}#mermaid-svg-VajeyT8uwM0FIarz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VajeyT8uwM0FIarz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VajeyT8uwM0FIarz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VajeyT8uwM0FIarz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VajeyT8uwM0FIarz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VajeyT8uwM0FIarz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VajeyT8uwM0FIarz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VajeyT8uwM0FIarz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VajeyT8uwM0FIarz .marker.cross{stroke:#333333;}#mermaid-svg-VajeyT8uwM0FIarz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VajeyT8uwM0FIarz p{margin:0;}#mermaid-svg-VajeyT8uwM0FIarz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VajeyT8uwM0FIarz .cluster-label text{fill:#333;}#mermaid-svg-VajeyT8uwM0FIarz .cluster-label span{color:#333;}#mermaid-svg-VajeyT8uwM0FIarz .cluster-label span p{background-color:transparent;}#mermaid-svg-VajeyT8uwM0FIarz .label text,#mermaid-svg-VajeyT8uwM0FIarz span{fill:#333;color:#333;}#mermaid-svg-VajeyT8uwM0FIarz .node rect,#mermaid-svg-VajeyT8uwM0FIarz .node circle,#mermaid-svg-VajeyT8uwM0FIarz .node ellipse,#mermaid-svg-VajeyT8uwM0FIarz .node polygon,#mermaid-svg-VajeyT8uwM0FIarz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VajeyT8uwM0FIarz .rough-node .label text,#mermaid-svg-VajeyT8uwM0FIarz .node .label text,#mermaid-svg-VajeyT8uwM0FIarz .image-shape .label,#mermaid-svg-VajeyT8uwM0FIarz .icon-shape .label{text-anchor:middle;}#mermaid-svg-VajeyT8uwM0FIarz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VajeyT8uwM0FIarz .rough-node .label,#mermaid-svg-VajeyT8uwM0FIarz .node .label,#mermaid-svg-VajeyT8uwM0FIarz .image-shape .label,#mermaid-svg-VajeyT8uwM0FIarz .icon-shape .label{text-align:center;}#mermaid-svg-VajeyT8uwM0FIarz .node.clickable{cursor:pointer;}#mermaid-svg-VajeyT8uwM0FIarz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VajeyT8uwM0FIarz .arrowheadPath{fill:#333333;}#mermaid-svg-VajeyT8uwM0FIarz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VajeyT8uwM0FIarz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VajeyT8uwM0FIarz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VajeyT8uwM0FIarz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VajeyT8uwM0FIarz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VajeyT8uwM0FIarz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VajeyT8uwM0FIarz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VajeyT8uwM0FIarz .cluster text{fill:#333;}#mermaid-svg-VajeyT8uwM0FIarz .cluster span{color:#333;}#mermaid-svg-VajeyT8uwM0FIarz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VajeyT8uwM0FIarz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VajeyT8uwM0FIarz rect.text{fill:none;stroke-width:0;}#mermaid-svg-VajeyT8uwM0FIarz .icon-shape,#mermaid-svg-VajeyT8uwM0FIarz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VajeyT8uwM0FIarz .icon-shape p,#mermaid-svg-VajeyT8uwM0FIarz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VajeyT8uwM0FIarz .icon-shape .label rect,#mermaid-svg-VajeyT8uwM0FIarz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VajeyT8uwM0FIarz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VajeyT8uwM0FIarz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VajeyT8uwM0FIarz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 虚拟化配额共享(显存硬隔离)
实例 A 超额申请显存
仅 A 自己容器内失败
实例 B、C 不受影响
时间片共享(无显存隔离)
实例 A 超额申请显存
整卡 OOM
实例 B、C 被连坐

关于 HAMi 的工作原理,简单说两点(理解机制比记住参数重要):

  1. 显存是硬隔离 :通过替换容器内的 CUDA 运行时做 API 层拦截,容器申请超过配额的显存会直接失败。关键好处是------单实例超额申请时,失败被限制在它自己容器内,不会把同卡的其他实例拖挂。这一点与时间片共享形成了本质区别。
  2. 算力是软限流 :按比例限制单个容器可占用的 SM 时间片。这是"尽力而为"的软限制,做不到硬件级强------所以 HAMi 的价值主要在显存隔离,算力限制用来防止极端饿死,而不是用来做严格的性能隔离。

这个 caveat 必须讲清楚,因为它直接影响使用姿势:共享的前提是显存配额留够余量(按实测峰值 + 10~20%),而不是指望算力限流来保证 SLA。如果某个模型需要严格的性能隔离且确定时延,那就不要走共享,老实给它整卡或者 MIG。

另外一个常被忽略的设计点:装箱策略 。虚拟化层在分配时应该优先填满已使用的卡,而不是把新实例摊到空白卡上------前者压缩碎片、腾出整卡给后来的大模型;后者会让每张卡都剩一点点配额,谁都用不上。"优先填满已用卡"(fill-first)的装箱策略,是提高整卡利用率最便宜的手段,几乎零成本,但效果显著。

四、三个核心 Trade-off

选型定了,真正的难点才开始。以下三个矛盾互相牵扯,每一个都没有标准答案,只能按我们的约束(卡少、任务长、冷启动贵)给出折中。

Trade-off A:缩到零省钱 vs 冷启动要命

这是最容易被人误解的一组矛盾------很多人以为"缩到零是免费的优化"。不是。冷启动的时间成本由某个请求承担,而这个请求通常是用户最敏感的第一个。

先给一个判断框架(简化模型,实践中按真实数据校准):

设某模型日均调用 N 次,单次实际 GPU 耗时 t ,单次分摊冷启动折算 GPU 时间 c(假设缩到零后每次都要付)。

  • 常驻 1 副本 :一天固定消耗约 24 GPU·小时
  • 缩到零 :一天消耗 N × (t + c)

两者谁大谁小,就是答案的方向。举个直观的例子(RFDiffusion,t ≈ 15 分钟 = 0.25h,c ≈ 2 分钟 ≈ 0.03h):

日均调用 N 缩零方案日消耗 对比常驻 24h 判断
20 次 ≈ 5.6 h 省约 77% 缩零划算,代价是前几次调用要多等约 2 分钟
100 次 ≈ 28 h 反超常驻 常驻或按需扩容更划算
200 次 ≈ 56 h 远超 必须常驻 + 多副本

但这个公式有个重要修正:看的是调用的时间集中度,不是日均 。如果一天 20 次调用全部集中在同一小时内到达,那等价于瞬时并发 20,缩到零毫无意义------因为它每次起身就被立刻打满。所以真正要统计的是"单位时间窗口内的请求密度",而不是总数。

把上面的数字画出来,交叉点一目了然------在交叉点左侧,缩零省得实实在在;越过交叉点之后,"省资源"的选择反而更贵:

图中蓝线为缩到零的日消耗 N×(t+c),红线为常驻一副本的固定 24 GPU·小时;两者在约 86 次/天处相交。

落到具体策略,我们按调用频率分成三档:

档位 策略 配套手段
高频(分钟级有请求) 保留常驻副本 冷启动代价 > 常驻成本
中频(小时级) 缩到零 + 权重预热 权重预置到本地高速盘或镜像内,冷启动压到 10~20 秒;精简镜像;GPU 节点池保留少量常驻节点避免连节点都现开
低频(天级) 缩到零,明确接受排队 把冷启动预期显式告诉 Agent(第三篇展开)

最后一个"冷启动优化手段"经常被遗漏:在 Agent 侧显式声明等待预期 。上层框架通常有自己的超时门禁,如果它不知道这次调用要冷启动 90 秒,就会在第 60 秒把请求判定为失败------然后重试,然后更慢,形成灾难性的重试风暴。所以冷启动不只是一个技术问题,它更是一份必须向上暴露的契约。

Trade-off B:长任务不被杀 vs 弹性要缩容

这是整套架构里风险最高的一处。RFDiffusion 单次跑 10~30 分钟,如果这期间弹性层判定流量回落、要缩容,被选中的 Pod 一删,就意味着:任务白跑、GPU 时间浪费、Agent 端还要重来一遍。

解法是四重防护,逐层拦截一个越缩越莽的请求,缺一不可:
#mermaid-svg-YQ5ItbxGItaG3Vyk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YQ5ItbxGItaG3Vyk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YQ5ItbxGItaG3Vyk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YQ5ItbxGItaG3Vyk .error-icon{fill:#552222;}#mermaid-svg-YQ5ItbxGItaG3Vyk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YQ5ItbxGItaG3Vyk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YQ5ItbxGItaG3Vyk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YQ5ItbxGItaG3Vyk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YQ5ItbxGItaG3Vyk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YQ5ItbxGItaG3Vyk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YQ5ItbxGItaG3Vyk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YQ5ItbxGItaG3Vyk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YQ5ItbxGItaG3Vyk .marker.cross{stroke:#333333;}#mermaid-svg-YQ5ItbxGItaG3Vyk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YQ5ItbxGItaG3Vyk p{margin:0;}#mermaid-svg-YQ5ItbxGItaG3Vyk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YQ5ItbxGItaG3Vyk .cluster-label text{fill:#333;}#mermaid-svg-YQ5ItbxGItaG3Vyk .cluster-label span{color:#333;}#mermaid-svg-YQ5ItbxGItaG3Vyk .cluster-label span p{background-color:transparent;}#mermaid-svg-YQ5ItbxGItaG3Vyk .label text,#mermaid-svg-YQ5ItbxGItaG3Vyk span{fill:#333;color:#333;}#mermaid-svg-YQ5ItbxGItaG3Vyk .node rect,#mermaid-svg-YQ5ItbxGItaG3Vyk .node circle,#mermaid-svg-YQ5ItbxGItaG3Vyk .node ellipse,#mermaid-svg-YQ5ItbxGItaG3Vyk .node polygon,#mermaid-svg-YQ5ItbxGItaG3Vyk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YQ5ItbxGItaG3Vyk .rough-node .label text,#mermaid-svg-YQ5ItbxGItaG3Vyk .node .label text,#mermaid-svg-YQ5ItbxGItaG3Vyk .image-shape .label,#mermaid-svg-YQ5ItbxGItaG3Vyk .icon-shape .label{text-anchor:middle;}#mermaid-svg-YQ5ItbxGItaG3Vyk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YQ5ItbxGItaG3Vyk .rough-node .label,#mermaid-svg-YQ5ItbxGItaG3Vyk .node .label,#mermaid-svg-YQ5ItbxGItaG3Vyk .image-shape .label,#mermaid-svg-YQ5ItbxGItaG3Vyk .icon-shape .label{text-align:center;}#mermaid-svg-YQ5ItbxGItaG3Vyk .node.clickable{cursor:pointer;}#mermaid-svg-YQ5ItbxGItaG3Vyk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YQ5ItbxGItaG3Vyk .arrowheadPath{fill:#333333;}#mermaid-svg-YQ5ItbxGItaG3Vyk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YQ5ItbxGItaG3Vyk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YQ5ItbxGItaG3Vyk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YQ5ItbxGItaG3Vyk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YQ5ItbxGItaG3Vyk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YQ5ItbxGItaG3Vyk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YQ5ItbxGItaG3Vyk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YQ5ItbxGItaG3Vyk .cluster text{fill:#333;}#mermaid-svg-YQ5ItbxGItaG3Vyk .cluster span{color:#333;}#mermaid-svg-YQ5ItbxGItaG3Vyk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YQ5ItbxGItaG3Vyk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YQ5ItbxGItaG3Vyk rect.text{fill:none;stroke-width:0;}#mermaid-svg-YQ5ItbxGItaG3Vyk .icon-shape,#mermaid-svg-YQ5ItbxGItaG3Vyk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YQ5ItbxGItaG3Vyk .icon-shape p,#mermaid-svg-YQ5ItbxGItaG3Vyk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YQ5ItbxGItaG3Vyk .icon-shape .label rect,#mermaid-svg-YQ5ItbxGItaG3Vyk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YQ5ItbxGItaG3Vyk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YQ5ItbxGItaG3Vyk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YQ5ItbxGItaG3Vyk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 仍被选中
被强删
极端情况
弹性层发出缩容信号
① 缩容限速

窗口拉长到 10 分钟,每次最多缩 1 个副本

兜底:防止长任务刚起就被判为多余
② 宽限期兜底

终止宽限期 > 单任务最大耗时

兜底:即使被选中,也能跑完再退
③ 删除优先级

忙 Pod 删除代价调高,空闲 Pod 先被删

兜底:大幅降低选中忙 Pod 的概率
④ 任务可恢复

优雅退出重投在途任务 + request_id 幂等

兜底:真被杀了也能重跑,且不重复消耗算力
最终效果:任务不丢,算力不重复消耗

防护层 机制 兜底什么
① 缩容限速 拉长缩容稳定窗口(10 分钟级)、每次最多缩 1 个副本 给长任务留出时间
② 宽限期兜底 终止宽限期必须 > 单任务最大耗时 即使 Pod 被选中,也能跑完再退
③ 删除优先级 Worker 处理任务时动态调高自己 Pod 的删除代价 K8s annotation 让空闲 Pod 优先被删
④ 任务可恢复 优雅退出时重投在途任务 + request_id 幂等去重 真被杀了也能重跑,且不重复消耗算力

第 ③ 层值得单独说一下:K8s 的 ReplicaSet 缩容时默认按创建时间选 Pod,完全不考虑它是否正在干活。通过让 Worker 在处理任务时提高自己的删除代价、结束时降回来,可以让"删除请求打到忙 Pod"的概率大幅下降------这是四层里性价比最高的一层。

再往前一步的经验是:不要让超长任务和短任务共用同一个弹性池。我们倾向于按任务时长划分"泳道":

  • 在线泳道(秒级):常规弹性池,可缩到零;
  • 长任务泳道(分钟到 20 分钟):常驻下限 + 极度保守的缩容策略;
  • 超长/旁路泳道(30 分钟以上):走任务级调度(如 KEDA 的 ScaledJob,一个 Job 跑一个任务,跑完即退),或离线批处理通道。

把不同时长的任务混在一个池子里,就等于要求弹性策略同时照顾两个互斥的目标,最后往往是两边都优化不到位。分离泳道比在同一个池子里打补丁要干净得多。

Trade-off C:整卡独占 vs 切分共享

直觉上"能切就切",但这是有边界的。共享的收益来自"小模型用不完显存",代价是干扰与不确定性。我们判断什么时候不该切:

  1. 显存需求接近整卡:本来就吃得差不多,切分得不到任何收益,只平白引入干扰风险;
  2. 时延敏感且没有规律:共享带来的算力争抢会明显放大尾部延迟,P99 会很难看;
  3. 依赖多卡拓扑或 NVLink 的任务:跨实例的通信与带宽隔离会破坏性能假设;
  4. 极低频但单次极贵的任务:切分带来的调度复杂度超过收益,宁可保证它 100% 跑成功。

反过来,什么时候必须切 也很清楚:显存峰值远低于单卡容量、调用量大、单任务时间短的模型。这类模型是切分收益的绝对主力。

配比的落地方法其实很朴素:按模型实测显存峰值 + 10~20% 余量切,而不是按模型"说"它需要多少。举个例子(数据需按实际校准):

卡型 模型 单实例配额 每张卡承载
A100-80G Protenix / Boltz-2(中) ~24GB / 50% 算力 2 个实例
L40S-48G Embedding 类(小) ~8GB / 25% 算力 4~5 个实例
A100-80G OpenFold3 / RFDiffusion 整卡独占 1 个实例

另外两个几乎"免费"的利用率提升不容忽视:结果缓存 (生命科学场景中相同输入 + 相同随机种子的重复请求比例并不低,命中即零 GPU 消耗,是投入产出比最高的一项优化之一)和任务去重合并(Agent 并发发起相同 tool call 时,在网关侧合并成一次真实推理)。

五、可靠性语义:放弃 exactly-once 的幻觉

最后一个取舍是语义层面的,但它决定了整条链路怎么写。

我们明确不承诺严格 exactly-once。在一个"异构存储(结果存储)与队列位点提交"必然分离的分布式系统里,要做到严格一次,需要两阶段提交或事务性输出,代价极高,而收益很小。

我们的选择是 at-least-once + 幂等:先写结果,后提交消费位点(顺序很重要,反过来的话可能出现"位点提交了但结果没落盘",那就是永久丢任务)。这会带来重复投递,而重复投递由幂等键消化------重复的消息命中缓存后直接返回,不会二次消耗 GPU。

配套的是按错误分类处理,无脑重试只会浪费算力:

错误类型 处理策略
参数非法 / 序列格式错误 不重试,直接失败让 Agent 修正输入
显存 OOM 检查配额是否偏小;降 batch / 降精度重试一次,仍失败进死信队列
推理超时 按截止时间判断,Agent 已经放弃的请求不要继续烧卡
消息队列 / 网络抖动 指数退避重试,限定次数
节点故障 / Pod 被驱逐 依赖未提交的位点自动重投(at-least-once 的天然收益)

一句话总结这套语义:允许重复计算(可幂等消除),不允许丢任务(不可恢复)。

六、本篇小结:决策清单

把这一篇的所有结论收拢成一张表:

问题 决策 主要理由
长任务怎么不超时 受理与执行解耦,异步返回结果 同步模型在分钟级任务面前物理失效
用什么放缓冲 消息队列,每模型独立 削峰 + 故障隔离 + 天然可被 Lag 观测
扩缩容听谁的 队列积压(Lag),不是 CPU Lag 是唯一既能反映真实负载又能缩到零的信号
弹性框架 KEDA(可缩到零)+ HPA 执行 K8s 原生 HPA 不支持缩到零
GPU 怎么分 虚拟化层配额共享为主,大模型整卡,强隔离需求用 MIG 显存硬隔离是共享安全的前提
长任务怎么保护 缩容限速 + 宽限期 + 删除优先级 + 任务可恢复 四层缺一层都会漏
一致性语义 at-least-once + 幂等 允许重复计算,不允许丢任务

到现在为止,我们把这套平台的"内部逻辑"讲完了。但有一个角色一直被当作背景板:调用方 Agent。

实际上,这套架构里最有价值、也最容易被忽略的部分,恰恰是平台与 Agent 之间的契约:怎么告诉 Agent"前面还有 8 个任务,预计等 4 分钟"、怎么告诉它"Protenix 挂了,要不要试试 Boltz-2"、怎么保证 Agent 的重试不会把自己的平台打垮、以及------怎么度量这一切到底有没有变好。第三篇我们就讲这件事。

相关推荐
IT_陈寒1 小时前
Vue的这个响应式陷阱我居然现在才发现
前端·人工智能·后端
lanxiaoqing021 小时前
演讲训练的第一步:先敢说,再会说
人工智能·学习·学习方法·口才
巫山老妖1 小时前
REA 工程解析:让 AI Agent 拿着证据做逆向
人工智能
梦想的颜色1 小时前
【AI速览】Codex 系列模型选型指南:效率、成本、任务匹配全维度决策
人工智能·chatgpt·codex·luna·astra·sol·大模型选型
安全指北针1 小时前
密态计算:数据 “可用不可见” 的底层技术解析
人工智能·算法
一一哥Sun1 小时前
IVF倒排索引:先看目录,再翻书
人工智能·深度学习·机器学习
涓涓5271 小时前
五金厂排程乱?管家通ERP标准版把期排稳
人工智能
Jmyd01232 小时前
3D 扫描仪选型技术拆解:精度、点云与软件链路
人工智能·算法·3d·三维数字化
幂律智能2 小时前
穿透式监管之下,合同分级分类如何落地
人工智能