首页 / 资讯中心 / 文章详情

CPU-Agent混合调度实战:CPU执行池与GPU路由降低推理成本

CPU-Agent混合调度实战:CPU执行池与GPU路由降低推理成本 ★ FEATURED ARTICLE
2024年下半年开始我手上所有Agent相关的项目几乎都碰到了同一个问题GPU资源不够烧。不是显存不够而是成本不够——线上业务聊天式应用把推理集群打到满负荷结果真正跑Agent任务工具调用、JSON提取、多步规划的高价值请求反而在排队。后来我把一部分执行引擎迁到CPU上按混部方式做了一套调度才把局面撑住。这篇就聊聊我当时对“CPU-Agent与混合调度”的理解和落地经验。如果你也是在跑LLM Agent服务、智能客服、自动化工作流这类场景的开发者或者正在为推理成本发愁这篇文章应该能省下你几周的试错时间。它不是什么学术新概念就是一套非常务实的分流执行架构便宜的CPU执行引擎处理轻量任务昂贵的GPU只留给真正需要大模型推理的请求中间用一层调度器把流量按策略分配出去。1. 混合调度要解决的核心矛盾1.1 为什么Agent服务不能全量跑GPU先说一个很多人没意识到的事实在Agent系统里真正需要“大模型智慧”的请求占比远比你想象的低。我做过粗略统计在典型的企业级Agent场景中超过60%的调用链是这类模式用户输入 → 意图识别 → 参数抽取 → 工具查询 → 结果格式化 → 返回。整条链路里只有意图识别和复杂参数抽取真正需要大模型的泛化能力其余步骤本质上是结构化处理。但这些中间步骤在传统架构里都会各自发一次LLM调用。假设一次完整Agent任务要经过5轮LLM调用实际只有1.5轮左右是“高智力需求”剩下的3.5轮用轻量模型甚至规则逻辑就能完成。全量GPU推理等于把所有计算都放在最贵的资源上执行而且这些中间步骤往往还要等GPU排队时延体验并不好。CPU-Agent的思路就是把这3.5轮低成本环节拿走放到用CPU推理的小模型上执行。现在开源的小参数模型比如7B甚至3B量级在量化到4-bit之后单机CPU上用ONNX Runtime或者llama.cpp这类推理框架跑单次请求延迟可以控制在几百毫秒到一两秒之间。对工具调用、数据抽取这种任务来说这个速度完全够用而且可以横向加机器。1.2 “混合调度”调的不是CPU是路由决策混合调度这个词容易误导人听起来像操作系统里的CPU调度比如CFS、时间片轮转之类。在Agent架构里混合调度更准确的说法是“异构执行路由”核心问题是一个请求进来到底该由哪类执行引擎来处理。这个决策直接影响服务质量。路由错了要么GPU被低价值请求拖垮要么CPU负载过高把时延搞到不可接受。所以混合调度的核心不是调CPU参数而是设计一套可靠的路由策略让请求在GPU和CPU两个执行池之间动态分配。我在实践中把路由决策纳入了几类特征的判断不只是看任务类型还看模型规模、实时负载、成本预算和SLA约束。举个例子一个Agent系统的GPU池当前排队已经超过30秒即便请求本身需要较高智力也可以先让CPU上的小模型试跑一版如果结果质量校验不过再升级到GPU。这种降级链路是我见过最实用的兜底方案。2. 调度策略的设计与取舍2.1 基于规则的调度快但脆最先落地的肯定是硬编码规则。比如按“任务类型路由”把JSON修复、格式转换、简单分类这类请求直接分给CPU执行池把复杂推理、长上下文理解、多轮对话生成分给GPU执行池。我曾经在模拟项目X里搭建过一个最小规则调度器分类逻辑非常简单AGENT_TASK_PRIORITY { code_completion: gpu, complex_reasoning: gpu, long_context_summary: gpu, tool_call_extraction: cpu, json_repair: cpu, intent_classification: cpu, keyword_extraction: cpu, }按类型路由最大的好处是零复杂度、零延迟损耗路由本身就是一个字典查表操作。坏处也很明显一旦流量分布与预设不符或者任务的智力需求判断是错的整个系统就僵住了。比如“tool_call_extraction”在某个领域B中非常复杂需要理解大量隐含语义CPU上的小模型怎么都抽不准确规则路由却不自知它会持续把小模型生成的错误结果发给下游工具造成一连串的失败。所以纯规则调度只适合作为初始版本它存在的意义是让系统先跑起来积累业务流量数据为后续的策略升级提供依据。2.2 基于负载的调度自适应但有滞后规则调度的脆弱性促使我很快加上了负载感知。GPU池的排队长度、显存利用率、平均时延这些指标可以实时反映集群的健康度。调度器定期我一般用1-2秒的窗口采集这些指标当GPU排队过长时把一部分本应上GPU的请求临时降级到CPU执行池。这个机制的本质是用CPU的“低智力容量”来吸收GPU的“流量尖峰”。高峰期保命低峰期把流量还给GPU以稳质量。负载调度的难点在滞后性。我踩过一个坑调度器每5秒采集一次GPU指标结果一次促销活动流量瞬间上升调度器还停留在上一轮的“GPU空闲”判断里所有请求一股脑被路由到GPU直接把推理队列打爆了。后来我把采集窗口缩到1秒并且加了“连续两次超阈值才切换”的滞回逻辑才压住了抖动。负载调度也不是万能的它只解决了“资源有没有”的问题没解决“质量行不行”的问题。比如GPU不忙但请求本身复杂度极低按负载策略它还是会被调度到GPU造成成本浪费。负载感知需要和类型规则配合着用。2.3 基于反馈的调度先把质量评分拉进来如果让我给一个词形容混合调度的最优状态那就是“路由是动态进化的”。规则负责初筛负载负责削峰而反馈评分则负责持续修正错误路由。我在项目中实现过一个简单的反馈回路CPU引擎产生结果后增加一个轻量质量评分器对结果进行结构化校验和置信度打分低于阈值的请求触发“升级执行”把请求重新转到GPU执行一次。这个机制在论文里叫“级联执行”在实际系统里非常有效。async def route_and_execute(request): # 第一层规则判断 target rule_router(request) # 第二层负载感知可选 if is_gpu_pool_busy() and target gpu: target fallback_to_cpu(request) # 第三层执行与反馈 if target cpu: result await cpu_executor.run(request) score quality_scorer(result, request) if score 0.7: result await gpu_executor.run(request) else: result await gpu_executor.run(request) return result这套结构里的关键点是质量评分器本身不能成为瓶颈。它不能自己又是一个大模型否则成本反而上去了。实践中我用的是组合校验必填字段是否存在、JSON能否正确解析、输出格式是否符合schema、关键词召回是否达标。这些规则加在一起判断“坏结果”的精确率大概能到85%-90%漏判的那部分就交给下游兜底逻辑去处理。还有一种更高级的反馈方案是拿隐式信号做路由评分比如用户是否点击了推荐结果、Agent后续工具是否执行成功、用户是否重新提问。这些信号收集周期长稳定性和收益波动比较大适合跑长周期实验不适合做实时调度。我的建议是先把显式校验做好隐式反馈等系统成熟后再说。3. 落地一个最小混合调度系统3.1 总体架构与组件职责一个能够稳定工作的混合调度系统大致包含四个组件接入网关、调度决策器、CPU执行池、GPU执行池。组件之间用消息队列解耦比直接HTTP调用更稳原因在于任务高峰期系统可以靠队列天然削峰不会直接把后端的执行引擎压垮。接入网关负责统一接收外部Agent请求把请求改造成标准结构体填充任务ID、类型标签、优先级、SLA超时时间、模型偏好等字段然后投递给调度决策器。调度决策器是整个系统的核心它负责读取请求元数据、实时集群指标和规则配置最终输出路由目标。我把决策器做成无状态服务可以水平扩展。它内部维护一个缓存每500毫秒从监控系统拉取一次各执行池的健康状态然后在决策时把“静态规则 动态负载 反馈评分”三层逻辑组合起来。CPU执行池和GPU执行池是真正干活的地方对外暴露完全一致的执行接口。这个接口一致性是混合调度中最重要的一条原则上层调用方不应该知道请求到底由谁执行。如果接口不一致那么调度本身就失去了意义因为升级降级都会变成对下游不可控的黑盒操作。ExecutionResult { request_id: str output: str execution_engine: cpu | gpu cost_usd: float latency_ms: int quality_score: float }3.2 路由决策参数的推导过程决策器需要计算的核心参数我建议优先关注这几个GPU池繁忙度、CPU池排队长度、当前请求的预估复杂度、预估执行成本。不要一上来搞机器学习模型先用可解释的启发式参数再根据统计数据去调整阈值。第一个参数是GPU繁忙度一个简单的计算公式gpu_busy_score (active_requests_in_gpu_queue gpu_inference_batch_capacity) / gpu_total_replica这个分数的含义是GPU集群当前“平均每个副本上有几个请求在排队”。如果得分大于1.0说明GPU已经过载小于0.5说明集群相对空闲。在我的系统里超过0.8时就会把低复杂度请求降级到CPU池超过1.2时所有非关键任务统一走CPU。第二个参数是CPU执行预估延迟。不同模型在不同机器上的延迟数据完全不同不要拍脑袋设阈值。我建议上线前先做压测把典型任务按输入长度分段测量CPU推理的P50和P95延迟。然后设定一个“兜底容量”指标cpu_budget_p95 p95_cpu_latency * current_cpu_queue_length如果这个值超过了请求的SLA剩余时间说明CPU池无法满足该请求的时延要求即便它很轻量也不能冒险全压在CPU上这时候就要牺牲一部分成本换取SLA稳定。参数计算标准化很重要。运维同学在群里看指标的时候如果每个指标都有明确的推导公式沟通过程会顺畅得多。我看过太多集群出问题是因为阈值拍脑袋然后大家天天线上吹牛、拍桌调参。3.3 执行引擎的接口一致性与降级链路设计接口一致性的实现细节我直接用一套协议对象来强制约束。每个执行引擎无论CPU还是GPU都实现同一个抽象类。class BaseExecutionEngine(ABC): abstractmethod async def run(self, request: AgentRequest) - ExecutionResult: ... abstractmethod def health_check(self) - EngineHealth: ...CPU执行引擎和GPU执行引擎在实际算子实现上差别很大CPU端我用了OpenMP多线程配合批处理把多个小请求合并在一个batch里推理GPU端则是用常见的连续批处理continuous batching提升吞吐。但在对外接口上二者必须无差别。这样的最大好处是降级链路可以做到“透明”。调度器从“万一”的角度出发永远给请求准备Plan B。我的降级链路设计成三层第一层正常路由按调度策略走。第二层执行失败降级CPU引擎执行超时或进程崩溃自动把请求重新投递给GPU引擎同时借助队列的重试机制。第三层结果质量降级CPU引擎成功返回结果但质量评分不达标则触发升级执行。这里需要特别注意的是幂等性。线路A执行到一半失败了线路B又重新执行如果该请求带有外部副作用比如已经写了数据库记录、已经调用了第三方接口那么重试可能造成重复操作。我的做法是在接入网关为每个请求生成全局唯一request_id并且把“已执行”状态写入Redis执行引擎在执行前先检查request_id是否已存在结果如果存在就直接返回缓存结果。3.4 模型选择的经验准则CPU执行池里的模型选择直接决定了这套架构的上限和下限。我试过几个不同配置踩了不少坑整理出一条经验准则。规模上强烈建议7B及以下实测下来4-bit量化后的7B模型在CPU上单请求延迟在500ms-2s之间能满足大多数非交互式Agent场景。14B以上的模型就不要指望CPU了除非你愿意忍受10秒级别的延迟这几乎等同于不可用。能力阈值上纯文本抽取、关键词还原、JSON修复、格式路由这一类任务是CPU模型的舒适区。而复杂推理、数学计算、长文档分析和多步对话——这些需要广阔知识和深层语义理解的任务小模型在CPU上的成功率会明显偏低。实测中我发现CPU小模型做情感判断还行做“因果推理”类的任务成功率会从GPU大模型的80%掉到50%以下。最后一个重要经验是量化方式。我建议使用4-bit或5-bit量化不要使用8-bit。8-bit在CPU上既不省内存又慢性价比极低。量化质量方面我对比过GPTQ和GGUF Q4_K_M这类量化格式后者配合llama.cpp框架在CPU上的兼容性和速度都更好。4. 监控、调参与踩坑记录4.1 关键监控指标与告警配置混合调度上线后最大的风险不是调度策略写得烂而是你根本不知道策略当前跑得怎么样。没有数据支撑的调度策略本质上就是盲飞。我强烈建议最少监控以下指标并且全部做成可视化面板路由分布每分钟进入CPU和GPU的请求数量及占比这个指标能直接反映流量策略是否符合预期各引擎成功率CPU和GPU各自的执行成功率以及质量评分通过率延迟分位数CPU池的P50/P95、GPU池的P50/P95、以及“升级执行”路径的P95降级率因负载过高或质量不合格而从CPU升级到GPU的请求比例成本核算每分钟CPU和GPU的推理成本估算告警阈值我建议分两档一档是“需要关注”一档是“必须立刻处理”。CPU池P95延迟超过3秒必须立刻介入超过2秒需要关注。降级率超过10%必须立刻介入超过5%需要关注。CPU池成功率低于95%必须立刻介入说明当前CPU模型可能不适合承接这类任务。这些阈值我都是从线上事故里“买”回来的。起初我设置CPU池P95超过5秒才告警理由是小模型本来就不快应该宽容一点结果一堆用户感受到超长等待工单反馈都炸了。后来又收紧发现CPU任务分布本身波动很大阈值太紧反而频繁误报。最终还是按请求类型分别设置延迟告警才平衡下来。4.2 双引擎压测与对比实验方法调参要有依据依据一定要来自压测数据而不是直觉。我推荐一套简单的双引擎对比压测流程。第一步准备一份有代表性的请求样本集。从生产日志里随机采样1000-2000条真实请求覆盖各种任务类型和输入长度分布。千万别用纯在线的人造的样例看不出真实负载特征。第二步分别对CPU池和GPU池跑同一份样本集记录每条请求的延迟、成功率、质量分数和成本。把结果存成明细表注意样本里面的每条请求最好都有任务类型标签之后可以按类型分析。第三步用这份数据画出“能力-成本”曲线。横轴是质量分数纵轴是单请求成本。你会看到一个非常明显的分界低复杂度任务在CPU上效果与GPU相当单位成本能降低80%以上高复杂度任务在CPU上的质量分数会断崖式下跌。这个分界点就是你路由规则里“CPU是否应该承接该类型请求”的决策边界。第四步做灰度切流。新策略先在1%流量上观察一天再逐步放大到10%、50%、100%。数据没有异常再全量。压测过程中要特别关注的是“极端输入长度”。我发现CPU推理对输入长度特别敏感同样的任务输入从500字涨到2000字CPU延迟能翻3倍以上而GPU的延迟增量只有30%-50%。混合调度的路由规则里长输入任务一定不能轻率降级到CPU。4.3 高峰期的资源排队策略我见过很多团队在高峰期直接把调度策略锁死把一切请求都扔到GPU上然后GPU被打爆后整体服务雪崩。这种做法与混合调度的初衷完全背道而驰。正确的事情是高峰期要敢于把更多的请求放到CPU上但要有节奏和规则。我实践的一套高峰期排队策略是“优先级分级 分配权重倾斜”。任务分成P0、P1、P2三个优先级。P0是核心交易链路或用户强交互类请求必须保障GPU资源和低延迟P1是Agent工具调用类请求可以容忍秒级延迟高峰期优先放CPUP2是离线批量处理类任务可以排队等候、可以随时降级。GPU池高峰期任务调度优先级P0请求优先占GPU算力P1请求按GPU负载动态分流P2请求全部切到CPU池或者延迟到低峰处理。在实际案例里我还尝试过用“容量预留”的思路给P0请求预留一部分GPU算力配额防止大量P1请求涌入时把P0的时延也拉垮。这个思路和公有云的抢占式实例机制相似只是我们去掉了竞价环节直接用固定的池子比例来分割。5. 常见问题与排查实录5.1 任务抖动CPU与GPU之间来回切换排查过最典型的一次调度问题是新版本上线后在监控面板上发现大量请求在CPU与GPU之间反复切换不是一路升上去就完事而是在几个执行引擎之间来回跳跃。整个系统的平均延迟暴涨比全切GPU时还慢。原因很快锁定了反馈回路过于灵敏。质量评分器对“字段缺失”定义得太严格CPU结果明明大部分字段都对只缺一个非核心字段评分就低于阈值触发升级。升级到GPU后结果倒是全了但因为GPU是拿完整prompt重新推理的输出风格和CPU的小模型版本不太一致导致下游某个校验步骤又认为这个结果不符合预期触发重试于是请求又被重新切回CPU。我解决这个问题的方式是给反馈回路加了一个“质量差异容忍窗口”只有当评分低于阈值且低于“历史平均分-方差”后才触发升级单次低分不升级。同时对“缺失字段”做了归一化处理非核心字段缺失只扣轻量分数不触发降级链路。另一个抖动来源是负载指标的高频波动。我遇到过GPU池瞬时排队数跳到阈值边缘调度器在1秒窗口内反复切换路由目标的情况。解决办法是给负载判断加上滞回区间阈值是0.8但只有连续两次采样都超过0.85才切换切换回GPU则要低于0.6。震荡频率明显下降。5.2 升级后请求执行时间对不上有一次线上排查某请求的记录显示CPU执行了800msGPU执行了1.2秒整条链路却显示了0.8秒的耗时。我一开始以为是监控布点问题后来才发现是链路的订阅消息里有重复request_id多个trace段串线了。这个问题的根因在于请求在升级过程中没有生成新的子请求ID全链路追踪系统无法区分“CPU执行”和“GPU执行”是两个阶段的独立操作。修复方式很简单在升级执行时生成新的子request_id并保留父request_id作为关联键。这样在执行时间统计时就能把CPU和GPU阶段分开计算成本归属也准确了。排查这个问题的方法也值得分享对于耗时异常的任务先去看全链路trace里的时间戳顺序确认是从CPU到GPU的切换点异常还是排队等待时间异常。大多数升级执行链路的时间损耗并不是发生在推理阶段而是发生在等待队列调度、消息传输、状态同步这些“非推理”环节。5.3 CPU推理质量不够怎么兜底这是混合调度被问得最多的问题“你把请求丢给CPU小模型结果质量差怎么办”我前面提过反馈评分升级但那是事后补救。如果任务本身质量要求极高就不应该让CPU承担试错成本。我所采取的做法是“质量分级路由”。把请求按“可容忍错误率”分成几个档位对错误容忍度高的批量任务如内容初筛、关键词提取CPU直接执行对结果需要直接返回给交互用户的CPU只做预推理生成初稿再由GPU做精修前文提到的级联模式对完全不能出错的核心任务支付类、强合规类直接禁用CPU不允许降级。级联模式下CPU还承担了一个重要职责它可以作为GPU的“候选生成器”先把初步结果算好GPU基于候选结果做增量精修而不是从零开始生成。这样能大大缩短GPU处理时间综合成本反而比全GPU更低。不过我建议级联模式只在特定场景开启因为它的中间产物管理、对账和调试复杂度会明显提升。如果业务本身没有强质量约束优先使用简单的独立路由模式。5.4 结果缓存与幂等性的魔鬼细节最后提一个容易翻车的点混合调度中的幂等性。在CPU和GPU两套引擎之间反复升级天然容易产生“同一请求被执行两次”的情况尤其当调用外部API时修复很麻烦。在最初版本中我假设“升级执行必然是在CPU失败后发生”所以没设计幂等机制结果一个用户点了按钮之后服务端连开了两个任务第三方API被调了两次幸好下游系统自己做了去重没有产生实质的资金损失。但已经是惊险一幕了。现在我的规范是所有Agent请求必须携带全局唯一ID且执行引擎在任何外部副作用操作前都要先检查ID在状态存储中是否完成。如果已完成直接返回已有结果。这个状态存储选用Redis或普通数据库都可以核心是操作要原子化并且要设置合理的过期时间避免死数据堆积。这套幂等机制在高峰期稍微多消耗一点状态查询的开销但换取的是系统的可重试性否则一旦高峰期出事修复成本远高于那一点额外开销。最后聊聊我对这个方向的感受CPU-Agent与混合调度这个方向核心精神是“资源匹配需求”。不同任务不同质量需求不同质量需求匹配不同成本和算力。如果只追求统一用最好的模型、最强的推理堆钱堆机器当然也能解决但SaaS和企业级Agent的运营压力会越来越大。从工程现状来看只要你的Agent服务日均请求量超过几万次CPU执行池的引入基本上可以做到立竿见影地降低成本。它不是用系统的稳定性换成本而是在保证核心链路质量的前提下把低价值请求从昂贵的算力上释放出去。我个人的实操体会是这三点规则先行、逐步演化、绝不盲切流量。先把基础路由逻辑做强再加负载感知再加反馈闭环每一次策略变化都走灰度每一次灰度都看数据。混合调度没有一步到位的方案只有不断迭代的决策器。如果你正准备在自建Agent系统里引入这套机制可以先从“把所有工具调用类请求切到CPU池配质量校验升级”这个小改动开始。这个改动风险低、数据效果显著又是完整链路的最小闭环。跑通之后你自然会看见下一步该调什么。
阅读完成 · 觉得有帮助?
咨询建站