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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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: