cvxcore:CVXPY 凸优化问题规范化(Canonicalization)的 C++ 内核详解

原创2026-10-06 21:07:391,463 阅读
文章标签:科学计算

cvxcore:CVXPY 凸优化问题规范化(Canonicalization)的 C++ 内核详解

导读

本文以 CVXPY 仓库中 cvxcore 的历史说明文档 为主体,系统讲解 cvxcore 的定位、工作原理、代码组织以及与 CVXPY 等建模语言的集成方式。读完本文,你将理解"凸优化建模语言 → 线性算子树(LinOp tree)→ 稀疏问题数据矩阵"这条核心链路是如何由 C++ 内核加速完成的,并掌握在 CVXPY 中通过 get_problem_matrix 获取 COO 格式问题矩阵、为其他 CVX.* 求解器接入 cvxcore 的完整步骤。


1. cvxcore 是什么:建模语言与求解器之间的公共规范化层

CVX、CVXPY、Convex.jl 等凸优化建模工具,工作方式如出一辙:先把用户书写的高层问题描述翻译成低层、规范的形式(canonical form),再交给后端求解器处理。这个"翻译"过程称为规范化(canonicalization),包括将目标函数与约束展开为线性算子树、把算子树组装为系数矩阵、附加常数列等步骤。

cvxcore 正是把这类建模系统共有的规范化操作抽离出来,打包成一个带简洁 C++ 接口的独立软件包:

  • 消除了在不同语言中重复实现规范化流程的需要;
  • 相比高层语言实现能带来显著的性能提升——原文档给出的历史实测结论是"大多数问题上相对最初版 CVXPY 实现有 3–10 倍加速"(该数字为 OLD_README 记录的历史性能结论,非当前版本基准)。

从当前仓库的 README.md 可以确认 cvxcore 的历史定位:它最初是为 CVX、CVXPY、Convex.jl 等多个建模语言设计的 C++ 后端,最终只被 CVXPY 采用;当前它被官方标记为 deprecated,但仍作为 CVXPY 默认的规范化后端在持续服役,因此理解它的架构对阅读 CVXPY 源码仍然很有价值。


2. 与 CVXPY 的集成:从 LinOp 树到稀疏问题矩阵

在 CVXPY 中,规范化链路可以概括为三个阶段:

  1. 构造线性算子树:目标函数与每条约束都被表示为一棵 Python 层的 LinOp 树(叶子是变量、参数、常数,内部节点是乘法、转置、切片、堆叠等仿射算子);
  2. 树 → C++ LinOp 树:通过树遍历把 Python 的 LinOp 树转换为 C++ 的 LinOp 树;
  3. C++ 建矩阵:把 C++ LinOp 树向量交给 cvxcore 的 build_matrix,返回包含稀疏问题数据的 ProblemData 结构,再转换回 Python 可用的 NumPy/SciPy 数组。

当前 CVXPY 侧的执行入口是 canonInterface.py 中的 get_problem_matrix:

def get_problem_matrix(linOps,
                       var_length,
                       id_to_col,
                       param_to_size,
                       param_to_col,
                       constr_length,
                       canon_backend=None):

其参数含义如下:

参数 含义
linOps Python linOp 树列表,表示一个仿射表达式(对应约束/目标)
var_length 问题变量的总长度
id_to_col 变量 id 到列偏移量的映射
param_to_size 参数 id 到参数尺寸的映射
param_to_col 参数 id 到张量中列的映射
constr_length 所有约束尺寸之和
canon_backend 规范化后端:'CPP'(默认)、'SCIPY'、'RUST'、'COO',可影响编译时间

get_problem_matrix 返回一个形状为 constr_length * (var_length + 1) 行、param_size + 1 列的稀疏 CSC 矩阵(其中 +1 列对应常数列偏移)。后端选择可通过 CVXPY_DEFAULT_CANON_BACKEND 环境变量全局切换(见 get_default_canon_backend)。

当选用 'CPP' 后端时,实际调用的是 cppbackend.py 中的 build_matrix:它构造 SWIG 生成的 cvxcore Python 绑定对象(cvxcore.ConstLinOpVector、cvxcore.IntIntMap),逐一调用 build_lin_op_tree 把 Python LinOp 树转换到 C++ 侧,再调用 cvxcore.build_matrix(lin_vec, var_length, id_to_col_C, param_to_size_C, num_threads) 获得 ProblemData,最后通过 getV/getI/getJ 取出三元组并拼装成 SciPy 的 csc_array。


3. 为其他 CVX.* 求解器接入 cvxcore:三步走

OLD_README 为希望在自己语言/求解器中复用 cvxcore 的开发者给出了明确的三步集成方案:

第 1 步:把目标与约束表示为线性算子树

在求解过程的某个节点,问题的目标与每条约束都必须是线性算子树(linear atom tree / LinOp tree)。将高层语言的 LinOp 树转换为 C++ LinOp 树时,需要做树遍历,并针对稠密矩阵与稀疏矩阵的不同表示处理若干特殊情形。

参考实现是 canonInterface.py 中的 build_lin_op_tree。当前仓库中 cppbackend.py 的实现是:先用一个栈做后序遍历(BFS 收集、逆序建树),通过 make_linC_from_linPy 把每个 Python LinOp 映射为 C++ cvxcore.LinOp,并用 linPy_to_linC 字典做记忆化,保证同一节点只构建一次;数值数据通过 set_linC_data 区分三种情况写入——切片(slice)、标量(走 set_dense_data)与矩阵(set_matrix_data,稀疏矩阵走 set_sparse_data)。

第 2 步:调用 build_matrix 获得稀疏问题数据

把 C++ LinOp 向量传入 cvxcore 的矩阵构建函数,返回一个 ProblemData 结构,其中包含问题的稀疏矩阵表示。目前问题数据以 COO 表示存放在 std::vector 中:

  • Python:可借助 cvxcore 的 getV / getI / getJ 函数高效地拆包为 NumPy 数组;
  • 其他语言:需要自行编写高效拆包逻辑(getV/getI/getJ 在 ProblemData.hpp 中实现,通过 SWIG 的 numpy.i typemap 暴露为返回连续一维数组的接口)。

第 3 步:在目标语言中复刻 canonInterface.py

完成前两步后,你实际上已经在目标语言里复刻出了 canonInterface.py,可以执行如下形式的核心调用:

V, I, J, b = canonInterface.get_problem_matrix(lin_expr_tree, var_offset_map)

其中 V, I, J 是问题矩阵 A 的 COO 表示,b 是常数向量。V, I, J 与 b 随后可直接按需传给你的求解器。


4. 源码级原理:从 LinOp 枚举到系数矩阵组装

4.1 算子类型枚举:26 种 LinOp

LinOp.hpp 定义了 operatortype 枚举,覆盖 CVXPY 规范化所需的全部仿射算子:

VARIABLE, PARAM, PROMOTE, MUL, RMUL, MUL_ELEM, DIV, SUM, NEG,
INDEX, TRANSPOSE, SUM_ENTRIES, TRACE, RESHAPE, DIAG_VEC, DIAG_MAT,
UPPER_TRI, CONV, HSTACK, VSTACK, SCALAR_CONST, DENSE_CONST,
SPARSE_CONST, NO_OP, KRON, KRON_R, KRON_L

LinOp 类的数据字段由算子类型决定(不执行字段合法性检查):shape_ 记录输出形状,args_ 保存子节点,slice_ 保存切片数据((start, end, step) 三元组),数值数据通过 set_dense_data(要求 Fortran 序连续 double 数组,内部用 Eigen::Map 映射为 Eigen::MatrixXd)或 set_sparse_data(从 COO 三元组构造 Eigen::SparseMatrix)注入。注意这两个方法的函数签名必须与 SWIG 接口文件中的 typemap 严格一致才能正常编译运行。

4.2 每个算子的系数提取:LinOpOperations

LinOpOperations.cpp 定义了与每个 LinOp 一一对应的系数提取函数(对应 18 个特殊情形的历史说法,实际当前枚举已扩展至 26 种算子,函数包括 get_variable_coeffs、get_const_coeffs、get_param_coeffs、get_mul_mat、get_transpose_mat、get_index_mat、get_reshape_mat、get_hstack_mat、get_kronr_mat、get_kronl_mat 等)。核心分发函数 get_node_coeffs(lin, arg_idx) 是一个大型 switch 语句,按 LinOp 类型路由到对应的系数提取函数,最终汇总成 Tensor——一个嵌套映射结构:参数 id → (变量 id → 系数矩阵列表)(见 Utils.hpp 的 typedef std::map<int, std::map<int, std::vector<Matrix>>> Tensor)。

4.3 矩阵组装与 OpenMP 并行

cvxcore.cpp 是矩阵构建算法的核心:

  • build_matrix(constraints, var_length, id_to_col, param_to_size, num_threads):逐约束处理,按约束形状的向量长度乘积(vecprod)计算垂直偏移;
  • process_constraint:对每个约束调用 lin_to_tensor 提取系数,把变量 id 通过 id_to_col 换算成水平列偏移(常量走 CONSTANT_ID = -1,其水平偏移即 var_length,落在最后一列);
  • add_matrix_to_vectors:用 Eigen 稀疏矩阵迭代器把系数块追加进 V/I/J 三元组向量;
  • 编译时若开启 _OPENMP,约束循环使用 #pragma omp parallel for 并行处理,线程数由 CVXPY 的 s.get_num_threads() 传入。

4.4 底层数据结构一览

类型 定义 说明
Matrix Eigen::SparseMatrix<double> cvxcore 的稀疏矩阵数据类型(当前 README 说明其存储可为广义 CSC/CSR,makeCompressed() 可转为标准格式)
Tensor map<int, map<int, vector<Matrix>>> 参数→(变量→系数矩阵块) 的三层嵌套
ProblemData 见 ProblemData.hpp 含 TensorV/TensorI/TensorJ 三个 map,按参数 id 与参数向量位置存放 COO 三元组
CONSTANT_ID -1 常数的统一伪 id(见 Utils.hpp)

5. SWIG 绑定与构建流程

cvxcore.i 是 SWIG 接口文件,负责把 cvxcore 暴露给多种常见编程语言(cvxcore.i):

  • %include "numpy.i" + import_array() 启用 NumPy C API;
  • 为 set_dense_data 应用 double* IN_FARRAY2 typemap(要求 Fortran 序二维数组);
  • 为 set_sparse_data 应用 INPLACE_ARRAY1 typemap;
  • 为 getV/getI/getJ 应用 ARGOUT_ARRAY1 typemap,使其在 Python 中返回连续一维 NumPy 数组;
  • 实例化 IntVector、IntIntMap、ConstLinOpVector 等 std 容器模板;
  • 声明 build_matrix 的两个重载入口(带 num_threads 与带 constr_offsets)。

生成的 Python 绑定是 cvxcore.py(SWIG 自动生成)。对 cvxcore 做修改后,当前仓库 README.md 记录了可见化流程:在 cvxpy/cvxcore 目录下运行 swig -Isrc -c++ -python python/cvxcore.i 重新生成绑定,再回到仓库根目录执行 pip install -e . 重新编译;或直接用仓库根目录的 bash rebuild_cvxcore.sh 一步完成。


6. 代码组织速览

目录/文件 内容
cvxpy/cvxcore/src/ cvxcore 的 C++ 源码:cvxcore.(c/h)pp 实现矩阵构建算法与 build_matrix 主入口;LinOp.hpp 定义 LinOp 类;LinOpOperations.(c/h)pp 实现各算子的系数提取;ProblemData.hpp 定义 build_matrix 的返回结构;Utils.hpp 定义 Matrix/Tensor 等基础类型
cvxpy/cvxcore/python/ 与 CVXPY 集成相关的 Python 代码:canonInterface.py 手工编写,负责 Python↔C++ LinOp 树转换与 get_problem_matrix;cppbackend.py 封装 SWIG 绑定调用;cvxcore.py 为 SWIG 自动生成的绑定
cvxpy/cvxcore/python/cvxcore.i SWIG 接口文件,向多种语言暴露函数与数据类型
tests 历史文档提及 test_linops.py(验证 LinOp 构建与表示正确性)与 huge_testman.py(在 EE364A 系列问题上做基准测试);这两个文件未保留在当前仓库树中,相关回归测试已由 cvxpy/tests/ 下的测试体系承接

7. 现状、限制与联系

  • 现状:cvxcore 目前仅服务于 CVXPY,官方在 README.md 中明确标注其为 deprecated,未来可能在不通知的情况下发生破坏 API 的变更;但作为 CVXPY 默认 'CPP' 规范化后端,其算法与数据结构仍是理解 CVXPY 矩阵规范化链路的钥匙。
  • 适用前提:'CPP' 后端依赖编译安装的 cvxcore Python 模块;若未安装,cppbackend.py 会抛出 ModuleNotFoundError 提示改用其他后端。
  • 联系:原文档署名联系邮箱为 {piq93,jackzhu,millerjp}@stanford.edu,对应项目早期(Stanford 团队)的贡献者;如需反馈问题,更合适的渠道是当前仓库的 issue 与 CONTRIBUTING.md 中说明的贡献流程。
登录后查看全文
cvxpy