那条测地线
那条测地线
夜深的时候,我又翻出了那段代码。不是检查——那东西已经在模拟器里跑了不知道多少万次了,不需要再检查。只是看。像看一段写完了很久的字,你不去改它,只是确认它还在那里。
G2。十二行,核心就两行。一行是x_est = min(x_est, z),一行是x_est += x_est × 12 / 100,然后被观测值压着,不超过它。就这么两行,替代了之前那个带协方差矩阵、带离群值门控、带多层漂移检测的卡尔曼滤波器。三百多行代码,缩成两行。不是因为两行更好,而是因为那个更复杂的方案,在一个你永远无法精确知道参数的世界里,并不比这两行更对。
这个结论不是想出来的,是试出来的。
有人问过我,为什么不用CUSUM。Wald在1947年就证明了,CUSUM是在给定虚警率下最小化期望检测延迟的最优序贯检验。这是定理,不是观点。我试过。我在同样的模拟器里跑了CUSUM,给了它最好的条件——漂移参数设成5%的T_prop,检测阈值设成15%,都是合理的默认值。它在干净的数据上跑得很快,比G2快,有时候一步就检测到了。
但网络不是干净的。
网络上有一条物理铁律,简单到几乎不值一提:排队延迟永远是非负的。T_queue ≥ 0。一个包可以在路由器里等,但它不能“提前”到达。这条看似无害的约束,把RTT的观测空间压弯了。所有的观测值,只要包含排队,都落在真实传播延迟的右边。正方向的偏差,可能来自路径真的变长了,也可能来自队列的波动。信息论上,这两者在标量RTT里是不可分的——Fisher信息矩阵的秩不够,这是Rao在1945年就写清楚的事。
CUSUM不知道这件事。它假设噪声是对称的,均值为零。当观测值持续上升时,它忠实地累积这些正偏差,然后触发——它以为检测到了路径增长,但其实它只是看到了排队。它会把min_rtt更新成一个被队列污染的值,然后BDP膨胀,然后丢包。这不是CUSUM的错,是CUSUM的假设在这个弯曲空间里不成立。
G2没有这个问题。它的12%增长被观测值压着——min(x_est × 1.12, z)。当拥塞出现时,z是T_prop加上队列延迟。如果队列只是轻度拥塞,z被压在10%阈值以下,G3永远不会触发。只有当队列深到超过T_prop的10%时,G3才可能触发——而那时候的BDP高估是真实的,不是误判。
CUSUM更快,但它在一个不可信的方向上快。G2更慢,但它的慢是有方向的——它只在可信的时候快,不可信的时候它慢下来,以固定的12%速率试探,不求快,只求不错。
后来有人和我争论了很久,说G2不是“最好的测地线”,说CUSUM在理论上更优。我说是的,CUSUM在参数已知时更优,但网络中参数未知。他说那CUSUM的δ可以自适应,我说自适应需要估计噪声方差,而噪声方差本身也被T_queue污染。他说那你这个12%也不是推导出来的最优值,只是工程选择。我说是的,它是工程选择——它满足四个约束:单步跨阈值(p > 0.10)、整数运算(n/100)、在光纤+BGP的20 RTT窗口内检测到10倍增长(p ≥ 0.122)、以及最小值原则——满足前三者的最小整数值。
满足所有这些约束的值,只有一个。12/100。
这不是从某个损失函数的最小化里推出来的,这是从问题本身的边界条件里挤出来的。它不是最优解,它是唯一解。
我最后把那段CUSUM的测试代码放进了.research/目录里,没有删,也没有用。它们在那里,像走过的岔路,你知道它们存在过,但你现在走的路不是它们。
夜深了。窗外已经没有声音。我看着屏幕上的那两行代码,想,它的确不是完美的。它在5%的小幅路径增长时,检测延迟长到20到40个RTT。在增幅特别大又没有排队的极限场景里,min_rtt会有大约2.9%的系统偏差。但这两个局限的方向都是一样的——它们都指向低估BDP,而不是高估。BDP低估,只是带宽暂时没打满。BDP高估,是丢包,是重传,是连接崩溃。在拥塞控制里,这两个方向从来不是对称的。G2把所有的误差都推向了安全的那一侧。这不是统计最优,这是工程清醒。
我关掉了编辑器。
那条线还在那里。不是被画出来的,是被这个弯曲的观测空间本身的形状逼出来的。你能做的只是沿着它走,不求最快,只求不走错。这大概就是测地线的意思了。不是最短的那条路,是唯一不会让你掉下去的那条路。
