java: cuopt-java initial release — LP/MILP/QP API + classifier JARs#1192
Draft
rgsl888prabhu wants to merge 21 commits into
Draft
java: cuopt-java initial release — LP/MILP/QP API + classifier JARs#1192rgsl888prabhu wants to merge 21 commits into
rgsl888prabhu wants to merge 21 commits into
Conversation
Adds the initial directory structure for a Java interface to cuOpt under java/, modeled after cuvs-java's architecture (Project Panama / FFM, Java 21 base API + Java 22 multi-release JAR for FFM impl). The first end-to-end demo is Solver.getVersion(), which exercises the full FFM bridge: Java 21 public API -> ServiceLoader SPI -> Java 22 FFM implementation -> jextract panama bindings -> libcuopt.so. Full LP/MILP/QP support lands in subsequent PRs. Build setup encodes three lessons from cuvs-java: - Java 21 base API + Java 22 multi-release JAR for FFM (cuvs Issue NVIDIA#1066) - maven-compiler-plugin 3.11.0+ with serial phase bindings (#1293) - Spotless under <plugins> only when added (NVIDIA#1090) Per-folder READMEs explain the role of each layer in the architecture. Adds build_java group and test_java file definition to dependencies.yaml so conda-forge openjdk + maven dependencies flow into developer envs. Toolchain prerequisites for local build (not auto-installed): - conda install -c conda-forge openjdk=22 maven - jextract from https://jdk.java.net/jextract/
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
Builds on the prior commit by: 1. Renaming the dependency group `build_java` -> `java` to match cuvs convention, and adding it to the `all` file's includes so all conda/environments/all_cuda-*_arch-*.yaml files now ship with openjdk=22.* and maven>=3.9.6. 2. Adding ci/build_java.sh and ci/test_java.sh, mirroring cuvs's pattern. ci/test_java.sh is a thin wrapper that delegates to ci/build_java.sh --run-java-tests. Splitting build/test into separate jobs is deferred (matches cuvs Issue NVIDIA#868) — requires shared-workflow surface for cross-job artifact passing. 3. Adding .github/workflows/pr.yaml jobs: - test_java paths group in changed-files - conda-java-build-and-tests-matrix (compute-matrix; amd64-only) - conda-java-build-and-tests (custom-job; gpu-l4-latest-1) - Adds conda-java-build-and-tests to pr-builder.needs 4. Adding java-build job to .github/workflows/build.yaml so main / release-branch / tag pushes produce cuopt-java JAR artifacts. CI will fail on this PR until two follow-up steps: - jextract is added to the rapidsai/ci-conda CI image (or pulled in ci/build_java.sh) - Generated panama bindings under java/cuopt-java/src/main/java22/com/nvidia/cuopt/internal/panama/ are committed (initial bootstrap; runs on a workstation with JDK 22 + jextract installed locally)
Builds out the full public Java API for LP, MILP, and QP on top of the skeleton + CI from the prior commits. Public API surface (Java 21, src/main/java/): - Problem: build problem (addVariable, addConstraint, setObjective), solve(), and post-solve accessors (status, objectiveValue, solveTime, lpStats, milpStats). Owns native handle + all solution arrays. - Variable / Constraint: immutable handles that delegate value() / reducedCost() / dualValue() / slack() back to the owning Problem. - LinearExpr / QuadraticExpr: mutable expression builders supporting both addTerm(coeff, var) (chainable single-term) and addTerms(double[], Variable[]) (bulk). Coefficients accumulate. - SolverSettings: typed setters (setTimeLimit, setOptimalityTolerance, setMethod, setRelativeMipGap, ...) + generic setParameter(name, value) escape hatch for forward compatibility. AutoCloseable. - DataModel: low-level escape hatch matching cuOpt Python's DataModel. CSR setters return this for chaining. - Enums: VType (CONTINUOUS/INTEGER/BINARY), CType (LE/GE/EQ), Sense, SolverMethod, PdlpSolverMode, TerminationStatus, ErrorStatus, ProblemCategory. - Records: LpStats, MilpStats. - CuOpt constants holder: static-importable INF, MINIMIZE, LESS_EQUAL, and parameter-name string constants (TIME_LIMIT, METHOD, etc.). FFM implementation (Java 22, src/main/java22/internal/): - CuOptProviderImpl: marshals Java arrays -> MemorySegment, calls jextract-generated cuopt_c_h methods, extracts solution data into Java arrays before freeing the native handle. - CsrBuilder: translates List<LinearExpr> constraint expressions into CSR (row-offsets, column-indices, values) sorted by column. - Native handles cross the SPI boundary as long (raw addresses); ProblemImpl reconstructs MemorySegment from the address on each call. Tests: - LpSolverTest: simple LP, diet LP, introspection. - MilpSolverTest: 0-1 knapsack. - QpSolverTest: mirrors cuOpt Python test_qp.py::test_solver. - LinearExprTest: addTerm/addTerms interchangeability, coefficient accumulation, length-mismatch and mixed-problem error paths. This commit completes the full LP/MILP/QP public API. Tests cannot run until: (1) JDK 22 + jextract installed on a workstation, (2) generated panama bindings committed under java/cuopt-java/src/main/java22/com/nvidia/cuopt/internal/panama/. See temp/cuopt-java-RESUME.md for the verify-locally checklist.
jextract for JDK 22 is not on conda-forge. Match cuvs's approach of auto-downloading it into java/panama-bindings/jextract-22/ on first run of generate-bindings.sh. - panama-bindings/generate-bindings.sh now downloads openjdk-22-jextract+6-47 from download.java.net if jextract isn't already on PATH. Subsequent builds reuse the local copy. - panama-bindings/.gitignore excludes jextract-22/ and the .tar.gz download artifact from git. - build.sh drops the hard error on missing jextract; the auto-download in generate-bindings.sh handles it. - README and dependencies.yaml comments updated to reflect the auto-download. CI implication: the first 'ci/build_java.sh' run will spend ~10 seconds downloading jextract (62 MB). Subsequent runs cache it in the conda env's workspace if persistent, or re-download otherwise. A follow-up could preinstall jextract in the rapidsai/ci-conda image to skip the download in CI.
Five fixes to get the build green end-to-end:
1. jextract: pass --header-class-name cuopt_c_h so the generated header
class is named what CuOptProviderImpl imports (default would be
headers_h after the umbrella include filename). Also add
--use-system-load-library so dlopen uses java.library.path.
2. Make package-private accessors PUBLIC across the
internal/-vs-linear_programming/ package boundary:
- Problem: linearObjective, quadraticObjective, objectiveSense,
objectiveOffset, constraintExpressions
- QuadraticExpr: quadVar1, quadVar2, quadCoeff
- SolverSettings: nativeHandle
- DataModel: all ~15 internal getters
Java's package-visibility doesn't cross the package boundary the
FFM impl needs to traverse. Marked with javadoc that these are
for internal FFM use only.
3. Multi-Release JAR semantics only apply to classes loaded from an
actual JAR file, NOT exploded target/classes/. So tests that touch
the FFM impl can't run via maven-surefire-plugin against
target/classes/. Moved them to *IT.java (failsafe convention) and
configured maven-failsafe-plugin with classesDirectory pointing at
the packaged JAR. Failsafe runs after package, so the JAR exists.
LinearExprTest stays as a surefire test (pure Java, no FFM impl).
SolverIT / LpSolverIT / MilpSolverIT / QpSolverIT run via failsafe.
4. build.sh: replace CMAKE_PREFIX_PATH with CUOPT_LIB_DIR. The previous
default fell through to the user's conda env path (set by conda
activate), missing the actual libcuopt.so in cpp/build/. Now we
probe cpp/build/ and $CONDA_PREFIX/lib in that order.
5. Commit the jextract-generated panama bindings (cuopt_c_h.java,
__fsid_t.java, cuOptMIPGetSolutionCallback.java,
cuOptMIPSetSolutionCallback.java). These regenerate on every build
and the drift gate enforces consistency from this commit forward.
Verified locally: full pipeline runs clean with
./java/build.sh (no SKIP_DRIFT_CHECK needed after this commit)
Test results:
Surefire: 4/4 LinearExprTest pass
Failsafe: 6/6 integration tests pass against libcuopt.so
(LP, MILP, QP all solve to feasibility/optimality)
Plumbs arm64 through the build and CI pipelines: - supported-platforms.properties: flip linux-aarch64=true. - generate-bindings.sh: detect uname -m and select matching jextract tarball (linux-aarch64 confirmed available on download.java.net) and CUDA include subdir (targets/aarch64-linux/include). - java/build.sh + ci/build_java.sh: add UNIT_TESTS_ONLY mode for the arm64 CPU runner, which has no GPU and so must skip integration tests. - ci/build_java.sh: drop the stale fail-fast jextract-availability check; auto-download in generate-bindings.sh now handles it. - pr.yaml: add conda-java-build-arm64-matrix + conda-java-build-arm64 jobs on linux-arm64-cpu16, building the JAR and running unit tests. amd64 keeps full IT on gpu-l4-latest-1. - build.yaml: add java-build-arm64 sibling job for main/release/tag. IT stays on amd64 until an arm64 GPU runner is wired into rapidsai/shared-workflows; flip --unit-tests-only to --run-java-tests and update the node_type when that lands.
The rapidsai/shared-workflows matrix already includes arm64 GPU rows (linux-arm64-gpu-l4-latest-1) used by conda-python-tests, so the Java arm64 leg no longer needs to be CPU-only. Rename the job to match the amd64 sibling and switch to the full IT script.
This was referenced May 12, 2026
Adds a Maven `classifier-jar` profile that produces
cuopt-java-<version>-<arch>-cuda<n>.jar bundling libcuopt.so,
libmps_parser.so, librmm.so, and librapids_logger.so under
<arch>/Linux/ inside the JAR, with manifest entry
Embedded-Libraries-Cuda-Version=<n>. Activated by
`mvn -Dcuda.version=<n>` or `CLASSIFIER_CUDA=<n> ./java/build.sh`.
The base JAR (always built) is unchanged: BYO libcuopt via
System.loadLibrary.
NativeLibraryLoader now inspects the JAR manifest. If the embedded
marker is present, it extracts each lib to a temp file and
System.load-s them in dependency order (rapids_logger → rmm →
mps_parser → cuopt). Otherwise it falls back to System.loadLibrary
("cuopt").
The jextract-generated System.loadLibrary in cuopt_c_h's static
initializer is rewritten by generate-bindings.sh to call
NativeLibraryLoader.ensureLoaded, so both distribution modes are
routed through the new loader.
Not bundled (size/license/ABI risk; static-linking follow-up tracked
in NVIDIA#1203): CUDA toolkit, gRPC, protobuf, abseil, TBB, libgomp,
libstdc++.
Local verification:
- Base build: mvn clean verify passes (4 unit + 6 IT)
- Classifier build: CLASSIFIER_CUDA=13 produces a 56 MB JAR;
manual run with empty LD_LIBRARY_PATH loads cuopt via embedded mode.
- Drift gate: regen is idempotent.
Adds two @FunctionalInterface types in the public API: MIPGetSolutionCallback.onSolution(double[] solution, double objectiveValue, double solutionBound) MIPSetSolutionCallback.provideSolution(double[] outSolution, double[] outObjective, double solutionBound) Registered via SolverSettings.setMIPGetSolutionCallback / setMIPSetSolutionCallback, both with null-clear semantics. Implementation in CuOptProviderImpl.registerMipCallbacks creates per-solve Arena-scoped FFM upcall stubs using the jextract-emitted allocate(Function, Arena) helpers on the cuOptMIP{Get,Set}SolutionCallback binding classes. The Get trampoline is always registered when settings is non-null and reads the user's lambda at call time (no-ops if null) — this is safe because Get registration has no solver side effects. The Set trampoline is only registered when the user has actually provided one, because registering it disables presolve per the C contract. Trampolines catch Throwable internally; exceptions never propagate across the FFM boundary. Includes MipCallbackIT with two cases: Get callback observes at least one incumbent and its primal[] is sized numVars; Set callback is invoked at least once.
Gates conda-cpp-tests, conda-python-build, conda-python-tests,
docs-build, all wheel-build/wheel-tests jobs, and
test-self-hosted-server with `if: false`, and drops them from
`pr-builder.needs`. Lanes kept on:
- pre-flight: check-lean-ci, prevent-merge-with-lean-ci,
compute-matrix-filters, changed-files, checks
- conda-cpp-build
- conda-java-build-and-tests (amd64 GPU IT)
- conda-java-build-and-tests-arm64 (arm64 GPU IT)
Revert this commit before merging PR NVIDIA#1192.
Collaborator
Author
|
/ok to test 6283b6f |
…encies) The shared rapidsai check `rapids-check-pr-job-dependencies` requires every top-level job in pr.yaml to be listed in `pr-builder.needs` (or in `ignored_pr_jobs`). The prior [TEMP] commit shrank that list to just the lanes we want CI signal on, which tripped the check and cascade-cancelled all downstream jobs. Restore the full list. pr-builder.yaml treats skipped jobs as success, so the `if: false` gates still effectively skip the non-Java lanes — they just have to be listed by name. Also adds the previously-missing `conda-java-build-and-tests-matrix` and `conda-java-build-and-tests-arm64-matrix` helper jobs to the list (they were never in `needs` since the Java lanes were added). Follow-up to 6283b6f. Both this and 6283b6f are temporary and should be reverted before merge.
…e non-Java PR lanes" Restores the original PR CI workflow: all lanes (cpp, java, python, docs, wheels, self-hosted server) run on each PR push. The two prior commits gated non-Java lanes off with `if: false` to speed up feedback during cuopt-java development; with the feature work landed, we want full CI signal again before merge. Reverts: eb91e7e (ci: list all jobs in pr-builder.needs ...) Reverts: 6283b6f ([TEMP] ci: disable non-Java PR lanes ...)
rapids-check-pr-job-dependencies (run as part of the shared `checks` workflow) requires every top-level job in pr.yaml to appear in `pr-builder.needs` (or in `ignored_pr_jobs`). The two Java matrix- compute helper jobs were missing from `pr-builder.needs` ever since they were introduced — pre-existing gap, surfaced now that CI is running on this PR. pr-builder treats skipped or success as passing, so adding these intermediate compute jobs to the list has no behavioral effect on green PRs.
Collaborator
Author
|
/ok to test 2e3d2fe |
The conda env build was failing with:
ValueError: No matching matrix found in 'cuda_version' for: {'cuda': '13', 'arch': 'x86_64'}
dependencies.yaml's `cuda_version` matrix is keyed on major.minor
(e.g. "13.0", "13.1"); we were stripping RAPIDS_CUDA_VERSION to just
the major ("13") via `%%.*` (longest prefix-match-from-end). The
result didn't match any matrix row, the dep-file-generator errored,
the conda env was created empty, and the subsequent build failed
with "java not found in PATH".
Switch to `%.*` (shortest prefix-match-from-end) to keep "13.0" from
"13.0.2", matching what ci/test_python.sh:23 does for the same env
variable.
Removes the following PR-only jobs entirely (not just `if: false`-d):
- conda-cpp-tests
- conda-python-build
- conda-python-tests
- docs-build
- wheel-build-cuopt-mps-parser
- wheel-build-libcuopt
- wheel-build-cuopt
- wheel-tests-cuopt
- wheel-build-cuopt-server
- wheel-build-cuopt-sh-client
- wheel-tests-cuopt-server
- test-self-hosted-server
And drops their references from `pr-builder.needs`.
Kept on:
- pre-flight (check-lean-ci, prevent-merge-with-lean-ci,
compute-matrix-filters, changed-files, checks)
- conda-cpp-build
- conda-java-build-and-tests* (amd64 GPU IT)
- conda-java-build-and-tests-arm64* (arm64 GPU IT)
Earlier `if: false` approach broke because the shared
`rapids-check-pr-job-dependencies` check requires every job to be
listed in pr-builder.needs. Removing the jobs entirely sidesteps
that. **Revert this commit before merge.**
The Java jobs were added before zizmor was wired into pre-commit (upstream PR NVIDIA#1181). They use `secrets: inherit` for the same reason all other reusable-workflow consumers in this repo do — the called workflow needs the script-env secrets to upload artifacts. Add the suppression comment used by the rest of the file.
Collaborator
Author
|
/ok to test 0aff867 |
Pre-existing gap on the Java jobs from my earlier commits, surfaced
by the upstream `permissions: {}` top-level default added in
PR NVIDIA#1181: with no top-level permissions and no per-job block, each
Java reusable-workflow call inherited the deny-all default and
GitHub Actions rejected the workflow with
is requesting 'actions: read, contents: read, ...', but is only
allowed 'actions: none, contents: none, ...'
Mirror the permissions block that `conda-cpp-build` uses (actions,
contents, packages, pull-requests, id-token) on the two Java
build+test jobs and the two compute-matrix helpers.
Collaborator
Author
|
/ok to test 8259ac5 |
Same pattern as 0aff867 (which fixed it on pr.yaml). The two java-build jobs in build.yaml were also added before zizmor was wired into pre-commit (upstream PR NVIDIA#1181). Add the suppression comment used by every other reusable-workflow consumer in this file.
Collaborator
Author
|
/ok to test 9689442 |
CI was failing on the jextract regen step with: ERROR: Could not locate a CUDA include directory. generate-bindings.sh needs CUDA dev headers to run jextract against cuopt_c.h's transitive #includes. The conda env used in CI for the Java jobs (`test_java` group in dependencies.yaml) only installs openjdk + maven + libcuopt — no CUDA dev headers. CI was already skipping the drift check (SKIP_DRIFT_CHECK=true), so the regen was wasted work and just a fragile failure point. Add a SKIP_BINDINGS_REGEN env var and set it in ci/build_java.sh — the committed bindings are trusted, with the dev-workstation drift gate (./java/build.sh without flags) as the authoritative check. Verified locally: with SKIP_BINDINGS_REGEN=true the regen step is skipped and the build still succeeds.
Collaborator
Author
|
/ok to test 54a8b9c |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Initial Java interface for cuOpt covering LP, MILP, and QP (beta). Models cuvs-java's architecture (Project Panama / FFM, Java 21 base API + Java 22 multi-release JAR for FFM impl). API surface follows Gurobi-style conventions for problem construction and Python cuOpt conventions for solution access.
For reviewers — what to skip
84% of this diff (~23,812 of ~28,262 lines) is auto-generated. The directory
java/cuopt-java/src/main/java22/com/nvidia/cuopt/internal/panama/is jextract output fromcpp/include/cuopt/linear_programming/cuopt_c.h. The single filecuopt_c_h.javais 23,515 lines. Skip the entirepanama/subdirectory — the drift gate injava/build.shkeeps it in sync with the C header on every build.The remaining ~4,350 hand-written lines, in suggested review order:
src/main/java/)Problem.java(421)src/main/java22/.../internal/, excl. panama)CuOptProviderImpl.java(531)pom.xml,build.sh,generate-bindings.sh, assembly)pom.xml(317)src/test/java/)pr.yaml,build.yaml,dependencies.yaml, env files)pr.yaml(~69).gitignoreWhat's in this PR
Public API (Java 21,
src/main/java/com/nvidia/cuopt/)Problem,Variable,Constraint,LinearExpr,QuadraticExpr,SolverSettings,DataModelVType,CType,Sense,SolverMethod,PdlpSolverMode,TerminationStatus,ErrorStatus,ProblemCategoryLpStats,MilpStats@FunctionalInterfacecallbacks:MIPGetSolutionCallback,MIPSetSolutionCallbackCuOptconstants holder,Solverentry points,CuOptExceptionspi/CuOptProvider(sealed) — Layer 4 bridge between Java 21 public API and Java 22 FFM implFFM implementation (Java 22,
src/main/java22/)CuOptProviderImpl— full impl: settings, parameter setters,solveProblem,solveDataModelCsrBuilder— translatesList<LinearExpr>to CSR + Q-matrix CSRNativeLibraryLoader— chooses between embedded (classifier JAR) and BYO (System.loadLibrary) modes via JAR manifestinternal/panama/— jextract bindings (committed, drift-gate-stable)Build / CI
java/build.shorchestrates regen bindings → drift gate →mvn verifypanama-bindings/generate-bindings.shauto-downloads jextract per arch on first run; idempotently normalizes outputci/build_java.sh+ci/test_java.shwith--run-java-tests/--unit-tests-onlymodespr.yaml:conda-java-build-and-tests(amd64 GPU IT) +conda-java-build-and-tests-arm64(arm64 GPU IT)build.yaml:java-build+java-build-arm64for main/release/tag pushesDistribution
cuopt-java-<version>.jar) — always built. BYOlibcuopt.soonjava.library.path(typical conda).cuopt-java-<version>-<arch>-cuda<n>.jar) — opt-in viamvn -Dcuda.version=<n>orCLASSIFIER_CUDA=<n> ./java/build.sh. Bundleslibcuopt.so,libmps_parser.so,librmm.so,librapids_logger.sounder<arch>/Linux/, with manifest entryEmbedded-Libraries-Cuda-Version=<n>. User still installs CUDA toolkit matching<n>.MIP user callbacks
SolverSettings.setMIPGetSolutionCallback(cb)— user-provided callback invoked when the solver finds a new incumbent. Reads the user's lambda at every native call so the latest registration wins.SolverSettings.setMIPSetSolutionCallback(cb)— user injects a candidate solution. Registering this disables presolve (per the C contract).Arenaand re-created on every solve, so cross-solve reuse of the sameSolverSettingsis safe. Trampolines catchThrowableso user-code exceptions never propagate across the native boundary.Tests
LinearExprTest(4)SolverIT,LpSolverIT,MilpSolverIT,QpSolverIT,MipCallbackIT(8 total)Design inputs
<build><plugins>only (cuvs Update to clang 20.1.8 #1090), MR-JAR test path (cuvs Cost per token is not real cost reduction — hidden safety, governance and liability costs are missing #1037).Out of scope — tracked as follow-up issues
NaN/-1/UNSETinLpStats/MilpStats. Cross-team C++ work.libcuopt.soTest plan
./java/build.shpasses locally (drift gate stable, 4 unit + 6 IT)CLASSIFIER_CUDA=13 ./java/build.shproduces a classifier JAR; embedded mode verified by running with emptyLD_LIBRARY_PATHconda-java-build-and-tests)conda-java-build-and-tests-arm64)