做推理平台的同学十有八九遇到过这种场景集群显存明明整体还有富余可线上总是有几个实例被长请求占满另外几个实例闲得发慌同一个固定system prompt每天被刷了上万遍每次新请求进来还是白白把前缀重新算一遍首token延迟居高不下。根子往往不在单机推理框架而在流量分发环节——在大模型推理的分布式部署里请求该送进哪台机器、哪些请求能同时进入、哪些请求要等一等这些决策直接决定了整集群的吞吐和延迟表现。这篇文章是系列第5篇前几篇聊过单机推理优化、KV Cache管理、连续批处理和Prefill/Decode分离今天把视角拉到分布式重点讲大模型推理调度里的两个核心部件缓存感知路由Cache-Aware Routing和全局准入控制Global Admission Control。前者解决“把请求送到有缓存的机器”后者解决“在流量入口统一控制并发与优先级”。这两个点说起来简单但真要在生产环境落地涉及状态同步、故障恢复、负载均衡和优先级策略的取舍坑不少。1. 分布式推理调度先想清楚你到底在调度什么1.1 单机调度和分布式调度的本质差异单机推理场景下主流的推理框架已经把调度做得很深了。Continuous Batching、PagedAttention、Prefix Caching、动态优先级队列这些都是在一张卡或者一台机器上把多个请求的KV Cache和计算资源安排明白。单机调度解决的是一个局部最优问题——给定一块GPU的显存和算力如何让尽可能多的请求同时跑起来同时保证延迟可控。但到了分布式集群问题维度变了。请求不是直接落到GPU上而是先经过网关和路由层被分发到某台具体的worker实例。这时候调度决策就变成了一个带特定前缀和会话历史的请求应该进哪台机器。这个决策如果做得不好单机调度再优秀也没有用。一个最典型的反例就是所有请求无脑轮询分发每个worker都在跑重复的前缀计算KV Cache全局复用率约等于零集群规模越大浪费越明显。所以单机调度管的是“GPU内部的秩序”分布式调度管的是“整个集群资源的配置”。前者关心怎么排后者关心往哪送。系列前几篇讲的内容相当于把每一台车修好这一篇讲的是交通系统怎么让车流不堵、不跑冤枉路。1.2 分布式调度绕不开的三个核心矛盾做过分布式系统的人都知道任何调度器本质上都是在做资源分配大模型推理调度最棘手的地方在于调度对象的特殊性。第一个矛盾是状态局部性与全局负载均衡的冲突。KV Cache是worker本地维护的不是共享存储。想让相同前缀的请求命中缓存就得把流量尽量固定到同一台worker但这台worker的负载就会持续升高反过来为了负载均衡把流量分散到多台worker缓存命中率又必然下降。一句话缓存复用天然鼓励“扎堆”负载均衡天然要求“分散”这两者是拧着的。第二个矛盾是瞬时并发不可控。推理请求的执行时间极不稳定同一个模型短请求可能几十毫秒返回长请求可能计算几分钟。就算只放进去10个请求如果其中8个都是超长输入GPU照样被打满其他请求全部排队。在微服务架构里我们可以通过超时、重试、熔断来兜底但推理请求一旦开始执行你很难安全地把它杀掉——中途kill掉一个生成任务不仅浪费前面所有计算还可能让业务侧拿到半截结果。第三个矛盾是不可抢占带来的决策压力。普通任务调度里我们可以用抢占式调度低优先级任务被高优先级任务挤掉之后重新排队。但大模型推理请求从PreFill到Decode是一个连贯的生成过程主流推理框架对“暂停-恢复”的支持还非常有限。请求一旦占住一个执行槽基本就要跑完。这就要求准入控制必须想清楚再放行不能随便先放进来看情况再说。想明白了这三个矛盾再看缓存感知路由和全局准入控制就清楚它们各自在解什么题了缓存感知路由是在状态局部性与负载均衡之间找平衡全局准入控制是在不可抢占和高并发之间建立闸门。2. 缓存感知路由把请求送到“有缓存”的那台机器2.1 为什么KV Cache命中率这么重要先聊一个容易被低估的基础点。在线推理场景里请求的输入前缀通常有很强的重复性固定system prompt、few-shot示例、多轮对话的历史消息、RAG场景下相同的检索上下文这些都是实实在在的重复计算。KV Cache复用的原理不复杂——如果两个请求共享一段前缀token那么这段前缀的KV状态可以缓存起来后一个请求直接从缓存位置开始计算跳过整个前缀的PreFill。收益有多大取决于前缀长度。一个包含2000个system prompt token的请求如果完全先算完PreFill再生成前面这部分可能要几百毫秒甚至更久。而如果KV Cache命中这2000个token的计算代价几乎为零首token延迟直接从几百毫秒降到几十毫秒。在多轮对话场景里前缀可能是整个聊天历史长度随轮次线性增长这种情况下命中与否就是用户的直观体感差异——上一轮聊了5000字下一轮是秒回还是要等好几秒。所以在分布式集群里路由的终极目标是让“带有相同前缀的请求”尽可能被送到“已经缓存了该前缀的worker”。这是降低首token延迟最直接的手段也是单机推理优化解决不了的事——单机模型的KV Cache再好请求被分发到错误的机器上也是白搭。2.2 哈希路由的局限与改进方向缓存感知路由要解决的核心问题是“把相关联的请求关联到同一个worker”。最简单的实现方案是一致性哈希按用户ID、会话ID或前缀哈希做路由相同key的请求天然落在同一节点。这个方案有它的价值实现简单状态少也不需要全局同步。但它有两个明显缺陷。第一哈希路由无法感知节点负载。假设某个热点前缀哈希到了worker AA被长请求打满而此时worker B完全空闲哈希路由依然把所有相关请求都打到A眼睁睁看着B空转。第二哈希路由无法感知节点状态。如果worker A掉线或者正在滚动重启哈希环会重新映射很多请求会被临时派到没有对应缓存的worker命中率大幅波动。所以纯哈希路由只能作为基线方案它保证了“关联性”但牺牲了“感知能力”。缓存感知路由的改进方向是在哈希路由的基础上引入可观测的全局状态——哪个worker当前缓存了什么前缀、哪个worker当前负载多少——然后基于这些状态做动态决策。本质上是给静态路由装上了“眼睛”。2.3 一个可落地的缓存感知路由方案缓存感知路由最核心的问题不是算法复杂不复杂而是路由层怎么知道每个worker的缓存情况。很多人第一反应是“让路由层直接查每个worker的KV Cache列表”。这在单机节点上可行在分布式集群里完全不现实。KV Cache内容本身是向量张量路由层不可能持有这些数据而且缓存条目成千上万瞬间动态变化做完整同步会打爆网络和内存。实际工程里我见过比较靠谱的做法是“摘要上报打分路由”。每个worker上的Agent周期性地向上汇报一个轻量的缓存摘要不包含KV向量本身只包含该worker当前缓存的前缀信息。最简单的摘要是维护一个前缀哈希集合上报格式可以是“前缀哈希最近活跃时间”再进阶一点可以用前缀树或者布隆过滤器压缩成长度可控的二进制摘要。布隆过滤器有误判率但在路由场景里这是可接受的——误判的代价只是把请求送到一个没有缓存前缀的worker相当于一次普通cache miss不会造成正确性问题。路由层拿到这些摘要后为每个候选worker计算一个综合得分得分最低的worker胜出。打分因子至少包含三块负载因子、缓存命中因子、稳定性因子。下面是一个简化的伪代码示例可以直接照着写进网关逻辑里def select_worker(request, workers, cache_index): prefix_tokens request.prefix_tokens prefix_len len(prefix_tokens) best_worker None best_score float(inf) for w in workers: # 负载因子越小越好归一化到 0~1 load_score w.normalized_load # 缓存命中因子当前worker上最长公共前缀占整个前缀的比例 hit_len cache_index.max_prefix_match(w.id, prefix_tokens) cache_score 1.0 - (hit_len / prefix_len) # 稳定性因子如果最近一段时间该请求前缀一直被路由到当前worker降低切换概率 stable_score 0.0 last_worker cache_index.last_routed_worker(prefix_tokens) if last_worker w.id: stable_score STABLE_PENALTY # 一个正数越小越容易被选中 # 综合得分权重根据业务调整负载权重通常最高 score ( WEIGHT_LOAD * load_score WEIGHT_CACHE * cache_score WEIGHT_STABLE * stable_score ) if score best_score: best_score score best_worker w return best_worker这段代码的核心就是反复权衡负载、缓存收益和稳定性三个目标。注意这里默认得分越低越好所以缓存命中率高的worker会得到更低的得分更容易被选中负载高的worker得分会升高被躲开最近刚被选过的worker会加上一个小的惩罚项避免请求在多个节点间来回抖动。权重怎么定我个人的经验是负载权重通常设为最高因为负载倾斜导致的是所有请求延迟同步上升而缓存命中带来的收益是局部概率性的。其次才是缓存权重。稳定性权重的意义在于防止路由震荡通常给一个比较小的值就够了。举个例子如果令WEIGHT_LOAD1.2WEIGHT_CACHE0.7WEIGHT_STABLE0.3那么在缓存命中收益明显、而负载差距不太大的情况下请求会倾向于留在原worker但如果一个worker的负载已经明显偏高缓存收益就不足以抵消负载代价请求会被分流走。2.4 调优心得冷启动与抖动抑制这个方案看起来直接落地时最容易踩的坑是两个冷启动和路由震荡。冷启动问题通常出现在集群扩容时。新上线的worker刚拉起模型没有任何KV Cache按上面的打分公式它永远拿不到流量。没有流量的worker又永远建不起缓存形成了一个无法打破的循环。解法是给新worker一段“探索期”在探索期内强制分配固定比例的流量过去比如5%10%让它的缓存逐渐热起来。也可以对冷启动节点做一个“折扣因子”让它的负载得分暂时偏低吸引一部分流量。路由震荡是缓存感知方案的高频问题。如果两个worker的缓存命中分数差异很小请求可能在几次调度决策之间来回切换结果是在哪个worker都没建立起稳定的缓存整体命中率反而不如纯哈希路由。抑制办法有两个一是设置最低收益门槛比如缓存命中长度低于整个前缀长度的30%时完全忽略缓存因子只按负载路由二是加入粘滞时间同一个前缀一旦落在某个worker上在T秒内不允许主动切换除非该worker的负载超过硬阈值或者已经不可用。T的经验值一般在10秒到1分钟之间取决于请求到达速率和缓存构建速度。在实际项目里缓存感知路由上线后我见过的最直观效果是相同系统提示词的请求首token延迟P50从400毫秒降到80毫秒左右这中间几乎没有增加任何额外算力开销。这就是“路由决策”的价值——它不制造算力但能让已有算力的利用率直接翻倍。3. 全局准入控制在流量入口统一“红绿灯”3.1 没有全局准入会出什么问题如果说缓存感知路由解决的是“效率”问题那么全局准入控制解决的就是“稳定”问题。这一层如果缺失最典型的线上事故是这样发生的突发流量涌入网关把请求全部转发到各worker每个worker的本地队列越排越长。低优先级的离线批处理任务先占了大部分执行槽在线的高优请求只能跟在后面等。等到用户体验到超时重试重试流量又打进来队列进一步膨胀最终整个集群进入雪崩状态。单机推理框架里的队列管理解决不了这个问题因为每个worker都只能看到自己本地的那条队列。一个worker排队排到100另一个worker完全空闲在单机视角下无法感知也没有任何机制把流量从排队的worker导到空闲的worker。更麻烦的是推理请求不可抢占高优请求一旦排在低优请求后面就要眼睁睁等对方把几万个token生成完。全局准入控制是在整个集群的入口处加一道统一闸门。每个请求进来先问控制器现在能不能进、进了哪条队列、什么时候放行。这是把“先到先得”改成“全局统筹”的关键一步。3.2 全局准入控制的四个核心要素一个可用的全局准入控制器至少需要四部分全局并发计数、请求级队列、租约管理和状态反馈。全局并发计数是整个准入机制的基础它决定了任意时刻整个集群允许有多少个请求正在执行。计数的粒度要根据业务定可以按模型维度、按租户维度、按优先级维度也可以做组合计数。比如一个在线模型可以配置高优先级并发上限为100离线任务并发上限为150总并发上限为200。并发计数的实现不用搞复杂分布式锁直接用一个全局计数器Redis的INCR/DECR或者内存状态就能搞定关键是保证原子性。请求级队列负责处理“当前并发满了但请求先别拒绝”的情况。控制器为不同优先级维护不同队列高优队列的调度权重更高。队列不是无限长的队列长度达到上限后新请求会被拒绝并返回明确错误码让客户端走退避重试而不是把请求挂在控制面上无限等待。租约管理是分布式系统里一个很容易被忽略、但绝不能省的部分。worker处理完一个请求后会上报释放并发配额但如果worker崩溃或者网络分区上报永远不会到达。如果没有租约机制配额就会泄漏随着时间推移全局并发计数显示满了实际集群里根本没有请求在跑。租约机制的做法是worker每个心跳周期上报当前正在处理的请求列表控制面记录更新时间超过N个心跳周期没有更新控制面强制回收该worker占用的所有并发配额同时把该worker从路由池中摘除。状态反馈则是准入控制器与弹性伸缩联动的桥梁。控制器统计每个队列的排队长度、等待时间、各worker的负载情况这些数据不光是内部用的还会输出给扩缩容模块作为伸缩依据。3.3 配额、优先级和队列的核心实现全局准入的决策逻辑并不复杂核心是一个Admit接口。一个简化版本的判断逻辑如下def admit_request(req): model req.model level req.priority inflight global_counter.get_inflight(model, level) quota quota_config.get_quota(model, level) # 还有配额直接准入并占用 if inflight quota: global_counter.incr(model, level) return AdmissionResult(decisionallow) # 配额用完看队列是否还有空间 queue admission_queue.get_queue(model, level) if queue.size() queue.max_len: queue.enqueue(req) return AdmissionResult(decisionwaiting) # 队列满了拒绝 return AdmissionResult(decisionreject, reasonqueue_full)控制器同时维护一个后台循环负责从队列中按优先级放行请求。放行前会再次检查全局并发计数因为有大量请求可能在等待期间被取消或超时。这个循环可以设计成一个简单的定时任务100毫秒扫一次队列优先放行高优队列低优队列在等待超过一定时间后提升优先级避免饿死。一个关键细节是准入决策和路由决策是两件事但必须串在一起。请求通过准入后还不能马上发给worker因为准入只保证了“全局有配额”没保证“目标worker有容量”。实际流程是准入控制器通过配额判断放不放行放行后立即调用缓存感知路由选择目标worker如果目标worker本地并发也满需要进入等待队列。这里有两种做法一种是把全局配额和worker本地容量合并判断一次性决定另一种是两步走先全局后本地。我推荐第二种解耦清晰排查问题时也容易定位是哪一层卡住了。3.4 与弹性伸缩、降级策略的配合全局准入控制不能独立工作它必须和集群的扩缩容机制组成一个反馈闭环。最直接的联动信号是排队延迟当队列排队的预期等待时间超过阈值比如5秒控制器触发扩容把新的worker加入调度池并将一部分排队请求路由到新worker。注意扩缩容信号最好不要用GPU利用率因为GPU利用率高只能说明有计算任务在跑不一定说明容量不足真正反映容量不足的是请求进不来、排队时间变长这个直接现象。缩容场景更考验设计。全局准入控制器不能简单地“摘除”一个正在处理请求的worker因为推理请求不可抢占。标准的做法是先让worker进入drain状态——控制器不再向它派发新请求但允许存量请求继续执行。等它处理完所有在途请求后再正式下线。这个过程需要控制面持续跟踪worker的inflight数量直到确认归零。另外还有一个降级策略值得提当高优先级流量激增、全局配额不足时可以考虑把低优先级队列的配额临时借用给高优先级或者直接拒绝部分离线任务换取在线业务稳定。这个决策可以做得很简单控制器根据队列压力动态调整quota_config低谷期放宽离线配额高峰期收紧。我见过一些团队在这一层接入了更细的“模型级限额”例如某个模型只允许同时跑50个请求超出部分全部排队这个模型即使性能下降也不会拖垮其他模型——这种隔离粒度在共享集群里尤其重要。4. 整体架构与落地实践4.1 控制面与数据面分离缓存感知路由和全局准入控制从实现上看是两个独立模块但生产环境里它们共享一份全局状态最好放进同一个控制面体系里统一管理。我推荐的控制面/数据面分离架构如下数据面是网关Gateway它直接接收外部请求负责协议解析、鉴权、调用控制面接口拿到路由和准入决策。网关本身不保存全局状态状态全部在控制面。网关需要保持轻量因为它是流量的必经之路任何重逻辑堆在这里都会成为性能瓶颈。控制面包含两个核心服务调度决策服务Scheduler和缓存索引服务Cache Index。调度决策服务维护全局并发计数、队列、配额对外提供Admit和SelectWorker两个接口。缓存索引服务接收各worker上报的缓存摘要和负载信息构建一个轻量级的全局视图。这两个服务之间有紧密的数据依赖调度决策选worker时要查询缓存索引实际部署时通常放在同一个进程内减少一次RPC开销。每个推理worker上部署一个Agent进程角色的作用类似Kubernetes里的Node Agent负责三件事周期性上报负载状态活跃请求数、GPU利用率、内存占用、上报缓存摘要前缀哈希集合或布隆过滤器、处理控制面的“摘除”和“下线”指令。值得说明的是Agent与worker之间最好不要直接调用内部接口而是通过本地文件或Unix Socket交换信息避免重复绑定端口。4.2 一次请求的完整调度链路把组件串起来看一次请求从进入网关到返回结果完整的调度链路是这样的客户端请求到达Gateway后Gateway先调用准入控制接口带上模型ID、优先级、预估输入长度等元数据。准入控制器检查全局并发计数如果配额充足则扣减计数并返回“允许进入”如果配额不足则把请求放入对应优先级的等待队列同时返回一个排队预估时间给客户端。这个预估时间是有用的客户端可以据此决定等待还是自行降级。准入通过后Gateway带着请求前缀哈希和会话ID调用路由接口。调度决策服务拿到请求前缀去缓存索引服务里查“哪些worker缓存了这个前缀”再结合Agent上报的实时负载数据按照打分公式选出最优worker。选完worker后Gateway把请求转发给该worker的推理服务。Worker执行推理期间Agent持续更新该worker的inflight状态和缓存条目。推理结束后Agent通过心跳把请求完成事件上报给控制面控制面释放并发配额并更新缓存索引中的条目活跃时间。至此一个完整的调度闭环结束。整个过程从Gateway视角看只有两次控制面交互一次准入、一次路由增加的网络开销在毫秒级对整体延迟影响很小。4.3 配置参数与部署建议几个关键的参数配置我直接给一个可以抄作业的经验值但强调一点这些值只是起点必须根据线上流量实测调整。状态上报周期12秒。太频繁会让控制面的状态聚合压力大太疏会让调度决策的时效性变差。租约过期时间35倍心跳周期。心跳1秒时租约设为3秒比较合理。太短容易被网络抖动误杀太长会导致故障worker的配额回收慢。队列最大长度按“预期QPS × 可接受的排队时间”估算。例如QPS为200可接受排队时间为2秒单队列上限就是400。注意大模型推理场景QPS往往不高但单个请求耗时很长队列长度不宜设太大否则排队时间也不可控。权重初始值可以参考上面说的WEIGHT_LOAD1.2、WEIGHT_CACHE0.7、WEIGHT_STABLE0.3。如果集群请求普遍前缀很长可以适当提高缓存权重如果集群请求短且杂建议降低缓存权重。配额初始值给在线高优请求设一个“硬上限”可以按“worker数量 × 单worker可容忍并发数”计算。比如10个worker每个worker最多同时跑20个请求那么高优配额上限设为200比较稳妥。部署顺序上建议分三步走先上线全局准入控制确保集群稳定不被打爆再上线缓存感知路由优化延迟最后才做弹性伸缩联动。如果一开始就同时上所有功能出了问题你连是哪一层引起的都分不清楚。这也是我踩过的坑——调度系统改动一定要小步快跑每一层都验证充分后再叠加下一层。5. 常见问题与排查技巧实录5.1 路由震荡导致缓存命中率不升反降这是缓存感知路由上线后最常见的现象整体命中率非但没提升反而比纯哈希路由还低。排查路径通常是这样先看路由决策日志观察同一个前缀的请求在这段时间是不是在不断切换worker。如果是基本可以判定是“状态抖动”造成的。我在实践中用过一个有效的排查方法给每个请求打上“上一跳worker”标签统计同一前缀请求的worker迁移次数。正常情况下一个稳定热点前缀的worker切换率应该趋近于零如果切换率超过10%就要怀疑路由策略的稳定性权重太小或者缓存索引更新延迟太大。解决办法就是前面说的两个调高最低缓存收益门槛以及增加粘滞时间。另一个思路是降低缓存索引的更新频率让路由决策在更长时间窗口内保持稳定避免被瞬时状态干扰。5.2 worker崩溃导致全局并发配额泄漏配额泄漏的典型表现是线上流量已经降下来了但所有新请求依然被准入控制拒绝错误码全是“quota exhausted”。查下来发现全局并发计数一直不减而这些请求明明已经结束。根因基本是worker非正常退出。Worker进程被OOM Kill或者所在的Node节点宕机Agent没有机会上报“请求完成”事件。控制面不知道这些请求结束了自然一直扣着配额。解决思路只有一个租约机制必须可靠。我对租约的实现要求是worker心跳里必须包含当前处理的请求ID列表控制面每次收到心跳就重置租约一旦租约超时控制面不仅回收该worker的配额还要把该worker的可用状态标记为false防止路由层继续往它派流量。配额泄漏还有一个隐蔽来源worker正常处理完请求但Agent上报消息丢失或者控制面处理消息时异常导致没释放。这种情况靠租约只能兜底更好的做法是引入周期性的对账任务比如每分钟把所有worker上报的inflight请求列表汇总一遍和全局计数做差值核对超过阈值直接告警。5.3 缓存索引膨胀导致状态同步开销失控缓存索引是一个时间越长越大的东西尤其是长期运行的多轮对话业务前缀类型成千上万。如果把所有前缀哈希都上报到控制面Agent的CPU和网络开销都会随运行时间线性增长最终缓存索引本身变成新的瓶颈。我的做法是给缓存摘要设置一个“长度门槛”只有超过一定长度的前缀才值得上报。短前缀的缓存命中收益很低比如20个token以下的前缀计算一遍PreFill只需要几毫秒命中带来的收益可以忽略。设一个1000 token的上限之后需要上报的条目数量会大幅下降。更进一步可以按活跃时间做淘汰缓存条目如果超过一段时间没有被命中就没必要继续占用路由层的内存了可以把Agent上报的“缓存条目活跃内存表”理解成一个带TTL的LRU。另外为了控制上报包大小布隆过滤器是一个很实用的工具。它的问题是有误判前面提到过在路由场景下误判的影响可以接受。我见过有人对此非常纠结试图实现100%准确的全局缓存索引结果状态同步开销导致整个控制面性能崩掉。说白了分布式系统的核心哲学是“尽力而为的一致性”路由层拿到的是一个近似的、延迟的全局视图只要决策质量在阈值以上就够了。5.4 热门前缀在所有worker重复缓存缓存感知路由上线一段时间后会遇到另一个问题热门的system prompt或者高频对话前缀几乎在每个worker上都缓存了一份。表面上看这是好事命中率很高。但从集群全局角度看这其实是浪费——热门前缀明明只需要23份副本结果所有worker的显存都用来存同一份KV Cache导致其他个性化前缀没有空间缓存。我建议在缓存索引里给高热度前缀设计“副本数上限”的约束。具体做法是路由打分时不只看“哪个worker缓存了该前缀”还要统计“整个集群有多少worker缓存了同一前缀”。如果副本数已经达到上限比如3个那么后续请求只在这3个worker之间做负载均衡不再复制到新的worker。这个约束需要与缓存淘汰机制联动——当副本数超过上限时超出的worker可以优先淘汰该前缀的缓存腾出空间给其他前缀。判断一个前缀是否热门不能靠感觉需要在Agent侧维护访问频次统计。连续一段时间内被频繁访问的前缀才允许增加副本数。这个逻辑有点类似CDN的缓存副本管理——看到热点了就分散冷门内容保持局部性最终目标是在“避免全集群重复缓存”和“热点容量扩展”之间找到平衡。5.5 效果验证怎么做得更靠谱最后补一段关于验证的建议。调度策略的效果最好在灰度阶段就用数据说话而不是上线后靠业务反馈来判断。我常用的做法是选取两个配置完全相同的集群一个启用新的调度策略一个保持老策略跑同一份模拟流量。模拟流量不能拍脑袋最好从线上日志里抽取真实请求的模式包括前缀分布、请求间隔、输入长度分布。需要对比的核心指标至少包含这五个前缀缓存命中率按token比例计算、首token延迟P50和P99、单worker的GPU利用率标准差、端到端吞吐量、排队延迟。其中GPU利用率标准差是衡量集群负载均衡程度的关键指标标准差越小说明各worker负载越均匀。首token延迟P99则反映了最差情况下的用户体验。这组指标可以做成一个监控大屏在灰度过程中实时观察任何一个指标出现劣化趋势就该立刻回滚策略。我个人在实际项目中还有一个小习惯把路由决策的日志完整落盘并和请求的最终性能数据做关联分析。这样在后续排查时你能精确地知道“每个请求被谁决策送到了哪里、命中情况如何、延迟多少”。调度系统的可观测性永远比调度算法本身更值得投资。调度系统做得越久我越觉得真正考验工程能力的不是那几个公式和算法而是状态一致性和反馈闭环。缓存感知路由依赖准确的缓存状态上报全局准入控制依赖准确的并发状态同步。谁能在“状态同步代价”和“决策收益”之间找到平衡谁就能做出真正稳定可用的推理调度系统。最后再分享一个小技巧上线任何新调度策略之前务必先把可观测性指标埋好至少做到每个worker的缓存命中率、队列深度、inflight数量随时可查。否则策略一出问题你会被线上日志淹没根本不知道该看哪里。这些都是我踩过的坑写出来希望后来的人能少走一段弯路。