Skip to content

java: cuopt-java initial release — LP/MILP/QP API + classifier JARs#1192

Draft
rgsl888prabhu wants to merge 28 commits into
NVIDIA:mainfrom
rgsl888prabhu:feat/java-api-skeleton
Draft

java: cuopt-java initial release — LP/MILP/QP API + classifier JARs#1192
rgsl888prabhu wants to merge 28 commits into
NVIDIA:mainfrom
rgsl888prabhu:feat/java-api-skeleton

Conversation

@rgsl888prabhu
Copy link
Copy Markdown
Collaborator

@rgsl888prabhu rgsl888prabhu commented May 8, 2026

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 from cpp/include/cuopt/linear_programming/cuopt_c.h. The single file cuopt_c_h.java is 23,515 lines. Skip the entire panama/ subdirectory — the drift gate in java/build.sh keeps it in sync with the C header on every build.

The remaining ~4,350 hand-written lines, in suggested review order:

Area Files Lines Biggest file
Public API (Java 21, src/main/java/) 23 2,105 Problem.java (421)
FFM impl (Java 22, src/main/java22/.../internal/, excl. panama) 4 902 CuOptProviderImpl.java (531)
Maven + build scripts (pom.xml, build.sh, generate-bindings.sh, assembly) 5 625 pom.xml (317)
Tests (src/test/java/) 5 315
CI / conda / dependencies (pr.yaml, build.yaml, dependencies.yaml, env files) 9 248 pr.yaml (~69)
READMEs / resources / .gitignore 8 156

What's in this PR

Public API (Java 21, src/main/java/com/nvidia/cuopt/)

  • Problem, Variable, Constraint, LinearExpr, QuadraticExpr, SolverSettings, DataModel
  • Enums: VType, CType, Sense, SolverMethod, PdlpSolverMode, TerminationStatus, ErrorStatus, ProblemCategory
  • Records: LpStats, MilpStats
  • @FunctionalInterface callbacks: MIPGetSolutionCallback, MIPSetSolutionCallback
  • CuOpt constants holder, Solver entry points, CuOptException
  • spi/CuOptProvider (sealed) — Layer 4 bridge between Java 21 public API and Java 22 FFM impl

FFM implementation (Java 22, src/main/java22/)

  • CuOptProviderImpl — full impl: settings, parameter setters, solveProblem, solveDataModel
  • CsrBuilder — translates List<LinearExpr> to CSR + Q-matrix CSR
  • NativeLibraryLoader — chooses between embedded (classifier JAR) and BYO (System.loadLibrary) modes via JAR manifest
  • internal/panama/ — jextract bindings (committed, drift-gate-stable)

Build / CI

  • java/build.sh orchestrates regen bindings → drift gate → mvn verify
  • panama-bindings/generate-bindings.sh auto-downloads jextract per arch on first run; idempotently normalizes output
  • ci/build_java.sh + ci/test_java.sh with --run-java-tests / --unit-tests-only modes
  • pr.yaml: conda-java-build-and-tests (amd64 GPU IT) + conda-java-build-and-tests-arm64 (arm64 GPU IT)
  • build.yaml: java-build + java-build-arm64 for main/release/tag pushes

Distribution

  • Base JAR (cuopt-java-<version>.jar) — always built. BYO libcuopt.so on java.library.path (typical conda).
  • Classifier JAR (cuopt-java-<version>-<arch>-cuda<n>.jar) — opt-in via mvn -Dcuda.version=<n> or CLASSIFIER_CUDA=<n> ./java/build.sh. Bundles libcuopt.so, libmps_parser.so, librmm.so, librapids_logger.so under <arch>/Linux/, with manifest entry Embedded-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).
  • FFM upcall stubs are bound to the per-solve Arena and re-created on every solve, so cross-solve reuse of the same SolverSettings is safe. Trampolines catch Throwable so user-code exceptions never propagate across the native boundary.

Tests

  • Unit: LinearExprTest (4)
  • Integration: SolverIT, LpSolverIT, MilpSolverIT, QpSolverIT, MipCallbackIT (8 total)
  • All green locally (~4s)

Design inputs

Out of scope — tracked as follow-up issues

# Item Reason
#1202 C API stats getters (LP residuals, iterations, gap, method; MIP nodes/simplex iters/presolve time/violations) Python sees these via Cython→C++ direct; the C ABI doesn't expose them. Currently NaN / -1 / UNSET in LpStats/MilpStats. Cross-team C++ work.
#1203 Static-link gRPC, protobuf, abseil, TBB, libgomp into libcuopt.so Removes runtime system-package friction for non-conda users. CMake-side change.

Test plan

  • ./java/build.sh passes locally (drift gate stable, 4 unit + 6 IT)
  • CLASSIFIER_CUDA=13 ./java/build.sh produces a classifier JAR; embedded mode verified by running with empty LD_LIBRARY_PATH
  • CI green on amd64 (conda-java-build-and-tests)
  • CI green on arm64 GPU (conda-java-build-and-tests-arm64)
  • CodeRabbit / coderabbit-review feedback addressed

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/
@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot Bot commented May 8, 2026

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.
@rgsl888prabhu rgsl888prabhu changed the title java: add cuopt-java skeleton with FFM bridge demo java: cuopt-java skeleton + CI + LP/MILP/QP public API May 11, 2026
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.
@rgsl888prabhu rgsl888prabhu changed the title java: cuopt-java skeleton + CI + LP/MILP/QP public API java: cuopt-java initial release — LP/MILP/QP API + classifier JARs May 12, 2026
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.
@rgsl888prabhu rgsl888prabhu self-assigned this May 12, 2026
@rgsl888prabhu rgsl888prabhu added feature request New feature or request non-breaking Introduces a non-breaking change labels May 12, 2026
@rgsl888prabhu
Copy link
Copy Markdown
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.
@rgsl888prabhu
Copy link
Copy Markdown
Collaborator Author

/ok to test 2e3d2fe

rgsl888prabhu and others added 3 commits May 12, 2026 15:11
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.
@rgsl888prabhu
Copy link
Copy Markdown
Collaborator Author

/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.
@rgsl888prabhu
Copy link
Copy Markdown
Collaborator Author

/ok to test f27ed56

}

/** Whether this is a ranged constraint (lower &lt;= lhs &lt;= upper). */
public boolean isRanged() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove lowerBound and upperBound. Constraints should just have a rhs

* }
* }</pre>
*/
public final class DataModel implements AutoCloseable {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need DataModel

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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<>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss implementation. I don't think we want to use HashMaps here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Parallel arrays + uniquify at CSR-build. int[] varIndices + double[] coeffs in LinearExpr; deduplication moved into CSRBuilder. ~5× less memory in the build phase (~88 bytes/term → ~16). numTerms() semantics shift from "distinct" to "raw" until the CSR build dedups.

  2. Hand-rolled primitive-keyed open-addressing map (int → double keyed by Variable.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.

  3. 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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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<>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss and possible remove Map here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Drop entirely. Remove both maps + the getVariable(String) / getConstraint(String) public methods. Users keep their own Variable/Constraint references. Cleanest; loses Gurobi parity (no getVarByName-style lookup) but no internal code paths depend on it — only one test reference exists (LpSolverIT.java:107-108, easy to update).

  2. Build lazily on first lookup. Zero cost if never looked up. Adds complexity for marginal savings since the empty-map cost is already tiny.

  3. 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() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistent capitalization of abbrevation isMIP

return false;
}

public boolean isQp() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistent capitalization isQP

/** Linear program: linear objective, linear constraints, all continuous variables. */
LP,
/** Integer program: linear objective and constraints, all variables integer. */
IP,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove IP.

* {@code src/main/java22/}; the JVM resolves it via
* {@link java.util.ServiceLoader} on Java 22+ runtimes.
*/
public interface CuOptProvider {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistent captialization: cuOpt

*
* <p>Output arrays are ready to copy into native {@code MemorySegment}s.
*/
final class CsrBuilder {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistent capitalization: CSRBuilder

* addresses); this class reconstructs {@code MemorySegment} from the
* addresses on each call.
*/
public final class CuOptProviderImpl implements CuOptProvider {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistent capitalization: cuOptProviderImpl


problem.solve(settings);

assertTrue(problem.isMip());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistent capitalization: isMIP

import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;

class MipCallbackIT {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistent capitalization: MIPCallbackIT

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it have an IT at the end

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IT is the Maven convention for integration tests, separating two phases:

  • *Test.java — run by Surefire in the test phase, against target/classes/ (exploded classes)
  • *IT.java — run by Failsafe in the integration-test phase, against the packaged JAR

This split matters specifically for cuopt-java because:

  1. 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 under META-INF/versions/22/ and is invisible to test runs that load from exploded classes. pom.xml configures 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)

  2. GPU dependency — every *IT.java test actually solves a problem, so it requires libcuopt.so on the path and a CUDA GPU. The *Test.java files (e.g. LinearExprTest) are pure-Java and run anywhere.

  3. Selective skippingUNIT_TESTS_ONLY=true ./java/build.sh runs 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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@rgsl888prabhu
Copy link
Copy Markdown
Collaborator Author

/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.
@rgsl888prabhu
Copy link
Copy Markdown
Collaborator Author

/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.
@rgsl888prabhu
Copy link
Copy Markdown
Collaborator Author

/ok to test 49f2ab9

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants