长尾延迟没有对冲机制,TIMEOUT_S 一个旋钮调不出好取值 #24

Closed
opened 2026-09-07 16:45:39 +08:00 by iomgaa · 2 comments
Owner

现象

某些中转渠道会把个别请求挂住很久然后正常返回 200——不报错、不限流、不返回 429/503。

最小复现(自建中转 + MiniMax-M3,非流式,并发 1、串行、关掉重试,提示词是「用一句话说明
什么是并发。不超过二十个字。」)。连发 20 次,每次的耗时:

10.1  57.6  197.8  6.4  15.0  21.7  187.1  15.1  10.1  49.8
 9.6  26.6  110.9  262.9  12.3   9.2   1.8  12.3  25.5  93.3    (秒)

20 次里 5 次超过 60 秒,最长 262.9 秒,全部正常返回 200。 二十个字的提示词、单个请求、
没有并发。中位数 15.1 秒。

真实负载(同一个源,一个 seed 1073 次调用):

  • 13% 的调用超过 60 秒,这 13% 吃掉了 71% 的模型总时间(另一个 seed 是 13.5% / 71.2%,
    两次几乎一样)
  • 这些慢调用的输出中位数只有 186 token,而输出 ≥200 token 的调用出字速度中位是 116 tok/s
    ——它们不是在生成,是在等
  • 延迟直方图是双峰的:主峰 0–10 秒(75%),另一个尖峰在 90–96 秒这个 6 秒宽的窗口里挤了
    75 次。一个这么窄的峰更像某个固定时长的机制,不像负载造成的排队

我们没能把它归因到自己这一侧。 按「本次调用开始时在飞的其它调用数」分桶之后,在飞 0 条
的桶里照样有这个 90 多秒的峰——也就是说一个请求独自在飞时也会挂。但我们也不敢反过来说它与
并发无关:同一批数据里,每次调用平均被扣掉的等待时间在在飞 0 条时是最低的几档之一,方向指向
「并发确实会加剧」。样本不足以下结论,两个方向我们都不排除。

为什么现有旋钮不够

现在能拧的只有 TIMEOUT_S,而它是个两难:

  • 调低(比如 40 秒)能切断挂起,但会误杀正常的长生成——推理模型吐 2000+ token 本来就要
    几十秒
  • 调高(我们现在是 300 秒)等于放任挂起,而且挂满超时之后还要重试一遍,总代价更大

单一的总时长阈值分不开「挂住了」和「正在认真生成」。

建议:对冲请求(hedged request)

发出请求后,若超过某阈值仍未收到首 token,就并发再发一个,谁先回用谁,另一个取消。

判据用 TTFT 而不是总时长,这样不会误杀慢生成——库已经在收 ttft_ms
TransportResult.ttft_msmiddleware/retry.py:386 搬进 LLMResponse)。挂起的特征正是
首 token 迟迟不来,而慢生成的首 token 是正常的。

配置形如:

<SCOPE>__HEDGE__AFTER_S=8            # 绝对阈值
<SCOPE>__HEDGE__AFTER_PERCENTILE=95  # 或者按该源的滚动分位数
<SCOPE>__HEDGE__MAX_EXTRA=1          # 最多额外发几个

默认关闭。开启后成本会小幅上升(被对冲掉的那次请求可能已经产生了计费),所以要在文档里
写明这个代价,并且让计费口径能把「对冲掉的那次」区分出来。

优先级

这条比另一个 issue「LLMResponse 答不出这次调用总共等了多久、试了几次」低:那一条是观测
缺失,会让下游的数字不可信;这一条是性能增强,下游可以先靠降 TIMEOUT_S 缓解。两条一起提
是因为它们指向同一个现象。

环境

polygateway 1.3.3,Python 3.12,OpenAI 兼容 transport,非流式。

## 现象 某些中转渠道会把个别请求挂住很久然后**正常返回 200**——不报错、不限流、不返回 429/503。 **最小复现**(自建中转 + MiniMax-M3,非流式,并发 1、串行、关掉重试,提示词是「用一句话说明 什么是并发。不超过二十个字。」)。连发 20 次,每次的耗时: ``` 10.1 57.6 197.8 6.4 15.0 21.7 187.1 15.1 10.1 49.8 9.6 26.6 110.9 262.9 12.3 9.2 1.8 12.3 25.5 93.3 (秒) ``` **20 次里 5 次超过 60 秒,最长 262.9 秒,全部正常返回 200。** 二十个字的提示词、单个请求、 没有并发。中位数 15.1 秒。 真实负载(同一个源,一个 seed 1073 次调用): - 13% 的调用超过 60 秒,这 13% 吃掉了 **71% 的模型总时间**(另一个 seed 是 13.5% / 71.2%, 两次几乎一样) - 这些慢调用的输出中位数只有 186 token,而输出 ≥200 token 的调用出字速度中位是 116 tok/s ——它们不是在生成,是在等 - 延迟直方图是双峰的:主峰 0–10 秒(75%),另一个尖峰在 90–96 秒这个 6 秒宽的窗口里挤了 75 次。一个这么窄的峰更像某个固定时长的机制,不像负载造成的排队 **我们没能把它归因到自己这一侧。** 按「本次调用开始时在飞的其它调用数」分桶之后,在飞 0 条 的桶里照样有这个 90 多秒的峰——也就是说一个请求独自在飞时也会挂。但我们也不敢反过来说它与 并发无关:同一批数据里,每次调用平均被扣掉的等待时间在在飞 0 条时是最低的几档之一,方向指向 「并发确实会加剧」。样本不足以下结论,两个方向我们都不排除。 ## 为什么现有旋钮不够 现在能拧的只有 `TIMEOUT_S`,而它是个两难: - 调低(比如 40 秒)能切断挂起,但会误杀正常的长生成——推理模型吐 2000+ token 本来就要 几十秒 - 调高(我们现在是 300 秒)等于放任挂起,而且挂满超时之后还要重试一遍,总代价更大 单一的总时长阈值分不开「挂住了」和「正在认真生成」。 ## 建议:对冲请求(hedged request) 发出请求后,若超过某阈值仍未收到首 token,就并发再发一个,谁先回用谁,另一个取消。 判据用 **TTFT 而不是总时长**,这样不会误杀慢生成——库已经在收 `ttft_ms` (`TransportResult.ttft_ms`,`middleware/retry.py:386` 搬进 `LLMResponse`)。挂起的特征正是 首 token 迟迟不来,而慢生成的首 token 是正常的。 配置形如: ``` <SCOPE>__HEDGE__AFTER_S=8 # 绝对阈值 <SCOPE>__HEDGE__AFTER_PERCENTILE=95 # 或者按该源的滚动分位数 <SCOPE>__HEDGE__MAX_EXTRA=1 # 最多额外发几个 ``` 默认关闭。开启后成本会小幅上升(被对冲掉的那次请求可能已经产生了计费),所以要在文档里 写明这个代价,并且让计费口径能把「对冲掉的那次」区分出来。 ## 优先级 这条比另一个 issue「`LLMResponse` 答不出这次调用总共等了多久、试了几次」低:那一条是观测 缺失,会让下游的数字不可信;这一条是性能增强,下游可以先靠降 `TIMEOUT_S` 缓解。两条一起提 是因为它们指向同一个现象。 ## 环境 polygateway 1.3.3,Python 3.12,OpenAI 兼容 transport,非流式。
Author
Owner

还需要注意前端需要返回真实的生成时间,所以可能涉及到添加多一个额外的参数,这个参数是排除了重试以及等待时间的裸生成时间,并且应该取对冲请求中快的那个。这样子就可以获取真实的请求时间,而不包含因为llm调用波动所带来的额外时间。然后对冲请求的相关参数中对于前端有用的部分也需要返回,如何算有用你来判断。

还需要注意前端需要返回真实的生成时间,所以可能涉及到添加多一个额外的参数,这个参数是排除了重试以及等待时间的裸生成时间,并且应该取对冲请求中快的那个。这样子就可以获取真实的请求时间,而不包含因为llm调用波动所带来的额外时间。然后对冲请求的相关参数中对于前端有用的部分也需要返回,如何算有用你来判断。
Author
Owner

已在 1.3.7 解决(opt-in,默认关闭):

  • {SCOPE}__HEDGE__AFTER_S 开启 chat 单路并发对冲:阈值到点向另一个等价源并发第二请求,谁先回用谁,输家取消收口。触发判据分流式(首 token 未至,误杀不了慢生成)与非流式(纯总时长阈值,物理上挂起与慢生成不可分,只能靠阈值取值)。
  • 对冲走完整限流/熔断准入,拿不到配额静默放弃(不击穿网关);输家取消按 est 保留预扣但不喂熔断(挂起不是源死亡证据);两败只计一次重试预算。
  • CallStats 新增三字段:generation_ms(裸生成时间=赢家那次 transport 墙钟,排除准入排队/退避/触发前等待/清理,与 total_latency_ms 的差值即波动开销)、hedgeshedge_won
  • 诚实代价:对冲窗口 in-flight 翻倍;输家可能已被上游计费(est 保留只是闸内记账,止不住上游计费);非流式阈值是取值问题;HEDGE__MAX_EXTRA v1 仅单路(>1 仅 warning,梯次多路预留)。

发布: https://gitea.iomgaa.online/iomgaa/PolyGateway/releases/tag/v1.3.7

已在 1.3.7 解决(opt-in,默认关闭): - `{SCOPE}__HEDGE__AFTER_S` 开启 chat 单路并发对冲:阈值到点向**另一个等价源**并发第二请求,谁先回用谁,输家取消收口。触发判据分流式(首 token 未至,误杀不了慢生成)与非流式(纯总时长阈值,物理上挂起与慢生成不可分,只能靠阈值取值)。 - 对冲走**完整限流/熔断准入**,拿不到配额静默放弃(不击穿网关);输家取消按 est 保留预扣但**不喂熔断**(挂起不是源死亡证据);两败只计一次重试预算。 - `CallStats` 新增三字段:`generation_ms`(**裸生成时间**=赢家那次 transport 墙钟,排除准入排队/退避/触发前等待/清理,与 `total_latency_ms` 的差值即波动开销)、`hedges`、`hedge_won`。 - **诚实代价**:对冲窗口 in-flight 翻倍;输家可能已被上游计费(est 保留只是闸内记账,止不住上游计费);非流式阈值是取值问题;`HEDGE__MAX_EXTRA` v1 仅单路(>1 仅 warning,梯次多路预留)。 发布: https://gitea.iomgaa.online/iomgaa/PolyGateway/releases/tag/v1.3.7
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: iomgaa/PolyGateway#24