From 6087b1249b7894e85fa902c15e773dafa4504dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Fri, 30 Jan 2026 12:42:01 +0100 Subject: [PATCH 01/23] Switch workflow over to 25.12 --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b9bb860..e36f46e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,12 +28,12 @@ jobs: python -m venv venvs/cu12 bash -c "source venvs/cu12/bin/activate && \ pip install --upgrade pip -qq && \ - pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu12==25.10.* cuda-toolkit[cudart,nvjitlink]==12.9.* -qq && + pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu12==25.12.* -qq && deactivate" python -m venv venvs/cu13 bash -c "source venvs/cu13/bin/activate && \ pip install --upgrade pip -qq && \ - pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu13==25.10.* cuda-toolkit[cudart,nvjitlink]==13.0.* -qq && + pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu13==25.12.* -qq && deactivate" # Get GAMS From dfbe5519cd74b35e8ee6dd8ccb9eb5c5f6717d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Fri, 30 Jan 2026 13:08:33 +0100 Subject: [PATCH 02/23] Resolve: Error getting solution bound for MIP without integer variables #4 --- gmscuopt.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index 113395b..112e83b 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -238,11 +238,16 @@ main (int argc, char *argv[]) goto DONE; } + int has_integer_vars = 0; + for (int j=0; j Date: Fri, 30 Jan 2026 13:48:38 +0100 Subject: [PATCH 03/23] Move integer variables indicator to outer scope to fix pipeline --- gmscuopt.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index 112e83b..19f66d0 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -125,6 +125,7 @@ main (int argc, char *argv[]) cuopt_float_t* upper_bounds=NULL; char* constraint_sense=NULL; char* variable_types=NULL; + int has_integer_vars = 0; // Create solver settings status = cuOptCreateSolverSettings(&settings); @@ -238,8 +239,6 @@ main (int argc, char *argv[]) goto DONE; } - int has_integer_vars = 0; - for (int j=0; j Date: Fri, 30 Jan 2026 16:26:58 +0100 Subject: [PATCH 04/23] Add new num_gpus option that only applies to specific LPs with usermap=0, default=1, low=1, high=2, hc=1, group=2 (lp). --- assets/optcuopt.def | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/optcuopt.def b/assets/optcuopt.def index 8a5d009..b7a402c 100644 --- a/assets/optcuopt.def +++ b/assets/optcuopt.def @@ -2,6 +2,7 @@ * optcuopt.def * num_cpu_threads integer 0 -1 -1 maxint 1 1 Controls the number of CPU threads used in the LP and MIP solvers (default GAMS Threads) +num_gpus integer 0 1 1 2 1 2 Controls the number of GPUs to use for the solve. This setting is only relevant for LP problems that uses concurrent mode and supports up to 2 GPUs at the moment. Using this mode will run PDLP and barrier in parallel on different GPUs to avoid sharing single GPU resources. presolve boolean 0 0 1 1 Controls whether presolve is enabled. Presolve can reduce problem size and improve solve time. Enabled by default for MIP, disabled by default for LP. dual_postsolve boolean 0 0 1 2 Controls whether dual postsolve is enabled. Disabling dual postsolve can improve solve time at the expense of not having access to the dual solution. Enabled by default for LP when presolve is enabled. This is not relevant for MIP problems time_limit integer 0 maxint 0 maxint 1 1 Controls the time limit in seconds after which the solver will stop and return the current solution (default GAMS ResLim) From 399353f31a556089d960abdf31ba7be877944572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Fri, 30 Jan 2026 16:50:34 +0100 Subject: [PATCH 05/23] Add rudimentary functions for writing the trace file (not connected yet) [skip ci] --- gmscuopt.c | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index 19f66d0..ce4b70b 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "gmomcc.h" #include "gevmcc.h" #include "optcc.h" @@ -23,8 +24,16 @@ printOut (gevHandle_t gev, char *fmt, ...) return rc; } -int -main (int argc, char *argv[]) +static char flnmiptrace[256]; +static char MIPTraceID[32] = ""; +static FILE *fpMIPTrace = NULL; +static int MIPTraceSeq = 0; + +int mipTraceOpen(const char *fname, const char *solverID, const int optFileNum, const char *inputName); +int mipTraceClose(); +int mipTraceLine(char seriesID, double node, int giveint, double seconds, double bestint, double bestbnd); + +int main(int argc, char *argv[]) { gmoHandle_t gmo=NULL; gevHandle_t gev=NULL; @@ -493,6 +502,74 @@ main (int argc, char *argv[]) } /* main */ + + +int mipTraceOpen(const char *fname, const char *solverID, const int optFileNum, const char *inputName) +{ + if (NULL != fpMIPTrace) + return 1; /* already open: error */ + + strcpy(flnmiptrace, fname); + fpMIPTrace = fopen(flnmiptrace, "w"); + if (NULL == fpMIPTrace) + return 3; + + strncpy(MIPTraceID, solverID, sizeof(MIPTraceID) - 1); + MIPTraceID[sizeof(MIPTraceID) - 1] = '\0'; + MIPTraceSeq = 1; + fprintf(fpMIPTrace, "* miptrace file %s: ID = %s.%d Instance = %s\n", flnmiptrace, MIPTraceID, optFileNum, inputName); + fprintf(fpMIPTrace, "* fields are lineNum, seriesID, node, seconds, bestFound, bestBound\n"); + fflush(fpMIPTrace); + return 0; +} /* mipTraceOpen */ + +int mipTraceClose() +{ + int rc; + if (NULL == fpMIPTrace) + return 2; /* already closed: error */ + fprintf(fpMIPTrace, "* miptrace file %s closed\n", flnmiptrace); + rc = fclose(fpMIPTrace); + fpMIPTrace = NULL; + return (0 == rc) ? 0 : 1; +} /* mipTraceClose */ + +int mipTraceLine(char seriesID, double node, int giveint, + double seconds, double bestint, double bestbnd) +{ + int rc; + + if (NULL == fpMIPTrace) + return -1; /* not open: error */ + + if (giveint) + { + if (bestbnd == GMS_SV_NA) + rc = fprintf(fpMIPTrace, "%d, %c, %g, %.15g, %.15g, na\n", MIPTraceSeq, + isalnum(seriesID) ? seriesID : 'X', + node, seconds, bestint); + else + rc = fprintf(fpMIPTrace, "%d, %c, %g, %.15g, %.15g, %.15g\n", MIPTraceSeq, + isalnum(seriesID) ? seriesID : 'X', + node, seconds, bestint, bestbnd); + } + else + { + if (bestbnd == GMS_SV_NA) + rc = fprintf(fpMIPTrace, "%d, %c, %g, %.15g, na, na\n", MIPTraceSeq, + isalnum(seriesID) ? seriesID : 'X', + node, seconds); + else + rc = fprintf(fpMIPTrace, "%d, %c, %g, %.15g, na, %g\n", MIPTraceSeq, + isalnum(seriesID) ? seriesID : 'X', + node, seconds, bestbnd); + } + fflush(fpMIPTrace); + MIPTraceSeq++; + + return rc; +} /* mipTraceLine */ + #if 0 t program for cuOpt linear programming solver */ From 04f054db031d5f0a226c821a1a76952797d2f328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Fri, 30 Jan 2026 17:19:17 +0100 Subject: [PATCH 06/23] Groundwork for miptrace --- assets/optcuopt.def | 1 + gmscuopt.c | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/assets/optcuopt.def b/assets/optcuopt.def index b7a402c..0d91c9f 100644 --- a/assets/optcuopt.def +++ b/assets/optcuopt.def @@ -55,6 +55,7 @@ absolute_gap_tolerance double 0 0.0001 0 maxdouble 1 2 Controls the absolute gap relative_gap_tolerance double 0 0.0001 0 maxdouble 1 2 Controls the relative gap tolerance used in PDLP's duality gap check primal_infeasible_tolerance double 0 1e-08 0 maxdouble 0 2 Unknown dual_infeasible_tolerance double 0 1e-08 0 maxdouble 0 2 Unknown +miptrace string 0 "" 1 1 filename of MIP trace file mip_heuristics_only boolean 0 0 1 3 Controls if only the GPU heuristics should be run mip_scaling boolean 0 1 1 3 Controls if scaling should be applied to the MIP problem mip_absolute_tolerance double 0 0.0001 0 maxdouble 1 3 Controls the MIP absolute tolerance diff --git a/gmscuopt.c b/gmscuopt.c index ce4b70b..22170f8 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -135,6 +135,7 @@ int main(int argc, char *argv[]) char* constraint_sense=NULL; char* variable_types=NULL; int has_integer_vars = 0; + flnmiptrace[0] = '\0'; // Create solver settings status = cuOptCreateSolverSettings(&settings); @@ -143,6 +144,16 @@ int main(int argc, char *argv[]) goto DONE; } + /* + mip_callback_context_t context = {0}; + context.n_variables = num_variables; + status = cuOptSetMIPGetSolutionCallback(settings, mip_get_solution_callback, &context); + if (status != CUOPT_SUCCESS) { + printOut(gev, "Error setting get-solution callback\n", status); + goto DONE; + } + */ + // Set solver parameters with GAMS options if (gevGetIntOpt(gev, gevThreadsRaw) != 0) { status = cuOptSetIntegerParameter(settings, CUOPT_NUM_CPU_THREADS, gevGetIntOpt(gev, gevThreadsRaw)); @@ -202,7 +213,13 @@ int main(int argc, char *argv[]) } } } - + + if(optGetDefinedStr(opt, "miptrace")) { + optGetStrStr(opt, "miptrace", flnmiptrace); + char sval2[256]; + mipTraceOpen(flnmiptrace, "cuOpt", gmoOptFile(gmo), gmoNameInput(gmo, sval2)); + } + if (!optGetDefinedStr(opt, "prob_read")) { constraint_matrix_row_offsets = malloc((num_constraints+1)*sizeof(cuopt_int_t)); constraint_matrix_column_indices = malloc(nnz*sizeof(cuopt_int_t)); @@ -493,6 +510,9 @@ int main(int argc, char *argv[]) free(constraint_sense); free(variable_types); + if(fpMIPTrace) + mipTraceClose(); + GAMSDONE: gmoFree(&gmo); gevFree(&gev); From 8d8d9982c5e93efbdd4a66d00496a634327bfa4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Fri, 30 Jan 2026 17:36:18 +0100 Subject: [PATCH 07/23] Attempt providing both x86_64 and arm64 builds --- .github/workflows/main-arm64.yml | 161 ++++++++++++++++++ .../workflows/{main.yml => main-x86_64.yml} | 18 +- README.md | 11 +- examples/trnsport_cuopt.ipynb | 4 +- examples/trnsport_cuopt_cu13.ipynb | 4 +- 5 files changed, 180 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/main-arm64.yml rename .github/workflows/{main.yml => main-x86_64.yml} (94%) diff --git a/.github/workflows/main-arm64.yml b/.github/workflows/main-arm64.yml new file mode 100644 index 0000000..003f69c --- /dev/null +++ b/.github/workflows/main-arm64.yml @@ -0,0 +1,161 @@ +name: Build cuOpt link for GAMS (ARM64) + +on: + push: +# branches: [main] + tags: + - '*' + pull_request: + +jobs: + build-link: + runs-on: ubuntu-24.04-arm + container: + image: python:3.12 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install dependencies + run: | + apt-get update && apt-get install -y patchelf curl unzip zip gcc + + # Get CUDA runtimes via pip + - name: Set up python virtual environments with NVIDIA dependencies for CUDA 12 and CUDA 13 (respectively) + run: | + mkdir -p venvs + python -m venv venvs/cu12 + bash -c "source venvs/cu12/bin/activate && \ + pip install --upgrade pip -qq && \ + pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu12==25.12.* -qq && + deactivate" + python -m venv venvs/cu13 + bash -c "source venvs/cu13/bin/activate && \ + pip install --upgrade pip -qq && \ + pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu13==25.12.* -qq && + deactivate" + + # Get GAMS (ARM64 version) + - name: Download and extract latest GAMS distribution + run: | + curl https://d37drm4t2jghv5.cloudfront.net/distributions/latest/linux/llinux_arm64_sfx.exe --output linux_arm64_sfx.exe + unzip -q linux_arm64_sfx.exe + mv gams*_linux_arm64_sfx gamsdist + rm llinux_arm64_sfx.exe + + # Build link + - name: Compile GAMS/cuOpt-link binary "gmscuopt.out" for CUDA 12 and then for CUDA 13 + run: | + export GAMSCAPI="gamsdist/apifiles/C/api" + export CUOPT="venvs/cu12/lib/python3.12/site-packages/libcuopt" + export JITLINK="venvs/cu12/lib/python3.12/site-packages/nvidia/nvjitlink/lib" + export CUOPT_VERSION="`cat "$CUOPT/VERSION"`" + export CUOPT_HASH="`cat "$CUOPT/GIT_COMMIT"`" + gcc -Wall gmscuopt.c -o gmscuopt-cu12.out \ + -DCUOPT_VERSION=\"$CUOPT_VERSION\" -DCUOPT_HASH=\"$CUOPT_HASH\" \ + -I $GAMSCAPI $GAMSCAPI/gmomcc.c $GAMSCAPI/optcc.c $GAMSCAPI/gevmcc.c \ + -I $CUOPT/include $JITLINK/libnvJitLink.so.12 -L $CUOPT/lib64 -lcuopt + patchelf --set-rpath \$ORIGIN gmscuopt-cu12.out + export CUOPT="venvs/cu13/lib/python3.12/site-packages/libcuopt" + export JITLINK="venvs/cu13/lib/python3.12/site-packages/nvidia/cu13/lib" + export CUOPT_VERSION="`cat "$CUOPT/VERSION"`" + export CUOPT_HASH="`cat "$CUOPT/GIT_COMMIT"`" + gcc -Wall gmscuopt.c -o gmscuopt-cu13.out \ + -DCUOPT_VERSION=\"$CUOPT_VERSION\" -DCUOPT_HASH=\"$CUOPT_HASH\" \ + -I $GAMSCAPI $GAMSCAPI/gmomcc.c $GAMSCAPI/optcc.c $GAMSCAPI/gevmcc.c \ + -I $CUOPT/include $JITLINK/libnvJitLink.so.13 -L $CUOPT/lib64 -lcuopt + patchelf --set-rpath \$ORIGIN gmscuopt-cu13.out + + # Collect dependencies for link and runtime convenience archive + - name: Prepare release artifact and runtime bundle + run: | + mkdir release-cu12 + cp gmscuopt-cu12.out release-cu12/gmscuopt.out + cp assets/* release-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/libcuopt/lib64/libcuopt.so release-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/libcuopt/lib64/libmps_parser.so release-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/libcuopt_cu12.libs/libgomp-*.so.1.0.0 release-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/libcuopt_cu12.libs/libtbb-*.so.2 release-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/libcuopt_cu12.libs/libtbbmalloc-*.so.2 release-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/rapids_logger/lib64/librapids_logger.so release-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/librmm/lib64/librmm.so release-cu12/ + mkdir runtime-cu12 + cp venvs/cu12/lib/python3.12/site-packages/nvidia/cu12/lib/libcudss.so.0 runtime-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/nvidia/cu12/lib/libcudss_mtlayer_gomp.so.0 runtime-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/nvidia/cusolver/lib/libcusolver.so.11 runtime-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/nvidia/cublas/lib/libcublas.so.12 runtime-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/nvidia/cublas/lib/libcublasLt.so.12 runtime-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/nvidia/nvjitlink/lib/libnvJitLink.so.12 runtime-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/nvidia/curand/lib/libcurand.so.10 runtime-cu12/ + cp venvs/cu12/lib/python3.12/site-packages/nvidia/cusparse/lib/libcusparse.so.12 runtime-cu12/ + mkdir release-cu13 + cp gmscuopt-cu13.out release-cu13/gmscuopt.out + cp assets/* release-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/libcuopt/lib64/libcuopt.so release-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/libcuopt/lib64/libmps_parser.so release-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/libcuopt_cu13.libs/libgomp-*.so.1.0.0 release-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/libcuopt_cu13.libs/libtbb-*.so.2 release-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/libcuopt_cu13.libs/libtbbmalloc-*.so.2 release-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/rapids_logger/lib64/librapids_logger.so release-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/librmm/lib64/librmm.so release-cu13/ + mkdir runtime-cu13 + cp venvs/cu13/lib/python3.12/site-packages/nvidia/cu13/lib/libcudss.so.0 runtime-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/nvidia/cu13/lib/libcudss_mtlayer_gomp.so.0 runtime-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/nvidia/cu13/lib/libnvJitLink.so.13 runtime-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/nvidia/cu13/lib/libcublas.so.13 runtime-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/nvidia/cu13/lib/libcublasLt.so.13 runtime-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/nvidia/cu13/lib/libcurand.so.10 runtime-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/nvidia/cu13/lib/libcusolver.so.12 runtime-cu13/ + cp venvs/cu13/lib/python3.12/site-packages/nvidia/cu13/lib/libcusparse.so.12 runtime-cu13/ + + # Upload artifacts + - name: Upload CUDA 12 link artifact to GitHub Actions (always) + uses: actions/upload-artifact@v4 + with: + name: cuopt-link-cu12-arm64 + path: release-cu12/ + + - name: Upload CUDA 12 runtime artifact to GitHub Actions (always) + uses: actions/upload-artifact@v4 + with: + name: cu12-runtime-arm64 + path: runtime-cu12/ + + - name: Upload CUDA 13 link artifact to GitHub Actions (always) + uses: actions/upload-artifact@v4 + with: + name: cuopt-link-cu13-arm64 + path: release-cu13/ + + - name: Upload CUDA 13 runtime artifact to GitHub Actions (always) + uses: actions/upload-artifact@v4 + with: + name: cu13-runtime-arm64 + path: runtime-cu13/ + + # Zip Files + - name: Create zip archive (only on tag push) + if: startsWith(github.ref, 'refs/tags/') + run: | + cd release-cu12 + zip -r ../cuopt-link-release-cu12-arm64.zip . + cd ../runtime-cu12 + zip -r ../cu12-runtime-arm64.zip . + cd ../release-cu13 + zip -r ../cuopt-link-release-cu13-arm64.zip . + cd ../runtime-cu13 + zip -r ../cu13-runtime-arm64.zip . + + # Create new release with archives + - name: Create GitHub Release (only on tag push) + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + files: | + cuopt-link-release-cu12-arm64.zip + cu12-runtime-arm64.zip + cuopt-link-release-cu13-arm64.zip + cu13-runtime-arm64.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/main.yml b/.github/workflows/main-x86_64.yml similarity index 94% rename from .github/workflows/main.yml rename to .github/workflows/main-x86_64.yml index e36f46e..f93f36e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main-x86_64.yml @@ -1,4 +1,4 @@ -name: Build cuOpt link for GAMS +name: Build cuOpt link for GAMS (x86_64) on: push: @@ -139,13 +139,13 @@ jobs: if: startsWith(github.ref, 'refs/tags/') run: | cd release-cu12 - zip -r ../cuopt-link-release-cu12.zip . + zip -r ../cuopt-link-release-cu12-x86_64.zip . cd ../runtime-cu12 - zip -r ../cu12-runtime.zip . + zip -r ../cu12-runtime-x86_64.zip . cd ../release-cu13 - zip -r ../cuopt-link-release-cu13.zip . + zip -r ../cuopt-link-release-cu13-x86_64.zip . cd ../runtime-cu13 - zip -r ../cu13-runtime.zip . + zip -r ../cu13-runtime-x86_64.zip . # Create new release with archives - name: Create GitHub Release (only on tag push) @@ -153,9 +153,9 @@ jobs: uses: softprops/action-gh-release@v2 with: files: | - cuopt-link-release-cu12.zip - cu12-runtime.zip - cuopt-link-release-cu13.zip - cu13-runtime.zip + cuopt-link-release-cu12-x86_64.zip + cu12-runtime-x86_64.zip + cuopt-link-release-cu13-x86_64.zip + cu13-runtime-x86_64.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 76a450c..4924155 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ You can get more details and tips by reading the blog post ["GPU-Accelerated Opt ## Requirements - **Operating System:** Linux, Windows 11 through WSL2 +- **CPU architecture:** x86_64, arm64 - **GAMS:** Version 49 or newer - **GAMSPy:** Version 1.12.1 or newer - **NVIDIA GPU:** Volta architecture or better @@ -17,11 +18,11 @@ You can get more details and tips by reading the blog post ["GPU-Accelerated Opt ## Getting started / installation - Make sure [CUDA runtime](https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64) is installed -- Download and unpack `cuopt-link-release-cu12.zip` or `cuopt-link-release-cu13.zip` (for CUDA 12 and 13 respectively) from the [releases page](https://github.com/GAMS-dev/cuoptlink-builder/releases): - - Unpack the contents of `cuopt-link-release-cu*.zip` into your GAMS system directory. For GAMSPy, you can find out your system directory by running `gamspy show base`. So for example you can run `unzip -o cuopt-link-release-cu*.zip -d $(gamspy show base)`. +- Download and unpack `cuopt-link-release-cu12.zip` or `cuopt-link-release-cu13-{x86_64,arm64}.zip` (for CUDA 12 and 13 respectively) from the [releases page](https://github.com/GAMS-dev/cuoptlink-builder/releases): + - Unpack the contents of `cuopt-link-release-cu*-{x86_64,arm64}.zip` into your GAMS system directory. For GAMSPy, you can find out your system directory by running `gamspy show base`. So for example you can run `unzip -o cuopt-link-release-cu*.zip -d $(gamspy show base)`. - **Caution:** This will overwrite any existing `gamsconfig.yaml` file in that directory. The contained `gamsconfig.yaml` contains a `solverConfig` section to make cuOpt available to GAMS. -The neccessary files from the CUDA 12 or 13 runtime can also be downloaded as convenient archive `cu12-runtime.zip` or `cu13-runtime.zip` from the [releases page](https://github.com/GAMS-dev/cuoptlink-builder/releases). +The neccessary files from the CUDA 12 or 13 runtime can also be downloaded as convenient archive `cu12-runtime-{x86_64,arm64}.zip` or `cu13-runtime-{x86_64,arm64}.zip` from the [releases page](https://github.com/GAMS-dev/cuoptlink-builder/releases). ## Test the setup @@ -33,5 +34,5 @@ gams trnsport lp cuopt ## Examples -- [examples/trnsport_cuopt.ipynb](examples/trnsport_cuopt.ipynb) for CUDA 12 -- [examples/trnsport_cuopt.ipynb](examples/trnsport_cuopt_cu13.ipynb) for CUDA 13 +- [examples/trnsport_cuopt.ipynb](examples/trnsport_cuopt.ipynb) for CUDA 12 on x86_64 +- [examples/trnsport_cuopt.ipynb](examples/trnsport_cuopt_cu13.ipynb) for CUDA 13 on x86_64 diff --git a/examples/trnsport_cuopt.ipynb b/examples/trnsport_cuopt.ipynb index 60cee67..f0bf1c6 100644 --- a/examples/trnsport_cuopt.ipynb +++ b/examples/trnsport_cuopt.ipynb @@ -40,8 +40,8 @@ "import sys\n", "!pip install -q gamspy\n", "gams_base_path = subprocess.check_output([sys.executable, '-m', 'gamspy', 'show', 'base']).decode('utf-8').strip()\n", - "!wget -nc -nv --show-progress -q \"https://github.com/GAMS-dev/cuoptlink-builder/releases/latest/download/cu12-runtime.zip\"\n", - "!wget -nc -nv --show-progress -q \"https://github.com/GAMS-dev/cuoptlink-builder/releases/latest/download/cuopt-link-release-cu12.zip\"\n", + "!wget -nc -nv --show-progress -q \"https://github.com/GAMS-dev/cuoptlink-builder/releases/latest/download/cu12-runtime-x86_64.zip\"\n", + "!wget -nc -nv --show-progress -q \"https://github.com/GAMS-dev/cuoptlink-builder/releases/latest/download/cuopt-link-release-cu12-x86_64.zip\"\n", "subprocess.run(f\"unzip -q -o cu12-runtime.zip -d {gams_base_path}\", shell=True, check=True)\n", "subprocess.run(f\"unzip -q -o cuopt-link-release-cu12.zip -d {gams_base_path}\", shell=True, check=True)" ] diff --git a/examples/trnsport_cuopt_cu13.ipynb b/examples/trnsport_cuopt_cu13.ipynb index 9cb4648..713b64e 100644 --- a/examples/trnsport_cuopt_cu13.ipynb +++ b/examples/trnsport_cuopt_cu13.ipynb @@ -32,8 +32,8 @@ "import sys\n", "!pip install -q gamspy\n", "gams_base_path = subprocess.check_output([sys.executable, '-m', 'gamspy', 'show', 'base']).decode('utf-8').strip()\n", - "!wget -nc -nv --show-progress -q \"https://github.com/GAMS-dev/cuoptlink-builder/releases/latest/download/cu13-runtime.zip\"\n", - "!wget -nc -nv --show-progress -q \"https://github.com/GAMS-dev/cuoptlink-builder/releases/latest/download/cuopt-link-release-cu13.zip\"\n", + "!wget -nc -nv --show-progress -q \"https://github.com/GAMS-dev/cuoptlink-builder/releases/latest/download/cu13-runtime-x86_64.zip\"\n", + "!wget -nc -nv --show-progress -q \"https://github.com/GAMS-dev/cuoptlink-builder/releases/latest/download/cuopt-link-release-cu13-x86_64.zip\"\n", "subprocess.run(f\"unzip -q -o cu13-runtime.zip -d {gams_base_path}\", shell=True, check=True)\n", "subprocess.run(f\"unzip -q -o cuopt-link-release-cu13.zip -d {gams_base_path}\", shell=True, check=True)" ] From 14ff113de938099a6f7dd7dd68ecf2ee063a9937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Fri, 30 Jan 2026 22:35:58 +0100 Subject: [PATCH 08/23] Fix typo --- .github/workflows/main-arm64.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main-arm64.yml b/.github/workflows/main-arm64.yml index 003f69c..f4b6896 100644 --- a/.github/workflows/main-arm64.yml +++ b/.github/workflows/main-arm64.yml @@ -39,10 +39,10 @@ jobs: # Get GAMS (ARM64 version) - name: Download and extract latest GAMS distribution run: | - curl https://d37drm4t2jghv5.cloudfront.net/distributions/latest/linux/llinux_arm64_sfx.exe --output linux_arm64_sfx.exe + curl https://d37drm4t2jghv5.cloudfront.net/distributions/latest/linux/linux_arm64_sfx.exe --output linux_arm64_sfx.exe unzip -q linux_arm64_sfx.exe mv gams*_linux_arm64_sfx gamsdist - rm llinux_arm64_sfx.exe + rm linux_arm64_sfx.exe # Build link - name: Compile GAMS/cuOpt-link binary "gmscuopt.out" for CUDA 12 and then for CUDA 13 From ae0a8fc1383bfaf6fddde9533cf29c587d228964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Mon, 2 Feb 2026 14:51:28 +0100 Subject: [PATCH 09/23] Use snake_case for trace functions and pass through state. Rudimentary link trace line and cuopt callback func --- gmscuopt.c | 114 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 46 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index 22170f8..2249e00 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -24,14 +24,24 @@ printOut (gevHandle_t gev, char *fmt, ...) return rc; } -static char flnmiptrace[256]; -static char MIPTraceID[32] = ""; -static FILE *fpMIPTrace = NULL; -static int MIPTraceSeq = 0; +static char fln_mip_trace[256]; +static char mip_trace_id[32] = ""; +static FILE *fp_mip_trace = NULL; +static int mip_trace_seq = 0; -int mipTraceOpen(const char *fname, const char *solverID, const int optFileNum, const char *inputName); -int mipTraceClose(); -int mipTraceLine(char seriesID, double node, int giveint, double seconds, double bestint, double bestbnd); +static int mip_trace_open(const char *fname, const char *solverID, const int optFileNum, const char *inputName); +static int mip_trace_close(); +static int mip_trace_line(char seriesID, double node, int giveint, double seconds, double bestint, double bestbnd); + +typedef struct sl_state_s +{ + gevHandle_t gev; + double tstart; + int nvars; +} sl_state_t; + +static void mip_get_solution_cb(const cuopt_float_t *solution, const cuopt_float_t *objective_value, + const cuopt_float_t *solution_bound, void *user_data); int main(int argc, char *argv[]) { @@ -135,7 +145,7 @@ int main(int argc, char *argv[]) char* constraint_sense=NULL; char* variable_types=NULL; int has_integer_vars = 0; - flnmiptrace[0] = '\0'; + fln_mip_trace[0] = '\0'; // Create solver settings status = cuOptCreateSolverSettings(&settings); @@ -144,10 +154,14 @@ int main(int argc, char *argv[]) goto DONE; } + sl_state_t context; + context.gev = gev; + context.tstart = gevTimeJNow(gev); + context.nvars = num_variables; + mip_trace_line('I', 0, 0, 0, 0, 0); + /* - mip_callback_context_t context = {0}; - context.n_variables = num_variables; - status = cuOptSetMIPGetSolutionCallback(settings, mip_get_solution_callback, &context); + status = cuOptSetMIPGetSolutionCallback(settings, mip_get_solution_cb, &context); if (status != CUOPT_SUCCESS) { printOut(gev, "Error setting get-solution callback\n", status); goto DONE; @@ -155,7 +169,8 @@ int main(int argc, char *argv[]) */ // Set solver parameters with GAMS options - if (gevGetIntOpt(gev, gevThreadsRaw) != 0) { + if (gevGetIntOpt(gev, gevThreadsRaw) != 0) + { status = cuOptSetIntegerParameter(settings, CUOPT_NUM_CPU_THREADS, gevGetIntOpt(gev, gevThreadsRaw)); if (status != CUOPT_SUCCESS) { printOut(gev, "Error setting number of CPU threads: %d\n", status); @@ -214,10 +229,10 @@ int main(int argc, char *argv[]) } } - if(optGetDefinedStr(opt, "miptrace")) { - optGetStrStr(opt, "miptrace", flnmiptrace); + if(optGetDefinedStr(opt, "mip_trace_")) { + optGetStrStr(opt, "mip_trace_", fln_mip_trace); char sval2[256]; - mipTraceOpen(flnmiptrace, "cuOpt", gmoOptFile(gmo), gmoNameInput(gmo, sval2)); + mip_trace_open(fln_mip_trace, "cuOpt", gmoOptFile(gmo), gmoNameInput(gmo, sval2)); } if (!optGetDefinedStr(opt, "prob_read")) { @@ -510,8 +525,8 @@ int main(int argc, char *argv[]) free(constraint_sense); free(variable_types); - if(fpMIPTrace) - mipTraceClose(); + if(fp_mip_trace) + mip_trace_close(); GAMSDONE: gmoFree(&gmo); @@ -522,73 +537,80 @@ int main(int argc, char *argv[]) } /* main */ - - -int mipTraceOpen(const char *fname, const char *solverID, const int optFileNum, const char *inputName) +int mip_trace_open(const char *fname, const char *solverID, const int optFileNum, const char *inputName) { - if (NULL != fpMIPTrace) + if (NULL != fp_mip_trace) return 1; /* already open: error */ - strcpy(flnmiptrace, fname); - fpMIPTrace = fopen(flnmiptrace, "w"); - if (NULL == fpMIPTrace) + strcpy(fln_mip_trace, fname); + fp_mip_trace = fopen(fln_mip_trace, "w"); + if (NULL == fp_mip_trace) return 3; - strncpy(MIPTraceID, solverID, sizeof(MIPTraceID) - 1); - MIPTraceID[sizeof(MIPTraceID) - 1] = '\0'; - MIPTraceSeq = 1; - fprintf(fpMIPTrace, "* miptrace file %s: ID = %s.%d Instance = %s\n", flnmiptrace, MIPTraceID, optFileNum, inputName); - fprintf(fpMIPTrace, "* fields are lineNum, seriesID, node, seconds, bestFound, bestBound\n"); - fflush(fpMIPTrace); + strncpy(mip_trace_id, solverID, sizeof(mip_trace_id) - 1); + mip_trace_id[sizeof(mip_trace_id) - 1] = '\0'; + mip_trace_seq = 1; + fprintf(fp_mip_trace, "* mip_trace_ file %s: ID = %s.%d Instance = %s\n", fln_mip_trace, mip_trace_id, optFileNum, inputName); + fprintf(fp_mip_trace, "* fields are lineNum, seriesID, node, seconds, bestFound, bestBound\n"); + fflush(fp_mip_trace); return 0; -} /* mipTraceOpen */ +} /* mip_trace_open */ -int mipTraceClose() +int mip_trace_close() { int rc; - if (NULL == fpMIPTrace) + if (NULL == fp_mip_trace) return 2; /* already closed: error */ - fprintf(fpMIPTrace, "* miptrace file %s closed\n", flnmiptrace); - rc = fclose(fpMIPTrace); - fpMIPTrace = NULL; + fprintf(fp_mip_trace, "* mip_trace_ file %s closed\n", fln_mip_trace); + rc = fclose(fp_mip_trace); + fp_mip_trace = NULL; return (0 == rc) ? 0 : 1; -} /* mipTraceClose */ +} /* mip_trace_close */ -int mipTraceLine(char seriesID, double node, int giveint, +int mip_trace_line(char seriesID, double node, int giveint, double seconds, double bestint, double bestbnd) { int rc; - if (NULL == fpMIPTrace) + if (NULL == fp_mip_trace) return -1; /* not open: error */ if (giveint) { if (bestbnd == GMS_SV_NA) - rc = fprintf(fpMIPTrace, "%d, %c, %g, %.15g, %.15g, na\n", MIPTraceSeq, + rc = fprintf(fp_mip_trace, "%d, %c, %g, %.15g, %.15g, na\n", mip_trace_seq, isalnum(seriesID) ? seriesID : 'X', node, seconds, bestint); else - rc = fprintf(fpMIPTrace, "%d, %c, %g, %.15g, %.15g, %.15g\n", MIPTraceSeq, + rc = fprintf(fp_mip_trace, "%d, %c, %g, %.15g, %.15g, %.15g\n", mip_trace_seq, isalnum(seriesID) ? seriesID : 'X', node, seconds, bestint, bestbnd); } else { if (bestbnd == GMS_SV_NA) - rc = fprintf(fpMIPTrace, "%d, %c, %g, %.15g, na, na\n", MIPTraceSeq, + rc = fprintf(fp_mip_trace, "%d, %c, %g, %.15g, na, na\n", mip_trace_seq, isalnum(seriesID) ? seriesID : 'X', node, seconds); else - rc = fprintf(fpMIPTrace, "%d, %c, %g, %.15g, na, %g\n", MIPTraceSeq, + rc = fprintf(fp_mip_trace, "%d, %c, %g, %.15g, na, %g\n", mip_trace_seq, isalnum(seriesID) ? seriesID : 'X', node, seconds, bestbnd); } - fflush(fpMIPTrace); - MIPTraceSeq++; + fflush(fp_mip_trace); + mip_trace_seq++; return rc; -} /* mipTraceLine */ +} /* mip_trace_line */ + +static void mip_get_solution_cb(const cuopt_float_t *solution, const cuopt_float_t *objective_value, + const cuopt_float_t *solution_bound, void *user_data){ + sl_state_t *state = (sl_state_t *)user_data; + double elapsed = (gevTimeJNow(state->gev) - state->tstart) * 3600.0 * 24.0; + double obj = *objective_value; + double bnd = *solution_bound; + mip_trace_line('I', 0, 0, elapsed, obj, bnd); +} #if 0 t program for cuOpt linear programming solver From cc0aae3821ba5fafad915345add50129ccc22922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Mon, 2 Feb 2026 14:56:01 +0100 Subject: [PATCH 10/23] Mark start of search more explicitly --- gmscuopt.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index 2249e00..675a5ca 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -158,7 +158,7 @@ int main(int argc, char *argv[]) context.gev = gev; context.tstart = gevTimeJNow(gev); context.nvars = num_variables; - mip_trace_line('I', 0, 0, 0, 0, 0); + mip_trace_line('S', 0, 0, 0, GMS_SV_NA, GMS_SV_NA); /* status = cuOptSetMIPGetSolutionCallback(settings, mip_get_solution_cb, &context); @@ -609,7 +609,7 @@ static void mip_get_solution_cb(const cuopt_float_t *solution, const cuopt_float double elapsed = (gevTimeJNow(state->gev) - state->tstart) * 3600.0 * 24.0; double obj = *objective_value; double bnd = *solution_bound; - mip_trace_line('I', 0, 0, elapsed, obj, bnd); + mip_trace_line('I', 0, 1, elapsed, obj, bnd); } #if 0 From 1f20c2ce2adb9e74c3937eb6a3549e639fad8b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Mon, 2 Feb 2026 15:02:48 +0100 Subject: [PATCH 11/23] Only deal with setting up context, initial row, and linkup of mip trace facility when it's active (trace file handle is open). --- gmscuopt.c | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index 675a5ca..245e76e 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -146,6 +146,7 @@ int main(int argc, char *argv[]) char* variable_types=NULL; int has_integer_vars = 0; fln_mip_trace[0] = '\0'; + sl_state_t context; // Create solver settings status = cuOptCreateSolverSettings(&settings); @@ -154,19 +155,20 @@ int main(int argc, char *argv[]) goto DONE; } - sl_state_t context; - context.gev = gev; - context.tstart = gevTimeJNow(gev); - context.nvars = num_variables; - mip_trace_line('S', 0, 0, 0, GMS_SV_NA, GMS_SV_NA); - - /* - status = cuOptSetMIPGetSolutionCallback(settings, mip_get_solution_cb, &context); - if (status != CUOPT_SUCCESS) { - printOut(gev, "Error setting get-solution callback\n", status); - goto DONE; + if (fp_mip_trace) + { + context.gev = gev; + context.tstart = gevTimeJNow(gev); + context.nvars = num_variables; + mip_trace_line('S', 0, 0, 0, GMS_SV_NA, GMS_SV_NA); + /* + status = cuOptSetMIPGetSolutionCallback(settings, mip_get_solution_cb, &context); + if (status != CUOPT_SUCCESS) { + printOut(gev, "Error setting get-solution callback\n", status); + goto DONE; + } + */ } - */ // Set solver parameters with GAMS options if (gevGetIntOpt(gev, gevThreadsRaw) != 0) From dde474843aca65599088910e7ec77ea63c360c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Wed, 4 Feb 2026 12:01:17 +0100 Subject: [PATCH 12/23] Store solution bound and write end trace line [skip ci] --- gmscuopt.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index 245e76e..dff88a6 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -395,7 +395,7 @@ int main(int argc, char *argv[]) // Get solution information cuopt_float_t solution_time; cuopt_int_t termination_status; - cuopt_float_t objective_value; + cuopt_float_t objective_value, solution_bound; status = cuOptGetTerminationStatus(solution, &termination_status); if (status != CUOPT_SUCCESS) { @@ -460,12 +460,12 @@ int main(int argc, char *argv[]) gmoSetHeadnTail(gmo, gmoHobjval, objective_value); if (gmoModelType(gmo) == gmoProc_mip && has_integer_vars) { - status = cuOptGetSolutionBound(solution, &objective_value); + status = cuOptGetSolutionBound(solution, &solution_bound); if (status != CUOPT_SUCCESS) { printOut(gev, "Error getting solution bound: %d\n", status); goto DONE; } - gmoSetHeadnTail(gmo, gmoTmipbest, objective_value); + gmoSetHeadnTail(gmo, gmoTmipbest, solution_bound); } status = cuOptGetPrimalSolution(solution, objective_coefficients); // reuse n-vector @@ -475,6 +475,12 @@ int main(int argc, char *argv[]) } gmoSetVarL(gmo, objective_coefficients); + if(fp_mip_trace) + { + double total_elapsed = (gevTimeJNow(gev) - context.tstart) * 3600.0 * 24.0; + mip_trace_line('E', 0, 1, total_elapsed, objective_value, solution_bound); + } + int presolve, dual_postsolve; cuOptGetIntegerParameter(settings, "presolve", &presolve); cuOptGetIntegerParameter(settings, "dual_postsolve", &dual_postsolve); From d45151c3c0a02a56b88e5fb0736622b694670950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Wed, 4 Feb 2026 17:56:56 +0100 Subject: [PATCH 13/23] Allow workflow dispatch for both workflows --- .github/workflows/main-arm64.yml | 1 + .github/workflows/main-x86_64.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/main-arm64.yml b/.github/workflows/main-arm64.yml index f4b6896..0486404 100644 --- a/.github/workflows/main-arm64.yml +++ b/.github/workflows/main-arm64.yml @@ -6,6 +6,7 @@ on: tags: - '*' pull_request: + workflow_dispatch: jobs: build-link: diff --git a/.github/workflows/main-x86_64.yml b/.github/workflows/main-x86_64.yml index f93f36e..4a157a1 100644 --- a/.github/workflows/main-x86_64.yml +++ b/.github/workflows/main-x86_64.yml @@ -6,6 +6,7 @@ on: tags: - '*' # Run only when a new tag is pushed pull_request: + workflow_dispatch: jobs: build-link: From f1580ad8ff5af88661afaafa237c778fde43807f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Thu, 5 Feb 2026 09:55:08 +0100 Subject: [PATCH 14/23] Temporarily use nightly build of cuOpt 26.2.0a131 to test cuOptSetMIPGetSolutionCallback (and enable its use in the link) --- .github/workflows/main-arm64.yml | 4 ++-- .github/workflows/main-x86_64.yml | 4 ++-- gmscuopt.c | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main-arm64.yml b/.github/workflows/main-arm64.yml index 0486404..367a5d1 100644 --- a/.github/workflows/main-arm64.yml +++ b/.github/workflows/main-arm64.yml @@ -29,12 +29,12 @@ jobs: python -m venv venvs/cu12 bash -c "source venvs/cu12/bin/activate && \ pip install --upgrade pip -qq && \ - pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu12==25.12.* -qq && + pip install --pre --extra-index-url=https://pypi.nvidia.com --extra-index-url=https://pypi.anaconda.org/rapidsai-wheels-nightly/simple/ cuopt-cu12==26.2.0a131 -qq && deactivate" python -m venv venvs/cu13 bash -c "source venvs/cu13/bin/activate && \ pip install --upgrade pip -qq && \ - pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu13==25.12.* -qq && + pip install --pre --extra-index-url=https://pypi.nvidia.com --extra-index-url=https://pypi.anaconda.org/rapidsai-wheels-nightly/simple/ cuopt-cu13==26.2.0a131 -qq && deactivate" # Get GAMS (ARM64 version) diff --git a/.github/workflows/main-x86_64.yml b/.github/workflows/main-x86_64.yml index 4a157a1..3196a1f 100644 --- a/.github/workflows/main-x86_64.yml +++ b/.github/workflows/main-x86_64.yml @@ -29,12 +29,12 @@ jobs: python -m venv venvs/cu12 bash -c "source venvs/cu12/bin/activate && \ pip install --upgrade pip -qq && \ - pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu12==25.12.* -qq && + pip install --pre --extra-index-url=https://pypi.nvidia.com --extra-index-url=https://pypi.anaconda.org/rapidsai-wheels-nightly/simple/ cuopt-cu12==26.2.0a131 -qq && deactivate" python -m venv venvs/cu13 bash -c "source venvs/cu13/bin/activate && \ pip install --upgrade pip -qq && \ - pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu13==25.12.* -qq && + pip install --pre --extra-index-url=https://pypi.nvidia.com --extra-index-url=https://pypi.anaconda.org/rapidsai-wheels-nightly/simple/ cuopt-cu13==26.2.0a131 -qq && deactivate" # Get GAMS diff --git a/gmscuopt.c b/gmscuopt.c index dff88a6..96dc4f9 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -161,13 +161,11 @@ int main(int argc, char *argv[]) context.tstart = gevTimeJNow(gev); context.nvars = num_variables; mip_trace_line('S', 0, 0, 0, GMS_SV_NA, GMS_SV_NA); - /* status = cuOptSetMIPGetSolutionCallback(settings, mip_get_solution_cb, &context); if (status != CUOPT_SUCCESS) { printOut(gev, "Error setting get-solution callback\n", status); goto DONE; } - */ } // Set solver parameters with GAMS options From 0bec5484eec0ec6e156bea533e5e18bac2872ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Thu, 5 Feb 2026 10:10:01 +0100 Subject: [PATCH 15/23] Fix option querying key for miptrace [skip ci] --- gmscuopt.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index 96dc4f9..b60fa08 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -229,8 +229,8 @@ int main(int argc, char *argv[]) } } - if(optGetDefinedStr(opt, "mip_trace_")) { - optGetStrStr(opt, "mip_trace_", fln_mip_trace); + if(optGetDefinedStr(opt, "miptrace")) { + optGetStrStr(opt, "miptrace", fln_mip_trace); char sval2[256]; mip_trace_open(fln_mip_trace, "cuOpt", gmoOptFile(gmo), gmoNameInput(gmo, sval2)); } From 88252ec95484165f2e31f616611861de33caf1b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Thu, 5 Feb 2026 10:49:40 +0100 Subject: [PATCH 16/23] Fixed miptrace facility (make it properly treat string options, move file handle to earlier spot) --- gmscuopt.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index b60fa08..3c3a7f3 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -155,6 +155,17 @@ int main(int argc, char *argv[]) goto DONE; } + if (optGetDefinedStr(opt, "miptrace")) + { + optGetStrStr(opt, "miptrace", fln_mip_trace); + char sval2[256]; + if (mip_trace_open(fln_mip_trace, "cuOpt", gmoOptFile(gmo), gmoNameInput(gmo, sval2))) + { + printOut(gev, "Error opening trace file >%s%s<: %d\n", optname, status); goto DONE; } - } else { + } else if(data_type == optDataDouble) { status = cuOptSetFloatParameter(settings, optname, dval); if (status != CUOPT_SUCCESS) { printOut(gev, "Error setting float option >%s<: %d\n", optname, status); @@ -229,12 +240,6 @@ int main(int argc, char *argv[]) } } - if(optGetDefinedStr(opt, "miptrace")) { - optGetStrStr(opt, "miptrace", fln_mip_trace); - char sval2[256]; - mip_trace_open(fln_mip_trace, "cuOpt", gmoOptFile(gmo), gmoNameInput(gmo, sval2)); - } - if (!optGetDefinedStr(opt, "prob_read")) { constraint_matrix_row_offsets = malloc((num_constraints+1)*sizeof(cuopt_int_t)); constraint_matrix_column_indices = malloc(nnz*sizeof(cuopt_int_t)); From 2940a409ada4fc90e71eb10e6ba304f0f9cd7de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Thu, 5 Feb 2026 11:07:39 +0100 Subject: [PATCH 17/23] Also treat pinf/ninf as na in trace line --- gmscuopt.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index 3c3a7f3..d69a081 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -578,6 +578,8 @@ int mip_trace_close() return (0 == rc) ? 0 : 1; } /* mip_trace_close */ +#define bnd_na(x) x == GMS_SV_NA || x == HUGE_VAL || x == -HUGE_VAL + int mip_trace_line(char seriesID, double node, int giveint, double seconds, double bestint, double bestbnd) { @@ -588,7 +590,7 @@ int mip_trace_line(char seriesID, double node, int giveint, if (giveint) { - if (bestbnd == GMS_SV_NA) + if (bnd_na(best_bnd)) rc = fprintf(fp_mip_trace, "%d, %c, %g, %.15g, %.15g, na\n", mip_trace_seq, isalnum(seriesID) ? seriesID : 'X', node, seconds, bestint); @@ -599,7 +601,7 @@ int mip_trace_line(char seriesID, double node, int giveint, } else { - if (bestbnd == GMS_SV_NA) + if (bnd_na(bestbnd)) rc = fprintf(fp_mip_trace, "%d, %c, %g, %.15g, na, na\n", mip_trace_seq, isalnum(seriesID) ? seriesID : 'X', node, seconds); From ac04b40f9c254ddf4257cb6bc1b2b6b0ba221375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Thu, 5 Feb 2026 11:16:44 +0100 Subject: [PATCH 18/23] Fix typo --- gmscuopt.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gmscuopt.c b/gmscuopt.c index d69a081..64fec12 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -590,7 +590,7 @@ int mip_trace_line(char seriesID, double node, int giveint, if (giveint) { - if (bnd_na(best_bnd)) + if (bnd_na(bestbnd)) rc = fprintf(fp_mip_trace, "%d, %c, %g, %.15g, %.15g, na\n", mip_trace_seq, isalnum(seriesID) ? seriesID : 'X', node, seconds, bestint); From 5f70a536ab9dfff2a03f2ac19ed9f50823f8bc80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Thu, 5 Feb 2026 11:54:59 +0100 Subject: [PATCH 19/23] Adding mip start and show warnings when using miptrace or mipstart for non-MIP models --- gmscuopt.c | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index 64fec12..723d7b1 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -147,6 +147,8 @@ int main(int argc, char *argv[]) int has_integer_vars = 0; fln_mip_trace[0] = '\0'; sl_state_t context; + int mipstart = 0; + cuopt_float_t *initial_levels = NULL; // Create solver settings status = cuOptCreateSolverSettings(&settings); @@ -157,13 +159,18 @@ int main(int argc, char *argv[]) if (optGetDefinedStr(opt, "miptrace")) { - optGetStrStr(opt, "miptrace", fln_mip_trace); - char sval2[256]; - if (mip_trace_open(fln_mip_trace, "cuOpt", gmoOptFile(gmo), gmoNameInput(gmo, sval2))) + if (gmoModelType(gmo) == gmoProc_mip) { - printOut(gev, "Error opening trace file >%s%s Date: Thu, 5 Feb 2026 12:03:00 +0100 Subject: [PATCH 20/23] Must adapt option definition of course for mipstart --- assets/optcuopt.def | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/optcuopt.def b/assets/optcuopt.def index 0d91c9f..07f82b9 100644 --- a/assets/optcuopt.def +++ b/assets/optcuopt.def @@ -55,7 +55,8 @@ absolute_gap_tolerance double 0 0.0001 0 maxdouble 1 2 Controls the absolute gap relative_gap_tolerance double 0 0.0001 0 maxdouble 1 2 Controls the relative gap tolerance used in PDLP's duality gap check primal_infeasible_tolerance double 0 1e-08 0 maxdouble 0 2 Unknown dual_infeasible_tolerance double 0 1e-08 0 maxdouble 0 2 Unknown -miptrace string 0 "" 1 1 filename of MIP trace file +miptrace string 0 "" 1 3 filename of MIP trace file +mipstart boolean -1 0 1 3 whether it should be tried to use the initial variable levels as initial MIP solution mip_heuristics_only boolean 0 0 1 3 Controls if only the GPU heuristics should be run mip_scaling boolean 0 1 1 3 Controls if scaling should be applied to the MIP problem mip_absolute_tolerance double 0 0.0001 0 maxdouble 1 3 Controls the MIP absolute tolerance From 153d25e2675414922dba9717d861520f71086d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Thu, 5 Feb 2026 12:10:05 +0100 Subject: [PATCH 21/23] Add missing newlines --- gmscuopt.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index 723d7b1..438aeb2 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -170,7 +170,7 @@ int main(int argc, char *argv[]) } } else - printOut(gev, "WARNING: Enabling a MIP trace is only allowed for model type MIP!"); + printOut(gev, "WARNING: Enabling a MIP trace is only allowed for model type MIP!\n"); } if (fp_mip_trace) @@ -189,7 +189,7 @@ int main(int argc, char *argv[]) mipstart = optGetIntStr(opt, "mipstart"); if(mipstart && gmoModelType(gmo) != gmoProc_mip) { - printOut(gev, "WARNING: Setting a MIP start is only allowed for model type MIP!"); + printOut(gev, "WARNING: Setting a MIP start is only allowed for model type MIP!\n"); mipstart = 0; } From 90a6d1eb092a0730153ac392d828910c36255fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Fri, 6 Feb 2026 13:58:07 +0100 Subject: [PATCH 22/23] Also take pass initial primal and dual solutions over from GAMS to cuOpt for LPs when PDLP mode is active (or concurrent) --- .gitignore | 7 ++++--- gmscuopt.c | 25 +++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 1987f1a..6dd0487 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ +.vscode/ /venv/* /release/* -trnsport.gms -trnsport.lst -gmscuopt.out +*.gms +*.lst +*.out cuopt.opt /examples/cu*runtime.zip /examples/cuopt-link-release-cu*.zip \ No newline at end of file diff --git a/gmscuopt.c b/gmscuopt.c index 438aeb2..9403a14 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -50,7 +50,7 @@ int main(int argc, char *argv[]) optHandle_t opt=NULL; cuopt_int_t status; char msg[256], filename[256]; - + if (!gevCreate(&gev,msg,sizeof(msg))) { printf("Error creating GEV: %s\n", msg); goto GAMSDONE; @@ -157,6 +157,7 @@ int main(int argc, char *argv[]) goto DONE; } + // Setup miptrace facility if option is enabled if (optGetDefinedStr(opt, "miptrace")) { if (gmoModelType(gmo) == gmoProc_mip) @@ -172,7 +173,6 @@ int main(int argc, char *argv[]) else printOut(gev, "WARNING: Enabling a MIP trace is only allowed for model type MIP!\n"); } - if (fp_mip_trace) { context.gev = gev; @@ -186,6 +186,7 @@ int main(int argc, char *argv[]) } } + // Check for MIP start feature mipstart = optGetIntStr(opt, "mipstart"); if(mipstart && gmoModelType(gmo) != gmoProc_mip) { @@ -254,6 +255,26 @@ int main(int argc, char *argv[]) } } + // Try taking primal or dual (marginal) values from user (for LPs) + if (gmoModelType(gmo) == gmoProc_lp) + { + cuopt_int_t chosen_method; + cuOptGetIntegerParameter(settings, "method", &chosen_method); + // only when some PDLP is used and we have basis + if ((chosen_method == CUOPT_METHOD_PDLP || chosen_method == CUOPT_METHOD_CONCURRENT) && gmoHaveBasis(gmo)) + { + int nvars = gmoN(gmo), nconstraints = gmoM(gmo); + cuopt_float_t *lvls = (cuopt_float_t *)malloc(sizeof(cuopt_float_t) * nvars); + cuopt_float_t *marginals = (cuopt_float_t *)malloc(sizeof(cuopt_float_t) * nconstraints); + gmoGetVarL(gmo, lvls); + gmoGetEquM(gmo, marginals); + cuOptSetInitialPrimalSolution(settings, lvls, nvars); + cuOptSetInitialDualSolution(settings, marginals, nconstraints); + free(lvls); + free(marginals); + } + } + if (!optGetDefinedStr(opt, "prob_read")) { constraint_matrix_row_offsets = malloc((num_constraints+1)*sizeof(cuopt_int_t)); constraint_matrix_column_indices = malloc(nnz*sizeof(cuopt_int_t)); From 043c4a8359c4fae9fc8914ebcbb8f748c19842f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schnabel?= Date: Fri, 6 Feb 2026 14:13:42 +0100 Subject: [PATCH 23/23] Better error checking and convert dbl array to float array in case cuopt uses float as cuopt_float_t --- gmscuopt.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/gmscuopt.c b/gmscuopt.c index 9403a14..28b6ca8 100644 --- a/gmscuopt.c +++ b/gmscuopt.c @@ -148,7 +148,6 @@ int main(int argc, char *argv[]) fln_mip_trace[0] = '\0'; sl_state_t context; int mipstart = 0; - cuopt_float_t *initial_levels = NULL; // Create solver settings status = cuOptCreateSolverSettings(&settings); @@ -259,17 +258,55 @@ int main(int argc, char *argv[]) if (gmoModelType(gmo) == gmoProc_lp) { cuopt_int_t chosen_method; - cuOptGetIntegerParameter(settings, "method", &chosen_method); + status = cuOptGetIntegerParameter(settings, "method", &chosen_method); + if (status != CUOPT_SUCCESS) + { + printOut(gev, "Error querying method option.\n"); + goto DONE; + } // only when some PDLP is used and we have basis if ((chosen_method == CUOPT_METHOD_PDLP || chosen_method == CUOPT_METHOD_CONCURRENT) && gmoHaveBasis(gmo)) { int nvars = gmoN(gmo), nconstraints = gmoM(gmo); - cuopt_float_t *lvls = (cuopt_float_t *)malloc(sizeof(cuopt_float_t) * nvars); - cuopt_float_t *marginals = (cuopt_float_t *)malloc(sizeof(cuopt_float_t) * nconstraints); + double *lvls = (double *)malloc(sizeof(double) * nvars); + double *marginals = (double *)malloc(sizeof(double) * nconstraints); gmoGetVarL(gmo, lvls); gmoGetEquM(gmo, marginals); - cuOptSetInitialPrimalSolution(settings, lvls, nvars); - cuOptSetInitialDualSolution(settings, marginals, nconstraints); +#if defined(CUOPT_INSTANTIATE_DOUBLE) + status = cuOptSetInitialPrimalSolution(settings, lvls, nvars); + if (status != CUOPT_SUCCESS) + { + printOut(gev, "Error setting primal solution for LP.\n"); + goto DONE; + } + status = cuOptSetInitialDualSolution(settings, marginals, nconstraints); + if (status != CUOPT_SUCCESS) + { + printOut(gev, "Error setting dual solution for LP.\n"); + goto DONE; + } +#else + cuopt_float_t *lvlsf = (cuopt_float_t *)malloc(sizeof(cuopt_float_t) * nvars); + cuopt_float_t *marginalsf = (cuopt_float_t *)malloc(sizeof(cuopt_float_t) * nconstraints); + for (int i = 0; i < nvars; i++) + lvlsf[i] = (cuopt_float_t)lvls[i]; + for (int i = 0; i < nconstraints; i++) + marginalsf[i] = (cuopt_float_t)marginals[i]; + status = cuOptSetInitialPrimalSolution(settings, lvlsf, nvars); + if (status != CUOPT_SUCCESS) + { + printOut(gev, "Error setting primal solution for LP.\n"); + goto DONE; + } + status = cuOptSetInitialDualSolution(settings, marginalsf, nconstraints); + if (status != CUOPT_SUCCESS) + { + printOut(gev, "Error setting dual solution for LP.\n"); + goto DONE; + } + free(lvlsf); + free(marginalsf); +#endif free(lvls); free(marginals); } @@ -413,14 +450,28 @@ int main(int argc, char *argv[]) // Maybe add mip start if(mipstart) { - if (initial_levels == NULL) + double *initial_levels = malloc(sizeof(double) * gmoN(gmo)); + gmoGetVarL(gmo, initial_levels); + #ifdef CUOPT_INSTANTIATE_DOUBLE + status = cuOptAddMIPStart(settings, initial_levels, gmoN(gmo)); + if (status != CUOPT_SUCCESS) { - initial_levels = malloc(sizeof(cuopt_float_t) * gmoN(gmo)); - gmoGetVarL(gmo, initial_levels); + printOut(gev, "Error setting MIP start.\n"); + goto DONE; } - cuOptAddMIPStart(settings, initial_levels, gmoN(gmo)); +#else + cuopt_float_t *initial_levelsf = malloc(sizeof(cuopt_float_t) * gmoN(gmo)); + for (int i = 0; i < gmoN(gmo); i++) + initial_levelsf[i] = (cuopt_float_t)initial_levels[i]; + status = cuOptAddMIPStart(settings, initial_levelsf, gmoN(gmo)); + if (status != CUOPT_SUCCESS) + { + printOut(gev, "Error setting MIP start.\n"); + goto DONE; + } + free(initial_levelsf); +#endif free(initial_levels); - initial_levels = NULL; } // Solve the problem