diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6b17bbe882..d552a14cbf 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,7 +5,7 @@ }, "metadata": { "description": "Agent skills for NVIDIA cuOpt: routing (VRP, TSP, PDP), LP/MILP/QP, installation (user/developer), and REST server.", - "version": "26.06.00" + "version": "26.08.00" }, "plugins": [ { diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index e740506140..268393ba2a 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "nvidia-cuopt-skills", "description": "Agent skills for NVIDIA cuOpt: routing (VRP, TSP, PDP), LP/MILP/QP, installation (Python/C/developer), and REST server. Use when building or solving optimization with cuOpt.", - "version": "26.06.00", + "version": "26.08.00", "author": { "name": "NVIDIA" }, diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 47760b6151..f1d4123a6a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -53,7 +53,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -69,7 +69,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -84,7 +84,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@main secrets: CONDA_RAPIDSAI_NIGHTLY_TOKEN: ${{ secrets.CONDA_RAPIDSAI_NIGHTLY_TOKEN }} CONDA_RAPIDSAI_TOKEN: ${{ secrets.CONDA_RAPIDSAI_TOKEN }} @@ -101,7 +101,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -119,7 +119,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@main secrets: CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN: ${{ secrets.CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN }} RAPIDSAI_PYPI_TOKEN: ${{ secrets.RAPIDSAI_PYPI_TOKEN }} @@ -139,7 +139,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -156,7 +156,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@main secrets: CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN: ${{ secrets.CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN }} RAPIDSAI_PYPI_TOKEN: ${{ secrets.RAPIDSAI_PYPI_TOKEN }} @@ -175,7 +175,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -195,7 +195,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@main secrets: CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN: ${{ secrets.CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN }} RAPIDSAI_PYPI_TOKEN: ${{ secrets.RAPIDSAI_PYPI_TOKEN }} @@ -215,7 +215,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} node_type: "gpu-l4-latest-1" @@ -225,7 +225,7 @@ jobs: arch: "amd64" file_to_upload: "docs/cuopt/build/html/" artifact-name: "cuopt_docs" - container_image: "rapidsai/ci-conda:26.06-latest" + container_image: "rapidsai/ci-conda:26.08-latest" script: "ci/build_docs.sh" wheel-build-cuopt-sh-client: permissions: @@ -235,7 +235,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -256,7 +256,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@main secrets: CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN: ${{ secrets.CONDA_RAPIDSAI_WHEELS_NIGHTLY_TOKEN }} RAPIDSAI_PYPI_TOKEN: ${{ secrets.RAPIDSAI_PYPI_TOKEN }} diff --git a/.github/workflows/build_test_publish_images.yaml b/.github/workflows/build_test_publish_images.yaml index 9fd2fc1923..e1fa2efd15 100644 --- a/.github/workflows/build_test_publish_images.yaml +++ b/.github/workflows/build_test_publish_images.yaml @@ -42,7 +42,7 @@ jobs: permissions: contents: read container: - image: rapidsai/ci-conda:26.06-latest + image: rapidsai/ci-conda:26.08-latest outputs: MATRIX: ${{ steps.compute-matrix.outputs.MATRIX }} CUOPT_VER: ${{ steps.compute-cuopt-ver.outputs.CUOPT_VER }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 0258585159..850753c88c 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -36,7 +36,7 @@ jobs: - test-self-hosted-server permissions: contents: read - uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@main if: always() with: needs: ${{ toJSON(needs) }} @@ -125,7 +125,7 @@ jobs: contents: read packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/changed-files.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/changed-files.yaml@main with: files_yaml: | build_docs: @@ -381,7 +381,7 @@ jobs: checks: permissions: contents: read - uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@main with: enable_check_generated_files: false conda-cpp-build: @@ -398,7 +398,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@main with: build_type: pull-request script: ci/build_cpp.sh @@ -411,7 +411,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@main if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp with: build_type: pull-request @@ -437,7 +437,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@main with: build_type: pull-request script: ci/build_python.sh @@ -450,7 +450,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@main if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_conda with: run_codecov: false @@ -473,7 +473,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main if: fromJSON(needs.changed-files.outputs.changed_file_groups).build_docs with: build_type: pull-request @@ -481,7 +481,7 @@ jobs: arch: "amd64" file_to_upload: "docs/cuopt/build/html/" artifact-name: "cuopt_docs" - container_image: "rapidsai/ci-conda:26.06-latest" + container_image: "rapidsai/ci-conda:26.08-latest" script: "ci/build_docs.sh" wheel-build-libcuopt: needs: [compute-matrix-filters, changed-files] @@ -493,7 +493,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: # build for every combination of arch and CUDA version, but only for the latest Python matrix_filter: ${{ needs.compute-matrix-filters.outputs.libcuopt_filter }} @@ -511,7 +511,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: pull-request script: ci/build_wheel_cuopt.sh @@ -526,7 +526,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@main if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_wheels with: build_type: pull-request @@ -549,7 +549,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: pull-request script: ci/build_wheel_cuopt_server.sh @@ -568,7 +568,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@main with: build_type: pull-request script: ci/build_wheel_cuopt_sh_client.sh @@ -586,7 +586,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@main if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_wheels with: build_type: pull-request diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index eac85da368..01c1e2b0b3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -35,7 +35,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@main with: build_type: ${{ inputs.build_type }} branch: ${{ inputs.branch }} @@ -57,7 +57,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@main with: run_codecov: false build_type: ${{ inputs.build_type }} @@ -80,7 +80,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@main with: build_type: ${{ inputs.build_type }} branch: ${{ inputs.branch }} @@ -102,7 +102,7 @@ jobs: id-token: write packages: read pull-requests: read - uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@main with: build_type: ${{ inputs.build_type }} branch: ${{ inputs.branch }} @@ -125,7 +125,7 @@ jobs: packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main with: build_type: ${{ inputs.build_type }} branch: ${{ inputs.branch }} @@ -133,7 +133,7 @@ jobs: sha: ${{ inputs.sha }} node_type: "gpu-l4-latest-1" arch: "amd64" - container_image: "rapidsai/ci-conda:26.06-latest" + container_image: "rapidsai/ci-conda:26.08-latest" script: ci/test_notebooks.sh nightly-summary: permissions: diff --git a/.github/workflows/trigger-breaking-change-alert.yaml b/.github/workflows/trigger-breaking-change-alert.yaml index 930d174da4..0347178e38 100644 --- a/.github/workflows/trigger-breaking-change-alert.yaml +++ b/.github/workflows/trigger-breaking-change-alert.yaml @@ -19,7 +19,7 @@ permissions: {} jobs: trigger-notifier: if: contains(github.event.pull_request.labels.*.name, 'breaking') - uses: rapidsai/shared-workflows/.github/workflows/breaking-change-alert.yaml@release/26.06 + uses: rapidsai/shared-workflows/.github/workflows/breaking-change-alert.yaml@main secrets: NV_SLACK_BREAKING_CHANGE_ALERT: ${{ secrets.NV_SLACK_BREAKING_CHANGE_ALERT }} permissions: diff --git a/RAPIDS_BRANCH b/RAPIDS_BRANCH index 5a399d5c50..ba2906d066 100644 --- a/RAPIDS_BRANCH +++ b/RAPIDS_BRANCH @@ -1 +1 @@ -release/26.06 +main diff --git a/README.md b/README.md index 8c75ee7511..79d00e744e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # cuOpt - GPU-accelerated Optimization [![Build Status](https://github.com/NVIDIA/cuopt/actions/workflows/build.yaml/badge.svg)](https://github.com/NVIDIA/cuopt/actions/workflows/build.yaml) -[![Version](https://img.shields.io/badge/version-26.06.00-blue)](https://github.com/NVIDIA/cuopt/releases) +[![Version](https://img.shields.io/badge/version-26.08.00-blue)](https://github.com/NVIDIA/cuopt/releases) [![Documentation](https://img.shields.io/badge/docs-latest-brightgreen)](https://docs.nvidia.com/cuopt/user-guide/latest/introduction.html) [![Docker Hub](https://img.shields.io/badge/docker-nvidia%2Fcuopt-blue?logo=docker)](https://hub.docker.com/r/nvidia/cuopt) [![Examples](https://img.shields.io/badge/examples-cuopt--examples-orange)](https://github.com/NVIDIA/cuopt-examples) diff --git a/VERSION b/VERSION index cdb610a24d..4e6864b4ca 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -26.06.00 +26.08.00 diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index acc652407d..3d8043e098 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -19,7 +19,7 @@ dependencies: - cuda-python>=12.9.2,<13.0 - cuda-sanitizer-api - cuda-version=12.9 -- cudf==26.6.*,>=0.0.0a0 +- cudf==26.8.*,>=0.0.0a0 - cupy>=13.6.0 - cxx-compiler - cython>=3.0.3 @@ -37,8 +37,8 @@ dependencies: - libgrpc >=1.78.0,<1.80.0a0 - libnvjitlink-dev - libprotobuf -- libraft-headers==26.6.*,>=0.0.0a0 -- librmm==26.6.*,>=0.0.0a0 +- libraft-headers==26.8.*,>=0.0.0a0 +- librmm==26.8.*,>=0.0.0a0 - make - msgpack-numpy==0.4.8 - msgpack-python==1.1.2 @@ -55,7 +55,7 @@ dependencies: - pip - pre-commit - psutil>=6.0.0 -- pylibraft==26.6.*,>=0.0.0a0 +- pylibraft==26.8.*,>=0.0.0a0 - pyrsistent - pytest-cov - pytest-rerunfailures @@ -66,7 +66,7 @@ dependencies: - rapids-logger==0.2.*,>=0.0.0a0 - re2 - requests -- rmm==26.6.*,>=0.0.0a0 +- rmm==26.8.*,>=0.0.0a0 - scikit-build-core>=0.11.0 - scipy>=1.14.1 - sphinx diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index ae6772ccae..a0bd8912b0 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -19,7 +19,7 @@ dependencies: - cuda-python>=12.9.2,<13.0 - cuda-sanitizer-api - cuda-version=12.9 -- cudf==26.6.*,>=0.0.0a0 +- cudf==26.8.*,>=0.0.0a0 - cupy>=13.6.0 - cxx-compiler - cython>=3.0.3 @@ -37,8 +37,8 @@ dependencies: - libgrpc >=1.78.0,<1.80.0a0 - libnvjitlink-dev - libprotobuf -- libraft-headers==26.6.*,>=0.0.0a0 -- librmm==26.6.*,>=0.0.0a0 +- libraft-headers==26.8.*,>=0.0.0a0 +- librmm==26.8.*,>=0.0.0a0 - make - msgpack-numpy==0.4.8 - msgpack-python==1.1.2 @@ -55,7 +55,7 @@ dependencies: - pip - pre-commit - psutil>=6.0.0 -- pylibraft==26.6.*,>=0.0.0a0 +- pylibraft==26.8.*,>=0.0.0a0 - pyrsistent - pytest-cov - pytest-rerunfailures @@ -66,7 +66,7 @@ dependencies: - rapids-logger==0.2.*,>=0.0.0a0 - re2 - requests -- rmm==26.6.*,>=0.0.0a0 +- rmm==26.8.*,>=0.0.0a0 - scikit-build-core>=0.11.0 - scipy>=1.14.1 - sphinx diff --git a/conda/environments/all_cuda-132_arch-aarch64.yaml b/conda/environments/all_cuda-132_arch-aarch64.yaml index 17d986feee..51904cc66c 100644 --- a/conda/environments/all_cuda-132_arch-aarch64.yaml +++ b/conda/environments/all_cuda-132_arch-aarch64.yaml @@ -19,7 +19,7 @@ dependencies: - cuda-python>=13.0.1,<14.0 - cuda-sanitizer-api - cuda-version=13.2 -- cudf==26.6.*,>=0.0.0a0 +- cudf==26.8.*,>=0.0.0a0 - cupy>=13.6.0 - cxx-compiler - cython>=3.0.3 @@ -37,8 +37,8 @@ dependencies: - libgrpc >=1.78.0,<1.80.0a0 - libnvjitlink-dev - libprotobuf -- libraft-headers==26.6.*,>=0.0.0a0 -- librmm==26.6.*,>=0.0.0a0 +- libraft-headers==26.8.*,>=0.0.0a0 +- librmm==26.8.*,>=0.0.0a0 - make - msgpack-numpy==0.4.8 - msgpack-python==1.1.2 @@ -55,7 +55,7 @@ dependencies: - pip - pre-commit - psutil>=6.0.0 -- pylibraft==26.6.*,>=0.0.0a0 +- pylibraft==26.8.*,>=0.0.0a0 - pyrsistent - pytest-cov - pytest-rerunfailures @@ -66,7 +66,7 @@ dependencies: - rapids-logger==0.2.*,>=0.0.0a0 - re2 - requests -- rmm==26.6.*,>=0.0.0a0 +- rmm==26.8.*,>=0.0.0a0 - scikit-build-core>=0.11.0 - scipy>=1.14.1 - sphinx diff --git a/conda/environments/all_cuda-132_arch-x86_64.yaml b/conda/environments/all_cuda-132_arch-x86_64.yaml index 62d876e71d..e28726c6a2 100644 --- a/conda/environments/all_cuda-132_arch-x86_64.yaml +++ b/conda/environments/all_cuda-132_arch-x86_64.yaml @@ -19,7 +19,7 @@ dependencies: - cuda-python>=13.0.1,<14.0 - cuda-sanitizer-api - cuda-version=13.2 -- cudf==26.6.*,>=0.0.0a0 +- cudf==26.8.*,>=0.0.0a0 - cupy>=13.6.0 - cxx-compiler - cython>=3.0.3 @@ -37,8 +37,8 @@ dependencies: - libgrpc >=1.78.0,<1.80.0a0 - libnvjitlink-dev - libprotobuf -- libraft-headers==26.6.*,>=0.0.0a0 -- librmm==26.6.*,>=0.0.0a0 +- libraft-headers==26.8.*,>=0.0.0a0 +- librmm==26.8.*,>=0.0.0a0 - make - msgpack-numpy==0.4.8 - msgpack-python==1.1.2 @@ -55,7 +55,7 @@ dependencies: - pip - pre-commit - psutil>=6.0.0 -- pylibraft==26.6.*,>=0.0.0a0 +- pylibraft==26.8.*,>=0.0.0a0 - pyrsistent - pytest-cov - pytest-rerunfailures @@ -66,7 +66,7 @@ dependencies: - rapids-logger==0.2.*,>=0.0.0a0 - re2 - requests -- rmm==26.6.*,>=0.0.0a0 +- rmm==26.8.*,>=0.0.0a0 - scikit-build-core>=0.11.0 - scipy>=1.14.1 - sphinx diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 3342ba375c..1dfccef1ea 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -47,6 +47,10 @@ #define CUOPT_DUALIZE "dualize" #define CUOPT_ORDERING "ordering" #define CUOPT_BARRIER_DUAL_INITIAL_POINT "barrier_dual_initial_point" +#define CUOPT_BARRIER_RELATIVE_FEASIBILITY_TOLERANCE "barrier_relative_feasibility_tolerance" +#define CUOPT_BARRIER_RELATIVE_OPTIMALITY_TOLERANCE "barrier_relative_optimality_tolerance" +#define CUOPT_BARRIER_RELATIVE_COMPLEMENTARITY_TOLERANCE \ + "barrier_relative_complementarity_tolerance" #define CUOPT_BARRIER_ITERATIVE_REFINEMENT "barrier_iterative_refinement" #define CUOPT_BARRIER_STEP_SCALE "barrier_step_scale" #define CUOPT_ELIMINATE_DENSE_COLUMNS "eliminate_dense_columns" diff --git a/cpp/include/cuopt/linear_programming/io/data_model_view.hpp b/cpp/include/cuopt/linear_programming/io/data_model_view.hpp index ca2fd30393..aa76d122bf 100644 --- a/cpp/include/cuopt/linear_programming/io/data_model_view.hpp +++ b/cpp/include/cuopt/linear_programming/io/data_model_view.hpp @@ -434,9 +434,9 @@ class data_model_view_t { std::vector(qc.linear_values.begin(), qc.linear_values.end()), std::vector(qc.linear_indices.begin(), qc.linear_indices.end()), static_cast(qc.rhs_value), - std::vector(qc.quadratic_values.begin(), qc.quadratic_values.end()), - std::vector(qc.quadratic_indices.begin(), qc.quadratic_indices.end()), - std::vector(qc.quadratic_offsets.begin(), qc.quadratic_offsets.end())}); + std::vector(qc.quadratic_row_indices.begin(), qc.quadratic_row_indices.end()), + std::vector(qc.quadratic_col_indices.begin(), qc.quadratic_col_indices.end()), + std::vector(qc.quadratic_values.begin(), qc.quadratic_values.end())}); } } diff --git a/cpp/include/cuopt/linear_programming/io/mps_data_model.hpp b/cpp/include/cuopt/linear_programming/io/mps_data_model.hpp index 9828a00c0c..dee74a3a42 100644 --- a/cpp/include/cuopt/linear_programming/io/mps_data_model.hpp +++ b/cpp/include/cuopt/linear_programming/io/mps_data_model.hpp @@ -239,7 +239,7 @@ class mps_data_model_t { * - row identity and type (from ROWS), * - sparse linear coefficients (from COLUMNS), * - RHS value (from RHS), - * - quadratic matrix Q in CSR (from QCMATRIX). + * - quadratic matrix Q in COO (SoA: row, col, value) from QCMATRIX — one triplet per nonzero. */ struct quadratic_constraint_t { /** ROWS declaration index (among all constraint rows), not an index into the linear CSR. */ @@ -251,19 +251,21 @@ class mps_data_model_t { std::vector linear_values{}; std::vector linear_indices{}; f_t rhs_value{f_t(0)}; + /** Q nonzeros: parallel arrays, same length (COO / SoA). Sorted by (row, col) in append. */ + std::vector quadratic_row_indices{}; + std::vector quadratic_col_indices{}; std::vector quadratic_values{}; - std::vector quadratic_indices{}; - std::vector quadratic_offsets{}; }; /** * @brief Append one complete quadratic constraint (row + linear + rhs + quadratic Q). * @note All span inputs are host memory; the model copies this data. * @param linear_values, linear_indices Same nnz; can be empty for a purely quadratic row (rare). - * @param quadratic_values, quadratic_indices CSR nnz; may be empty if Q is empty. - * @param quadratic_offsets CSR row starts; must be non-empty. - * @param constraint_row_type MPS ROWS type; must be 'L'. 'G' and 'E' quadratic rows are not - * supported. + * @param quadratic_values, quadratic_row_indices, quadratic_col_indices COO triplets; same + * length; may all be empty if Q is empty. Stored sorted by (row, col). + * @param constraint_row_type MPS ROWS type: 'L' (<=) or 'G' (>=). Stored as given; 'G' rows are + * converted to '<=' form when building the SOCP for the barrier solver. Equality ('E') is + * not supported. */ void append_quadratic_constraint(i_t constraint_row_index, const std::string& constraint_row_name, @@ -272,8 +274,8 @@ class mps_data_model_t { std::span linear_indices, f_t rhs_value, std::span quadratic_values, - std::span quadratic_indices, - std::span quadratic_offsets); + std::span quadratic_row_indices, + std::span quadratic_col_indices); const std::vector& get_quadratic_constraints() const; diff --git a/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp b/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp index aa164ca756..56b373c897 100644 --- a/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp +++ b/cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp @@ -64,9 +64,10 @@ class optimization_problem_interface_t { std::vector linear_values{}; std::vector linear_indices{}; f_t rhs_value{f_t(0)}; + /** Q in COO: parallel arrays, same length. */ + std::vector quadratic_row_indices{}; + std::vector quadratic_col_indices{}; std::vector quadratic_values{}; - std::vector quadratic_indices{}; - std::vector quadratic_offsets{}; }; virtual ~optimization_problem_interface_t() = default; @@ -89,9 +90,9 @@ class optimization_problem_interface_t { std::vector(qc.linear_values.begin(), qc.linear_values.end()), std::vector(qc.linear_indices.begin(), qc.linear_indices.end()), static_cast(qc.rhs_value), - std::vector(qc.quadratic_values.begin(), qc.quadratic_values.end()), - std::vector(qc.quadratic_indices.begin(), qc.quadratic_indices.end()), - std::vector(qc.quadratic_offsets.begin(), qc.quadratic_offsets.end())}); + std::vector(qc.quadratic_row_indices.begin(), qc.quadratic_row_indices.end()), + std::vector(qc.quadratic_col_indices.begin(), qc.quadratic_col_indices.end()), + std::vector(qc.quadratic_values.begin(), qc.quadratic_values.end())}); } set_quadratic_constraints(std::move(converted_constraints)); } diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index b30286f9ce..3597b85016 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -280,6 +280,9 @@ class pdlp_solver_settings_t { i_t dualize{-1}; i_t ordering{-1}; i_t barrier_dual_initial_point{-1}; + f_t barrier_relative_feasibility_tolerance{1.0e-8}; + f_t barrier_relative_optimality_tolerance{1.0e-8}; + f_t barrier_relative_complementarity_tolerance{1.0e-8}; bool eliminate_dense_columns{true}; pdlp_precision_t pdlp_precision{pdlp_precision_t::DefaultPrecision}; bool barrier_iterative_refinement{true}; diff --git a/cpp/src/barrier/barrier.cu b/cpp/src/barrier/barrier.cu index e7604fb60e..fd6f9dbc6e 100644 --- a/cpp/src/barrier/barrier.cu +++ b/cpp/src/barrier/barrier.cu @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -34,6 +35,8 @@ #include #include +#include +#include #include #include @@ -43,6 +46,7 @@ #include #include #include +#include namespace cuopt::linear_programming::dual_simplex { @@ -61,6 +65,29 @@ namespace cuopt::linear_programming::dual_simplex { cuda::std::make_tuple(a, b), out, size, cuda::std::multiplies<>{}, stream.value()); } +// out[i] = is_native_free_linear[i] ? 0 : a[i] * b[i] +[[maybe_unused]] static void pairwise_multiply_skip_native_free_linear( + float* a, float* b, int* is_native_free_linear, float* out, int size, rmm::cuda_stream_view stream) +{ + cub::DeviceTransform::Transform( + cuda::std::make_tuple(a, b, is_native_free_linear), + out, + size, + [] __host__ __device__(float x_j, float d_j, int free_j) { return free_j ? 0.f : x_j * d_j; }, + stream.value()); +} + +[[maybe_unused]] static void pairwise_multiply_skip_native_free_linear( + double* a, double* b, int* is_native_free_linear, double* out, int size, rmm::cuda_stream_view stream) +{ + cub::DeviceTransform::Transform( + cuda::std::make_tuple(a, b, is_native_free_linear), + out, + size, + [] __host__ __device__(double x_j, double d_j, int free_j) { return free_j ? 0.0 : x_j * d_j; }, + stream.value()); +} + [[maybe_unused]] static void axpy( float alpha, float* x, float beta, float* y, float* out, int size, rmm::cuda_stream_view stream) { @@ -93,7 +120,7 @@ class iteration_data_t { public: iteration_data_t(const lp_problem_t& lp, i_t num_upper_bounds, - const std::vector& free_variable_indices, + const std::vector& native_free_linear_indices, const csc_matrix_t& Qin, const simplex_solver_settings_t& settings) : upper_bounds(num_upper_bounds), @@ -164,10 +191,12 @@ class iteration_data_t { d_inv_diag(lp.num_cols, lp.handle_ptr->get_stream()), d_cols_to_remove(0, lp.handle_ptr->get_stream()), d_augmented_diagonal_indices_(0, lp.handle_ptr->get_stream()), + d_cone_csr_indices_(0, lp.handle_ptr->get_stream()), + d_cone_Q_values_(0, lp.handle_ptr->get_stream()), use_augmented(false), has_factorization(false), - n_free_vars(0), - d_is_free_(0, lp.handle_ptr->get_stream()), + n_native_free_linear(0), + d_is_native_free_linear_(0, lp.handle_ptr->get_stream()), num_factorizations(0), has_solve_info(false), settings_(settings), @@ -209,9 +238,11 @@ class iteration_data_t { d_dw_residual_(0, lp.handle_ptr->get_stream()), d_wv_residual_(0, lp.handle_ptr->get_stream()), d_bound_rhs_(0, lp.handle_ptr->get_stream()), - d_complementarity_xz_rhs_(lp.num_cols, lp.handle_ptr->get_stream()), + d_complementarity_xz_rhs_(0, lp.handle_ptr->get_stream()), d_complementarity_wv_rhs_(0, lp.handle_ptr->get_stream()), d_dual_rhs_(lp.num_cols, lp.handle_ptr->get_stream()), + d_complementarity_target_(lp.num_cols, lp.handle_ptr->get_stream()), + d_cone_hessian_dx_(0, lp.handle_ptr->get_stream()), d_Q_diag_(0, lp.handle_ptr->get_stream()), d_Qx_(Qin.m, lp.handle_ptr->get_stream()), restrict_u_(0), @@ -219,192 +250,242 @@ class iteration_data_t { sum_reduce_helper_(lp.handle_ptr->get_stream()), indefinite_Q(false), Q_diagonal(false), - symbolic_status(0) + symbolic_status(0), + cone_combined_step_(false), + cone_sigma_mu_(f_t(0)) { raft::common::nvtx::range fun_scope("Barrier: LP Data Creation"); - // Set up free variable tracking for QPs - if (!free_variable_indices.empty()) { - n_free_vars = free_variable_indices.size(); - std::vector is_free_host(lp.num_cols, 0); - for (i_t j : free_variable_indices) { - is_free_host[j] = 1; + { + raft::common::nvtx::range scope("Barrier: LP Data: native free linear"); + // Set up native free linear variable tracking (linear columns only, j < cone_start). + if (!native_free_linear_indices.empty()) { + n_native_free_linear = native_free_linear_indices.size(); + std::vector is_native_free_linear_host(lp.num_cols, 0); + for (i_t j : native_free_linear_indices) { + is_native_free_linear_host[j] = 1; + } + d_is_native_free_linear_.resize(lp.num_cols, stream_view_); + raft::copy( + d_is_native_free_linear_.data(), is_native_free_linear_host.data(), lp.num_cols, stream_view_); + settings.log.printf("Native free linear (QP): %d\n", n_native_free_linear); } - d_is_free_.resize(lp.num_cols, stream_view_); - raft::copy(d_is_free_.data(), is_free_host.data(), lp.num_cols, stream_view_); - settings.log.printf("Free variables (QP) : %d\n", n_free_vars); } bool has_Q = Q.x.size() > 0; indefinite_Q = false; - if (has_Q) { - Qdiag.resize(lp.num_cols, 0.0); - - for (i_t j = 0; j < Q.n; j++) { - const i_t col_start = Q.col_start[j]; - const i_t col_end = Q.col_start[j + 1]; - for (i_t p = col_start; p < col_end; p++) { - const i_t i = Q.i[p]; - if (j == i) { - Qdiag[j] = Q.x[p]; - break; + { + raft::common::nvtx::range scope("Barrier: LP Data: Q setup"); + if (has_Q) { + Qdiag.resize(lp.num_cols, 0.0); + + for (i_t j = 0; j < Q.n; j++) { + const i_t col_start = Q.col_start[j]; + const i_t col_end = Q.col_start[j + 1]; + for (i_t p = col_start; p < col_end; p++) { + const i_t i = Q.i[p]; + if (j == i) { + Qdiag[j] = Q.x[p]; + break; + } } } - } - Q_diagonal = Q.is_diagonal(); + Q_diagonal = Q.is_diagonal(); - if (Q_diagonal) { - // Check to ensure that Q is positive semi-definite - for (i_t j = 0; j < lp.num_cols; j++) { - if (Qdiag[j] < 0.0) { - settings_.log.printf( - "Q is not positive semidefinite: Q(%d, %d) = %e\n", j, j, Qdiag[j]); - indefinite_Q = true; - return; + if (Q_diagonal) { + // Check to ensure that Q is positive semi-definite + for (i_t j = 0; j < lp.num_cols; j++) { + if (Qdiag[j] < 0.0) { + settings_.log.printf( + "Q is not positive semidefinite: Q(%d, %d) = %e\n", j, j, Qdiag[j]); + indefinite_Q = true; + return; + } } + } else if (settings.check_Q) { + // TODO: Check to ensure that Q is positive semi-definite + // This requires us to perform a Cholesky factorization. } - } else if (settings.check_Q) { - // TODO: Check to ensure that Q is positive semi-definite - // This requires us to perform a Cholesky factorization. + + d_Q_diag_.resize(lp.num_cols, stream_view_); + raft::copy(d_Q_diag_.data(), Qdiag.data(), Qdiag.size(), stream_view_); } + } - d_Q_diag_.resize(lp.num_cols, stream_view_); - raft::copy(d_Q_diag_.data(), Qdiag.data(), Qdiag.size(), stream_view_); + { + raft::common::nvtx::range scope("Barrier: LP Data: SOC setup"); + if (!lp.second_order_cone_dims.empty()) { + cone_var_start_ = lp.cone_var_start; + i_t total_cone_dim = + std::accumulate(lp.second_order_cone_dims.begin(), lp.second_order_cone_dims.end(), i_t(0)); + cuopt_assert(cone_var_start_ >= 0, "cone_var_start must be nonnegative"); + cuopt_assert(cone_var_start_ + total_cone_dim <= lp.num_cols, + "cone variables exceed problem dimension"); + cuopt_assert(cone_var_start_ + total_cone_dim == lp.num_cols, + "barrier expects [linear | cone] layout"); + cones_.emplace( + std::span(lp.second_order_cone_dims.data(), lp.second_order_cone_dims.size()), + raft::device_span{}, + raft::device_span{}, + stream_view_); + cuopt_assert(cone_count() > 0, "second-order cone topology must contain at least one cone"); + cuopt_assert(cone_entry_count() == total_cone_dim, "second-order cone entry count mismatch"); + } } - // Allocating GPU flag data for Form ADAT - RAFT_CUDA_TRY(cub::DeviceSelect::Flagged( - nullptr, - flag_buffer_size, - d_inv_diag_prime.data(), // Not the actual input but just to allcoate the memory - thrust::make_transform_iterator(d_cols_to_remove.data(), cuda::std::logical_not{}), - d_inv_diag_prime.data(), - d_num_flag.data(), - inv_diag.size(), - stream_view_)); + { + raft::common::nvtx::range scope("Barrier: LP Data: complementarity buffers"); + const i_t linear_xz_rhs_size = linear_xz_size(lp.num_cols); + d_complementarity_xz_rhs_.resize(linear_xz_rhs_size, stream_view_); - d_flag_buffer.resize(flag_buffer_size, stream_view_); + // Allocating GPU flag data for Form ADAT + RAFT_CUDA_TRY(cub::DeviceSelect::Flagged( + nullptr, + flag_buffer_size, + d_inv_diag_prime.data(), // Not the actual input but just to allcoate the memory + thrust::make_transform_iterator(d_cols_to_remove.data(), cuda::std::logical_not{}), + d_inv_diag_prime.data(), + d_num_flag.data(), + inv_diag.size(), + stream_view_)); - // Create the upper bounds vector - n_upper_bounds = 0; - for (i_t j = 0; j < lp.num_cols; j++) { - if (lp.upper[j] < inf) { upper_bounds[n_upper_bounds++] = j; } + d_flag_buffer.resize(flag_buffer_size, stream_view_); } - if (n_upper_bounds > 0) { - settings.log.printf("Upper bounds : %d\n", n_upper_bounds); + + { + raft::common::nvtx::range scope("Barrier: LP Data: upper bounds"); + // Create the upper bounds vector + n_upper_bounds = 0; + for (i_t j = 0; j < lp.num_cols; j++) { + if (lp.upper[j] < inf) { upper_bounds[n_upper_bounds++] = j; } + } + if (n_upper_bounds > 0) { + settings.log.printf("Upper bounds : %d\n", n_upper_bounds); + } } - // Decide if we are going to use the augmented system or not - n_dense_columns = 0; - i_t n_dense_rows = 0; - i_t max_row_nz = 0; - f_t estimated_nz_AAT = 0.0; std::vector dense_columns_unordered; + { + raft::common::nvtx::range scope("Barrier: LP Data: dense columns and augmented"); + // Decide if we are going to use the augmented system or not + n_dense_columns = 0; + i_t n_dense_rows = 0; + i_t max_row_nz = 0; + f_t estimated_nz_AAT = 0.0; + + const bool has_soc = has_cones(); + if (has_Q || has_soc) { + // QP and SOCP always use the augmented KKT; skip dense-column / ADAT heuristics. + use_augmented = true; + n_dense_columns = 0; + } else { + f_t start_column_density = tic(); - f_t start_column_density = tic(); - - // Do not look for dense columns if Q is not diagonal - if (!has_Q || Q_diagonal) { - find_dense_columns( - lp.A, settings, dense_columns_unordered, n_dense_rows, max_row_nz, estimated_nz_AAT); - } - if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } + // Do not look for dense columns if Q is not diagonal + if (!has_Q || Q_diagonal) { + find_dense_columns( + lp.A, settings, dense_columns_unordered, n_dense_rows, max_row_nz, estimated_nz_AAT); + } + if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } #ifdef PRINT_INFO - for (i_t j : dense_columns_unordered) { - settings.log.printf("Dense column %6d\n", j); - } + for (i_t j : dense_columns_unordered) { + settings.log.printf("Dense column %6d\n", j); + } #endif - float64_t column_density_time = toc(start_column_density); - if (!settings.eliminate_dense_columns) { dense_columns_unordered.clear(); } - n_dense_columns = static_cast(dense_columns_unordered.size()); - if (n_dense_columns > 0) { - settings.log.printf("Dense columns : %d\n", n_dense_columns); - } - if (n_dense_rows > 0) { - settings.log.printf("Dense rows : %d\n", n_dense_rows); - } - settings.log.printf("Density estimator time : %.2fs\n", column_density_time); - if ((settings.augmented != 0) && - (n_dense_columns > 50 || n_dense_rows > 10 || - lp.A.m == 0 /* handle case with no constraints */ || - (max_row_nz > 5000 && estimated_nz_AAT > 1e10) || settings.augmented == 1)) { - use_augmented = true; - n_dense_columns = 0; - } - - if (has_Q && !use_augmented) { - // For now let's not deal with dense columns - n_dense_columns = 0; - use_augmented = !Q_diagonal; - } + float64_t column_density_time = toc(start_column_density); + if (!settings.eliminate_dense_columns) { dense_columns_unordered.clear(); } + n_dense_columns = static_cast(dense_columns_unordered.size()); + if (n_dense_columns > 0) { + settings.log.printf("Dense columns : %d\n", n_dense_columns); + } + if (n_dense_rows > 0) { + settings.log.printf("Dense rows : %d\n", n_dense_rows); + } + settings.log.printf("Density estimator time : %.2fs\n", column_density_time); + if ((settings.augmented != 0) && + (n_dense_columns > 50 || n_dense_rows > 10 || + lp.A.m == 0 /* handle case with no constraints */ || + (max_row_nz > 5000 && estimated_nz_AAT > 1e10) || settings.augmented == 1)) { + use_augmented = true; + n_dense_columns = 0; + } + } - if (use_augmented) { - settings.log.printf("Linear system : augmented\n"); - } else { - settings.log.printf("Linear system : ADAT\n"); + if (use_augmented) { + settings.log.printf("Linear system : augmented\n"); + } else { + settings.log.printf("Linear system : ADAT\n"); + } } - // D = I + EET - diag.set_scalar(1.0); - if (n_upper_bounds > 0) { - for (i_t k = 0; k < n_upper_bounds; k++) { - i_t j = upper_bounds[k]; - diag[j] = 2.0; + { + raft::common::nvtx::range scope("Barrier: LP Data: diag and inv_diag"); + // D = I + EET + diag.set_scalar(1.0); + if (n_upper_bounds > 0) { + for (i_t k = 0; k < n_upper_bounds; k++) { + i_t j = upper_bounds[k]; + diag[j] = 2.0; + } } - } - // D = I + EET + Q (if Q is diagonal) - if (has_Q && !use_augmented) { - // this means that Q is diagonal - for (i_t j = 0; j < Q.n; j++) { - diag[j] += Qdiag[j]; + // D = I + EET + Q (if Q is diagonal) + if (has_Q && !use_augmented) { + // this means that Q is diagonal + for (i_t j = 0; j < Q.n; j++) { + diag[j] += Qdiag[j]; + } } - } - inv_diag.set_scalar(1.0); - if (use_augmented) { diag.multiply_scalar(-1.0); } - if (n_upper_bounds > 0 || (has_Q && !use_augmented)) { diag.inverse(inv_diag); } - // TMP diag and inv_diag should directly created and filled on the GPU - raft::copy(d_inv_diag.data(), inv_diag.data(), inv_diag.size(), stream_view_); - inv_sqrt_diag.set_scalar(1.0); - if (n_upper_bounds > 0 || (has_Q && !use_augmented)) { inv_diag.sqrt(inv_sqrt_diag); } + inv_diag.set_scalar(1.0); + if (use_augmented) { diag.multiply_scalar(-1.0); } + if (n_upper_bounds > 0 || (has_Q && !use_augmented)) { diag.inverse(inv_diag); } + // TMP diag and inv_diag should directly created and filled on the GPU + raft::copy(d_inv_diag.data(), inv_diag.data(), inv_diag.size(), stream_view_); + inv_sqrt_diag.set_scalar(1.0); + if (n_upper_bounds > 0 || (has_Q && !use_augmented)) { inv_diag.sqrt(inv_sqrt_diag); } + } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } - // Copy A into AD - AD = lp.A; - if (!use_augmented && n_dense_columns > 0) { - cols_to_remove.resize(lp.num_cols, 0); - for (i_t k : dense_columns_unordered) { - cols_to_remove[k] = 1; - } - d_cols_to_remove.resize(cols_to_remove.size(), stream_view_); - raft::copy( - d_cols_to_remove.data(), cols_to_remove.data(), cols_to_remove.size(), stream_view_); - dense_columns.clear(); - dense_columns.reserve(n_dense_columns); - for (i_t j = 0; j < lp.num_cols; j++) { - if (cols_to_remove[j]) { dense_columns.push_back(j); } - } - AD.remove_columns(cols_to_remove); + { + raft::common::nvtx::range scope("Barrier: LP Data: AD matrix setup"); + // Copy A into AD + AD = lp.A; + if (!use_augmented && n_dense_columns > 0) { + cols_to_remove.resize(lp.num_cols, 0); + for (i_t k : dense_columns_unordered) { + cols_to_remove[k] = 1; + } + d_cols_to_remove.resize(cols_to_remove.size(), stream_view_); + raft::copy( + d_cols_to_remove.data(), cols_to_remove.data(), cols_to_remove.size(), stream_view_); + dense_columns.clear(); + dense_columns.reserve(n_dense_columns); + for (i_t j = 0; j < lp.num_cols; j++) { + if (cols_to_remove[j]) { dense_columns.push_back(j); } + } + AD.remove_columns(cols_to_remove); - sparse_mark.resize(lp.num_cols, 1); - for (i_t k : dense_columns) { - sparse_mark[k] = 0; - } + sparse_mark.resize(lp.num_cols, 1); + for (i_t k : dense_columns) { + sparse_mark[k] = 0; + } - A_dense.resize(AD.m, n_dense_columns); - i_t k = 0; - for (i_t j : dense_columns) { - A_dense.from_sparse(lp.A, j, k++); + A_dense.resize(AD.m, n_dense_columns); + i_t k = 0; + for (i_t j : dense_columns) { + A_dense.from_sparse(lp.A, j, k++); + } } - } - AD.transpose(AT); + AD.transpose(AT); + } // device_AD / device_A / ADAT path is only used when forming ADAT (!use_augmented). if (!use_augmented) { + raft::common::nvtx::range scope("Barrier: LP Data: device AD path"); device_AD.copy(AD, handle_ptr->get_stream()); d_original_A_values.resize(device_AD.x.size(), handle_ptr->get_stream()); raft::copy(d_original_A_values.data(), @@ -421,44 +502,185 @@ class iteration_data_t { } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } - i_t factorization_size = use_augmented ? lp.num_rows + lp.num_cols : lp.num_rows; - chol = - std::make_unique>(handle_ptr, settings, factorization_size); - chol->set_positive_definite(false); + { + raft::common::nvtx::range scope("Barrier: LP Data: Cholesky init"); + i_t factorization_size = use_augmented ? lp.num_rows + lp.num_cols : lp.num_rows; + chol = std::make_unique>( + handle_ptr, settings, factorization_size); + chol->set_positive_definite(false); + } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } - // Perform symbolic analysis - symbolic_status = 0; - if (use_augmented) { - // Build the sparsity pattern of the augmented system - form_augmented(true); - if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } - symbolic_status = chol->analyze(device_augmented); - } else { - form_adat(true); - if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } - symbolic_status = chol->analyze(device_ADAT); + { + raft::common::nvtx::range scope("Barrier: LP Data: symbolic analysis"); + // Perform symbolic analysis + symbolic_status = 0; + if (use_augmented) { + { + raft::common::nvtx::range form_scope("Barrier: LP Data: form augmented"); + // Build the sparsity pattern of the augmented system + form_augmented(true); + } + if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } + symbolic_status = chol->analyze(device_augmented); + } else { + { + raft::common::nvtx::range form_scope("Barrier: LP Data: form ADAT"); + form_adat(true); + } + if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return; } + symbolic_status = chol->analyze(device_ADAT); + } + } + } + + bool has_cones() const { return cones_.has_value(); } + + cone_data_t& cones() + { + cuopt_assert(cones_.has_value(), "second-order cone data is not initialized"); + return *cones_; + } + + const cone_data_t& cones() const + { + cuopt_assert(cones_.has_value(), "second-order cone data is not initialized"); + return *cones_; + } + + i_t cone_count() const { return has_cones() ? cones_->n_cones : i_t(0); } + + i_t cone_entry_count() const + { + return has_cones() ? static_cast(cones_->n_cone_entries) : i_t(0); + } + + i_t cone_start() const { return cone_var_start_; } + + i_t cone_end() const { return cone_start() + cone_entry_count(); } + + i_t linear_xz_size(std::size_t full_xz_size) const + { + return has_cones() ? cone_start() : static_cast(full_xz_size); + } + + bool is_cone_variable(i_t variable) const + { + return has_cones() && variable >= cone_start() && variable < cone_end(); + } + + f_t complementarity_degree(std::size_t num_primal_variables, i_t num_upper_bounds) const + { + const bool has_soc = has_cones(); + f_t degree = static_cast(num_primal_variables) + static_cast(num_upper_bounds); + // Native QP free variables (linear only): no x·z complementarity in the barrier degree. + degree -= static_cast(n_native_free_linear); + if (has_soc) { + degree -= static_cast(cone_entry_count()); + degree += static_cast(cone_count()); } + return degree; } void form_augmented(bool first_call = false) { - i_t n = A.n; - i_t m = A.m; - i_t nnzA = A.col_start[n]; - i_t nnzQ = Q.n > 0 ? Q.col_start[n] : 0; - i_t factorization_size = n + m; - const f_t dual_perturb = 0.0; - const f_t primal_perturb = 1e-6; + i_t n = A.n; + i_t m = A.m; + i_t nnzA = A.col_start[n]; + i_t nnzQ = Q.n > 0 ? Q.col_start[n] : 0; + i_t factorization_size = n + m; + + const bool has_soc = has_cones(); + const i_t m_c = cone_entry_count(); + i_t total_block_nnz = 0; + + std::vector cone_offsets_host; + std::vector cone_block_offsets_host; + if (first_call) { - i_t new_nnz = 2 * nnzA + n + m + nnzQ; + if (has_soc) { + raft::common::nvtx::range scope("Barrier: augmented: SOC offsets"); + // Initialize the cone block offsets + const i_t n_cones = cone_count(); + cone_offsets_host.resize(n_cones + 1); + cone_block_offsets_host.resize(n_cones + 1); + raft::copy( + cone_offsets_host.data(), cones().cone_offsets.data(), n_cones + 1, stream_view_); + handle_ptr->sync_stream(); + for (i_t k = 0; k < n_cones; ++k) { + const auto q_k = cone_offsets_host[k + 1] - cone_offsets_host[k]; + cone_block_offsets_host[k + 1] = cone_block_offsets_host[k] + q_k * q_k; + } + total_block_nnz = static_cast(cone_block_offsets_host[n_cones]); + } + + i_t new_nnz = 2 * nnzA + n + m + nnzQ + total_block_nnz; // conservative estimate of nnz csr_matrix_t augmented_CSR(n + m, n + m, new_nnz); std::vector augmented_diagonal_indices(n + m, -1); + std::vector cone_csr_indices_host(total_block_nnz, -1); + std::vector cone_Q_values_host(total_block_nnz, f_t(0)); i_t q = 0; i_t off_diag_Qnz = 0; - for (i_t i = 0; i < n; i++) { + { + raft::common::nvtx::range scope("Barrier: augmented: host CSR build"); + for (i_t i = 0; i < n; i++) { augmented_CSR.row_start[i] = q; - if (nnzQ == 0) { + + const bool is_cone_row = is_cone_variable(i); + + if (is_cone_row) { + // Determine which cone this variable belongs to and its local row + i_t local_idx = i - cone_start(); + i_t k = 0; + while (k + 1 < cone_count() && + cone_offsets_host[k + 1] <= static_cast(local_idx)) { + k++; + } + i_t local_r = + static_cast(static_cast(local_idx) - cone_offsets_host[k]); + i_t q_k = static_cast(cone_offsets_host[k + 1] - cone_offsets_host[k]); + i_t cone_col_start = cone_start() + static_cast(cone_offsets_host[k]); + i_t block_base = static_cast(cone_block_offsets_host[k]) + local_r * q_k; + + // Merge-join: Q entries (sorted) with dense cone block columns (contiguous) + i_t qp = (nnzQ > 0) ? Q.col_start[i] : 0; + i_t q_end = (nnzQ > 0) ? Q.col_start[i + 1] : 0; + + // Q entries before cone block + while (qp < q_end && Q.i[qp] < cone_col_start) { + augmented_CSR.j[q] = Q.i[qp]; + augmented_CSR.x[q++] = -Q.x[qp]; + off_diag_Qnz++; + qp++; + } + + // Dense cone block, absorbing any Q entries that fall inside + for (i_t c = 0; c < q_k; c++) { + i_t col = cone_col_start + c; + f_t q_contrib = f_t(0); + f_t initial_val = (c == local_r) ? f_t(-dual_perturb) + : f_t(0); // diagonal entry of the cone block column + + if (qp < q_end && Q.i[qp] == col) { + q_contrib = Q.x[qp]; + qp++; + } + + cone_csr_indices_host[block_base + c] = q; + cone_Q_values_host[block_base + c] = q_contrib; + if (col == i) { augmented_diagonal_indices[i] = q; } + augmented_CSR.j[q] = col; + augmented_CSR.x[q++] = initial_val - q_contrib; + } + + // Q entries after cone block + while (qp < q_end) { + augmented_CSR.j[q] = Q.i[qp]; + augmented_CSR.x[q++] = -Q.x[qp]; + off_diag_Qnz++; + qp++; + } + } else if (nnzQ == 0) { augmented_diagonal_indices[i] = q; augmented_CSR.j[q] = i; augmented_CSR.x[q++] = -diag[i] - dual_perturb; @@ -487,42 +709,61 @@ class iteration_data_t { // AT block, we can use A in csc directly const i_t col_beg = A.col_start[i]; const i_t col_end = A.col_start[i + 1]; - for (i_t p = col_beg; p < col_end; ++p) { - augmented_CSR.j[q] = A.i[p] + n; - augmented_CSR.x[q++] = A.x[p]; + for (i_t p = col_beg; p < col_end; ++p) { + augmented_CSR.j[q] = A.i[p] + n; + augmented_CSR.x[q++] = A.x[p]; + } + } + + for (i_t k = n; k < n + m; ++k) { + // A block, we can use AT in csc directly + augmented_CSR.row_start[k] = q; + const i_t l = k - n; + const i_t col_beg = AT.col_start[l]; + const i_t col_end = AT.col_start[l + 1]; + for (i_t p = col_beg; p < col_end; ++p) { + augmented_CSR.j[q] = AT.i[p]; + augmented_CSR.x[q++] = AT.x[p]; + } + augmented_diagonal_indices[k] = q; + augmented_CSR.j[q] = k; + augmented_CSR.x[q++] = primal_perturb; } + augmented_CSR.row_start[n + m] = q; + augmented_CSR.nz_max = q; + augmented_CSR.j.resize(q); + augmented_CSR.x.resize(q); + i_t expected_nnz = 2 * nnzA + (n - m_c) + total_block_nnz + m + off_diag_Qnz; + settings_.log.debug("augmented nz %d predicted %d\n", q, expected_nnz); + cuopt_assert(q == expected_nnz, "augmented nnz != predicted"); + cuopt_assert(A.col_start[n] == AT.col_start[m], "A nz != AT nz"); } - for (i_t k = n; k < n + m; ++k) { - // A block, we can use AT in csc directly - augmented_CSR.row_start[k] = q; - const i_t l = k - n; - const i_t col_beg = AT.col_start[l]; - const i_t col_end = AT.col_start[l + 1]; - for (i_t p = col_beg; p < col_end; ++p) { - augmented_CSR.j[q] = AT.i[p]; - augmented_CSR.x[q++] = AT.x[p]; + { + raft::common::nvtx::range scope("Barrier: augmented: device upload"); + device_augmented.copy(augmented_CSR, handle_ptr->get_stream()); + d_augmented_diagonal_indices_.resize(augmented_diagonal_indices.size(), + handle_ptr->get_stream()); + raft::copy(d_augmented_diagonal_indices_.data(), + augmented_diagonal_indices.data(), + augmented_diagonal_indices.size(), + handle_ptr->get_stream()); + + if (has_soc) { + d_cone_csr_indices_.resize(total_block_nnz, handle_ptr->get_stream()); + raft::copy(d_cone_csr_indices_.data(), + cone_csr_indices_host.data(), + total_block_nnz, + handle_ptr->get_stream()); + d_cone_Q_values_.resize(total_block_nnz, handle_ptr->get_stream()); + raft::copy(d_cone_Q_values_.data(), + cone_Q_values_host.data(), + total_block_nnz, + handle_ptr->get_stream()); } - augmented_diagonal_indices[k] = q; - augmented_CSR.j[q] = k; - augmented_CSR.x[q++] = primal_perturb; - } - augmented_CSR.row_start[n + m] = q; - augmented_CSR.nz_max = q; - augmented_CSR.j.resize(q); - augmented_CSR.x.resize(q); - settings_.log.debug("augmented nz %d predicted %d\n", q, off_diag_Qnz + nnzA + n); - cuopt_assert(q == 2 * nnzA + n + m + off_diag_Qnz, "augmented nnz != predicted"); - cuopt_assert(A.col_start[n] == AT.col_start[m], "A nz != AT nz"); - - device_augmented.copy(augmented_CSR, handle_ptr->get_stream()); - d_augmented_diagonal_indices_.resize(augmented_diagonal_indices.size(), - handle_ptr->get_stream()); - raft::copy(d_augmented_diagonal_indices_.data(), - augmented_diagonal_indices.data(), - augmented_diagonal_indices.size(), - handle_ptr->get_stream()); - handle_ptr->sync_stream(); + + handle_ptr->sync_stream(); + } #ifdef CHECK_SYMMETRY csc_matrix_t augmented_transpose(1, 1, 1); augmented.transpose(augmented_transpose); @@ -537,20 +778,26 @@ class iteration_data_t { cuopt_assert(error.norm1() <= 1e-2, "|| Aug - Aug^T ||_1 > 1e-2"); #endif } else { + const i_t linear_n = has_soc ? cone_start() : n; + + // Primal diagonal: linear block includes dual_perturb; SOC block is filled by scatter below. + // Native free variables use barrier D = 0 in augmented_multiply; omit span_diag[j] here so + // the factorized matrix matches the matvec (only -q_diag - dual_perturb on the diagonal). thrust::for_each_n(rmm::exec_policy(handle_ptr->get_stream()), thrust::make_counting_iterator(0), - i_t(n), + linear_n, [span_x = cuopt::make_span(device_augmented.x), span_diag_indices = cuopt::make_span(d_augmented_diagonal_indices_), span_q_diag = cuopt::make_span(d_Q_diag_), span_diag = cuopt::make_span(d_diag_), + span_is_native_free_linear = cuopt::make_span(d_is_native_free_linear_), dual_perturb_value = dual_perturb] __device__(i_t j) { - f_t q_diag = span_q_diag.size() > 0 ? span_q_diag[j] : 0.0; - span_x[span_diag_indices[j]] = - -q_diag - span_diag[j] - dual_perturb_value; + f_t q_diag = span_q_diag.size() > 0 ? span_q_diag[j] : 0.0; + const f_t d_j = (span_is_native_free_linear.size() > 0 && span_is_native_free_linear[j]) ? f_t(0) : span_diag[j]; + span_x[span_diag_indices[j]] = -q_diag - d_j - dual_perturb_value; }); - RAFT_CHECK_CUDA(handle_ptr->get_stream()); + thrust::for_each_n(rmm::exec_policy(handle_ptr->get_stream()), thrust::make_counting_iterator(n), i_t(m), @@ -560,6 +807,16 @@ class iteration_data_t { span_x[span_diag_indices[j]] = primal_perturb_value; }); RAFT_CHECK_CUDA(handle_ptr->get_stream()); + + if (has_soc) { + scatter_hessian_into_augmented(cones(), + device_augmented.x, + d_cone_csr_indices_, + d_cone_Q_values_, + handle_ptr->get_stream(), + dual_perturb); + RAFT_CHECK_CUDA(handle_ptr->get_stream()); + } } } @@ -570,43 +827,53 @@ class iteration_data_t { float64_t start_form_adat = tic(); const i_t m = AD.m; - raft::copy(device_AD.x.data(), - d_original_A_values.data(), - d_original_A_values.size(), - handle_ptr->get_stream()); - if (n_dense_columns > 0) { - // Adjust inv_diag - d_inv_diag_prime.resize(AD.n, stream_view_); - // Copy If - cub::DeviceSelect::Flagged( - d_flag_buffer.data(), - flag_buffer_size, - d_inv_diag.data(), - thrust::make_transform_iterator(d_cols_to_remove.data(), cuda::std::logical_not{}), - d_inv_diag_prime.data(), - d_num_flag.data(), - d_inv_diag.size(), - stream_view_); - RAFT_CHECK_CUDA(stream_view_); - } else { - d_inv_diag_prime.resize(inv_diag.size(), stream_view_); - raft::copy(d_inv_diag_prime.data(), d_inv_diag.data(), inv_diag.size(), stream_view_); + { + raft::common::nvtx::range scope("Barrier: Form ADAT: restore A"); + raft::copy(device_AD.x.data(), + d_original_A_values.data(), + d_original_A_values.size(), + handle_ptr->get_stream()); + } + { + raft::common::nvtx::range scope("Barrier: Form ADAT: inv_diag prime"); + if (n_dense_columns > 0) { + // Adjust inv_diag + d_inv_diag_prime.resize(AD.n, stream_view_); + // Copy If + cub::DeviceSelect::Flagged( + d_flag_buffer.data(), + flag_buffer_size, + d_inv_diag.data(), + thrust::make_transform_iterator(d_cols_to_remove.data(), cuda::std::logical_not{}), + d_inv_diag_prime.data(), + d_num_flag.data(), + d_inv_diag.size(), + stream_view_); + RAFT_CHECK_CUDA(stream_view_); + } else { + d_inv_diag_prime.resize(inv_diag.size(), stream_view_); + raft::copy(d_inv_diag_prime.data(), d_inv_diag.data(), inv_diag.size(), stream_view_); + } } cuopt_assert(static_cast(d_inv_diag_prime.size()) == AD.n, "inv_diag_prime.size() != AD.n"); - thrust::for_each_n(rmm::exec_policy(stream_view_), - thrust::make_counting_iterator(0), - i_t(device_AD.x.size()), - [span_x = cuopt::make_span(device_AD.x), - span_scale = cuopt::make_span(d_inv_diag_prime), - span_col_ind = cuopt::make_span(device_AD.col_index)] __device__(i_t i) { - span_x[i] *= span_scale[span_col_ind[i]]; - }); - RAFT_CHECK_CUDA(stream_view_); + { + raft::common::nvtx::range scope("Barrier: Form ADAT: scale AD"); + thrust::for_each_n(rmm::exec_policy(stream_view_), + thrust::make_counting_iterator(0), + i_t(device_AD.x.size()), + [span_x = cuopt::make_span(device_AD.x), + span_scale = cuopt::make_span(d_inv_diag_prime), + span_col_ind = cuopt::make_span(device_AD.col_index)] __device__(i_t i) { + span_x[i] *= span_scale[span_col_ind[i]]; + }); + RAFT_CHECK_CUDA(stream_view_); + } if (settings_.concurrent_halt != nullptr && *settings_.concurrent_halt == 1) { return; } if (first_call) { + raft::common::nvtx::range scope("Barrier: Form ADAT: cusparse init"); try { initialize_cusparse_data( handle_ptr, device_A, device_AD, device_ADAT, cusparse_info); @@ -617,8 +884,11 @@ class iteration_data_t { } if (settings_.concurrent_halt != nullptr && *settings_.concurrent_halt == 1) { return; } - multiply_kernels(handle_ptr, device_A, device_AD, device_ADAT, cusparse_info); - handle_ptr->sync_stream(); + { + raft::common::nvtx::range scope("Barrier: Form ADAT: ADAT multiply"); + multiply_kernels(handle_ptr, device_A, device_AD, device_ADAT, cusparse_info); + handle_ptr->sync_stream(); + } auto adat_nnz = device_ADAT.row_start.element(device_ADAT.m, handle_ptr->get_stream()); float64_t adat_time = toc(start_form_adat); @@ -1436,8 +1706,9 @@ class iteration_data_t { f_t beta, rmm::device_uvector& y) { - const i_t m = A.m; - const i_t n = A.n; + const i_t m = A.m; + const i_t n = A.n; + const bool has_soc = has_cones(); rmm::device_uvector d_x1(n, handle_ptr->get_stream()); rmm::device_uvector d_x2(m, handle_ptr->get_stream()); @@ -1452,19 +1723,36 @@ class iteration_data_t { // y1 <- alpha ( -D * x_1 + A^T x_2) + beta * y1 rmm::device_uvector d_r1(n, handle_ptr->get_stream()); + thrust::fill_n(rmm::exec_policy(stream_view_), d_r1.begin(), n, f_t(0)); - // diag.pairwise_product(x1, r1); - // r1 <- D * x_1 - pairwise_multiply(d_x1.data(), d_diag_.data(), d_r1.data(), n, stream_view_); + // r1 <- D * x_1 on linear indices; barrier D is zero on native free variables + const i_t linear_n = has_soc ? cone_start() : n; + if (n_native_free_linear > 0) { + pairwise_multiply_skip_native_free_linear( + d_x1.data(), d_diag_.data(), d_is_native_free_linear_.data(), d_r1.data(), linear_n, stream_view_); + } else { + pairwise_multiply(d_x1.data(), d_diag_.data(), d_r1.data(), linear_n, stream_view_); + } + RAFT_CHECK_CUDA(stream_view_); - // r1 <- Q x1 + D x1 + // r1 <- D * x_1 + H x_1 (cone Hessian block H = S^T S; accumulate_cone_hessian_matvec) + if (has_soc) { + const i_t m_c = cone_entry_count(); + accumulate_cone_hessian_matvec(raft::device_span(d_x1.data() + cone_start(), m_c), + cones(), + raft::device_span(d_r1.data() + cone_start(), m_c), + stream_view_); + RAFT_CHECK_CUDA(stream_view_); + } + + // r1 <- Q x1 + D x1 + H x1 (cone: same H as above) if (Q.n > 0) { // matrix_vector_multiply(Q, 1.0, x1, 1.0, r1); cusparse_Q_view_.spmv(1.0, d_x1, 1.0, d_r1); } // y1 <- - alpha * r1 + beta * y1 - // y1.axpy(-alpha, r1, beta); + // flip the sign of r1 = (Q x1 + D x1 + H x1) axpy(-alpha, d_r1.data(), beta, d_y1.data(), d_y1.data(), n, stream_view_); // matrix_transpose_vector_multiply(A, alpha, x2, 1.0, y1); @@ -1554,13 +1842,22 @@ class iteration_data_t { std::vector Qdiag; bool Q_diagonal; rmm::device_uvector d_augmented_diagonal_indices_; + rmm::device_uvector d_cone_csr_indices_; + rmm::device_uvector d_cone_Q_values_; bool indefinite_Q; cusparse_view_t cusparse_Q_view_; + std::optional> cones_; + i_t cone_var_start_ = 0; + bool use_augmented; i_t symbolic_status; - i_t n_free_vars{0}; - rmm::device_uvector d_is_free_; // 1 if variable is free (QP only), 0 otherwise + i_t n_native_free_linear{0}; + rmm::device_uvector d_is_native_free_linear_; // 1 if native free linear (j < cone_start), else 0 + + // Adaptive regularization for the augmented system + f_t dual_perturb{1e-8}; + f_t primal_perturb{1e-8}; std::unique_ptr> chol; @@ -1656,6 +1953,8 @@ class iteration_data_t { rmm::device_uvector d_complementarity_xz_rhs_; rmm::device_uvector d_complementarity_wv_rhs_; rmm::device_uvector d_dual_rhs_; + rmm::device_uvector d_complementarity_target_; + rmm::device_uvector d_cone_hessian_dx_; rmm::device_uvector d_Q_diag_; rmm::device_uvector d_Qx_; @@ -1665,6 +1964,9 @@ class iteration_data_t { transform_reduce_helper_t transform_reduce_helper_; sum_reduce_helper_t sum_reduce_helper_; + bool cone_combined_step_; + f_t cone_sigma_mu_; + rmm::cuda_stream_view stream_view_; const simplex_solver_settings_t& settings_; @@ -1763,6 +2065,8 @@ int barrier_solver_t::initial_point(iteration_data_t& data) { raft::common::nvtx::range fun_scope("Barrier: initial_point"); const bool use_augmented = data.use_augmented; + const bool has_soc = data.has_cones(); + const bool has_native_free_linear = data.n_native_free_linear > 0; // Perform a numerical factorization i_t status; @@ -1892,6 +2196,50 @@ int barrier_solver_t::initial_point(iteration_data_t& data) dense_vector_t dual_res(lp.num_cols); float64_t epsilon_adjust = 10.0; + + // Data-dependent initial point for SOCP, following SeDuMi (Sturm, 1999). + // mu = sqrt((1 + ||b||_inf) * (1 + ||c||_inf)) + // primal and dual: x = mu * e_K, z = mu * e_K + // where e_K is the identity of the symmetric cone: + // LP block: e = 1, SOC block: e = (sqrt(2), 0, ..., 0) + if (has_soc) { + const i_t cs = data.cone_start(); + const f_t norm_b = vector_norm_inf(lp.rhs); + const f_t norm_c = vector_norm_inf(lp.objective); + const f_t mu = std::sqrt((1.0 + norm_b) * (1.0 + norm_c)); + const f_t sqrt2 = std::sqrt(2.0); + const f_t x_soc = mu * sqrt2; + const f_t z_soc = mu * sqrt2; + // Linear orthant + for (i_t j = 0; j < cs; ++j) { + data.x[j] = mu; + data.z[j] = mu; + } + if (has_native_free_linear) { + for (i_t j : presolve_info.native_free_linear_indices) { + if (j < cs) { data.z[j] = 0.0; } + } + } + // SOC blocks + i_t off = 0; + for (i_t k = 0; k < static_cast(lp.second_order_cone_dims.size()); ++k) { + i_t q_k = lp.second_order_cone_dims[k]; + data.x[cs + off] = x_soc; + data.z[cs + off] = z_soc; + for (i_t j = 1; j < q_k; ++j) { + data.x[cs + off + j] = 0.0; + data.z[cs + off + j] = 0.0; + } + off += q_k; + } + data.y.set_scalar(0.0); + if (data.n_upper_bounds > 0) { + data.w.set_scalar(mu); + data.v.set_scalar(mu); + } + return 0; + } + if (settings.barrier_dual_initial_point == -1 || settings.barrier_dual_initial_point == 0) { // Use the dual starting point suggested by the paper // On Implementing Mehrotra’s Predictor–Corrector Interior-Point Method for Linear Programming @@ -1924,8 +2272,10 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.v[k] = -c[j] + epsilon; } } - // Now hande the case with no upper bounds + // Now handle the case with no upper bounds (skip cone variables) + const i_t cone_end = data.cone_end(); for (i_t j = 0; j < lp.num_cols; j++) { + if (has_soc && j >= data.cone_start() && j < cone_end) continue; if (lp.upper[j] == inf) { if (c[j] > epsilon_adjust) { data.z[j] = c[j]; @@ -1935,8 +2285,8 @@ int barrier_solver_t::initial_point(iteration_data_t& data) } } // Free variables have z = 0 (no complementarity condition) - if (data.n_free_vars > 0) { - for (i_t j : presolve_info.free_variable_indices) { + if (has_native_free_linear) { + for (i_t j : presolve_info.native_free_linear_indices) { data.z[j] = 0.0; } } @@ -1960,7 +2310,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.v.multiply_scalar(-1.0); data.v.ensure_positive(epsilon_adjust); - data.z.ensure_positive(epsilon_adjust); + data.z.ensure_positive_skip_range(epsilon_adjust, data.cone_start(), data.cone_entry_count()); } else { // First compute rhs = A*Dinv*c dense_vector_t rhs(lp.num_rows); @@ -1984,7 +2334,7 @@ int barrier_solver_t::initial_point(iteration_data_t& data) data.gather_upper_bounds(data.z, data.v); data.v.multiply_scalar(-1.0); data.v.ensure_positive(epsilon_adjust); - data.z.ensure_positive(epsilon_adjust); + data.z.ensure_positive_skip_range(epsilon_adjust, data.cone_start(), data.cone_entry_count()); } // Verify A'*y + z - E*v - Q*x = c @@ -2001,24 +2351,35 @@ int barrier_solver_t::initial_point(iteration_data_t& data) settings.log.printf("||A^T y + z - E*v - Q*x - c ||: %e\n", vector_norm2(data.dual_residual)); #endif - // Make sure (w, x, v, z) > 0 - if (data.n_free_vars > 0) { + // Make sure (w, x, v, z) > 0. Cone blocks are not nonnegative orthant variables — skip them for + // box positivity; NT scaling keeps cone iterates feasible separately. + std::vector free_x_save; + if (has_native_free_linear) { + free_x_save.resize(data.n_native_free_linear); + for (i_t k = 0; k < data.n_native_free_linear; ++k) { + free_x_save[k] = data.x[presolve_info.native_free_linear_indices[k]]; + } + } + data.w.ensure_positive(epsilon_adjust); + if (has_soc) { + data.x.ensure_positive_skip_range(epsilon_adjust, data.cone_start(), data.cone_entry_count()); + } else if (has_native_free_linear) { std::vector nonnegative_variables(data.x.size(), 1); - for (i_t j : presolve_info.free_variable_indices) { + for (i_t j : presolve_info.native_free_linear_indices) { nonnegative_variables[j] = 0; } - data.x.ensure_positive(epsilon_adjust, nonnegative_variables); - - for (i_t j : presolve_info.free_variable_indices) { - data.z[j] = 0.0; - } - } else { data.x.ensure_positive(epsilon_adjust); } - data.w.ensure_positive(epsilon_adjust); - + // Native free variables (QP/SOCP): restore possibly negative x and set z = 0 + if (has_native_free_linear) { + for (i_t k = 0; k < data.n_native_free_linear; ++k) { + i_t j = presolve_info.native_free_linear_indices[k]; + data.x[j] = free_x_save[k]; + data.z[j] = 0.0; + } + } #ifdef PRINT_INFO settings.log.printf("min v %e min z %e\n", data.v.minimum(), data.z.minimum()); #endif @@ -2202,10 +2563,27 @@ void barrier_solver_t::gpu_compute_residual_norms(const rmm::device_uv primal_residual_norm = std::max(device_vector_norm_inf(data.d_primal_residual_, stream_view_), device_vector_norm_inf(data.d_bound_residual_, stream_view_)); - dual_residual_norm = device_vector_norm_inf(data.d_dual_residual_, stream_view_); + dual_residual_norm = device_vector_norm_inf(data.d_dual_residual_, stream_view_); + const bool has_soc = data.has_cones(); + const i_t linear_xz_size = data.linear_xz_size(data.d_complementarity_xz_residual_.size()); + auto linear_xz_span = + raft::device_span(data.d_complementarity_xz_residual_.data(), linear_xz_size); complementarity_residual_norm = - std::max(device_vector_norm_inf(data.d_complementarity_xz_residual_, stream_view_), + std::max(device_vector_norm_inf(linear_xz_span, stream_view_), device_vector_norm_inf(data.d_complementarity_wv_residual_, stream_view_)); + if (has_soc) { + f_t cone_complementarity_norm = f_t(0); + auto cone_dot = data.cones().scratch.template get_slot<0>(); + data.cones().segmented_sum( + data.d_complementarity_xz_residual_.data() + data.cone_start(), cone_dot, stream_view_); + cone_complementarity_norm = thrust::reduce(rmm::exec_policy(stream_view_), + cone_dot.begin(), + cone_dot.end(), + f_t(0), + thrust::maximum()); + complementarity_residual_norm = + std::max(complementarity_residual_norm, cone_complementarity_norm); + } } template @@ -2213,42 +2591,58 @@ f_t barrier_solver_t::gpu_max_step_to_boundary(iteration_data_t& x, const rmm::device_uvector& dx) { - // For x-sized vectors with free variables, skip free vars in ratio test - const bool has_free = data.n_free_vars > 0 && static_cast(x.size()) == lp.num_cols; - - if (has_free) { - auto is_free_ptr = data.d_is_free_.data(); - auto ratio_test_free = [is_free_ptr] HD(const thrust::tuple t) { - const f_t dx_val = thrust::get<0>(t); - const f_t x_val = thrust::get<1>(t); - const i_t is_free = thrust::get<2>(t); - if (is_free) return f_t(1.0); - if (dx_val < f_t(0.0)) return -x_val / dx_val; + const bool has_soc = data.has_cones() && static_cast(x.size()) >= data.cone_end(); + const bool has_native_free_linear = data.n_native_free_linear > 0 && static_cast(x.size()) == lp.num_cols; + + auto reduce_segment = [&](i_t start, i_t len) -> f_t { + if (len <= 0) { return f_t(1); } + if (has_native_free_linear) { + auto native_free_linear_ptr = data.d_is_native_free_linear_.data() + start; + // step size computation for nonnegative variables with free variables + auto ratio_test_free = [native_free_linear_ptr] HD(const thrust::tuple t) { + const f_t dx_val = thrust::get<0>(t); + const f_t x_val = thrust::get<1>(t); + const i_t is_native_free_linear = thrust::get<2>(t); + if (is_native_free_linear) return f_t(1.0); + if (dx_val < f_t(0.0)) return -x_val / dx_val; + return f_t(1.0); + }; + return data.transform_reduce_helper_.transform_reduce( + thrust::make_zip_iterator(dx.data() + start, x.data() + start, native_free_linear_ptr), + thrust::minimum(), + ratio_test_free, + f_t(1.0), + len, + stream_view_); + } + // step size computation for nonnegative variables without free variables + auto ratio_test = [] HD(const thrust::tuple t) { + const f_t dx = thrust::get<0>(t); + const f_t x = thrust::get<1>(t); + if (dx < f_t(0.0)) return -x / dx; return f_t(1.0); }; - return data.transform_reduce_helper_.transform_reduce( - thrust::make_zip_iterator(dx.data(), x.data(), is_free_ptr), + thrust::make_zip_iterator(dx.data() + start, x.data() + start), thrust::minimum(), - ratio_test_free, + ratio_test, f_t(1.0), - x.size(), + len, stream_view_); - } + }; - return data.transform_reduce_helper_.transform_reduce( - thrust::make_zip_iterator(dx.data(), x.data()), - thrust::minimum(), - [] HD(const thrust::tuple t) { - const f_t dx = thrust::get<0>(t); - const f_t x = thrust::get<1>(t); + // step size computation for when there are soc constraints + if (has_soc) { + const i_t cs = data.cone_start(); + const i_t mc = data.cone_entry_count(); + f_t alpha = reduce_segment(0, cs); + const i_t tail_start = cs + mc; + const i_t tail_size = static_cast(x.size()) - tail_start; + if (tail_size > 0) { alpha = std::min(alpha, reduce_segment(tail_start, tail_size)); } + return alpha; + } - if (dx < f_t(0.0)) return -x / dx; - return f_t(1.0); - }, - f_t(1.0), - x.size(), - stream_view_); + return reduce_segment(0, static_cast(x.size())); } template @@ -2258,98 +2652,136 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t& dy, pinned_dense_vector_t& dv, pinned_dense_vector_t& dz, + f_t& dual_perturb, + f_t& primal_perturb, f_t& max_residual) { raft::common::nvtx::range fun_scope("Barrier: compute_search_direction"); const bool debug = false; const bool use_augmented = data.use_augmented; + const bool has_soc = data.has_cones(); + const bool has_native_free_linear = data.n_native_free_linear > 0; + const i_t m_c = data.cone_entry_count(); + const i_t cone_var_start = data.cone_start(); + const i_t linear_size = data.linear_xz_size(lp.num_cols); + + // Linear (orthant) block only; SOC uses recover_cone_dz_from_target. + auto recover_linear_orthant_dz = [&](raft::device_span target, + raft::device_span z, + raft::device_span dx_span, + raft::device_span x, + raft::device_span dz_span) { + if (dz_span.empty()) return; + if (has_native_free_linear) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple( + target.data(), z.data(), dx_span.data(), x.data(), data.d_is_native_free_linear_.data()), + dz_span.data(), + dz_span.size(), + [] HD(f_t target_val, f_t z_val, f_t dx_val, f_t x_val, i_t is_native_free_linear) { + if (is_native_free_linear) return f_t(0); + return target_val - (z_val * dx_val) / x_val; + }, + stream_view_.value()); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(target.data(), z.data(), dx_span.data(), x.data()), + dz_span.data(), + dz_span.size(), + [] HD(f_t target_val, f_t z_val, f_t dx_val, f_t x_val) { + return target_val - (z_val * dx_val) / x_val; + }, + stream_view_.value()); + } + RAFT_CHECK_CUDA(stream_view_); + }; - { - raft::common::nvtx::range fun_scope("Barrier: GPU allocation and copies"); - - // TMP allocation and copy should happen only once where it's written in a first place - data.d_bound_rhs_.resize(data.bound_rhs.size(), stream_view_); - raft::copy( - data.d_bound_rhs_.data(), data.bound_rhs.data(), data.bound_rhs.size(), stream_view_); - data.d_x_.resize(data.x.size(), stream_view_); - raft::copy(data.d_x_.data(), data.x.data(), data.x.size(), stream_view_); - data.d_z_.resize(data.z.size(), stream_view_); - raft::copy(data.d_z_.data(), data.z.data(), data.z.size(), stream_view_); - data.d_w_.resize(data.w.size(), stream_view_); - raft::copy(data.d_w_.data(), data.w.data(), data.w.size(), stream_view_); - data.d_v_.resize(data.v.size(), stream_view_); - raft::copy(data.d_v_.data(), data.v.data(), data.v.size(), stream_view_); - data.d_upper_bounds_.resize(data.upper_bounds.size(), stream_view_); - raft::copy(data.d_upper_bounds_.data(), - data.upper_bounds.data(), - data.upper_bounds.size(), - stream_view_); - data.d_dy_.resize(dy.size(), stream_view_); - raft::copy(data.d_dy_.data(), dy.data(), dy.size(), stream_view_); - data.d_dx_.resize(dx.size(), stream_view_); - raft::copy(data.d_h_.data(), data.primal_rhs.data(), data.primal_rhs.size(), stream_view_); - raft::copy(data.d_dual_rhs_.data(), data.dual_rhs.data(), data.dual_rhs.size(), stream_view_); - data.d_dz_.resize(dz.size(), stream_view_); - data.d_dv_.resize(dv.size(), stream_view_); - data.d_dw_.resize(data.bound_rhs.size(), stream_view_); - raft::copy(data.d_dw_.data(), data.bound_rhs.data(), data.bound_rhs.size(), stream_view_); - data.d_dw_residual_.resize(data.n_upper_bounds, stream_view_); - data.d_wv_residual_.resize(data.d_complementarity_wv_rhs_.size(), stream_view_); - data.d_xz_residual_.resize(data.d_complementarity_xz_rhs_.size(), stream_view_); - data.d_primal_residual_.resize(lp.rhs.size(), stream_view_); - raft::copy(data.d_primal_residual_.data(), lp.rhs.data(), lp.rhs.size(), stream_view_); - data.d_bound_residual_.resize(data.bound_residual.size(), stream_view_); - data.d_upper_.resize(lp.upper.size(), stream_view_); - raft::copy(data.d_upper_.data(), lp.upper.data(), lp.upper.size(), stream_view_); - } + copy_step_rhs_to_device(data, stream_view_); + data.d_dx_.resize(dx.size(), stream_view_); + data.d_dy_.resize(dy.size(), stream_view_); + data.d_dz_.resize(dz.size(), stream_view_); + data.d_dv_.resize(dv.size(), stream_view_); // Solves the linear system // // dw dx dy dv dz - // [ 0 A 0 0 0 ] [ dw ] = [ rp ] - // [ I E' 0 0 0 ] [ dx ] [ rw ] - // [ 0 0 A' -E I ] [ dy ] [ rd ] - // [ 0 Z 0 0 X ] [ dv ] [ rxz ] - // [ V 0 0 W 0 ] [ dz ] [ rwv ] + // [ 0 A 0 0 0 ] [ dw ] = [ rp ] + // [ I E' 0 0 0 ] [ dx ] [ rw ] + // [ 0 0 A' -E I ] [ dy ] [ rd ] + // [ 0 Z(S) 0 0 X(S^-T)] [ dv ] [ rxz ] + // [ V 0 0 W 0 ] [ dz ] [ rwv ] + + // NT-scaling: + // \lambda = Sx = S^-T z + // Affine step: (\lambda + S \delta xa) \circ (\lambda + S^-T \delta za) = 0 + // S \delta xa + S^-T \delta za = - \lambda + // \delta za = -S^T (S \delta xa + \lambda) = - S^T S \delta xa -S^T \lambda= - S^T S \delta xa + // - z + if (has_soc && !data.cone_combined_step_) { + auto& cones = data.cones(); + cones.x = raft::device_span(data.d_x_.data() + cone_var_start, m_c); + cones.z = raft::device_span(data.d_z_.data() + cone_var_start, m_c); + launch_nt_scaling(cones, stream_view_); + } max_residual = 0.0; { raft::common::nvtx::range fun_scope("Barrier: GPU diag, inv diag and sqrt inv diag formation"); - // diag = z ./ x - // For native free variables (QP): use Q diagonal if available, otherwise a static regularizer - if (data.n_free_vars > 0) { - constexpr f_t free_var_reg = 1e-7; - if (data.Q.n > 0 && data.Q_diagonal) { + // diag = z ./ x on the linear (non-cone) coordinates; cone block is overwritten below. + if (has_native_free_linear) { + if (use_augmented) { + // Augmented KKT: D = 0 on native free linear vars (Q curvature is in the KKT block, not D). cub::DeviceTransform::Transform( cuda::std::make_tuple( - data.d_z_.data(), data.d_x_.data(), data.d_is_free_.data(), data.d_Q_diag_.data()), + data.d_z_.data(), data.d_x_.data(), data.d_is_native_free_linear_.data()), data.d_diag_.data(), - data.d_diag_.size(), - [free_var_reg] HD(f_t z_j, f_t x_j, i_t is_free, f_t q_jj) { - if (!is_free) return z_j / x_j; - return (q_jj > f_t(0)) ? f_t(0) : free_var_reg; + linear_size, + [] HD(f_t z_j, f_t x_j, i_t is_native_free_linear) { + return is_native_free_linear ? f_t(0) : (z_j / x_j); }, stream_view_.value()); } else { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_z_.data(), data.d_x_.data(), data.d_is_free_.data()), - data.d_diag_.data(), - data.d_diag_.size(), - [free_var_reg] HD(f_t z_j, f_t x_j, i_t is_free) { - return is_free ? free_var_reg : (z_j / x_j); - }, - stream_view_.value()); + constexpr f_t free_var_reg = 1e-7; + if (data.Q.n > 0 && data.Q_diagonal) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple( + data.d_z_.data(), data.d_x_.data(), data.d_is_native_free_linear_.data(), data.d_Q_diag_.data()), + data.d_diag_.data(), + linear_size, + [free_var_reg] HD(f_t z_j, f_t x_j, i_t is_native_free_linear, f_t q_jj) { + if (!is_native_free_linear) return z_j / x_j; + return (q_jj > f_t(0)) ? f_t(0) : free_var_reg; + }, + stream_view_.value()); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_z_.data(), data.d_x_.data(), data.d_is_native_free_linear_.data()), + data.d_diag_.data(), + linear_size, + [free_var_reg] HD(f_t z_j, f_t x_j, i_t is_native_free_linear) { + return is_native_free_linear ? free_var_reg : (z_j / x_j); + }, + stream_view_.value()); + } } } else { cub::DeviceTransform::Transform(cuda::std::make_tuple(data.d_z_.data(), data.d_x_.data()), data.d_diag_.data(), - data.d_diag_.size(), + linear_size, cuda::std::divides<>{}, stream_view_.value()); } RAFT_CHECK_CUDA(stream_view_); + + // TODO: need to double check this + // Set cone diagonal part to be 1 + if (has_soc) { + thrust::fill_n( + rmm::exec_policy(stream_view_), data.d_diag_.begin() + cone_var_start, m_c, f_t(1)); + } + // diag = z ./ x + E * (v ./ w) * E' if (data.n_upper_bounds > 0) { @@ -2365,8 +2797,9 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t 0 && data.Q_diagonal) { + // In ADAT mode, diagonal Q is folded into D. In augmented mode, Q is an explicit block in the + // KKT matrix, so adding it here would double count the quadratic objective. + if (!use_augmented && data.Q.n > 0 && data.Q_diagonal) { cub::DeviceTransform::Transform( cuda::std::make_tuple(data.d_Q_diag_.data(), data.d_diag_.data()), data.d_diag_.data(), @@ -2376,18 +2809,25 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t::gpu_compute_search_direction(iteration_data_t::gpu_compute_search_direction(iteration_data_t 0) { + if (data.n_upper_bounds > 0) { cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_inv_diag.data(), - data.d_tmp3_.data(), - data.d_complementarity_xz_rhs_.data(), - data.d_x_.data(), + cuda::std::make_tuple(data.d_bound_rhs_.data(), + data.d_v_.data(), + data.d_complementarity_wv_rhs_.data(), + data.d_w_.data()), + thrust::make_permutation_iterator(data.d_tmp3_.data(), data.d_upper_bounds_.data()), + data.n_upper_bounds, + [] HD(f_t bound_rhs, f_t v, f_t complementarity_wv_rhs, f_t w) { + return (complementarity_wv_rhs - v * bound_rhs) / w; + }, + stream_view_.value()); + RAFT_CHECK_CUDA(stream_view_); + } + if (has_native_free_linear) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_tmp3_.data(), + data.d_complementarity_target_.data(), data.d_dual_rhs_.data(), - data.d_is_free_.data()), - thrust::make_zip_iterator(data.d_tmp3_.data(), data.d_tmp4_.data()), + data.d_is_native_free_linear_.data()), + data.d_tmp3_.data(), lp.num_cols, - [] HD(f_t inv_diag, f_t tmp3, f_t complementarity_xz_rhs, f_t x, f_t dual_rhs, i_t is_free) - -> thrust::tuple { - const f_t xz_term = is_free ? f_t(0) : (complementarity_xz_rhs / x); - const f_t tmp = tmp3 - xz_term + dual_rhs; - return {tmp, inv_diag * tmp}; + [] HD(f_t tmp3, f_t target, f_t dual_rhs, i_t is_native_free_linear) { + const f_t comp_term = is_native_free_linear ? f_t(0) : target; + return tmp3 + dual_rhs - comp_term; }, stream_view_.value()); } else { cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_inv_diag.data(), - data.d_tmp3_.data(), - data.d_complementarity_xz_rhs_.data(), - data.d_x_.data(), - data.d_dual_rhs_.data()), - thrust::make_zip_iterator(data.d_tmp3_.data(), data.d_tmp4_.data()), + cuda::std::make_tuple( + data.d_tmp3_.data(), data.d_dual_rhs_.data(), data.d_complementarity_target_.data()), + data.d_tmp3_.data(), lp.num_cols, - [] HD(f_t inv_diag, f_t tmp3, f_t complementarity_xz_rhs, f_t x, f_t dual_rhs) - -> thrust::tuple { - const f_t tmp = tmp3 + -(complementarity_xz_rhs / x) + dual_rhs; - return {tmp, inv_diag * tmp}; - }, + [] HD(f_t tmp3, f_t dual_rhs, f_t target) { return tmp3 + dual_rhs - target; }, stream_view_.value()); } RAFT_CHECK_CUDA(stream_view_); raft::copy(data.d_r1_.data(), data.d_tmp3_.data(), data.d_tmp3_.size(), stream_view_); raft::copy(data.d_r1_prime_.data(), data.d_tmp3_.data(), data.d_tmp3_.size(), stream_view_); + } - // h <- A @ tmp4 .+ primal_rhs + if (!use_augmented) { + raft::common::nvtx::range fun_scope("Barrier: GPU compute H"); + cub::DeviceTransform::Transform( + cuda::std::make_tuple(data.d_inv_diag.data(), data.d_tmp3_.data()), + data.d_tmp4_.data(), + lp.num_cols, + [] HD(f_t inv_diag, f_t tmp3) { return inv_diag * tmp3; }, + stream_view_.value()); + RAFT_CHECK_CUDA(stream_view_); data.cusparse_view_.spmv(1, data.cusparse_tmp4_, 1, data.cusparse_h_); } if (use_augmented) { raft::common::nvtx::range fun_scope("Barrier: GPU augmented solve"); - // r1 <- dual_rhs -complementarity_xz_rhs ./ x + E * ((complementarity_wv_rhs - v .* bound_rhs) - // ./ w) + // Augmented RHS [dx; dy]: primal block is d_r1_ (assembled above). + // linear j: dual_rhs[j] - complementarity_target[j] + // + E_j*((complementarity_wv_rhs - v.*bound_rhs)./w) (target = xz_rhs/x; free: 0) + // cone j: dual_rhs[j] - complementarity_target[j] (NT target: -z or combined centering term) + // Constraint block: primal_rhs. + // TODO: d_augmented_rhs can be preallocated rmm::device_uvector d_augmented_rhs(lp.num_cols + lp.num_rows, stream_view_); raft::copy(d_augmented_rhs.data(), data.d_r1_.data(), lp.num_cols, stream_view_); raft::copy( @@ -2525,10 +2966,33 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t(op, d_augmented_rhs, d_augmented_soln); + const f_t solve_err = + iterative_refinement(op, d_augmented_rhs, d_augmented_soln); if (solve_err > 1e-1) { settings.log.printf("|| Aug (dx, dy) - aug_rhs || %e after IR\n", solve_err); } + + // Adaptive regularization: increase/decrease based on IR quality. + // Only adapt on calls where we actually (re)factorized — the affine step. + if (did_factorize) { + constexpr f_t min_perturb = 1e-8; + constexpr f_t max_perturb = 1e-1; + if (solve_err > 1e-2) { + f_t old_dp = dual_perturb; + dual_perturb = std::min(max_perturb, dual_perturb * 10.0); + primal_perturb = std::min(max_perturb, primal_perturb * 10.0); + settings.log.printf( + " reg UP: %e -> %e (solve_err=%e)\n", old_dp, dual_perturb, solve_err); + } else if (solve_err < 1e-4) { + f_t old_dp = dual_perturb; + dual_perturb = std::max(min_perturb, dual_perturb / 10.0); + primal_perturb = std::max(min_perturb, primal_perturb / 10.0); + if (old_dp != dual_perturb) { + settings.log.printf( + " reg DOWN: %e -> %e (solve_err=%e)\n", old_dp, dual_perturb, solve_err); + } + } + } } raft::copy(data.d_dx_.data(), d_augmented_soln.data(), lp.num_cols, stream_view_); @@ -2774,34 +3238,23 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t 0) { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_complementarity_xz_rhs_.data(), - data.d_z_.data(), - data.d_dx_.data(), - data.d_x_.data(), - data.d_is_free_.data()), - data.d_dz_.data(), - data.d_dz_.size(), - [] HD(f_t complementarity_xz_rhs, f_t z, f_t dx, f_t x, i_t is_free) { - return is_free ? f_t(0) : ((complementarity_xz_rhs - z * dx) / x); - }, - stream_view_.value()); - } else { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(data.d_complementarity_xz_rhs_.data(), - data.d_z_.data(), - data.d_dx_.data(), - data.d_x_.data()), - data.d_dz_.data(), - data.d_dz_.size(), - [] HD(f_t complementarity_xz_rhs, f_t z, f_t dx, f_t x) { - return (complementarity_xz_rhs - z * dx) / x; - }, - stream_view_.value()); + const i_t linear_dz_size = has_soc ? cone_var_start : static_cast(data.d_dz_.size()); + + if (has_soc) { + recover_cone_dz_from_target( + raft::device_span(data.d_dx_.data() + cone_var_start, m_c), + data.cones(), + raft::device_span(data.d_complementarity_target_.data() + cone_var_start, m_c), + raft::device_span(data.d_dz_.data() + cone_var_start, m_c), + stream_view_); } + + recover_linear_orthant_dz( + raft::device_span(data.d_complementarity_target_.data(), linear_dz_size), + raft::device_span(data.d_z_.data(), linear_dz_size), + raft::device_span(data.d_dx_.data(), linear_dz_size), + raft::device_span(data.d_x_.data(), linear_dz_size), + raft::device_span(data.d_dz_.data(), linear_dz_size)); RAFT_CHECK_CUDA(stream_view_); raft::copy(dz.data(), data.d_dz_.data(), data.d_dz_.size(), stream_view_); } @@ -2810,18 +3263,29 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t out, + raft::device_span rhs, + raft::device_span z, + raft::device_span dz_span, + raft::device_span dx_span, + raft::device_span x) { + if (out.empty()) return; + cub::DeviceTransform::Transform( + cuda::std::make_tuple(rhs.data(), z.data(), dz_span.data(), dx_span.data(), x.data()), + out.data(), + out.size(), + [] HD(f_t complementarity_xz_rhs, f_t z_val, f_t dz_val, f_t dx_val, f_t x_val) { + return z_val * dx_val + x_val * dz_val - complementarity_xz_rhs; + }, + stream_view_.value()); + }; + compute_linear_xz_residual( + raft::device_span(data.d_xz_residual_.data(), linear_size), + raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), + raft::device_span(data.d_z_.data(), linear_size), + raft::device_span(data.d_dz_.data(), linear_size), + raft::device_span(data.d_dx_.data(), linear_size), + raft::device_span(data.d_x_.data(), linear_size)); RAFT_CHECK_CUDA(stream_view_); const f_t xz_residual_norm = device_vector_norm_inf(data.d_xz_residual_, stream_view_); @@ -2981,40 +3445,187 @@ i_t barrier_solver_t::gpu_compute_search_direction(iteration_data_t -void barrier_solver_t::compute_affine_rhs(iteration_data_t& data) +void copy_host_iterate_to_device(iteration_data_t& data, rmm::cuda_stream_view stream) { - raft::common::nvtx::range fun_scope("Barrier: compute_affine_rhs"); + raft::common::nvtx::range fun_scope("Barrier: copy_host_iterate_to_device"); + + data.d_x_.resize(data.x.size(), stream); + data.d_z_.resize(data.z.size(), stream); + raft::copy(data.d_x_.data(), data.x.data(), data.x.size(), stream); + raft::copy(data.d_z_.data(), data.z.data(), data.z.size(), stream); + + data.d_w_.resize(data.w.size(), stream); + data.d_v_.resize(data.v.size(), stream); + raft::copy(data.d_w_.data(), data.w.data(), data.w.size(), stream); + raft::copy(data.d_v_.data(), data.v.data(), data.v.size(), stream); + + data.d_y_.resize(data.y.size(), stream); + raft::copy(data.d_y_.data(), data.y.data(), data.y.size(), stream); + + data.d_upper_bounds_.resize(data.upper_bounds.size(), stream); + raft::copy(data.d_upper_bounds_.data(), + data.upper_bounds.data(), + data.upper_bounds.size(), + stream); +} - data.primal_rhs = data.primal_residual; - data.bound_rhs = data.bound_residual; - data.dual_rhs = data.dual_residual; +// One-time static problem data (constant across barrier iterations). +template +void copy_static_problem_data_to_device(const lp_problem_t& lp, + iteration_data_t& data, + rmm::cuda_stream_view stream) +{ + raft::common::nvtx::range fun_scope("Barrier: copy_static_problem_data_to_device"); - raft::copy(data.d_complementarity_xz_rhs_.data(), - data.d_complementarity_xz_residual_.data(), - data.d_complementarity_xz_residual_.size(), - stream_view_); - raft::copy(data.d_complementarity_wv_rhs_.data(), - data.d_complementarity_wv_residual_.data(), - data.d_complementarity_wv_residual_.size(), - stream_view_); + data.d_primal_residual_.resize(lp.rhs.size(), stream); + raft::copy(data.d_primal_residual_.data(), lp.rhs.data(), lp.rhs.size(), stream); + + data.d_upper_.resize(lp.upper.size(), stream); + raft::copy(data.d_upper_.data(), lp.upper.data(), lp.upper.size(), stream); + + data.d_bound_residual_.resize(data.bound_residual.size(), stream); + data.d_dw_residual_.resize(data.n_upper_bounds, stream); +} + +// Per Mehrotra step: affine/corrector RHS only (iterate stays on device from initial sync / next_iterate). +template +void copy_step_rhs_to_device(iteration_data_t& data, rmm::cuda_stream_view stream) +{ + raft::common::nvtx::range fun_scope("Barrier: copy_step_rhs_to_device"); + + data.d_bound_rhs_.resize(data.bound_rhs.size(), stream); + raft::copy( + data.d_bound_rhs_.data(), data.bound_rhs.data(), data.bound_rhs.size(), stream); + + raft::copy(data.d_h_.data(), data.primal_rhs.data(), data.primal_rhs.size(), stream); + raft::copy(data.d_dual_rhs_.data(), data.dual_rhs.data(), data.dual_rhs.size(), stream); + + data.d_dw_.resize(data.bound_rhs.size(), stream); + raft::copy(data.d_dw_.data(), data.bound_rhs.data(), data.bound_rhs.size(), stream); + + data.d_xz_residual_.resize(data.d_complementarity_xz_rhs_.size(), stream); + data.d_wv_residual_.resize(data.d_complementarity_wv_rhs_.size(), stream); +} + +template +void fill_linear_complementarity_target(iteration_data_t& data, + raft::device_span target, + raft::device_span xz_rhs, + raft::device_span x, + rmm::cuda_stream_view stream) +{ + if (target.empty()) return; + const bool has_native_free_linear = data.n_native_free_linear > 0; + if (has_native_free_linear) { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(xz_rhs.data(), x.data(), data.d_is_native_free_linear_.data()), + target.data(), + target.size(), + [] HD(f_t complementarity_xz_rhs, f_t x_val, i_t is_native_free_linear) { + if (is_native_free_linear) return f_t(0); + return complementarity_xz_rhs / x_val; + }, + stream.value()); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(xz_rhs.data(), x.data()), + target.data(), + target.size(), + [] HD(f_t complementarity_xz_rhs, f_t x_val) { return complementarity_xz_rhs / x_val; }, + stream.value()); + } + RAFT_CHECK_CUDA(stream); +} - // x.*z -> -x .* z +template +void fill_affine_cone_complementarity_target(iteration_data_t& data, + i_t cone_var_start, + i_t m_c, + rmm::cuda_stream_view stream) +{ + if (m_c == 0) return; + auto& cones = data.cones(); + cones.x = raft::device_span(data.d_x_.data() + cone_var_start, m_c); + cones.z = raft::device_span(data.d_z_.data() + cone_var_start, m_c); + auto cone_target = + raft::device_span(data.d_complementarity_target_.data() + cone_var_start, m_c); cub::DeviceTransform::Transform( - data.d_complementarity_xz_rhs_.data(), - data.d_complementarity_xz_rhs_.data(), - data.d_complementarity_xz_rhs_.size(), - [] HD(f_t xz_rhs) { return -xz_rhs; }, - stream_view_.value()); - RAFT_CHECK_CUDA(stream_view_); + cones.z.data(), + cone_target.data(), + m_c, + [] HD(f_t z_val) { return -z_val; }, + stream.value()); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + RAFT_CHECK_CUDA(stream); +} + +template +void fill_corrector_cone_complementarity_target(iteration_data_t& data, + i_t cone_var_start, + i_t m_c, + f_t sigma_mu, + rmm::cuda_stream_view stream) +{ + if (m_c == 0) return; + auto& cones = data.cones(); + cones.x = raft::device_span(data.d_x_.data() + cone_var_start, m_c); + cones.z = raft::device_span(data.d_z_.data() + cone_var_start, m_c); + auto cone_target = + raft::device_span(data.d_complementarity_target_.data() + cone_var_start, m_c); + compute_combined_cone_rhs_term(raft::device_span(data.d_dx_aff_.data() + cone_var_start, m_c), + raft::device_span(data.d_dz_aff_.data() + cone_var_start, m_c), + cones, + sigma_mu, + cone_target, + stream); +} + +template +void barrier_solver_t::compute_affine_rhs(iteration_data_t& data) +{ + raft::common::nvtx::range fun_scope("Barrier: compute_affine_rhs"); + const bool has_soc = data.has_cones(); + const i_t linear_size = data.linear_xz_size(lp.num_cols); + const i_t cone_var_start = data.cone_start(); + const i_t m_c = data.cone_entry_count(); + + data.primal_rhs = data.primal_residual; + data.bound_rhs = data.bound_residual; + data.dual_rhs = data.dual_residual; + data.cone_combined_step_ = false; + data.cone_sigma_mu_ = f_t(0); + + auto negate_complementarity_rhs = [&](raft::device_span out, + raft::device_span residual) { + if (out.empty()) return; + cub::DeviceTransform::Transform( + residual.data(), + out.data(), + out.size(), + [] HD(f_t rhs) { return -rhs; }, + stream_view_.value()); + }; + // xz -> -x .* z + negate_complementarity_rhs( + raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), + raft::device_span(data.d_complementarity_xz_residual_.data(), linear_size)); // w.*v -> -w .* v - cub::DeviceTransform::Transform( - data.d_complementarity_wv_rhs_.data(), - data.d_complementarity_wv_rhs_.data(), - data.d_complementarity_wv_rhs_.size(), - [] HD(f_t wv_rhs) { return -wv_rhs; }, - stream_view_.value()); + negate_complementarity_rhs(cuopt::make_span(data.d_complementarity_wv_rhs_), + cuopt::make_span(data.d_complementarity_wv_residual_)); RAFT_CHECK_CUDA(stream_view_); + + fill_linear_complementarity_target( + data, + raft::device_span(data.d_complementarity_target_.data(), linear_size), + raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), + raft::device_span(data.d_x_.data(), linear_size), + stream_view_); + if (has_soc) { + cuopt_assert(cone_var_start + m_c == lp.num_cols, "barrier expects [linear | cone] layout"); + fill_affine_cone_complementarity_target(data, cone_var_start, m_c, stream_view_); + } } template @@ -3022,6 +3633,7 @@ void barrier_solver_t::compute_target_mu( iteration_data_t& data, f_t mu, f_t& mu_aff, f_t& sigma, f_t& new_mu) { raft::common::nvtx::range fun_scope("Barrier: compute_target_mu"); + const bool has_soc = data.has_cones(); f_t complementarity_aff_sum = 0.0; // TMP no copy and data should always be on the GPU @@ -3040,7 +3652,23 @@ void barrier_solver_t::compute_target_mu( f_t step_dual_aff = std::min(gpu_max_step_to_boundary(data, data.d_v_, data.d_dv_aff_), gpu_max_step_to_boundary(data, data.d_z_, data.d_dz_aff_)); - if (data.Q.n > 0) { step_primal_aff = step_dual_aff = std::min(step_primal_aff, step_dual_aff); } + if (has_soc) { + i_t cs = data.cone_start(); + i_t mc = data.cone_entry_count(); + auto [cone_p, cone_d] = + compute_cone_step_length(data.cones(), + raft::device_span(data.d_dx_aff_.data() + cs, mc), + raft::device_span(data.d_dz_aff_.data() + cs, mc), + std::min(step_primal_aff, step_dual_aff), + stream_view_); + f_t cone_aff = std::min(cone_p, cone_d); + step_primal_aff = std::min(step_primal_aff, cone_aff); + step_dual_aff = step_primal_aff; + } + + if (data.Q.n > 0 || has_soc) { + step_primal_aff = step_dual_aff = std::min(step_primal_aff, step_dual_aff); + } // Compute complementarity_xz_aff_sum = sum(x_aff * z_aff), // where x_aff = x + step_primal_aff * dx_aff and z_aff = z + step_dual_aff * dz_aff @@ -3091,55 +3719,63 @@ void barrier_solver_t::compute_target_mu( stream_view_); complementarity_aff_sum = complementarity_xz_aff_sum + complementarity_wv_aff_sum; - f_t mu_denom = static_cast(data.x.size()) + static_cast(data.n_upper_bounds); - mu_denom -= static_cast(data.n_free_vars); - mu_denom = std::max(mu_denom, f_t(1.0)); - mu_aff = complementarity_aff_sum / mu_denom; - sigma = std::max(0.0, std::min(1.0, std::pow(mu_aff / mu, 3.0))); - new_mu = sigma * mu_aff; -} - -template -static void fill_linear_cc_rhs(iteration_data_t& data, - f_t new_mu, - raft::device_span out, - raft::device_span dx_aff, - raft::device_span dz_aff, - rmm::cuda_stream_view stream_view) -{ - if (out.empty()) return; - if (data.n_free_vars > 0) { - auto is_free_ptr = data.d_is_free_.data(); - cub::DeviceTransform::Transform( - cuda::std::make_tuple(dx_aff.data(), dz_aff.data(), is_free_ptr), - out.data(), - out.size(), - [new_mu] HD(f_t dx_aff_val, f_t dz_aff_val, i_t is_free) { - return is_free ? f_t(0) : (-(dx_aff_val * dz_aff_val) + new_mu); - }, - stream_view.value()); - } else { - cub::DeviceTransform::Transform( - cuda::std::make_tuple(dx_aff.data(), dz_aff.data()), - out.data(), - out.size(), - [new_mu] HD(f_t dx_aff_val, f_t dz_aff_val) { return -(dx_aff_val * dz_aff_val) + new_mu; }, - stream_view.value()); - } + const f_t mu_denom = data.complementarity_degree(data.x.size(), data.n_upper_bounds); + mu_aff = complementarity_aff_sum / mu_denom; + sigma = std::max(0.0, std::min(1.0, std::pow(mu_aff / mu, 3.0))); + new_mu = sigma * mu; } template void barrier_solver_t::compute_cc_rhs(iteration_data_t& data, f_t& new_mu) { raft::common::nvtx::range fun_scope("Barrier: compute_cc_rhs"); - - fill_linear_cc_rhs(data, - new_mu, - cuopt::make_span(data.d_complementarity_xz_rhs_), - cuopt::make_span(data.d_dx_aff_), - cuopt::make_span(data.d_dz_aff_), - stream_view_); + const bool has_soc = data.has_cones(); + const bool has_native_free_linear = data.n_native_free_linear > 0; + const i_t linear_size = data.linear_xz_size(lp.num_cols); + + auto fill_linear_cc_rhs = [&](raft::device_span out, + raft::device_span dx_aff, + raft::device_span dz_aff) { + if (out.empty()) return; + if (has_native_free_linear) { + auto native_free_linear_ptr = data.d_is_native_free_linear_.data(); + cub::DeviceTransform::Transform( + cuda::std::make_tuple(dx_aff.data(), dz_aff.data(), native_free_linear_ptr), + out.data(), + out.size(), + [new_mu] HD(f_t dx_aff_val, f_t dz_aff_val, i_t is_native_free_linear) { + return is_native_free_linear ? f_t(0) : (-(dx_aff_val * dz_aff_val) + new_mu); + }, + stream_view_.value()); + } else { + cub::DeviceTransform::Transform( + cuda::std::make_tuple(dx_aff.data(), dz_aff.data()), + out.data(), + out.size(), + [new_mu] HD(f_t dx_aff_val, f_t dz_aff_val) { return -(dx_aff_val * dz_aff_val) + new_mu; }, + stream_view_.value()); + } + }; + fill_linear_cc_rhs(raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), + raft::device_span(data.d_dx_aff_.data(), linear_size), + raft::device_span(data.d_dz_aff_.data(), linear_size)); RAFT_CHECK_CUDA(stream_view_); + + const i_t cone_var_start = data.cone_start(); + const i_t m_c = data.cone_entry_count(); + + fill_linear_complementarity_target( + data, + raft::device_span(data.d_complementarity_target_.data(), linear_size), + raft::device_span(data.d_complementarity_xz_rhs_.data(), linear_size), + raft::device_span(data.d_x_.data(), linear_size), + stream_view_); + if (has_soc) { + cuopt_assert(cone_var_start + m_c == lp.num_cols, "barrier expects [linear | cone] layout"); + fill_corrector_cone_complementarity_target( + data, cone_var_start, m_c, new_mu, stream_view_); + } + cub::DeviceTransform::Transform( cuda::std::make_tuple(data.d_dw_aff_.data(), data.d_dv_aff_.data()), data.d_complementarity_wv_rhs_.data(), @@ -3147,21 +3783,19 @@ void barrier_solver_t::compute_cc_rhs(iteration_data_t& data [new_mu] HD(f_t dw_aff, f_t dv_aff) { return -(dw_aff * dv_aff) + new_mu; }, stream_view_.value()); RAFT_CHECK_CUDA(stream_view_); - // TMP should be CPU to 0 if CPU and GPU to 0 if GPU data.primal_rhs.set_scalar(0.0); data.bound_rhs.set_scalar(0.0); data.dual_rhs.set_scalar(0.0); + data.cone_combined_step_ = has_soc; + data.cone_sigma_mu_ = has_soc ? new_mu : f_t(0); } template void barrier_solver_t::compute_final_direction(iteration_data_t& data) { raft::common::nvtx::range fun_scope("Barrier: compute_final_direction"); - // TODO Nicolas: Redundant copies - data.d_y_.resize(data.y.size(), stream_view_); data.d_dy_aff_.resize(data.dy_aff.size(), stream_view_); - raft::copy(data.d_y_.data(), data.y.data(), data.y.size(), stream_view_); raft::copy(data.d_dy_aff_.data(), data.dy_aff.data(), data.dy_aff.size(), stream_view_); #ifdef FINITE_CHECK @@ -3224,6 +3858,8 @@ void barrier_solver_t::compute_primal_dual_step_length(iteration_data_ f_t& step_dual) { raft::common::nvtx::range fun_scope("Barrier: compute_primal_dual_step_length"); + const bool has_soc = data.has_cones(); + f_t max_step_primal = 0.0; f_t max_step_dual = 0.0; max_step_primal = std::min(gpu_max_step_to_boundary(data, data.d_w_, data.d_dw_), @@ -3231,10 +3867,23 @@ void barrier_solver_t::compute_primal_dual_step_length(iteration_data_ max_step_dual = std::min(gpu_max_step_to_boundary(data, data.d_v_, data.d_dv_), gpu_max_step_to_boundary(data, data.d_z_, data.d_dz_)); + if (has_soc) { + i_t cs = data.cone_start(); + i_t mc = data.cone_entry_count(); + auto [cone_primal, cone_dual] = + compute_cone_step_length(data.cones(), + raft::device_span(data.d_dx_.data() + cs, mc), + raft::device_span(data.d_dz_.data() + cs, mc), + f_t(1), + stream_view_); + max_step_primal = std::min(max_step_primal, cone_primal); + max_step_dual = std::min(max_step_dual, cone_dual); + } + step_primal = step_scale * max_step_primal; step_dual = step_scale * max_step_dual; - if (data.Q.n > 0) { step_primal = step_dual = std::min(step_primal, step_dual); } + if (data.Q.n > 0 || has_soc) { step_primal = step_dual = std::min(step_primal, step_dual); } } template @@ -3325,11 +3974,8 @@ void barrier_solver_t::compute_mu(iteration_data_t& data, f_ { raft::common::nvtx::range fun_scope("Barrier: compute_mu"); - f_t mu_denom = static_cast(data.x.size()) + static_cast(data.n_upper_bounds); - mu_denom -= static_cast(data.n_free_vars); // free vars don't contribute to mu - mu_denom = std::max(mu_denom, f_t(1.0)); - - mu = (data.sum_reduce_helper_.sum(data.d_complementarity_xz_residual_.begin(), + const f_t mu_denom = data.complementarity_degree(data.x.size(), data.n_upper_bounds); + mu = (data.sum_reduce_helper_.sum(data.d_complementarity_xz_residual_.begin(), data.d_complementarity_xz_residual_.size(), stream_view_) + data.sum_reduce_helper_.sum(data.d_complementarity_wv_residual_.begin(), @@ -3595,6 +4241,10 @@ lp_status_t barrier_solver_t::solve(f_t start_time, lp_solution_t 0) { settings.log.printf("Quadratic objective matrix: %d nonzeros\n", lp.Q.row_start[lp.Q.n]); } + if (lp.second_order_cone_dims.size() > 0) { + settings.log.printf("Second-order cones: %d\n", + static_cast(lp.second_order_cone_dims.size())); + } settings.log.printf("\n"); // Compute the number of free variables @@ -3613,7 +4263,7 @@ lp_status_t barrier_solver_t::solve(f_t start_time, lp_solution_t 0) { create_Q(lp, Q); } iteration_data_t data( - lp, num_upper_bounds, presolve_info.free_variable_indices, Q, settings); + lp, num_upper_bounds, presolve_info.native_free_linear_indices, Q, settings); if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { settings.log.printf("Barrier solver halted\n"); return lp_status_t::CONCURRENT_LIMIT; @@ -3651,17 +4301,42 @@ lp_status_t barrier_solver_t::solve(f_t start_time, lp_solution_t(data, stream_view_); + copy_static_problem_data_to_device(lp, data, stream_view_); + compute_residuals>(data.w, data.x, data.y, data.v, data.z, data); f_t primal_residual_norm = std::max(vector_norm_inf(data.primal_residual, stream_view_), vector_norm_inf(data.bound_residual, stream_view_)); - f_t dual_residual_norm = vector_norm_inf(data.dual_residual, stream_view_); + f_t dual_residual_norm = vector_norm_inf(data.dual_residual, stream_view_); + const bool has_soc = data.has_cones(); + const i_t linear_xz_size = data.linear_xz_size(data.complementarity_xz_residual.size()); + auto linear_xz_span = + raft::host_span(data.complementarity_xz_residual.data(), linear_xz_size); + // compute complementarity residual norm for nonnegativity constraints f_t complementarity_residual_norm = - std::max(vector_norm_inf(data.complementarity_xz_residual, stream_view_), + std::max(vector_norm_inf(linear_xz_span, stream_view_), vector_norm_inf(data.complementarity_wv_residual, stream_view_)); - f_t mu = (data.complementarity_xz_residual.sum() + data.complementarity_wv_residual.sum()) / - (static_cast(n) + static_cast(num_upper_bounds)); + // compute complementarity residual norm for second-order cones + if (has_soc) { + f_t cone_complementarity_norm = f_t(0); + i_t off = data.cone_start(); + for (auto q_k : lp.second_order_cone_dims) { + f_t cone_dot = f_t(0); + for (i_t j = 0; j < q_k; ++j) { + cone_dot += data.complementarity_xz_residual[off + j]; + } + cone_complementarity_norm = std::max(cone_complementarity_norm, cone_dot); + off += q_k; + } + complementarity_residual_norm = + std::max(complementarity_residual_norm, cone_complementarity_norm); + } + f_t mu_denom = data.complementarity_degree(n, num_upper_bounds); + f_t mu = + (data.complementarity_xz_residual.sum() + data.complementarity_wv_residual.sum()) / mu_denom; f_t norm_b = vector_norm_inf(data.b, stream_view_); f_t norm_c = vector_norm_inf(data.c, stream_view_); @@ -3686,6 +4361,11 @@ lp_status_t barrier_solver_t::solve(f_t start_time, lp_solution_t::solve(f_t start_time, lp_solution_t::solve(f_t start_time, lp_solution_t::solve(f_t start_time, lp_solution_t::solve(f_t start_time, lp_solution_t::solve(f_t start_time, lp_solution_t::solve(f_t start_time, lp_solution_t::solve(f_t start_time, lp_solution_t& dy, pinned_dense_vector_t& dv, pinned_dense_vector_t& dz, + f_t& dual_perturb, + f_t& primal_perturb, f_t& max_residual); private: diff --git a/cpp/src/barrier/dense_vector.hpp b/cpp/src/barrier/dense_vector.hpp index 5d6f2b12ec..2d5d0b335c 100644 --- a/cpp/src/barrier/dense_vector.hpp +++ b/cpp/src/barrier/dense_vector.hpp @@ -184,6 +184,28 @@ class dense_vector_t : public std::vector { } } + void ensure_positive_skip_range(f_t epsilon_adjust, i_t skip_start, i_t skip_count) + { + if (skip_count == 0) { + ensure_positive(epsilon_adjust); + return; + } + const i_t n = this->size(); + const i_t skip_end = skip_start + skip_count; + f_t min_val = std::numeric_limits::max(); + for (i_t i = 0; i < n; i++) { + if (i >= skip_start && i < skip_end) continue; + min_val = std::min(min_val, (*this)[i]); + } + if (min_val <= 0.0) { + const f_t delta = -min_val + epsilon_adjust; + for (i_t i = 0; i < n; i++) { + if (i >= skip_start && i < skip_end) continue; + (*this)[i] += delta; + } + } + } + void ensure_positive(f_t epsilon_adjust, const std::vector& mask) { f_t min_x = inf; diff --git a/cpp/src/barrier/second_order_cone_kernels.cuh b/cpp/src/barrier/second_order_cone_kernels.cuh new file mode 100644 index 0000000000..95b2eebe64 --- /dev/null +++ b/cpp/src/barrier/second_order_cone_kernels.cuh @@ -0,0 +1,1081 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// ============================================================================= +// SOC (second-order cone) kernels for the cuOpt barrier solver. +// +// x_soc : cone primal block +// z_soc : cone dual block +// W, W^{-1} : Nesterov-Todd scaling matrix and inverse. W is symmetric for +// SOC, so W^{T} = W +// H : S^{T} S = S^{2}, the cone KKT block added to the +// primal-reduced system +// eta : sqrt(z_J / x_J), where x_J = sqrt(det_J(x_soc)) +// w : NT scaling direction with det_J(w) = 1 and +// w[head] = sqrt(1 + ||w_tail||^2) +// +// Cone vectors are packed flat: +// entries [cone_offsets[i], cone_offsets[i + 1]) belong to cone i. +// ============================================================================= + +namespace cuopt::linear_programming::dual_simplex { + +inline constexpr int soc_block_size = 256; + +/** + * Reusable device workspace for second-order cone kernels. + * + * The scratch object owns only temporary storage. Kernels may reuse the scalar + * slots and `temp_cone` sequentially inside a higher-level operation, but no + * persistent NT scaling or iterate state is stored here. + */ +template +struct cone_scratch_t { + i_t n_cones; // number of SOC blocks + std::size_t n_cone_entries; // total packed cone dimension + + rmm::device_uvector slots; // [n_slots * n_cones] + + // Per-cone step candidates before the final min reduction. + rmm::device_uvector step_alpha_primal; // [n_cones] + rmm::device_uvector step_alpha_dual; // [n_cones] + + // TODO: Consider moving this out to the barrier layer when we wire it in + rmm::device_uvector temp_cone; // [n_cone_entries] + + cone_scratch_t(i_t n_cones_in, std::size_t n_cone_entries_in, rmm::cuda_stream_view stream) + : n_cones(n_cones_in), + n_cone_entries(n_cone_entries_in), + slots(0, stream), + step_alpha_primal(0, stream), + step_alpha_dual(0, stream), + temp_cone(0, stream) + { + const auto n_cones_size = static_cast(n_cones); + + slots.resize(n_cones_size * static_cast(n_slots), stream); + step_alpha_primal.resize(n_cones_size, stream); + step_alpha_dual.resize(n_cones_size, stream); + temp_cone.resize(n_cone_entries, stream); + } + + template + raft::device_span get_slot() const + { + static_assert(slot_idx >= 0 && slot_idx < n_slots, "scratch slot index out of range"); + const auto n_cones_size = static_cast(n_cones); + const auto begin = static_cast(slot_idx) * n_cones_size; + const auto end = begin + n_cones_size; + return cuopt::make_span(slots, begin, end); + } + + template + raft::device_span get_slot() + { + const auto const_slot = static_cast(*this).template get_slot(); + return raft::device_span(const_cast(const_slot.data()), const_slot.size()); + } +}; + +struct to_size_t_t { + template + HD std::size_t operator()(value_t value) const + { + return value; + } +}; + +template +HD f_t cone_step_length_from_scalars( + f_t u0, f_t du0, f_t du_tail_sq, f_t u_tail_du_tail, f_t u_tail_sq, f_t alpha_max) +{ + const auto a = du0 * du0 - du_tail_sq; + const auto b = u0 * du0 - u_tail_du_tail; + const auto c_raw = u0 * u0 - u_tail_sq; + const auto c = c_raw > 0 ? c_raw : 0; + const auto disc = b * b - a * c; + auto alpha = alpha_max; + + if (u0 >= 0 && du0 < 0) { alpha = cuda::std::min(alpha, -u0 / du0); } + + if ((a > 0 && b > 0) || disc < 0) { return alpha; } + + if (a == 0) { + return alpha; + } else if (c == 0) { + alpha = a >= 0 ? alpha : 0; + } else { + const auto t = -(b + copysign(sqrt(disc), b)); + auto r1 = c / t; + auto r2 = t / a; + if (r1 < 0) { r1 = alpha; } + if (r2 < 0) { r2 = alpha; } + alpha = cuda::std::min(alpha, cuda::std::min(r1, r2)); + } + + return alpha; +} + +template +__global__ void __launch_bounds__(soc_block_size) + step_length_single_kernel(raft::device_span u, + raft::device_span du, + raft::device_span alpha, + raft::device_span du_tail_sq, + raft::device_span u_tail_du_tail, + raft::device_span u_tail_sq, + raft::device_span cone_offsets, + f_t alpha_max, + i_t n_cones) +{ + const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (cone >= n_cones) { return; } + + const auto off = cone_offsets[cone]; + alpha[cone] = cone_step_length_from_scalars( + u[off], du[off], du_tail_sq[cone], u_tail_du_tail[cone], u_tail_sq[cone], alpha_max); +} + +/** + * Device storage for second-order cone topology, NT scaling, and iterate views. + * + * Flat arrays are packed by cone: entries + * [cone_offsets[i], cone_offsets[i + 1]) belong to cone i, whose dimension is + * cone_dimensions[i]. + * + * The primal/dual cone vectors are non-owning spans over the SOC slice of the + * solver's global x/z vectors. The caller must keep the underlying storage + * alive for the lifetime of this object. + */ +template +struct cone_data_t { + // Topology. This is immutable after construction. + i_t n_cones; // number of SOC blocks + std::size_t n_cone_entries; // total packed cone dimension = sum(cone_dimensions) + + rmm::device_uvector cone_offsets; // [n_cones + 1], prefix sum of dimensions + rmm::device_uvector cone_dimensions; // [n_cones], dimension q_i of each cone + // Owning cone per entry for upcoming flat per-entry SOC kernels. + rmm::device_uvector element_cone_ids; // [n_cone_entries] + segmented_sum_t segmented_sum; + + // Non-owning iterate views over the cone portion of x/z. + raft::device_span x; // [n_cone_entries], SOC primal block + raft::device_span z; // [n_cone_entries], SOC dual block + + // Persistent Nesterov-Todd scaling state, recomputed from x/z each iteration. + rmm::device_uvector eta; // [n_cones], sqrt(|z|_J / |x|_J) + rmm::device_uvector w; // [n_cone_entries], unit-J-norm NT direction + rmm::device_uvector lambda; // [n_cone_entries], NT point lambda = W^{-T} z + + cone_scratch_t scratch; + + cone_data_t(std::span cone_dimensions_host, + raft::device_span x_in, + raft::device_span z_in, + rmm::cuda_stream_view stream) + : n_cones(cone_dimensions_host.size()), + n_cone_entries( + std::reduce(cone_dimensions_host.begin(), cone_dimensions_host.end(), std::size_t{0})), + cone_offsets(n_cones + 1, stream), + cone_dimensions(n_cones, stream), + element_cone_ids(n_cone_entries, stream), + segmented_sum(cone_dimensions_host, cuopt::make_span(cone_offsets), stream), + x(x_in), + z(z_in), + eta(n_cones, stream), + w(n_cone_entries, stream), + lambda(n_cone_entries, stream), + scratch(n_cones, n_cone_entries, stream) + { + raft::copy(cone_dimensions.data(), cone_dimensions_host.data(), n_cones, stream); + cone_offsets.set_element_to_zero_async(0, stream); + auto policy = rmm::exec_policy(stream); + + auto cone_dimensions_as_offsets = + thrust::make_transform_iterator(cone_dimensions.begin(), to_size_t_t{}); + thrust::inclusive_scan(policy, + cone_dimensions_as_offsets, + cone_dimensions_as_offsets + n_cones, + cone_offsets.begin() + 1, + cuda::std::plus{}); + + thrust::upper_bound(policy, + cone_offsets.begin() + 1, + cone_offsets.end(), + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(n_cone_entries), + element_cone_ids.begin()); + segmented_sum.template prepare_workspace(stream); + } +}; + +template +__global__ void __launch_bounds__(soc_block_size) + nt_finalize_scaling_scalars_kernel(raft::device_span x, + raft::device_span z, + raft::device_span x_scale, + raft::device_span z_scale, + raft::device_span eta, + raft::device_span cone_offsets, + i_t n_cones) +{ + const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (cone >= n_cones) { return; } + + const auto off = cone_offsets[cone]; + const auto x_tail_norm = sqrt(x_scale[cone]); + const auto z_tail_norm = sqrt(z_scale[cone]); + const auto x_det = (x[off] - x_tail_norm) * (x[off] + x_tail_norm); + const auto z_det = (z[off] - z_tail_norm) * (z[off] + z_tail_norm); + + x_scale[cone] = sqrt(x_det); + z_scale[cone] = sqrt(z_det); + eta[cone] = sqrt(z_scale[cone] / x_scale[cone]); +} + +template +__global__ void __launch_bounds__(soc_block_size) + nt_finalize_w_scale_kernel(raft::device_span w, + raft::device_span tail_sq, + raft::device_span w_scale, + raft::device_span cone_offsets, + i_t n_cones) +{ + const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (cone >= n_cones) { return; } + + const auto cone_off = cone_offsets[cone]; + const auto head = w[cone_off]; + const auto tail_norm = sqrt(tail_sq[cone]); + const auto residual = (head - tail_norm) * (head + tail_norm); + w_scale[cone] = sqrt(residual); +} + +/** + * Write unnormalized w: + * + * w_0 = z_0 / z_scale + x_0 / x_scale + * w_tail = z_tail / z_scale - x_tail / x_scale. + */ +template +__global__ void __launch_bounds__(soc_block_size) + nt_write_w_kernel(raft::device_span x, + raft::device_span z, + raft::device_span x_scale, + raft::device_span z_scale, + raft::device_span w, + raft::device_span cone_offsets, + raft::device_span element_cone_ids) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= w.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + if (idx == cone_off) { + w[idx] = z[idx] / z_scale[cone] + x[idx] / x_scale[cone]; + return; + } + + w[idx] = z[idx] / z_scale[cone] - x[idx] / x_scale[cone]; +} + +template +__global__ void __launch_bounds__(soc_block_size) + nt_normalize_w_kernel(raft::device_span w, + raft::device_span w_scale, + raft::device_span element_cone_ids) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= w.size()) { return; } + + const auto cone = element_cone_ids[idx]; + w[idx] /= w_scale[cone]; +} + +template +__global__ void __launch_bounds__(soc_block_size) + nt_finalize_head_kernel(raft::device_span w, + raft::device_span normalized_tail_sq, + raft::device_span cone_offsets, + i_t n_cones) +{ + const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (cone >= n_cones) { return; } + + w[cone_offsets[cone]] = sqrt(1 + normalized_tail_sq[cone]); +} + +template +__global__ void __launch_bounds__(soc_block_size) + nt_write_lambda_kernel(raft::device_span x, + raft::device_span z, + raft::device_span x_scale, + raft::device_span z_scale, + raft::device_span w_scale, + raft::device_span lambda, + raft::device_span cone_offsets, + raft::device_span element_cone_ids) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= lambda.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + const auto local_idx = idx - cone_off; + + const auto x_scale_cone = x_scale[cone]; + const auto z_scale_cone = z_scale[cone]; + const auto gamma = static_cast(0.5) * w_scale[cone]; + const auto head_scale = sqrt(x_scale_cone * z_scale_cone); + + if (local_idx == 0) { + lambda[idx] = gamma * head_scale; + return; + } + + const auto x_head = x[cone_off]; + const auto z_head = z[cone_off]; + const auto denom = z_head / z_scale_cone + x_head / x_scale_cone + static_cast(2) * gamma; + const auto coeff_z = (gamma + x_head / x_scale_cone) / z_scale_cone; + const auto coeff_x = (gamma + z_head / z_scale_cone) / x_scale_cone; + + const auto lambda_tail = (coeff_z * z[idx] + coeff_x * x[idx]) / denom; + lambda[idx] = lambda_tail * head_scale; +} + +/** + * Build Nesterov-Todd scaling for packed SOC blocks. + * + * Given interior cone primal/dual blocks x and z: + * + * det_J(x) = x_0^2 - ||x_tail||^2 + * det_J(z) = z_0^2 - ||z_tail||^2 + * x_scale = sqrt(det_J(x)), z_scale = sqrt(det_J(z)) + * eta = sqrt(z_scale / x_scale) + * w_tmp_0 = z_0 / z_scale + x_0 / x_scale + * w_tmp_tail = z_tail / z_scale - x_tail / x_scale + * w_scale = sqrt(det_J(w_tmp)) + * w = w_tmp / w_scale + * w_0 = sqrt(1 + ||w_tail||^2) to re-impose det_J(w) = 1 + * + * Scratch slots: + * 0: ||x_tail||^2 -> x_scale + * 1: ||z_tail||^2 -> z_scale + */ +template +void launch_nt_scaling(cone_data_t& cones, rmm::cuda_stream_view stream) +{ + auto x_scale = cones.scratch.template get_slot<0>(); + auto z_scale = cones.scratch.template get_slot<1>(); + auto w_scale = cones.scratch.template get_slot<2>(); + + const auto span_x = cones.x; + const auto span_z = cones.z; + const auto cone_offsets = cuopt::make_span(cones.cone_offsets); + const auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); + + auto x_tail_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [span_x, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : span_x[idx] * span_x[idx]; + }); + cones.segmented_sum(x_tail_sq_terms, x_scale, stream); + + auto z_tail_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [span_z, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : span_z[idx] * span_z[idx]; + }); + cones.segmented_sum(z_tail_sq_terms, z_scale, stream); + + const auto cone_grid_dim = + raft::ceildiv(static_cast(cones.n_cones), soc_block_size); + nt_finalize_scaling_scalars_kernel + <<>>( + cones.x, cones.z, x_scale, z_scale, cuopt::make_span(cones.eta), cone_offsets, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + const auto element_grid_dim = raft::ceildiv(cones.n_cone_entries, soc_block_size); + + auto w = cuopt::make_span(cones.w); + nt_write_w_kernel<<>>( + cones.x, cones.z, x_scale, z_scale, w, cone_offsets, element_cone_ids); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + auto unnormalized_tail_sq_terms = + thrust::make_transform_iterator(thrust::make_counting_iterator(0), + [cone_offsets, element_cone_ids, w] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : w[idx] * w[idx]; + }); + cones.segmented_sum(unnormalized_tail_sq_terms, w_scale, stream); + + nt_finalize_w_scale_kernel<<>>( + w, w_scale, w_scale, cone_offsets, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + nt_normalize_w_kernel + <<>>(w, w_scale, element_cone_ids); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + // Persist lambda while w_scale still stores sqrt(det_J(w_tmp)). + nt_write_lambda_kernel + <<>>(cones.x, + cones.z, + x_scale, + z_scale, + w_scale, + cuopt::make_span(cones.lambda), + cone_offsets, + element_cone_ids); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + // w_scale is overwritten from here + auto normalized_tail_terms = + thrust::make_transform_iterator(thrust::make_counting_iterator(0), + [cone_offsets, element_cone_ids, w] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : w[idx] * w[idx]; + }); + cones.segmented_sum(normalized_tail_terms, w_scale, stream); + + nt_finalize_head_kernel<<>>( + cuopt::make_span(cones.w), w_scale, cone_offsets, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +template +__global__ void __launch_bounds__(soc_block_size) + apply_w_inv_write_kernel(raft::device_span v, + raft::device_span out, + raft::device_span w, + raft::device_span eta, + raft::device_span tail_dot, + raft::device_span cone_offsets, + raft::device_span element_cone_ids) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= out.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + const auto local_idx = idx - cone_off; + + const auto w0 = w[cone_off]; + const auto zeta = tail_dot[cone]; + const auto v0 = v[cone_off]; + const auto inv_eta = f_t(1) / eta[cone]; + + if (local_idx == 0) { + out[idx] = inv_eta * (w0 * v0 - zeta); + return; + } + + const auto coeff = -v0 + zeta / (f_t(1) + w0); + out[idx] = inv_eta * (v[idx] + coeff * w[idx]); +} + +template +__global__ void __launch_bounds__(soc_block_size) + apply_w_write_kernel(raft::device_span v, + raft::device_span out, + raft::device_span w, + raft::device_span eta, + raft::device_span tail_dot, + raft::device_span cone_offsets, + raft::device_span element_cone_ids) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= out.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + const auto local_idx = idx - cone_off; + + const auto w0 = w[cone_off]; + const auto zeta = tail_dot[cone]; + const auto v0 = v[cone_off]; + const auto cone_eta = eta[cone]; + + if (local_idx == 0) { + out[idx] = cone_eta * (w0 * v0 + zeta); + return; + } + + const auto coeff = v0 + zeta / (f_t(1) + w0); + out[idx] = cone_eta * (v[idx] + coeff * w[idx]); +} + +template +__global__ void __launch_bounds__(soc_block_size) + apply_hessian_write_kernel(raft::device_span v, + raft::device_span out, + raft::device_span w, + raft::device_span eta, + raft::device_span wv_dot, + raft::device_span cone_offsets, + raft::device_span element_cone_ids, + raft::device_span bias, + f_t output_scale, + f_t bias_scale) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= out.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + const auto local_idx = idx - cone_off; + + const auto eta_sq = (eta[cone] * eta[cone]); + const auto coeff = 2 * wv_dot[cone] * eta_sq; + const int sign = (local_idx == 0) * 2 - 1; + const auto value = coeff * w[idx] - eta_sq * v[idx] * sign; + const auto h_value = output_scale * value; + + out[idx] = bias.empty() ? h_value : bias_scale * bias[idx] + h_value; +} + +template +__global__ void __launch_bounds__(soc_block_size) + gather_cone_heads_kernel(raft::device_span values, + raft::device_span heads, + raft::device_span cone_offsets, + i_t n_cones) +{ + const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (cone >= n_cones) { return; } + + heads[cone] = values[cone_offsets[cone]]; +} + +/** + * Build the Mehrotra corrector shift: + * + * d = (W dx_aff) o (W^{-T} dz_aff) - sigma_mu e. + * + * On entry, `scaled_dx` is W dx_aff and `scaled_dz` is W^{-T} dz_aff. The + * cone head uses the full dot product, and tail entries use the SOC Jordan + * product: + * + * d_0 = - sigma_mu + * d_tail = scaled_dx_0 * scaled_dz_tail + scaled_dz_0 * scaled_dx_tail. + */ +template +__global__ void __launch_bounds__(soc_block_size) + combined_cone_shift_write_kernel(raft::device_span shift, + raft::device_span scaled_dx, + raft::device_span scaled_dz, + raft::device_span full_dot, + raft::device_span scaled_dx_head, + raft::device_span scaled_dz_head, + raft::device_span cone_offsets, + raft::device_span element_cone_ids, + f_t sigma_mu) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= shift.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + const auto local_idx = idx - cone_off; + + if (local_idx == 0) { + shift[idx] = full_dot[cone] - sigma_mu; + return; + } + + shift[idx] = scaled_dx_head[cone] * scaled_dz[idx] + scaled_dz_head[cone] * scaled_dx[idx]; +} + +/** + * Per-cone scalar stage for p = lambda \ d: + * + * p_0 = (lambda_0 d_0 - ) / det_J(lambda) + * inv_lambda_0 = 1 / lambda_0. + * + * A second flat kernel writes `-p`, which lets the final W^{-1} call produce + * q = -W^{-1} p without adding an output-scale argument to W^{-1}. + */ +template +__global__ void __launch_bounds__(soc_block_size) + jordan_divide_by_lambda_scalar_kernel(raft::device_span shift, + raft::device_span nt_point, + raft::device_span lambda_tail_dot, + raft::device_span lambda_tail_sq, + raft::device_span p0, + raft::device_span inv_lambda0, + raft::device_span cone_offsets, + i_t n_cones) +{ + const auto cone = static_cast(blockIdx.x * blockDim.x + threadIdx.x); + if (cone >= n_cones) { return; } + + const auto cone_off = cone_offsets[cone]; + const auto lambda0 = nt_point[cone_off]; + const auto lambda_tail_norm = sqrt(lambda_tail_sq[cone]); + const auto det_lambda = (lambda0 - lambda_tail_norm) * (lambda0 + lambda_tail_norm); + + // repurpose the heads in lambda_tail_dot, lambda_tail_sq for each cone + p0[cone] = (lambda0 * shift[cone_off] - lambda_tail_dot[cone]) / det_lambda; + inv_lambda0[cone] = 1 / lambda0; +} + +template +__global__ void __launch_bounds__(soc_block_size) + jordan_divide_by_lambda_write_kernel(raft::device_span shift, + raft::device_span nt_point, + raft::device_span p0, + raft::device_span inv_lambda0, + raft::device_span cone_offsets, + raft::device_span element_cone_ids, + raft::device_span out) +{ + const auto idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (idx >= out.size()) { return; } + + const auto cone = element_cone_ids[idx]; + const auto cone_off = cone_offsets[cone]; + const auto local_idx = idx - cone_off; + + if (local_idx == 0) { + out[idx] = -p0[cone]; + return; + } + + out[idx] = (p0[cone] * nt_point[idx] - shift[idx]) * inv_lambda0[cone]; +} + +/** + * Apply the Nesterov-Todd scaling matrix: out = W^{-1} v. + * + * For each cone: + * zeta = + * (W^{-1}v)_0 = inv_eta * (w_0 v_0 - zeta) + * (W^{-1}v)_tail = inv_eta * (v_tail + (-v_0 + zeta / (1 + w_0)) w_tail) + */ +template +void apply_w_inv(raft::device_span v, + raft::device_span out, + cone_data_t& cones, + rmm::cuda_stream_view stream) +{ + auto w = cuopt::make_span(cones.w); + auto eta = cuopt::make_span(cones.eta); + auto cone_offsets = cuopt::make_span(cones.cone_offsets); + auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); + auto tail_dot = cones.scratch.template get_slot<0>(); + + auto tail_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [v, w, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : w[idx] * v[idx]; + }); + cones.segmented_sum(tail_terms, tail_dot, stream); + + const auto grid_dim = raft::ceildiv(out.size(), soc_block_size); + apply_w_inv_write_kernel<<>>( + v, out, w, eta, tail_dot, cone_offsets, element_cone_ids); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +/** + * Apply the multiplication of Nesterov-Todd scaling matrix: + * out = W v. + * + * For each cone, + * zeta = + * (W * v)_0 = eta * (w_0 v_0 + zeta) + * (W * v)_tail = + * eta * (v_tail + (v_0 + zeta / (1 + w_0)) w_tail) + */ +template +void apply_w(raft::device_span v, + raft::device_span out, + cone_data_t& cones, + rmm::cuda_stream_view stream) +{ + auto w = cuopt::make_span(cones.w); + auto eta = cuopt::make_span(cones.eta); + auto cone_offsets = cuopt::make_span(cones.cone_offsets); + auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); + auto tail_dot = cones.scratch.template get_slot<0>(); + + auto tail_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [v, w, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : w[idx] * v[idx]; + }); + cones.segmented_sum(tail_terms, tail_dot, stream); + + const auto grid_dim = raft::ceildiv(out.size(), soc_block_size); + apply_w_write_kernel<<>>( + v, out, w, eta, tail_dot, cone_offsets, element_cone_ids); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +/** + * Apply the cone KKT block H = S^T S = S^2. + * + * With rho = : + * (Hv)_0 = eta^{2} (2 w_0 rho - v_0) + * (Hv)_tail = eta^{2} (2 w_tail rho + v_tail) + */ +template +void apply_hessian(raft::device_span v, + raft::device_span out, + cone_data_t& cones, + rmm::cuda_stream_view stream, + f_t output_scale = 1, + raft::device_span bias = {}, + f_t bias_scale = 0) +{ + auto w = cuopt::make_span(cones.w); + auto eta = cuopt::make_span(cones.eta); + auto cone_offsets = cuopt::make_span(cones.cone_offsets); + auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); + auto wv_dot = cones.scratch.template get_slot<0>(); + + auto wv_terms = + thrust::make_transform_iterator(thrust::make_counting_iterator(0), + [v, w] HD(std::size_t idx) -> f_t { return w[idx] * v[idx]; }); + cones.segmented_sum(wv_terms, wv_dot, stream); + + const auto grid_dim = raft::ceildiv(out.size(), soc_block_size); + apply_hessian_write_kernel<<>>( + v, out, w, eta, wv_dot, cone_offsets, element_cone_ids, bias, output_scale, bias_scale); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +/** + * Recover the SOC dual direction after the reduced KKT solve. + * + * The reduced solve gives `dx`; the cone equation supplies the target RHS. + * This function applies the cone block H = S^2 and writes: + * dz = cone_target - H dx. + */ +template +void recover_cone_dz_from_target(raft::device_span dx, + cone_data_t& cones, + raft::device_span cone_target, + raft::device_span dz, + rmm::cuda_stream_view stream) +{ + apply_hessian(dx, dz, cones, stream, -1, cone_target, 1); +} + +/** + * Accumulate the SOC cone-block matvec into an existing output vector. + * + * Used by matrix-free products with the primal-reduced KKT block: + * out += H x, where H = S^2. + */ +template +void accumulate_cone_hessian_matvec(raft::device_span x, + cone_data_t& cones, + raft::device_span out, + rmm::cuda_stream_view stream) +{ + auto out_input = raft::device_span(out.data(), out.size()); + apply_hessian(x, out, cones, stream, 1, out_input, 1); +} + +template +__global__ void __launch_bounds__(soc_block_size) + scatter_hessian_into_augmented_kernel(raft::device_span augmented_x, + raft::device_span csr_indices, + raft::device_span q_values, + raft::device_span w, + raft::device_span eta, + raft::device_span cone_offsets, + raft::device_span block_offsets, + i_t n_cones, + f_t dual_perturb_value) +{ + const auto e = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (e >= csr_indices.size()) { return; } + + i_t lo = 0; + i_t hi = n_cones; + while (lo < hi) { + const i_t mid = lo + (hi - lo) / 2; + if (block_offsets[mid + 1] <= e) { + lo = mid + 1; + } else { + hi = mid; + } + } + + const auto cone = lo; + const auto off = cone_offsets[cone]; + const auto q = cone_offsets[cone + 1] - off; + const auto blk_off = block_offsets[cone]; + const auto local = e - blk_off; + const auto r = local / q; + const auto c = local % q; + + const auto eta_sq = eta[cone] * eta[cone]; + const auto w0 = w[off]; + const auto u_r = (r == 0) ? w0 : w[off + r]; + const auto u_c = (c == 0) ? w0 : w[off + c]; + auto val = f_t{2} * u_r * eta_sq * u_c; + const auto diag_correction = (r == 0) ? -eta_sq : eta_sq; + if (r == c) { val += diag_correction + dual_perturb_value; } + + augmented_x[csr_indices[e]] = -val - q_values[e]; +} + +template +void scatter_hessian_into_augmented(const cone_data_t& cones, + rmm::device_uvector& augmented_x, + const rmm::device_uvector& csr_indices, + const rmm::device_uvector& q_values, + rmm::cuda_stream_view stream, + f_t dual_perturb_value) +{ + const auto count = csr_indices.size(); + if (count == 0) { return; } + cuopt_assert(count == q_values.size(), "cone CSR index and Q-value arrays must match"); + + // TODO: This offset calculation should be done in the barrier layer, + // because it is already done in the barrier layer for the augmented system, see + // cone_block_offsets_host. + rmm::device_uvector block_offsets(cones.n_cones + 1, stream); + block_offsets.set_element_to_zero_async(0, stream); + + auto block_sizes = thrust::make_transform_iterator( + cones.cone_dimensions.begin(), + [] HD(i_t q) -> std::size_t { return static_cast(q) * q; }); + thrust::inclusive_scan( + rmm::exec_policy(stream), block_sizes, block_sizes + cones.n_cones, block_offsets.begin() + 1); + + const auto grid = raft::ceildiv(count, soc_block_size); + scatter_hessian_into_augmented_kernel + <<>>(cuopt::make_span(augmented_x), + cuopt::make_span(csr_indices), + cuopt::make_span(q_values), + cuopt::make_span(cones.w), + cuopt::make_span(cones.eta), + cuopt::make_span(cones.cone_offsets), + cuopt::make_span(block_offsets), + cones.n_cones, + dual_perturb_value); + RAFT_CUDA_TRY(cudaPeekAtLastError()); +} + +/** + * Compute the maximum primal and dual step lengths that keep SOC blocks + * feasible: + * + * x + alpha dx in Q, z + alpha dz in Q, alpha <= alpha_max. + * + * For one cone u + alpha du, feasibility is + * + * u_0 + alpha du_0 >= ||u_tail + alpha du_tail||. + * + * Squaring gives the quadratic + * + * c + 2 b alpha + a alpha^2 >= 0, + * + * where c = det_J(u), b = u_0 du_0 - , and + * a = det_J(du). The per-cone kernel below solves for the first boundary + * crossing, and the final reductions take the global minimum over cones. + */ +template +std::pair compute_cone_step_length(cone_data_t& cones, + raft::device_span dx, + raft::device_span dz, + f_t alpha_max, + rmm::cuda_stream_view stream) +{ + auto cone_offsets = cuopt::make_span(cones.cone_offsets); + auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); + auto slot_0 = cones.scratch.template get_slot<0>(); + auto slot_1 = cones.scratch.template get_slot<1>(); + auto slot_2 = cones.scratch.template get_slot<2>(); + + auto run_pass = [&](raft::device_span u, + raft::device_span du, + raft::device_span alpha) { + auto du_tail_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [du, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : du[idx] * du[idx]; + }); + cones.segmented_sum(du_tail_sq_terms, slot_0, stream); + + auto u_tail_du_tail_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [u, du, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : u[idx] * du[idx]; + }); + cones.segmented_sum(u_tail_du_tail_terms, slot_1, stream); + + auto u_tail_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [u, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : u[idx] * u[idx]; + }); + cones.segmented_sum(u_tail_sq_terms, slot_2, stream); + + const auto grid_dim = + raft::ceildiv(static_cast(cones.n_cones), soc_block_size); + step_length_single_kernel<<>>( + u, du, alpha, slot_0, slot_1, slot_2, cone_offsets, alpha_max, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + }; + + auto alpha_primal = cuopt::make_span(cones.scratch.step_alpha_primal); + auto alpha_dual = cuopt::make_span(cones.scratch.step_alpha_dual); + + run_pass(cones.x, dx, alpha_primal); + run_pass(cones.z, dz, alpha_dual); + + const auto primal = thrust::reduce(rmm::exec_policy(stream), + alpha_primal.begin(), + alpha_primal.end(), + alpha_max, + thrust::minimum()); + const auto dual = thrust::reduce(rmm::exec_policy(stream), + alpha_dual.begin(), + alpha_dual.end(), + alpha_max, + thrust::minimum()); + + return {primal, dual}; +} + +/** + * Build the SOC corrector target for the reduced KKT solve. + * + * Mehrotra's corrector uses affine cone directions to form + * + * d = (W dx_aff) o (W^{-T} dz_aff) - sigma_mu e, + * + * where `o` is the SOC Jordan product and `e = (1, 0, ..., 0)` per cone. + * The reduced KKT solve needs the cone target + * + * q = W * (-p), where p = lambda \ d and lambda = W^{-T} z. + * + * On return, `out` holds `q`. Internally, `out` is reused for `W^{-T} dz_aff` and + * then `d`; `scratch.temp_cone` is reused for `W dx_aff`, then `-p`. + */ +template +void compute_combined_cone_rhs_term(raft::device_span dx_aff, + raft::device_span dz_aff, + cone_data_t& cones, + f_t sigma_mu, + raft::device_span out, + rmm::cuda_stream_view stream) +{ + auto cone_offsets = cuopt::make_span(cones.cone_offsets); + auto element_cone_ids = cuopt::make_span(cones.element_cone_ids); + + auto scratch_cone = cuopt::make_span(cones.scratch.temp_cone); + auto scaled_dx = raft::device_span(scratch_cone.data(), scratch_cone.size()); + auto scaled_dz = raft::device_span(out.data(), out.size()); + auto slot_0 = cones.scratch.template get_slot<0>(); + auto slot_1 = cones.scratch.template get_slot<1>(); + auto slot_2 = cones.scratch.template get_slot<2>(); + + apply_w(dx_aff, scratch_cone, cones, stream); + apply_w_inv(dz_aff, out, cones, stream); + + auto full_product_terms = thrust::make_transform_iterator( + thrust::make_zip_iterator(scaled_dx.begin(), scaled_dz.begin()), + thrust::make_zip_function([] HD(f_t dx, f_t dz) -> f_t { return dx * dz; })); + cones.segmented_sum(full_product_terms, slot_0, stream); + + // `out` currently aliases W^{-T} dz_aff and is about to be overwritten with d. + // Stage both head vectors first because every tail entry needs them. + const auto cone_grid_dim = + raft::ceildiv(static_cast(cones.n_cones), soc_block_size); + gather_cone_heads_kernel<<>>( + scaled_dx, slot_1, cone_offsets, cones.n_cones); + gather_cone_heads_kernel<<>>( + scaled_dz, slot_2, cone_offsets, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + const auto element_grid_dim = raft::ceildiv(cones.n_cone_entries, soc_block_size); + combined_cone_shift_write_kernel + <<>>( + out, scaled_dx, scaled_dz, slot_0, slot_1, slot_2, cone_offsets, element_cone_ids, sigma_mu); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + auto shift = raft::device_span(out.data(), out.size()); + auto nt_point = raft::device_span(cones.lambda.data(), cones.lambda.size()); + + // compute W *(-(\lambda inv_circ shift)) + auto lambda_tail_dot_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [shift, nt_point, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : nt_point[idx] * shift[idx]; + }); + cones.segmented_sum(lambda_tail_dot_terms, slot_0, stream); + + auto lambda_tail_sq_terms = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), + [nt_point, cone_offsets, element_cone_ids] HD(std::size_t idx) -> f_t { + const auto cone = element_cone_ids[idx]; + return idx == cone_offsets[cone] ? 0 : nt_point[idx] * nt_point[idx]; + }); + cones.segmented_sum(lambda_tail_sq_terms, slot_1, stream); + + jordan_divide_by_lambda_scalar_kernel + <<>>( + shift, nt_point, slot_0, slot_1, slot_0, slot_1, cone_offsets, cones.n_cones); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + // Note that we implicitly multiply by -1 here since we are writing -p. + jordan_divide_by_lambda_write_kernel + <<>>( + shift, nt_point, slot_0, slot_1, cone_offsets, element_cone_ids, scratch_cone); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + + apply_w(scratch_cone, out, cones, stream); +} + +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/barrier/second_order_cone_reduction.cuh b/cpp/src/barrier/second_order_cone_reduction.cuh new file mode 100644 index 0000000000..ada9fcfb6a --- /dev/null +++ b/cpp/src/barrier/second_order_cone_reduction.cuh @@ -0,0 +1,261 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace cuopt::linear_programming::dual_simplex { + +template +__global__ void __launch_bounds__(warps_per_cta* raft::WarpSize) + warp_per_cone_reduce_kernel(InputIt input, + raft::device_span small_cone_ids, + raft::device_span cone_offsets, + OutputIt output, + value_t init); + +/** + * Segmented-sum dispatcher for packed second-order cone vectors. + * + * Cone dimensions are fixed for a solve, so the constructor partitions cone + * ids once by reduction strategy. Each call then reuses those partitions: + * small cones use one warp per cone, medium cones use CUB DeviceSegmentedReduce, + * and large cones use CUB DeviceReduce one cone at a time. The object owns the + * CUB workspace for those medium/large paths. Call `prepare_workspace` once + * before using a CUB-backed path. + */ +template +struct segmented_sum_t { + static_assert(warp_cone_dim > 0); + static_assert(large_cone_cutoff > warp_cone_dim); + + raft::device_span cone_offsets; + rmm::device_uvector small_cone_ids; // cone dimension <= warp_cone_dim + rmm::device_uvector medium_cone_ids; // warp_cone_dim < cone dimension <= large_cone_cutoff + + std::vector large_cone_offsets; + std::vector large_cone_ids; + std::vector large_cone_dimensions; + + // Maximum CUB temporary storage needed by prepared medium/large reductions. + std::size_t cub_workspace_bytes = 0; + rmm::device_buffer cub_workspace; + + private: + template + void prepare_workspace_for_type(rmm::cuda_stream_view stream) + { + auto input = thrust::make_constant_iterator(value_t{}); + auto output = thrust::make_discard_iterator(); + + if (!medium_cone_ids.is_empty()) { + const auto medium_begin_offsets = + thrust::make_permutation_iterator(cone_offsets.data(), medium_cone_ids.begin()); + const auto medium_end_offsets = + thrust::make_permutation_iterator(cone_offsets.data() + 1, medium_cone_ids.begin()); + + std::size_t temp_storage_bytes = 0; + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Sum(nullptr, + temp_storage_bytes, + input, + output, + medium_cone_ids.size(), + medium_begin_offsets, + medium_end_offsets, + stream.value())); + cub_workspace_bytes = std::max(cub_workspace_bytes, temp_storage_bytes); + } + + for (std::size_t i = 0; i < large_cone_ids.size(); ++i) { + std::size_t temp_storage_bytes = 0; + RAFT_CUDA_TRY(cub::DeviceReduce::Sum(nullptr, + temp_storage_bytes, + input + large_cone_offsets[i], + output + large_cone_ids[i], + large_cone_dimensions[i], + stream.value())); + cub_workspace_bytes = std::max(cub_workspace_bytes, temp_storage_bytes); + } + + if (cub_workspace.size() < cub_workspace_bytes) { + cub_workspace.resize(cub_workspace_bytes, stream); + } + } + + public: + template + void prepare_workspace(rmm::cuda_stream_view stream) + { + prepare_workspace_for_type(stream); + (prepare_workspace_for_type(stream), ...); + } + + template + void operator()(InputIt input, OutputIt output, value_t init, rmm::cuda_stream_view stream) + { + if (!small_cone_ids.is_empty()) { + // Each warp reduces one small cone. `warps_per_cta` only controls how + // many independent cone reductions are packed into one CTA; the default + // of 8 gives a conventional 256-thread block. + const auto n_small = small_cone_ids.size(); + const auto grid = (n_small + warps_per_cta - 1) / warps_per_cta; + warp_per_cone_reduce_kernel + <<>>( + input, cuopt::make_span(small_cone_ids), cone_offsets, output, init); + RAFT_CUDA_TRY(cudaPeekAtLastError()); + } + + if (!medium_cone_ids.is_empty()) { + cuopt_assert(cub_workspace_bytes > 0 && cub_workspace.size() >= cub_workspace_bytes, + "segmented_sum_t::prepare_workspace must be called before reducing medium or " + "large cones"); + + const auto medium_output = thrust::make_permutation_iterator(output, medium_cone_ids.begin()); + const auto medium_begin_offsets = + thrust::make_permutation_iterator(cone_offsets.data(), medium_cone_ids.begin()); + const auto medium_end_offsets = + thrust::make_permutation_iterator(cone_offsets.data() + 1, medium_cone_ids.begin()); + + std::size_t temp_storage_bytes = cub_workspace_bytes; + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Sum(cub_workspace.data(), + temp_storage_bytes, + input, + medium_output, + medium_cone_ids.size(), + medium_begin_offsets, + medium_end_offsets, + stream.value())); + } + + if (!large_cone_ids.empty()) { + cuopt_assert(cub_workspace_bytes > 0 && cub_workspace.size() >= cub_workspace_bytes, + "segmented_sum_t::prepare_workspace must be called before reducing medium or " + "large cones"); + + for (std::size_t i = 0; i < large_cone_ids.size(); ++i) { + std::size_t temp_storage_bytes = cub_workspace_bytes; + RAFT_CUDA_TRY(cub::DeviceReduce::Sum(cub_workspace.data(), + temp_storage_bytes, + input + large_cone_offsets[i], + output + large_cone_ids[i], + large_cone_dimensions[i], + stream.value())); + } + } + } + + template + void operator()(InputIt input, raft::device_span output, rmm::cuda_stream_view stream) + { + operator()(input, output.data(), f_t{0}, stream); + } + + segmented_sum_t(std::span cone_dimensions_host, + raft::device_span cone_offsets_in, + rmm::cuda_stream_view stream) + : cone_offsets(cone_offsets_in), + small_cone_ids(0, stream), + medium_cone_ids(0, stream), + cub_workspace(0, stream) + { + std::vector small_cone_ids_host; + std::vector medium_cone_ids_host; + + std::size_t cone_offset = 0; + i_t cone = 0; + for (const auto cone_dimension : cone_dimensions_host) { + if (cone_dimension <= warp_cone_dim) { + small_cone_ids_host.push_back(cone); + } else if (cone_dimension <= large_cone_cutoff) { + medium_cone_ids_host.push_back(cone); + } else { + large_cone_ids.push_back(cone); + large_cone_offsets.push_back(cone_offset); + large_cone_dimensions.push_back(cone_dimension); + } + cone_offset += cone_dimension; + ++cone; + } + + bool need_sync = false; + if (!small_cone_ids_host.empty()) { + cuopt::device_copy(small_cone_ids, small_cone_ids_host, stream); + need_sync = true; + } + if (!medium_cone_ids_host.empty()) { + cuopt::device_copy(medium_cone_ids, medium_cone_ids_host, stream); + need_sync = true; + } + if (need_sync) { stream.synchronize(); } + } +}; + +template +__global__ void __launch_bounds__(warps_per_cta* raft::WarpSize) + warp_per_cone_reduce_kernel(InputIt input, + raft::device_span small_cone_ids, + raft::device_span cone_offsets, + OutputIt output, + value_t init) +{ + static_assert(warps_per_cta > 0); + static_assert(warps_per_cta * raft::WarpSize <= 1024); + + using warp_reduce_t = cub::WarpReduce; + __shared__ typename warp_reduce_t::TempStorage temp_storage[warps_per_cta]; + + const auto lane_id = raft::laneId(); + const auto warp_idx = threadIdx.x / raft::WarpSize; + const auto slot = blockIdx.x * warps_per_cta + warp_idx; + if (slot >= small_cone_ids.size()) { return; } + + const auto cone = small_cone_ids[slot]; + const auto off = cone_offsets[cone]; + const auto dim = cone_offsets[cone + 1] - off; + + auto sum = init; + for (std::size_t i = lane_id; i < dim; i += raft::WarpSize) { + sum = sum + input[off + i]; + } + + sum = warp_reduce_t(temp_storage[warp_idx]).Sum(sum); + if (lane_id == 0) { output[cone] = sum; } +} + +} // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/barrier/translate_soc.hpp b/cpp/src/barrier/translate_soc.hpp new file mode 100644 index 0000000000..d3e805960a --- /dev/null +++ b/cpp/src/barrier/translate_soc.hpp @@ -0,0 +1,1044 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cuopt::linear_programming::detail { + +/** Convert MPS >= ('G') quadratic row to <= ('L') form on a working copy for SOC conversion. */ +template +void normalize_quadratic_constraint_g_to_l(qc_t& qc) +{ + if (qc.constraint_row_type != 'G') { return; } + for (auto& v : qc.linear_values) { + v = -v; + } + for (auto& v : qc.quadratic_values) { + v = -v; + } + qc.rhs_value = -qc.rhs_value; + qc.constraint_row_type = 'L'; +} + +/** + * @brief Expand QCMATRIX second-order cone (and rotated / affine variants) into the + * canonical slack form expected by the simplex/PDLP path: extra variables, equality + * rows, optional cone aliases, column permutation, and `user_problem` cone metadata. + * + * Preconditions: `csr_A` and `user_problem` already reflect the linear model for `n` variables + * and original rows; this routine augments dimensions and CSR row storage in place. + */ +template +void apply_soc_qcmatrix_conversion_for_simplex( + int n, + const std::vector::quadratic_constraint_t>& + qcs, + dual_simplex::csr_matrix_t& csr_A, + dual_simplex::user_problem_t& user_problem) +{ + cuopt_expects(!qcs.empty(), + error_type_t::ValidationError, + "Quadratic-constraint flag is set, but no constraints were provided"); + + // Use a practical tolerance for text-parsed MPS numeric values. + const f_t tol = std::numeric_limits::epsilon() * 2; + + // SOC conversion accepts: + // 1) diagonal Lorentz-form QCMATRIX rows: + // -s*x_head^2 + sum_i s*x_tail_i^2 <= 0 (any common s > 0; divide by s to normalize) + // 2) rotated SOC rows: + // -2*d*x_head0*x_head1 + sum_i s*x_tail_i^2 <= 0 (d>0, s>0; canonical d=s) + // symmetric Q off-diagonals (-d,-d) give x^T Q x cross term -2*d*x0*x1, i.e. a*x0*x1 + // in the inequality 2*d*x0*x1 >= s*||tail||^2 with a = 2*d. Lift uses sqrt(d/s) on heads. + // 3) quadratic rows with linear part: + // sum_i s*x_tail_i^2 + a^T x <= 0 + // represented as diagonal +s QCMATRIX entries plus linear terms in COLUMNS. + // We introduce an auxiliary t = -(1/s)*a^T x so the row becomes: + // sum_i x_tail_i^2 - t <= 0 + // then lift it as rotated SOC with implicit second head fixed at 1/2. + // The barrier consumes SOCs as trailing variable blocks [head, tails...], so we validate all + // QCMATRIX blocks first, convert rotated cones via slack variables in standard SOC coordinates, + // then apply a single column permutation to the linear model. + struct rotated_soc_t { + i_t head0{}; + i_t head1{}; + std::vector tails{}; + bool head1_is_constant_half{false}; + /// For two-head rotated SOC: sqrt(d/s) where Q_off = -d and tail diagonals +s (canonical 1). + f_t head_lift_sqrt_ratio{f_t(1)}; + }; + // This is the index of the auxiliary variable for the linear part of the quadratic constraint. + std::vector qc_affine_heads(qcs.size(), static_cast(-1)); + i_t n_affine_linear_aux = 0; + for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { + if (!qcs[qc_i].linear_values.empty()) { + qc_affine_heads[qc_i] = static_cast(n + n_affine_linear_aux); + ++n_affine_linear_aux; + } + } + + const i_t n_with_affine_aux = static_cast(n + n_affine_linear_aux); + + std::vector> cone_vars; + std::vector cone_dims; + std::vector cone_is_rotated; + std::vector rotated_cones; + std::vector is_cone_var(static_cast(n_with_affine_aux), 0); + cone_vars.reserve(qcs.size()); + cone_dims.reserve(qcs.size()); + cone_is_rotated.reserve(qcs.size()); + rotated_cones.reserve(qcs.size()); + std::vector qc_soc_uniform_scale(qcs.size(), f_t(1)); + + for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { + auto qc = qcs[qc_i]; + cuopt_expects(qc.constraint_row_type != 'E', + error_type_t::ValidationError, + "Equality quadratic constraints are not supported for SOC conversion"); + cuopt_expects(qc.constraint_row_type == 'L' || qc.constraint_row_type == 'G', + error_type_t::ValidationError, + "Quadratic constraint '%s' ROWS type must be 'L' (<=) or 'G' (>=)", + qc.constraint_row_name.c_str()); + normalize_quadratic_constraint_g_to_l(qc); + cuopt_expects(qc.rhs_value < tol && qc.rhs_value > -tol, + error_type_t::ValidationError, + "SOC conversion currently requires rhs = 0 for quadratic constraints"); + cuopt_expects(qc.linear_values.size() == qc.linear_indices.size(), + error_type_t::ValidationError, + "Quadratic constraint '%s' linear_values and linear_indices length mismatch", + qc.constraint_row_name.c_str()); + + const i_t q_nnz = static_cast(qc.quadratic_values.size()); + cuopt_expects(qc.quadratic_row_indices.size() == static_cast(q_nnz) && + qc.quadratic_col_indices.size() == static_cast(q_nnz), + error_type_t::ValidationError, + "Quadratic constraint '%s' Q COO row/col/value length mismatch", + qc.constraint_row_name.c_str()); + cuopt_expects(q_nnz >= 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC must have at least 1 entry in Q (nnz %d)", + qc.constraint_row_name.c_str(), + static_cast(q_nnz)); + + // This is the index of the auxiliary variable for the linear part of the quadratic + // constraint. + const i_t affine_head = qc_affine_heads[qc_i]; + const bool has_linear_part = affine_head >= 0; + if (has_linear_part) { + size_t nonzero_terms = 0; + for (size_t p = 0; p < qc.linear_values.size(); ++p) { + const i_t idx = qc.linear_indices[p]; + const f_t v = qc.linear_values[p]; + cuopt_expects(idx >= 0 && idx < n, + error_type_t::ValidationError, + "Quadratic constraint '%s' linear index %d is outside [0, %d)", + qc.constraint_row_name.c_str(), + static_cast(idx), + static_cast(n)); + if (v > -tol && v < tol) { continue; } + ++nonzero_terms; + } + cuopt_expects(nonzero_terms > 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' has linear section but all linear coefficients are " + "zero", + qc.constraint_row_name.c_str()); + } + + // Verify Q as either: + // - standard SOC: one diagonal -s (head), tail diagonals +s for a common s > 0, + // - rotated SOC: symmetric (-s,-s) off-diagonal pair on the two heads, tails +s, + // - affine SOC: tail diagonals +s and linear terms (no Q off-diagonals). + // Feasibility is unchanged after dividing the quadratic row by s; affine rows also scale + // linear coefficients when forming the auxiliary t = -(1/s) a^T x. + + auto approx_eq_scaled = [&](f_t a, f_t b) { + const f_t scale = std::max({f_t(1), std::abs(a), std::abs(b)}); + return std::abs(a - b) <= tol * scale; + }; + + // Sort COO by (row, col); O(nnz log nnz). Enforce at most one stored entry per row (SOC CSR). + std::vector perm(static_cast(q_nnz)); + std::iota(perm.begin(), perm.end(), size_t{0}); + std::sort(perm.begin(), perm.end(), [&](size_t a, size_t b) { + const i_t ra = qc.quadratic_row_indices[a]; + const i_t rb = qc.quadratic_row_indices[b]; + if (ra != rb) { return ra < rb; } + return qc.quadratic_col_indices[a] < qc.quadratic_col_indices[b]; + }); + + std::vector> q_entries; + q_entries.reserve(static_cast(q_nnz)); + for (size_t t = 0; t < static_cast(q_nnz); ++t) { + const size_t ix = perm[t]; + const i_t r = qc.quadratic_row_indices[ix]; + const i_t c = qc.quadratic_col_indices[ix]; + const f_t v = qc.quadratic_values[ix]; + cuopt_expects(r >= 0 && r < n && c >= 0 && c < n, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q entry (%d,%d) outside [0,%d)", + qc.constraint_row_name.c_str(), + static_cast(r), + static_cast(c), + static_cast(n)); + if (!q_entries.empty()) { + const i_t prev_r = std::get<0>(q_entries.back()); + cuopt_expects(r != prev_r, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d: expected at most one stored entry per " + "row (CSR layout); duplicate or unsorted row in COO", + qc.constraint_row_name.c_str(), + static_cast(r)); + } + q_entries.emplace_back(r, c, v); + } + + std::vector> pos_diag_rows; + std::vector> neg_diag_rows; + std::vector> offdiag_entries; + pos_diag_rows.reserve(q_entries.size()); + neg_diag_rows.reserve(1); + offdiag_entries.reserve(4); + + for (const auto& [r, c, v] : q_entries) { + if (r == c) { + if (v > tol) { + pos_diag_rows.emplace_back(r, v); + } else if (v < -tol) { + neg_diag_rows.emplace_back(r, v); + } else { + cuopt_expects(false, + error_type_t::ValidationError, + "Quadratic constraint '%s' Q row %d: diagonal SOC entry is near zero " + "(%.17g)", + qc.constraint_row_name.c_str(), + static_cast(r), + static_cast(v)); + } + } else { + offdiag_entries.emplace_back(r, c, v); + } + } + + std::vector tail_vars; + tail_vars.reserve(pos_diag_rows.size()); + for (const auto& pr : pos_diag_rows) { + tail_vars.push_back(pr.first); + } + + f_t uniform_s = f_t(0); + bool have_uniform_s = false; + auto note_positive_s = [&](f_t v) { + cuopt_expects(v > tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected strictly positive diagonal tail " + "coefficient, got %.17g", + qc.constraint_row_name.c_str(), + static_cast(v)); + if (!have_uniform_s) { + uniform_s = v; + have_uniform_s = true; + } else { + cuopt_expects( + approx_eq_scaled(v, uniform_s), + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: all positive diagonal coefficients must match; got " + "%.17g vs %.17g", + qc.constraint_row_name.c_str(), + static_cast(v), + static_cast(uniform_s)); + } + }; + + std::vector cone; + i_t cone_dim = 0; + char is_rotated = 0; + i_t head = static_cast(-1); + + if (offdiag_entries.empty()) { + if (!has_linear_part) { + if (pos_diag_rows.empty()) { + cuopt_expects(neg_diag_rows.size() == 1 && q_nnz == 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected tail diagonals +s with head -s, " + "or a single head row with q_nnz=1", + qc.constraint_row_name.c_str()); + const f_t neg_v = neg_diag_rows[0].second; + cuopt_expects(neg_v < -tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: cone head diagonal must be negative " + "(%.17g)", + qc.constraint_row_name.c_str(), + static_cast(neg_v)); + uniform_s = -neg_v; + have_uniform_s = true; + head = neg_diag_rows[0].first; + cuopt_expects( + static_cast(tail_vars.size()) == q_nnz - 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected %d diagonal +s entries (tails), found %zu", + qc.constraint_row_name.c_str(), + static_cast(q_nnz - 1), + tail_vars.size()); + cone.reserve(1); + cone.push_back(head); + cone_dim = static_cast(cone.size()); + is_rotated = static_cast(0); + } else { + for (const auto& pr : pos_diag_rows) { + note_positive_s(pr.second); + } + cuopt_expects(have_uniform_s, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: could not infer uniform positive scale s", + qc.constraint_row_name.c_str()); + cuopt_expects( + neg_diag_rows.size() == 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected exactly one diagonal -s (cone head) for " + "%zu tail entries, found %zu negative diagonals", + qc.constraint_row_name.c_str(), + tail_vars.size(), + neg_diag_rows.size()); + cuopt_expects( + static_cast(tail_vars.size()) == q_nnz - 1, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: expected %d diagonal +s entries (tails), found %zu", + qc.constraint_row_name.c_str(), + static_cast(q_nnz - 1), + tail_vars.size()); + const f_t neg_v = neg_diag_rows[0].second; + cuopt_expects( + approx_eq_scaled(neg_v, -uniform_s), + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: cone head diagonal must be -s with the same s as " + "positive tail diagonals; head %.17g vs -s = %.17g", + qc.constraint_row_name.c_str(), + static_cast(neg_v), + static_cast(-uniform_s)); + head = neg_diag_rows[0].first; + cone.reserve(static_cast(q_nnz)); + cone.push_back(head); + cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); + cone_dim = static_cast(cone.size()); + is_rotated = static_cast(0); + } + } else { + cuopt_expects( + neg_diag_rows.empty(), + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms cannot contain negative diagonal " + "Q entries", + qc.constraint_row_name.c_str()); + cuopt_expects(affine_head >= 0, + error_type_t::ValidationError, + "Quadratic constraint '%s' internal error: affine SOC head index invalid", + qc.constraint_row_name.c_str()); + for (const auto& pr : pos_diag_rows) { + note_positive_s(pr.second); + } + cuopt_expects(have_uniform_s, + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms must have at least one " + "diagonal +s term in Q", + qc.constraint_row_name.c_str()); + cuopt_expects(!tail_vars.empty(), + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms must have at least one " + "diagonal +s term in Q", + qc.constraint_row_name.c_str()); + for (const i_t tail : tail_vars) { + cuopt_expects( + tail != affine_head, + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms requires the linear head variable to be " + "distinct from quadratic diagonal variables", + qc.constraint_row_name.c_str()); + } + + cone.reserve(tail_vars.size() + 1); + cone.push_back(affine_head); + cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); + cone_dim = static_cast(tail_vars.size() + 2); + is_rotated = static_cast(1); + rotated_cones.push_back( + rotated_soc_t{affine_head, static_cast(-1), tail_vars, true, f_t(1)}); + } + } else { + cuopt_expects(!has_linear_part, + error_type_t::ValidationError, + "Quadratic constraint '%s' with linear terms cannot include rotated-SOC " + "off-diagonal entries", + qc.constraint_row_name.c_str()); + cuopt_expects(neg_diag_rows.empty(), + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q cannot contain diagonal head " + "entries; found %zu negative diagonals", + qc.constraint_row_name.c_str(), + neg_diag_rows.size()); + for (const auto& pr : pos_diag_rows) { + note_positive_s(pr.second); + } + cuopt_expects(have_uniform_s, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q: could not infer uniform scale s", + qc.constraint_row_name.c_str()); + cuopt_expects( + offdiag_entries.size() == 2, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q must contain exactly one symmetric off-diagonal " + "pair (-d,-d); found %zu off-diagonal entries", + qc.constraint_row_name.c_str(), + offdiag_entries.size()); + + const i_t a = std::get<0>(offdiag_entries[0]); + const i_t b = std::get<1>(offdiag_entries[0]); + const f_t v0 = std::get<2>(offdiag_entries[0]); + cuopt_expects( + v0 < -tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q off-diagonal must be negative; got %.17g", + qc.constraint_row_name.c_str(), + static_cast(v0)); + cuopt_expects(a != b, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q off-diagonal pair must use distinct " + "variables", + qc.constraint_row_name.c_str()); + cuopt_expects(std::get<0>(offdiag_entries[1]) == b && std::get<1>(offdiag_entries[1]) == a, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q must have symmetric entries (a,b) " + "and (b,a) with the same value", + qc.constraint_row_name.c_str()); + const f_t v1 = std::get<2>(offdiag_entries[1]); + cuopt_expects( + v1 < -tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q off-diagonal must be negative; got %.17g", + qc.constraint_row_name.c_str(), + static_cast(v1)); + cuopt_expects( + approx_eq_scaled(v0, v1), + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q symmetric off-diagonals must match; got %.17g " + "and %.17g", + qc.constraint_row_name.c_str(), + static_cast(v0), + static_cast(v1)); + const f_t cross_d = -v0; + cuopt_expects( + cross_d > tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q cross coefficient d = -Q_off must be positive", + qc.constraint_row_name.c_str()); + const f_t head_lift_sqrt_ratio = std::sqrt(cross_d / uniform_s); + cuopt_expects(std::isfinite(static_cast(head_lift_sqrt_ratio)), + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q head lift ratio sqrt(d/s) is not " + "finite (d=%.17g, s=%.17g)", + qc.constraint_row_name.c_str(), + static_cast(cross_d), + static_cast(uniform_s)); + cuopt_expects(static_cast(tail_vars.size()) == q_nnz - 2, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q: expected %d diagonal +s entries " + "(tails), found %zu", + qc.constraint_row_name.c_str(), + static_cast(q_nnz - 2), + tail_vars.size()); + cuopt_expects(q_nnz >= 3, + error_type_t::ValidationError, + "Quadratic constraint '%s' rotated SOC Q must have at least 1 tail entry", + qc.constraint_row_name.c_str()); + + cone.reserve(static_cast(q_nnz)); + cone.push_back(a); + cone.push_back(b); + cone.insert(cone.end(), tail_vars.begin(), tail_vars.end()); + cone_dim = static_cast(cone.size()); + is_rotated = static_cast(1); + rotated_cones.push_back(rotated_soc_t{a, b, tail_vars, false, head_lift_sqrt_ratio}); + } + + cuopt_expects(have_uniform_s && uniform_s > tol, + error_type_t::ValidationError, + "Quadratic constraint '%s' SOC Q: uniform scale s must be positive (got %.17g)", + qc.constraint_row_name.c_str(), + static_cast(uniform_s)); + qc_soc_uniform_scale[qc_i] = uniform_s; + + for (const i_t var : cone) { + cuopt_expects(var >= 0 && var < static_cast(is_cone_var.size()), + error_type_t::ValidationError, + "SOC variable index %d is outside [0, %d)", + static_cast(var), + static_cast(is_cone_var.size())); + } + cone_dims.push_back(cone_dim); + cone_vars.push_back(std::move(cone)); + cone_is_rotated.push_back(is_rotated); + } + // Add affine linear auxiliary variables and linking rows. + if (n_affine_linear_aux > 0) { + const f_t inf = std::numeric_limits::infinity(); + const i_t n_old = static_cast(n); + const i_t n_aug = n_with_affine_aux; + const i_t m_old = csr_A.m; + const i_t m_aug = static_cast(m_old + n_affine_linear_aux); + i_t row_write_cursor = m_old; + + user_problem.objective.resize(static_cast(n_aug), f_t(0)); + user_problem.lower.resize(static_cast(n_aug), -inf); + user_problem.upper.resize(static_cast(n_aug), inf); + user_problem.var_types.resize( + static_cast(n_aug), + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); + if (!user_problem.col_names.empty()) { + user_problem.col_names.resize(static_cast(n_aug)); + } + + for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { + const i_t aux_j = qc_affine_heads[qc_i]; + if (aux_j < 0) { continue; } + user_problem.lower[static_cast(aux_j)] = f_t(0); + user_problem.upper[static_cast(aux_j)] = inf; + if (!user_problem.col_names.empty()) { + user_problem.col_names[static_cast(aux_j)] = + "_CUOPT_qc_linear_aux_" + std::to_string(static_cast(aux_j - n_old)); + } + } + + user_problem.rhs.resize(static_cast(m_aug)); + user_problem.row_sense.resize(static_cast(m_aug)); + if (!user_problem.row_names.empty()) { + user_problem.row_names.resize(static_cast(m_aug)); + } + + csr_A.n = n_aug; + dual_simplex::sparse_vector_t eq_row; + eq_row.n = n_aug; + + for (size_t qc_i = 0; qc_i < qcs.size(); ++qc_i) { + const i_t aux_j = qc_affine_heads[qc_i]; + if (aux_j < 0) { continue; } + const auto& qc = qcs[qc_i]; + eq_row.i.clear(); + eq_row.x.clear(); + // Define auxiliary as t = -(1/s) a^T x so QC linear part matches normalized cone row. + const f_t inv_s = f_t(1) / qc_soc_uniform_scale[qc_i]; + eq_row.i.push_back(aux_j); + eq_row.x.push_back(f_t(1)); + for (size_t p = 0; p < qc.linear_values.size(); ++p) { + const f_t v = qc.linear_values[p]; + if (v > -tol && v < tol) { continue; } + eq_row.i.push_back(qc.linear_indices[p]); + eq_row.x.push_back(v * inv_s); + } + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_write_cursor)] = 'E'; + user_problem.rhs[static_cast(row_write_cursor)] = f_t(0); + if (!user_problem.row_names.empty()) { + user_problem.row_names[static_cast(row_write_cursor)] = + "_CUOPT_qc_linear_link_" + qc.constraint_row_name; + } + ++row_write_cursor; + } + + cuopt_expects(row_write_cursor == m_aug, + error_type_t::RuntimeError, + "Internal error: affine QC linking row count mismatch"); + cuopt_expects(csr_A.m == m_aug, + error_type_t::RuntimeError, + "Internal error: CSR row count after affine QC linking"); + } + + i_t n_prob = n_with_affine_aux; + + // Convert rotated SOC cones to standard SOC cones. + if (!rotated_cones.empty()) { + cuopt_expects(user_problem.Q_values.empty(), + error_type_t::ValidationError, + "Rotated SOC conversion is currently not supported when the objective has " + "quadratic terms"); + + const f_t inf = std::numeric_limits::infinity(); + const f_t inv_sqrt_2 = f_t(1) / std::sqrt(f_t(2)); + const f_t half = f_t(0.5); + + for (const auto& rc : rotated_cones) { + cuopt_expects(user_problem.var_types[static_cast(rc.head0)] == + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, + error_type_t::ValidationError, + "Rotated SOC head variables must be continuous"); + if (!rc.head1_is_constant_half) { + cuopt_expects(user_problem.var_types[static_cast(rc.head1)] == + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, + error_type_t::ValidationError, + "Rotated SOC head variables must be continuous"); + } + for (const i_t t : rc.tails) { + cuopt_expects(user_problem.var_types[static_cast(t)] == + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS, + error_type_t::ValidationError, + "Rotated SOC tail variables must be continuous"); + } + } + + // Lift each rotated cone into standard SOC coordinates with two slacks: + // With x_i' = sqrt(d/s)*x_hi, canonical s0 = (x_0'+x_1')/sqrt(2), s1 = (x_0'-x_1')/sqrt(2) + // so 2*d*x_h0*x_h1 >= s*sum tail^2 <=> 2*x_0'*x_1' >= sum (x_tail)^2 => s0^2 >= s1^2 + + // ... Only the rotated heads are replaced by slacks; tails stay as original variables. + i_t n_slack_total = 0; + for (size_t ci = 0; ci < cone_is_rotated.size(); ++ci) { + if (cone_is_rotated[ci]) { n_slack_total += 2; } + } + + const i_t n_old = n_prob; + n_prob = static_cast(n_old + n_slack_total); + + user_problem.objective.resize(static_cast(n_prob), f_t(0)); + user_problem.lower.resize(static_cast(n_prob), -inf); + user_problem.upper.resize(static_cast(n_prob), inf); + user_problem.var_types.resize( + static_cast(n_prob), + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); + if (!user_problem.col_names.empty()) { + user_problem.col_names.resize(static_cast(n_prob)); + for (i_t j = n_old; j < n_prob; ++j) { + user_problem.col_names[static_cast(j)] = + "_CUOPT_rsoc_slack_" + std::to_string(static_cast(j - n_old)); + } + } + + is_cone_var.resize(static_cast(n_prob), 0); + + const i_t m_old = csr_A.m; + user_problem.rhs.resize(static_cast(m_old + n_slack_total)); + user_problem.row_sense.resize(static_cast(m_old + n_slack_total)); + if (!user_problem.row_names.empty()) { + user_problem.row_names.resize(static_cast(m_old + n_slack_total)); + for (i_t r = m_old; r < m_old + n_slack_total; ++r) { + user_problem.row_names[static_cast(r)] = + "_CUOPT_rsoc_lift_" + std::to_string(static_cast(r - m_old)); + } + } + + csr_A.n = n_prob; + + dual_simplex::sparse_vector_t eq_row; + size_t ri = 0; + i_t slack_base = n_old; + i_t row_idx = m_old; + + for (size_t ci = 0; ci < cone_vars.size(); ++ci) { + if (!cone_is_rotated[ci]) { continue; } + const auto& rc = rotated_cones[ri++]; + const i_t dim = cone_dims[ci]; + std::vector new_cone; + new_cone.reserve(static_cast(dim)); + new_cone.push_back(slack_base); + new_cone.push_back(slack_base + 1); + new_cone.insert(new_cone.end(), rc.tails.begin(), rc.tails.end()); + cone_vars[ci] = std::move(new_cone); + + is_cone_var[static_cast(slack_base)] = 1; + is_cone_var[static_cast(slack_base + 1)] = 1; + + eq_row.n = n_prob; + // If the second head is not constant half, we need to lift it. + if (!rc.head1_is_constant_half) { + const f_t h = inv_sqrt_2 * rc.head_lift_sqrt_ratio; + // s_0 - h * x_h0 - h * x_h1 = 0 (h = inv_sqrt_2 * sqrt(d/s)) + eq_row.i = {rc.head0, rc.head1, slack_base}; + eq_row.x = {-h, -h, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + ++row_idx; + + // s_1 - h * x_h0 + h * x_h1 = 0 + eq_row.i = {rc.head0, rc.head1, slack_base + 1}; + eq_row.x = {-h, h, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + ++row_idx; + + is_cone_var[static_cast(rc.head0)] = 0; + is_cone_var[static_cast(rc.head1)] = 0; + } else { + // One head is constant half, so we can lift it directly. + // s_0 - inv_sqrt_2 * x_h0 = inv_sqrt_2 * (1/2) + eq_row.i = {rc.head0, slack_base}; + eq_row.x = {-inv_sqrt_2, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = inv_sqrt_2 * half; + ++row_idx; + + // s_1 - inv_sqrt_2 * x_h0 = -inv_sqrt_2 * (1/2) + eq_row.i = {rc.head0, slack_base + 1}; + eq_row.x = {-inv_sqrt_2, f_t(1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = -inv_sqrt_2 * half; + ++row_idx; + + is_cone_var[static_cast(rc.head0)] = 0; + } + + slack_base += 2; + } + + cuopt_expects(ri == rotated_cones.size(), + error_type_t::RuntimeError, + "Internal error: rotated SOC cone metadata mismatch"); + cuopt_expects(slack_base == n_prob, + error_type_t::RuntimeError, + "Internal error: slack variable count mismatch"); + cuopt_expects(row_idx == m_old + n_slack_total, + error_type_t::RuntimeError, + "Internal error: rotated SOC equality row count mismatch"); + cuopt_expects(csr_A.m == m_old + n_slack_total, + error_type_t::RuntimeError, + "Internal error: CSR row count after rotated SOC lift"); + } + + // If a variable appears in multiple cones, create per-cone aliases and add linking rows + // alias - original = 0 so cone variable blocks are disjoint. + { + std::vector first_owner(static_cast(n_prob), static_cast(-1)); + std::vector> cone_alias_pairs; // (alias, original) + + for (size_t ci = 0; ci < cone_vars.size(); ++ci) { + auto& cone = cone_vars[ci]; + for (auto& var : cone) { + cuopt_expects(var >= 0 && var < n_prob, + error_type_t::ValidationError, + "SOC variable index %d is outside [0, %d)", + static_cast(var), + static_cast(n_prob)); + const auto idx = static_cast(var); + if (first_owner[idx] == static_cast(-1)) { + first_owner[idx] = static_cast(ci); + continue; + } + if (first_owner[idx] != static_cast(ci)) { + const i_t alias = static_cast(n_prob + cone_alias_pairs.size()); + cone_alias_pairs.emplace_back(alias, var); + var = alias; + } + } + } + + if (!cone_alias_pairs.empty()) { + const i_t n_old = n_prob; + const i_t n_new = static_cast(n_old + cone_alias_pairs.size()); + const i_t m_old = csr_A.m; + const i_t m_new = static_cast(m_old + cone_alias_pairs.size()); + + user_problem.objective.resize(static_cast(n_new), f_t(0)); + user_problem.lower.resize(static_cast(n_new), -std::numeric_limits::infinity()); + user_problem.upper.resize(static_cast(n_new), std::numeric_limits::infinity()); + user_problem.var_types.resize( + static_cast(n_new), + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); + if (!user_problem.col_names.empty()) { + user_problem.col_names.resize(static_cast(n_new)); + } + + for (const auto& [alias, original] : cone_alias_pairs) { + // Cone copies are not box-constrained; linking rows tie them to the linear original. + user_problem.lower[static_cast(alias)] = + -std::numeric_limits::infinity(); + user_problem.upper[static_cast(alias)] = std::numeric_limits::infinity(); + user_problem.var_types[static_cast(alias)] = + user_problem.var_types[static_cast(original)]; + // Keep objective unchanged: alias coefficient stays zero and alias==original links + // values. + if (!user_problem.col_names.empty()) { + user_problem.col_names[static_cast(alias)] = + "_CUOPT_cone_alias_" + std::to_string(static_cast(alias - n_old)); + } + } + + user_problem.rhs.resize(static_cast(m_new)); + user_problem.row_sense.resize(static_cast(m_new)); + if (!user_problem.row_names.empty()) { + user_problem.row_names.resize(static_cast(m_new)); + } + + csr_A.n = n_new; + dual_simplex::sparse_vector_t eq_row; + eq_row.n = n_new; + i_t row_idx = m_old; + for (const auto& [alias, original] : cone_alias_pairs) { + eq_row.i = {alias, original}; + eq_row.x = {f_t(1), f_t(-1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + if (!user_problem.row_names.empty()) { + user_problem.row_names[static_cast(row_idx)] = + "_CUOPT_cone_alias_link_" + std::to_string(static_cast(row_idx - m_old)); + } + ++row_idx; + } + + cuopt_expects(row_idx == m_new, + error_type_t::RuntimeError, + "Internal error: cone alias linking row count mismatch"); + cuopt_expects(csr_A.m == m_new, + error_type_t::RuntimeError, + "Internal error: CSR row count after cone alias linking"); + + n_prob = n_new; + } + } + + // Box-bounded originals cannot sit in the cone block: introduce a free cone copy and + // alias - original = 0 so linear rows/objective keep the bounded variable. + { + const f_t neg_inf = -std::numeric_limits::infinity(); + const f_t pos_inf = std::numeric_limits::infinity(); + auto is_box_free = [&](i_t j) { + return user_problem.lower[static_cast(j)] == neg_inf && + user_problem.upper[static_cast(j)] == pos_inf; + }; + + std::vector> bound_split_pairs; // (cone_alias, linear_original) + + for (auto& cone : cone_vars) { + for (auto& var : cone) { + cuopt_expects(var >= 0 && var < n_prob, + error_type_t::ValidationError, + "SOC variable index %d is outside [0, %d)", + static_cast(var), + static_cast(n_prob)); + if (!is_box_free(var)) { + const i_t alias = static_cast(n_prob + bound_split_pairs.size()); + bound_split_pairs.emplace_back(alias, var); + var = alias; + } + } + } + + if (!bound_split_pairs.empty()) { + const i_t n_old = n_prob; + const i_t n_new = static_cast(n_old + bound_split_pairs.size()); + const i_t m_old = csr_A.m; + const i_t m_new = static_cast(m_old + bound_split_pairs.size()); + + user_problem.objective.resize(static_cast(n_new), f_t(0)); + user_problem.lower.resize(static_cast(n_new), neg_inf); + user_problem.upper.resize(static_cast(n_new), pos_inf); + user_problem.var_types.resize( + static_cast(n_new), + cuopt::linear_programming::dual_simplex::variable_type_t::CONTINUOUS); + if (!user_problem.col_names.empty()) { + user_problem.col_names.resize(static_cast(n_new)); + } + + for (const auto& [alias, original] : bound_split_pairs) { + user_problem.var_types[static_cast(alias)] = + user_problem.var_types[static_cast(original)]; + if (!user_problem.col_names.empty()) { + user_problem.col_names[static_cast(alias)] = + "_CUOPT_cone_bound_split_" + std::to_string(static_cast(alias - n_old)); + } + } + + user_problem.rhs.resize(static_cast(m_new)); + user_problem.row_sense.resize(static_cast(m_new)); + if (!user_problem.row_names.empty()) { + user_problem.row_names.resize(static_cast(m_new)); + } + + csr_A.n = n_new; + dual_simplex::sparse_vector_t eq_row; + eq_row.n = n_new; + i_t row_idx = m_old; + for (const auto& [alias, original] : bound_split_pairs) { + eq_row.i = {alias, original}; + eq_row.x = {f_t(1), f_t(-1)}; + eq_row.sort(); + csr_A.append_row(eq_row); + user_problem.row_sense[static_cast(row_idx)] = 'E'; + user_problem.rhs[static_cast(row_idx)] = f_t(0); + if (!user_problem.row_names.empty()) { + user_problem.row_names[static_cast(row_idx)] = + "_CUOPT_cone_bound_split_link_" + std::to_string(static_cast(row_idx - m_old)); + } + ++row_idx; + } + + cuopt_expects(row_idx == m_new, + error_type_t::RuntimeError, + "Internal error: cone bound-split linking row count mismatch"); + cuopt_expects(csr_A.m == m_new, + error_type_t::RuntimeError, + "Internal error: CSR row count after cone bound-split linking"); + + n_prob = n_new; + } + } + + is_cone_var.assign(static_cast(n_prob), 0); + for (const auto& cone : cone_vars) { + for (const i_t var : cone) { + cuopt_expects(var >= 0 && var < n_prob, + error_type_t::ValidationError, + "SOC variable index %d is outside [0, %d) after cone aliasing", + static_cast(var), + static_cast(n_prob)); + is_cone_var[static_cast(var)] = 1; + } + } + + std::vector old_to_new(static_cast(n_prob), i_t{-1}); + std::vector new_to_old; + new_to_old.reserve(static_cast(n_prob)); + for (i_t j = 0; j < n_prob; ++j) { + if (is_cone_var[static_cast(j)]) { continue; } + old_to_new[static_cast(j)] = static_cast(new_to_old.size()); + new_to_old.push_back(j); + } + const i_t cone_var_start = static_cast(new_to_old.size()); + for (const auto& cone : cone_vars) { + for (const i_t old_j : cone) { + old_to_new[static_cast(old_j)] = static_cast(new_to_old.size()); + new_to_old.push_back(old_j); + } + } + cuopt_expects(static_cast(new_to_old.size()) == n_prob, + error_type_t::RuntimeError, + "Internal error while building SOC variable permutation"); + + for (i_t row = 0; row < csr_A.m; ++row) { + for (i_t p = csr_A.row_start[static_cast(row)]; + p < csr_A.row_start[static_cast(row + 1)]; + ++p) { + const i_t old_j = csr_A.j[static_cast(p)]; + cuopt_expects(old_j >= 0 && old_j < n_prob, + error_type_t::ValidationError, + "Linear constraint matrix column index %d is outside [0, %d)", + static_cast(old_j), + static_cast(n_prob)); + csr_A.j[static_cast(p)] = old_to_new[static_cast(old_j)]; + } + } + + auto permute_dense_by_old_to_new = [&](auto& values, const char* name) { + if (values.empty()) { return; } + using value_t = typename std::decay_t::value_type; + cuopt_expects(values.size() == static_cast(n_prob), + error_type_t::ValidationError, + "%s length %zu does not match number of variables %d", + name, + values.size(), + static_cast(n_prob)); + std::vector permuted(values.size()); + for (i_t old_j = 0; old_j < n_prob; ++old_j) { + permuted[static_cast(old_to_new[static_cast(old_j)])] = + std::move(values[static_cast(old_j)]); + } + values = std::move(permuted); + }; + + permute_dense_by_old_to_new(user_problem.objective, "objective"); + permute_dense_by_old_to_new(user_problem.lower, "lower bounds"); + permute_dense_by_old_to_new(user_problem.upper, "upper bounds"); + permute_dense_by_old_to_new(user_problem.var_types, "variable types"); + permute_dense_by_old_to_new(user_problem.col_names, "column names"); + + if (!user_problem.Q_values.empty()) { + const i_t n_model = static_cast(n); + cuopt_expects(user_problem.Q_indices.size() == user_problem.Q_values.size(), + error_type_t::ValidationError, + "Quadratic objective indices and values length mismatch"); + cuopt_expects(user_problem.Q_offsets.size() == static_cast(n_model) + 1, + error_type_t::ValidationError, + "Quadratic objective CSR offsets length must be n+1 when SOC QCMATRIX " + "conversion permutes variables"); + cuopt_expects(user_problem.Q_offsets[0] == 0, + error_type_t::ValidationError, + "Quadratic objective CSR offsets[0] must be 0"); + cuopt_expects(user_problem.Q_offsets[static_cast(n_model)] == + static_cast(user_problem.Q_values.size()), + error_type_t::ValidationError, + "Quadratic objective CSR last offset must equal number of nonzeros"); + + std::vector q_offsets(static_cast(n_prob) + 1, 0); + for (i_t old_row = 0; old_row < n_model; ++old_row) { + const i_t p_beg = user_problem.Q_offsets[static_cast(old_row)]; + const i_t p_end = user_problem.Q_offsets[static_cast(old_row + 1)]; + cuopt_expects( + p_beg >= 0 && p_beg <= p_end && p_end <= static_cast(user_problem.Q_values.size()), + error_type_t::ValidationError, + "Quadratic objective CSR offsets are invalid at row %d", + static_cast(old_row)); + const i_t new_row = old_to_new[static_cast(old_row)]; + q_offsets[static_cast(new_row + 1)] = p_end - p_beg; + } + for (i_t row = 0; row < n_prob; ++row) { + q_offsets[static_cast(row + 1)] += q_offsets[static_cast(row)]; + } + + std::vector q_indices(user_problem.Q_values.size()); + std::vector q_values(user_problem.Q_values.size()); + auto q_write = q_offsets; + for (i_t old_row = 0; old_row < n_model; ++old_row) { + const i_t new_row = old_to_new[static_cast(old_row)]; + for (i_t p = user_problem.Q_offsets[static_cast(old_row)]; + p < user_problem.Q_offsets[static_cast(old_row + 1)]; + ++p) { + const i_t old_col = user_problem.Q_indices[static_cast(p)]; + cuopt_expects(old_col >= 0 && old_col < n_model, + error_type_t::ValidationError, + "Quadratic objective column index %d is outside [0, %d)", + static_cast(old_col), + static_cast(n_model)); + const i_t dst = q_write[static_cast(new_row)]++; + q_indices[static_cast(dst)] = old_to_new[static_cast(old_col)]; + q_values[static_cast(dst)] = user_problem.Q_values[static_cast(p)]; + } + } + + user_problem.Q_offsets = std::move(q_offsets); + user_problem.Q_indices = std::move(q_indices); + user_problem.Q_values = std::move(q_values); + } + + user_problem.cone_var_start = cone_var_start; + user_problem.second_order_cone_dims = std::move(cone_dims); + user_problem.num_rows = csr_A.m; + user_problem.num_cols = n_prob; +} + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index a193bddded..721465d2a1 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -16,10 +16,16 @@ #include #include #include -#include namespace cuopt::linear_programming::dual_simplex { +template +/** Number of leading linear columns; SOCP cone variables occupy [linear_cols, num_cols). */ +static i_t linear_var_count(const lp_problem_t& problem) +{ + return problem.second_order_cone_dims.empty() ? problem.num_cols : problem.cone_var_start; +} + template i_t remove_empty_cols(lp_problem_t& problem, i_t& num_empty_cols, @@ -27,26 +33,18 @@ i_t remove_empty_cols(lp_problem_t& problem, { constexpr bool verbose = false; if (verbose) { printf("Removing %d empty columns\n", num_empty_cols); } - // We have a variable x_j that does not appear in any rows - // The cost function - // sum_{k != j} c_k * x_k + c_j * x_j - // becomes - // sum_{k != j} c_k * x_k + c_j * l_j if c_j > 0 - // or - // sum_{k != j} c_k * x_k + c_j * u_j if c_j < 0 presolve_info.removed_variables.reserve(num_empty_cols); presolve_info.removed_values.reserve(num_empty_cols); presolve_info.removed_reduced_costs.reserve(num_empty_cols); - // Check to see if a variable participates in a quadratic objective std::vector has_quadratic_term(problem.num_cols, false); + i_t linear_cols = linear_var_count(problem); if (problem.Q.n > 0) { - for (i_t j = 0; j < problem.num_cols; ++j) { + for (i_t j = 0; j < linear_cols; ++j) { const i_t row_start = problem.Q.row_start[j]; const i_t row_end = problem.Q.row_start[j + 1]; if (row_end - row_start == 0) { continue; } - // Q is symmetric, so its sufficient to check only the row size has_quadratic_term[j] = true; } } @@ -55,12 +53,13 @@ i_t remove_empty_cols(lp_problem_t& problem, i_t new_cols = 0; for (i_t j = 0; j < problem.num_cols; ++j) { bool remove_var = false; - if ((problem.A.col_start[j + 1] - problem.A.col_start[j]) == 0) { - if (problem.objective[j] >= 0 && problem.lower[j] > -inf && !has_quadratic_term[j]) { + if (j < linear_cols && (problem.A.col_start[j + 1] - problem.A.col_start[j]) == 0) { + bool non_removable = has_quadratic_term[j]; + if (problem.objective[j] >= 0 && problem.lower[j] > -inf && !non_removable) { presolve_info.removed_values.push_back(problem.lower[j]); problem.obj_constant += problem.objective[j] * problem.lower[j]; remove_var = true; - } else if (problem.objective[j] <= 0 && problem.upper[j] < inf && !has_quadratic_term[j]) { + } else if (problem.objective[j] <= 0 && problem.upper[j] < inf && !non_removable) { presolve_info.removed_values.push_back(problem.upper[j]); problem.obj_constant += problem.objective[j] * problem.upper[j]; remove_var = true; @@ -123,6 +122,12 @@ i_t remove_empty_cols(lp_problem_t& problem, problem.Q.check_matrix("After removing empty columns"); } + if (!problem.second_order_cone_dims.empty()) { + i_t new_cone_start = col_old_to_new[problem.cone_var_start]; + assert(new_cone_start != -1); + problem.cone_var_start = new_cone_start; + } + problem.objective = objective; problem.lower = lower; problem.upper = upper; @@ -272,6 +277,113 @@ i_t convert_less_than_to_equal(const user_problem_t& user_problem, // We must convert rows in the form: a_i^T x <= beta // into: a_i^T x + s_i = beta, s_i >= 0 + if (!problem.second_order_cone_dims.empty()) { + const i_t old_num_cols = problem.num_cols; + const i_t linear_cols = linear_var_count(problem); + const i_t num_slacks = less_rows; + const i_t num_cols = old_num_cols + num_slacks; + const i_t old_nnz = problem.A.col_start[old_num_cols]; + const i_t nnz = old_nnz + num_slacks; + const i_t new_cone_start = linear_cols + num_slacks; + + auto old_A = problem.A; + csc_matrix_t expanded_A(problem.A.m, num_cols, nnz); + + std::vector objective(num_cols, 0.0); + std::vector lower(num_cols, 0.0); + std::vector upper(num_cols, INFINITY); + std::vector old_to_new(old_num_cols, -1); + + for (i_t j = 0; j < linear_cols; ++j) { + old_to_new[j] = j; + objective[j] = problem.objective[j]; + lower[j] = problem.lower[j]; + upper[j] = problem.upper[j]; + } + for (i_t j = linear_cols; j < old_num_cols; ++j) { + old_to_new[j] = j + num_slacks; + objective[old_to_new[j]] = problem.objective[j]; + lower[old_to_new[j]] = problem.lower[j]; + upper[old_to_new[j]] = problem.upper[j]; + } + + i_t nz = 0; + for (i_t j = 0; j < linear_cols; ++j) { + expanded_A.col_start[j] = nz; + for (i_t p = old_A.col_start[j]; p < old_A.col_start[j + 1]; ++p) { + expanded_A.i[nz] = old_A.i[p]; + expanded_A.x[nz] = old_A.x[p]; + ++nz; + } + } + + i_t slack_col = linear_cols; + for (i_t i = 0; i < problem.num_rows; i++) { + if (row_sense[i] == 'L') { + expanded_A.col_start[slack_col] = nz; + expanded_A.i[nz] = i; + expanded_A.x[nz] = 1.0; + new_slacks.push_back(slack_col); + row_sense[i] = 'E'; + ++slack_col; + ++nz; + --less_rows; + } + } + + for (i_t j = linear_cols; j < old_num_cols; ++j) { + i_t new_j = old_to_new[j]; + expanded_A.col_start[new_j] = nz; + for (i_t p = old_A.col_start[j]; p < old_A.col_start[j + 1]; ++p) { + expanded_A.i[nz] = old_A.i[p]; + expanded_A.x[nz] = old_A.x[p]; + ++nz; + } + } + expanded_A.col_start[num_cols] = nz; + assert(less_rows == 0); + assert(slack_col == new_cone_start); + assert(nz == nnz); + + if (problem.Q.n > 0) { + const auto old_Q = problem.Q; + const i_t q_nnz = old_Q.row_start[old_num_cols]; + + problem.Q.row_start.assign(num_cols + 1, 0); + for (i_t row = 0; row < old_num_cols; ++row) { + i_t new_row = old_to_new[row]; + problem.Q.row_start[new_row + 1] = old_Q.row_start[row + 1] - old_Q.row_start[row]; + } + for (i_t row = 0; row < num_cols; ++row) { + problem.Q.row_start[row + 1] += problem.Q.row_start[row]; + } + + problem.Q.j.resize(q_nnz); + problem.Q.x.resize(q_nnz); + auto row_starts = problem.Q.row_start; + for (i_t row = 0; row < old_num_cols; ++row) { + i_t new_row = old_to_new[row]; + for (i_t p = old_Q.row_start[row]; p < old_Q.row_start[row + 1]; ++p) { + problem.Q.j[row_starts[new_row]] = old_to_new[old_Q.j[p]]; + problem.Q.x[row_starts[new_row]] = old_Q.x[p]; + ++row_starts[new_row]; + } + } + problem.Q.m = num_cols; + problem.Q.n = num_cols; + problem.Q.nz_max = q_nnz; + } + + problem.A = expanded_A; + problem.A.n = num_cols; + problem.objective = objective; + problem.lower = lower; + problem.upper = upper; + problem.num_cols = num_cols; + problem.cone_var_start = new_cone_start; + return 0; + } + i_t num_cols = problem.num_cols + less_rows; i_t nnz = problem.A.col_start[problem.num_cols] + less_rows; problem.A.col_start.resize(num_cols + 1); @@ -571,16 +683,18 @@ void convert_user_problem(const user_problem_t& user_problem, } // Copy info from user_problem to problem - problem.num_rows = user_problem.num_rows; - problem.num_cols = user_problem.num_cols; - problem.A = user_problem.A; - problem.objective = user_problem.objective; - problem.obj_scale = user_problem.obj_scale; - problem.obj_constant = user_problem.obj_constant; - problem.objective_is_integral = user_problem.objective_is_integral; - problem.rhs = user_problem.rhs; - problem.lower = user_problem.lower; - problem.upper = user_problem.upper; + problem.num_rows = user_problem.num_rows; + problem.num_cols = user_problem.num_cols; + problem.A = user_problem.A; + problem.objective = user_problem.objective; + problem.obj_scale = user_problem.obj_scale; + problem.obj_constant = user_problem.obj_constant; + problem.objective_is_integral = user_problem.objective_is_integral; + problem.rhs = user_problem.rhs; + problem.lower = user_problem.lower; + problem.upper = user_problem.upper; + problem.cone_var_start = user_problem.cone_var_start; + problem.second_order_cone_dims = user_problem.second_order_cone_dims; // Make a copy of row_sense so we can modify it std::vector row_sense = user_problem.row_sense; @@ -637,6 +751,7 @@ void convert_user_problem(const user_problem_t& user_problem, settings.log.debug( "equality rows %d less rows %d columns %d\n", equal_rows, less_rows, problem.num_cols); if (settings.barrier && settings.dualize != 0 && user_problem.Q_values.size() == 0 && + problem.second_order_cone_dims.empty() && (settings.dualize == 1 || (settings.dualize == -1 && less_rows > 1.2 * problem.num_cols && equal_rows < 2e4))) { settings.log.debug("Dualizing in presolve\n"); @@ -808,8 +923,8 @@ void convert_user_problem(const user_problem_t& user_problem, problem.Q.x = user_problem.Q_values; } - // Add artifical variables - if (!settings.barrier_presolve) { + // Artificial vars break [linear | cone]; only for barrier_presolve-off LP-style layouts. + if (!settings.barrier_presolve && problem.second_order_cone_dims.empty()) { add_artifical_variables(problem, user_problem.range_rows, equality_rows, new_slacks); } } @@ -821,13 +936,17 @@ i_t presolve(const lp_problem_t& original, presolve_info_t& presolve_info) { problem = original; + const i_t linear_cols = linear_var_count(problem); + const bool has_cones = !problem.second_order_cone_dims.empty(); std::vector row_sense(problem.num_rows, '='); - // Check for free variables + + // Check for free variables (linear block only; cone columns are handled by the barrier SOC layout) i_t free_variables = 0; - for (i_t j = 0; j < problem.num_cols; j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] == -inf && problem.upper[j] == inf) { free_variables++; } } + // Barrier presolve phase 1: bound free linear variables from equality rows. if (settings.barrier_presolve && free_variables > 0) { // Try to remove free variables std::vector constraints_to_check; @@ -835,7 +954,7 @@ i_t presolve(const lp_problem_t& original, std::vector row_marked(problem.num_rows, 0); current_free_variables.reserve(problem.num_cols); constraints_to_check.reserve(problem.num_rows); - for (i_t j = 0; j < problem.num_cols; j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] == -inf && problem.upper[j] == inf) { current_free_variables.push_back(j); const i_t col_start = problem.A.col_start[j]; @@ -859,10 +978,30 @@ i_t presolve(const lp_problem_t& original, std::vector upper_bound_coefficient(problem.num_cols, 0.0); if (!constraints_to_check.empty()) { - // Check if the constraints are feasible csr_matrix_t Arow(0, 0, 0); problem.A.to_compressed_row(Arow); + // Keep only rows safe for phase-1 bound inference: no cone columns, exactly one free linear. + { + std::vector safe_constraints; + safe_constraints.reserve(constraints_to_check.size()); + for (i_t i : constraints_to_check) { + bool touches_cone = false; + i_t free_linear_in_row = 0; + for (i_t p = Arow.row_start[i]; p < Arow.row_start[i + 1]; ++p) { + const i_t j = Arow.j[p]; + if (j >= linear_cols) { + touches_cone = true; + continue; + } + if (problem.lower[j] == -inf && problem.upper[j] == inf) { free_linear_in_row++; } + } + if (touches_cone || free_linear_in_row != 1) { continue; } + safe_constraints.push_back(i); + } + constraints_to_check.swap(safe_constraints); + } + // The constraints are in the form: // sum_j a_j x_j = beta for (i_t i : constraints_to_check) { @@ -876,6 +1015,7 @@ i_t presolve(const lp_problem_t& original, f_t last_free_coeff_i = 0.0; for (i_t p = row_start; p < row_end; p++) { const i_t j = Arow.j[p]; + if (j >= linear_cols) { continue; } const f_t aij = Arow.x[p]; const f_t lower_j = problem.lower[j]; const f_t upper_j = problem.upper[j]; @@ -929,6 +1069,7 @@ i_t presolve(const lp_problem_t& original, // x_j <= 1/a_ij * (rhs - upper_activity_i) const i_t j = last_free_i; const f_t a_ij = last_free_coeff_i; + if (a_ij == 0) { continue; } const f_t max_bound = 1e10; bool bounded = false; if (a_ij > 0) { @@ -986,6 +1127,9 @@ i_t presolve(const lp_problem_t& original, problem.lower[j] = -inf; } } + if (!(problem.lower[j] == -inf && problem.upper[j] == inf)) { + presolve_info.phase1_bounded_linear_indices.push_back(j); + } } // Record bounded free variables for dual correction in uncrush. @@ -1016,11 +1160,12 @@ i_t presolve(const lp_problem_t& original, } } + // Barrier presolve phase 2: negate one-sided bounds (-inf < x <= u -> -u <= x < inf). // The original problem may have a variable without a lower bound // but a finite upper bound - // -inf < x_j <= u_j + // -inf < x_j <= u_j (linear variables only) i_t no_lower_bound = 0; - for (i_t j = 0; j < problem.num_cols; j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] == -inf && problem.upper[j] < inf) { no_lower_bound++; } } @@ -1031,7 +1176,7 @@ i_t presolve(const lp_problem_t& original, // Handle -inf < x_j <= u_j by substituting x'_j = -x_j, giving -u_j <= x'_j < inf if (settings.barrier_presolve && no_lower_bound > 0) { presolve_info.negated_variables.reserve(no_lower_bound); - for (i_t j = 0; j < problem.num_cols; j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] == -inf && problem.upper[j] < inf) { presolve_info.negated_variables.push_back(j); @@ -1066,10 +1211,10 @@ i_t presolve(const lp_problem_t& original, } } - // The original problem may have nonzero lower bounds - // 0 != l_j <= x_j <= u_j + // Barrier presolve phase 3: shift nonzero lower bounds to zero (cone/stack columns not shifted). + // 0 != l_j <= x_j <= u_j (linear variables only; cone/stack columns are not shifted) i_t nonzero_lower_bounds = 0; - for (i_t j = 0; j < problem.num_cols; j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] != 0.0 && problem.lower[j] > -inf) { nonzero_lower_bounds++; } } if (settings.barrier_presolve && nonzero_lower_bounds > 0) { @@ -1092,7 +1237,7 @@ i_t presolve(const lp_problem_t& original, // so we get the constant term c_j * l_j std::vector lower_bounds_removed(problem.num_cols, false); - for (i_t j = 0; j < problem.num_cols; j++) { + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] != 0.0 && problem.lower[j] > -inf) { lower_bounds_removed[j] = true; presolve_info.removed_lower_bounds[j] = problem.lower[j]; @@ -1101,7 +1246,7 @@ i_t presolve(const lp_problem_t& original, auto old_objective = problem.objective; if (problem.Q.n > 0) { - for (i_t row = 0; row < problem.num_cols; row++) { + for (i_t row = 0; row < linear_cols; row++) { i_t row_start = problem.Q.row_start[row]; i_t row_end = problem.Q.row_start[row + 1]; for (i_t p = row_start; p < row_end; p++) { @@ -1122,7 +1267,7 @@ i_t presolve(const lp_problem_t& original, } std::vector kahan_compensation(problem.num_rows, 0.0); - for (i_t j = 0; j < problem.num_cols; j++) { + for (i_t j = 0; j < linear_cols; j++) { if (lower_bounds_removed[j]) { i_t col_start = problem.A.col_start[j]; i_t col_end = problem.A.col_start[j + 1]; @@ -1142,7 +1287,7 @@ i_t presolve(const lp_problem_t& original, } } - // Check for empty rows + // Barrier presolve phase 4: remove empty rows and empty linear columns. i_t num_empty_rows = 0; { csr_matrix_t Arow(0, 0, 0); @@ -1160,7 +1305,7 @@ i_t presolve(const lp_problem_t& original, // Check for empty cols i_t num_empty_cols = 0; { - for (i_t j = 0; j < problem.num_cols; ++j) { + for (i_t j = 0; j < linear_cols; ++j) { if ((problem.A.col_start[j + 1] - problem.A.col_start[j]) == 0) { num_empty_cols++; } } } @@ -1169,19 +1314,33 @@ i_t presolve(const lp_problem_t& original, remove_empty_cols(problem, num_empty_cols, presolve_info); } + // Check for free variables (exclude cone variables — they are naturally unbounded) + free_variables = 0; + for (i_t j = 0; j < linear_cols; j++) { + if (problem.lower[j] == -inf && problem.upper[j] == inf) { free_variables++; } + } problem.Q.check_matrix("Before free variable expansion"); - // For QPs, keep free variables as-is rather than - // splitting x = v - w. The barrier solver handles them natively with a - // static regularizer on the diagonal instead of z/x complementarity terms. - if (settings.barrier_presolve && free_variables > 0 && problem.Q.n > 0) { - presolve_info.free_variable_indices.clear(); - for (i_t j = 0; j < problem.num_cols; j++) { + // Barrier presolve phase 5: free linear variables — 5a native (QP/SOCP) or 5b v-w split (LP). + // QP/SOCP augmented system: keep free linears as-is (no x = v - w); barrier uses D = 0 on them. + const bool register_native_free_linear = free_variables > 0 && (problem.Q.n > 0 || has_cones); + if (register_native_free_linear) { + presolve_info.free_variable_pairs.clear(); + presolve_info.native_free_linear_indices.clear(); + // Only linear decision variables can be "native" free variables; cone/stack columns + // are unbounded by construction and must not be counted here. + i_t native_free_count = 0; + for (i_t j = 0; j < linear_cols; j++) { if (problem.lower[j] == -inf && problem.upper[j] == inf) { - presolve_info.free_variable_indices.push_back(j); + presolve_info.native_free_linear_indices.push_back(j); + native_free_count++; } } - } else if (settings.barrier_presolve && free_variables > 0) { + settings.log.printf( + "Keeping %d native free linear variables for augmented-system barrier (QP/SOCP)\n", + native_free_count); + } else if (settings.barrier_presolve && !has_cones && free_variables > 0) { + // Phase 5b: x_j = v - w with v, w >= 0 (LP without cones; SOCP/QP use phase 5a). // We have a variable x_j: with -inf < x_j < inf // we create new variables v and w with 0 <= v, w and x_j = v - w // Constraints @@ -1194,123 +1353,163 @@ i_t presolve(const lp_problem_t& original, // becomes // sum_{k != j} c_k x_k + c_j v - c_j w - std::vector pair_index(problem.num_cols, -1); - i_t num_cols = problem.num_cols + free_variables; - i_t nnz = problem.A.col_start[problem.num_cols]; - for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf) { - nnz += (problem.A.col_start[j + 1] - problem.A.col_start[j]); + const i_t old_num_cols = problem.num_cols; + const i_t new_cone_start = + problem.second_order_cone_dims.empty() ? 0 : linear_cols + free_variables; + const i_t num_cols = old_num_cols + free_variables; + + auto old_A = problem.A; + auto old_Q = problem.Q; + auto old_objective = problem.objective; + auto old_lower = problem.lower; + auto old_upper = problem.upper; + + std::vector partner_index(old_num_cols, -1); + std::vector orig_to_new(old_num_cols, -1); + std::vector is_free(old_num_cols, false); + + i_t next_partner = linear_cols; + for (i_t j = 0; j < linear_cols; ++j) { + orig_to_new[j] = j; + if (old_lower[j] == -inf && old_upper[j] == inf) { + is_free[j] = true; + partner_index[j] = next_partner++; } } + for (i_t j = linear_cols; j < old_num_cols; ++j) { + orig_to_new[j] = j + free_variables; + } + assert(next_partner == new_cone_start || problem.second_order_cone_dims.empty()); - problem.A.col_start.resize(num_cols + 1); - problem.A.i.resize(nnz); - problem.A.x.resize(nnz); - problem.lower.resize(num_cols); - problem.upper.resize(num_cols); - problem.objective.resize(num_cols); + i_t nnz = old_A.col_start[old_num_cols]; + for (i_t j = 0; j < linear_cols; ++j) { + if (is_free[j]) { nnz += old_A.col_start[j + 1] - old_A.col_start[j]; } + } - presolve_info.free_variable_pairs.resize(free_variables * 2); - i_t pair_count = 0; - i_t q = problem.A.col_start[problem.num_cols]; - i_t col = problem.num_cols; - for (i_t j = 0; j < problem.num_cols; j++) { - if (problem.lower[j] == -inf && problem.upper[j] == inf) { - for (i_t p = problem.A.col_start[j]; p < problem.A.col_start[j + 1]; p++) { - i_t i = problem.A.i[p]; - f_t aij = problem.A.x[p]; - problem.A.i[q] = i; - problem.A.x[q] = -aij; - q++; + csc_matrix_t expanded_A(problem.A.m, num_cols, nnz); + i_t nz = 0; + for (i_t j = 0; j < linear_cols; ++j) { + expanded_A.col_start[j] = nz; + for (i_t p = old_A.col_start[j]; p < old_A.col_start[j + 1]; ++p) { + expanded_A.i[nz] = old_A.i[p]; + expanded_A.x[nz] = old_A.x[p]; + ++nz; + } + } + for (i_t j = 0; j < linear_cols; ++j) { + if (partner_index[j] != -1) { + expanded_A.col_start[partner_index[j]] = nz; + for (i_t p = old_A.col_start[j]; p < old_A.col_start[j + 1]; ++p) { + expanded_A.i[nz] = old_A.i[p]; + expanded_A.x[nz] = -old_A.x[p]; + ++nz; } - problem.lower[col] = 0.0; - problem.upper[col] = inf; - problem.objective[col] = -problem.objective[j]; - presolve_info.free_variable_pairs[pair_count++] = j; - presolve_info.free_variable_pairs[pair_count++] = col; - pair_index[j] = col; - problem.A.col_start[++col] = q; - problem.lower[j] = 0.0; } } + for (i_t j = linear_cols; j < old_num_cols; ++j) { + i_t new_j = orig_to_new[j]; + expanded_A.col_start[new_j] = nz; + for (i_t p = old_A.col_start[j]; p < old_A.col_start[j + 1]; ++p) { + expanded_A.i[nz] = old_A.i[p]; + expanded_A.x[nz] = old_A.x[p]; + ++nz; + } + } + expanded_A.col_start[num_cols] = nz; + + std::vector objective(num_cols); + std::vector lower(num_cols, -INFINITY); + std::vector upper(num_cols, INFINITY); + presolve_info.free_variable_pairs.clear(); + presolve_info.free_variable_pairs.reserve(free_variables * 2); + + for (i_t j = 0; j < linear_cols; ++j) { + objective[j] = old_objective[j]; + if (is_free[j]) { + lower[j] = 0.0; + upper[j] = inf; + objective[partner_index[j]] = -old_objective[j]; + lower[partner_index[j]] = 0.0; + upper[partner_index[j]] = inf; + presolve_info.free_variable_pairs.push_back(j); + presolve_info.free_variable_pairs.push_back(partner_index[j]); + } else { + lower[j] = old_lower[j]; + upper[j] = old_upper[j]; + } + } + for (i_t j = linear_cols; j < old_num_cols; ++j) { + i_t new_j = orig_to_new[j]; + objective[new_j] = old_objective[j]; + lower[new_j] = old_lower[j]; + upper[new_j] = old_upper[j]; + } - if (problem.Q.n > 0) { + if (old_Q.n > 0) { std::vector row_counts(num_cols, 0); - i_t nz_count = problem.Q.row_start[problem.num_cols]; - for (i_t row = 0; row < problem.Q.n; row++) { - i_t q_start = problem.Q.row_start[row]; - i_t q_end = problem.Q.row_start[row + 1]; - row_counts[row] = q_end - q_start; - for (i_t qj = q_start; qj < q_end; qj++) { - i_t col = problem.Q.j[qj]; - if (pair_index[row] != -1 && pair_index[col] != -1) { - assert(pair_index[row] >= problem.num_cols); - assert(pair_index[col] >= problem.num_cols); - row_counts[row]++; - row_counts[pair_index[row]] += 2; - nz_count += 3; - } else if (pair_index[col] != -1) { - assert(pair_index[col] >= problem.num_cols); - row_counts[row]++; + i_t nz_count = 0; + for (i_t row = 0; row < old_Q.n; ++row) { + i_t new_row = orig_to_new[row]; + i_t partner_row = partner_index[row]; + i_t q_start = old_Q.row_start[row]; + i_t q_end = old_Q.row_start[row + 1]; + for (i_t qj = q_start; qj < q_end; ++qj) { + i_t col = old_Q.j[qj]; + i_t partner_col = partner_index[col]; + row_counts[new_row]++; + nz_count++; + if (partner_col != -1) { + row_counts[new_row]++; nz_count++; - } else if (pair_index[row] != -1) { - assert(pair_index[row] >= problem.num_cols); - row_counts[pair_index[row]]++; + } + if (partner_row != -1) { + row_counts[partner_row]++; nz_count++; + if (partner_col != -1) { + row_counts[partner_row]++; + nz_count++; + } } } } std::vector Q_row_start(num_cols + 1); Q_row_start[0] = 0; - for (i_t row = 0; row < num_cols; row++) { + for (i_t row = 0; row < num_cols; ++row) { Q_row_start[row + 1] = Q_row_start[row] + row_counts[row]; } std::vector Q_j(nz_count); std::vector Q_x(nz_count); auto row_starts = Q_row_start; - // First copy the original Q ma - for (i_t row = 0; row < problem.Q.n; row++) { - i_t q_start = problem.Q.row_start[row]; - i_t q_end = problem.Q.row_start[row + 1]; - i_t q_nz = Q_row_start[row]; - for (i_t qj = q_start; qj < q_end; qj++) { - i_t col = problem.Q.j[qj]; - f_t qij = problem.Q.x[qj]; - Q_j[q_nz] = col; - Q_x[q_nz] = qij; - q_nz++; - } - row_starts[row] = q_nz; - } - - // Expand the Q matrix for the free variables - for (i_t row = 0; row < problem.Q.n; row++) { - i_t q_start = problem.Q.row_start[row]; - i_t q_end = problem.Q.row_start[row + 1]; - for (i_t qj = q_start; qj < q_end; qj++) { - i_t col = problem.Q.j[qj]; - f_t qij = problem.Q.x[qj]; - if (pair_index[row] != -1 && pair_index[col] != -1) { - Q_j[row_starts[row]] = pair_index[col]; - Q_x[row_starts[row]] = -qij; - row_starts[row]++; - - Q_j[row_starts[pair_index[row]]] = col; - Q_x[row_starts[pair_index[row]]] = -qij; - row_starts[pair_index[row]]++; - - Q_j[row_starts[pair_index[row]]] = pair_index[col]; - Q_x[row_starts[pair_index[row]]] = qij; - row_starts[pair_index[row]]++; - } else if (pair_index[col] != -1) { - Q_j[row_starts[row]] = pair_index[col]; - Q_x[row_starts[row]] = -qij; - row_starts[row]++; - } else if (pair_index[row] != -1) { - Q_j[row_starts[pair_index[row]]] = col; - Q_x[row_starts[pair_index[row]]] = -qij; - row_starts[pair_index[row]]++; + for (i_t row = 0; row < old_Q.n; ++row) { + i_t new_row = orig_to_new[row]; + i_t partner_row = partner_index[row]; + i_t q_start = old_Q.row_start[row]; + i_t q_end = old_Q.row_start[row + 1]; + for (i_t qj = q_start; qj < q_end; ++qj) { + i_t col = old_Q.j[qj]; + f_t qij = old_Q.x[qj]; + i_t new_col = orig_to_new[col]; + i_t partner_col = partner_index[col]; + + Q_j[row_starts[new_row]] = new_col; + Q_x[row_starts[new_row]] = qij; + row_starts[new_row]++; + + if (partner_col != -1) { + Q_j[row_starts[new_row]] = partner_col; + Q_x[row_starts[new_row]] = -qij; + row_starts[new_row]++; + } + if (partner_row != -1) { + Q_j[row_starts[partner_row]] = new_col; + Q_x[row_starts[partner_row]] = -qij; + row_starts[partner_row]++; + if (partner_col != -1) { + Q_j[row_starts[partner_row]] = partner_col; + Q_x[row_starts[partner_row]] = qij; + row_starts[partner_row]++; + } } } } @@ -1323,12 +1522,17 @@ i_t presolve(const lp_problem_t& original, problem.Q.check_matrix("After free variable expansion"); } - // assert(problem.A.p[num_cols] == nnz); - problem.A.n = num_cols; - problem.num_cols = num_cols; + problem.A = expanded_A; + problem.A.n = num_cols; + problem.objective = objective; + problem.lower = lower; + problem.upper = upper; + problem.num_cols = num_cols; + if (!problem.second_order_cone_dims.empty()) { problem.cone_var_start = new_cone_start; } } - if (settings.barrier_presolve && settings.folding != 0 && problem.Q.n == 0) { + if (settings.barrier_presolve && settings.folding != 0 && problem.Q.n == 0 && + problem.second_order_cone_dims.empty()) { folding(problem, settings, presolve_info); } @@ -1406,7 +1610,7 @@ void crush_primal_solution(const user_problem_t& user_problem, // including previously added slacks, are reset before writing new values. solution.assign(problem.num_cols, 0.0); for (i_t j = 0; j < user_problem.num_cols; j++) { - solution[j] = user_solution[j]; + solution[user_col_to_problem_col(user_problem, problem, j)] = user_solution[j]; } std::vector primal_residual(problem.num_rows); @@ -1446,7 +1650,7 @@ void crush_primal_solution_with_slack(const user_problem_t& user_probl // Re-crush can be called with a reused output vector; clear stale entries first. solution.assign(problem.num_cols, 0.0); for (i_t j = 0; j < user_problem.num_cols; j++) { - solution[j] = user_solution[j]; + solution[user_col_to_problem_col(user_problem, problem, j)] = user_solution[j]; } std::vector primal_residual(problem.num_rows); @@ -1493,9 +1697,9 @@ f_t crush_dual_solution(const user_problem_t& user_problem, for (i_t i = 0; i < user_problem.num_rows; i++) { y[i] = user_y[i]; } - z.resize(problem.num_cols); + z.assign(problem.num_cols, 0.0); for (i_t j = 0; j < user_problem.num_cols; j++) { - z[j] = user_z[j]; + z[user_col_to_problem_col(user_problem, problem, j)] = user_z[j]; } std::vector is_range_row(problem.num_rows, false); @@ -1562,6 +1766,17 @@ f_t crush_dual_solution(const user_problem_t& user_problem, return dual_res_inf; } +template +static i_t user_col_to_problem_col(const user_problem_t& user_problem, + const lp_problem_t& problem, + i_t user_col) +{ + if (user_problem.second_order_cone_dims.empty()) { return user_col; } + if (problem.cone_var_start <= user_problem.cone_var_start) { return user_col; } + if (user_col < user_problem.cone_var_start) { return user_col; } + return problem.cone_var_start + (user_col - user_problem.cone_var_start); +} + template void uncrush_primal_solution(const user_problem_t& user_problem, const lp_problem_t& problem, @@ -1571,9 +1786,9 @@ void uncrush_primal_solution(const user_problem_t& user_problem, user_solution.resize(user_problem.num_cols); assert(problem.num_cols >= user_problem.num_cols); assert(solution.size() >= user_problem.num_cols); - std::copy(solution.begin(), - solution.begin() + std::min((i_t)solution.size(), user_problem.num_cols), - user_solution.data()); + for (i_t j = 0; j < user_problem.num_cols; ++j) { + user_solution[j] = solution[user_col_to_problem_col(user_problem, problem, j)]; + } } template @@ -1677,13 +1892,25 @@ void uncrush_solution(const presolve_info_t& presolve_info, if (num_free_variables > 0) { settings.log.printf("Post-solve: Handling free variables %d\n", num_free_variables); // We added free variables so we need to map the crushed solution back to the original variables + std::vector remove_partner(input_x.size(), false); for (i_t k = 0; k < 2 * num_free_variables; k += 2) { const i_t u = free_variable_pairs[k]; const i_t v = free_variable_pairs[k + 1]; input_x[u] -= input_x[v]; + remove_partner[v] = true; + } + std::vector compact_x; + std::vector compact_z; + compact_x.reserve(input_x.size() - num_free_variables); + compact_z.reserve(input_z.size() - num_free_variables); + for (i_t j = 0; j < static_cast(input_x.size()); ++j) { + if (!remove_partner[j]) { + compact_x.push_back(input_x[j]); + compact_z.push_back(input_z[j]); + } } - input_z.resize(input_z.size() - num_free_variables); - input_x.resize(input_x.size() - num_free_variables); + input_x = compact_x; + input_z = compact_z; } if (presolve_info.removed_variables.size() > 0) { @@ -1754,6 +1981,11 @@ void uncrush_solution(const presolve_info_t& presolve_info, } } + if (!presolve_info.phase1_bounded_linear_indices.empty()) { + settings.log.printf("Post-solve: %d linear column(s) had phase-1 bound tightening (x unchanged)\n", + static_cast(presolve_info.phase1_bounded_linear_indices.size())); + } + // Dual correction for originally free variables that received implied bounds. // Barrier produced (y, z) with z_j != 0 satisfying A^T y + z = c + Qx. // We need corrected (y_bar, z_bar) with z_bar_j = 0 for all j in F_b where @@ -1772,6 +2004,8 @@ void uncrush_solution(const presolve_info_t& presolve_info, settings.log.printf("Post-solve: Correcting duals for %d bounded free variables\n", static_cast(presolve_info.bounded_free_variables.size())); const csc_matrix_t& A = original_problem.A; + csr_matrix_t A_row(A.m, A.n, A.nnz()); + A.to_compressed_row(A_row); // Traverse in reverse order, to ensure that all z_j = 0 after the correction for (auto it = presolve_info.bounded_free_variables.rbegin(); it != presolve_info.bounded_free_variables.rend(); @@ -1781,15 +2015,9 @@ void uncrush_solution(const presolve_info_t& presolve_info, if (w_j == 0.0) { continue; } const f_t du = w_j / bfv.coefficient; input_y[bfv.constraint] += du; - for (i_t j = 0; j < A.n; j++) { - const i_t col_start = A.col_start[j]; - const i_t col_end = A.col_start[j + 1]; - for (i_t p = col_start; p < col_end; p++) { - if (A.i[p] == bfv.constraint) { - input_z[j] -= A.x[p] * du; - break; - } - } + const auto [row_start, row_end] = A_row.get_constraint_range(bfv.constraint); + for (i_t p = row_start; p < row_end; ++p) { + input_z[A_row.j[p]] -= A_row.x[p] * du; } } } diff --git a/cpp/src/dual_simplex/presolve.hpp b/cpp/src/dual_simplex/presolve.hpp index c050454b33..bc67627c21 100644 --- a/cpp/src/dual_simplex/presolve.hpp +++ b/cpp/src/dual_simplex/presolve.hpp @@ -49,6 +49,8 @@ struct lp_problem_t { f_t obj_constant; f_t obj_scale; // 1.0 for min, -1.0 for max bool objective_is_integral{false}; + i_t cone_var_start{0}; + std::vector second_order_cone_dims; void write_mps(const std::string& path) const { @@ -162,10 +164,14 @@ struct presolve_info_t { // Variables that were negated to handle -inf < x_j <= u_j std::vector negated_variables; + + // Free variable indices for QP/SOCP augmented barrier (not split, handled natively) + std::vector native_free_linear_indices; + + // Linear columns where phase 1 tightened a fully free var to a one-sided bound (same x index) + std::vector phase1_bounded_linear_indices; // Originally-free variables that received implied bounds, with the constraint used std::vector> bounded_free_variables; - // Free variable indices for QP augmented system (not split, handled natively) - std::vector free_variable_indices; }; template diff --git a/cpp/src/dual_simplex/scaling.cpp b/cpp/src/dual_simplex/scaling.cpp index 1531c91486..2db6c75b43 100644 --- a/cpp/src/dual_simplex/scaling.cpp +++ b/cpp/src/dual_simplex/scaling.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -16,21 +16,174 @@ template i_t column_scaling(const lp_problem_t& unscaled, const simplex_solver_settings_t& settings, lp_problem_t& scaled, - std::vector& column_scaling) + std::vector& col_scale, + std::vector& row_scale) { scaled = unscaled; i_t m = scaled.num_rows; i_t n = scaled.num_cols; - if (!settings.scale_columns || unscaled.Q.n > 0) { + row_scale.assign(m, 1.0); + + // ========================================================================= + // Ruiz equilibration for SOCP (and QP) problems + // ========================================================================= + // For SOCP problems, apply Ruiz equilibration: alternating row and column + // infinity-norm scaling to bring the constraint matrix close to equilibrium. + // This dramatically improves the conditioning of the augmented KKT system. + // Ruiz equilibration for SOCP (and QP) problems, applied only when the + // constraint matrix has a large row-norm imbalance. + if (!unscaled.second_order_cone_dims.empty() || unscaled.Q.n > 0) { + col_scale.assign(n, 1.0); + + // Decide whether Ruiz scaling is needed by checking row-norm imbalance. + // If max_row_norm / min_row_norm is small, the matrix is already well-conditioned + // and scaling can hurt (e.g. by amplifying tiny noise coefficients). + csr_matrix_t Arow_check(0, 0, 0); + scaled.A.to_compressed_row(Arow_check); + f_t max_row_norm = 0; + f_t min_row_norm = std::numeric_limits::max(); + for (i_t i = 0; i < m; ++i) { + f_t row_norm = 0; + for (i_t p = Arow_check.row_start[i]; p < Arow_check.row_start[i + 1]; ++p) { + f_t a = std::abs(Arow_check.x[p]); + if (a > row_norm) row_norm = a; + } + if (row_norm > 0) { + max_row_norm = std::max(max_row_norm, row_norm); + min_row_norm = std::min(min_row_norm, row_norm); + } + } + f_t row_norm_ratio = (min_row_norm > 0) ? max_row_norm / min_row_norm : 1.0; + + if (row_norm_ratio < 100.0) { + settings.log.printf("Skipping Ruiz equilibration (row norm ratio %.1f < 100)\n", + row_norm_ratio); + return 0; + } + + // Apply Ruiz equilibration + csr_matrix_t Arow(0, 0, 0); + scaled.A.to_compressed_row(Arow); + + constexpr i_t max_ruiz_iterations = 10; + for (i_t iter = 0; iter < max_ruiz_iterations; ++iter) { + f_t max_deviation = 0.0; + + // --- Row scaling: scale each row by 1/sqrt(max|a_ij|) --- + std::vector r(m); + for (i_t i = 0; i < m; ++i) { + f_t rm = 0.0; + for (i_t p = Arow.row_start[i]; p < Arow.row_start[i + 1]; ++p) { + f_t a = std::abs(Arow.x[p]); + if (a > rm) rm = a; + } + r[i] = rm > 0 ? 1.0 / std::sqrt(rm) : 1.0; + max_deviation = std::max(max_deviation, std::abs(rm - 1.0)); + } + for (i_t j = 0; j < n; ++j) { + for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { + scaled.A.x[p] *= r[scaled.A.i[p]]; + } + } + for (i_t i = 0; i < m; ++i) { + for (i_t p = Arow.row_start[i]; p < Arow.row_start[i + 1]; ++p) { + Arow.x[p] *= r[i]; + } + scaled.rhs[i] *= r[i]; + row_scale[i] *= r[i]; + } + + // --- Column scaling: scale each column by 1/sqrt(max|a_ij|) --- + // For cone variables, use a uniform scale per cone block to preserve SOC structure. + std::vector c(n); + const i_t cone_start = unscaled.second_order_cone_dims.empty() ? n : unscaled.cone_var_start; + + // Linear columns: scale independently + for (i_t j = 0; j < cone_start; ++j) { + f_t cm = 0.0; + for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { + f_t a = std::abs(scaled.A.x[p]); + if (a > cm) cm = a; + } + c[j] = cm > 0 ? 1.0 / std::sqrt(cm) : 1.0; + max_deviation = std::max(max_deviation, std::abs(cm - 1.0)); + } + + // Cone columns: uniform scale per cone block + i_t cone_off = cone_start; + for (i_t k = 0; k < static_cast(unscaled.second_order_cone_dims.size()); ++k) { + i_t q_k = unscaled.second_order_cone_dims[k]; + // Find max column inf-norm across all columns in this cone + f_t cone_max = 0.0; + for (i_t j = cone_off; j < cone_off + q_k; ++j) { + for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { + f_t a = std::abs(scaled.A.x[p]); + if (a > cone_max) cone_max = a; + } + } + f_t cone_scale = cone_max > 0 ? 1.0 / std::sqrt(cone_max) : 1.0; + max_deviation = std::max(max_deviation, std::abs(cone_max - 1.0)); + for (i_t j = cone_off; j < cone_off + q_k; ++j) { + c[j] = cone_scale; + } + cone_off += q_k; + } + for (i_t j = 0; j < n; ++j) { + for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) { + scaled.A.x[p] *= c[j]; + } + } + for (i_t i = 0; i < m; ++i) { + for (i_t p = Arow.row_start[i]; p < Arow.row_start[i + 1]; ++p) { + Arow.x[p] *= c[Arow.j[p]]; + } + } + for (i_t j = 0; j < n; ++j) { + scaled.objective[j] *= c[j]; + col_scale[j] *= c[j]; + } + // Bounds use +/-inf for unbounded sides (see types.hpp). Use +/-1e20 as a practical + // sentinel: we do not expect finite bounds beyond this magnitude, and skipping scale + // on |bound| >= 1e20 avoids overflow when dividing very large limits by small c[j]. + for (i_t j = 0; j < n; ++j) { + if (scaled.lower[j] > -1e20) scaled.lower[j] /= c[j]; + if (scaled.upper[j] < 1e20) scaled.upper[j] /= c[j]; + } + if (scaled.Q.n > 0) { + for (i_t row = 0; row < scaled.Q.m; ++row) { + for (i_t p = scaled.Q.row_start[row]; p < scaled.Q.row_start[row + 1]; ++p) { + i_t col = scaled.Q.j[p]; + scaled.Q.x[p] *= c[row] * c[col]; + } + } + } + if (max_deviation < 0.1) break; + } + + f_t a_min = std::numeric_limits::max(); + f_t a_max = 0; + for (i_t p = 0; p < scaled.A.col_start[n]; ++p) { + f_t a = std::abs(scaled.A.x[p]); + if (a > 0) { + a_min = std::min(a_min, a); + a_max = std::max(a_max, a); + } + } + settings.log.printf("Ruiz equilibration: coefficient range [%e, %e]\n", a_min, a_max); + return 0; + } + + if (!settings.scale_columns) { settings.log.printf("Skipping column scaling\n"); - column_scaling.resize(n, 1.0); + col_scale.resize(n, 1.0); return 0; } - column_scaling.resize(n); - f_t max = 0; - f_t min = std::numeric_limits::max(); + col_scale.resize(n); + + f_t max_col_norm = 0; + f_t min_col_norm = std::numeric_limits::max(); for (i_t j = 0; j < n; ++j) { const i_t col_start = scaled.A.col_start[j]; const i_t col_end = scaled.A.col_start[j + 1]; @@ -39,57 +192,63 @@ i_t column_scaling(const lp_problem_t& unscaled, const f_t x = scaled.A.x[p]; sum += x * x; } - f_t col_norm_j = column_scaling[j] = sum > 0 ? std::sqrt(sum) : 1.0; - max = std::max(col_norm_j, max); - min = std::min(col_norm_j, min); + const f_t col_norm = sum > 0 ? std::sqrt(sum) : 1.0; + col_scale[j] = f_t(1) / col_norm; + max_col_norm = std::max(col_norm, max_col_norm); + min_col_norm = std::min(col_norm, min_col_norm); } - settings.log.printf("Scaling matrix. Maximum column norm %e, minimum column norm %e\n", max, min); - // C(j, j) = 1/column_scaling(j) + settings.log.printf("Scaling matrix. Maximum column norm %e, minimum column norm %e\n", + max_col_norm, + min_col_norm); // scaled_A = unscaled_A * C for (i_t j = 0; j < n; ++j) { const i_t col_start = scaled.A.col_start[j]; const i_t col_end = scaled.A.col_start[j + 1]; for (i_t p = col_start; p < col_end; ++p) { - scaled.A.x[p] /= column_scaling[j]; + scaled.A.x[p] *= col_scale[j]; } - } - // scaled_obj = C*unscaled_obj - for (i_t j = 0; j < n; ++j) { - scaled.objective[j] /= column_scaling[j]; - } - // scaled_lower = C^{-1} * unscaled_lower - // scaled_upper = C^{-1} * unscaled_upper - for (i_t j = 0; j < n; ++j) { - scaled.lower[j] *= column_scaling[j]; - scaled.upper[j] *= column_scaling[j]; + scaled.objective[j] *= col_scale[j]; + if (scaled.lower[j] > -1e20) scaled.lower[j] /= col_scale[j]; + if (scaled.upper[j] < 1e20) scaled.upper[j] /= col_scale[j]; } for (i_t i = 0; i < unscaled.Q.n; ++i) { const i_t row_start = unscaled.Q.row_start[i]; const i_t row_end = unscaled.Q.row_start[i + 1]; - i_t row = i; for (i_t p = row_start; p < row_end; ++p) { - i_t col = unscaled.Q.j[p]; - scaled.Q.x[p] = unscaled.Q.x[p] / (column_scaling[row] * column_scaling[col]); + const i_t col = unscaled.Q.j[p]; + scaled.Q.x[p] = unscaled.Q.x[p] * col_scale[i] * col_scale[col]; } } return 0; } template -void unscale_solution(const std::vector& column_scaling, +void unscale_solution(const std::vector& col_scale, + const std::vector& row_scale, const std::vector& scaled_x, + const std::vector& scaled_y, const std::vector& scaled_z, std::vector& unscaled_x, + std::vector& unscaled_y, std::vector& unscaled_z) { const i_t n = scaled_x.size(); unscaled_x.resize(n); unscaled_z.resize(n); for (i_t j = 0; j < n; ++j) { - unscaled_x[j] = scaled_x[j] / column_scaling[j]; - unscaled_z[j] = scaled_z[j] * column_scaling[j]; + // column_scaling multiplies A(:,j) and c_j by col_scale[j], divides bounds: + // x_orig = col_scale .* x_scaled, z_orig = z_scaled / col_scale. + unscaled_x[j] = scaled_x[j] * col_scale[j]; + unscaled_z[j] = scaled_z[j] / col_scale[j]; + } + + const i_t m = scaled_y.size(); + unscaled_y.resize(m); + for (i_t i = 0; i < m; ++i) { + // y_unscaled = R * y_scaled (row scaling: constraint was scaled by R) + unscaled_y[i] = scaled_y[i] * row_scale[i]; } } @@ -98,12 +257,16 @@ void unscale_solution(const std::vector& column_scaling, template int column_scaling(const lp_problem_t& unscaled, const simplex_solver_settings_t& settings, lp_problem_t& scaled, - std::vector& column_scaling); + std::vector& col_scale, + std::vector& row_scale); -template void unscale_solution(const std::vector& column_scaling, +template void unscale_solution(const std::vector& col_scale, + const std::vector& row_scale, const std::vector& scaled_x, + const std::vector& scaled_y, const std::vector& scaled_z, std::vector& unscaled_x, + std::vector& unscaled_y, std::vector& unscaled_z); #endif diff --git a/cpp/src/dual_simplex/scaling.hpp b/cpp/src/dual_simplex/scaling.hpp index 120660c765..94aca2055d 100644 --- a/cpp/src/dual_simplex/scaling.hpp +++ b/cpp/src/dual_simplex/scaling.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -20,13 +20,17 @@ template i_t column_scaling(const lp_problem_t& unscaled, const simplex_solver_settings_t& settings, lp_problem_t& scaled, - std::vector& column_scaling); + std::vector& column_scaling, + std::vector& row_scaling); template void unscale_solution(const std::vector& column_scaling, + const std::vector& row_scaling, const std::vector& scaled_x, + const std::vector& scaled_y, const std::vector& scaled_z, std::vector& unscaled_x, + std::vector& unscaled_y, std::vector& unscaled_z); } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 500822e1bd..35510c37b9 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -79,7 +79,7 @@ struct simplex_solver_settings_t { use_left_looking_lu(false), eliminate_singletons(true), print_presolve_stats(true), - barrier_presolve(false), + barrier_presolve(true), cudss_deterministic(false), deterministic(false), barrier(false), @@ -163,7 +163,7 @@ struct simplex_solver_settings_t { use_left_looking_lu; // true to use left looking LU factorization, false to use right looking bool eliminate_singletons; // true to eliminate singletons from the basis bool print_presolve_stats; // true to print presolve stats - bool barrier_presolve; // true to use barrier presolve + bool barrier_presolve; // bound shifts, free-var bounding; LP v-w split / folding when no cones bool cudss_deterministic; // true to use cuDSS deterministic mode, false for non-deterministic bool barrier; // true to use barrier method, false to use dual simplex method bool deterministic; // true to use B&B deterministic mode, false to use non-deterministic mode diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 9d8a62efc3..cf39c99e20 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -61,6 +61,42 @@ void write_matlab(const std::string& filename, const dual_simplex::lp_problem_t< fclose(fid); } +template +bool validate_barrier_cone_layout(const lp_problem_t& problem, + const simplex_solver_settings_t& settings) +{ + if (problem.second_order_cone_dims.empty()) { return true; } + + i_t cone_end = problem.cone_var_start; + for (auto q_k : problem.second_order_cone_dims) { + if (q_k <= 1) { + settings.log.printf( + "Error: second-order cone dimensions must be at least 2; use linear variables instead of " + "Q^1\n"); + return false; + } + cone_end += q_k; + } + + if (cone_end != problem.num_cols) { + settings.log.printf("Error: conic variables must form a trailing block [linear | cone]\n"); + return false; + } + + for (i_t j = problem.cone_var_start; j < cone_end; ++j) { + if (problem.lower[j] != 0.0 && problem.lower[j] > -inf) { + settings.log.printf("Error: explicit lower bound on conic variable %d is not supported\n", j); + return false; + } + if (problem.upper[j] < inf) { + settings.log.printf("Error: explicit upper bound on conic variable %d is not supported\n", j); + return false; + } + } + + return true; +} + } // namespace template @@ -170,9 +206,10 @@ lp_status_t solve_linear_program_with_advanced_basis( presolved_lp.num_cols, presolved_lp.A.col_start[presolved_lp.num_cols]); std::vector column_scales; + std::vector row_scales_simplex; { raft::common::nvtx::range scope_scaling("DualSimplex::scaling"); - column_scaling(presolved_lp, settings, lp, column_scales); + column_scaling(presolved_lp, settings, lp, column_scales, row_scales_simplex); } assert(presolved_lp.num_cols == lp.num_cols); lp_problem_t phase1_problem(original_lp.handle_ptr, 1, 1, 1); @@ -293,13 +330,21 @@ lp_status_t solve_linear_program_with_advanced_basis( } if (status == dual::status_t::OPTIMAL) { std::vector unscaled_x(lp.num_cols); + std::vector unscaled_y(lp.num_rows); std::vector unscaled_z(lp.num_cols); - unscale_solution(column_scales, solution.x, solution.z, unscaled_x, unscaled_z); + unscale_solution(column_scales, + row_scales_simplex, + solution.x, + solution.y, + solution.z, + unscaled_x, + unscaled_y, + unscaled_z); uncrush_solution(presolve_info, settings, original_lp, unscaled_x, - solution.y, + unscaled_y, unscaled_z, original_solution.x, original_solution.y, @@ -349,16 +394,18 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us // Convert the user problem to a linear program with only equality constraints std::vector new_slacks; - simplex_solver_settings_t barrier_settings = settings; - barrier_settings.barrier_presolve = true; dualize_info_t dualize_info; - convert_user_problem(user_problem, barrier_settings, original_lp, new_slacks, dualize_info); + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + if (!validate_barrier_cone_layout(original_lp, settings)) { + return lp_status_t::NUMERICAL_ISSUES; + } + lp_solution_t lp_solution(original_lp.num_rows, original_lp.num_cols); // Presolve the linear program presolve_info_t presolve_info; lp_problem_t presolved_lp(user_problem.handle_ptr, 1, 1, 1); - const i_t ok = presolve(original_lp, barrier_settings, presolved_lp, presolve_info); + const i_t ok = presolve(original_lp, settings, presolved_lp, presolve_info); if (ok == CONCURRENT_HALT_RETURN) { return lp_status_t::CONCURRENT_LIMIT; } if (ok == TIME_LIMIT_RETURN) { return lp_status_t::TIME_LIMIT; } if (ok == -1) { return lp_status_t::INFEASIBLE; } @@ -369,12 +416,13 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us presolved_lp.num_cols, presolved_lp.A.col_start[presolved_lp.num_cols]); std::vector column_scales; - column_scaling(presolved_lp, barrier_settings, barrier_lp, column_scales); + std::vector row_scales; + column_scaling(presolved_lp, settings, barrier_lp, column_scales, row_scales); // Solve using barrier lp_solution_t barrier_solution(barrier_lp.num_rows, barrier_lp.num_cols); - barrier_solver_t barrier_solver(barrier_lp, presolve_info, barrier_settings); + barrier_solver_t barrier_solver(barrier_lp, presolve_info, settings); lp_status_t barrier_status = barrier_solver.solve(start_time, barrier_solution); if (barrier_status == lp_status_t::OPTIMAL) { #ifdef COMPUTE_SCALED_RESIDUALS @@ -394,9 +442,16 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us #endif // Unscale the solution std::vector unscaled_x(barrier_lp.num_cols); + std::vector unscaled_y(barrier_lp.num_rows); std::vector unscaled_z(barrier_lp.num_cols); - unscale_solution( - column_scales, barrier_solution.x, barrier_solution.z, unscaled_x, unscaled_z); + unscale_solution(column_scales, + row_scales, + barrier_solution.x, + barrier_solution.y, + barrier_solution.z, + unscaled_x, + unscaled_y, + unscaled_z); std::vector residual = presolved_lp.rhs; matrix_vector_multiply(presolved_lp.A, 1.0, unscaled_x, -1.0, residual); @@ -410,7 +465,7 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us unscaled_dual_residual[j] -= presolved_lp.objective[j]; } matrix_transpose_vector_multiply( - presolved_lp.A, 1.0, barrier_solution.y, 1.0, unscaled_dual_residual); + presolved_lp.A, 1.0, unscaled_y, 1.0, unscaled_dual_residual); f_t unscaled_dual_residual_norm = vector_norm_inf(unscaled_dual_residual); settings.log.printf( "Unscaled Dual infeasibility (abs/rel): %.2e/%.2e\n", @@ -420,10 +475,10 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us // Undo presolve uncrush_solution(presolve_info, - barrier_settings, + settings, original_lp, unscaled_x, - barrier_solution.y, + unscaled_y, unscaled_z, lp_solution.x, lp_solution.y, @@ -564,7 +619,8 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us uncrush_primal_solution(user_problem, original_lp, lp_solution.x, solution.x); uncrush_dual_solution( user_problem, original_lp, lp_solution.y, lp_solution.z, solution.y, solution.z); - solution.objective = barrier_solution.objective; + solution.objective = + barrier_solution.user_objective / user_problem.obj_scale - user_problem.obj_constant; solution.user_objective = barrier_solution.user_objective; solution.l2_primal_residual = barrier_solution.l2_primal_residual; solution.l2_dual_residual = barrier_solution.l2_dual_residual; @@ -641,7 +697,7 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us lp_solution_t crossover_solution(original_lp.num_rows, original_lp.num_cols); std::vector vstatus(original_lp.num_cols); crossover_status_t crossover_status = crossover( - original_lp, barrier_settings, lp_solution, start_time, crossover_solution, vstatus); + original_lp, settings, lp_solution, start_time, crossover_solution, vstatus); settings.log.printf("Crossover status: %d\n", crossover_status); if (crossover_status == crossover_status_t::OPTIMAL) { barrier_status = lp_status_t::OPTIMAL; } } diff --git a/cpp/src/dual_simplex/user_problem.hpp b/cpp/src/dual_simplex/user_problem.hpp index 73c4c391be..8b0588064c 100644 --- a/cpp/src/dual_simplex/user_problem.hpp +++ b/cpp/src/dual_simplex/user_problem.hpp @@ -52,6 +52,8 @@ struct user_problem_t { std::vector Q_offsets; std::vector Q_indices; std::vector Q_values; + i_t cone_var_start{0}; + std::vector second_order_cone_dims; }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/vector_math.cuh b/cpp/src/dual_simplex/vector_math.cuh index abc7263858..32c75ea366 100644 --- a/cpp/src/dual_simplex/vector_math.cuh +++ b/cpp/src/dual_simplex/vector_math.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -9,6 +9,10 @@ #include +#include +#include +#include + #include #include @@ -28,6 +32,7 @@ struct norm_inf_max { template f_t device_custom_vector_norm_inf(InputIteratorT in, i_t size, rmm::cuda_stream_view stream_view) { + if (size == 0) { return 0; } // FIXME: Tmp storage stored in vector_math class. auto d_out = rmm::device_scalar(stream_view); rmm::device_uvector d_temp_storage(0, stream_view); @@ -62,6 +67,12 @@ f_t device_vector_norm_inf(const rmm::device_uvector& in, rmm::cuda_stream_ return device_custom_vector_norm_inf(in.data(), in.size(), stream_view); } +template +f_t device_vector_norm_inf(raft::device_span in, rmm::cuda_stream_view stream_view) +{ + return device_custom_vector_norm_inf(in.data(), in.size(), stream_view); +} + // TMP we should just have a CPU and GPU version to do the comparison // Should never have to norm inf a CPU vector if we are using the GPU template @@ -71,4 +82,12 @@ f_t vector_norm_inf(const std::vector& x, rmm::cuda_stream_view return device_vector_norm_inf(d_x, stream_view); } +template +f_t vector_norm_inf(raft::host_span x, rmm::cuda_stream_view stream_view) +{ + rmm::device_uvector d_x(x.size(), stream_view); + raft::copy(d_x.data(), x.data(), x.size(), stream_view); + return device_vector_norm_inf(d_x, stream_view); +} + } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/io/lp_parser.cpp b/cpp/src/io/lp_parser.cpp index 84daa101b4..81ec63f323 100644 --- a/cpp/src/io/lp_parser.cpp +++ b/cpp/src/io/lp_parser.cpp @@ -118,41 +118,42 @@ bool is_free_keyword(std::string_view lower) { return lower == "free"; } bool is_infinity_text(std::string_view lower) { return lower == "inf" || lower == "infinity"; } -// Builds the symmetric Q in CSR from LP-format raw upper-triangular triples. +// Builds the symmetric Q in COO from LP-format raw upper-triangular triples. // Each input triple (i, j, c) with i <= j represents `c * x_i * x_j` in the // LP source. The output Q satisfies x^T Q x = sum of those terms. // Diagonal (i == j): Q[i,i] = c (one entry). // Off-diagonal (i != j): Q[i,j] = Q[j,i] = c/2 (two entries; symmetric split). template -void build_symmetric_q_csr(const std::vector>& raw_triples, +void build_symmetric_q_coo(const std::vector>& raw_triples, i_t n_vars, - std::vector& out_values, - std::vector& out_indices, - std::vector& out_offsets) + std::vector& out_row_indices, + std::vector& out_col_indices, + std::vector& out_values) { - std::vector>> row_data(n_vars); + std::vector>> row_data(static_cast(n_vars)); for (const auto& [i, j, c] : raw_triples) { if (i == j) { - row_data[i].emplace_back(i, c); + row_data[static_cast(i)].emplace_back(i, c); } else { - row_data[i].emplace_back(j, c / f_t(2)); - row_data[j].emplace_back(i, c / f_t(2)); + row_data[static_cast(i)].emplace_back(j, c / f_t(2)); + row_data[static_cast(j)].emplace_back(i, c / f_t(2)); } } for (auto& row : row_data) { std::sort(row.begin(), row.end()); } - out_offsets.clear(); - out_indices.clear(); + out_row_indices.clear(); + out_col_indices.clear(); out_values.clear(); - out_offsets.reserve(static_cast(n_vars) + 1); - out_offsets.push_back(0); + out_row_indices.reserve(raw_triples.size() * 2); + out_col_indices.reserve(raw_triples.size() * 2); + out_values.reserve(raw_triples.size() * 2); for (i_t r = 0; r < n_vars; ++r) { - for (const auto& [col, val] : row_data[r]) { + for (const auto& [col, val] : row_data[static_cast(r)]) { + out_row_indices.push_back(r); + out_col_indices.push_back(col); out_values.push_back(val); - out_indices.push_back(col); } - out_offsets.push_back(static_cast(out_values.size())); } } @@ -1507,10 +1508,10 @@ void flush_quadratic_constraints(mps_data_model_t& problem, const i_t linear_row_count = static_cast(parser.row_names.size()); i_t k = 0; for (const auto& block : parser.quadratic_constraint_blocks) { + std::vector q_row_indices; + std::vector q_col_indices; std::vector q_values; - std::vector q_indices; - std::vector q_offsets; - build_symmetric_q_csr(block.quad_triples, n_vars, q_values, q_indices, q_offsets); + build_symmetric_q_coo(block.quad_triples, n_vars, q_row_indices, q_col_indices, q_values); problem.append_quadratic_constraint(linear_row_count + k, block.row_name, static_cast(block.row_type), @@ -1518,8 +1519,8 @@ void flush_quadratic_constraints(mps_data_model_t& problem, block.linear_indices, block.rhs_value, q_values, - q_indices, - q_offsets); + q_row_indices, + q_col_indices); ++k; } } diff --git a/cpp/src/io/mps_data_model.cpp b/cpp/src/io/mps_data_model.cpp index 7ae359e450..0172918c62 100644 --- a/cpp/src/io/mps_data_model.cpp +++ b/cpp/src/io/mps_data_model.cpp @@ -9,6 +9,7 @@ #include #include +#include #include namespace cuopt::linear_programming::io { @@ -139,32 +140,51 @@ void mps_data_model_t::set_quadratic_objective_matrix(std::span -void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_index, - const std::string& constraint_row_name, - char constraint_row_type, - std::span linear_values, - std::span linear_indices, - f_t rhs_value, - std::span quadratic_values, - std::span quadratic_indices, - std::span quadratic_offsets) +void mps_data_model_t::append_quadratic_constraint( + i_t constraint_row_index, + const std::string& constraint_row_name, + char constraint_row_type, + std::span linear_values, + std::span linear_indices, + f_t rhs_value, + std::span quadratic_values, + std::span quadratic_row_indices, + std::span quadratic_col_indices) { mps_parser_expects(constraint_row_index >= 0, error_type_t::ValidationError, "constraint_row_index must be non-negative"); - mps_parser_expects(constraint_row_type == 'L', + mps_parser_expects(constraint_row_type == 'L' || constraint_row_type == 'G', error_type_t::ValidationError, - "Quadratic constraint ROWS type must be 'L' (less-or-equal); got '%c'. " - "Only 'L' is supported for convex quadratic constraints.", + "Quadratic constraint ROWS type must be 'L' (<=) or 'G' (>=); got '%c'.", constraint_row_type); mps_parser_expects(linear_values.size() == linear_indices.size(), error_type_t::ValidationError, "linear_values and linear_indices must have the same nnz count"); - mps_parser_expects( - !quadratic_offsets.empty(), error_type_t::ValidationError, "quadratic_offsets cannot be empty"); + const size_t q_nnz = quadratic_values.size(); + mps_parser_expects(q_nnz == quadratic_row_indices.size(), + error_type_t::ValidationError, + "quadratic_values and quadratic_row_indices must have the same length"); + mps_parser_expects(q_nnz == quadratic_col_indices.size(), + error_type_t::ValidationError, + "quadratic_values and quadratic_col_indices must have the same length"); + + if (!linear_values.empty()) { + mps_parser_expects(linear_values.data() != nullptr && linear_indices.data() != nullptr, + error_type_t::ValidationError, + "linear_values and linear_indices cannot be null when non-empty"); + } + + if (q_nnz > 0) { + mps_parser_expects(quadratic_values.data() != nullptr && + quadratic_row_indices.data() != nullptr && + quadratic_col_indices.data() != nullptr, + error_type_t::ValidationError, + "Q COO spans cannot be null when nnz > 0"); + } quadratic_constraint_t qc; qc.constraint_row_index = constraint_row_index; @@ -173,9 +193,33 @@ void mps_data_model_t::append_quadratic_constraint(i_t constraint_row_ qc.rhs_value = rhs_value; qc.linear_values.assign(linear_values.begin(), linear_values.end()); qc.linear_indices.assign(linear_indices.begin(), linear_indices.end()); - qc.quadratic_values.assign(quadratic_values.begin(), quadratic_values.end()); - qc.quadratic_indices.assign(quadratic_indices.begin(), quadratic_indices.end()); - qc.quadratic_offsets.assign(quadratic_offsets.begin(), quadratic_offsets.end()); + + if (q_nnz == 0) { + qc.quadratic_row_indices.clear(); + qc.quadratic_col_indices.clear(); + qc.quadratic_values.clear(); + } else { + std::vector wr(quadratic_row_indices.begin(), quadratic_row_indices.end()); + std::vector wc(quadratic_col_indices.begin(), quadratic_col_indices.end()); + std::vector wv(quadratic_values.begin(), quadratic_values.end()); + + std::vector perm(q_nnz); + std::iota(perm.begin(), perm.end(), size_t{0}); + std::stable_sort(perm.begin(), perm.end(), [&](size_t a, size_t b) { + if (wr[a] != wr[b]) { return wr[a] < wr[b]; } + return wc[a] < wc[b]; + }); + + qc.quadratic_row_indices.resize(q_nnz); + qc.quadratic_col_indices.resize(q_nnz); + qc.quadratic_values.resize(q_nnz); + for (size_t t = 0; t < q_nnz; ++t) { + const size_t ix = perm[t]; + qc.quadratic_row_indices[t] = wr[ix]; + qc.quadratic_col_indices[t] = wc[ix]; + qc.quadratic_values[t] = wv[ix]; + } + } quadratic_constraints_.push_back(std::move(qc)); } diff --git a/cpp/src/io/mps_parser.cpp b/cpp/src/io/mps_parser.cpp index 535938b09c..20a47eb0a1 100644 --- a/cpp/src/io/mps_parser.cpp +++ b/cpp/src/io/mps_parser.cpp @@ -8,6 +8,7 @@ #include #include + #include #include @@ -17,14 +18,17 @@ #include #include #include +#include +#include #include #include +#include #include -#include namespace { using cuopt::linear_programming::io::error_type_t; using cuopt::linear_programming::io::mps_parser_expects; +using cuopt::linear_programming::io::mps_parser_expects_fatal; std::vector string_to_buffer(std::string_view input) { @@ -34,6 +38,229 @@ std::vector string_to_buffer(std::string_view input) } } // end namespace +namespace { + +/** @brief ceil(log2(x)) for x >= 1; used for sparse-vs-dense CSR heuristic only. */ +inline size_t ceil_log2_size_t(size_t x) +{ + size_t b = 0; + size_t y = (x <= 1) ? 0 : (x - 1); + while (y > 0) { + y >>= 1; + ++b; + } + return (b == 0) ? 1 : b; +} + +/** + * @brief Reusable scratch for converting (row,col,value) triples to CSR via CSC (double transpose). + * + * Avoids `std::vector>` per column/row, which is allocation-heavy for large Q + * blocks (e.g. many QCMATRIX sections in portfolio models). + */ +template +struct double_transpose_scratch_t { + std::vector col_nnz{}; + std::vector col_off{}; + std::vector col_wr{}; + std::vector csc_rows{}; + std::vector csc_vals{}; + std::vector row_nnz{}; + std::vector row_off{}; + std::vector row_wr{}; + /** Indices 0..nnz-1 for sort-by-(row,col) sparse CSR path (no symmetrization). */ + std::vector sort_perm{}; +}; + +/** + * @brief CSR from triples without full O(num_cols) column sweeps: count per row, sort by (row,col), + * scatter. Same numeric result ordering as the dense double-transpose path for non-symmetrized + * inputs when column order within a row is the stable sort order for equal keys (see std::sort). + * + * Used for QCMATRIX / QMATRIX when nnz is small enough vs matrix dimension that sorting beats + * scanning every column index twice. + */ +template +void triples_to_csr_sparse_sorted_flat(const std::vector>& entries, + i_t num_rows, + i_t num_cols, + f_t value_scale, + double_transpose_scratch_t& scratch, + std::vector& out_values, + std::vector& out_indices, + std::vector& out_offsets) +{ + static_cast(num_cols); // square Q paths pass num_cols == num_rows + const size_t nnz = entries.size(); + const size_t nr = static_cast(num_rows); + + scratch.row_nnz.assign(nr, 0); + for (const auto& e : entries) { + ++scratch.row_nnz[static_cast(std::get<0>(e))]; + } + + scratch.row_off.resize(nr + 1); + scratch.row_off[0] = 0; + for (size_t r = 0; r < nr; ++r) { + scratch.row_off[r + 1] = scratch.row_off[r] + scratch.row_nnz[r]; + } + const size_t csr_nnz = static_cast(scratch.row_off[nr]); + + out_values.resize(csr_nnz); + out_indices.resize(csr_nnz); + + scratch.sort_perm.resize(nnz); + std::iota(scratch.sort_perm.begin(), scratch.sort_perm.end(), size_t{0}); + std::stable_sort(scratch.sort_perm.begin(), scratch.sort_perm.end(), [&](size_t a, size_t b) { + const auto& ta = entries[a]; + const auto& tb = entries[b]; + const i_t ra = std::get<0>(ta); + const i_t rb = std::get<0>(tb); + if (ra != rb) { return ra < rb; } + return std::get<1>(ta) < std::get<1>(tb); + }); + + scratch.row_wr.resize(nr); + std::copy(scratch.row_off.begin(), scratch.row_off.begin() + nr, scratch.row_wr.begin()); + + for (size_t k = 0; k < nnz; ++k) { + const size_t ix = scratch.sort_perm[k]; + const auto& e = entries[ix]; + const i_t row = std::get<0>(e); + const i_t col = std::get<1>(e); + const f_t val = std::get<2>(e); + const size_t sr = static_cast(row); + const i_t w = scratch.row_wr[sr]++; + out_indices[static_cast(w)] = col; + out_values[static_cast(w)] = val * value_scale; + } + + out_offsets = std::move(scratch.row_off); +} + +/** + * @brief Build CSR from coordinate triples using the same double-transpose semantics as the + * previous nested-vector implementation: CSC column-major buckets, then CSR with columns sorted + * within each row (column index ascending). + * + * @param symmetrize_upper_triangular If true (QUADOBJ), each off-diagonal (r,c) also adds (c,r). + */ +template +void triples_to_csr_double_transpose_flat(const std::vector>& entries, + i_t num_rows, + i_t num_cols, + bool symmetrize_upper_triangular, + f_t value_scale, + double_transpose_scratch_t& scratch, + std::vector& out_values, + std::vector& out_indices, + std::vector& out_offsets) +{ + if (entries.empty()) { + out_values.clear(); + out_indices.clear(); + out_offsets.assign(static_cast(num_rows) + 1, 0); + return; + } + + const size_t nc = static_cast(num_cols); + const size_t nr = static_cast(num_rows); + const size_t nnz = entries.size(); + const size_t n_dim = std::max(nr, nc); + + // Without symmetrization (QCMATRIX, QMATRIX), avoid O(n) full-column sweeps when blocks are + // sparse: sort triples by (row,col) and scatter in one pass. QUADOBJ keeps the dense transpose + // path because symmetrization duplicates off-diagonal entries. + if (!symmetrize_upper_triangular) { + const size_t lg = ceil_log2_size_t(nnz); + // Roughly: sort cost ~ nnz·lg vs ~O(n) column iterations in dense path; 48 is a tunable fudge. + constexpr size_t k_sparse_vs_dense_heuristic = 96; + if (nnz * lg < k_sparse_vs_dense_heuristic * n_dim) { + triples_to_csr_sparse_sorted_flat( + entries, num_rows, num_cols, value_scale, scratch, out_values, out_indices, out_offsets); + return; + } + } + + scratch.col_nnz.assign(nc, 0); + for (const auto& entry : entries) { + const i_t r = std::get<0>(entry); + const i_t c = std::get<1>(entry); + scratch.col_nnz[static_cast(c)]++; + if (symmetrize_upper_triangular && r != c) { scratch.col_nnz[static_cast(r)]++; } + } + + scratch.col_off.resize(nc + 1); + scratch.col_off[0] = 0; + for (size_t c = 0; c < nc; ++c) { + scratch.col_off[c + 1] = scratch.col_off[c] + scratch.col_nnz[c]; + } + const i_t csc_nnz = scratch.col_off[nc]; + scratch.csc_rows.resize(static_cast(csc_nnz)); + scratch.csc_vals.resize(static_cast(csc_nnz)); + scratch.col_wr.resize(nc); + std::copy(scratch.col_off.begin(), scratch.col_off.begin() + nc, scratch.col_wr.begin()); + + for (const auto& entry : entries) { + const i_t r = std::get<0>(entry); + const i_t c = std::get<1>(entry); + const f_t v = std::get<2>(entry); + { + const size_t sc = static_cast(c); + const i_t p = scratch.col_wr[sc]++; + scratch.csc_rows[static_cast(p)] = r; + scratch.csc_vals[static_cast(p)] = v; + } + if (symmetrize_upper_triangular && r != c) { + const size_t sr = static_cast(r); + const i_t p = scratch.col_wr[sr]++; + scratch.csc_rows[static_cast(p)] = c; + scratch.csc_vals[static_cast(p)] = v; + } + } + + scratch.row_nnz.assign(nr, 0); + for (i_t cc = 0; cc < num_cols; ++cc) { + const size_t col_z = static_cast(cc); + const i_t lo = scratch.col_off[col_z]; + const i_t hi = scratch.col_off[col_z + 1]; + for (i_t t = lo; t < hi; ++t) { + const i_t row = scratch.csc_rows[static_cast(t)]; + scratch.row_nnz[static_cast(row)]++; + } + } + + scratch.row_off.resize(nr + 1); + scratch.row_off[0] = 0; + for (size_t r = 0; r < nr; ++r) { + scratch.row_off[r + 1] = scratch.row_off[r] + scratch.row_nnz[r]; + } + const size_t csr_nnz = static_cast(scratch.row_off[nr]); + + out_values.resize(csr_nnz); + out_indices.resize(csr_nnz); + scratch.row_wr.resize(nr); + std::copy(scratch.row_off.begin(), scratch.row_off.begin() + nr, scratch.row_wr.begin()); + + for (i_t cc = 0; cc < num_cols; ++cc) { + const size_t col_z = static_cast(cc); + const i_t lo = scratch.col_off[col_z]; + const i_t hi = scratch.col_off[col_z + 1]; + for (i_t t = lo; t < hi; ++t) { + const i_t row = scratch.csc_rows[static_cast(t)]; + const f_t val = scratch.csc_vals[static_cast(t)]; + const size_t sr = static_cast(row); + const i_t w = scratch.row_wr[sr]++; + out_indices[static_cast(w)] = cc; + out_values[static_cast(w)] = val * value_scale; + } + } + + out_offsets = std::move(scratch.row_off); +} + +} // namespace + namespace cuopt::linear_programming::io { template @@ -74,7 +301,7 @@ BoundType convert(std::string_view str) return LowerBoundIntegerVariable; } else if (str == "UI") { return UpperBoundIntegerVariable; - } else if (str == "SC") { + } else if (str == "SC" || str == "LC") { return SemiContinuousVariable; } else { mps_parser_expects(false, @@ -288,115 +515,72 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) problem.set_variable_types(std::move(var_types)); problem.set_maximize(maximize); - // Helper function to build CSR format using double transpose (O(m+n+nnz) instead of - // O(nnz*log(nnz))) For QUADOBJ: handles upper triangular input by expanding to full symmetric - // matrix. + // Build CSR from (row,col,value) triples via double transpose (CSC then CSR). Uses flat buffers + // plus reusable scratch to avoid per-column/per-row `std::vector` allocations — important for + // large Q blocks and many QCMATRIX sections. // - // @p value_scale: - // QUADOBJ/QMATRIX use 0.5 (MPS ½ xᵀQx vs internal xᵀQx); - // QCMATRIX uses 1.0 (symmetric Q defines xᵀQx directly in the constraint). - auto build_csr_via_transpose = [](const std::vector>& entries, - i_t num_rows, - i_t num_cols, - bool symmetrize_upper_triangular, - f_t value_scale) { - struct CSRResult { - std::vector values; - std::vector indices; - std::vector offsets; - }; - - if (entries.empty()) { - CSRResult result; - result.offsets.resize(num_rows + 1, 0); - return result; - } - - // First transpose: build CSC format (entries sorted by column) - std::vector>> csc_data(num_cols); - for (const auto& entry : entries) { - i_t row = std::get<0>(entry); - i_t col = std::get<1>(entry); - f_t val = std::get<2>(entry); - - // For QUADOBJ (upper triangular), add both (row,col) and (col,row) if off-diagonal - csc_data[col].emplace_back(row, val); - if (symmetrize_upper_triangular && row != col) { csc_data[row].emplace_back(col, val); } - } - - // Second transpose: convert CSC to CSR (entries sorted by row, columns within rows sorted) - std::vector>> csr_data(num_rows); - for (i_t col = 0; col < num_cols; ++col) { - for (const auto& [row, val] : csc_data[col]) { - csr_data[row].emplace_back(col, val); - } - } - - // Build final CSR format - CSRResult result; - result.offsets.reserve(num_rows + 1); - result.offsets.push_back(0); - - for (i_t row = 0; row < num_rows; ++row) { - for (const auto& [col, val] : csr_data[row]) { - // While the mps format expects to optimize for 0.5 xT Q x, cuopt optimizes for xT Q xExpand - // commentComment on line L488 so we have to multiply the value by value_scale=0.5 to get - // the correct value. - result.values.push_back(val * value_scale); - result.indices.push_back(col); - } - result.offsets.push_back(result.values.size()); - } - - return result; - }; + // value_scale: QUADOBJ/QMATRIX use 0.5 (MPS ½ xᵀQx vs internal xᵀQx); QCMATRIX uses 1.0. + double_transpose_scratch_t triple_csr_scratch{}; + std::vector quad_csr_values{}; + std::vector quad_csr_indices{}; + std::vector quad_csr_offsets{}; // Process QUADOBJ data if present (upper triangular format) if (!quadobj_entries.empty()) { - // Convert quadratic objective entries to CSR format using double transpose - // QUADOBJ stores upper triangular elements, so we expand to full symmetric matrix constexpr f_t k_mps_quad_half_scale = f_t(0.5); // MPS ½ xᵀQx vs internal xᵀQx - auto csr_result = build_csr_via_transpose( - quadobj_entries, num_vars_for_quad, num_vars_for_quad, true, k_mps_quad_half_scale); - - // Use optimized double transpose method - O(m+n+nnz) instead of O(nnz*log(nnz)) - problem.set_quadratic_objective_matrix( - csr_result.values, csr_result.indices, csr_result.offsets); + triples_to_csr_double_transpose_flat(quadobj_entries, + num_vars_for_quad, + num_vars_for_quad, + true, + k_mps_quad_half_scale, + triple_csr_scratch, + quad_csr_values, + quad_csr_indices, + quad_csr_offsets); + problem.set_quadratic_objective_matrix(quad_csr_values, quad_csr_indices, quad_csr_offsets); } else if (!qmatrix_entries.empty()) { - // Convert quadratic objective entries to CSR format using double transpose - // QMATRIX stores full symmetric matrix constexpr f_t k_mps_quad_half_scale = f_t(0.5); - auto csr_result = build_csr_via_transpose( - qmatrix_entries, num_vars_for_quad, num_vars_for_quad, false, k_mps_quad_half_scale); - - // Use optimized double transpose method - O(m+n+nnz) instead of O(nnz*log(nnz)) - problem.set_quadratic_objective_matrix( - csr_result.values, csr_result.indices, csr_result.offsets); + triples_to_csr_double_transpose_flat(qmatrix_entries, + num_vars_for_quad, + num_vars_for_quad, + false, + k_mps_quad_half_scale, + triple_csr_scratch, + quad_csr_values, + quad_csr_indices, + quad_csr_offsets); + problem.set_quadratic_objective_matrix(quad_csr_values, quad_csr_indices, quad_csr_offsets); } // QCMATRIX: one symmetric Q per constraint row (no extra ½ factor vs file coeffs). - // Bundle row metadata, row-linear coefficients (from COLUMNS), rhs, and quadratic part together. + // Store Q in COO (row, col, value) per block — avoids O(n) CSR row-pointer materialization. constexpr f_t k_qcmatrix_value_scale = f_t(1); - const i_t linear_row_count = static_cast(row_types.size() - quadratic_row_ids.size()); - i_t quadratic_row_id = 0; for (const auto& block : qcmatrix_blocks_) { - auto csr_result = build_csr_via_transpose( - block.entries, num_vars_for_quad, num_vars_for_quad, false, k_qcmatrix_value_scale); const i_t row_id = block.constraint_row_id; mps_parser_expects(row_id >= 0 && row_id < static_cast(row_types.size()), error_type_t::ValidationError, "QCMATRIX row index %d is out of range for constraints", static_cast(row_id)); - problem.append_quadratic_constraint(linear_row_count + quadratic_row_id, - row_names[row_id], - static_cast(row_types[row_id]), - A_values[row_id], - A_indices[row_id], - b_values[row_id], - csr_result.values, - csr_result.indices, - csr_result.offsets); - ++quadratic_row_id; + const i_t nnz = static_cast(block.entries.size()); + std::vector qc_rows(static_cast(nnz)); + std::vector qc_cols(static_cast(nnz)); + std::vector qc_vals(static_cast(nnz)); + for (i_t e = 0; e < nnz; ++e) { + qc_rows[static_cast(e)] = std::get<0>(block.entries[static_cast(e)]); + qc_cols[static_cast(e)] = std::get<1>(block.entries[static_cast(e)]); + qc_vals[static_cast(e)] = + std::get<2>(block.entries[static_cast(e)]) * k_qcmatrix_value_scale; + } + problem.append_quadratic_constraint( + row_id, + row_names[row_id], + static_cast(row_types[row_id]), + std::span(A_values[row_id].data(), A_values[row_id].size()), + std::span(A_indices[row_id].data(), A_indices[row_id].size()), + b_values[row_id], + std::span(qc_vals.data(), qc_vals.size()), + std::span(qc_rows.data(), qc_rows.size()), + std::span(qc_cols.data(), qc_cols.size())); } if (!quadratic_row_ids.empty()) { @@ -694,9 +878,7 @@ mps_parser_t::mps_parser_t(mps_data_model_t& problem, // raft::common::nvtx::range fun_scope("mps parser"); std::vector buf = detail::file_to_string(file); - parse_string(buf.data()); - fill_problem(problem); } diff --git a/cpp/src/io/mps_writer.cpp b/cpp/src/io/mps_writer.cpp index 73489277ce..dae7d75a9a 100644 --- a/cpp/src/io/mps_writer.cpp +++ b/cpp/src/io/mps_writer.cpp @@ -157,7 +157,15 @@ void mps_writer_t::write(const std::string& mps_file_path) std::vector constraint_bounds(problem_.get_constraint_bounds().size()); std::vector variable_lower_bounds(problem_.get_variable_lower_bounds().size()); std::vector variable_upper_bounds(problem_.get_variable_upper_bounds().size()); - std::vector variable_types(problem_.get_variable_types().size()); + // Default unset variable types to continuous ('C'); API models often omit set_variable_types. + std::vector variable_types(static_cast(n_variables), 'C'); + { + const auto& src_types = problem_.get_variable_types(); + const size_t n_copy = std::min(src_types.size(), variable_types.size()); + for (size_t j = 0; j < n_copy; ++j) { + variable_types[j] = src_types[j]; + } + } std::vector row_types(problem_.get_row_types().size()); std::vector constraint_matrix_offsets(problem_.get_constraint_matrix_offsets().size()); std::vector constraint_matrix_indices(problem_.get_constraint_matrix_indices().size()); @@ -178,9 +186,6 @@ void mps_writer_t::write(const std::string& mps_file_path) problem_.get_variable_upper_bounds().data(), problem_.get_variable_upper_bounds().data() + problem_.get_variable_upper_bounds().size(), variable_upper_bounds.data()); - std::copy(problem_.get_variable_types().data(), - problem_.get_variable_types().data() + problem_.get_variable_types().size(), - variable_types.data()); std::copy(problem_.get_row_types().data(), problem_.get_row_types().data() + problem_.get_row_types().size(), row_types.data()); @@ -243,8 +248,8 @@ void mps_writer_t::write(const std::string& mps_file_path) const auto& qc = quadratic_constraints[q]; std::string row_name = qc.constraint_row_name.empty() ? "QC" + std::to_string(q) : qc.constraint_row_name; - // Quadratic rows are currently restricted to MPS 'L' (<=). - mps_file << " L " << row_name << "\n"; + char const type = qc.constraint_row_type; + mps_file << " " << type << " " << row_name << "\n"; } // COLUMNS section @@ -497,20 +502,19 @@ void mps_writer_t::write(const std::string& mps_file_path) if (problem_.has_quadratic_constraints()) { for (const auto& qc : problem_.get_quadratic_constraints()) { mps_file << "QCMATRIX " << qc.constraint_row_name << "\n"; - const i_t n_quad_rows = static_cast(qc.quadratic_offsets.size()) - 1; - for (i_t i = 0; i < n_quad_rows; ++i) { + const i_t nnz = static_cast(qc.quadratic_values.size()); + for (i_t p = 0; p < nnz; ++p) { + const i_t i = qc.quadratic_row_indices[static_cast(p)]; + const i_t j = qc.quadratic_col_indices[static_cast(p)]; + f_t v = qc.quadratic_values[static_cast(p)]; std::string row_var_name = static_cast(i) < problem_.get_variable_names().size() - ? problem_.get_variable_names()[i] + ? problem_.get_variable_names()[static_cast(i)] : "C" + std::to_string(i); - for (i_t p = qc.quadratic_offsets[i]; p < qc.quadratic_offsets[i + 1]; ++p) { - i_t j = qc.quadratic_indices[p]; - f_t v = qc.quadratic_values[p]; - std::string col_var_name = static_cast(j) < problem_.get_variable_names().size() - ? problem_.get_variable_names()[j] - : "C" + std::to_string(j); - if (v != f_t(0)) { - mps_file << " " << row_var_name << " " << col_var_name << " " << v << "\n"; - } + std::string col_var_name = static_cast(j) < problem_.get_variable_names().size() + ? problem_.get_variable_names()[static_cast(j)] + : "C" + std::to_string(j); + if (v != f_t(0)) { + mps_file << " " << row_var_name << " " << col_var_name << " " << v << "\n"; } } } diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 0b4c360b10..505b4e02f7 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -98,6 +98,9 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_INTEGRALITY_TOLERANCE, &mip_settings.tolerances.integrality_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-5)}, {CUOPT_MIP_ABSOLUTE_GAP, &mip_settings.tolerances.absolute_mip_gap, f_t(0.0), std::numeric_limits::infinity(), std::max(f_t(1e-10), std::numeric_limits::epsilon())}, {CUOPT_MIP_RELATIVE_GAP, &mip_settings.tolerances.relative_mip_gap, f_t(0.0), f_t(1e-1), f_t(1e-4)}, + {CUOPT_BARRIER_RELATIVE_FEASIBILITY_TOLERANCE, &pdlp_settings.barrier_relative_feasibility_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-8)}, + {CUOPT_BARRIER_RELATIVE_OPTIMALITY_TOLERANCE, &pdlp_settings.barrier_relative_optimality_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-8)}, + {CUOPT_BARRIER_RELATIVE_COMPLEMENTARITY_TOLERANCE, &pdlp_settings.barrier_relative_complementarity_tolerance, f_t(0.0), f_t(1e-1), f_t(1e-8)}, {CUOPT_PRIMAL_INFEASIBLE_TOLERANCE, &pdlp_settings.tolerances.primal_infeasible_tolerance, f_t(0.0), f_t(1e-1), std::max(f_t(1e-10), std::numeric_limits::epsilon())}, {CUOPT_DUAL_INFEASIBLE_TOLERANCE, &pdlp_settings.tolerances.dual_infeasible_tolerance, f_t(0.0), f_t(1e-1), std::max(f_t(1e-10), std::numeric_limits::epsilon())}, {CUOPT_MIP_CUT_CHANGE_THRESHOLD, &mip_settings.cut_change_threshold, f_t(-1.0), std::numeric_limits::infinity(), f_t(-1.0)}, diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 43d507b2f9..cf741eee0b 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -507,9 +507,14 @@ run_barrier(dual_simplex::user_problem_t& user_problem, barrier_settings.barrier = true; barrier_settings.crossover = settings.crossover; barrier_settings.eliminate_dense_columns = settings.eliminate_dense_columns; - barrier_settings.barrier_iterative_refinement = settings.barrier_iterative_refinement; - barrier_settings.barrier_step_scale = settings.barrier_step_scale; + barrier_settings.barrier_iterative_refinement = settings.barrier_iterative_refinement; + barrier_settings.barrier_step_scale = settings.barrier_step_scale; barrier_settings.cudss_deterministic = settings.cudss_deterministic; + barrier_settings.barrier_relative_feasibility_tol = + settings.barrier_relative_feasibility_tolerance; + barrier_settings.barrier_relative_optimality_tol = settings.barrier_relative_optimality_tolerance; + barrier_settings.barrier_relative_complementarity_tol = + settings.barrier_relative_complementarity_tolerance; barrier_settings.barrier_relaxed_feasibility_tol = settings.tolerances.relative_primal_tolerance; barrier_settings.barrier_relaxed_optimality_tol = settings.tolerances.relative_dual_tolerance; barrier_settings.barrier_relaxed_complementarity_tol = settings.tolerances.relative_gap_tolerance; @@ -1749,6 +1754,10 @@ optimization_problem_solution_t solve_qp(optimization_problem_t(op_problem.get_quadratic_constraints().size())); + } if (settings.user_problem_file != "") { CUOPT_LOG_INFO("Writing user problem to file: %s", settings.user_problem_file.c_str()); op_problem.write_to_mps(settings.user_problem_file); @@ -1803,6 +1812,24 @@ optimization_problem_solution_t solve_lp( // This needs to be called before pdlp is initialized init_handler(op_problem.get_handle_ptr()); + if (op_problem.has_quadratic_objective() || op_problem.has_quadratic_constraints()) { + if (op_problem.has_quadratic_objective()) { + CUOPT_LOG_INFO("Problem has a quadratic objective. Using Barrier."); + } + if (op_problem.has_quadratic_constraints()) { + CUOPT_LOG_INFO("Problem has %d quadratic constraints. Using Barrier with SOC conversion.", + static_cast(op_problem.get_quadratic_constraints().size())); + } + settings.method = method_t::Barrier; + settings.presolver = presolver_t::None; + // Quadratic objective support is minimization-only. + if (op_problem.has_quadratic_objective() && op_problem.get_sense()) { + CUOPT_LOG_ERROR("Quadratic problems must be minimized"); + return optimization_problem_solution_t(pdlp_termination_status_t::NumericalError, + op_problem.get_handle_ptr()->get_stream()); + } + } + raft::common::nvtx::range fun_scope("Running solver"); if (problem_checking) { diff --git a/cpp/src/pdlp/termination_strategy/infeasibility_information.cu b/cpp/src/pdlp/termination_strategy/infeasibility_information.cu index 9268e17910..96de3a788d 100644 --- a/cpp/src/pdlp/termination_strategy/infeasibility_information.cu +++ b/cpp/src/pdlp/termination_strategy/infeasibility_information.cu @@ -26,13 +26,7 @@ #include #include -#include -#include -#include #include -#include -#include -#include namespace cuopt::linear_programming::detail { template diff --git a/cpp/src/pdlp/translate.hpp b/cpp/src/pdlp/translate.hpp index 7d78da80eb..2bfaa127ed 100644 --- a/cpp/src/pdlp/translate.hpp +++ b/cpp/src/pdlp/translate.hpp @@ -12,10 +12,16 @@ #include #include +#include + +#include #include #include +#include +#include +#include namespace cuopt::linear_programming { @@ -111,8 +117,6 @@ static dual_simplex::user_problem_t cuopt_problem_to_user_problem( csr_A.j = std::vector(cuopt::host_copy(model.variables, handle_ptr->get_stream())); csr_A.row_start = std::vector(cuopt::host_copy(model.offsets, handle_ptr->get_stream())); - csr_A.to_compressed_col(user_problem.A); - user_problem.rhs.resize(m); user_problem.row_sense.resize(m); user_problem.range_rows.clear(); @@ -186,6 +190,13 @@ static dual_simplex::user_problem_t cuopt_problem_to_user_problem( user_problem.Q_indices = model.Q_indices; user_problem.Q_values = model.Q_values; + if (model.original_problem_ptr->has_quadratic_constraints()) { + detail::apply_soc_qcmatrix_conversion_for_simplex( + n, model.original_problem_ptr->get_quadratic_constraints(), csr_A, user_problem); + } + + csr_A.to_compressed_col(user_problem.A); + return user_problem; } @@ -211,7 +222,6 @@ static dual_simplex::user_problem_t cuopt_optimization_problem_to_user csr_A.row_start.resize(1); csr_A.row_start[0] = 0; } - csr_A.to_compressed_col(user_problem.A); user_problem.rhs.resize(m); user_problem.row_sense.resize(m); @@ -291,6 +301,13 @@ static dual_simplex::user_problem_t cuopt_optimization_problem_to_user user_problem.Q_indices = model.get_quadratic_objective_indices(); user_problem.Q_values = model.get_quadratic_objective_values(); + if (model.has_quadratic_constraints()) { + detail::apply_soc_qcmatrix_conversion_for_simplex( + static_cast(n), model.get_quadratic_constraints(), csr_A, user_problem); + } + + csr_A.to_compressed_col(user_problem.A); + return user_problem; } diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 3642044551..f768f46479 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -17,8 +17,9 @@ if(BUILD_TESTS) PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_SOURCE_DIR}" - "${dejavu_SOURCE_DIR}" ) + # Third-party headers: SYSTEM matches libcuopt and suppresses NVCC sign-conversion noise. + target_include_directories(cuopttestutils SYSTEM PRIVATE "${dejavu_SOURCE_DIR}") target_link_libraries(cuopttestutils PUBLIC @@ -48,6 +49,8 @@ function(ConfigureTest CMAKE_TEST_NAME) "${CUOPT_TEST_DIR}" "${papilo_SOURCE_DIR}/src" "${papilo_BINARY_DIR}" + ) + target_include_directories(${CMAKE_TEST_NAME} SYSTEM PRIVATE "${pslp_SOURCE_DIR}/include" "${dejavu_SOURCE_DIR}" ) diff --git a/cpp/tests/dual_simplex/CMakeLists.txt b/cpp/tests/dual_simplex/CMakeLists.txt index dc4ab35b73..f6dff93227 100644 --- a/cpp/tests/dual_simplex/CMakeLists.txt +++ b/cpp/tests/dual_simplex/CMakeLists.txt @@ -6,4 +6,5 @@ ConfigureTest(DUAL_SIMPLEX_TEST ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/solve.cpp ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/solve_barrier.cu - LABELS numopt) + ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/second_order_cone_kernels.cu +) diff --git a/cpp/tests/dual_simplex/unit_tests/second_order_cone_kernels.cu b/cpp/tests/dual_simplex/unit_tests/second_order_cone_kernels.cu new file mode 100644 index 0000000000..a29e840deb --- /dev/null +++ b/cpp/tests/dual_simplex/unit_tests/second_order_cone_kernels.cu @@ -0,0 +1,628 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include + +#include + +#include + +#include +#include +#include +#include + +namespace cuopt::linear_programming::dual_simplex::test { + +// Mirrors `cone_step_length_from_scalars` in second_order_cone_kernels.cuh (device). +double host_cone_step_length_from_scalars(double u0, + double du0, + double du_tail_sq, + double u_tail_du_tail, + double u_tail_sq, + double alpha_max) +{ + const auto a = du0 * du0 - du_tail_sq; + const auto b = u0 * du0 - u_tail_du_tail; + const auto c_raw = u0 * u0 - u_tail_sq; + const auto c = c_raw > 0.0 ? c_raw : 0.0; + const auto disc = b * b - a * c; + auto alpha = alpha_max; + + if (u0 >= 0.0 && du0 < 0.0) { alpha = std::min(alpha, -u0 / du0); } + + if ((a > 0.0 && b > 0.0) || disc < 0.0) { return alpha; } + + if (a == 0.0) { + return alpha; + } else if (c == 0.0) { + alpha = a >= 0.0 ? alpha : 0.0; + } else { + const auto t = -(b + std::copysign(std::sqrt(disc), b)); + auto r1 = c / t; + auto r2 = t / a; + if (r1 < 0.0) { r1 = alpha; } + if (r2 < 0.0) { r2 = alpha; } + alpha = std::min(alpha, std::min(r1, r2)); + } + + return alpha; +} + +TEST(second_order_cone_kernels, topology_and_scratch_layout) +{ + auto stream = rmm::cuda_stream_default; + + std::vector cone_dimensions{3, 2, 5}; + rmm::device_uvector x(10, stream); + rmm::device_uvector z(10, stream); + + cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); + + EXPECT_EQ(cones.n_cones, std::size_t{3}); + EXPECT_EQ(cones.n_cone_entries, std::size_t{10}); + EXPECT_EQ(cones.x.data(), x.data()); + EXPECT_EQ(cones.z.data(), z.data()); + + EXPECT_EQ(cuopt::host_copy(cones.cone_offsets, stream), (std::vector{0, 3, 5, 10})); + EXPECT_EQ(cuopt::host_copy(cones.cone_dimensions, stream), cone_dimensions); + EXPECT_EQ(cuopt::host_copy(cones.element_cone_ids, stream), + (std::vector{0, 0, 0, 1, 1, 2, 2, 2, 2, 2})); + EXPECT_EQ(cuopt::host_copy(cones.segmented_sum.small_cone_ids, stream), + (std::vector{0, 1, 2})); + EXPECT_TRUE(cuopt::host_copy(cones.segmented_sum.medium_cone_ids, stream).empty()); + EXPECT_TRUE(cones.segmented_sum.large_cone_ids.empty()); + EXPECT_TRUE(cones.segmented_sum.large_cone_offsets.empty()); + EXPECT_TRUE(cones.segmented_sum.large_cone_dimensions.empty()); + + EXPECT_EQ(cones.eta.size(), 3); + EXPECT_EQ(cones.w.size(), 10); + + auto& scratch = cones.scratch; + EXPECT_EQ(scratch.n_cones, cones.n_cones); + EXPECT_EQ(scratch.n_cone_entries, cones.n_cone_entries); + EXPECT_EQ(scratch.slots.size(), 3 * cone_dimensions.size()); + EXPECT_EQ(scratch.step_alpha_primal.size(), cone_dimensions.size()); + EXPECT_EQ(scratch.step_alpha_dual.size(), cone_dimensions.size()); + EXPECT_EQ(scratch.temp_cone.size(), x.size()); + + EXPECT_EQ(scratch.get_slot<0>().size(), cone_dimensions.size()); + EXPECT_EQ(scratch.get_slot<1>().data(), scratch.get_slot<0>().data() + cones.n_cones); + EXPECT_EQ(scratch.get_slot<2>().data(), scratch.get_slot<1>().data() + cones.n_cones); +} + +TEST(second_order_cone_kernels, segmented_sum_uses_all_cone_size_buckets) +{ + auto stream = rmm::cuda_stream_default; + + std::vector cone_dimensions{65, 3, 66, 32769}; + rmm::device_uvector x(32903, stream); + rmm::device_uvector z(32903, stream); + cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); + + EXPECT_EQ(cuopt::host_copy(cones.segmented_sum.small_cone_ids, stream), (std::vector{1})); + EXPECT_EQ(cuopt::host_copy(cones.segmented_sum.medium_cone_ids, stream), + (std::vector{0, 2})); + EXPECT_EQ(cones.segmented_sum.large_cone_ids, (std::vector{3})); + EXPECT_EQ(cones.segmented_sum.large_cone_offsets, (std::vector{134})); + EXPECT_EQ(cones.segmented_sum.large_cone_dimensions, (std::vector{32769})); + + std::vector values_host(cones.n_cone_entries, 1.0); + rmm::device_uvector values(values_host.size(), stream); + rmm::device_uvector sums(cone_dimensions.size(), stream); + raft::copy(values.data(), values_host.data(), values_host.size(), stream); + + EXPECT_GT(cones.segmented_sum.cub_workspace_bytes, 0); + const auto workspace_size = cones.segmented_sum.cub_workspace.size(); + EXPECT_GT(workspace_size, 0); + + cones.segmented_sum(values.data(), cuopt::make_span(sums), stream); + + EXPECT_EQ(cuopt::host_copy(sums, stream), (std::vector{65.0, 3.0, 66.0, 32769.0})); + EXPECT_EQ(cones.segmented_sum.cub_workspace.size(), workspace_size); +} + +TEST(second_order_cone_kernels, nt_scaling_matches_host_reference) +{ + auto stream = rmm::cuda_stream_default; + + std::vector cone_dimensions{3, 65, 32769}; + std::size_t n_cone_entries = 0; + for (const auto dim : cone_dimensions) { + n_cone_entries += static_cast(dim); + } + + std::vector x_host(n_cone_entries); + std::vector z_host(n_cone_entries); + std::size_t offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + x_host[offset] = 100.0 + static_cast(cone); + z_host[offset] = 80.0 + static_cast(cone); + for (int local_idx = 1; local_idx < dim; ++local_idx) { + x_host[offset + local_idx] = 0.001 * static_cast((local_idx % 5) + 1); + z_host[offset + local_idx] = 0.0015 * static_cast((local_idx % 7) + 1); + } + offset += static_cast(dim); + } + + auto x = cuopt::device_copy(x_host, stream); + auto z = cuopt::device_copy(z_host, stream); + cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); + const auto workspace_size = cones.segmented_sum.cub_workspace.size(); + EXPECT_GT(workspace_size, 0); + + launch_nt_scaling(cones, stream); + EXPECT_EQ(cones.segmented_sum.cub_workspace.size(), workspace_size); + + auto eta_host = cuopt::host_copy(cones.eta, stream); + auto w_host = cuopt::host_copy(cones.w, stream); + + std::vector expected_eta(cone_dimensions.size()); + std::vector expected_w(n_cone_entries); + + offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + + double x_tail_sq = 0.0; + double z_tail_sq = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + x_tail_sq += x_host[idx] * x_host[idx]; + z_tail_sq += z_host[idx] * z_host[idx]; + } + + const auto x_tail_norm = std::sqrt(x_tail_sq); + const auto z_tail_norm = std::sqrt(z_tail_sq); + const auto x_det = (x_host[offset] - x_tail_norm) * (x_host[offset] + x_tail_norm); + const auto z_det = (z_host[offset] - z_tail_norm) * (z_host[offset] + z_tail_norm); + ASSERT_GT(x_det, 0.0) << "cone " << cone; + ASSERT_GT(z_det, 0.0) << "cone " << cone; + + const auto x_scale = std::sqrt(x_det); + const auto z_scale = std::sqrt(z_det); + + expected_eta[cone] = std::sqrt(z_scale / x_scale); + + double normalized_xz_dot = 0.0; + for (int local_idx = 0; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + normalized_xz_dot += x_host[idx] * z_host[idx] / (x_scale * z_scale); + } + const auto w_det = 2.0 + 2.0 * normalized_xz_dot; + ASSERT_GT(w_det, 0.0) << "cone " << cone; + const auto w_scale = std::sqrt(w_det); + + expected_w[offset] = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + expected_w[idx] = (z_host[idx] / z_scale - x_host[idx] / x_scale) / w_scale; + } + + double normalized_tail_sq = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + normalized_tail_sq += expected_w[idx] * expected_w[idx]; + } + expected_w[offset] = std::sqrt(1.0 + normalized_tail_sq); + + offset += static_cast(dim); + } + + for (std::size_t i = 0; i < expected_eta.size(); ++i) { + EXPECT_NEAR(eta_host[i], expected_eta[i], 1e-10) << "cone " << i; + } + + for (std::size_t i = 0; i < expected_w.size(); ++i) { + EXPECT_NEAR(w_host[i], expected_w[i], 1e-10) << "entry " << i; + } + + offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + + double tail_sq = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + tail_sq += w_host[idx] * w_host[idx]; + } + + EXPECT_NEAR(w_host[offset] * w_host[offset] - tail_sq, 1.0, 1e-10) << "cone " << cone; + offset += static_cast(dim); + } +} + +TEST(second_order_cone_kernels, cone_step_length_matches_host_reference) +{ + auto stream = rmm::cuda_stream_default; + + std::vector cone_dimensions{3, 65, 32769}; + std::size_t n_cone_entries = 0; + for (const auto dim : cone_dimensions) { + n_cone_entries += static_cast(dim); + } + + std::vector x_host(n_cone_entries); + std::vector z_host(n_cone_entries); + std::vector dx_host(n_cone_entries); + std::vector dz_host(n_cone_entries); + + std::size_t offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + + x_host[offset] = 12.0 + static_cast(cone); + z_host[offset] = 14.0 + static_cast(cone); + dx_host[offset] = (cone == 0) ? -30.0 : 0.2; + dz_host[offset] = (cone == 1) ? -25.0 : 0.15; + + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + + x_host[idx] = 0.001 * static_cast((local_idx % 5) + 1); + z_host[idx] = 0.0015 * static_cast((local_idx % 7) + 1); + dx_host[idx] = 0.02 * static_cast((local_idx % 5) - 2); + dz_host[idx] = -0.015 * static_cast((local_idx % 7) - 3); + } + + offset += static_cast(dim); + } + + auto compute_expected_step = [&](std::vector const& u, + std::vector const& du, + double alpha_max, + std::vector& per_cone_alpha) { + auto global_alpha = alpha_max; + std::size_t off = 0; + + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + + double du_tail_sq = 0.0; + double u_tail_du_tail = 0.0; + double u_tail_sq = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = off + local_idx; + du_tail_sq += du[idx] * du[idx]; + u_tail_du_tail += u[idx] * du[idx]; + u_tail_sq += u[idx] * u[idx]; + } + + per_cone_alpha[cone] = host_cone_step_length_from_scalars( + u[off], du[off], du_tail_sq, u_tail_du_tail, u_tail_sq, alpha_max); + global_alpha = std::min(global_alpha, per_cone_alpha[cone]); + + off += static_cast(dim); + } + + return global_alpha; + }; + + constexpr double alpha_max = 0.99; + std::vector expected_primal_per_cone(cone_dimensions.size()); + std::vector expected_dual_per_cone(cone_dimensions.size()); + const auto expected_primal = + compute_expected_step(x_host, dx_host, alpha_max, expected_primal_per_cone); + const auto expected_dual = + compute_expected_step(z_host, dz_host, alpha_max, expected_dual_per_cone); + + auto x = cuopt::device_copy(x_host, stream); + auto z = cuopt::device_copy(z_host, stream); + auto dx = cuopt::device_copy(dx_host, stream); + auto dz = cuopt::device_copy(dz_host, stream); + + cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); + const auto [step_primal, step_dual] = + compute_cone_step_length(cones, + raft::device_span(dx.data(), dx.size()), + raft::device_span(dz.data(), dz.size()), + alpha_max, + stream); + + EXPECT_LT(expected_primal, alpha_max); + EXPECT_LT(expected_dual, alpha_max); + EXPECT_NEAR(step_primal, expected_primal, 1e-12); + EXPECT_NEAR(step_dual, expected_dual, 1e-12); + + const auto primal_per_cone = cuopt::host_copy(cones.scratch.step_alpha_primal, stream); + const auto dual_per_cone = cuopt::host_copy(cones.scratch.step_alpha_dual, stream); + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + EXPECT_NEAR(primal_per_cone[cone], expected_primal_per_cone[cone], 1e-12) + << "primal cone " << cone; + EXPECT_NEAR(dual_per_cone[cone], expected_dual_per_cone[cone], 1e-12) << "dual cone " << cone; + } +} + +TEST(second_order_cone_kernels, scaling_operators_match_host_reference) +{ + auto stream = rmm::cuda_stream_default; + + std::vector cone_dimensions{3, 65, 32769}; + std::size_t n_cone_entries = 0; + for (const auto dim : cone_dimensions) { + n_cone_entries += static_cast(dim); + } + + std::vector x_host(n_cone_entries); + std::vector z_host(n_cone_entries); + std::vector v_host(n_cone_entries); + std::vector cone_target_host(n_cone_entries); + std::vector accum_initial_host(n_cone_entries); + std::size_t offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + x_host[offset] = 100.0 + static_cast(cone); + z_host[offset] = 80.0 + static_cast(cone); + v_host[offset] = 0.75 + 0.1 * static_cast(cone); + cone_target_host[offset] = 0.4 + 0.03 * static_cast(cone); + accum_initial_host[offset] = -0.2 + 0.02 * static_cast(cone); + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + x_host[idx] = 0.001 * static_cast((local_idx % 5) + 1); + z_host[idx] = 0.0015 * static_cast((local_idx % 7) + 1); + v_host[idx] = 0.002 * static_cast((local_idx % 11) - 5); + cone_target_host[idx] = 0.003 * static_cast((local_idx % 13) - 6); + accum_initial_host[idx] = 0.004 * static_cast((local_idx % 17) - 8); + } + offset += static_cast(dim); + } + + auto x = cuopt::device_copy(x_host, stream); + auto z = cuopt::device_copy(z_host, stream); + auto v = cuopt::device_copy(v_host, stream); + auto cone_target = cuopt::device_copy(cone_target_host, stream); + auto accum = cuopt::device_copy(accum_initial_host, stream); + rmm::device_uvector w_out(n_cone_entries, stream); + rmm::device_uvector w_inv_out(n_cone_entries, stream); + rmm::device_uvector h_out(n_cone_entries, stream); + rmm::device_uvector w_tmp(n_cone_entries, stream); + // apply_w then apply_w on same v: should match apply_hessian (H = W^2 for symmetric NT W). + rmm::device_uvector w_squared_v(n_cone_entries, stream); + rmm::device_uvector recovered_dz(n_cone_entries, stream); + + cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); + launch_nt_scaling(cones, stream); + + auto v_span = raft::device_span(v.data(), v.size()); + apply_w(v_span, cuopt::make_span(w_out), cones, stream); + apply_w_inv(v_span, cuopt::make_span(w_inv_out), cones, stream); + apply_hessian(v_span, cuopt::make_span(h_out), cones, stream); + recover_cone_dz_from_target( + v_span, + cones, + raft::device_span(cone_target.data(), cone_target.size()), + cuopt::make_span(recovered_dz), + stream); + accumulate_cone_hessian_matvec(v_span, cones, cuopt::make_span(accum), stream); + apply_w(v_span, cuopt::make_span(w_tmp), cones, stream); + apply_w(raft::device_span(w_tmp.data(), w_tmp.size()), + cuopt::make_span(w_squared_v), + cones, + stream); + + auto eta_host = cuopt::host_copy(cones.eta, stream); + auto w_host = cuopt::host_copy(cones.w, stream); + auto w_out_host = cuopt::host_copy(w_out, stream); + auto w_inv_out_host = cuopt::host_copy(w_inv_out, stream); + auto h_out_host = cuopt::host_copy(h_out, stream); + auto w_squared_v_host = cuopt::host_copy(w_squared_v, stream); + auto recovered_dz_host = cuopt::host_copy(recovered_dz, stream); + auto accum_host = cuopt::host_copy(accum, stream); + + std::vector expected_w(n_cone_entries); + std::vector expected_w_inv(n_cone_entries); + std::vector expected_h(n_cone_entries); + std::vector expected_h_unscaled(n_cone_entries); + + offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + const auto w0 = w_host[offset]; + const auto v0 = v_host[offset]; + const auto eta = eta_host[cone]; + + double tail_dot = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + tail_dot += w_host[idx] * v_host[idx]; + } + + expected_w[offset] = eta * (w0 * v0 + tail_dot); + expected_w_inv[offset] = (w0 * v0 - tail_dot) / eta; + + const auto rho = w0 * v0 + tail_dot; + expected_h_unscaled[offset] = (eta * eta) * (2.0 * w0 * rho - v0); + expected_h[offset] = expected_h_unscaled[offset]; + + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + + expected_w[idx] = eta * (v_host[idx] + (v0 + tail_dot / (1.0 + w0)) * w_host[idx]); + expected_w_inv[idx] = (v_host[idx] + (-v0 + tail_dot / (1.0 + w0)) * w_host[idx]) / eta; + expected_h_unscaled[idx] = (eta * eta) * (2.0 * w_host[idx] * rho + v_host[idx]); + expected_h[idx] = expected_h_unscaled[idx]; + } + + offset += static_cast(dim); + } + + for (std::size_t i = 0; i < n_cone_entries; ++i) { + EXPECT_NEAR(w_out_host[i], expected_w[i], 1e-9) << "W entry " << i; + EXPECT_NEAR(w_inv_out_host[i], expected_w_inv[i], 1e-9) << "W inverse entry " << i; + EXPECT_NEAR(h_out_host[i], expected_h[i], 1e-9) << "H entry " << i; + EXPECT_NEAR(h_out_host[i], w_squared_v_host[i], 1e-9) << "W^2 v vs H v entry " << i; + EXPECT_NEAR(recovered_dz_host[i], cone_target_host[i] - expected_h_unscaled[i], 1e-9) + << "recovered dz entry " << i; + EXPECT_NEAR(accum_host[i], accum_initial_host[i] + expected_h_unscaled[i], 1e-9) + << "accumulated H entry " << i; + } +} + +TEST(second_order_cone_kernels, combined_cone_rhs_matches_host_reference) +{ + auto stream = rmm::cuda_stream_default; + + std::vector cone_dimensions{3, 65, 32769}; + std::size_t n_cone_entries = 0; + for (const auto dim : cone_dimensions) { + n_cone_entries += static_cast(dim); + } + + std::vector x_host(n_cone_entries); + std::vector z_host(n_cone_entries); + std::vector dx_aff_host(n_cone_entries); + std::vector dz_aff_host(n_cone_entries); + std::size_t offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + x_host[offset] = 120.0 + static_cast(cone); + z_host[offset] = 90.0 + static_cast(cone); + dx_aff_host[offset] = 0.25 + 0.05 * static_cast(cone); + dz_aff_host[offset] = -0.3 + 0.04 * static_cast(cone); + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + x_host[idx] = 0.001 * static_cast((local_idx % 5) + 1); + z_host[idx] = 0.0015 * static_cast((local_idx % 7) + 1); + dx_aff_host[idx] = 0.002 * static_cast((local_idx % 11) - 5); + dz_aff_host[idx] = 0.001 * static_cast((local_idx % 13) - 6); + } + offset += static_cast(dim); + } + + auto x = cuopt::device_copy(x_host, stream); + auto z = cuopt::device_copy(z_host, stream); + auto dx_aff = cuopt::device_copy(dx_aff_host, stream); + auto dz_aff = cuopt::device_copy(dz_aff_host, stream); + rmm::device_uvector out(n_cone_entries, stream); + + cone_data_t cones(cone_dimensions, cuopt::make_span(x), cuopt::make_span(z), stream); + launch_nt_scaling(cones, stream); + + constexpr double sigma_mu = 0.37; + compute_combined_cone_rhs_term(raft::device_span(dx_aff.data(), dx_aff.size()), + raft::device_span(dz_aff.data(), dz_aff.size()), + cones, + sigma_mu, + cuopt::make_span(out), + stream); + + auto eta_host = cuopt::host_copy(cones.eta, stream); + auto w_host = cuopt::host_copy(cones.w, stream); + auto out_host = cuopt::host_copy(out, stream); + + auto apply_w_ref = [&](std::vector const& v) { + std::vector result(n_cone_entries); + std::size_t off = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + const auto w0 = w_host[off]; + const auto v0 = v[off]; + + double tail_dot = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = off + local_idx; + tail_dot += w_host[idx] * v[idx]; + } + + result[off] = eta_host[cone] * (w0 * v0 + tail_dot); + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = off + local_idx; + result[idx] = eta_host[cone] * (v[idx] + (v0 + tail_dot / (1.0 + w0)) * w_host[idx]); + } + + off += static_cast(dim); + } + return result; + }; + + auto apply_w_inv_ref = [&](std::vector const& v) { + std::vector result(n_cone_entries); + std::size_t off = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + const auto w0 = w_host[off]; + const auto v0 = v[off]; + + double tail_dot = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = off + local_idx; + tail_dot += w_host[idx] * v[idx]; + } + + result[off] = (w0 * v0 - tail_dot) / eta_host[cone]; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = off + local_idx; + result[idx] = (v[idx] + (-v0 + tail_dot / (1.0 + w0)) * w_host[idx]) / eta_host[cone]; + } + + off += static_cast(dim); + } + return result; + }; + + // Same order as compute_combined_cone_rhs_term: apply_w(dx_aff), apply_w_inv(dz_aff). + auto scaled_dx = apply_w_ref(dx_aff_host); + auto scaled_dz = apply_w_inv_ref(dz_aff_host); + auto nt_point = apply_w_inv_ref(z_host); + + std::vector shift(n_cone_entries); + offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + + double head_dot = 0.0; + for (int local_idx = 0; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + head_dot += scaled_dx[idx] * scaled_dz[idx]; + } + + shift[offset] = head_dot - sigma_mu; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + shift[idx] = scaled_dx[offset] * scaled_dz[idx] + scaled_dz[offset] * scaled_dx[idx]; + } + + offset += static_cast(dim); + } + + std::vector minus_p(n_cone_entries); + offset = 0; + for (std::size_t cone = 0; cone < cone_dimensions.size(); ++cone) { + const auto dim = cone_dimensions[cone]; + const auto lambda0 = nt_point[offset]; + + double lambda_tail_dot = 0.0; + double lambda_tail_sq = 0.0; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + lambda_tail_dot += nt_point[idx] * shift[idx]; + lambda_tail_sq += nt_point[idx] * nt_point[idx]; + } + + const auto lambda_tail_norm = std::sqrt(lambda_tail_sq); + const auto det_lambda = (lambda0 - lambda_tail_norm) * (lambda0 + lambda_tail_norm); + ASSERT_GT(lambda0, 0.0) << "cone " << cone; + ASSERT_GT(det_lambda, 0.0) << "cone " << cone; + + const auto p_head = (lambda0 * shift[offset] - lambda_tail_dot) / det_lambda; + minus_p[offset] = -p_head; + for (int local_idx = 1; local_idx < dim; ++local_idx) { + const auto idx = offset + local_idx; + minus_p[idx] = (p_head * nt_point[idx] - shift[idx]) / lambda0; + } + + offset += static_cast(dim); + } + + auto expected = apply_w_ref(minus_p); + for (std::size_t i = 0; i < n_cone_entries; ++i) { + EXPECT_NEAR(out_host[i], expected[i], 1e-8) << "entry " << i; + } +} + +} // namespace cuopt::linear_programming::dual_simplex::test diff --git a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu index b0cbe624dc..ae4efb9b86 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu +++ b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu @@ -5,8 +5,6 @@ */ /* clang-format on */ -#include - #include #include @@ -16,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -146,7 +145,6 @@ TEST(barrier, dual_variable_greater_than) user_problem.A.x[nnz++] = 1.0; user_problem.A.i[nnz] = 1; user_problem.A.x[nnz++] = 2.0; - user_problem.A.print_matrix(); EXPECT_EQ(nnz, nz); user_problem.rhs.resize(m); @@ -181,6 +179,821 @@ TEST(barrier, dual_variable_greater_than) EXPECT_NEAR(solution.z[1], 0.0, 1e-5); } +TEST(barrier, cone_metadata_reindexed_when_slack_is_inserted_before_cones) +{ + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 1; + constexpr int n = 5; + constexpr int nz = 5; + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective.assign(n, 0.0); + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start.resize(n + 1); + for (int j = 0; j < n; ++j) { + user_problem.A.col_start[j] = j; + user_problem.A.i[j] = 0; + user_problem.A.x[j] = 1.0; + } + user_problem.A.col_start[n] = nz; + user_problem.rhs = {1.0}; + user_problem.row_sense = {'L'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + user_problem.num_range_rows = 0; + user_problem.second_order_cone_dims = {2, 2}; + user_problem.cone_var_start = 1; + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + settings.scale_columns = false; + + std::vector new_slacks; + dualize_info_t dualize_info; + lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + + ASSERT_EQ(new_slacks.size(), 1); + EXPECT_EQ(new_slacks[0], 1); + EXPECT_EQ(original_lp.num_cols, 6); + EXPECT_EQ(original_lp.second_order_cone_dims, user_problem.second_order_cone_dims); + EXPECT_EQ(original_lp.cone_var_start, 2); + + lp_problem_t barrier_lp(user_problem.handle_ptr, + original_lp.num_rows, + original_lp.num_cols, + original_lp.A.col_start[original_lp.num_cols]); + std::vector column_scales; + std::vector row_scales; + column_scaling(original_lp, settings, barrier_lp, column_scales, row_scales); + + EXPECT_EQ(barrier_lp.second_order_cone_dims, user_problem.second_order_cone_dims); + EXPECT_EQ(barrier_lp.cone_var_start, 2); +} + +TEST(barrier, presolve_reindexes_cone_start_after_empty_column_removal) +{ + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 1; + constexpr int n = 4; + constexpr int nz = 3; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {1.0, 0.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 0, 1, 2, 3}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 0; + user_problem.A.x[1] = -1.0; + user_problem.A.i[2] = 0; + user_problem.A.x[2] = 0.5; + + user_problem.rhs = {1.0}; + user_problem.row_sense = {'E'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + user_problem.num_range_rows = 0; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + settings.scale_columns = false; + + std::vector new_slacks; + dualize_info_t dualize_info; + lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + + presolve_info_t presolve_info; + lp_problem_t presolved_lp(user_problem.handle_ptr, 1, 1, 1); + ASSERT_EQ(presolve(original_lp, settings, presolved_lp, presolve_info), 0); + + EXPECT_EQ(presolved_lp.num_cols, 3); + EXPECT_EQ(presolved_lp.second_order_cone_dims, std::vector({3})); + EXPECT_EQ(presolved_lp.cone_var_start, 0); + + lp_problem_t barrier_lp(user_problem.handle_ptr, + presolved_lp.num_rows, + presolved_lp.num_cols, + presolved_lp.A.col_start[presolved_lp.num_cols]); + std::vector column_scales; + std::vector row_scales; + ASSERT_EQ(column_scaling(presolved_lp, settings, barrier_lp, column_scales, row_scales), 0); + EXPECT_EQ(barrier_lp.cone_var_start, 0); +} + +TEST(barrier, presolve_keeps_native_free_variables_before_cones) +{ + // Layout: [x0, x1 | cone x2, x3, x4] with x0, x1 free and a 3-dimensional SOC block. + // SOCP barrier presolve keeps native free variables (no x = v - w split); cone_var_start + // and column count stay unchanged. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 1; + constexpr int n = 5; + constexpr int nz = 5; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 0.0, 0.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 1, 2, 3, 4, 5}; + for (int j = 0; j < n; ++j) { + user_problem.A.i[j] = 0; + user_problem.A.x[j] = 1.0; + } + + user_problem.rhs = {1.0}; + user_problem.row_sense = {'E'}; + user_problem.lower = {-inf, -inf, 0.0, 0.0, 0.0}; + user_problem.upper.assign(n, inf); + user_problem.num_range_rows = 0; + user_problem.cone_var_start = 2; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + settings.scale_columns = false; + + std::vector new_slacks; + dualize_info_t dualize_info; + lp_problem_t original_lp(user_problem.handle_ptr, 1, 1, 1); + convert_user_problem(user_problem, settings, original_lp, new_slacks, dualize_info); + + presolve_info_t presolve_info; + lp_problem_t presolved_lp(user_problem.handle_ptr, 1, 1, 1); + ASSERT_EQ(presolve(original_lp, settings, presolved_lp, presolve_info), 0); + + EXPECT_EQ(presolved_lp.num_cols, 5); + EXPECT_EQ(presolved_lp.cone_var_start, 2); + EXPECT_EQ(presolved_lp.second_order_cone_dims, std::vector({3})); + EXPECT_TRUE(presolve_info.free_variable_pairs.empty()); + ASSERT_EQ(presolve_info.native_free_linear_indices.size(), 2); + EXPECT_EQ(presolve_info.native_free_linear_indices[0], 0); + EXPECT_EQ(presolve_info.native_free_linear_indices[1], 1); + EXPECT_EQ(presolved_lp.lower[0], -inf); + EXPECT_EQ(presolved_lp.lower[1], -inf); + EXPECT_EQ(presolved_lp.upper[0], inf); + EXPECT_EQ(presolved_lp.upper[1], inf); +} + +TEST(barrier, uncrush_solution_removes_non_tail_free_variable_partner) +{ + using namespace cuopt::linear_programming::dual_simplex; + + raft::handle_t handle{}; + init_handler(&handle); + lp_problem_t original_lp(&handle, 0, 3, 0); + + presolve_info_t presolve_info; + presolve_info.free_variable_pairs = {0, 1}; + + simplex_solver_settings_t settings; + std::vector crushed_x{5.0, 2.0, 9.0, 8.0}; + std::vector crushed_y{}; + std::vector crushed_z{7.0, 11.0, 13.0, 17.0}; + std::vector uncrushed_x(3); + std::vector uncrushed_y(0); + std::vector uncrushed_z(3); + + uncrush_solution(presolve_info, + settings, + original_lp, + crushed_x, + crushed_y, + crushed_z, + uncrushed_x, + uncrushed_y, + uncrushed_z); + + EXPECT_EQ(uncrushed_x, std::vector({3.0, 9.0, 8.0})); + EXPECT_EQ(uncrushed_z, std::vector({7.0, 13.0, 17.0})); +} + +TEST(barrier, rejects_middle_cone_input_before_barrier) +{ + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 3; + constexpr int n = 5; + constexpr int nz = 3; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {1.0, 0.0, 0.0, 0.0, 1.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 1, 1, 2, 2, 3}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 1; + user_problem.A.x[1] = 1.0; + user_problem.A.i[2] = 2; + user_problem.A.x[2] = 1.0; + + user_problem.rhs = {2.0, 1.0, 3.0}; + user_problem.row_sense = {'E', 'E', 'E'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + user_problem.num_range_rows = 0; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + lp_solution_t solution(m, n); + + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + EXPECT_EQ(status, lp_status_t::NUMERICAL_ISSUES); +} + +TEST(barrier, socp_min_x0_subject_to_norm_constraint) +{ + // minimize x_0 + // subject to x_1 = 1 + // (x_0, x_1, x_2) in Q^3 + // + // Optimal: x* = (1, 1, 0), obj* = 1 + + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 1; + constexpr int n = 3; + constexpr int nz = 1; + + user_problem.num_rows = m; + user_problem.num_cols = n; + + user_problem.objective = {1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + user_problem.A.col_start = {0, 0, 1, 1}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + + user_problem.rhs = {1.0}; + user_problem.row_sense = {'E'}; + + user_problem.lower = {0.0, 0.0, 0.0}; + user_problem.upper = {inf, inf, inf}; + + user_problem.num_range_rows = 0; + user_problem.problem_name = "socp_norm_cone"; + + user_problem.cone_var_start = 0; + user_problem.second_order_cone_dims = {3}; + + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.0, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[2]), 0.0, 1e-4); +} + +TEST(barrier, mixed_linear_and_soc_block) +{ + // Variables ordered as [l | t, u, v], where (t, u, v) \in Q^3. + // + // minimize l + // subject to l - t = 0 + // u = 1 + // (t, u, v) in Q^3 + // + // Optimal: l* = 1, t* = 1, u* = 1, v* = 0, obj* = 1. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 2; + constexpr int n = 4; + constexpr int nz = 4; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {1.0, 0.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l, t, u, v + user_problem.A.col_start = {0, 1, 2, 3, 3}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 0; + user_problem.A.x[1] = -1.0; + user_problem.A.i[2] = 1; + user_problem.A.x[2] = 1.0; + + user_problem.rhs = {0.0, 1.0}; + user_problem.row_sense = {'E', 'E'}; + + user_problem.lower = {0.0, 0.0, 0.0, 0.0}; + user_problem.upper = {inf, inf, inf, inf}; + + user_problem.num_range_rows = 0; + user_problem.problem_name = "mixed_linear_and_soc_block"; + + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.0, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[3]), 0.0, 1e-4); +} + +TEST(barrier, mixed_linear_and_soc_tail_coupling) +{ + // Variables ordered as [l | t, u, v], where (t, u, v) \in Q^3. + // + // minimize t + // subject to l - u = 0 + // l + u = 2 + // (t, u, v) in Q^3 + // + // Optimal: l* = 1, t* = 1, u* = 1, v* = 0, obj* = 1. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 2; + constexpr int n = 4; + constexpr int nz = 4; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l, t, u, v + user_problem.A.col_start = {0, 2, 2, 4, 4}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 1; + user_problem.A.x[1] = 1.0; + user_problem.A.i[2] = 0; + user_problem.A.x[2] = -1.0; + user_problem.A.i[3] = 1; + user_problem.A.x[3] = 1.0; + + user_problem.rhs = {0.0, 2.0}; + user_problem.row_sense = {'E', 'E'}; + user_problem.lower = {0.0, 0.0, 0.0, 0.0}; + user_problem.upper = {inf, inf, inf, inf}; + + user_problem.num_range_rows = 0; + user_problem.problem_name = "mixed_linear_and_soc_tail_coupling"; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + settings.scale_columns = true; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.0, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[3]), 0.0, 1e-4); +} + +TEST(barrier, mixed_linear_and_soc_tail_coupling_with_inequality) +{ + // Variables ordered as [l | t, u, v], where (t, u, v) \in Q^3. + // + // minimize t + // subject to l - u = 0 + // l + u >= 2 + // (t, u, v) in Q^3 + // + // Optimal: l* = 1, t* = 1, u* = 1, v* = 0, obj* = 1. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 2; + constexpr int n = 4; + constexpr int nz = 4; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l, t, u, v + user_problem.A.col_start = {0, 2, 2, 4, 4}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 1; + user_problem.A.x[1] = 1.0; + user_problem.A.i[2] = 0; + user_problem.A.x[2] = -1.0; + user_problem.A.i[3] = 1; + user_problem.A.x[3] = 1.0; + + user_problem.rhs = {0.0, 2.0}; + user_problem.row_sense = {'E', 'G'}; + user_problem.lower = {0.0, 0.0, 0.0, 0.0}; + user_problem.upper = {inf, inf, inf, inf}; + + user_problem.num_range_rows = 0; + user_problem.problem_name = "mixed_linear_and_soc_tail_coupling_with_inequality"; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + settings.scale_columns = true; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.0, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[3]), 0.0, 1e-4); +} + +TEST(barrier, mixed_linear_and_two_soc_blocks) +{ + // Variables ordered as [l1, l2 | t1, u1, v1 | t2, u2, v2], + // where (t1, u1, v1), (t2, u2, v2) \in Q^3. + // + // minimize t1 + t2 + // subject to l1 - u1 = 0 + // l2 - u2 = 0 + // l1 + l2 = 3 + // l1 - l2 = 1 + // + // Optimal: l1* = 2, l2* = 1, t1* = 2, u1* = 2, v1* = 0, + // t2* = 1, u2* = 1, v2* = 0, obj* = 3. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 4; + constexpr int n = 8; + constexpr int nz = 8; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l1, l2, t1, u1, v1, t2, u2, v2 + user_problem.A.col_start = {0, 3, 6, 6, 7, 7, 7, 8, 8}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 2; + user_problem.A.x[1] = 1.0; + user_problem.A.i[2] = 3; + user_problem.A.x[2] = 1.0; + user_problem.A.i[3] = 1; + user_problem.A.x[3] = 1.0; + user_problem.A.i[4] = 2; + user_problem.A.x[4] = 1.0; + user_problem.A.i[5] = 3; + user_problem.A.x[5] = -1.0; + user_problem.A.i[6] = 0; + user_problem.A.x[6] = -1.0; + user_problem.A.i[7] = 1; + user_problem.A.x[7] = -1.0; + + user_problem.rhs = {0.0, 0.0, 3.0, 1.0}; + user_problem.row_sense = {'E', 'E', 'E', 'E'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + + user_problem.num_range_rows = 0; + user_problem.problem_name = "mixed_linear_and_two_soc_blocks"; + user_problem.cone_var_start = 2; + user_problem.second_order_cone_dims = {3, 3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 3.0, 1e-4); + EXPECT_NEAR(solution.x[0], 2.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 2.0, 1e-4); + EXPECT_NEAR(solution.x[3], 2.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[4]), 0.0, 1e-4); + EXPECT_NEAR(solution.x[5], 1.0, 1e-4); + EXPECT_NEAR(solution.x[6], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[7]), 0.0, 1e-4); +} + +TEST(barrier, mixed_linear_and_two_soc_blocks_with_inequality) +{ + // Variables ordered as [l1, l2 | t1, u1, v1 | t2, u2, v2], + // where (t1, u1, v1), (t2, u2, v2) \in Q^3. + // + // minimize t1 + t2 + // subject to l1 - u1 = 0 + // l2 - u2 = 0 + // l1 + l2 >= 3 + // l1 - l2 = 1 + // + // Optimal: l1* = 2, l2* = 1, t1* = 2, u1* = 2, v1* = 0, + // t2* = 1, u2* = 1, v2* = 0, obj* = 3. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 4; + constexpr int n = 8; + constexpr int nz = 8; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l1, l2, t1, u1, v1, t2, u2, v2 + user_problem.A.col_start = {0, 3, 6, 6, 7, 7, 7, 8, 8}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 2; + user_problem.A.x[1] = 1.0; + user_problem.A.i[2] = 3; + user_problem.A.x[2] = 1.0; + user_problem.A.i[3] = 1; + user_problem.A.x[3] = 1.0; + user_problem.A.i[4] = 2; + user_problem.A.x[4] = 1.0; + user_problem.A.i[5] = 3; + user_problem.A.x[5] = -1.0; + user_problem.A.i[6] = 0; + user_problem.A.x[6] = -1.0; + user_problem.A.i[7] = 1; + user_problem.A.x[7] = -1.0; + + user_problem.rhs = {0.0, 0.0, 3.0, 1.0}; + user_problem.row_sense = {'E', 'E', 'G', 'E'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + + user_problem.num_range_rows = 0; + user_problem.problem_name = "mixed_linear_and_two_soc_blocks_with_inequality"; + user_problem.cone_var_start = 2; + user_problem.second_order_cone_dims = {3, 3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + settings.scale_columns = true; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 3.0, 1e-4); + EXPECT_NEAR(solution.x[0], 2.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 2.0, 1e-4); + EXPECT_NEAR(solution.x[3], 2.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[4]), 0.0, 1e-4); + EXPECT_NEAR(solution.x[5], 1.0, 1e-4); + EXPECT_NEAR(solution.x[6], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[7]), 0.0, 1e-4); +} + +TEST(barrier, free_linear_prefix_is_uncrushed_correctly_with_soc_block) +{ + // Variables ordered as [l | t, u, v], where (t, u, v) \in Q^3 and l is free. + // + // minimize t + // subject to l - u = 0 + // u = 1 + // (t, u, v) in Q^3 + // + // Native free variable l is kept through presolve; end-to-end solve returns + // l* = 1, t* = 1, u* = 1, v* = 0, obj* = 1. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 2; + constexpr int n = 4; + constexpr int nz = 3; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l, t, u, v + user_problem.A.col_start = {0, 1, 1, 3, 3}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 0; + user_problem.A.x[1] = -1.0; + user_problem.A.i[2] = 1; + user_problem.A.x[2] = 1.0; + + user_problem.rhs = {0.0, 1.0}; + user_problem.row_sense = {'E', 'E'}; + user_problem.lower = {-inf, 0.0, 0.0, 0.0}; + user_problem.upper = {inf, inf, inf, inf}; + + user_problem.num_range_rows = 0; + user_problem.problem_name = "free_linear_prefix_is_uncrushed_correctly_with_soc_block"; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.0, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[3]), 0.0, 1e-4); +} + +TEST(barrier, qp_with_soc_block) +{ + // Variables ordered as [l | t, u, v], where (t, u, v) \in Q^3. + // + // minimize 0.5 l^2 + t + // subject to l + u = 2 + // (t, u, v) in Q^3 + // + // Since t >= |u| and u = 2 - l with l >= 0, the objective becomes + // 0.5 l^2 + |2 - l|, which is minimized at l* = 1, u* = 1, t* = 1, v* = 0. + raft::handle_t handle{}; + init_handler(&handle); + + using namespace cuopt::linear_programming::dual_simplex; + user_problem_t user_problem(&handle); + + constexpr int m = 1; + constexpr int n = 4; + constexpr int nz = 2; + + user_problem.num_rows = m; + user_problem.num_cols = n; + user_problem.objective = {0.0, 1.0, 0.0, 0.0}; + + user_problem.A.m = m; + user_problem.A.n = n; + user_problem.A.nz_max = nz; + user_problem.A.reallocate(nz); + // Columns: l, t, u, v + user_problem.A.col_start = {0, 1, 1, 2, 2}; + user_problem.A.i[0] = 0; + user_problem.A.x[0] = 1.0; + user_problem.A.i[1] = 0; + user_problem.A.x[1] = 1.0; + + user_problem.rhs = {2.0}; + user_problem.row_sense = {'E'}; + user_problem.lower.assign(n, 0.0); + user_problem.upper.assign(n, inf); + + user_problem.Q_offsets = {0, 1, 1, 1, 1}; + user_problem.Q_indices = {0}; + user_problem.Q_values = {1.0}; + + user_problem.num_range_rows = 0; + user_problem.problem_name = "qp_with_soc_block"; + user_problem.cone_var_start = 1; + user_problem.second_order_cone_dims = {3}; + user_problem.var_types.assign(n, variable_type_t::CONTINUOUS); + + simplex_solver_settings_t settings; + settings.barrier = true; + settings.dualize = 0; + + lp_solution_t solution(m, n); + auto status = solve_linear_program_with_barrier(user_problem, settings, solution); + + EXPECT_EQ(status, lp_status_t::OPTIMAL); + EXPECT_NEAR(solution.objective, 1.5, 1e-4); + EXPECT_NEAR(solution.x[0], 1.0, 1e-4); + EXPECT_NEAR(solution.x[1], 1.0, 1e-4); + EXPECT_NEAR(solution.x[2], 1.0, 1e-4); + EXPECT_NEAR(std::abs(solution.x[3]), 0.0, 1e-4); +} + TEST(barrier, min_x_squared_free_variable_dual_correction) { // minimize x^2 (Q = [2.0], so 0.5 * x^T Q x = x^2) diff --git a/cpp/tests/linear_programming/parser_test.cpp b/cpp/tests/linear_programming/parser_test.cpp index 4263a594ce..086432e7b3 100644 --- a/cpp/tests/linear_programming/parser_test.cpp +++ b/cpp/tests/linear_programming/parser_test.cpp @@ -2155,8 +2155,9 @@ End EXPECT_EQ(qc.constraint_row_type, static_cast(LesserThanOrEqual)); EXPECT_NEAR(qc.rhs_value, 10.0, tolerance); EXPECT_TRUE(qc.linear_indices.empty()); - // Q = diag(1, 1). CSR: offsets=[0, 1, 2], indices=[0, 1], values=[1, 1]. - EXPECT_EQ(qc.quadratic_offsets, (std::vector{0, 1, 2})); + // Q = diag(1, 1) stored as COO triplets (row, col, value). + EXPECT_EQ(qc.quadratic_row_indices, (std::vector{0, 1})); + EXPECT_EQ(qc.quadratic_col_indices, (std::vector{0, 1})); ASSERT_EQ(qc.quadratic_values.size(), 2u); EXPECT_NEAR(qc.quadratic_values[0], 1.0, tolerance); EXPECT_NEAR(qc.quadratic_values[1], 1.0, tolerance); @@ -2165,7 +2166,7 @@ End TEST(lp_parser, qc_cross_term_splits_symmetrically) { // `4 x*y` in the LP source means coefficient on x_i * x_j = 4 in the - // symmetric x^T Q x. Split into Q[x,y] = Q[y,x] = 2 in the CSR. + // symmetric x^T Q x. Split into Q[x,y] = Q[y,x] = 2 in the COO storage. auto m = parse_lp_string(R"LP( Minimize x + y @@ -2175,8 +2176,9 @@ End )LP"); ASSERT_EQ(m.get_quadratic_constraints().size(), 1u); const auto& qc = nth_qc(m, 0); - // Q has 4 entries (all of [[1,2],[2,1]]). - EXPECT_EQ(qc.quadratic_offsets, (std::vector{0, 2, 4})); + // Q has 4 entries (all of [[1,2],[2,1]]) stored as COO triplets. + EXPECT_EQ(qc.quadratic_row_indices, (std::vector{0, 0, 1, 1})); + EXPECT_EQ(qc.quadratic_col_indices, (std::vector{0, 1, 0, 1})); ASSERT_EQ(qc.quadratic_values.size(), 4u); EXPECT_NEAR(qc.quadratic_values[0], 1.0, tolerance); // (0, 0) EXPECT_NEAR(qc.quadratic_values[1], 2.0, tolerance); // (0, 1) @@ -2730,16 +2732,16 @@ TEST(qps_parser, qcmatrix_append_api) model_t::quadratic_constraint_t default_qcm; EXPECT_EQ(0, default_qcm.constraint_row_index); EXPECT_TRUE(default_qcm.quadratic_values.empty()); - EXPECT_TRUE(default_qcm.quadratic_indices.empty()); - EXPECT_TRUE(default_qcm.quadratic_offsets.empty()); + EXPECT_TRUE(default_qcm.quadratic_row_indices.empty()); + EXPECT_TRUE(default_qcm.quadratic_col_indices.empty()); EXPECT_TRUE(default_qcm.linear_values.empty()); EXPECT_TRUE(default_qcm.linear_indices.empty()); EXPECT_EQ(0.0, default_qcm.rhs_value); // QC0: [[10, 2], [2, 2]] const std::vector qc0_values = {10.0, 2.0, 2.0, 2.0}; - const std::vector qc0_indices = {0, 1, 0, 1}; - const std::vector qc0_offsets = {0, 2, 4}; + const std::vector qc0_row_indices = {0, 0, 1, 1}; + const std::vector qc0_col_indices = {0, 1, 0, 1}; const std::vector qc0_linear_values = {1.0, 1.0}; const std::vector qc0_linear_indices = {0, 1}; model.append_quadratic_constraint(0, @@ -2749,13 +2751,13 @@ TEST(qps_parser, qcmatrix_append_api) qc0_linear_indices, 5.0, qc0_values, - qc0_indices, - qc0_offsets); + qc0_row_indices, + qc0_col_indices); // QC1: [[4, 1], [1, 6]] const std::vector qc1_values = {4.0, 1.0, 1.0, 6.0}; - const std::vector qc1_indices = {0, 1, 0, 1}; - const std::vector qc1_offsets = {0, 2, 4}; + const std::vector qc1_row_indices = {0, 0, 1, 1}; + const std::vector qc1_col_indices = {0, 1, 0, 1}; const std::vector qc1_linear_values = {3.0, 1.0}; const std::vector qc1_linear_indices = {0, 1}; model.append_quadratic_constraint(1, @@ -2765,8 +2767,8 @@ TEST(qps_parser, qcmatrix_append_api) qc1_linear_indices, 10.0, qc1_values, - qc1_indices, - qc1_offsets); + qc1_row_indices, + qc1_col_indices); ASSERT_TRUE(model.has_quadratic_constraints()); const auto& qcs = model.get_quadratic_constraints(); @@ -2779,8 +2781,8 @@ TEST(qps_parser, qcmatrix_append_api) EXPECT_EQ(qc0_linear_indices, qcs[0].linear_indices); EXPECT_EQ(5.0, qcs[0].rhs_value); EXPECT_EQ(qc0_values, qcs[0].quadratic_values); - EXPECT_EQ(qc0_indices, qcs[0].quadratic_indices); - EXPECT_EQ(qc0_offsets, qcs[0].quadratic_offsets); + EXPECT_EQ(qc0_row_indices, qcs[0].quadratic_row_indices); + EXPECT_EQ(qc0_col_indices, qcs[0].quadratic_col_indices); EXPECT_EQ(1, qcs[1].constraint_row_index); EXPECT_EQ("QC1", qcs[1].constraint_row_name); @@ -2789,8 +2791,8 @@ TEST(qps_parser, qcmatrix_append_api) EXPECT_EQ(qc1_linear_indices, qcs[1].linear_indices); EXPECT_EQ(10.0, qcs[1].rhs_value); EXPECT_EQ(qc1_values, qcs[1].quadratic_values); - EXPECT_EQ(qc1_indices, qcs[1].quadratic_indices); - EXPECT_EQ(qc1_offsets, qcs[1].quadratic_offsets); + EXPECT_EQ(qc1_row_indices, qcs[1].quadratic_row_indices); + EXPECT_EQ(qc1_col_indices, qcs[1].quadratic_col_indices); } // QCQP MPS: each quadratic constraint bundles row + linear + rhs + quadratic. diff --git a/cpp/tests/qp/unit_tests/no_constraints.cu b/cpp/tests/qp/unit_tests/no_constraints.cu index 75190b37fc..dc99a2ce5c 100644 --- a/cpp/tests/qp/unit_tests/no_constraints.cu +++ b/cpp/tests/qp/unit_tests/no_constraints.cu @@ -52,6 +52,8 @@ TEST(no_constraints_test, simple_test) op_problem.set_quadratic_objective_matrix(Q_values_host, 2, Q_indices_host, 2, Q_offsets_host, 3); auto settings = cuopt::linear_programming::pdlp_solver_settings_t(); + // Tighter than default: unconstrained QP needs small x at the origin, not just small objective. + settings.barrier_relative_complementarity_tolerance = 1e-12; auto solution = cuopt::linear_programming::solve_lp(op_problem, settings); EXPECT_EQ(solution.get_termination_status(), diff --git a/cpp/tests/qp/unit_tests/two_variable_test.cu b/cpp/tests/qp/unit_tests/two_variable_test.cu index aac618994a..f1fb4852ec 100644 --- a/cpp/tests/qp/unit_tests/two_variable_test.cu +++ b/cpp/tests/qp/unit_tests/two_variable_test.cu @@ -59,6 +59,7 @@ TEST(two_variable_test, simple_test) op_problem.set_quadratic_objective_matrix(Q_values_host, 2, Q_indices_host, 2, Q_offsets_host, 3); auto settings = cuopt::linear_programming::pdlp_solver_settings_t(); + settings.barrier_relative_complementarity_tolerance = 1e-12; auto solution = cuopt::linear_programming::solve_lp(op_problem, settings); EXPECT_EQ(solution.get_termination_status(), diff --git a/datasets/qcqp/socp1.mps b/datasets/qcqp/socp1.mps new file mode 100644 index 0000000000..3e479054d0 --- /dev/null +++ b/datasets/qcqp/socp1.mps @@ -0,0 +1,23 @@ +NAME +ROWS + N OBJ + L c2 + E c1 +COLUMNS + y OBJ -1 + z OBJ -1 + x c1 1 +RHS + rhs c2 0 + rhs c1 1 +RANGES +BOUNDS + FR bounds y + FR bounds z + LO bounds x 0 + PL bounds x +QCMATRIX c2 + y y 1 + z z 1 + x x -1 +ENDATA diff --git a/datasets/qcqp/socp2.mps b/datasets/qcqp/socp2.mps new file mode 100644 index 0000000000..6542d1d4f8 --- /dev/null +++ b/datasets/qcqp/socp2.mps @@ -0,0 +1,26 @@ +NAME +ROWS + N OBJ + L c2 + G c1 + E c3 +COLUMNS + x OBJ 1 + y c1 1 + tau c3 1 +RHS + rhs c2 0 + rhs c1 0.7071067811865476 + rhs c3 1 +RANGES +BOUNDS + FR bounds x + LO bounds y 0 + PL bounds y + LO bounds tau 0 + PL bounds tau +QCMATRIX c2 + x x 1 + y y 1 + tau tau -1 +ENDATA diff --git a/datasets/qcqp/socp3.mps b/datasets/qcqp/socp3.mps new file mode 100644 index 0000000000..ae92220042 --- /dev/null +++ b/datasets/qcqp/socp3.mps @@ -0,0 +1,22 @@ +NAME +ROWS + N OBJ + L c2 + L c3 + G c1 +COLUMNS + y c1 1 + x c2 1 +RHS + rhs c2 1 + rhs c3 0 + rhs c1 2 +RANGES +BOUNDS + FR bounds y + LO bounds x 0 + PL bounds x +QCMATRIX c3 + y y 1 + x x -1 +ENDATA diff --git a/datasets/qcqp/socp4.mps b/datasets/qcqp/socp4.mps new file mode 100644 index 0000000000..584725963f --- /dev/null +++ b/datasets/qcqp/socp4.mps @@ -0,0 +1,33 @@ +NAME +ROWS + N OBJ + L c2 + E c1a + E c1b + E c1c +COLUMNS + x2 c1b 1 + x2 OBJ -2 + x3 c1c 1 + x3 OBJ -1 + x1 c1a 1 + x4 c1b -1 + x5 c1c -1 +RHS + rhs c2 0 + rhs c1a 1 + rhs c1b 0 + rhs c1c 0 +RANGES +BOUNDS + FR bounds x2 + FR bounds x3 + LO bounds x1 0 + PL bounds x1 + FR bounds x4 + FR bounds x5 +QCMATRIX c2 + x1 x1 -1 + x4 x4 1 + x5 x5 1 +ENDATA diff --git a/dependencies.yaml b/dependencies.yaml index d3424f17c2..17466758c0 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -283,7 +283,7 @@ dependencies: common: - output_types: [conda] packages: - - libcuopt-tests==26.6.*,>=0.0.0a0 + - libcuopt-tests==26.8.*,>=0.0.0a0 build_wheels: common: - output_types: [requirements, pyproject] @@ -393,12 +393,12 @@ dependencies: common: - output_types: [requirements, pyproject] packages: - - cuopt-cu13==26.6.*,>=0.0.0a0 + - cuopt-cu13==26.8.*,>=0.0.0a0 depends_on_libcuopt: common: - output_types: conda packages: - - &libcuopt_unsuffixed libcuopt==26.6.*,>=0.0.0a0 + - &libcuopt_unsuffixed libcuopt==26.8.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -411,18 +411,18 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - libcuopt-cu12==26.6.*,>=0.0.0a0 + - libcuopt-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - libcuopt-cu13==26.6.*,>=0.0.0a0 + - libcuopt-cu13==26.8.*,>=0.0.0a0 - {matrix: null, packages: [*libcuopt_unsuffixed]} depends_on_cuopt: common: - output_types: conda packages: - - &cuopt_unsuffixed cuopt==26.6.*,>=0.0.0a0 + - &cuopt_unsuffixed cuopt==26.8.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -435,18 +435,18 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - cuopt-cu12==26.6.*,>=0.0.0a0 + - cuopt-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - cuopt-cu13==26.6.*,>=0.0.0a0 + - cuopt-cu13==26.8.*,>=0.0.0a0 - {matrix: null, packages: [*cuopt_unsuffixed]} depends_on_cuopt_server: common: - output_types: conda packages: - - &cuopt_server_unsuffixed cuopt-server==26.6.*,>=0.0.0a0 + - &cuopt_server_unsuffixed cuopt-server==26.8.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -459,18 +459,18 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - cuopt-server-cu12==26.6.*,>=0.0.0a0 + - cuopt-server-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - cuopt-server-cu13==26.6.*,>=0.0.0a0 + - cuopt-server-cu13==26.8.*,>=0.0.0a0 - {matrix: null, packages: [*cuopt_server_unsuffixed]} depends_on_cuopt_sh_client: common: - output_types: [conda, requirements, pyproject] packages: - - &cuopt_sh_client_unsuffixed cuopt-sh-client==26.6.*,>=0.0.0a0 + - &cuopt_sh_client_unsuffixed cuopt-sh-client==26.8.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -480,12 +480,12 @@ dependencies: common: - output_types: conda packages: - - libraft-headers==26.6.*,>=0.0.0a0 + - libraft-headers==26.8.*,>=0.0.0a0 depends_on_librmm: common: - output_types: conda packages: - - &librmm_unsuffixed librmm==26.6.*,>=0.0.0a0 + - &librmm_unsuffixed librmm==26.8.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -497,12 +497,12 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - librmm-cu12==26.6.*,>=0.0.0a0 + - librmm-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - librmm-cu13==26.6.*,>=0.0.0a0 + - librmm-cu13==26.8.*,>=0.0.0a0 - {matrix: null, packages: [*librmm_unsuffixed]} depends_on_cupy: common: @@ -537,7 +537,7 @@ dependencies: common: - output_types: conda packages: - - &rmm_unsuffixed rmm==26.6.*,>=0.0.0a0 + - &rmm_unsuffixed rmm==26.8.*,>=0.0.0a0 - output_types: requirements packages: # pip recognizes the index as a global option for the requirements.txt file @@ -549,12 +549,12 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - rmm-cu12==26.6.*,>=0.0.0a0 + - rmm-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - rmm-cu13==26.6.*,>=0.0.0a0 + - rmm-cu13==26.8.*,>=0.0.0a0 - matrix: packages: - *rmm_unsuffixed @@ -563,7 +563,7 @@ dependencies: common: - output_types: conda packages: - - &cudf_unsuffixed cudf==26.6.*,>=0.0.0a0 + - &cudf_unsuffixed cudf==26.8.*,>=0.0.0a0 - output_types: requirements packages: - --extra-index-url=https://pypi.anaconda.org/rapidsai-wheels-nightly/simple @@ -574,12 +574,12 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - cudf-cu12==26.6.*,>=0.0.0a0 + - cudf-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - cudf-cu13==26.6.*,>=0.0.0a0 + - cudf-cu13==26.8.*,>=0.0.0a0 - matrix: packages: - *cudf_unsuffixed @@ -588,7 +588,7 @@ dependencies: common: - output_types: conda packages: - - &pylibraft_unsuffixed pylibraft==26.6.*,>=0.0.0a0 + - &pylibraft_unsuffixed pylibraft==26.8.*,>=0.0.0a0 - output_types: requirements packages: - --extra-index-url=https://pypi.anaconda.org/rapidsai-wheels-nightly/simple @@ -599,12 +599,12 @@ dependencies: cuda: "12.*" cuda_suffixed: "true" packages: - - pylibraft-cu12==26.6.*,>=0.0.0a0 + - pylibraft-cu12==26.8.*,>=0.0.0a0 - matrix: cuda: "13.*" cuda_suffixed: "true" packages: - - pylibraft-cu13==26.6.*,>=0.0.0a0 + - pylibraft-cu13==26.8.*,>=0.0.0a0 - matrix: packages: - *pylibraft_unsuffixed diff --git a/docs/cuopt/source/versions1.json b/docs/cuopt/source/versions1.json index 507dfe57a4..dc92d1b660 100644 --- a/docs/cuopt/source/versions1.json +++ b/docs/cuopt/source/versions1.json @@ -1,10 +1,14 @@ [ { - "version": "26.06.00", - "url": "https://docs.nvidia.com/cuopt/user-guide/26.06.00/", + "version": "26.08.00", + "url": "https://docs.nvidia.com/cuopt/user-guide/26.08.00/", "name": "latest", "preferred": true }, + { + "version": "26.06.00", + "url": "https://docs.nvidia.com/cuopt/user-guide/26.06.00/" + }, { "version": "26.04.00", "url": "https://docs.nvidia.com/cuopt/user-guide/26.04.00/" diff --git a/gemini-extension.json b/gemini-extension.json index c5ef9883f8..48e8cb7ef7 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "nvidia-cuopt-skills", "description": "Agent skills for NVIDIA cuOpt optimization engine: routing, LP/MILP/QP, installation, and server.", - "version": "26.06.00", + "version": "26.08.00", "contextFileName": "AGENTS.md" } diff --git a/helmchart/cuopt-server/Chart.yaml b/helmchart/cuopt-server/Chart.yaml index 811ac067cb..5d638a1366 100644 --- a/helmchart/cuopt-server/Chart.yaml +++ b/helmchart/cuopt-server/Chart.yaml @@ -1,17 +1,17 @@ apiVersion: v2 -appVersion: 26.6.0 +appVersion: 26.8.0 description: A Helm chart for NVIDIA cuOpt Server with GPU support home: https://docs.nvidia.com/cuopt/user-guide/latest/resources.html keywords: -- nvidia -- cuopt -- optimization -- gpu + - nvidia + - cuopt + - optimization + - gpu maintainers: -- email: cuopt@nvidia.com - name: cuopt-maintainer + - email: cuopt@nvidia.com + name: cuopt-maintainer name: cuopt-server sources: -- https://docs.nvidia.com/cuopt/user-guide/latest/resources.html + - https://docs.nvidia.com/cuopt/user-guide/latest/resources.html type: application -version: 26.6.0 +version: 26.8.0 diff --git a/helmchart/cuopt-server/values.yaml b/helmchart/cuopt-server/values.yaml index 6adafea79e..1d744615fa 100644 --- a/helmchart/cuopt-server/values.yaml +++ b/helmchart/cuopt-server/values.yaml @@ -7,7 +7,7 @@ replicaCount: 1 image: repository: nvidia/cuopt pullPolicy: IfNotPresent - tag: "26.6.0-cuda12.9-py3.12" + tag: "26.8.0-cuda12.9-py3.12" imagePullSecrets: [] nameOverride: "" diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd index 4a83f3a058..b25ba76e3a 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.pxd @@ -7,11 +7,27 @@ # cython: embedsignature = True # cython: language_level = 3 +from libc.stddef cimport size_t from libcpp cimport bool from libcpp.string cimport string from libcpp.vector cimport vector +cdef extern from "cuopt/linear_programming/io/mps_data_model.hpp" namespace "cuopt::linear_programming::io" nogil: # noqa + + cdef cppclass mps_data_model_t[i_t, f_t]: + cppclass quadratic_constraint_t: + int constraint_row_index + string constraint_row_name + char constraint_row_type + vector[double] linear_values + vector[int] linear_indices + double rhs_value + vector[int] quadratic_row_indices + vector[int] quadratic_col_indices + vector[double] quadratic_values + + cdef extern from "cuopt/linear_programming/io/data_model_view.hpp" namespace "cuopt::linear_programming::io" nogil: # noqa cdef cppclass data_model_view_t[i_t, f_t]: @@ -54,6 +70,8 @@ cdef extern from "cuopt/linear_programming/io/data_model_view.hpp" namespace "cu void set_row_names(const vector[string] row_names) except + void set_problem_name(const string problem_name) except + void set_objective_name(const string objective_name) except + + void set_quadratic_constraints( + vector[mps_data_model_t[i_t, f_t].quadratic_constraint_t] constraints) except + cdef extern from "cuopt/linear_programming/io/writer.hpp" namespace "cuopt::linear_programming::io" nogil: # noqa diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model.py b/python/cuopt/cuopt/linear_programming/data_model/data_model.py index 648809eac1..5e6fd6744d 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model.py +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model.py @@ -4,6 +4,8 @@ import os import time +import numpy as np + from . import data_model_wrapper from .utilities import catch_cuopt_exception @@ -288,6 +290,90 @@ def set_quadratic_objective_matrix(self, Q_values, Q_indices, Q_offsets): """ super().set_quadratic_objective_matrix(Q_values, Q_indices, Q_offsets) + def get_quadratic_constraints(self): + """ + Return quadratic (QCMATRIX) constraints appended to this model. + + Each entry is a dict with keys including ``constraint_row_index``, + ``constraint_row_name``, ``constraint_row_type``, COO arrays, and ``rhs_value``. + """ + return self.quadratic_constraints + + @catch_cuopt_exception + def clear_quadratic_constraints(self): + """ + Remove all quadratic (QCMATRIX) constraints from the model. + + Quadratic constraints are converted to second-order cone form internally + and solved with the barrier method. + """ + super().clear_quadratic_constraints() + + @catch_cuopt_exception + def add_quadratic_constraint( + self, + constraint_row_index, + constraint_row_name="", + linear_values=None, + linear_indices=None, + rhs_value=0.0, + quadratic_values=None, + quadratic_row_indices=None, + quadratic_col_indices=None, + sense="L", + ): + """ + Add one quadratic constraint in MPS QCMATRIX form. + + Each constraint has a linear part (optional) and a quadratic part in COO + format. Call multiple times to add several QCMATRIX rows; each call must + use a distinct ``constraint_row_index``. + + Parameters + ---------- + constraint_row_index : int + ROWS declaration index for this quadratic row (among all rows). + constraint_row_name : str, optional + Optional row name (for MPS export). + linear_values, linear_indices : array-like, optional + Sparse linear coefficients on the same variable index space. + rhs_value : float, optional + Right-hand side of the quadratic row. + quadratic_values, quadratic_row_indices, quadratic_col_indices : array-like + COO triplets for the quadratic matrix Q in + ``linear^T x + x^T Q x {sense} rhs_value``. + sense : str, optional + MPS row type: ``'L'`` (default, ``<=``) or ``'G'`` (``>=``), same values as + :meth:`set_row_types`. Rows are stored with the given sense; ``'G'`` is converted to + ``'L'`` when the barrier solver builds second-order cones from QCMATRIX data. + Equality (``'E'``) is not supported. + + Notes + ----- + When any quadratic constraint is present, cuOpt selects the barrier + solver and converts QCMATRIX rows to second-order cones. + """ + if hasattr(sense, "value"): + sense = sense.value + if sense == "E": + raise ValueError("Equality constraints are not supported.") + if sense not in ("L", "G"): + raise ValueError( + f"Invalid sense {sense!r}; use 'L' or 'G' like set_row_types " + "(equality 'E' is not supported)." + ) + super().add_quadratic_constraint( + constraint_row_index, + constraint_row_name, + linear_values, + linear_indices, + rhs_value, + quadratic_values, + quadratic_row_indices, + quadratic_col_indices, + constraint_row_type=sense, + ) + @catch_cuopt_exception def set_variable_lower_bounds(self, variable_lower_bounds): """ diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pxd b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pxd index 6c798a0f6a..9f9f5ba5cf 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pxd +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pxd @@ -12,3 +12,7 @@ from libcpp.memory cimport unique_ptr cdef class DataModel: cdef unique_ptr[data_model_view_t[int, double]] c_data_model_view + + cdef void _set_cpp_quadratic_constraints( + self, data_model_view_t[int, double]* c_data_model_view + ) diff --git a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx index 7722fb2437..7c764b1413 100644 --- a/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/data_model/data_model_wrapper.pyx @@ -7,7 +7,7 @@ # cython: embedsignature = True # cython: language_level = 3 -from .data_model cimport data_model_view_t, write_mps +from .data_model cimport data_model_view_t, mps_data_model_t, write_mps import warnings @@ -17,10 +17,14 @@ from cuopt.utilities import get_data_ptr from libc.stdint cimport uintptr_t from libcpp.memory cimport unique_ptr +from libcpp.string cimport string from libcpp.utility cimport move +from libcpp.vector cimport vector def type_cast(np_obj, np_type, name): + if not isinstance(np_obj, np.ndarray): + np_obj = np.asarray(np_obj) obj_type = np_obj.dtype if ((np.issubdtype(np_type, np.floating) and @@ -62,6 +66,79 @@ cdef class DataModel: self.variable_types = np.array([]) self.variable_names = np.array([]) self.row_names = np.array([]) + self.quadratic_constraints = [] + + def clear_quadratic_constraints(self): + self.quadratic_constraints = [] + + def add_quadratic_constraint( + self, + constraint_row_index, + constraint_row_name="", + linear_values=None, + linear_indices=None, + rhs_value=0.0, + quadratic_values=None, + quadratic_row_indices=None, + quadratic_col_indices=None, + constraint_row_type="L", + ): + linear_values = ( + np.array([], dtype=np.float64) + if linear_values is None + else type_cast(linear_values, np.float64, "linear_values") + ) + linear_indices = ( + np.array([], dtype=np.int32) + if linear_indices is None + else type_cast(linear_indices, np.int32, "linear_indices") + ) + if linear_values.shape[0] != linear_indices.shape[0]: + raise ValueError("linear_values and linear_indices must have the same length") + quadratic_values = ( + np.array([], dtype=np.float64) + if quadratic_values is None + else type_cast(quadratic_values, np.float64, "quadratic_values") + ) + quadratic_row_indices = ( + np.array([], dtype=np.int32) + if quadratic_row_indices is None + else type_cast(quadratic_row_indices, np.int32, "quadratic_row_indices") + ) + quadratic_col_indices = ( + np.array([], dtype=np.int32) + if quadratic_col_indices is None + else type_cast(quadratic_col_indices, np.int32, "quadratic_col_indices") + ) + if not ( + quadratic_values.shape[0] + == quadratic_row_indices.shape[0] + == quadratic_col_indices.shape[0] + ): + raise ValueError( + "quadratic_values, quadratic_row_indices, and " + "quadratic_col_indices must have the same length" + ) + row_type = str(constraint_row_type) + if row_type == "E": + raise ValueError("Equality constraints are not supported.") + if row_type not in ("L", "G"): + raise ValueError( + f"Invalid constraint_row_type {row_type!r}; use 'L' or 'G' like set_row_types." + ) + self.quadratic_constraints.append( + { + "constraint_row_index": int(constraint_row_index), + "constraint_row_name": str(constraint_row_name), + "constraint_row_type": row_type, + "linear_values": linear_values, + "linear_indices": linear_indices, + "rhs_value": float(rhs_value), + "quadratic_values": quadratic_values, + "quadratic_row_indices": quadratic_row_indices, + "quadratic_col_indices": quadratic_col_indices, + } + ) def set_maximize(self, maximize): self.maximize = maximize @@ -379,10 +456,65 @@ cdef class DataModel: self.get_initial_dual_solution().shape[0] ) + if self.quadratic_constraints: + self._set_cpp_quadratic_constraints(c_data_model_view) + + cdef void _set_cpp_quadratic_constraints( + self, data_model_view_t[int, double]* c_data_model_view + ): + cdef vector[mps_data_model_t[int, double].quadratic_constraint_t] constraints + cdef mps_data_model_t[int, double].quadratic_constraint_t qc + cdef dict item + cdef size_t i + cdef uintptr_t c_linear_values + cdef uintptr_t c_linear_indices + cdef uintptr_t c_quadratic_values + cdef uintptr_t c_quadratic_row_indices + cdef uintptr_t c_quadratic_col_indices + cdef size_t linear_nnz + cdef size_t quadratic_nnz + + for item in self.quadratic_constraints: + qc.constraint_row_index = item["constraint_row_index"] + qc.constraint_row_name = item["constraint_row_name"].encode("utf-8") + qc.constraint_row_type = ord(item.get("constraint_row_type", "L")) + qc.rhs_value = item["rhs_value"] + + linear_nnz = item["linear_values"].shape[0] + qc.linear_values.resize(linear_nnz) + qc.linear_indices.resize(linear_nnz) + if linear_nnz > 0: + c_linear_values = get_data_ptr(item["linear_values"]) + c_linear_indices = get_data_ptr(item["linear_indices"]) + for i in range(linear_nnz): + qc.linear_values[i] = (c_linear_values)[i] + qc.linear_indices[i] = (c_linear_indices)[i] + + quadratic_nnz = item["quadratic_values"].shape[0] + qc.quadratic_values.resize(quadratic_nnz) + qc.quadratic_row_indices.resize(quadratic_nnz) + qc.quadratic_col_indices.resize(quadratic_nnz) + if quadratic_nnz > 0: + c_quadratic_values = get_data_ptr(item["quadratic_values"]) + c_quadratic_row_indices = get_data_ptr(item["quadratic_row_indices"]) + c_quadratic_col_indices = get_data_ptr(item["quadratic_col_indices"]) + for i in range(quadratic_nnz): + qc.quadratic_values[i] = (c_quadratic_values)[i] + qc.quadratic_row_indices[i] = (c_quadratic_row_indices)[i] + qc.quadratic_col_indices[i] = (c_quadratic_col_indices)[i] + + constraints.push_back(qc) + + c_data_model_view.set_quadratic_constraints(constraints) + def writeMPS(self, user_problem_file): - self.variable_types = type_cast( - self.variable_types, "S1", "variable_types" - ) + n_vars = self.get_variable_lower_bounds().shape[0] + if self.variable_types.shape[0] == 0 and n_vars > 0: + self.variable_types = np.array(["C"] * n_vars, dtype="S1") + else: + self.variable_types = type_cast( + self.variable_types, "S1", "variable_types" + ) self.set_data_model_view() write_mps(self.c_data_model_view.get()[0], user_problem_file.encode('utf-8')) diff --git a/python/cuopt/cuopt/linear_programming/io/parser.pxd b/python/cuopt/cuopt/linear_programming/io/parser.pxd index 402873f9ab..b4bea47d3c 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser.pxd +++ b/python/cuopt/cuopt/linear_programming/io/parser.pxd @@ -15,6 +15,17 @@ from libcpp.vector cimport vector cdef extern from "cuopt/linear_programming/io/mps_data_model.hpp" namespace "cuopt::linear_programming::io": # noqa cdef cppclass mps_data_model_t[i_t, f_t]: + cppclass quadratic_constraint_t: + int constraint_row_index + string constraint_row_name + char constraint_row_type + vector[double] linear_values + vector[int] linear_indices + double rhs_value + vector[int] quadratic_row_indices + vector[int] quadratic_col_indices + vector[double] quadratic_values + bool maximize_ vector[f_t] A_ vector[i_t] A_indices_ @@ -36,6 +47,7 @@ cdef extern from "cuopt/linear_programming/io/mps_data_model.hpp" namespace "cuo vector[char] row_types_ string objective_name_ string problem_name_ + const vector[quadratic_constraint_t]& get_quadratic_constraints() const cdef extern from "cuopt/linear_programming/io/utilities/cython_parser.hpp" namespace "cuopt::cython": # noqa diff --git a/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx index 61e10b1864..3cc0e1a110 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx @@ -135,6 +135,53 @@ cdef _marshal_data_model(mps_data_model_t[int, double]* dm, data_model): data_model.set_objective_name(dm.objective_name_.decode()) data_model.set_problem_name(dm.problem_name_.decode()) + cdef size_t qi + cdef size_t n_qc = dm.get_quadratic_constraints().size() + cdef mps_data_model_t[int, double].quadratic_constraint_t qc + cdef size_t linear_nnz, quadratic_nnz + cdef double[:] linear_values_view + cdef int[:] linear_indices_view + cdef double[:] quadratic_values_view + cdef int[:] quadratic_row_indices_view + cdef int[:] quadratic_col_indices_view + + for qi in range(n_qc): + qc = dm.get_quadratic_constraints()[qi] + linear_nnz = qc.linear_values.size() + if linear_nnz > 0: + linear_values_view = qc.linear_values.data() + linear_values = np.asarray(linear_values_view).copy() + linear_indices_view = qc.linear_indices.data() + linear_indices = np.asarray(linear_indices_view).copy() + else: + linear_values = None + linear_indices = None + + quadratic_nnz = qc.quadratic_values.size() + if quadratic_nnz > 0: + quadratic_values_view = qc.quadratic_values.data() + quadratic_values = np.asarray(quadratic_values_view).copy() + quadratic_row_indices_view = qc.quadratic_row_indices.data() + quadratic_row_indices = np.asarray(quadratic_row_indices_view).copy() + quadratic_col_indices_view = qc.quadratic_col_indices.data() + quadratic_col_indices = np.asarray(quadratic_col_indices_view).copy() + else: + quadratic_values = None + quadratic_row_indices = None + quadratic_col_indices = None + + data_model.add_quadratic_constraint( + qc.constraint_row_index, + qc.constraint_row_name.decode("utf-8"), + linear_values=linear_values, + linear_indices=linear_indices, + rhs_value=qc.rhs_value, + quadratic_values=quadratic_values, + quadratic_row_indices=quadratic_row_indices, + quadratic_col_indices=quadratic_col_indices, + sense=chr(qc.constraint_row_type), + ) + return data_model diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 25d9d634da..84ca43c5b5 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -256,8 +256,10 @@ def __mul__(self, other): qvars1 = [self] * len(other.vars) qvars2 = other.vars qcoeffs = other.coefficients - vars = [self] - coeffs = [other.constant] + vars, coeffs = [], [] + if other.constant != 0.0: + vars = [self] + coeffs = [other.constant] return QuadraticExpression( qvars1=qvars1, qvars2=qvars2, @@ -283,6 +285,8 @@ def __le__(self, other): # var1 <= var2 -> var1 - var2 <= 0 expr = self - other return Constraint(expr, LE, 0.0) + case QuadraticExpression(): + return Constraint(self - other, LE, 0.0) case _: raise ValueError("Unsupported operation") @@ -295,6 +299,8 @@ def __ge__(self, other): # var1 >= var2 -> var1 - var2 >= 0 expr = self - other return Constraint(expr, GE, 0.0) + case QuadraticExpression(): + return Constraint(self - other, GE, 0.0) case _: raise ValueError("Unsupported operation") @@ -314,8 +320,9 @@ def __eq__(self, other): class QuadraticExpression: """ QuadraticExpressions contain quadratic terms, linear terms, and a constant. - QuadraticExpressions can be used to create quadratic objectives in - the Problem. + Use them for quadratic objectives (``Problem.setObjective``) or quadratic + constraints via ``<=`` or ``>=`` comparisons passed to + :py:meth:`Problem.addConstraint` (equality is not supported). QuadraticExpressions can be added and subtracted with other QuadraticExpressions, LinearExpressions, and Variables, and can also be multiplied and divided by scalars. @@ -734,6 +741,9 @@ def __rsub__(self, other): # other - self -> other + self * -1.0 return other + self * -1.0 + def __neg__(self): + return self * -1.0 + def __imul__(self, other): # Compute expr *= constant match other: @@ -834,13 +844,91 @@ def __truediv__(self, other): ) def __le__(self, other): - raise Exception("Quadratic constraints not supported") + match other: + case int() | float(): + return Constraint(self, LE, float(other)) + case Variable() | LinearExpression() | QuadraticExpression(): + return Constraint(self - other, LE, 0.0) + case _: + raise ValueError( + "Can't compare QuadraticExpression with type %s" + % type(other).__name__ + ) def __ge__(self, other): - raise Exception("Quadratic constraints not supported") + match other: + case int() | float(): + return Constraint(self, GE, float(other)) + case Variable() | LinearExpression() | QuadraticExpression(): + return Constraint(self - other, GE, 0.0) + case _: + raise ValueError( + "Can't compare QuadraticExpression with type %s" + % type(other).__name__ + ) def __eq__(self, other): - raise Exception("Quadratic constraints not supported") + raise ValueError("Equality constraints are not supported.") + + +def _quadratic_expression_to_qcmatrix(expr, rhs): + """Build QCMATRIX COO data for a quadratic row ``expr`` sense ``rhs``. + + Used for both ``<=`` (``LE``) and ``>=`` (``GE``); row sense is stored on + ``Constraint.Sense``, not in this helper. The constant term is moved to + ``rhs_value``. + + Duplicate linear variable indices and duplicate Q (row, col) triplets are + merged by summing coefficients, matching linear ``Constraint`` behavior. + """ + rhs_value = float(rhs) - expr.constant + + linear_coeff = {} + for var, coeff in zip(expr.vars, expr.coefficients): + if coeff == 0.0: + continue + idx = var.index + linear_coeff[idx] = linear_coeff.get(idx, 0.0) + coeff + + linear_indices = [] + linear_values = [] + for idx in sorted(linear_coeff): + coeff = linear_coeff[idx] + if coeff != 0.0: + linear_indices.append(idx) + linear_values.append(coeff) + + quad_coeff = {} + for var1, var2, coeff in zip(expr.qvars1, expr.qvars2, expr.qcoefficients): + if coeff == 0.0: + continue + key = (var1.index, var2.index) + quad_coeff[key] = quad_coeff.get(key, 0.0) + coeff + if expr.qmatrix is not None: + q_coo = expr.qmatrix.tocoo() + for row, col, value in zip(q_coo.row, q_coo.col, q_coo.data): + if value == 0.0: + continue + key = (expr.qvars[row].index, expr.qvars[col].index) + quad_coeff[key] = quad_coeff.get(key, 0.0) + value + + quadratic_row_indices = [] + quadratic_col_indices = [] + quadratic_values = [] + for (row, col), value in sorted(quad_coeff.items()): + if value != 0.0: + quadratic_row_indices.append(row) + quadratic_col_indices.append(col) + quadratic_values.append(value) + + return ( + linear_values, + linear_indices, + quadratic_values, + quadratic_row_indices, + quadratic_col_indices, + rhs_value, + ) class LinearExpression: @@ -1150,6 +1238,8 @@ def __le__(self, other): # expr1 <= expr2 -> expr1 - expr2 <= 0 expr = self - other return Constraint(expr, LE, 0.0) + case QuadraticExpression(): + return Constraint(self - other, LE, 0.0) def __ge__(self, other): match other: @@ -1159,6 +1249,8 @@ def __ge__(self, other): # expr1 >= expr2 -> expr1 - expr2 >= 0 expr = self - other return Constraint(expr, GE, 0.0) + case QuadraticExpression(): + return Constraint(self - other, GE, 0.0) def __eq__(self, other): match other: @@ -1172,16 +1264,15 @@ def __eq__(self, other): class Constraint: """ - cuOpt constraint object containing a linear expression, - the sense of the constraint, and the right-hand side of - the constraint. - Constraints are associated with a problem and can be - created using :py:meth:`Problem.addConstraint`. + cuOpt constraint object containing a linear or quadratic (QCMATRIX) + expression, the sense of the constraint, and the right-hand side. + Constraints are associated with a problem and can be created using + :py:meth:`Problem.addConstraint`. Parameters ---------- - expr : LinearExpression - Linear expression corresponding to a problem. + expr : LinearExpression or QuadraticExpression + Expression corresponding to the constraint. sense : enum Sense of the constraint. Either LE for <=, GE for >= or EQ for == . @@ -1197,7 +1288,9 @@ class Constraint: Sense : LE, GE or EQ Row sense. LE for <=, GE for >= or EQ for == . RHS : float - Constraint right-hand side value. + Constraint right-hand side value (linear rows). + is_quadratic : bool + True when the row is exported as a QCMATRIX quadratic constraint. Slack : float Computed LHS - RHS with current solution. DualValue : float @@ -1205,10 +1298,41 @@ class Constraint: """ def __init__(self, expr, sense, rhs, name=""): + self.index = -1 + self.Sense = sense + self.ConstraintName = name + self.DualValue = float("nan") + self.Slack = float("nan") + + if isinstance(expr, QuadraticExpression): + self.is_quadratic = True + ( + linear_values, + linear_indices, + quadratic_values, + quadratic_row_indices, + quadratic_col_indices, + rhs_value, + ) = _quadratic_expression_to_qcmatrix(expr, rhs) + self.linear_values = np.array(linear_values, dtype=np.float64) + self.linear_indices = np.array(linear_indices, dtype=np.int32) + self.quadratic_values = np.array(quadratic_values, dtype=np.float64) + self.quadratic_row_indices = np.array( + quadratic_row_indices, dtype=np.int32 + ) + self.quadratic_col_indices = np.array( + quadratic_col_indices, dtype=np.int32 + ) + self.rhs_value = rhs_value + self.RHS = rhs_value + self.vindex_coeff_dict = {} + self.vars = expr.vars + return + + self.is_quadratic = False self.vindex_coeff_dict = {} nz = len(expr) self.vars = expr.vars - self.index = -1 for i in range(nz): v_idx = expr.vars[i].index v_coeff = expr.coefficients[i] @@ -1217,11 +1341,7 @@ def __init__(self, expr, sense, rhs, name=""): if v_idx in self.vindex_coeff_dict else v_coeff ) - self.Sense = sense self.RHS = rhs - expr.getConstant() - self.ConstraintName = name - self.DualValue = float("nan") - self.Slack = float("nan") def __len__(self): return len(self.vindex_coeff_dict) @@ -1254,9 +1374,12 @@ def getCoefficient(self, var): def compute_slack(self): # Computes the constraint Slack in the current solution. - lhs = 0.0 - for var in self.vars: - lhs += var.Value * self.vindex_coeff_dict[var.index] + index_to_var = {var.index: var for var in self.vars} + lhs = sum( + index_to_var[v_idx].Value * coeff + for v_idx, coeff in self.vindex_coeff_dict.items() + ) + return self.RHS - lhs @@ -1401,6 +1524,8 @@ def _to_data_model(self): "values": [], } for constr in self.constrs: + if constr.is_quadratic: + continue csr_dict["column_indices"].extend( list(constr.vindex_coeff_dict.keys()) ) @@ -1420,6 +1545,8 @@ def _to_data_model(self): else: for constr in self.constrs: + if constr.is_quadratic: + continue self.rhs.append(constr.RHS) self.row_sense.append(constr.Sense) @@ -1463,6 +1590,27 @@ def _to_data_model(self): dm.set_row_names(self.row_names) dm.set_problem_name(self.Name) + linear_constr_count = sum(1 for c in self.constrs if not c.is_quadratic) + quad_index = 0 + for constr in self.constrs: + if not constr.is_quadratic: + continue + row_name = constr.ConstraintName + if row_name == "": + row_name = "Q" + str(constr.index) + dm.add_quadratic_constraint( + constraint_row_index=linear_constr_count + quad_index, + constraint_row_name=row_name, + linear_values=constr.linear_values, + linear_indices=constr.linear_indices, + rhs_value=constr.rhs_value, + quadratic_values=constr.quadratic_values, + quadratic_row_indices=constr.quadratic_row_indices, + quadratic_col_indices=constr.quadratic_col_indices, + sense=constr.Sense, + ) + quad_index += 1 + self.model = dm def update(self): @@ -1530,15 +1678,15 @@ def addVariable( def addConstraint(self, constr, name=""): """ Adds a constraint to the problem defined by constraint object - and name. A constraint is generated using LinearExpression, - Sense and RHS. + and name. A constraint is generated using LinearExpression or + QuadraticExpression comparisons (``<=``, ``>=``, or ``==``). Parameters ---------- constr : :py:class:`Constraint` - Constructed using LinearExpressions (See Examples) + Constructed using expression comparisons (see Examples). name : string - Name of the variable. Optional. + Name of the constraint. Optional. Examples -------- @@ -1548,6 +1696,7 @@ def addConstraint(self, constr, name=""): >>> problem.addConstraint(2*x - 3*y <= 10, name="Constr1") >>> expr = 3*x + y >>> problem.addConstraint(expr + x == 20, name="Constr2") + >>> problem.addConstraint(-x*x + y*y <= 0, name="soc") """ if self.solved: self.reset_solved_values() # Reset all solved values @@ -1587,6 +1736,10 @@ def updateConstraint(self, constr, coeffs=[], rhs=None): """ self.reset_solved_values() if isinstance(constr, Constraint): + if constr.is_quadratic: + raise ValueError( + "updateConstraint applies to linear constraints only" + ) if isinstance(coeffs, dict): coeffs = coeffs.items() for var, coeff in coeffs: @@ -1827,6 +1980,12 @@ def NumConstraints(self): # Returns number of contraints in the problem. return len(self.constrs) + def getQuadraticConstraints(self): + """ + Returns all quadratic (QCMATRIX) constraints in the problem. + """ + return [c for c in self.constrs if c.is_quadratic] + @property def NumNZs(self): # Returns number of non-zeros in the problem. @@ -1873,6 +2032,8 @@ def getCSR(self): return self.dict_to_object(self.constraint_csr_matrix) csr_dict = {"row_pointers": [0], "column_indices": [], "values": []} for constr in self.constrs: + if constr.is_quadratic: + continue csr_dict["column_indices"].extend( list(constr.vindex_coeff_dict.keys()) ) @@ -1949,10 +2110,14 @@ def populate_solution(self, solution): dual_sol = None if not IsMIP: dual_sol = solution.get_dual_solution() - for i, constr in enumerate(self.constrs): - if dual_sol is not None and len(dual_sol) > 0: - constr.DualValue = dual_sol[i] + linear_row = 0 + for constr in self.constrs: + if constr.is_quadratic: + continue + if dual_sol is not None and len(dual_sol) > linear_row: + constr.DualValue = dual_sol[linear_row] constr.Slack = constr.compute_slack() + linear_row += 1 self.solved = True def solve(self, settings=solver_settings.SolverSettings()): @@ -1977,3 +2142,4 @@ def solve(self, settings=solver_settings.SolverSettings()): solution = solver.Solve(self.model, settings) # Post Solve self.populate_solution(solution) + return solution diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 66242550aa..749601926d 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -150,6 +150,16 @@ def test_model(): assert prob.ObjValue == pytest.approx(5 * x.Value + 3 * y.Value + 70) +def test_constraint_duplicate_terms_slack(): + """Merged coeffs in vindex_coeff_dict must not be double-counted in slack.""" + prob = Problem() + x = prob.addVariable() + c = prob.addConstraint(5 * x + 7 * x <= 18) + assert c.getCoefficient(x) == 12 + x.Value = 1.0 + assert c.compute_slack() == pytest.approx(6.0) + + def test_semi_continuous_variable(): prob = Problem("Semi-continuous") x = prob.addVariable(lb=5.0, ub=10.0, vtype=SEMI_CONTINUOUS, name="x") diff --git a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py new file mode 100644 index 0000000000..64b94182e2 --- /dev/null +++ b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_barrier.py @@ -0,0 +1,425 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Tests for SOCP problems solved via the barrier method. + +Quadratic constraints (QCMATRIX form) are converted to second-order cones and +solved with the barrier interior-point method. +""" + +import numpy as np +import pytest + +from cuopt.linear_programming import data_model, solver, solver_settings +from cuopt.linear_programming.problem import Problem +from cuopt.linear_programming.solver.solver_parameters import ( + CUOPT_BARRIER_RELATIVE_COMPLEMENTARITY_TOLERANCE, + CUOPT_BARRIER_RELATIVE_FEASIBILITY_TOLERANCE, + CUOPT_BARRIER_RELATIVE_OPTIMALITY_TOLERANCE, + CUOPT_METHOD, + CUOPT_PRESOLVE, +) +from cuopt.linear_programming.solver_settings import SolverMethod + + +def _barrier_settings(): + settings = solver_settings.SolverSettings() + settings.set_parameter(CUOPT_METHOD, SolverMethod.Barrier) + settings.set_parameter(CUOPT_PRESOLVE, 0) + return settings + + +def _solve_socp(dm, settings=None): + if settings is None: + settings = _barrier_settings() + return solver.Solve(dm, settings) + + +def _lorentz_qcoo(variable_indices): + """COO triplets for sum_{i>0} x_i^2 - x_0^2 <= 0 on variable_indices.""" + indices = [int(i) for i in variable_indices] + q_values = [-1.0] + [1.0] * (len(indices) - 1) + return ( + np.array(q_values, dtype=np.float64), + np.array(indices, dtype=np.int32), + np.array(indices, dtype=np.int32), + ) + + +def _build_socp_min_x0_model(): + """min x0 s.t. x1=1 and Lorentz cone on (x0,x1,x2); optimal obj=1.""" + dm = data_model.DataModel() + + A_values = np.array([1.0], dtype=np.float64) + A_indices = np.array([1], dtype=np.int32) + A_offsets = np.array([0, 1], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([1.0], dtype=np.float64)) + + dm.set_objective_coefficients(np.array([1.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds( + np.array([np.inf, np.inf, np.inf], dtype=np.float64) + ) + + qv, qr, qc = _lorentz_qcoo([0, 1, 2]) + dm.add_quadratic_constraint( + constraint_row_index=1, + constraint_row_name="soc", + quadratic_values=qv, + quadratic_row_indices=qr, + quadratic_col_indices=qc, + ) + return dm + + +def _rotated_qcoo(variable_indices): + """COO triplets for -2 x_0 x_1 + sum_{i>1} x_i^2 <= 0 on variable_indices.""" + indices = [int(i) for i in variable_indices] + head0, head1 = indices[0], indices[1] + tails = indices[2:] + q_rows = [head0, head1] + tails + q_cols = [head1, head0] + tails + q_values = [-1.0, -1.0] + [1.0] * len(tails) + return ( + np.array(q_values, dtype=np.float64), + np.array(q_rows, dtype=np.int32), + np.array(q_cols, dtype=np.int32), + ) + + +def test_quadratic_constraint_api(): + dm = data_model.DataModel() + + dm.add_quadratic_constraint( + constraint_row_index=0, + constraint_row_name="qc0", + quadratic_values=np.array([-1.0, 1.0, 1.0], dtype=np.float64), + quadratic_row_indices=np.array([0, 1, 2], dtype=np.int32), + quadratic_col_indices=np.array([0, 1, 2], dtype=np.int32), + ) + dm.add_quadratic_constraint( + constraint_row_index=1, + constraint_row_name="qc_ge", + sense="G", + quadratic_values=np.array([1.0, -1.0, -1.0], dtype=np.float64), + quadratic_row_indices=np.array([0, 1, 2], dtype=np.int32), + quadratic_col_indices=np.array([0, 1, 2], dtype=np.int32), + ) + qcs = dm.get_quadratic_constraints() + assert qcs[1]["constraint_row_type"] == "G" + assert qcs[1]["quadratic_values"][0] == pytest.approx(1.0) + with pytest.raises(ValueError, match="Equality constraints are not supported"): + dm.add_quadratic_constraint( + constraint_row_index=2, + sense="E", + quadratic_values=np.array([-1.0], dtype=np.float64), + quadratic_row_indices=np.array([0], dtype=np.int32), + quadratic_col_indices=np.array([0], dtype=np.int32), + ) + dm.clear_quadratic_constraints() + +def test_quadratic_constraint_duplicate_terms_merged(): + """Duplicate linear/Q terms in a quadratic constraint are summed on export.""" + prob = Problem() + x = prob.addVariable(lb=0.0) + y = prob.addVariable(lb=0.0) + c = prob.addConstraint(2 * x * x - y + 5 * x * x + 3 * y <= 18.0) + assert c.is_quadratic + assert c.rhs_value == pytest.approx(18.0) + linear = dict(zip(c.linear_indices.tolist(), c.linear_values.tolist())) + assert linear == {y.index: 2.0} + quad = list( + zip( + c.quadratic_row_indices.tolist(), + c.quadratic_col_indices.tolist(), + c.quadratic_values.tolist(), + ) + ) + assert quad == [(x.index, x.index, 7.0)] + + +def test_quadratic_constraint_problem_solve(): + """Problem.addConstraint + solve (Lorentz via expression).""" + prob = Problem() + x0 = prob.addVariable(lb=0.0, name="x0") + x1 = prob.addVariable(lb=0.0, name="x1") + x2 = prob.addVariable(lb=0.0, name="x2") + prob.addConstraint(x1 == 1.0) + prob.addConstraint(-x0 * x0 + x1 * x1 + x2 * x2 <= 0, name="soc") + prob.setObjective(x0) + + solution = prob.solve(_barrier_settings()) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert len(prob.getQuadraticConstraints()) == 1 + + +def test_quadratic_constraint_duplicate_terms_solve(): + """Duplicate Q terms merge to the same Lorentz row (-x0^2 + x1^2 + x2^2 <= 0).""" + prob = Problem() + x0 = prob.addVariable(lb=0.0, name="x0") + x1 = prob.addVariable(lb=0.0, name="x1") + x2 = prob.addVariable(lb=0.0, name="x2") + prob.addConstraint(x1 == 1.0) + prob.addConstraint( + -4 * x0 * x0 + 3 * x0 * x0 + x1 * x1 + x2 * x2 <= 0, name="soc" + ) + prob.setObjective(x0) + + solution = prob.solve(_barrier_settings()) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) + + +def test_quadratic_constraint_problem_solve_ge_sense(): + """>= quadratic rows are converted to <= form when building SOCP for the barrier.""" + prob = Problem() + x0 = prob.addVariable(lb=0.0, name="x0") + x1 = prob.addVariable(lb=0.0, name="x1") + x2 = prob.addVariable(lb=0.0, name="x2") + prob.addConstraint(x1 == 1.0) + prob.addConstraint(x0 * x0 - x1 * x1 - x2 * x2 >= 0, name="soc") + prob.setObjective(x0) + + solution = prob.solve(_barrier_settings()) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert prob.model.get_quadratic_constraints()[0]["constraint_row_type"] == "G" + + +def test_rotated_second_order_cone_min_tail(): + # minimize x2 + # subject to x0 = 1, x1 = 1 + # 2 x0 x1 >= x2^2 (rotated SOC on (x0, x1, x2)) + # + # Optimal: x* = (1, 1, 0), obj* = 0 + + dm = data_model.DataModel() + + A_values = np.array([1.0, 1.0], dtype=np.float64) + A_indices = np.array([0, 1], dtype=np.int32) + A_offsets = np.array([0, 1, 2], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([1.0, 1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([1.0, 1.0], dtype=np.float64)) + + dm.set_objective_coefficients(np.array([0.0, 0.0, 1.0], dtype=np.float64)) + dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds( + np.array([np.inf, np.inf, np.inf], dtype=np.float64) + ) + + qv, qr, qc = _rotated_qcoo([0, 1, 2]) + dm.add_quadratic_constraint( + constraint_row_index=2, + constraint_row_name="rsoc", + quadratic_values=qv, + quadratic_row_indices=qr, + quadratic_col_indices=qc, + ) + + solution = _solve_socp(dm) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(0.0, abs=1e-3) + x = solution.get_primal_solution() + assert x[0] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[1] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[2]) == pytest.approx(0.0, abs=1e-3) + + +def test_multiple_rotated_second_order_cone_constraints(): + # Two disjoint rotated cones: + # 2 x0 x1 >= x2^2 with x0 = x1 = 1 + # 2 x3 x4 >= x5^2 with x3 = 2, x4 = 1 + # minimize x2 + x5 -> 0 + + dm = data_model.DataModel() + + A_values = np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float64) + A_indices = np.array([0, 1, 3, 4], dtype=np.int32) + A_offsets = np.array([0, 1, 2, 3, 4], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([1.0, 1.0, 2.0, 1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([1.0, 1.0, 2.0, 1.0], dtype=np.float64)) + + dm.set_objective_coefficients( + np.array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0], dtype=np.float64) + ) + dm.set_variable_lower_bounds(np.zeros(6, dtype=np.float64)) + dm.set_variable_upper_bounds(np.full(6, np.inf, dtype=np.float64)) + + qv0, qr0, qc0 = _rotated_qcoo([0, 1, 2]) + dm.add_quadratic_constraint( + constraint_row_index=4, + constraint_row_name="rsoc1", + quadratic_values=qv0, + quadratic_row_indices=qr0, + quadratic_col_indices=qc0, + ) + qv1, qr1, qc1 = _rotated_qcoo([3, 4, 5]) + dm.add_quadratic_constraint( + constraint_row_index=5, + constraint_row_name="rsoc2", + quadratic_values=qv1, + quadratic_row_indices=qr1, + quadratic_col_indices=qc1, + ) + + solution = _solve_socp(dm) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(0.0, abs=1e-3) + x = solution.get_primal_solution() + assert x[0] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[1] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[2]) == pytest.approx(0.0, abs=1e-3) + assert x[3] == pytest.approx(2.0, rel=1e-3, abs=1e-3) + assert x[4] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[5]) == pytest.approx(0.0, abs=1e-3) + + +def test_multiple_quadratic_constraints(): + """Port of C++ mixed_linear_and_two_soc_blocks (solve_barrier.cu). + + Two Lorentz QCMATRIX rows via add_quadratic_constraint. + Variables [l1, l2 | t1, u1, v1 | t2, u2, v2]; Lorentz cones on (t1,u1,v1) and (t2,u2,v2). + + minimize t1 + t2 + subject to l1 - u1 = 0, l2 - u2 = 0, l1 + l2 = 3, l1 - l2 = 1 + + Optimal: l1*=2, l2*=1, t1*=2, u1*=2, v1*=0, t2*=1, u2*=1, v2*=0, obj*=3. + """ + dm = data_model.DataModel() + + A_values = np.array([1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0], dtype=np.float64) + A_indices = np.array([0, 3, 1, 6, 0, 1, 0, 1], dtype=np.int32) + A_offsets = np.array([0, 2, 4, 6, 8], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([0.0, 0.0, 3.0, 1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([0.0, 0.0, 3.0, 1.0], dtype=np.float64)) + + dm.set_objective_coefficients( + np.array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0], dtype=np.float64) + ) + dm.set_variable_lower_bounds(np.zeros(8, dtype=np.float64)) + dm.set_variable_upper_bounds(np.full(8, np.inf, dtype=np.float64)) + + qv0, qr0, qc0 = _lorentz_qcoo([2, 3, 4]) + dm.add_quadratic_constraint( + constraint_row_index=4, + constraint_row_name="soc1", + quadratic_values=qv0, + quadratic_row_indices=qr0, + quadratic_col_indices=qc0, + ) + + qv1, qr1, qc1 = _lorentz_qcoo([5, 6, 7]) + dm.add_quadratic_constraint( + constraint_row_index=5, + constraint_row_name="soc2", + quadratic_values=qv1, + quadratic_row_indices=qr1, + quadratic_col_indices=qc1, + ) + + solution = _solve_socp(dm) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(3.0, rel=1e-3, abs=1e-3) + x = solution.get_primal_solution() + assert x[0] == pytest.approx(2.0, rel=1e-3, abs=1e-3) + assert x[1] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[2] == pytest.approx(2.0, rel=1e-3, abs=1e-3) + assert x[3] == pytest.approx(2.0, rel=1e-3, abs=1e-3) + assert abs(x[4]) == pytest.approx(0.0, abs=1e-3) + assert x[5] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[6] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[7]) == pytest.approx(0.0, abs=1e-3) + + +def test_socp_min_x0_norm_constraint(): + # minimize x0 + # subject to x1 = 1 + # (x0, x1, x2) in Lorentz cone (x0^2 >= x1^2 + x2^2, x0 >= 0) + # + # Optimal: x* = (1, 1, 0), obj* = 1 + + solution = _solve_socp(_build_socp_min_x0_model()) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) + x = solution.get_primal_solution() + assert x[0] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[1] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[2]) == pytest.approx(0.0, abs=1e-3) + + +def test_socp_mixed_linear_and_cone_block(): + # Variables [l, t, u, v]; Lorentz cone on (t, u, v). + # + # minimize l + # subject to l - t = 0 + # u = 1 + # (t, u, v) in Lorentz cone + # + # Optimal: l* = 1, t* = 1, u* = 1, v* = 0, obj* = 1. + + dm = data_model.DataModel() + + A_values = np.array([1.0, -1.0, 1.0], dtype=np.float64) + A_indices = np.array([0, 1, 2], dtype=np.int32) + A_offsets = np.array([0, 2, 3], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([0.0, 1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([0.0, 1.0], dtype=np.float64)) + + dm.set_objective_coefficients( + np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float64) + ) + dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds( + np.array([np.inf, np.inf, np.inf, np.inf], dtype=np.float64) + ) + + qv, qr, qc = _lorentz_qcoo([1, 2, 3]) + dm.add_quadratic_constraint( + constraint_row_index=2, + constraint_row_name="soc", + quadratic_values=qv, + quadratic_row_indices=qr, + quadratic_col_indices=qc, + ) + + solution = _solve_socp(dm) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) + x = solution.get_primal_solution() + assert x[0] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[1] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert x[2] == pytest.approx(1.0, rel=1e-3, abs=1e-3) + assert abs(x[3]) == pytest.approx(0.0, abs=1e-3) + + +def test_socp_barrier_relative_tolerances(): + settings = _barrier_settings() + settings.set_parameter(CUOPT_BARRIER_RELATIVE_FEASIBILITY_TOLERANCE, 1e-10) + settings.set_parameter(CUOPT_BARRIER_RELATIVE_OPTIMALITY_TOLERANCE, 1e-10) + settings.set_parameter(CUOPT_BARRIER_RELATIVE_COMPLEMENTARITY_TOLERANCE, 1e-10) + + solution = solver.Solve(_build_socp_min_x0_model(), settings) + + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-4, abs=1e-4) diff --git a/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py new file mode 100644 index 0000000000..549ef99b7d --- /dev/null +++ b/python/cuopt/cuopt/tests/second_order_cone_programming/test_socp_parser.py @@ -0,0 +1,147 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Tests for SOCP / QCMATRIX MPS I/O: writeMPS, ParseMps, and external dataset parsing. +""" + +import os + +import numpy as np +import pytest + +from cuopt.linear_programming import data_model, io as mps_parser, solver, solver_settings +from cuopt.linear_programming.solver.solver_parameters import ( + CUOPT_METHOD, + CUOPT_PRESOLVE, +) +from cuopt.linear_programming.solver_settings import SolverMethod + +RAPIDS_DATASET_ROOT_DIR = os.getenv("RAPIDS_DATASET_ROOT_DIR") +if RAPIDS_DATASET_ROOT_DIR is None: + RAPIDS_DATASET_ROOT_DIR = os.path.join(os.getcwd(), "datasets") + + +def _barrier_settings(): + settings = solver_settings.SolverSettings() + settings.set_parameter(CUOPT_METHOD, SolverMethod.Barrier) + settings.set_parameter(CUOPT_PRESOLVE, 0) + return settings + + +def _solve_socp(dm, settings=None): + if settings is None: + settings = _barrier_settings() + return solver.Solve(dm, settings) + + +def _lorentz_qcoo(variable_indices): + """COO triplets for sum_{i>0} x_i^2 - x_0^2 <= 0 on variable_indices.""" + indices = [int(i) for i in variable_indices] + q_values = [-1.0] + [1.0] * (len(indices) - 1) + return ( + np.array(q_values, dtype=np.float64), + np.array(indices, dtype=np.int32), + np.array(indices, dtype=np.int32), + ) + + +def _build_socp_min_x0_model(): + """min x0 s.t. x1=1 and Lorentz cone on (x0,x1,x2); optimal obj=1.""" + dm = data_model.DataModel() + + A_values = np.array([1.0], dtype=np.float64) + A_indices = np.array([1], dtype=np.int32) + A_offsets = np.array([0, 1], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([1.0], dtype=np.float64)) + + dm.set_objective_coefficients(np.array([1.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds( + np.array([np.inf, np.inf, np.inf], dtype=np.float64) + ) + + qv, qr, qc = _lorentz_qcoo([0, 1, 2]) + dm.add_quadratic_constraint( + constraint_row_index=1, + constraint_row_name="soc", + quadratic_values=qv, + quadratic_row_indices=qr, + quadratic_col_indices=qc, + ) + return dm + + +def test_socp_mps_write_parse_solve(tmp_path): + """writeMPS -> ParseMps preserves QCMATRIX and barrier solve.""" + dm = _build_socp_min_x0_model() + mps_file = tmp_path / "socp_lorentz.mps" + dm.writeMPS(str(mps_file)) + + dm2 = mps_parser.ParseMps(str(mps_file)) + qcs = dm2.get_quadratic_constraints() + assert len(qcs) == 1 + assert qcs[0]["constraint_row_name"] == "soc" + assert qcs[0]["constraint_row_type"] == "L" + + solution = _solve_socp(dm2) + assert solution.get_termination_reason() == "Optimal" + assert solution.get_primal_objective() == pytest.approx(1.0, rel=1e-3, abs=1e-3) + + +def test_socp_mps_write_parse_preserves_ge_sense(tmp_path): + """writeMPS -> ParseMps preserves QCMATRIX row sense ('G' for >=).""" + dm = data_model.DataModel() + + A_values = np.array([1.0], dtype=np.float64) + A_indices = np.array([1], dtype=np.int32) + A_offsets = np.array([0, 1], dtype=np.int32) + dm.set_csr_constraint_matrix(A_values, A_indices, A_offsets) + + dm.set_constraint_lower_bounds(np.array([1.0], dtype=np.float64)) + dm.set_constraint_upper_bounds(np.array([1.0], dtype=np.float64)) + + dm.set_objective_coefficients(np.array([1.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_lower_bounds(np.array([0.0, 0.0, 0.0], dtype=np.float64)) + dm.set_variable_upper_bounds( + np.array([np.inf, np.inf, np.inf], dtype=np.float64) + ) + + # x0^2 - x1^2 - x2^2 >= 0 (MPS row type 'G') + dm.add_quadratic_constraint( + constraint_row_index=1, + constraint_row_name="soc_ge", + sense="G", + quadratic_values=np.array([1.0, -1.0, -1.0], dtype=np.float64), + quadratic_row_indices=np.array([0, 1, 2], dtype=np.int32), + quadratic_col_indices=np.array([0, 1, 2], dtype=np.int32), + ) + + mps_file = tmp_path / "socp_ge.mps" + dm.writeMPS(str(mps_file)) + + dm2 = mps_parser.ParseMps(str(mps_file)) + qcs = dm2.get_quadratic_constraints() + assert len(qcs) == 1 + assert qcs[0]["constraint_row_name"] == "soc_ge" + assert qcs[0]["constraint_row_type"] == "G" + + +def test_parse_qcmatrix_dataset_mps(): + """Parse external QCQP MPS with QCMATRIX when dataset is available.""" + mps_path = os.path.join(RAPIDS_DATASET_ROOT_DIR, "qcqp", "QC_Test_1.mps") + if not os.path.exists(mps_path): + pytest.skip(f"{mps_path} not in dataset root") + + dm = mps_parser.ParseMps(mps_path) + qcs = dm.get_quadratic_constraints() + assert len(qcs) == 2 + assert qcs[0]["constraint_row_name"] == "QC0" + assert qcs[1]["constraint_row_name"] == "QC1" + for qc in qcs: + assert qc["constraint_row_type"] == "L" + assert qc["quadratic_row_indices"].shape == qc["quadratic_col_indices"].shape + assert qc["quadratic_values"].shape == qc["quadratic_row_indices"].shape diff --git a/python/cuopt/pyproject.toml b/python/cuopt/pyproject.toml index 4c1de77c81..d32557ca48 100644 --- a/python/cuopt/pyproject.toml +++ b/python/cuopt/pyproject.toml @@ -20,17 +20,17 @@ license = "Apache-2.0" requires-python = ">=3.11" dependencies = [ "cuda-python>=13.0.1,<14.0", - "cudf==26.6.*,>=0.0.0a0", + "cudf==26.8.*,>=0.0.0a0", "cupy-cuda13x>=13.6.0", - "libcuopt==26.6.*,>=0.0.0a0", + "libcuopt==26.8.*,>=0.0.0a0", "numba-cuda>=0.22.1", "numba>=0.60.0,<0.65.0", "numpy>=1.23.5,<3.0", "pandas>=2.0", - "pylibraft==26.6.*,>=0.0.0a0", + "pylibraft==26.8.*,>=0.0.0a0", "pyyaml>=6.0.0", "rapids-logger==0.2.*,>=0.0.0a0", - "rmm==26.6.*,>=0.0.0a0", + "rmm==26.8.*,>=0.0.0a0", "scipy>=1.14.1", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ @@ -103,9 +103,9 @@ requires = [ "cmake>=4.0", "cupy-cuda13x>=13.6.0", "cython>=3.0.3", - "libcuopt==26.6.*,>=0.0.0a0", + "libcuopt==26.8.*,>=0.0.0a0", "ninja", - "pylibraft==26.6.*,>=0.0.0a0", + "pylibraft==26.8.*,>=0.0.0a0", "rapids-logger==0.2.*,>=0.0.0a0", - "rmm==26.6.*,>=0.0.0a0", + "rmm==26.8.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. diff --git a/python/cuopt_self_hosted/pyproject.toml b/python/cuopt_self_hosted/pyproject.toml index 4de47de72e..74a665319b 100644 --- a/python/cuopt_self_hosted/pyproject.toml +++ b/python/cuopt_self_hosted/pyproject.toml @@ -40,7 +40,7 @@ test = [ "pytest<9.0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. mps = [ - "cuopt-cu13==26.6.*,>=0.0.0a0", + "cuopt-cu13==26.8.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [project.urls] diff --git a/python/cuopt_server/pyproject.toml b/python/cuopt_server/pyproject.toml index 4f9f141011..cd054a080e 100644 --- a/python/cuopt_server/pyproject.toml +++ b/python/cuopt_server/pyproject.toml @@ -21,7 +21,7 @@ license = "Apache-2.0" license-files = ["LICENSE"] requires-python = ">=3.11" dependencies = [ - "cuopt==26.6.*,>=0.0.0a0", + "cuopt==26.8.*,>=0.0.0a0", "cupy-cuda13x>=13.6.0", "fastapi", "jsonref==1.1.0", diff --git a/python/libcuopt/pyproject.toml b/python/libcuopt/pyproject.toml index 0fe5dfb8df..b5190ec719 100644 --- a/python/libcuopt/pyproject.toml +++ b/python/libcuopt/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ ] dependencies = [ "cuda-toolkit[cublas,cudart,curand,cusolver,cusparse,nvtx]==13.*", - "librmm==26.6.*,>=0.0.0a0", + "librmm==26.8.*,>=0.0.0a0", "nvidia-cudss-cu13", "nvidia-nvjitlink>=13.0,<14", "rapids-logger==0.2.*,>=0.0.0a0", @@ -76,7 +76,7 @@ dependencies-file = "../../dependencies.yaml" matrix-entry = "cuda_suffixed=true;use_cuda_wheels=true" requires = [ "cmake>=4.0", - "librmm==26.6.*,>=0.0.0a0", + "librmm==26.8.*,>=0.0.0a0", "ninja", "rapids-logger==0.2.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. diff --git a/skills/cuopt-developer/SKILL.md b/skills/cuopt-developer/SKILL.md index 34a2cf0502..2490a517ea 100644 --- a/skills/cuopt-developer/SKILL.md +++ b/skills/cuopt-developer/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-developer -version: "26.06.00" +version: "26.08.00" description: Modify, build, test, debug, and contribute to NVIDIA cuOpt (C++/CUDA, Python, server, CI). Use for solver internals, PRs, DCO, and code conventions. --- diff --git a/skills/cuopt-install/SKILL.md b/skills/cuopt-install/SKILL.md index d2aef94715..6220c341ce 100644 --- a/skills/cuopt-install/SKILL.md +++ b/skills/cuopt-install/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-install -version: "26.06.00" +version: "26.08.00" description: Install cuOpt for Python, C, or as a server (pip, conda, Docker) — system requirements, install commands, and verification. Use when the user wants to install or verify cuOpt for any user-facing interface. For building cuOpt from source or contributing to cuOpt, see cuopt-developer. --- diff --git a/skills/cuopt-numerical-optimization-api-c/SKILL.md b/skills/cuopt-numerical-optimization-api-c/SKILL.md index 46c7173787..eea2ab5c4d 100644 --- a/skills/cuopt-numerical-optimization-api-c/SKILL.md +++ b/skills/cuopt-numerical-optimization-api-c/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-numerical-optimization-api-c -version: "26.06.00" +version: "26.08.00" description: LP, MILP, and QP (beta) with cuOpt — C API only. Use when the user is embedding LP, MILP, or QP in C/C++. --- diff --git a/skills/cuopt-numerical-optimization-api-cli/SKILL.md b/skills/cuopt-numerical-optimization-api-cli/SKILL.md index a9e4bcc3eb..dacd0a23a7 100644 --- a/skills/cuopt-numerical-optimization-api-cli/SKILL.md +++ b/skills/cuopt-numerical-optimization-api-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-numerical-optimization-api-cli -version: "26.06.00" +version: "26.08.00" description: LP, MILP, and QP (beta) with cuOpt — CLI only (MPS files, cuopt_cli). Use when the user is solving LP, MILP, or QP from MPS via command line. --- diff --git a/skills/cuopt-numerical-optimization-api-python/SKILL.md b/skills/cuopt-numerical-optimization-api-python/SKILL.md index 35800ea2c6..44b3e00f30 100644 --- a/skills/cuopt-numerical-optimization-api-python/SKILL.md +++ b/skills/cuopt-numerical-optimization-api-python/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-numerical-optimization-api-python -version: "26.06.00" +version: "26.08.00" description: Solve Linear Programming (LP), Mixed-Integer Linear Programming (MILP), and Quadratic Programming (QP, beta) with the Python API. Use when the user asks about optimization with linear or quadratic objectives, linear constraints, integer variables, scheduling, resource allocation, facility location, production planning, portfolio optimization, or least squares. --- diff --git a/skills/cuopt-routing-api-python/SKILL.md b/skills/cuopt-routing-api-python/SKILL.md index 5307776c96..a8bc2393a8 100644 --- a/skills/cuopt-routing-api-python/SKILL.md +++ b/skills/cuopt-routing-api-python/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-routing-api-python -version: "26.06.00" +version: "26.08.00" description: Vehicle routing (VRP, TSP, PDP) with cuOpt — Python API only. Use when the user is building or solving routing in Python. --- diff --git a/skills/cuopt-server-api-python/SKILL.md b/skills/cuopt-server-api-python/SKILL.md index ea3064efb5..fef3cbb056 100644 --- a/skills/cuopt-server-api-python/SKILL.md +++ b/skills/cuopt-server-api-python/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-server-api-python -version: "26.06.00" +version: "26.08.00" description: cuOpt REST server — start server, endpoints, Python/curl client examples. Use when the user is deploying or calling the REST API. --- diff --git a/skills/cuopt-server-common/SKILL.md b/skills/cuopt-server-common/SKILL.md index 62842bb5b4..3f86a96825 100644 --- a/skills/cuopt-server-common/SKILL.md +++ b/skills/cuopt-server-common/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-server-common -version: "26.06.00" +version: "26.08.00" description: cuOpt REST server — what it does and how requests flow. Domain concepts; no deploy or client code. --- diff --git a/skills/cuopt-user-rules/SKILL.md b/skills/cuopt-user-rules/SKILL.md index 02ebb3888c..47ce33a4e4 100644 --- a/skills/cuopt-user-rules/SKILL.md +++ b/skills/cuopt-user-rules/SKILL.md @@ -1,6 +1,6 @@ --- name: cuopt-user-rules -version: "26.06.00" +version: "26.08.00" description: Base rules for end users calling NVIDIA cuOpt (routing/LP/MILP/QP/install/server). Not for cuOpt internals — use cuopt-developer for those. --- diff --git a/skills/numerical-optimization-formulation/SKILL.md b/skills/numerical-optimization-formulation/SKILL.md index d4f88f89bc..fa21ca6ae4 100644 --- a/skills/numerical-optimization-formulation/SKILL.md +++ b/skills/numerical-optimization-formulation/SKILL.md @@ -1,6 +1,6 @@ --- name: numerical-optimization-formulation -version: "26.06.00" +version: "26.08.00" description: Numerical optimization (LP, MILP, QP) — concepts, problem-text parsing, and formulation patterns. What LP, MILP, and QP are, required formulation questions, modeling elements, common patterns, and how to parse problem statements (parameters, constraints, decisions, objective). Domain concepts; no API or interface. --- diff --git a/skills/routing-formulation/SKILL.md b/skills/routing-formulation/SKILL.md index 293fbe940c..d755f9037d 100644 --- a/skills/routing-formulation/SKILL.md +++ b/skills/routing-formulation/SKILL.md @@ -1,6 +1,6 @@ --- name: routing-formulation -version: "26.06.00" +version: "26.08.00" description: Vehicle routing (VRP, TSP, PDP) — problem types and data requirements. Domain concepts; no API or interface. --- diff --git a/skills/skill-evolution/SKILL.md b/skills/skill-evolution/SKILL.md index 84c893bf02..9c2dbc5b9e 100644 --- a/skills/skill-evolution/SKILL.md +++ b/skills/skill-evolution/SKILL.md @@ -1,6 +1,6 @@ --- name: skill-evolution -version: "26.06.00" +version: "26.08.00" description: After solving a non-trivial problem, detect generalizable learnings and propose skill updates so future interactions benefit automatically. Always active — applies to every interaction. --- @@ -137,7 +137,7 @@ When skill evolution creates an entirely new skill directory, add `origin: skill ```yaml --- name: new-skill-name -version: "26.06.00" +version: "26.08.00" description: ... origin: skill-evolution --- diff --git a/skills/skill-evolution/skill-card.md b/skills/skill-evolution/skill-card.md index 9a4bccd28e..6978f664ff 100644 --- a/skills/skill-evolution/skill-card.md +++ b/skills/skill-evolution/skill-card.md @@ -8,7 +8,7 @@ This skill is ready for commercial/non-commercial use.
### License/Terms of Use:
Apache 2.0
## Use Case:
-Developers and engineers use this skill to capture generalizable learnings from agent interactions and propose updates to existing skills, ensuring continuous improvement of agent guidance without blocking the user's primary task.
+Developers and engineers use this skill to continuously improve agent skill files by capturing generalizable learnings from problem-solving interactions and proposing structured updates.
### Deployment Geography for Use:
Global
@@ -23,12 +23,12 @@ Mitigation: Review and scan skill before deployment.
## Skill Output:
**Output Type(s):** [Analysis, Code]
-**Output Format:** [Markdown with inline code diffs]
+**Output Format:** [Markdown with inline code blocks]
**Output Parameters:** [1D]
**Other Properties Related to Output:** [None]
## Skill Version(s):
-26.06.00 (source: frontmatter)
+26.08.00 (source: frontmatter)
## Ethical Considerations:
NVIDIA believes Trustworthy AI is a shared responsibility and we have established policies and practices to enable development for a wide array of AI applications. When downloaded or used in accordance with our terms of service, developers should work with their internal team to ensure this skill meets requirements for the relevant industry and use case and addresses unforeseen product misuse.
diff --git a/skills/skill-evolution/skill.oms.sig b/skills/skill-evolution/skill.oms.sig index a814104939..18f66aa4ea 100644 --- a/skills/skill-evolution/skill.oms.sig +++ b/skills/skill-evolution/skill.oms.sig @@ -1 +1 @@ -{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"x509CertificateChain":{"certificates":[{"rawBytes":"MIICgzCCAgmgAwIBAgIUKIyS7SxNteQIiWzK1dWj85E6520wCgYIKoZIzj0EAwMwVTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjEpMCcGA1UEAwwgTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBJQ0EgMDEwHhcNMjYwNDAxMDAwMDAwWhcNMjgwNDIyMTUzMzA5WjBUMQswCQYDVQQGEwJVUzEbMBkGA1UECgwSTlZJRElBIENvcnBvcmF0aW9uMSgwJgYDVQQDDB9OVklESUEgQWdlbnQgU2tpbGxzIFNpZ25pbmcgMDAxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEYoRM9bQl/dGlwSRNi6bTpIJUXH8Nv9GciP6LSflJYYMLCc296kpyuTSsk5ddbAWiDcFX3C/ydX3jwc+qCLYP6uHy9XphyLjOQ27Yb2J6rBLVtRBS1mgGco/Gr7fL6ODco4GaMIGXMB0GA1UdDgQWBBRQ/5ZW3nJ6lmo9SVk7I15o7UGmpTAfBgNVHSMEGDAWgBRPGpILxMBBleJSsBGjrMKsby1CgjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9vY3NwLm5kaXMubnZpZGlhLmNvbTAKBggqhkjOPQQDAwNoADBlAjAUygu/GiOCIXrgGr4SmLgeEVDcEitfFUv7ALbvLVGVyMysB3mxmO/uInZfXzWcJZsCMQDxuoxj4ZmO30jhkPIcCxGFCOvnUsnfU3TfGcouYm4M6iRpbKvtVnHPiy4bi6pcKf0="},{"rawBytes":"MIICiDCCAg6gAwIBAgIUZsIuSv9NkpJCNqtYEfCouVv5BzowCgYIKoZIzj0EAwMwUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTAgFw0yNjA0MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowVTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjEpMCcGA1UEAwwgTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBJQ0EgMDEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASI72cR3ctKGg4VWnB3bNja6g1Z2PnOmFEopkPof+QeIcPk9rT+g9MjJnq51EQXL93a7C2GJ9J985G4o2V85VD7wJ1RaXhluHW2rf3y8bQGeAYaKMr5s/hUgn+M3/9WlWejgaAwgZ0wHQYDVR0OBBYEFE8akgvEwEGV4lKwEaOswqxvLUKCMB8GA1UdIwQYMBaAFItnoAjjfuCEUvzyvWyI2vOGvwPjMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMDcGCCsGAQUFBwEBBCswKTAnBggrBgEFBQcwAYYbaHR0cDovL29jc3AubmRpcy5udmlkaWEuY29tMAoGCCqGSM49BAMDA2gAMGUCMQCeIMMfAbyzPDacw2MxG+Yt1cikrJX/DVxiGfXuHmkkXn6VgSzE79+lkqDErpVO2gYCMCNEColOyvUvkzZGUEI1hQ3PfMgi3FIo9tHoBKMw4/wGBLFpu/0ubtmbBXM6/UMOEw=="},{"rawBytes":"MIICRTCCAcygAwIBAgIUeJdY3rV86EdvFmG7L8LJBsyQFYkwCgYIKoZIzj0EAwMwUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTAgFw0yNjA0MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABAYpiXCDjJ9NT2eSDhyHJVSw1Tbze18cGG2F/578oWvHxg23eQAhNRYdq88i1iOshZSO6C29doKui5Xpmo/7Ctw9Sx4PP2RzOmIuOLCuTdNtKcTRwi4GEsd5BAFvWj42M6NjMGEwHQYDVR0OBBYEFItnoAjjfuCEUvzyvWyI2vOGvwPjMB8GA1UdIwQYMBaAFItnoAjjfuCEUvzyvWyI2vOGvwPjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cAMGQCMCwtAjWLaNwgGWNCgdyNoTyvNhqWRECRJV2r3+7w8g0PL6NHLOsbkgE09BH95h8XlgIwTaQmbbUh2ChAJ5TA1wRiVDnCcvbzHlZl2jM2FcwQQZlk19LOAbyGMRixbu2Ww/rj"}]},"tlogEntries":[]},"dsseEnvelope":{"payload":"ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YxIiwKICAic3ViamVjdCI6IFsKICAgIHsKICAgICAgIm5hbWUiOiAic2tpbGwtZXZvbHV0aW9uIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhMjU2IjogIjhmZGViMGU0ZmE4YmIzY2FjNDVmMTBhZTZkYzZmYTNiNDEzNWIwMmFhOWQyYzVkMjY4NDA0NTMxNzBjNDZlZDMiCiAgICAgIH0KICAgIH0KICBdLAogICJwcmVkaWNhdGVUeXBlIjogImh0dHBzOi8vbW9kZWxfc2lnbmluZy9zaWduYXR1cmUvdjEuMCIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJzZXJpYWxpemF0aW9uIjogewogICAgICAiYWxsb3dfc3ltbGlua3MiOiBmYWxzZSwKICAgICAgImhhc2hfdHlwZSI6ICJzaGEyNTYiLAogICAgICAiaWdub3JlX3BhdGhzIjogWwogICAgICAgICIuZ2l0aHViIiwKICAgICAgICAiLmdpdGlnbm9yZSIsCiAgICAgICAgIi5naXRhdHRyaWJ1dGVzIiwKICAgICAgICAiLmdpdCIKICAgICAgXSwKICAgICAgIm1ldGhvZCI6ICJmaWxlcyIKICAgIH0sCiAgICAicmVzb3VyY2VzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiU0tJTEwubWQiLAogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogIjNjZjM5NDA4NmQ4MmY4MDFmNDk2NzEwYmRjZTJjOTZlZmFhMjEzM2MxZDAwYzAxYzA5N2Q3N2I3MmU3MGI4M2IiCiAgICAgIH0sCiAgICAgIHsKICAgICAgICAibmFtZSI6ICJza2lsbC1jYXJkLm1kIiwKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI2MDlhNzhmYzY3ZTU2NWY0MTc0N2MwYWIwMzgxMGQwZTJjYzk3ODY0NjUwOTAyZDcwNjg5MWVhZmY3OGU5MGEwIgogICAgICB9CiAgICBdCiAgfQp9","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MGUCMEEOy8isxKJRKBjKoyIlCeOC458Vg3EuaXCLD8E/4fmYxxnt2zZifq5NWf9vLfhheAIxALtU3qZWNacF74dg3lrp/BHuGnbg9SvuMQKZRJ9Il8KxNMT/Ud5dfHW+1F3+EpE+BQ==","keyid":""}]}} \ No newline at end of file +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"x509CertificateChain":{"certificates":[{"rawBytes":"MIICgzCCAgmgAwIBAgIUKIyS7SxNteQIiWzK1dWj85E6520wCgYIKoZIzj0EAwMwVTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjEpMCcGA1UEAwwgTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBJQ0EgMDEwHhcNMjYwNDAxMDAwMDAwWhcNMjgwNDIyMTUzMzA5WjBUMQswCQYDVQQGEwJVUzEbMBkGA1UECgwSTlZJRElBIENvcnBvcmF0aW9uMSgwJgYDVQQDDB9OVklESUEgQWdlbnQgU2tpbGxzIFNpZ25pbmcgMDAxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEYoRM9bQl/dGlwSRNi6bTpIJUXH8Nv9GciP6LSflJYYMLCc296kpyuTSsk5ddbAWiDcFX3C/ydX3jwc+qCLYP6uHy9XphyLjOQ27Yb2J6rBLVtRBS1mgGco/Gr7fL6ODco4GaMIGXMB0GA1UdDgQWBBRQ/5ZW3nJ6lmo9SVk7I15o7UGmpTAfBgNVHSMEGDAWgBRPGpILxMBBleJSsBGjrMKsby1CgjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9vY3NwLm5kaXMubnZpZGlhLmNvbTAKBggqhkjOPQQDAwNoADBlAjAUygu/GiOCIXrgGr4SmLgeEVDcEitfFUv7ALbvLVGVyMysB3mxmO/uInZfXzWcJZsCMQDxuoxj4ZmO30jhkPIcCxGFCOvnUsnfU3TfGcouYm4M6iRpbKvtVnHPiy4bi6pcKf0="},{"rawBytes":"MIICiDCCAg6gAwIBAgIUZsIuSv9NkpJCNqtYEfCouVv5BzowCgYIKoZIzj0EAwMwUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTAgFw0yNjA0MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowVTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjEpMCcGA1UEAwwgTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBJQ0EgMDEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASI72cR3ctKGg4VWnB3bNja6g1Z2PnOmFEopkPof+QeIcPk9rT+g9MjJnq51EQXL93a7C2GJ9J985G4o2V85VD7wJ1RaXhluHW2rf3y8bQGeAYaKMr5s/hUgn+M3/9WlWejgaAwgZ0wHQYDVR0OBBYEFE8akgvEwEGV4lKwEaOswqxvLUKCMB8GA1UdIwQYMBaAFItnoAjjfuCEUvzyvWyI2vOGvwPjMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMDcGCCsGAQUFBwEBBCswKTAnBggrBgEFBQcwAYYbaHR0cDovL29jc3AubmRpcy5udmlkaWEuY29tMAoGCCqGSM49BAMDA2gAMGUCMQCeIMMfAbyzPDacw2MxG+Yt1cikrJX/DVxiGfXuHmkkXn6VgSzE79+lkqDErpVO2gYCMCNEColOyvUvkzZGUEI1hQ3PfMgi3FIo9tHoBKMw4/wGBLFpu/0ubtmbBXM6/UMOEw=="},{"rawBytes":"MIICRTCCAcygAwIBAgIUeJdY3rV86EdvFmG7L8LJBsyQFYkwCgYIKoZIzj0EAwMwUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTAgFw0yNjA0MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowUTELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEk5WSURJQSBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcTlZJRElBIEFnZW50IENhcGFiaWxpdGllcyBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABAYpiXCDjJ9NT2eSDhyHJVSw1Tbze18cGG2F/578oWvHxg23eQAhNRYdq88i1iOshZSO6C29doKui5Xpmo/7Ctw9Sx4PP2RzOmIuOLCuTdNtKcTRwi4GEsd5BAFvWj42M6NjMGEwHQYDVR0OBBYEFItnoAjjfuCEUvzyvWyI2vOGvwPjMB8GA1UdIwQYMBaAFItnoAjjfuCEUvzyvWyI2vOGvwPjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cAMGQCMCwtAjWLaNwgGWNCgdyNoTyvNhqWRECRJV2r3+7w8g0PL6NHLOsbkgE09BH95h8XlgIwTaQmbbUh2ChAJ5TA1wRiVDnCcvbzHlZl2jM2FcwQQZlk19LOAbyGMRixbu2Ww/rj"}]},"tlogEntries":[]},"dsseEnvelope":{"payload":"ewogICJfdHlwZSI6ICJodHRwczovL2luLXRvdG8uaW8vU3RhdGVtZW50L3YxIiwKICAic3ViamVjdCI6IFsKICAgIHsKICAgICAgIm5hbWUiOiAic2tpbGwtZXZvbHV0aW9uIiwKICAgICAgImRpZ2VzdCI6IHsKICAgICAgICAic2hhMjU2IjogIjIyYjcxMGEyOWFhYzU0NzJjZGQ0OWFlZDg4MTFhZGMyMjYwZjVjMGExMzk2YWEyNzA4MGM0ODI5ODk4YzhjZGQiCiAgICAgIH0KICAgIH0KICBdLAogICJwcmVkaWNhdGVUeXBlIjogImh0dHBzOi8vbW9kZWxfc2lnbmluZy9zaWduYXR1cmUvdjEuMCIsCiAgInByZWRpY2F0ZSI6IHsKICAgICJyZXNvdXJjZXMiOiBbCiAgICAgIHsKICAgICAgICAiYWxnb3JpdGhtIjogInNoYTI1NiIsCiAgICAgICAgImRpZ2VzdCI6ICI5Y2RjZmJmMTAxN2I4ODVkMTZmNGQ1YzFiZDI5NWQwMjA2MWFhNjExMDA3M2MxMWEyNzljNjA1YTBhMzlmNjc1IiwKICAgICAgICAibmFtZSI6ICJTS0lMTC5tZCIKICAgICAgfSwKICAgICAgewogICAgICAgICJhbGdvcml0aG0iOiAic2hhMjU2IiwKICAgICAgICAiZGlnZXN0IjogImUwNTM3MDViYjY2ZDZiZGIzZDY0NWIxYzMxMzVlMjQyMTExOGM3MDI3MDMwOGFiNWJmZjQ5YmJmNDZmZGZhMjYiLAogICAgICAgICJuYW1lIjogInNraWxsLWNhcmQubWQiCiAgICAgIH0KICAgIF0sCiAgICAic2VyaWFsaXphdGlvbiI6IHsKICAgICAgIm1ldGhvZCI6ICJmaWxlcyIsCiAgICAgICJhbGxvd19zeW1saW5rcyI6IGZhbHNlLAogICAgICAiaWdub3JlX3BhdGhzIjogWwogICAgICAgICIuZ2l0IiwKICAgICAgICAiLmdpdGh1YiIsCiAgICAgICAgIi5naXRpZ25vcmUiLAogICAgICAgICIuZ2l0YXR0cmlidXRlcyIKICAgICAgXSwKICAgICAgImhhc2hfdHlwZSI6ICJzaGEyNTYiCiAgICB9CiAgfQp9","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MGUCMArpr3Lhi56I/2OHSrdWFzKUx2D7fNnkdsetykK0tFvW30cp0wgE4PbH7pXYmbsaAgIxAO/E9DXRr5XeRMfU4bRW6hePBjQdt2s4XlRfY6QV3/4n6OuFQsjNPpiReg/Nm/06Cw==","keyid":""}]}} \ No newline at end of file