
1. 从“能算”到“算得快且稳”QP优化到底在解决什么问题如果你做过轨迹规划、模型预测控制、投资组合或者SVM训练大概率绕不开一个词——二次规划也就是QPQuadratic Programming。它是一类特殊的凸优化问题目标函数是二次的约束是线性的。听起来好像挺窄但实际工程里只要你的代价函数是“平方误差”形式约束是线性的那它就是一个QP。我最早接触QP是在做机械臂轨迹跟踪的时候。当时用MPC模型预测控制做滚动优化每一步都要在线解一个QP。一开始用现成的求解器发现偶尔会解不出来或者解出来的结果抖得厉害。后来才明白QP虽然理论上是凸的但数值上并不总是“稳”的。条件数、约束的线性相关性、变量尺度差异都会让求解器在边缘反复横跳。所以这篇文章想聊的不是“QP是什么”这种教科书定义而是一个从业者在实际项目里怎么把QP用对、用稳、用快。我会从问题构造、求解器选型、参数调优、常见坑几个角度展开尽量把那些文档里不写、但实际会踩的经验说清楚。适合已经知道QP基本形式、但在工程落地时遇到困难的朋友如果你刚入门也能从里面的类比和排查表里找到入手点。核心关键词二次规划、凸优化、KKT条件、有效集法、内点法、ADMM、数值稳定性、求解器调参。这些词后面都会反复出现我会尽量用生活化的例子把它们串起来。2. 问题构造你的QP到底长什么样2.1 标准形式与工程形式的差距教科书上的QP标准形式通常是minimize (1/2) x^T P x q^T x subject to A x b G x h其中P是半正定或正定矩阵。这个形式很干净但工程里你拿到的往往不是这样。比如MPC里你的决策变量可能是未来N步的控制量目标函数里既有跟踪误差的平方又有控制增量的平方还有终端代价。约束里既有状态约束又有控制量上下限还有控制增量约束。这时候P矩阵可能是稀疏的、带块结构的q向量里混着参考轨迹和当前状态。我见过不少新手直接把这种问题丢给求解器结果要么慢得没法用要么解出来的控制量像抽风一样。问题往往出在没有做预处理。预处理包括变量缩放、约束去冗余、矩阵稀疏化。举个例子如果你的状态量单位是米控制量单位是牛顿两者数值可能差好几个数量级P矩阵的条件数就会爆炸。求解器在迭代时梯度方向会被大数值分量主导小数值分量几乎不动收敛自然慢。注意变量缩放不是可选项是必选项。我通常会把所有变量缩放到[-1,1]附近或者至少让它们的量级在同一数量级。缩放矩阵是对角的构造起来很简单但效果立竿见影。2.2 凸性检查别把非凸问题当凸问题解QP要求P是半正定。如果P不是半正定那它就不是凸QP可能有多个局部极小值求解器给出的解依赖初始点甚至可能发散。工程里P通常来自平方项天然半正定。但有一种情况容易翻车你手动构造的P矩阵因为数值误差或者建模错误出现了负特征值。我遇到过一次在写一个带柔顺控制的QP时为了惩罚末端速度我直接用了v^T W vW是对角阵理论上正定。但因为坐标系变换矩阵写错了一个符号导致P出现了一个很小的负特征值-1e-8量级。求解器没报错但解出来的力方向是反的。后来加了一步eig(P)检查最小特征值才定位到问题。所以我的习惯是每次构造完P先算一下最小特征值。如果小于-1e-6说明建模有问题如果在[-1e-6, 1e-8]之间可以加一个小的正则项P eps*Ieps取1e-8到1e-6保证严格正定。这个操作叫Tikhonov正则化代价是引入一点点偏差但换来了数值稳定性。2.3 约束的线性相关与冗余约束G x h里如果两行G是线性相关的比如x1 x2 1和2x1 2x2 2那第二个约束是冗余的。冗余约束本身不改变可行域但会让有效集法在迭代时反复切换增加计算量。更麻烦的是近似线性相关比如两行G的夹角只有0.01度数值上几乎平行。这时候求解器可能会在两者之间震荡收敛很慢。处理办法有两个一是用QR分解或者SVD对G做行约简去掉相关行二是在求解器里设置一个约束容差让它在判断激活时忽略那些“几乎不激活”的约束。我通常两个都做离线做一次行约简在线设置constraint_tolerance 1e-6。这样既减少了问题规模又避免了在线震荡。3. 求解器选型有效集、内点还是ADMM3.1 有效集法小规模、热启动友好有效集法的思路很直观先猜哪些不等式约束是“激活”的也就是取等号的把它们当成等式约束解一个等式约束QP。然后检查解是否满足其他不等式如果不满足就调整激活集继续迭代。它的优点是热启动非常方便如果你有上一次的解和激活集可以直接从那里开始通常几步就收敛。MPC里相邻两个控制周期的QP差别很小热启动能省掉大量计算。但有效集法有个缺点最坏情况下迭代次数可能很多。如果激活集频繁切换比如约束边界附近有抖动它就会反复横跳。我试过在一个机械臂避障场景里障碍物距离约束一直在激活边缘有效集法平均要迭代20多次而内点法只要8次。所以有效集法适合**约束变化不剧烈、规模较小变量数100**的场景。3.2 内点法大规模、多项式复杂度内点法从可行域内部出发通过障碍函数把不等式约束“软化”然后沿着中心路径逼近最优解。它的理论迭代次数是O(sqrt(n) log(1/eps))n是变量数eps是精度。这个复杂度是多项式级的所以大规模问题变量数几百到几千通常用内点法更稳。但内点法对初始点要求高必须严格可行。如果初始点不可行需要先解一个辅助问题找可行点。另外内点法每次迭代要解一个KKT系统矩阵规模随变量数增长所以单次迭代比有效集法贵。不过它的迭代次数对问题规模不敏感所以总体算下来大规模问题还是内点法划算。我个人的经验变量数50用有效集法热启动变量数200用内点法中间地带看约束变化频率。如果约束变化快内点法更稳如果变化慢有效集法更快。3.3 ADMM分布式、可分解ADMM交替方向乘子法严格来说不是专门解QP的但它对可分解的QP特别有效。比如你的QP可以拆成几个子问题每个子问题只涉及一部分变量那ADMM可以并行求解然后通过乘子协调。这在多机器人协同、分布式MPC里很常见。ADMM的收敛速度依赖惩罚参数rho。rho太大子问题解得太准但乘子更新慢rho太小乘子更新快但子问题解得太糙。我通常用自适应rho如果残差下降慢就增大rho如果残差震荡就减小rho。具体实现里每10次迭代检查一次残差按1.1或0.9的因子调整。这个技巧能把收敛速度提升30%以上。提示ADMM的解不保证严格满足约束只保证残差收敛到零。所以如果你的应用对约束满足要求极高比如安全约束要么多迭代几次要么在最后加一个投影步骤。4. 数值稳定性那些让求解器“发疯”的细节4.1 条件数与预处理QP的KKT系统条件数直接决定求解器能多快收敛。条件数等于最大奇异值除以最小奇异值。如果P的特征值范围是[1e-8, 1e8]条件数就是1e16双精度浮点根本解不动。这时候必须做预处理。预处理分三步行缩放、列缩放、对角正则。行缩放是给每个约束乘一个系数让G的行范数接近1列缩放是给每个变量乘一个系数让P的对角元接近1对角正则是在P上加一个小eps*I。我通常用equilibrate函数很多求解器自带自动做前两步然后手动加eps1e-8。实测下来一个条件数1e12的QP经过预处理后条件数降到1e4求解时间从200ms降到15ms。这个提升在实时控制里是决定性的。4.2 热启动与冷启动热启动是用上一次的解作为初始点。对于MPC这种滚动优化相邻两步的QP只差一个状态更新解的变化很小热启动能减少50%-80%的迭代次数。但热启动有个坑如果上一次的解不可行比如约束变了直接拿过来用会导致求解器报错。所以热启动前要检查可行性不可行的话要么投影到可行域要么退回到冷启动。冷启动是从一个默认点开始比如零向量或者可行域中心。冷启动的优点是稳定缺点是慢。我通常设置一个策略如果热启动在3次迭代内残差没下降就切换到冷启动。这样兼顾了速度和鲁棒性。4.3 终止准则别让求解器“过度工作”求解器的终止准则通常是残差 tol。tol设得太小求解器会一直迭代到浮点精度极限浪费时间设得太大解不够准控制效果差。我一般设tol 1e-6对于大多数控制问题足够。如果对精度要求高可以设1e-8但要注意迭代次数可能翻倍。还有一个隐藏问题残差的计算方式。有些求解器用绝对残差有些用相对残差。绝对残差对问题尺度敏感相对残差更通用。我建议用相对残差并且同时监控原始残差和对偶残差。如果原始残差降了但对偶残差没降说明解还没收敛到KKT点不能停。5. 实操流程从建模到部署的完整链路5.1 建模阶段把物理问题写成QP假设你要做一个倒立摆的MPC。状态是角度theta和角速度theta_dot控制量是力矩u。线性化后的动力学是x_{k1} A x_k B u_k目标函数是让theta和theta_dot回到零同时惩罚uJ sum_{k0}^{N-1} (x_k^T Q x_k u_k^T R u_k) x_N^T Qf x_N约束是|u_k| u_max|theta_k| theta_max。把x和u堆成一个向量z [x_0, u_0, x_1, u_1, ..., x_N]用动力学方程消去x_1到x_N得到z只含u_0到u_{N-1}和x_0。然后J变成z的二次函数约束变成z的线性不等式。这就是标准QP形式。这个消去过程叫condensing。它的好处是变量数从(N1)n_x Nn_u降到N*n_u规模小很多。坏处是P矩阵变稠密失去稀疏性。如果N很大比如N50condensing后的P矩阵可能很大求解反而慢。这时候可以用稀疏QP保留x变量利用A和B的稀疏结构。我通常N20用condensingN50用稀疏。5.2 求解器配置参数怎么调以常见的开源求解器为例不点名只说通用参数max_iter最大迭代次数设1000足够超过说明有问题。tol相对残差容差设1e-6。constraint_tolerance约束违反容差设1e-6。warm_start开启热启动。scaling开启自动缩放。regularization设1e-8。这些参数不是固定的要根据问题调。比如你的约束很紧constraint_tolerance要设小一点1e-8如果约束很松1e-4也行。我一般先跑一次看迭代次数和残差曲线再决定调哪个。5.3 在线求解时间预算与降级策略实时控制里QP求解有时间预算比如1ms。如果求解器在1ms内没收敛你得有个降级策略。常见的有固定迭代次数不管收不收敛迭代10次就返回当前解。这个解可能不满足约束但通常接近最优。回退到上一次解如果这次求解失败直接用上一次的控制量。软化约束把硬约束改成软约束加一个大的惩罚项保证问题总是可行。我通常用固定迭代次数回退。固定迭代次数保证时间可控回退保证安全。软化约束只在约束可能冲突时用比如两个障碍物把路堵死了硬约束无解只能软化。6. 常见问题与排查技巧实录6.1 求解器返回“不可行”怎么办不可行意味着约束矛盾没有点能同时满足所有约束。排查步骤检查约束符号G x h里的不等号方向对不对我见过有人把写成导致可行域为空。检查约束边界h是不是设得太小比如u_max设成0.1但实际需要1.0的力矩那肯定不可行。检查线性相关如果两个约束是x 1和x 2那不可行。用linprog找一个可行点如果找不到就是真不可行。软化约束如果不可行是暂时的比如传感器噪声导致加松弛变量惩罚松弛量。6.2 解出来的控制量抖动严重抖动通常是因为QP的解对输入敏感。输入里的微小噪声导致激活集切换解就跳变。解决办法加正则项在目标函数里加rho * ||u - u_prev||^2惩罚控制量变化。rho取0.01到0.1。加滤波对解出来的u做低通滤波截止频率根据控制带宽定。增大约束容差让求解器不要频繁切换激活集。我试过在MPC里加rho0.05的变化惩罚抖动幅度从0.3降到0.02效果很明显。6.3 求解时间忽长忽短求解时间不稳定通常是因为迭代次数不稳定。原因可能是热启动失效上一次的解不可行导致这次从冷启动开始。条件数变化某些状态下P矩阵条件数变差。激活集震荡约束在激活边缘反复切换。排查方法记录每次求解的迭代次数和残差曲线。如果迭代次数突然跳变看对应的状态和约束。我遇到过一次机械臂在某个特定角度时两个关节的约束同时激活导致有效集法震荡。后来把这两个约束合并成一个问题解决。6.4 常见问题速查表现象可能原因排查方法解决措施返回不可行约束矛盾用linprog找可行点软化约束或修正约束解抖动激活集切换记录激活集历史加正则项或滤波求解慢条件数大计算P的条件数预处理或正则化迭代次数多热启动失效检查初始点可行性投影或冷启动解不满足约束容差太大检查约束违反量减小constraint_tolerance残差震荡rho不合适监控原始/对偶残差自适应调整rho7. 进阶技巧让QP求解快上加快7.1 利用稀疏结构如果你的QP来自MPCP矩阵通常是块三对角或者带状。利用这个结构KKT系统的求解可以从O(n^3)降到O(n)。具体做法是用稀疏LU分解或者Ricci迭代。很多求解器支持稀疏矩阵输入你只需要用稀疏格式比如CSR构造P和G求解器会自动利用。我做过一个对比N100的MPC稠密求解要80ms稀疏求解只要6ms。差距是数量级的。所以能用稀疏就用稀疏别图省事用稠密。7.2 代码生成与嵌入式部署嵌入式平台上求解器不能太大也不能依赖动态内存。这时候可以用代码生成工具把求解器编译成C代码固定内存分配。生成的代码通常比通用求解器快2-5倍因为编译器能做常量传播和循环展开。但代码生成有个限制问题结构必须固定。变量数、约束数、稀疏模式都不能变。如果你的MPC horizon是固定的那没问题如果horizon可变就得生成多个版本或者用通用求解器。7.3 多精度求解有些平台支持半精度或单精度浮点。单精度求解比双精度快2倍左右但精度低。如果你的应用对精度要求不高比如消费级机器人可以试试单精度。我试过在单精度下解MPC控制效果和双精度几乎没差别但求解时间减半。不过要注意单精度下条件数容限更小预处理要更仔细。8. 我个人在实际操作中的体会QP优化这件事理论上的最优解和工程上的可用解往往是两回事。我见过太多人把QP当成黑盒调不通就换求解器换了好几个还是不行。其实问题通常不在求解器而在问题构造和数值预处理。把变量缩放做好把冗余约束去掉把正则项加上大部分问题都能解决。另外别迷信“最优解”。实时控制里一个80%最优但稳定的解比100%最优但抖动的解好得多。所以我在目标函数里总会加一点正则牺牲一点最优性换来确定性和鲁棒性。这个取舍在工程里是值得的。最后分享一个小技巧记录每次求解的迭代次数和残差。不用很复杂就存两个数组跑完之后画个图。如果迭代次数突然跳变或者残差下降变慢那就是问题出现的信号。这个习惯帮我定位过很多隐蔽的bug比调试器还管用。