docs: finalize streaming stall design with independent review errata
B1 chunk = any delta (content or reasoning), B2 telemetry plumbing via explicit _emit pass, B3 orchestration entry gate for independent stall hedging; N1-N9 advisory fixes merged. Human decisions recorded in design section 9.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 流式中段沉默:计量口径与对冲(已批准设计)
|
||||
|
||||
- 状态: **已批准(2026-09-11 人类拍板 §9 五项,见 §9 决策记录)**;本文件只做设计,不含实现
|
||||
- 状态: **已批准 + 独立审查补遗已并入(2026-09-11,`reviews/2026-09-11-streaming-stall-design-review.md`,B1-B3/N1-N9 全处置,见 §11)**;本文件只做设计,不含实现
|
||||
- 基线: main `a28d944` / 1.3.7(已发布,含单路并发对冲与 `CallStats.generation_ms/hedges/hedge_won`)
|
||||
- 输入: `findings/2026-09-11-streaming-stall-hedging-research.md`(业界范式与来源,下称"调研",引用处标注 §x.y);下游 Terminal-Bench 实测 4407 次成功调用遥测(需求来源);1.3.7 设计 `designs/2026-09-10-24-hedged-requests-design.md`;远端 issue #27(P1)/#28(P2)
|
||||
- 关联: ARCH §6.4 取消语义、§7.2 重试;既有流式看门狗 `streaming.py`;1.3.7 对冲机制(本设计直接站在其编排与记账语义上)
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
### P1:中段沉默计入 `generation_ms`,"裸生成时间"名不副实(issue #27)
|
||||
|
||||
`CallStats.generation_ms` 的口径是赢家 transport 调用的完整墙钟:`middleware/retry.py:~335` `gen_started` 紧贴 `transport.complete()` 调用前,`:~346` sink 在返回后立即记录。对流式,`complete()` 返回 = 整个流读完,故中段沉默全部计入——这是 1.3.7 设计明写的口径,但"裸生成时间"这个名字误导了下游把它当活跃生成时间用。
|
||||
`CallStats.generation_ms` 的口径是赢家 transport 调用的完整墙钟:`middleware/retry.py:332` `gen_started` 紧贴 `transport.complete()` 调用前,`:346` sink 在返回后立即记录。对流式,`complete()` 返回 = 整个流读完,故中段沉默全部计入——这是 1.3.7 设计明写的口径,但"裸生成时间"这个名字误导了下游把它当活跃生成时间用。
|
||||
|
||||
下游实测(Terminal-Bench,4407 次成功调用):
|
||||
|
||||
@@ -43,23 +43,23 @@
|
||||
| 沉默扣除唯一先例 pi-tps:gap ≥500ms 判 stall,**减所有超阈值段**,连续停顿合并一次,样本不足返回 null | 调研 §B3 | P1 的 `stall_ms/stall_count` 口径直接照此;"只减最大一段"无任何先例,否决 |
|
||||
| Gregg off-CPU:墙钟分解须**成对报告 + 归因**,只报净数无法行动 | 调研 §B2 | 1B' 下由 `generation_ms`(净)+ `stall_ms`/`stall_count` 成对报告满足,墙钟 = `generation_ms + stall_ms` 可还原 |
|
||||
| NVIDIA TTFT/ITL/E2EL:排队计入 TTFT,基准口径**不扣除**沉默 | 调研 §B1 | 扣沉默是库自造口径,必须显式声明、不与基准口径混写 |
|
||||
| vLLM `generation_time_ms` 排除条款:token 数 <2 时间隔类字段 null;`n>1` 无法归因即抑制 | 调研 §B3 | 三新字段的 None 条款照抄:内容 chunk <2 / 非流式 → None |
|
||||
| vLLM `generation_time_ms` 排除条款:token 数 <2 时间隔类字段 null;`n>1` 无法归因即抑制 | 调研 §B3 | 三新字段的 None 条款照抄:增量 chunk <2 / 非流式 → None |
|
||||
| Envoy/gRPC:判赢在**响应头**即 commit 不回头;per_try_timeout 只在响应开始前有效,中段停顿不重新对冲 | 调研 §A2 | P2 赢家裁定与"不回头"语义的参照;约束未来流式透传(§5.2) |
|
||||
|
||||
另:配置命名先例对照见调研 §3.3,本设计键名与其对齐。
|
||||
|
||||
## 3. 共享观测原语:内容 chunk 单调时间戳序列
|
||||
## 3. 共享观测原语:增量 chunk 单调时间戳序列
|
||||
|
||||
P1(计量)与 P2(中段触发)共用同一份底层观测,避免两套计时逻辑漂移:
|
||||
|
||||
| 项 | 设计 |
|
||||
| --- | --- |
|
||||
| 记录点 | transport 层(`openai_compat.py:605` 现算 `max_gap` 处已逐 chunk 循环,时间戳序列在此追加记录,增量成本近零) |
|
||||
| 记录内容 | 每个**内容 chunk** 的单调钟时间戳;空 chunk / SSE keepalive 注释**不计时**(LangChain 语义,调研 §A3——keepalive 字节会重置 httpx read timeout,对"有字节无内容"的卡顿失效,我们的序列天然免疫) |
|
||||
| 记录点 | transport 层(`openai_compat.py:580-590` 现算 `max_gap` 的循环处已逐 chunk 迭代,时间戳序列在此追加记录,增量成本近零;:605 是 `max_inter_token_ms` 装配处) |
|
||||
| 记录内容 | **chunk = 任何增量产出(`content` 或 `reasoning_content` 都算),与看门狗/首 token 活性口径一致**(`openai_compat.py:585-588` 注释"思考增量同样是存活证据"同款);**仅空 chunk / usage 帧 / SSE keepalive 不计时**(keepalive 字节会重置 httpx read timeout,对"有字节无内容"的卡顿失效,本序列天然免疫,LangChain 语义见调研 §A3) |
|
||||
| 派生原则 | 一切计量(`stall_ms` 等)与中段触发信号(§5.2)都从此序列/其增量事件派生,**不在热路径做判定**(调研 §3.2-1);热路径只做"记一个时间戳 + set 一个 Event" |
|
||||
| chunk ≠ token | chunk 边界是传输/解析缓冲产物,非 token 边界(vLLM #27485 陷阱,调研 §B1);一切间隔类口径按 chunk 声明,不假装是 ITL |
|
||||
|
||||
P2 触发所需的活体信号由此派生:transport 每记一个内容 chunk 即刷新一个 `asyncio.Event`(语义 = "自上一内容 chunk 以来未再沉默"),编排侧以"等该事件超时"实现 stall 计时器——与 1.3.7 首 token 事件同构,零新机制类别。
|
||||
P2 触发所需的活体信号由此派生:transport 每记一个增量 chunk 即刷新一个 `asyncio.Event`(语义 = "自上一增量 chunk 以来未再沉默"),编排侧以"等该事件超时"实现 stall 计时器——与 1.3.7 首 token 事件同构,零新机制类别。
|
||||
|
||||
## 4. P1 方案:沉默计量口径(1B' 已批准)
|
||||
|
||||
@@ -67,9 +67,9 @@ P2 触发所需的活体信号由此派生:transport 每记一个内容 chunk
|
||||
|
||||
| 项 | 内容 |
|
||||
| --- | --- |
|
||||
| 语义 | 下游用既有 `LLMResponse.max_inter_token_ms`(`types.py:417-418`)自算 `generation_ms − max_inter_token_ms`,即**只减最大一段**;库仅澄清文档 |
|
||||
| 语义 | 下游用既有 `LLMResponse.max_inter_token_ms`(`types.py:406`)自算 `generation_ms − max_inter_token_ms`,即**只减最大一段**;库仅澄清文档 |
|
||||
| 改动面 | README/CHANGELOG 口径澄清,零代码 |
|
||||
| 优点 | 零实现、零契约变更;`max_inter_token_ms` 已在遥测表(`telemetry/schema.py:43/85`),存量数据即可分析(本次实测正是用它定位的病) |
|
||||
| 优点 | 零实现、零契约变更;`max_inter_token_ms` 已在遥测表(`telemetry/schema.py` COLUMNS),存量数据即可分析(本次实测正是用它定位的病) |
|
||||
| 缺点 | 多段沉默场景**系统性高估**活跃时间(调研 §B3:只减最大一段无任何先例);口径责任散落每个下游,各家算法漂移后跨项目不可比;无 `stall_count`,违反 Gregg 归因原则 |
|
||||
|
||||
**否决理由(人类裁决,2026-09-11)**:口径必须库内收敛,不得散落各下游自算。
|
||||
@@ -80,12 +80,13 @@ P2 触发所需的活体信号由此派生:transport 每记一个内容 chunk
|
||||
|
||||
| 项 | 内容 |
|
||||
| --- | --- |
|
||||
| 语义 | `generation_ms` 重定义为**净生成时间** = 赢家 transport 墙钟 − Σ(所有 > 阈值的相邻**内容 chunk** 沉默,pi-tps 式口径);`CallStats` 新增两归因字段(默认 None):`stall_ms: int \| None`(沉默总额)、`stall_count: int \| None`(超阈值段数,连续停顿合并一次);**不新增 `active_generation_ms`**——`generation_ms` 本身就是净口径 |
|
||||
| 语义 | `generation_ms` 重定义为**净生成时间** = 赢家 transport 墙钟 − Σ(所有 > 阈值的相邻**增量 chunk** 沉默,pi-tps 式口径);`CallStats` 新增两归因字段(默认 None):`stall_ms: int \| None`(沉默总额)、`stall_count: int \| None`(= **超阈值相邻间隔个数**;pi-tps"连续停顿合并一次"子句不移植——gap 口径下任意两超阈 gap 间至少隔一个 chunk,无可合并对象,该子句是 pi-tps 事件流语义、在本口径下无操作定义,§11-N6);**不新增 `active_generation_ms`**——`generation_ms` 本身就是净口径 |
|
||||
| 边界与换算(§11-N1/N4/N8 钉死) | 净值 = 墙钟 − 中段沉默,**仍含 TTFT 与尾部**(末 chunk→流结束);尾部归活跃侧是有意选择(墙钟基底口径,尾部通常≈0),与 pi-tps"止于最后更新"的差异显式声明,README 口径表同步。钟同源纪律:沉默序列(transport)与墙钟(retry 注入 `now`)生产同为 `time.monotonic`。换算只此一处:TransportResult `stall_ms: float \| None`;retry 层 `wall_ms=int((now−gen_started)*1000)` 照旧,`net = max(0, wall_ms − int(stall_ms or 0))`(clamp 防御);`CallStats.stall_ms = int(stall_ms)`(None 透传)——各实现不得另取整。FakeClock 测试用自控 stall 值的假 transport,真实 transport 测试不注入假钟 |
|
||||
| 归因与还原 | 墙钟 = `generation_ms + stall_ms`;净数与沉默总额/次数成对报告,Gregg 成对报告原则(调研 §B2)在净口径下依然满足 |
|
||||
| 排除条款(照 vLLM,与原案一致) | 非流式 / 内容 chunk <2 → `stall_ms`/`stall_count` 为 None 且 `generation_ms` **不退减**(= 原墙钟);对冲输家的沉默**不进**赢家快照(输家数据留在其 attempt 遥测行,可 join 还原) |
|
||||
| 排除条款(照 vLLM,与原案一致) | 非流式 / 增量 chunk <2 → `stall_ms`/`stall_count` 为 None 且 `generation_ms` **不退减**(= 原墙钟);增量口径与看门狗一致(`content`/`reasoning_content` 均计)——reasoning 模型长 thinking 阶段只流 reasoning_content,若只计 content 会把正常推理误计为沉默(P1 虚高、P2 误触发);非流式场景 net≈wall(无中段沉默概念),下游预算公式以 `stall_ms is None` 为分支条件;对冲输家的沉默**不进**赢家快照——已完成但竞速落选的输家其沉默在其 attempt 遥测行(可 join 还原);被取消的输家无 TransportResult、行是 `hedge_cancelled` error 行、stall 恒 None,其存在性由该行表达 |
|
||||
| 阈值 | 新配置 `{SCOPE}__STREAM__STALL_THRESHOLD_MS`,**默认 1000ms**(§9-③ 拍板);人类附加要求"需要是一个可以调整的参数,对外提供调整接口"——调整接口 = 既有装配路径(env 键 / `GatewaySettings` 字段 / `GatewayClient` 构造 kwarg 三层均可调),**不提供 per-call 覆盖**(§8 非目标 F);**不做自适应**(防与对冲阈值耦出反馈环,调研 §3.2-3) |
|
||||
| 遥测 | 遥测表新增 `stall_ms`/`stall_count` 两列(§9-⑤ 拍板):`telemetry/schema.py` PG/SQLite 各一次可空 DDL + recorder/`middleware/telemetry.py` 透传;`generation_ms` **仍不进遥测列**(YAGNI——下游要墙钟用 `latency_ms` 或 `generation_ms + stall_ms` 还原) |
|
||||
| 改动面 | `types.py:417-418` 邻域(`CallStats` 增两字段);`ports.py` `TransportResult` 增 `stall_ms`/`stall_count: float \| None` 默认 None;`openai_compat.py:605` 邻域从 chunk 时间戳序列计算两值(沿用 §3 原语);`middleware/retry.py:~335/~346` 邻域(赢家记账改为 `record_generation`(净值) + context 存 stall 两值进快照,口径与 H8 同款);`telemetry/schema.py:43/85` 邻域(PG/SQLite 各一次可空 DDL)+ recorder + `middleware/telemetry.py` 透传;`config.py:46` 邻域(新键 + loader);`client.py` 构造透传 |
|
||||
| 改动面 | `types.py:296-324`(`CallStats` 增两字段);`ports.py` `TransportResult` 增 `stall_ms`/`stall_count: float \| None` 默认 None;`openai_compat.py:580-590`(max_gap 循环邻域;:605 是装配处)从 chunk 时间戳序列计算两值(沿用 §3 原语);`middleware/retry.py:332/346` 邻域(赢家记账改为 `record_generation`(净值) + context 存 stall 两值进快照,口径与 H8 同款);遥测透传链路(PM 裁决选 (b):LLMResponse 不增顶层字段——CallStats 已承载赢家口径,避免同数据两处平铺漂移):`middleware/telemetry.py` `_emit` 显式从 TransportResult 传 stall 两值 + emitter `_AttemptUsage` 增两字段 + `ports.py` `TelemetryRecorder.record_llm_call` 签名加两参(**第三次端口签名变更**,§7 端口行补录;emitter 装配闸 `signature.bind` 以该协议签名为判据,漏改协议即装配期炸)+ 两 recorder 实现(PG/SQLite)透传;`telemetry/schema.py` 编辑点 = DDL 末尾(created_at 后) + `SQLITE_BACKFILL`/`_PG_BACKFILL_DECLS` 保序对齐 + `COLUMNS` + 列数断言测试 + `telemetry_schema_sql` 同源再生成;配置走 `_load_hedge`(`config.py:745-769`)同款 loader + `GatewaySettings` 字段;`client.py` 构造透传 |
|
||||
|
||||
#### 契约变更的正当性与披露
|
||||
|
||||
@@ -137,13 +138,13 @@ P2 触发所需的活体信号由此派生:transport 每记一个内容 chunk
|
||||
|
||||
| 项 | 内容 |
|
||||
| --- | --- |
|
||||
| 触发 | 新配置 `{SCOPE}__HEDGE__STALL_AFTER_S`(**独立于 `HEDGE__AFTER_S` 生效**,§9-④ 拍板"否,独立生效";调研 §3.3 命名);语义:主路**相邻内容 chunk 间隔**超此阈值 → 触发。机制 = §3 原语派生的"内容 chunk Event 等待超时",与首 token 事件同构;`_past_hedge_window`(`retry.py:542-562`)扩为"首 token 未至走 AFTER_S,首 token 已至走 STALL_AFTER_S" |
|
||||
| 触发 | 新配置 `{SCOPE}__HEDGE__STALL_AFTER_S`(**独立于 `HEDGE__AFTER_S` 生效**,§9-④ 拍板"否,独立生效";调研 §3.3 命名);语义:主路**相邻增量 chunk 间隔**超此阈值 → 触发。机制 = §3 原语派生的"增量 chunk Event 等待超时",与首 token 事件同构;`_past_hedge_window`(`retry.py:542-562`)扩为"首 token 未至走 AFTER_S,首 token 已至走 STALL_AFTER_S"。并发纪律(Event + 复检方案,§11-N3):①超时到期后触发对冲前必须复检 `is_set()`(chunk 与超时同一事件循环 pass 到达时防误触发);②check→clear 之间不得有 await;③对冲路 chunk Event 恒 None(1.3.7 首 token 约定延伸声明);④stall 等待每轮迭代自建 waiter,逐字套用 `_past_hedge_window` 的 waiter 收口 + `cancelling()` 穿透纪律(retry.py:556-568);⑤等待者唯一(编排),Event 无需多等待者语义 |
|
||||
| 对冲执行 | 触发后主路**不取消**;并发异源副路走**完整准入 + 熔断**,无配额静默放弃(1.3.7 设计 §4.3 语义原样复用);`FIRST_COMPLETED` 赛跑;输家取消,est 保留不喂熔断、遥测 `hedge_cancelled`(1.3.7 设计 §4.4/§4.5 全部复用,零新记账语义) |
|
||||
| 赢家计量 | 赢家路为准:`generation_ms`(1B' 净口径)/`stall_ms`/`stall_count`(1B' 同批落地,§9-①)全部取赢家路;`hedge_won` 语义不变(赢家是否对冲路) |
|
||||
| 无拼接污染 | `chat()` 是**库内聚合流**——调用方拿完整 `LLMResponse`,流在库内消费完毕才返回;中段切换赢家时两路内容从未交付调用方,**无拼接问题**。这与调研 §3.1 基于"流透传"假设的 commit 语义不同。**约束明写**:未来若做流式透传,commit 点须重审——一旦任一路首 chunk 已透传下游,即按 Envoy"判赢不回头"锁定该路,中段对冲对该次调用永久关闭(§8 非目标:本版不做透传) |
|
||||
| 与看门狗的交互 | 两参数语义独立:若主路沉默超 `inter_token_timeout_s` 被看门狗掐死,**对冲路已在跑 → 主路之死不算失败,等副路**;副路未起 → 照旧 Transient 重试。装配守卫:`stall_after_s ≥ inter_token_timeout_s` 时 warning(对冲窗永不开,形同虚设);建议 `stall_after_s < inter_token_timeout_s` |
|
||||
| 与看门狗的交互 | 两参数语义独立:若主路沉默超 `inter_token_timeout_s` 被看门狗掐死,**对冲路已在跑 → 本轮超级尝试不因主路之死而败(等副路);attempt 级记账(breaker `record_failure`/`attempt_fails`/遥测 error 行)照常**;副路未起 → 照旧 Transient 重试。装配守卫升级为硬守卫(§6,§11-N5):`stall_after_s ≥ 有效最小值` → ValueError,对冲窗永不开的情形在装配期直接拒绝。不对称声明:看门狗掐死喂熔断失败 vs 对冲输家取消不喂熔断,同为源沉默事件不同处置——前者是 1.3.6 起既有看门狗语义、后者是 1.3.7 已批准语义,本设计不改任一;运维"挂起率"统计须知此不对称 |
|
||||
| 成本披露 | 实测沉默 >20s 占 **19.0%**——触发率远高于首 token 对冲(ttft 仅 3/4407 超 20s);每触发一次最多 +1 并发请求 + 一份 est 预扣滞留(1.3.7 §4.4 口径);**默认必须关闭**,文档明写"对冲 = 用配额换延迟" |
|
||||
| 改动面 | `retry.py:542-562` 邻域(双阈值触发窗 + 与看门狗掐死的汇合规则);`ports.py` + `openai_compat.py:569-575/605` 邻域(`complete` 加内容 chunk Event 参数——公共端口签名第二次变更,须进批准项);`config.py` 邻域(新键 + 交叉守卫);`client.py` 透传;遥测复用 `hedge_cancelled`,零新列 |
|
||||
| 改动面 | `retry.py`:`__call__` 入口门改为"两阈值(`hedge_after_s`/`stall_after_s`)任一已设即进对冲编排;仅 `stall_after_s` 且请求非流式 → 逐字走单路(等价未配置,中段沉默无非流式概念)"(§9-④ 独立生效的编排落点);`_past_hedge_window`(`retry.py:542-562`)从一次性等待重构为两阶段(首 token 前:`AFTER_S` 已设才等;首 token 后:`STALL_AFTER_S` 循环等待)+ 与看门狗掐死的汇合规则;`ports.py` + `openai_compat.py:569-575/580-590` 邻域(`complete` 加增量 chunk Event 参数——公共端口签名第二次变更,§9 整体批准覆盖);`config.py` 邻域(新键 + 交叉守卫);`client.py` 透传;遥测复用 `hedge_cancelled`,零新列 |
|
||||
|
||||
**方案 2C 不单列,作为 2B 的裁定细节**:看门狗掐死瞬间原子地并发补一路(掐与对冲合一)。评估:省一个参数但把"观测阈值"与"掐死阈值"耦合,掐死时副路从零开始(TTFT 全付),总延迟 = 沉默阈值 + 完整生成,严格劣于 2B 的"沉默中提前并发";且与 2A 链叠加时行为难推理。否决,仅留档。
|
||||
|
||||
@@ -165,9 +166,9 @@ P2 触发所需的活体信号由此派生:transport 每记一个内容 chunk
|
||||
| 键 | 值域 | 缺省 | 守卫与既有键关系 |
|
||||
| --- | --- | --- | --- |
|
||||
| `{SCOPE}__STREAM__STALL_THRESHOLD_MS` | 正整数毫秒 | **1000**(§9-③ 拍板,可调) | 仅计量口径,不改任何行为;不设 = 用缺省;调整接口 = env 键 / `GatewaySettings` 字段 / `GatewayClient` 构造 kwarg 三层,无 per-call 覆盖;命名对齐调研 §3.3(pi-tps 500ms 先例,但场景不同) |
|
||||
| `{SCOPE}__HEDGE__STALL_AFTER_S` | 有限正数秒(复用 `ensure_call_deadline` 同款校验) | **未设 = 关闭** | 独立于 `HEDGE__AFTER_S`;`stall_after_s ≥ inter_token_timeout_s` → 装配 warning(§5.2);单源 scope 设阈值 → warning 允许(1.3.7 §5 同款);与 `call_deadline_s` 组合守卫沿用 1.3.7 §5/§6 |
|
||||
| `{SCOPE}__HEDGE__STALL_AFTER_S` | 有限正数秒(复用 `ensure_call_deadline` 同款校验) | **未设 = 关闭** | 独立于 `HEDGE__AFTER_S`;仅对流式生效,纯非流式 scope 设置时装配 warning(非流式静默退化);`stall_after_s ≥ min over sources of (inter_token_timeout_s or timeout_s)`(有效值,`openai_compat.py:573` 回落同款)→ **ValueError**(与 1.3.7 `hedge_after_s ≥ min(timeout_s)` 同款硬守卫,config.py:820-824——stall 场景无非流式残留收益,warning 论证不足);`stall_after_s ≥ call_deadline_s` → ValueError,落进 `check_hedge_assembly`(config.py:772);单源 scope 设阈值 → warning 允许(1.3.7 §5 同款) |
|
||||
|
||||
`STREAM` 段须加入 `_RESERVED_SEGMENTS`(`config.py:57` 邻域,1.3.7 为 `HEDGE` 做过同款);3 段键天然跳过 `_load_sources`(`config.py:401-407`)。装配路径与 1.3.7 对冲同款:`GatewaySettings` 字段 + `__post_init__` 守卫 + `GatewayClient.__init__` keyword-only 入口即校;不提供 per-call 覆盖(阈值是源/渠道特性,1.3.7 §5 同款判据)。
|
||||
`STREAM` 段须加入 `_RESERVED_SEGMENTS`(`config.py:57` 邻域,1.3.7 为 `HEDGE` 做过同款);3 段键天然跳过 `_load_sources`(`config.py:427-431`)。装配路径与 1.3.7 对冲同款:`GatewaySettings` 字段 + `__post_init__` 守卫 + `GatewayClient.__init__` keyword-only 入口即校;不提供 per-call 覆盖(阈值是源/渠道特性,1.3.7 §5 同款判据)。
|
||||
|
||||
## 7. 与既有契约的关系
|
||||
|
||||
@@ -175,7 +176,7 @@ P2 触发所需的活体信号由此派生:transport 每记一个内容 chunk
|
||||
| --- | --- |
|
||||
| `CallStats`/`LLMResponse` 只增不删不改名 | 两新字段(`stall_ms`/`stall_count`)全 `int \| None`、默认 None;1.3.7 及以前构造的位置调用不炸(1.3.7 §4.5 同款论证);`generation_ms` 语义重定义为净口径(**已批准例外**,§4.2 正当性与披露),`max_inter_token_ms` 原语义一字不动 |
|
||||
| 遥测迁移 | `stall_ms`/`stall_count` 两列可空,PG/SQLite 各一次 DDL;旧行 null;迁移纪律照 issue #12/#13/#15 教训与 1.3.7 §4.5 备选调先例;**加列已拍板**(§9-⑤) |
|
||||
| 公共端口签名 | `Transport.complete` 随 2B 第二次加参(内容 chunk Event,必填 kw),与 1.3.7 `first_token_event` 同款约定;另 `TransportResult` 随 1B' 增 `stall_ms`/`stall_count`(默认 None) |
|
||||
| 公共端口签名 | `Transport.complete` 随 2B 第二次加参(增量 chunk Event,必填 kw),与 1.3.7 `first_token_event` 同款约定;`TransportResult` 随 1B' 增 `stall_ms`/`stall_count`(默认 None);`TelemetryRecorder.record_llm_call` 签名随 1B' 加 `stall_ms`/`stall_count` 两参(**第三次端口签名变更**,emitter 装配闸 `signature.bind` 以该协议签名为判据)——三次变更均 §9 整体批准覆盖 |
|
||||
| 文档登记义务 | README 能力表 + CHANGELOG + ARCHITECTURE.md(口径段与对冲触发段)三处同步,发版按 CLAUDE.md §4.4.1 清单执行 |
|
||||
| 版本归口 | **1.3.8 确认**(人类拍板 P1 + P2 同批发);P1 含 `generation_ms` 口径变更,披露义务照 §4.2;P2 纯增量(新键 + 默认关闭行为) |
|
||||
|
||||
@@ -204,7 +205,27 @@ P2 触发所需的活体信号由此派生:transport 每记一个内容 chunk
|
||||
|
||||
## 10. 自查记录
|
||||
|
||||
- 行数 210 ≤400;无超 15 行代码块(全文无代码块,仅表格与伪语义表述)。
|
||||
- 方案改动面均列具体文件:行号(1B'、2B);行号沿用 PM 已验证事实,未重新验证。
|
||||
- 行数 231 ≤400;无超 15 行代码块(全文无代码块,仅表格与伪语义表述)。
|
||||
- 方案改动面均列具体文件:行号(1B'、2B);行号以 2026-09-11 独立审查核实记录为准(全部实读源码,见 §11)。
|
||||
- 调研结论引用处标注(调研 §x.y);未重查网络。
|
||||
- 2026-09-11 批准确认:§9 五项 + 同批发 1.3.8 全部落地本文;1A/1C 保留为否决留档,1B 原案标注被 1B' 取代。
|
||||
- 2026-09-11 独立审查补遗:`reviews/2026-09-11-streaming-stall-design-review.md` 的 B1-B3/N1-N9 经 PM 逐条裁决全处置,修正直接并入正文对应小节,§11 只做索引;§9 五项拍板不变。
|
||||
|
||||
## 11. 独立审查补遗(2026-09-11)
|
||||
|
||||
审查报告 `reviews/2026-09-11-streaming-stall-design-review.md` 裁决"有条件实施"(3 阻断 + 9 建议);PM 逐条裁决如下,全部修正已并入正文对应小节(**本节只做索引,正文为准**)。§9 五项拍板不变。
|
||||
|
||||
| 发现 | PM 裁决 / 处置 | 正文落点 |
|
||||
| --- | --- | --- |
|
||||
| B1 chunk 定义未钉死 | chunk = 任何增量产出(`content` 或 `reasoning_content` 都算),与看门狗/首 token 活性口径一致;仅空 chunk/usage 帧/SSE keepalive 不计时;排除条款"内容 chunk <2"改"增量 chunk <2"(reasoning 模型长 thinking 只流 reasoning_content,只计 content 会误计沉默:P1 虚高、P2 误触发) | §3 记录内容行、§4.2 语义/排除条款、§2 vLLM 行 |
|
||||
| B2 遥测透传链路遗漏 | PM 裁决选 (b):`_emit` 显式从 TransportResult 传 stall 两值,LLMResponse 不增顶层字段(CallStats 已承载赢家口径,避免两处平铺漂移);`ports.py` `record_llm_call` 签名加两参(第三次端口签名变更,补进 §7 端口行);emitter `_AttemptUsage` + 两 recorder 实现 + schema.py 五处编辑点全列;输家表述修正:被取消输家无 TransportResult、行是 `hedge_cancelled` error 行、stall 恒 None,存在性由该行表达 | §4.2 改动面/排除条款、§7 端口行 |
|
||||
| B3 编排入口遗漏 | `__call__` 入口门改"两阈值任一已设即进对冲编排;仅 `stall_after_s` 且请求非流式 → 逐字走单路(等价未配置)";`_past_hedge_window` 从一次性等待重构为两阶段(首 token 前 AFTER_S 已设才等 / 首 token 后 STALL_AFTER_S 循环等待);纯非流式 scope 设置给装配 warning | §5.2 改动面、§6 配置表 |
|
||||
| N1 计时边界未明写 | 净值 = 墙钟 − 中段沉默,仍含 TTFT 与尾部;尾部归活跃侧是有意选择(墙钟基底,尾部≈0),与 pi-tps"止于最后更新"的差异显式声明,README 口径表同步 | §4.2 边界与换算行 |
|
||||
| N2 非流式双口径 | 非流式 net≈wall(无中段沉默概念);下游预算公式以 `stall_ms is None` 为分支条件 | §4.2 排除条款 |
|
||||
| N3 Event 并发纪律 | 采 Event + 复检方案:①超时到期后触发前复检 `is_set()`;②check→clear 之间无 await;③对冲路 chunk Event 恒 None(1.3.7 首 token 约定延伸);④每轮迭代自建 waiter,逐字套用 `_past_hedge_window` waiter 收口 + `cancelling()` 穿透;⑤等待者唯一(编排) | §5.2 触发行 |
|
||||
| N4 钟同源与负值防御 | 沉默序列(transport)与墙钟(retry 注入 `now`)生产同为 `time.monotonic`;`net = max(0, wall − stall)` clamp;FakeClock 测试用自控 stall 值的假 transport,真实 transport 测试不注入假钟 | §4.2 边界与换算行 |
|
||||
| N5 守卫三缺口 | 守卫用有效值 `min over sources of (inter_token_timeout_s or timeout_s)`(回落同款);`stall_after_s ≥ 该有效最小值` → **ValueError**(与 1.3.7 `hedge_after_s ≥ min(timeout_s)` 同款硬守卫——stall 场景无非流式残留收益,warning 论证不足);`≥ call_deadline_s` → ValueError,点名落进 `check_hedge_assembly` | §6 配置表、§5.2 与看门狗交互行 |
|
||||
| N6 `stall_count` 定义 | = 超阈值相邻间隔个数;删"连续停顿合并一次"子句(gap 口径下任意两超阈 gap 间至少隔一个 chunk,无可合并对象,该子句移植自 pi-tps 事件流语义、在此无操作定义) | §4.2 语义行 |
|
||||
| N7 引用勘误 | CallStats 在 `types.py:296-324`(原"417-418 邻域"错位,417-418 是 LLMResponse 邻域,`max_inter_token_ms` 在 :406);`config.py:401-407` → `:427-431`;"`config.py:46` 邻域" → `_load_hedge`(`config.py:745-769`)同款 loader + `GatewaySettings` 字段(:46 是 per-source 四段键表 `_SOURCE_FIELDS`,与 §6 三段键矛盾);max_gap 循环在 `openai_compat.py:580-590`(:605 是装配处);遥测编辑点 = DDL 末尾(created_at 后) + `SQLITE_BACKFILL`/`_PG_BACKFILL_DECLS` 保序对齐 + `COLUMNS` + 列数断言测试 + `telemetry_schema_sql` 同源再生成;§5.2"须进批准项"改"§9 整体批准覆盖" | §3、§4.1/§4.2/§5.2 改动面、§6、§7 |
|
||||
| N8 换算钉死 | TransportResult `stall_ms: float \| None`;retry 层 `wall_ms=int((now−gen_started)*1000)` 照旧,`net = max(0, wall_ms − int(stall_ms or 0))`;`CallStats.stall_ms = int(stall_ms)`(None 透传)——换算只此一处,各实现不得另取整 | §4.2 边界与换算行 |
|
||||
| N9 看门狗措辞 | "主路之死不算失败"改"本轮超级尝试不因主路之死而败(等副路);attempt 级记账(breaker `record_failure`/`attempt_fails`/遥测 error 行)照常";补不对称声明:看门狗掐死喂熔断失败 vs 对冲输家取消不喂熔断,同为源沉默事件不同处置——前者是 1.3.6 起既有看门狗语义、后者是 1.3.7 已批准语义,本设计不改任一;运维"挂起率"统计须知此不对称 | §5.2 与看门狗交互行 |
|
||||
|
||||
Reference in New Issue
Block a user