
做实时 MPC 时平均耗时往往不是最难的问题。真正棘手的是大部分帧很快某一帧却突然迭代几十次而且离线重新运行还不一定能够复现。最近在一个充电站滚动功率分配模型中我遇到了这样一个慢帧正常帧通常只需要几次迭代但个别帧达到 40 多次耗时从 1 ms 左右升到 3 ms 以上。最终定位发现问题不在模型不可行也不是简单的“slack 没有生效”而是 warm start 平移以后用户定义的 slack 初值没有跟上阶段约束的变化导致初始点落在不等式约束外。内点法虽然最终能够找到解但需要很多轮迭代把原始变量、内部 slack 和对偶变量重新拉回协调状态。这篇文章记录完整的排查过程。1. 问题场景这个模型同时给 4 辆车分配充电功率。状态量包括4 辆车的 SOC。4 个充电枪的当前功率。控制量包括4 个充电枪的功率变化率。4 个用于放松最低 SOC 要求的业务 slack。模型预测时域为 36 个 stage状态维度nx8控制维度nu8。动态方程为soc_dot_i charge_rate_i * charge_power_i charge_power_dot_i power_rate_i模型同时考虑单枪功率范围、站端总功率上限、车辆离站前的最低 SOC、电价、功率平滑性和 SOC 目标。其中最低 SOC 约束写成soc_min_required_i - soc_i - slack_soc_i 0slack_soc_i会进入代价函数因此最低 SOC 是一个可以付出代价后适当放松的软约束。2. 慢帧的表现在线滚动运行时日志中出现了下面的慢帧[OCP][WARN] slow_solve modelChargingStationModel frame8 iter40 time_ms3.22846 threshold_ms2另一轮详细迭代日志中不等式残差下降过程非常慢迭代First orderInequalityComplementary0141.95947.1810.6501076.55238.4350.351209.78228.8890.046301.19511.6960.0056400.6983.1510.00018420.00280.01250.00000143000这不是单次线性代数计算突然变慢。每轮迭代仍然只需要约0.1 ms总耗时增长主要来自迭代次数增加。因此排查重点应该从“哪一个函数变慢了”转向“为什么初始点需要这么多轮才能进入收敛区域”。3. 第一个坑离线 replay 必须恢复求解前现场慢帧发生后最自然的做法是保存数据并离线回灌。但第一次 replay 时同一个文件只迭代了 15 次Solver Status: solved Total iterations: 15 Run time(ms): 1.392这说明当时保存的并不是在线求解开始前的完整现场。连续帧 MPC 的下一次求解不仅依赖当前状态和参数还依赖上一帧留下的 warm-start 数据包括各 stage 的状态和控制初值。内点法 slack 和对偶变量。等式约束与动力学乘子。当前 barrier 参数。warm start 是否有效。如果 dump 保存的是求解后的结果或者 replay 加载后又执行了一次普通 reset / warm-start 初始化离线问题就已经不是现场那个问题了。因此慢求解 dump 应该在solve()真正修改变量之前保存输入快照replay 时则恢复这份快照并直接求解不再二次生成 warm start。只有先解决“复现一致性”后面的数值分析才有意义。4. 从 629 个约束中找到真正的问题恢复完整输入后replay 工具输出每个 stage 的约束摘要并按违反程度排序。这个模型共有 629 个不等式约束分量最严重的两项是stage35 index13 g0.0886656 slack1 stage34 index13 g0.0670329 slack1约束统一采用g(x, u) 0所以g0.0886656表示初值已经违反约束而不是“距离边界还有 0.0886656”。生成的模型文档可以把index13映射回原始公式g[13] soc_min_required_0 - soc_0 - slack_soc_0 0到这里原因已经比较清楚预测时域后段的soc_min_required_0发生变化但从上一帧平移过来的slack_soc_0仍然太小无法覆盖新的 SOC 缺口。5. 有 soft constraint为什么初值仍会违反约束这里容易混淆两种不同的 slack。第一种是用户模型里的业务 slacksoc_min_required - soc - slack_soc 0它是一个优化变量也会受到slack_soc 0和 slack 代价的约束。它的作用是让原问题在 SOC 目标过紧时仍然存在可接受的解。第二种是内点法内部的 slack。求解器会把一般不等式写成g(x, u) s 0, s 0这两个 slack 不是同一个变量。业务约束“允许放松”不代表任意 warm-start 初值都自动位于可行域内。如果用户 slack 的初值太小g(x,u)仍然可能大于 0。内点法需要在动力学、功率边界、SOC 目标和对偶条件之间逐步调整因而出现几十次迭代。这也解释了为什么只修改内部 slack 初始化不能从根本上解决问题。例如s max(s_min, max(-g, 0.0));这能保证内部 slack 为正但当g 0时无法消除原始变量已经造成的约束违反。强行在求解器内部“掩盖”这部分残差反而可能破坏 KKT 方程的一致性。6. 正确的优化位置模型侧 warm-start sanitize这个问题最直接的修复是在进入 IPM 迭代之前根据模型语义修正 warm-start 原始变量。对于充电功率初值应满足0 charge_power_i max_power_i对于最低 SOC 软约束用户 slack 初值应满足slack_soc_i max( 0.005, soc_min_required_i - soc_i 0.01 )其中0.005保证 slack 不从零边界开始。soc_min_required_i - soc_i覆盖当前 SOC 缺口。0.01留出少量严格可行裕量。修正后对应约束满足soc_min_required_i - soc_i - slack_soc_i -0.01模型定义中只需要声明 warm-start 边界ocp.warm_start_state_box_bound( fcharge_power_{idx}, lower0.0, upper_paramfmax_power_{idx}, )ocp.warm_start_control_box_bound( fslack_soc_{idx}, lower[ 0.005, p[fsoc_min_required_{idx}] - x[fsoc_{idx}] 0.01, ], )代码生成器会生成对应的 CsanitizeWarmStart()求解器在冷启动、原始变量 warm start、rollout warm start 和 primal-dual 平移 warm start 之后统一调用。这样做有两个好处求解器只负责通用数值流程不需要知道 SOC、充电功率等业务含义。demo 侧不再重复手写 clamp 逻辑模型重新生成后规则会自动进入模型 SDK。7. 为什么不在通用 solver 里自动投影所有约束对简单 box bound可以直接 clamplower z upper但一般非线性约束可能是distance(x, obstacle) safe_distancefriction_circle(x, u) limit nonlinear_energy(x, u) budget对于这类约束solver 并不知道应该修改哪个变量也不知道应该投影到哪一侧。一次“自动拉回”本身就可能变成另一个非线性优化问题。因此更合理的边界是通用 solver 提供 warm-start hook 和一致的调用时机。codegen 为常见 box bound 自动生成修正规则。复杂非线性约束由用户提供初值或者由模型实现专用 repair / projection 逻辑。把业务语义留在模型侧比在 IPM 内部堆启发式规则更容易验证也更适合产品化。8. 优化后的结果当前同一充电站 rolling demo 的一轮 160 帧结果为指标结果求解成功160 / 160 帧平均求解耗时0.380 ms最大求解耗时1.089 ms最大迭代次数26最大站端功率违反量0 kW相比此前个别帧 40 多次迭代、3 至 4.5 ms 的表现慢帧尾部明显收敛。以上数据来自当前开发环境绝对耗时会受 CPU、编译选项和模型参数影响。这里更重要的结论不是某个固定毫秒数而是通过 replay 找到慢帧对应的约束再从模型语义上改善初值。9. 这次排查留下的几个经验第一不要只看平均耗时。实时系统更应该关注最大耗时、P99 和最大迭代次数。第二replay 必须保存求解前的完整输入现场。只保存状态和参数通常不足以复现连续帧 warm start 问题。第三约束诊断需要输出 stage、约束索引、原始表达式和违反量。只打印一个总的 inequality residual很难定位模型问题。第四soft constraint 解决的是问题可行性不会自动提供一个好的初值。用户 slack 和内点法内部 slack 必须区分。第五warm-start 修正应该放在模型层。通用 solver 提供机制模型提供语义codegen 负责把规则带到 C 运行时。结语实时 MPC 的工程难点不只是把单次求解做快还包括如何发现偶发慢帧、如何准确复现以及如何把数值问题映射回具体模型约束。这次问题最终不是通过继续压缩单次迭代耗时解决的而是通过完整 replay、逐阶段约束分析和模型侧 warm-start sanitize减少了不必要的迭代。如果你也在处理 MPC/OCP 的慢求解、失败回灌、C 部署或模型代码生成问题可以访问RTOCP - 实时 OCP/MPC C SDK