java: cuopt-java initial release — LP/MILP/QP API + classifier JARs#1192
java: cuopt-java initial release — LP/MILP/QP API + classifier JARs#1192rgsl888prabhu wants to merge 28 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.
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.
|
/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.
|
/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.**
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.
|
/ok to test 54a8b9c |
The Java CI conda env was missing libcusparse.so.12 (and friends) at
load time:
java.lang.UnsatisfiedLinkError: /opt/conda/envs/java/lib/libcuopt.so:
libcusparse.so.12: cannot open shared object file: No such file or
directory
libcuopt's conda recipe lists libcublas / libcudss-dev / libcusparse-dev
only under `host:` (build-time) and explicitly `ignore_run_exports`-es
them; CUDA runtime libs are not declared as `run:` deps. Python tests
work around this by pulling them in transitively via cudf / cupy /
cuda-python. The Java env has no such chain, so we have to declare
them ourselves.
Adds libcublas, libcudart, libcudss, libcusparse, libnvjitlink to the
`java` dep group in dependencies.yaml. Mirrors the ldd of libcuopt.so
(libcublasLt rides along with libcublas).
A cleaner fix would be to add these to libcuopt's `run:` deps, but
that's a broader change with cross-team coordination.
|
/ok to test f27ed56 |
| } | ||
|
|
||
| /** Whether this is a ranged constraint (lower <= lhs <= upper). */ | ||
| public boolean isRanged() { |
There was a problem hiding this comment.
Let's remove isRanged(). Constraints should not be ranged.
| * Lower bound for ranged constraints. For one-sided constraints, | ||
| * returns the appropriate one-sided bound (e.g. rhs for GE; -infinity for LE). | ||
| */ | ||
| public double lowerBound() { |
There was a problem hiding this comment.
Remove lowerBound and upperBound. Constraints should just have a rhs
| * } | ||
| * }</pre> | ||
| */ | ||
| public final class DataModel implements AutoCloseable { |
There was a problem hiding this comment.
I don't think we need DataModel
There was a problem hiding this comment.
The user should create problems directly with the Problem
|
|
||
| // LinkedHashMap preserves insertion order (useful for deterministic | ||
| // CSR builds), keys are Variables, values are summed coefficients. | ||
| private final Map<Variable, Double> terms = new LinkedHashMap<>(); |
There was a problem hiding this comment.
Let's discuss implementation. I don't think we want to use HashMaps here.
There was a problem hiding this comment.
Agreed worth a look. Current LinkedHashMap<Variable, Double> was chosen for two properties we'd lose with naive arrays: same-variable coefficient accumulation (Gurobi convention — e.addTerm(2,x).addTerm(3,x) → 5x is one term, not two) and deterministic insertion-order iteration during CSR builds.
Three alternatives that keep the user-facing API the same:
-
Parallel arrays + uniquify at CSR-build.
int[] varIndices + double[] coeffsinLinearExpr; deduplication moved intoCSRBuilder. ~5× less memory in the build phase (~88 bytes/term → ~16).numTerms()semantics shift from "distinct" to "raw" until the CSR build dedups. -
Hand-rolled primitive-keyed open-addressing map (
int → doublekeyed byVariable.index(), ~50 LOC of arithmetic + linear probing). Preserves the O(1)-merge semantics, no boxing, no API change, no new dependency. ~16-24 bytes/term. -
fastutil(Int2DoubleOpenHashMap) or similar. Same payoff as (2) without writing the hash logic ourselves, but adds a runtime dependency to cuopt-java.
The work-log under temp/ flagged this as a deferred ~2-day optimization for 10⁶-variable problems — it's small at typical sizes. My lean is (2) since it preserves the merge semantics, no API contract change, and no new dep. Happy to take any of the three; which direction do you want?
| * @param presolveTime wall-clock time spent in presolve, seconds | ||
| * @param rootRelaxationTime wall-clock time spent on the root LP relaxation, seconds | ||
| */ | ||
| public record MilpStats( |
There was a problem hiding this comment.
We should use MIP instead of MILP. Capitalization should be consistent so MIPStats
| private final List<Variable> variables = new ArrayList<>(); | ||
| private final List<Constraint> constraints = new ArrayList<>(); | ||
| private final List<LinearExpr> constraintExpressions = new ArrayList<>(); | ||
| private final Map<String, Variable> variablesByName = new HashMap<>(); |
There was a problem hiding this comment.
Let's discuss and possible remove Map here.
There was a problem hiding this comment.
The two name-keyed maps (variablesByName, constraintsByName) back Problem.getVariable(String) / Problem.getConstraint(String) for Gurobi-style name lookup. They're populated only when a non-empty name is passed at addVariable/addConstraint time — so problems built without naming entities pay essentially nothing (~96 bytes total per Problem for two empty HashMap headers).
Three options:
-
Drop entirely. Remove both maps + the
getVariable(String)/getConstraint(String)public methods. Users keep their ownVariable/Constraintreferences. Cleanest; loses Gurobi parity (nogetVarByName-style lookup) but no internal code paths depend on it — only one test reference exists (LpSolverIT.java:107-108, easy to update). -
Build lazily on first lookup. Zero cost if never looked up. Adds complexity for marginal savings since the empty-map cost is already tiny.
-
Keep as-is. Cost-when-empty is ~zero; users who name things get O(1) lookup.
The internal solve path uses getVariable(int) and getConstraint(int) (by index), not by name — so dropping name lookup wouldn't touch the FFM impl. My lean is (1) if you want simpler; (3) if Gurobi parity is worth keeping. Which way?
| return n; | ||
| } | ||
|
|
||
| public boolean isMip() { |
There was a problem hiding this comment.
Consistent capitalization of abbrevation isMIP
| return false; | ||
| } | ||
|
|
||
| public boolean isQp() { |
There was a problem hiding this comment.
Consistent capitalization isQP
| /** Linear program: linear objective, linear constraints, all continuous variables. */ | ||
| LP, | ||
| /** Integer program: linear objective and constraints, all variables integer. */ | ||
| IP, |
| * {@code src/main/java22/}; the JVM resolves it via | ||
| * {@link java.util.ServiceLoader} on Java 22+ runtimes. | ||
| */ | ||
| public interface CuOptProvider { |
There was a problem hiding this comment.
Consistent capitalization: cuOptProvider
| * grouped at the bottom. They're used with | ||
| * {@link com.nvidia.cuopt.linear_programming.SolverSettings#setParameter(String, Object)}. | ||
| */ | ||
| public final class CuOpt { |
There was a problem hiding this comment.
Consistent capitalization cuOpt
| * the runtime environment is misconfigured (e.g., the native library | ||
| * cannot be loaded, or the JVM is older than Java 22). | ||
| */ | ||
| public class CuOptException extends RuntimeException { |
There was a problem hiding this comment.
Consistent captialization: cuOpt
| * | ||
| * <p>Output arrays are ready to copy into native {@code MemorySegment}s. | ||
| */ | ||
| final class CsrBuilder { |
There was a problem hiding this comment.
Consistent capitalization: CSRBuilder
| * addresses); this class reconstructs {@code MemorySegment} from the | ||
| * addresses on each call. | ||
| */ | ||
| public final class CuOptProviderImpl implements CuOptProvider { |
There was a problem hiding this comment.
Consistent capitalization: cuOptProviderImpl
|
|
||
| problem.solve(settings); | ||
|
|
||
| assertTrue(problem.isMip()); |
There was a problem hiding this comment.
Consistent capitalization: isMIP
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class MipCallbackIT { |
There was a problem hiding this comment.
Consistent capitalization: MIPCallbackIT
There was a problem hiding this comment.
Why does it have an IT at the end
There was a problem hiding this comment.
IT is the Maven convention for integration tests, separating two phases:
*Test.java— run by Surefire in thetestphase, againsttarget/classes/(exploded classes)*IT.java— run by Failsafe in theintegration-testphase, against the packaged JAR
This split matters specifically for cuopt-java because:
-
MR-JAR semantics are only honored when classes are loaded from an actual JAR file, not from exploded
target/classes/. The Java 22 FFM implementation lives underMETA-INF/versions/22/and is invisible to test runs that load from exploded classes.pom.xmlconfigures Failsafe with<classesDirectory>${project.build.directory}/${project.build.finalName}.jar</classesDirectory>to force JAR-based loading. (cuvs Issue Cost per token is not real cost reduction — hidden safety, governance and liability costs are missing #1037) -
GPU dependency — every
*IT.javatest actually solves a problem, so it requireslibcuopt.soon the path and a CUDA GPU. The*Test.javafiles (e.g.LinearExprTest) are pure-Java and run anywhere. -
Selective skipping —
UNIT_TESTS_ONLY=true ./java/build.shruns only the fast Surefire batch (useful when iterating without a GPU). Arm64 CI used this mode before the GPU runner was wired up.
So MipCallbackIT keeps the IT suffix because it solves a MIP at runtime — i.e. it's an integration test, not a unit test.
| * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| package com.nvidia.cuopt.linear_programming; |
There was a problem hiding this comment.
I don't think we should call this com.nvidia.cuopt.linear_programming. We should try to deprecate the linear_programming term since cuOpt contains much more than this now.
Previous attempt added `libcudart` to the `java` dep group, but the mamba solver rejected it: libcudart =* * does not exist (perhaps a typo or a missing channel). The conda-forge feedstock for the CUDA runtime is named `cuda-cudart` (checked against https://api.anaconda.org/package/conda-forge/...). `libcudart` is not a published package name on any of the channels the test_java env uses (rapidsai, rapidsai-nightly, conda-forge). The other four (libcublas, libcudss, libcusparse, libnvjitlink) do exist under those names — no change needed.
Per review feedback (chris-maes): Problem.isMip() → isMIP() Problem.isQp() → isQP() MilpStats → MIPStats (record class rename) MilpStats.java → MIPStats.java (file rename) Problem.milpStats() → mipStats() (method rename; field too) CsrBuilder → CSRBuilder (internal class rename) CsrBuilder.java → CSRBuilder.java (file rename) CsrBuilder.Csr → CSRBuilder.CSR (inner record + method names) ProblemCategory.IP removed (folded into MIP) Java is okay with either PascalCase (Csr) or all-caps (CSR) for acronyms in identifiers; this PR moves to all-caps consistently.
…del)
Per review feedback (chris-maes):
Constraint.isRanged() / lowerBound() / upperBound() removed.
Constraints now only carry (sense, rhs). The dual `Constraint`
constructor for ranged constraints is gone; only the (CType, rhs)
constructor remains.
DataModel.java removed entirely. Users construct problems via the
`Problem` modeling API. Cascade removals:
- CuOptProvider.solveDataModel(DataModel, SolverSettings) — SPI
- CuOptProviderImpl.solveDataModel(...) — impl
- CuOptProviderImpl.buildNativeFromDataModel(...) — impl
- CuOptProviderImpl.zeros() / positiveInfinities() — only used by the above
- Solver.solve(DataModel) / solve(DataModel, SolverSettings) — convenience entry
- The orphaned `solveAndExtract(Arena, ProblemBuild, ...)` overload
- SolveResult javadoc reference to DataModel → Problem
Build still green; the existing IT suite continues to compile against
the Problem modeling API (DataModel was never used by tests).
Per review feedback (chris-maes): consistent capitalization matching the cuOpt brand (lowercase 'c'). Four classes renamed: CuOpt → cuOpt (public static constants holder) CuOptException → cuOptException (RuntimeException) CuOptProvider → cuOptProvider (sealed SPI interface) CuOptProviderImpl → cuOptProviderImpl (Java 22 FFM impl) File names updated to match (git mv), all type references updated, and the ServiceLoader registration at META-INF/services/com.nvidia.cuopt.spi.cuOptProvider renamed + content updated to point at com.nvidia.cuopt.internal.cuOptProviderImpl. Full IT suite (4 unit + 6 IT) passes locally after the rename; ServiceLoader resolves the renamed impl correctly.
|
/ok to test cd20d99 |
Per review feedback (chris-maes): com.nvidia.cuopt.linear_programming → com.nvidia.cuopt.optimization Reviewer's reasoning: cuOpt covers more than linear programming now (routing, etc.), so `linear_programming` is a misleading namespace. The new `com.nvidia.cuopt.optimization` is broad enough to host the LP/MILP/QP API alongside future siblings (e.g. `com.nvidia.cuopt.routing`). Also renames the MIP callback IT class: MipCallbackIT → MIPCallbackIT Cascade: 18 main classes + 5 test classes moved from linear_programming/ to optimization/, and ~10 files outside the package updated their imports. Full IT suite (4 unit + 6 IT) passes locally after the rename. The C-side header path (`cpp/include/cuopt/linear_programming/cuopt_c.h`) is unchanged — only the Java package was renamed.
|
/ok to test 4925008 |
Mirrors cuvs's behavior: every CI run produces both the base JAR
(BYO libcuopt) and the per-(arch, cuda) classifier JAR that bundles
libcuopt + libmps_parser + librmm + librapids_logger and carries the
Embedded-Libraries-Cuda-Version manifest entry.
Wires CLASSIFIER_CUDA=${RAPIDS_CUDA_VERSION%%.*} into ci/build_java.sh
so the existing classifier-jar Maven profile activates in CI. Locally
verified earlier in the PR session (CLASSIFIER_CUDA=13 ./java/build.sh
produces a 56 MB classifier JAR; embedded mode loads libcuopt without
LD_LIBRARY_PATH).
Each CI matrix entry now produces two JARs uploaded under
cuopt-java-cuda / cuopt-java-arm64-cuda artifacts; with the two arch
× two CUDA-major matrix rows, that's four classifier JARs per push
ready for Maven Central.
|
/ok to test 49f2ab9 |
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)