4. 调度窗口与等待时间

调度窗口这个概念,说白了就是系统愿意等多久。

你想想看,当一个请求进来的时候,它不会立刻被处理。系统会把它放进一个缓冲区,等一等,看看后面还有没有其他请求。这个「等一等」的时间窗口,就是调度窗口。

4.1 调度窗口是什么

我习惯把调度窗口理解成一个「集货时间」。就像快递站收件,不会来一个发一个,而是等个半小时,凑够一车再发。推理系统也一样,等一批请求凑齐了,一起送进GPU做批处理。

调度窗口的大小,直接决定了两个东西:

  • 批处理的大小——窗口越大,攒的请求越多,batch size越大
  • 请求的等待时间——窗口越大,每个请求等得越久

这里有个微妙的平衡。我在项目中遇到过,有人把调度窗口设成500毫秒,想着能攒个大batch提高吞吐。结果呢?延迟直接飙到600多毫秒,用户那边超时重试了一大堆。嗯,这就是典型的「捡了芝麻丢了西瓜」。

核心矛盾:调度窗口越大,吞吐越高,但延迟也越高。调度窗口越小,延迟越低,但吞吐上不去。

4.2 等待时间对延迟的影响

等待时间是怎么影响延迟的?我画个图你就明白了。

调度窗口与延迟的关系 时间 请求A 请求B 请求C 请求D 调度窗口=200ms 批处理执行 等80ms 等0ms 等-110ms 请求A等了80ms才开始处理,请求C到达时窗口已关闭,进入下一批

你看这个图。请求A到达后,系统开始计时。200毫秒的窗口期内,请求B也到了。窗口关闭时,A和B一起被处理。请求A实际等了80毫秒,请求B几乎没等。

但请求C呢?它来晚了,窗口已经关了。它只能等下一轮。这一等,可能就是200毫秒加上处理时间。我见过最夸张的情况,一个请求等了整整3个窗口周期才被处理——因为每次窗口关闭前都有新请求插进来,它一直被往后挤。

注意:等待时间不是均匀分布的。先到的请求等得短,后到的可能等得长。如果请求到达时间刚好卡在窗口关闭前,它几乎不用等。如果卡在窗口刚关闭后,它就要等整整一个窗口周期。

4.3 如何设置合理的调度窗口

这个问题没有标准答案。我做了这么多年,只能说「看场景」。不过有几个经验法则可以分享。

4.3.1 根据延迟预算反推

先问自己一个问题:你的服务允许的最大延迟是多少?

假设你的SLA要求P99延迟在200毫秒以内。模型推理本身需要100毫秒。那留给调度的窗口最多就是100毫秒。你不可能设150毫秒,因为那样即使不排队,光等待时间就超标了。

我一般会留20%的余量。比如推理需要100毫秒,总预算200毫秒,那调度窗口我设60-80毫秒。剩下的20-40毫秒给网络传输和排队抖动。

延迟预算 推理时间 建议调度窗口 余量
100ms 50ms 30-40ms 10-20ms
200ms 80ms 80-100ms 20-40ms
500ms 150ms 250-300ms 50-100ms
1000ms 200ms 600-700ms 100-200ms

4.3.2 根据请求到达率调整

请求到达率是动态的。高峰期和低谷期,调度窗口应该不一样。

我曾经做过一个在线服务,白天QPS 5000,晚上QPS 500。如果固定用50毫秒的窗口,白天能攒到250个请求一批,效率很高。但晚上呢?50毫秒可能只等到1-2个请求,批处理几乎没效果。

我的做法是动态调整:

# 伪代码:动态调度窗口
def calculate_window(current_qps, target_batch_size):
    # 目标:每个窗口能攒够 target_batch_size 个请求
    if current_qps == 0:
        return max_window  # 没请求时用最大窗口
    
    # 期望的窗口大小
    expected_window = (target_batch_size / current_qps) * 1000  # 单位毫秒
    
    # 限制在合理范围内
    return min(max_window, max(min_window, expected_window))

这个逻辑很简单:QPS高的时候,窗口自动缩小;QPS低的时候,窗口自动放大。但要注意设一个上限和下限,防止窗口过大或过小。

小技巧:我习惯把最小窗口设为10毫秒,最大窗口设为500毫秒。10毫秒以下,系统调度的开销占比太大,不划算。500毫秒以上,用户体验明显变差。

4.3.3 根据模型特性调整

不同模型的推理时间不一样。小模型可能只要10毫秒,大模型可能要500毫秒。

对于小模型,调度窗口可以设小一点。因为推理快,即使batch小一点,吞吐也够用。对于大模型,调度窗口可以设大一点。因为推理慢,需要攒更多请求来充分利用GPU。

我遇到过最头疼的情况是混合部署——同一个GPU上跑多个模型。每个模型的推理时间不一样,调度窗口怎么设?我的方案是:按最慢的模型来设窗口,然后对快的模型做「虚拟等待」。说白了就是快的模型等一等,跟慢的模型一起走。

4.3.4 实际调优步骤

纸上谈兵没用,最终还是要上线调。我一般按这个步骤来:

  1. 先设一个保守值——比如50毫秒。这个值通常不会出大问题。
  2. 观察P99延迟——如果P99延迟远低于SLA,说明窗口可以放大。
  3. 逐步放大窗口——每次增加20毫秒,观察延迟和吞吐的变化。
  4. 找到拐点——当延迟开始接近SLA上限时,停止放大。
  5. 留出安全余量——最终值比拐点小10-20%,应对突发流量。

记住:调度窗口不是越大越好,也不是越小越好。它是一个权衡工具。你的目标不是「最大化吞吐」,也不是「最小化延迟」,而是「在满足延迟SLA的前提下,最大化吞吐」。

嗯,说到这我想起一个坑。有一次我把调度窗口设得很大,想着能提高吞吐。结果发现吞吐反而下降了。为什么?因为窗口太大,请求在缓冲区里等太久,客户端那边超时重试了。重试的请求又进来排队,导致缓冲区更满,延迟更高,更多请求超时……这就是典型的「拥塞崩溃」。后来我加了重试去重和请求超时丢弃的逻辑,才把这个问题解决。

所以,调度窗口的设定,一定要结合客户端的超时时间来考虑。窗口不能超过客户端超时时间的一半,否则重试风暴会让你欲哭无泪。