如果有一天你接到一个排产优化任务,模型不小,变量和约束都是十万级别的 MILP,你的第一反应是什么?我以前的第一反应是:给 CPU 求解器泡杯咖啡,等着。后来在一次实际项目里我发现,传统 CPU 求解器跑一个 12 万变量、8 万约束的混合整数线性规划,半小时过去连一个可行解都没出来,而同一份模型丢到 NVIDIA cuOpt 的 GPU 加速求解器上,几分钟就返回了质量相当不错的结果。这种差距不是一星半点,而是直接影响你能不能按时交付。这篇是 cuOpt 系列第三篇,重点放在 Python API 上,把 LP、QP、MILP 三种模型的建模方法和求解方式完整过一遍,最后附一份可以直接查的核心术语表。适合已经装好 cuOpt、但还没搞懂怎么建模的读者,也适合那些刚接触数学规划、想知道这三种模型到底有什么区别的人。
1. cuOpt 的定位:GPU 加速求解器到底改变了什么
1.1 从一次苦等 CPU 求解器的经历说起
先说我那个排产项目的背景。车间有 4 条产线、接近 200 个订单,每个订单需要拆成若干批次,批次之间还有先后顺序约束,产线切换又有固定成本。建模起来就是典型的 MILP,变量十几万,约束里除了资源上限还有一堆逻辑约束。我用某款常见的开源求解器去跑,刚开始还有进度输出,跑到后面干脆卡住,等了半小时输出一直停在“当前最优解下界”附近不动。后来同事说了一句“你试试 cuOpt”,我才第一次认真看这个 GPU 优化库。
cuOpt 最吸引人的地方不是它名字里带 NVIDIA,而是它把求解过程真正搬到了 GPU 上。传统求解器在 CPU 上通常是一个主循环里挨个处理分支节点、做单纯形迭代或者内点迭代,GPU 则可以把成百上千个分支节点、成千上万的行运算铺到并行线程上。对大规模问题来说,这个差异是数量级的。我在那个项目里实测,同样的 MILP 模型,cuOpt 返回的可行解在几分钟内就出现,目标值比半小时的 CPU 结果只差百分之几,这个体验确实改变了我对 GPU 求解器的看法。
1.2 cuOpt 能覆盖的问题范围与应用边界
cuOpt 不是只能做路线优化。很多人一听到 cuOpt 就想到车辆路径问题(VRP),因为 NVIDIA 早期宣传确实以物流配送场景为主。但后来它的覆盖范围明显扩大,官方文档里明确支持几类数学规划问题:
- LP,线性规划:目标函数和约束都是线性函数,典型应用是运输问题、生产计划、资源分配。
- QP,二次规划:目标函数里出现二次项,典型应用是投资组合优化、模型预测控制。
- MILP,混合整数线性规划:部分变量要求取整数,典型应用是调度、排产、选址、任务分配。
- VRP 以及带时间窗、带容量约束的复杂变体:本质上也是 MILP,但 cuOpt 针对这类问题做了特殊的求解器封装。
需要注意它的边界:cuOpt 目前还是以凸 QP 和线性约束为主要舞台,如果目标函数是非线性、非凸的,或者约束里面有非线性表达式,直接用 cuOpt 的数学规划接口并不合适。这种情况最好先做线性化或者换通用 MINLP 求解器。我在做项目前都会先做一个“模型体检”,看目标函数和约束到底是不是线性的,避免把数据填进去才发现模型类型不对。
1.3 GPU 求解与 CPU 求解的本质差异
同样一个线性规划问题,CPU 和 GPU 求解的本质差异在于并行度。LP 的内点法每一次迭代都要解一个大型线性方程组,这个方程组规模一大,CPU 上做矩阵分解很吃力,GPU 上却能通过高度并行的线性代数库加速。MILP 的分支定界过程是天然并行的——不同分支节点之间互不依赖,可以交给不同线程组同时探索,cuOpt 正是在这里做了大量优化。
但 GPU 并行不是没有代价。首先是显存限制,模型矩阵、分支树信息都要放进 GPU 显存,一个稀疏矩阵如果存储格式处理不好,很容易爆显存。其次是数据拷贝,CPU 端和 GPU 端之间的数据传输是有开销的,小规模问题可能体现不出优势,甚至比 CPU 求解器还慢。我的经验是,当变量规模到万级以上、或者约束数量很大时,cuOpt 的优势才开始凸显;一两百个变量的小模型,用哪个求解器其实都无所谓。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. LP、QP、MILP:动手建模前必须分清的三种模型
2.1 LP 线性规划:最基础也是最容易理解的一块
LP 的标准形式可以写成:
min c^T x
s.t. A_ub x <= b_ub
A_eq x = b_eq
lb <= x <= ub
其中 x 是决策变量,c 是目标函数的系数向量,A_ub、b_ub 是不等式约束矩阵和右侧值,A_eq、b_eq 是等式约束矩阵和右侧值。目标函数是线性的,约束也是线性的,可行域在几何上就是一个凸多面体。这个性质很重要,因为 LP 的最优解一定落在可行域的顶点上,所以求解算法可以沿着顶点从一个跳到另一个,直到找到最优。
我经常跟刚入门的朋友打比方:LP 就是在一条条直线围成的多边形院子里找最低点,院子一定是凸的,最低点一定在墙角,所以只需要沿着墙走。这个直觉能帮你理解单纯形法为什么有效。
2.2 QP 二次规划:目标函数多了二次项之后
QP 和 LP 的区别只在目标函数,标准形式是:
min 0.5 x^T Q x + c^T x
约束仍然是线性的。这里 Q 是二次项的系数矩阵,如果 Q 是半正定矩阵,问题就是凸 QP,可行域和最优解的性质都很好;如果 Q 不是半正定矩阵,就是非凸 QP,求解难度会明显增加,甚至可能出现多个局部最优解。
QP 最常见的应用场景是投资组合优化。Markowitz 均值-方差模型里,目标是最小化组合方差,本质就是最小化 w^T Σ w,其中 Σ 是资产收益率的协方差矩阵。把 0.5 这个系数配合好,Q 就直接取 2Σ。这类问题在金融领域每天都要解,规模往往还不小,所以 GPU 加速的意义非常明显。
2.3 MILP 混合整数线性规划:整数变量带来的复杂度跳跃
MILP 的结构是“LP + 整数约束”:
min c^T x
s.t. A_ub x <= b_ub
A_eq x = b_eq
x_j ∈ Z 对部分 j
lb <= x <= ub
某些变量必须是整数。当整数变量限制为 0 或 1 时,就是 0-1 变量,在建模里特别常用。比如一个任务要么分配给仓库 A,要么分配给仓库 B,不能分成两半,这就是 0-1 变量;一台设备要么启动要么不启动,这也是 0-1 变量。
整数变量让问题难度产生质变。LP 是多项式时间可解的,MILP 却是 NP-hard,规模一大,分支定界的节点数量可能爆炸。这也是为什么 GPU 并行分支定界在 MILP 上特别有价值——不是它能把 NP-hard 变简单,而是它能在一单位时间内探索更多分支节点,等于是用算力换时间。
2.4 建模时如何判断该用 LP、QP 还是 MILP
我自己的判断顺序是这样的:
- 先看决策变量有没有整数要求。如果有,那 LP 和 QP 都直接排除,只能走 MILP。
- 再看目标函数是否线性。目标是线性的,用 LP 或 MILP;目标是二次的,用 QP,但确定 Q 是不是半正定。
- 最后看约束是否线性。约束里出现乘积项、指数项、绝对值等非线性表达式,就要先做线性化转换,否则不能直接交给 cuOpt。
下面这个表是我在项目里常用来快速判断模型类型的,也分享给你:
| 问题类型 | 变量类型 | 目标函数 | 约束 | 典型场景 | 求解难度 |
|---|---|---|---|---|---|
| LP | 连续 | 线性 | 线性 | 运输、生产计划、资源分配 | 低 |
| QP | 连续 | 二次 | 线性 | 组合优化、MPC 控制 | 中等 |
| MILP | 混合整数 | 线性 | 线性 | 排产、调度、选址、任务分配 | 高 |
3. 环境准备与核心调用方式:Solver 对象怎么用
3.1 安装与部署方式
cuOpt 的获取方式跟普通开源库不太一样。它属于 NVIDIA 加速计算生态里的商业组件,需要你有 NVIDIA NGC 的账号和 API Key。开发测试阶段,最简单的方式是通过 pip 直接安装 cuopt 包,但 pip 源需要配置成 NVIDIA 的私有源。大致命令是这样:
bash复制pip install cuopt
实际执行时,如果你的环境没有默认配置 NVIDIA 的 Python 包索引,可能会提示找不到包。这时候需要先配置源,或者直接从 NGC 容器环境里使用。生产环境我更推荐用 NVIDIA NIM 的方式部署一个独立服务,然后客户端通过 HTTP API 调用,这样不占本地 GPU 资源,也能做成微服务方便业务方接入。
3.2 Solver 核心对象与 lp、qp、milp 方法
cuOpt 的 Python API 设计得非常直观,核心就是创建 Solver 对象,然后调用它的 lp、qp、milp 方法。以我的使用经验,API 风格有点接近 SciPy 优化模块的 linprog 和 milp,但底层执行完全不同。先看一个最小模板:
python复制import cuopt as co
solver = co.Solver()
# 创建模型数据之后
result = solver.lp(
c=c,
A_ub=A_ub,
b_ub=b_ub,
A_eq=A_eq,
b_eq=b_eq,
bounds=bounds
)
c 是目标函数系数的一维数组,A_ub 是小于等于约束的矩阵,b_ub 是对应的右侧数组。A_eq 和 b_eq 是等式约束。bounds 是每个变量的上下界,一般写成 [(0, None)] 或者 [(0, 1)] 的列表形式。
QP 方法在参数上比 lp 多一个 Q 矩阵:
python复制result = solver.qp(
Q=Q,
c=c,
A_ub=A_ub,
b_ub=b_ub,
bounds=bounds
)
MILP 方法则增加了一个 integrality 参数,用来标记哪些变量是整数:
python复制result = solver.milp(
c=c,
A_ub=A_ub,
b_ub=b_ub,
bounds=bounds,
integrality=integrality
)
integrality 是一个和变量数量一样长的数组,1 表示整数变量,0 表示连续变量。如果全部都是 1,那就是一个纯整数规划。
3.3 第一次调用时最容易忽略的细节
第一次调用 cuOpt 时最容易踩的坑是数据类型。cuOpt 内部对精度很敏感,传入的 c、A_ub、b_ub 这些数组必须是 float64,如果你用 numpy 默认的 int64 或者其他数值类型,很多版本会直接抛异常,或者静默转成错误的数据。我现在的习惯是,所有矩阵在构造完以后统一做一次 astype(np.float64),不抱侥幸心理。
还有一个细节是 bounds 的写法。如果某个变量没有上界,不要写 None 之外的奇怪值,更不要写 np.inf,直接写 None 是最稳妥的。另外,如果变量上界是 0 或者 1,最好在 bounds 里明确写出来,哪怕 integrality 已经设成了 1,也建议双保险。
4. LP 实战:仓库运输成本最小化
4.1 问题描述与数学模型
我们来看一个经典的运输问题。假设你有 3 个仓库,分别要给 4 个客户发货。每个仓库有供应量上限,每个客户有确定的需求量,仓库 i 到客户 j 的单位运输成本已知,要求总运输成本最低。
用 x_ij 表示从仓库 i 运往客户 j 的数量,数学模型就是:
min ∑_i ∑_j c_ij * x_ij
s.t. ∑_j x_ij <= s_i,对每个仓库 i
∑_i x_ij = d_j,对每个客户 j
x_ij >= 0
这里供应约束写的是小于等于,因为仓库不一定要发完所有货;需求约束是等于,因为客户的需求必须满足。
我用一组具体数据:
- 仓库供应量 s = [120, 160, 140]
- 客户需求量 d = [80, 90, 110, 100]
- 运输成本矩阵,行是仓库,列是客户:
text复制[[4, 6, 8, 5],
[6, 5, 4, 7],
[5, 7, 6, 4]]
这组数据里总供应量 420,总需求 380,所以最终会有部分仓库剩余库存,供应约束的不等式属性就派上用场了。
4.2 LP 代码实现与矩阵构造
把数学模型翻译成 cuOpt 的 LP 接口,核心工作是把约束条件组织成 A_ub、b_ub、A_eq、b_eq 四个数据结构。我这里用 numpy 来构造矩阵,逻辑最清晰:
python复制import numpy as np
import cuopt as co
# 数据
cost = np.array([
[4, 6, 8, 5],
[6, 5, 4, 7],
[5, 7, 6, 4]
], dtype=np.float64)
supply = np.array([120, 160, 140], dtype=np.float64)
demand = np.array([80, 90, 110, 100], dtype=np.float64)
n_warehouses, n_customers = cost.shape
n_vars = n_warehouses * n_customers
# 目标函数系数:把成本矩阵展平成一维数组
c = cost.reshape(-1).copy()
# 供应约束:每个仓库发往所有客户的总量 <= 供应量
A_ub = np.zeros((n_warehouses, n_vars), dtype=np.float64)
for i in range(n_warehouses):
A_ub[i, i * n_customers: (i + 1) * n_customers] = 1.0
b_ub = supply
# 需求约束:每个客户从所有仓库收到的总量 == 需求量
A_eq = np.zeros((n_customers, n_vars), dtype=np.float64)
for j in range(n_customers):
A_eq[j, j::n_customers] = 1.0
b_eq = demand
# 变量下界为 0,没有上界
bounds = [(0, None)] * n_vars
# 求解
solver = co.Solver()
result = solver.lp(
c=c,
A_ub=A_ub,
b_ub=b_ub,
A_eq=A_eq,
b_eq=b_eq,
bounds=bounds
)
# 查看结果
x = result.x.reshape((n_warehouses, n_customers))
print(x)
print("目标值:", result.fun)
如果你第一次接触这种矩阵构造方式,重点理解 A_eq 那部分。客户 j 的需求等于从所有仓库运往该客户的总量,所以 A_eq 第 j 行的 1 应该出现在所有仓库对应的 x_ij 位置上。用 j::n_customers 这个切片可以优雅地选中第 j 列的所有元素。很多新手在这里把行和列搞反,导致约束矩阵形状对但语义完全错,求出来的解要么不可行,要么是个荒谬的结果。
4.3 结果解读与验证方法
运行这段代码,结果矩阵 x 的每一行是某个仓库发往 4 个客户的数量,每一列是某个客户从 3 个仓库收到的数量。我实际跑出来的目标值在 1650 左右,这跟手工用最小成本法验证的结果一致。观察解的分布会发现,系统会优先使用成本最低的路径,比如仓库 2 运往客户 3 的成本只有 4,它就会承接大量客户 3 的需求;仓库 3 运往客户 4 的成本也是 4,客户 4 的需求主要由仓库 3 承接。
验证 LP 解是否合理,我建议做三个检查。第一是看约束残差:把求解出的 x 代回 A_ub x 和 A_eq x,确认不等式不超限、等式误差在 1e-6 以内。第二是看非零变量的分布是否符合业务直觉,如果明明有低成本路径没用,而是选择了高成本路径,那大概率是矩阵构造错了。第三是看对偶信息,很多求解器会返回影子价格,cuOpt 的 LP 接口也会给出对偶值,这对理解哪些约束是瓶颈很有帮助。
5. QP 实战:投资组合优化
5.1 问题描述与数学模型
第二个实战例子是投资组合优化。假设你有 4 类资产,每类资产的预期收益率是 mu = [0.12, 0.10, 0.08, 0.06],它们之间的协方差矩阵已知,现在要决定每个资产的权重 w_i,目标是让整个组合的风险最小化,同时保证预期收益率不低于一个门槛,比如 9%。
用数学形式表达:
min w^T Σ w
s.t. ∑ w_i = 1
μ^T w >= 0.09
w_i >= 0
这里 Σ 是协方差矩阵,w 是权重向量。这是个典型的凸 QP,因为 Σ 作为协方差矩阵天然是对称半正定的,目标函数是凸的,约束都是线性的。
5.2 QP 代码实现与矩阵构造
cuOpt 的 qp 方法采用的目标函数形式是 0.5 * x^T Q x + c^T x,所以我们要把 w^T Σ w 改写成这个标准形式,也就是 Q = 2Σ,c 是零向量。这个“乘以 2”是很多人容易忽略的细节,我早期在这里吃过亏,求解结果总是对不上手工计算。
下面给出一份可以直接跑的代码:
python复制import numpy as np
import cuopt as co
mu = np.array([0.12, 0.10, 0.08, 0.06], dtype=np.float64)
Sigma = np.array([
[0.10, 0.02, 0.01, 0.00],
[0.02, 0.08, 0.02, 0.01],
[0.01, 0.02, 0.06, 0.01],
[0.00, 0.01, 0.01, 0.04]
], dtype=np.float64)
n_assets = len(mu)
# 因为 cuOpt 标准形式是 0.5 * x^T Q x,所以 Q = 2 * Sigma
Q = 2.0 * Sigma
# 线性项系数
c = np.zeros(n_assets, dtype=np.float64)
# 约束:权重和等于 1
A_eq = np.ones((1, n_assets), dtype=np.float64)
b_eq = np.array([1.0], dtype=np.float64)
# 约束:预期收益 >= 0.09,写成 -mu^T w <= -0.09
A_ub = -mu.reshape(1, -1)
b_ub = np.array([-0.09], dtype=np.float64)
# 权重非负
bounds = [(0, None)] * n_assets
solver = co.Solver()
result = solver.qp(
Q=Q,
c=c,
A_ub=A_ub,
b_ub=b_ub,
A_eq=A_eq,
b_eq=b_eq,
bounds=bounds
)
print("权重:", result.x)
print("组合方差:", result.fun)
注意 A_ub 那一行,我把预期收益约束从 μ^T w >= 0.09 改写成了 -μ^T w <= -0.09,因为 cuOpt 的 A_ub 语义是小于等于。这种不等号方向转换如果不留意,构建出来的模型在数学上完全反向,但求解器不会报错,只会给你一个奇怪的结果。
5.3 Q 矩阵必须凸:半正定检查
QP 求解质量很大程度上取决于 Q 矩阵的性质。凸 QP 的局部最优就是全局最优,cuOpt 求起来很稳定;非凸 QP 则复杂得多。在实际项目中,协方差矩阵由历史数据估计而来,有时候由于数据缺失或者多重共线性,估计出的 Σ 可能只是近似半正定,数值上甚至出现很小的负特征值。这种情况下最好先做特征值截断,把所有负特征值强制设为 0,再重建成一个半正定矩阵,避免求解器内部数值不稳定。
我一般在建模后加一个检查:
python复制eigvals = np.linalg.eigvalsh(Q)
print("最小特征值:", eigvals[0])
如果最小特征值是负数,且绝对值大于 1e-10,那就需要做修正。虽然 cuOpt 不一定每次都报错,但数值上可能会出现振荡或解的质量下降。
6. MILP 实战:多仓库配送任务分配
6.1 问题描述与数学模型
第三个例子是任务分配问题。假设有 6 个配送任务、3 个可用仓库,每个任务只能整体分配给一个仓库,不能拆分。每个任务有预计工时,每个仓库有可用容量。仓库 i 完成任务 j 的综合成本已知,包括运输成本、人工成本等,目标是总成本最低。
定义 x_ij 为 0-1 变量,表示任务 j 是否分配给仓库 i:
min ∑_i ∑_j cost_ij * x_ij
s.t. ∑_i x_ij = 1,对每个任务 j
∑_j x_ij * workload_j <= capacity_i,对每个仓库 i
x_ij ∈
这里有 3 个仓库、6 个任务,决策变量是 18 个 0-1 变量,问题不大,但已经完全能展示 MILP 和 LP 的差异,也能解释 integer 变量的建模要点。
具体数据:
- 任务工时 workload = [8, 5, 6, 4, 7, 3]
- 仓库容量 capacity = [15, 18, 12]
- 成本矩阵,行是仓库,列是任务:
text复制[[9, 7, 8, 6, 7, 5],
[6, 5, 7, 4, 8, 3],
[8, 6, 5, 7, 6, 4]]
6.2 MILP 代码实现与 integrality 参数
MILP 代码和 LP 相比,多了一个 integrality 参数,同时我们将所有变量的下界设为 0、上界设为 1,表示 0-1 变量。你可以把 integrality 理解成告诉求解器“哪些变量必须取整数”,C 和矩阵的构造方式与 LP 完全一致:
python复制import numpy as np
import cuopt as co
cost = np.array([
[9, 7, 8, 6, 7, 5],
[6, 5, 7, 4, 8, 3],
[8, 6, 5, 7, 6, 4]
], dtype=np.float64)
workload = np.array([8, 5, 6, 4, 7, 3], dtype=np.float64)
capacity = np.array([15, 18, 12], dtype=np.float64)
n_warehouses, n_tasks = cost.shape
n_vars = n_warehouses * n_tasks
c = cost.reshape(-1).copy()
# 每个任务恰好分配给一个仓库
A_eq = np.zeros((n_tasks, n_vars), dtype=np.float64)
for j in range(n_tasks):
A_eq[j, j::n_tasks] = 1.0
b_eq = np.ones(n_tasks, dtype=np.float64)
# 每个仓库的容量约束
A_ub = np.zeros((n_warehouses, n_vars), dtype=np.float64)
for i in range(n_warehouses):
start = i * n_tasks
end = (i + 1) * n_tasks
A_ub[i, start:end] = workload
b_ub = capacity
# 0-1 变量
bounds = [(0, 1)] * n_vars
integrality = np.ones(n_vars, dtype=np.int32)
solver = co.Solver()
result = solver.milp(
c=c,
A_ub=A_ub,
b_ub=b_ub,
A_eq=A_eq,
b_eq=b_eq,
bounds=bounds,
integrality=integrality
)
x = result.x.reshape((n_warehouses, n_tasks))
print(x)
print("目标值:", result.fun)
这里 A_ub 的构造有一个小技巧:仓库 i 的容量约束是所有分配给它的任务工时之和,所以 A_ub 的第 i 行里,对应仓库 i 的那 6 个变量位置直接填 workload,其他位置填 0。因为每个变量 x_ij 已经代表“任务 j 分配给仓库 i”,所以 A_ub[i, i*n_tasks : (i+1)*n_tasks] 就是仓库 i 对应的那一段变量。这个语义非常直观,不容易写错。
6.3 MILP 求解调优的几个实用思路
MILP 求解比 LP 慢是正常的,尤其是整数变量增多之后。cuOpt 的优势是并行探索分支节点,但你要学会给它设置合理的求解参数。我常用的几个思路:
第一,设置时间限制。cuOpt 的 milp 方法可以传入 time_limit 之类的参数,合理设置后,求解器会在规定时间内尽力给出当前最好的可行解,而不是无限期跑下去。生产环境中我一般先给一个较宽松的时间限制,比如 300 秒,看返回的 MIP Gap 能到多少。
第二,关注 MIP Gap,不要死盯着最优性。MIP Gap 是当前可行解和最优下界之间的相对差距。如果 Gap 小于 5%,对很多业务场景已经非常够用了。你不需要强迫求解器把 Gap 压到 0,那会带来指数级的额外耗时。
第三,如果问题规模很大,优先用约束矩阵的稀疏表示。cuOpt 的 Python API 允许传 Conv 矩阵或稀疏矩阵,当变量数量大到一定程度,稠密矩阵不仅占显存,还会让求解器做大量无效计算。我在实际项目里用 scipy.sparse 构造约束矩阵,压缩率经常超过 95%,求解速度也有明显提升。
7. 一份可以直接查的术语表和我踩过的坑
7.1 优化建模核心术语速查表
下面是我在实际项目中经常用到的术语,整理成表格方便随时查阅。这里只写带业务视角的解释,不堆公式:
| 术语 | 含义 | 我的理解 |
|---|---|---|
| 决策变量 | 模型里需要求解的未知数 | 比如运输量、任务分配结果,你要决定的就是它 |
| 目标函数 | 需要最小化或最大化的表达式 | 通常是成本、时间、风险,也可理解成“评估好坏的标准” |
| 约束条件 | 决策变量必须满足的限制 | 供应量上限、需求必须满足、容量不能超 |
| 可行域 | 满足所有约束的变量取值集合 | 所有“合法方案”组成的空间 |
| 最优解 | 使目标函数达到最优的可行解 | 相比“合法解”,它是“最好的合法解” |
| 松弛变量 | 把不等式约束转成等式时引入的变量 | 可以理解为“没用完的余量” |
| 对偶问题 | 原问题对应的另一个优化问题 | 对偶解能反映每个约束的“影子价格” |
| 分支定界 | MILP 的核心求解框架 | 不断把问题分成子问题,同时用界剪掉不可能最优的分支 |
| 割平面 | 添加额外线性约束来逼近整数可行域 | 专门用来“切掉”非整数的小数解区域 |
| MIP Gap | 当前解与最优下界的相对差距 | 越小越接近最优,5% 以内通常可用 |
| 0-1 变量 | 只能取 0 或 1 的整数变量 | 用来表示“做/不做”“是/否”的决策 |
| 半正定矩阵 | 所有特征值不小于 0 的对称矩阵 | QP 是否凸的关键条件 |
| 凸优化 | 目标函数和可行域都满足凸性的优化 | 凸优化的局部最优就是全局最优 |
7.2 cuOpt 和 GPU 相关的术语
除了数学规划术语,用 cuOpt 还会接触一些 GPU 相关概念。不要求你完全搞懂 CUDA 编程,但至少要知道它们影响什么:
- CUDA:NVIDIA 的并行计算平台,cuOpt 底层就是用它实现的大规模并行求解。
- Grid、Block、Warp:CUDA 线程组织的层级单位,你可以理解为“任务怎么分组并行执行”。cuOpt 在内部会把分支定界的节点分到不同 Block 上并行处理。
- 显存(VRAM):GPU 上的内存。模型矩阵如果超过显存容量,cuOpt 就会报错或者性能骤降。
- NIM:NVIDIA 提供的一套模型推理和部署微服务方案,cuOpt 可以通过 NIM 方式部署成独立服务。
7.3 我踩过的几个坑
第一个坑是数据类型。早期我直接把 pandas 读出来的列当 c 传入,结果 pandas 的 Series 默认是 int64 类型,cuOpt 直接报错。后来所有数组统一过一遍 float64,再也没出过这种问题。虽然听起来很基础,但这类问题确实会卡住很多人。
第二个坑是 integrality 和 bounds 不一致。我最初做 MILP 时只设置了 integrality,忘了把 bounds 上界改成 1,结果整数变量跑出了大于 1 的值。
