Column generation solver for the minimum-cost multicommodity flow (MCF) problem with path-based and tree-based Dantzig-Wolfe decompositions.
Based on: S. Spoorendonk and B. Petersen, Tree-based formulation for the multi-commodity flow problem, 2025.
Given a directed graph
Let
Reduced cost of a path
Group commodities by source:
Reduced cost of a tree
Dantzig-Wolfe column generation. The restricted master starts with
demand/convexity rows and slacks sized by
The LB uses the LP dual objective
pricer.price is the source-level dispatcher; each per-source call
(PriceOneSource) is the A* inner body. Postponement is a
one-iter-ahead filter: a source that emits no negative-RC column is
skipped on the next non-final call. Flags are cleared whenever the
main iteration commits columns (clearPostponed, keeps the cursor so
partial pricing resumes), when pricing finally exhausts
(resetPostponed, rewinds to source 0), and after the warm-start
pass. filter_for_new_caps rewrites the flag vector wholesale after
a cut round: sources whose best-path arcs were touched by a new cap
are flipped in (postponed=0), all others are postponed until a
later sweep re-examines them.
Verbosity::Iteration prints one row per CG iteration:
| column | meaning |
|---|---|
It |
iteration number |
UB |
running min LP obj over MCF-feasible iters |
LB |
best Lagrangian/Farley bound so far |
LP_obj |
current LP objective (carries slack penalty while #slk > 0) |
#col, #row |
columns / rows in the LP right now |
#slk |
basic slack columns; non-zero means LP_obj is a penalty, not a bound |
+col, -col |
columns added / purged this iteration (*N = produced but not committed on gap exit) |
+cut, -cut |
capacity rows added / purged this iteration |
t_LP, t_PR, t_SP, t_Tot |
per-iter seconds (LP, pricing, separation, total) |
Requires C++23, CMake 3.20+, and zlib. HiGHS ships as a FetchContent dependency — no external install needed.
cmake -B build -DCMAKE_INSTALL_MESSAGE=LAZY
cmake --build build -j$(nproc)| Flag | Default | Effect |
|---|---|---|
-DMCFCG_USE_CUOPT=ON |
OFF | Enable the NVIDIA cuOpt GPU LP backend. Defaults to cuOpt's incremental delta C API (MCFCG_CUOPT_DELTA_API, below), which requires the fork. |
-DMCFCG_CUOPT_DELTA_API=OFF |
ON | Opt out of the delta C API for stock (non-fork) cuOpt: falls back to the rebuild-from-scratch path, which recreates the whole LP every CG iteration — a serious performance degradation. Default ON requires the fork's cuopt_c_delta.h (configure errors if absent). Only meaningful with -DMCFCG_USE_CUOPT=ON. |
-DMCFCG_USE_COPT=ON |
OFF | Enable the COPT LP backend (requires COPT installed, COPT_HOME set) |
-DMCFCG_USE_MOSEK=ON |
OFF | Enable the MOSEK CPU barrier LP backend (requires MOSEK, MOSEK_HOME set) |
-DMCFCG_NATIVE_ARCH=OFF |
ON | Disable -march=native. Keep ON for SIMD auto-vectorization of the hot cost[a] - mu[a] pricing loop; only turn OFF for portable binaries. |
The cuOpt backend mutates the restricted master incrementally (add/delete
columns and rows, re-solve), so by default it uses cuOpt's incremental delta C
API (MCFCG_CUOPT_DELTA_API=ON). Stock cuOpt has no such API; the default path
needs a cuOpt build that ships cuopt_c_delta.h — the
spoorendonk/cuopt fork (delta-api
branch). Build that fork first (configure errors out if the header is
missing), or reconfigure with -DMCFCG_CUOPT_DELTA_API=OFF to fall back to the
rebuild-from-scratch path on stock cuOpt — a serious performance degradation
(the whole LP is recreated every CG iteration), supported only as a
compatibility fallback. To use the fork, point the configure at it (an install
prefix or a source checkout both work):
# one combined build with both COPT and the cuOpt delta fork
cmake -B build -DCMAKE_INSTALL_MESSAGE=LAZY \
-DMCFCG_USE_COPT=ON \
-DMCFCG_USE_CUOPT=ON \
-DCUOPT_INCLUDE_DIR=/path/to/cuopt/cpp/include \
-DCUOPT_LIBRARY=/path/to/cuopt/cpp/build/libcuopt.so
cmake --build build -j$(nproc)libcuopt.so (and its librmm / rapids_logger deps) must be reachable by
the dynamic loader at run time. The build embeds the cuOpt library directory
as an RPATH — derived from CUOPT_LIBRARY — so build/mcfcg_cli, the tests,
and the tools run without exporting LD_LIBRARY_PATH. If you later move the
fork's build directory, either reconfigure (so the RPATH updates) or put the
new location on LD_LIBRARY_PATH:
export LD_LIBRARY_PATH=/path/to/cuopt/cpp/build:$LD_LIBRARY_PATHFour LP backends implement a common interface: HiGHS (default, FetchContent,
no license/GPU), cuOpt (GPU barrier), COPT (CPU/GPU barrier), and
MOSEK (CPU barrier). Select at run time with --solver.
Pinned barrier configuration. For fair cross-solver comparison every backend
runs the same regime: presolve off, crossover off, convergence tolerance 1e-4
(the MCF feasibility design target; see include/mcfcg/util/tolerances.h
BARRIER_TOL). Each solver prints a one-line provenance banner to stderr at
construction (captured in the CG / benchmark logs), e.g.:
[lp-config] backend=mosek method=barrier exec=CPU presolve=off crossover=off tol=0.0001 threads=auto(32)
threads=auto(N) reports the backend's effective thread count (N = hardware
concurrency when the backend auto-selects); exec is CPU or GPU. The banner
reports the steady-state pins — a stall-recovery certify solve transiently runs
crossover on the crossover-capable backends (HiGHS/COPT/MOSEK; see below).
HiGHS crossover-on-certify (stall recovery). HiGHS uses the HiPO
interior-point method with crossover off per iteration, so the bulk of CG is
fast. But a pure interior-point solution is not a vertex: on the path
formulation of large instances its central duals can fail to price an improving
column, and demand-row slacks settle at O(tol) > 0 rather than exactly 0, so the
CG loop cannot certify a slack-free upper bound. When the loop detects this stall
(pricing exhausted but not optimal, or an interior solve spuriously reporting
infeasible after cuts), it re-requests that one solve as a certify solve via
LPSolver::solve(certify=true) — HiGHS then runs crossover to round the interior
point to a vertex (discriminating duals, slacks exactly 0). COPT and MOSEK
likewise run crossover (basis identification) only on a certify solve; the cuOpt
GPU barrier has no crossover and treats certify as a no-op (the loop skips its
stall recovery there, via certify_runs_crossover()). This keeps "crossover off"
honest for the common case (every backend runs crossover-off steady-state, and
in practice only HiGHS ever stalls) while letting any crossover-capable backend
certify the cases that need a vertex — crossover fires only on the stalled
solves, not every iteration.
GTEST_BRIEF=1 ctest --test-dir build --output-on-failure --progress -j$(nproc)A single test:
./build/mcfcg_tests --gtest_filter='PathCGSingleSource.OptimalObjective'
./build/mcfcg_integration_tests --gtest_filter='GridCorrectness.Grid1'./build/mcfcg_cli <instance_path> [options]| Option | Default | Meaning |
|---|---|---|
| `--formulation path | tree` | path |
--max-iters N |
10000 | CG iteration cap |
--trips PATH |
auto | TNTP trips file (auto-detected from net path) |
--coef N |
auto | TNTP demand coefficient (auto per city) |
--threads N |
0 | Pricing threads (0 = hardware concurrency, 1 = serial) |
--batch-size N |
0 | Sources priced per batch (0 = all) |
--solver NAME |
highs | LP backend: highs, cuopt, copt, mosek |
--copt-gpu-mode N |
2 | COPT barrier execution: 0 = CPU, 1/2 = GPU (default 2). Only affects --solver copt. |
--verbose-solver |
off | Enable the LP backend's own log output |
--col-age-limit N |
5 | Purge columns after N idle iters (0 disables) |
--row-inactivity N |
5 | Purge cap rows after N idle iters (0 disables) |
--neg-rc-tol X |
-1e-3 | Reduced-cost acceptance threshold |
--strategy S |
pricer-light | pricer-light or pricer-heavy preset |
# CommaLab format
./build/mcfcg_cli data/commalab/grid/grid1
# TNTP transportation format (auto-detects trips file and demand coefficient)
./build/mcfcg_cli data/transportation/Winnipeg_net.tntp.gz
# Tree formulation
./build/mcfcg_cli data/commalab/grid/grid1 --formulation treeFour instance families from public sources:
| Family | Format | Source |
|---|---|---|
| Grid | CommaLab | UniPi MCF benchmark |
| Planar | CommaLab | UniPi MCF benchmark |
| Transportation | TNTP (gz) | TransportationNetworks |
| Intermodal | CommaLab (gz) | Lienkamp & Schiffer 2024 |
Download scripts are in scripts/.
MIT