Explore self-contained Java classifier JARs with a statically linked libcuopt - #1818
Draft
ramakrishnap-nv wants to merge 30 commits into
Draft
Explore self-contained Java classifier JARs with a statically linked libcuopt#1818ramakrishnap-nv wants to merge 30 commits into
ramakrishnap-nv wants to merge 30 commits into
Conversation
|
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. |
cuopt_static existed only inside the BUILD_TESTS block, because the internal tests were its only consumer. Embedding cuOpt into a single self-contained shared object needs the same archive, so it is now gated on BUILD_TESTS or a new CUOPT_BUILD_STATIC_LIB option, with the tests block left to add_subdirectory alone. build_static_libcuopt.sh builds that archive scoped to what the Java bindings expose — no routing, no gRPC — and reports its size. The shared libcuopt is 554 MB against 29 DT_NEEDED entries, and Maven Central caps an upload bundle at 1 GB, so the measurement decides whether self-contained classifier JARs are feasible at all. Contributes to #1817. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
A published JAR is the only thing a consumer installs, so the library has to come out of it. NativeLibraryLoader keeps -Dcuopt.native.dir first for a library built from source, then falls back to a copy embedded in the JAR, then to the library path. The embedded copy is extracted once per user and reused when the size already matches, since re-extracting hundreds of megabytes on every JVM start would dominate startup. build_cuopt_java_jar.sh packages one classifier, placing the library where the loader looks. It refuses a library that still carries a DT_NEEDED on libcuopt.so: that loads on the build machine and fails for a consumer who installed nothing else, which is the whole failure this is meant to remove. The POM gains a classifier and a native-resource directory, both empty by default so the source build and the test suite are unchanged. Contributes to #1817. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Linking libcuopt_static.a into cuopt_jni removes the dependency on libcuopt.so, but not on the libraries cuOpt itself needs and conda ships only as shared objects. Each surfaced as an UnsatisfiedLinkError in turn: rmm's exception typeinfo, TBB via KaMinPar, NCCL, then cuDSS. They are linked and packaged beside the JNI library, which finds them through its $ORIGIN RPATH, and the loader lays them out before loading it. NCCL is 279 MB of the 405 MB result and is only needed for distributed PDLP, which a Java JAR cannot reach. cpp/CMakeLists.txt has no switch to compile that path out; adding one is the single biggest size win available. The shared libcuopt path is untouched: CUOPT_STATIC_BUILD_DIR is empty by default, cuopt.native.dir is still tried first, and the source build still passes 35/35. Contributes to #1817. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
ramakrishnap-nv
force-pushed
the
java-static-classifiers
branch
from
August 27, 2026 18:27
dd6ae68 to
e521ce7
Compare
ci/build_java_static.sh runs the whole path — static libcuopt, static link, packaging — and then checks the result. java-static-build runs it alongside java-build, which still covers the shared libcuopt path. verify_jar_dependencies.sh is the check worth having. Every missing library found while getting this working (rmm, TBB, NCCL, cuDSS) appeared only as an UnsatisfiedLinkError at run time, because the build environment supplies them all and the JAR looks fine there. It reads DT_NEEDED and allows only what is packaged in the JAR, provided by the CUDA toolkit, or part of the base system. Reading DT_NEEDED rather than resolving against a library directory matters: the first version pointed ldd at the conda prefix and passed a JAR with libnccl.so.2 deleted, since the prefix contains it either way. Contributes to #1817. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
The job was only in build.yaml, which runs on branch and nightly builds, so it would never have run on the PR proposing it. pr.yaml now runs it too, gated on the same test_java and test_cpp file groups as java-build and included in the pr-builder aggregator so a failure fails the PR. It runs on cpu16 rather than a GPU node: the job compiles the JAR and inspects its dependencies, but does not execute it. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Collaborator
Author
|
/ok to test a0816cb |
A publishing workflow consumes a Maven repository tree, so the shape is fixed here rather than left to whatever downloads these JARs. assemble_maven_repo.sh gathers the classifier JARs, the POM renamed from pom.xml to cuopt-<version>.pom, and the sources and javadoc JARs that Maven Central requires, into com/nvidia/cuopt/cuopt/<version>/. It reads the version from a JAR name so the layout can only describe artifacts that exist, and refuses a non-empty output directory so a stale tree cannot be published. java-static-build uploads that tree as cuopt_java_maven_repo. Requested by @paul-aiyedun for the nightly Sonatype snapshot workflow in rapidsai/build-infra#379. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Collaborator
Author
|
/ok to test 87b17bb |
CI Test Summary⏭️ All 5 test job(s) skipped. |
java-static-build now runs as a matrix over CUDA major and architecture, producing cuda12, cuda12-arm64, cuda13 and cuda13-arm64, each uploaded as cuopt_java_<arch>_cu<major>. java-static-gather downloads them and assembles one cuopt_java_maven_repo artifact, which is what a publishing workflow consumes. Standardized on cuDF's conventions while doing so: argparse.sh gives the scripts one way to reject a missing or empty flag, the matrix comes from compute-matrix.yaml filtered to one entry per arch and CUDA major, and each classifier directory carries its own POM, sources and javadoc JARs so the gather step can work from those directories alone. CI measured the first classifier at 599 MB, against 405 MB locally: the difference is libcuopt_jni.so growing from 173 MB to 371 MB once every CUDA architecture is built. Four classifiers therefore exceed the 1 GB Maven Central bundle limit together, though each fits individually. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Collaborator
Author
|
/ok to test 6df31e9 |
Testing the packaged JAR with a bespoke smoke test would have covered a fraction of what the suite already covers, so java-static-test runs the suite itself through a packaged-jar-tests profile, following cuDF's ci/test_packaged_java.sh. Main compilation is skipped so the JAR supplies both the classes and the native libraries, and the artifact is fetched with rapids-download-from-github rather than gh, which is what handles the pull-request and nightly cases. Two things this found. NativeTestSupport.assumeNativeLibrary required cuopt.native.dir, which encodes "a native library means a source build". Run against a classifier JAR the suite reported 35 found, 14 passed, 21 aborted — silently skipping every native test, in the configuration where they matter most. It now accepts either route. PackagedJarOriginCheck asserts the classes and the embedded library really came from a JAR, because a stray target/classes on the classpath would shadow it and the run would pass while testing the wrong thing. Confirmed it fails when cuopt.native.dir is set to bypass the JAR. It matches none of surefire's default name patterns, so the profile names it explicitly. Against a classifier JAR: 38/38. From source, unchanged at 35/35, with the origin check excluded. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Collaborator
Author
|
/ok to test a4219ed |
rapids-check-pr-job-dependencies requires every job to be a dependency of pr-builder. The build, test and gather jobs were listed but the matrix job that feeds them was not, so the checks job failed. pr-test-summary remains the only job outside the aggregator, which is expected and already ignored. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Collaborator
Author
|
/ok to test a623cc4 |
All four java-static-test jobs failed resolving maven-source-plugin from Maven Central with a 429. cuopt_mvn exists to retry exactly that, but the packaging and test scripts called mvn directly and so never got it. The version is now read from the POM's update marker instead of by invoking Maven. That removes a network round trip from the packaging step, and avoids capturing the wrapper's merged stderr into the version string. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Collaborator
Author
|
/ok to test 78822d7 |
4 tasks
cuOpt's C++ logger writes console output directly to std::cout when log_to_console is enabled (the common case), bypassing Java's System.out entirely. In the Java bindings, that raw write to the process's native stdout stream corrupts Maven Surefire's forked-JVM IPC protocol, which also uses stdout as its channel -- intermittently turning a passing test run into a reported "VM crash" depending on whether a log line happens to interleave with a protocol frame. Reproduced locally: NativeIntegrationTest's PDLP/MIP solves reliably trigger Surefire's "Corrupted channel by directly writing to native stream" warning, occasionally escalating to a hard failure. Add a console-sink override hook to the shared logger (set_console_log_callback), used only when a caller registers one; behavior for the Python, C, CLI, and server bindings is unchanged. The Java JNI layer registers a callback that forwards each log line to a new NativeLogSink.onLogLine, which writes it through System.out -- letting Surefire (and any other System.out interceptor, e.g. a redirect or logging bridge) see it like ordinary Java output instead of a raw native write. Known residual gap: PSLP, a vendored third-party presolver linked into libcuopt, prints its own status lines directly via printf and does not go through cuopt's logger, so it is not covered by this callback. It surfaces far less often than the fix's scope (only a short presolve status line, versus the solver's console banner and progress log on every solve), but is a separate, harder fix (patching or forking the vendored library) tracked separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Collaborator
Author
|
/ok to test 9722612 |
Root-caused the residual Corrupted channel failures still hitting java-static-test after the NativeLogSink fix: PSLP v0.0.11's run_presolver() gates every other console message behind stgs->verbose (print_start_message, print_end_message), but calls print_infeas_or_unbnd_message() unconditionally when it detects the problem is infeasible or unbounded. cuOpt already sets verbose = false when calling PSLP (third_party_presolve.cpp), specifically to keep it silent, so this one line slips through despite that and writes straight to the process's native stdout -- bypassing System.out exactly like the raw write NativeLogSink was built to intercept, and corrupting Surefire's forked-JVM protocol the same way. The infeasible/unbounded status itself is unaffected: it already flows back to the caller through run_presolver()'s typed return value, not by parsing this printed text, so cuOpt's own (properly routed) status reporting is unchanged. Filed and fixed upstream: dance858/PSLP#55. Until a release containing it is available, patch the vendored v0.0.11 source at fetch time via a new PATCH_COMMAND on PSLP's FetchContent_Declare. Verified locally: rebuilt libcuopt_static + the JNI layer with the patch applied (confirmed via the fetched source) and ran the full Java suite, including ProblemIntegrationTest's infeasible-solve case which is what triggers this code path, 50 times in a loop. Every run passed with zero "Corrupted channel" occurrences (previously this reproduced on the very first attempt). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Collaborator
Author
|
/ok to test 6f37f3d |
…sh diagnostics inline Root cause of java-static-test's flaky Surefire "Corrupted channel" crash, found by reproducing it locally against a self-contained classifier JAR inside the real rapidsai/ci-conda container (a plain -Dcuopt.native.dir run never hit it, because that path's broader LD_LIBRARY_PATH happened to expose the missing library anyway): libcudss.so.0 dlopen()s a separate OpenMP threading backend, libcudss_mtlayer_gomp.so.0, from cudssSetThreadingLayer at runtime. That companion was missing from both NativeLibraryLoader's embedded- resource list and build_cuopt_java_jar.sh's packaging step, so in a genuinely consumer-like environment (no libcuopt, no broader LD_LIBRARY_PATH -- exactly what java-static-test runs) the call fails and cuDSS writes its own failure message straight to the process's native stdout: FAILED: CUDSS call ended unsuccessfully with status = 3, details: "cudssSetThreadingLayer" That's a raw write cuOpt's own logger never sees, so no amount of NativeLogSink/PSLP fixing (see #1825) could catch it -- three independent sources were writing to the same stream. Verified with 3 clean runs against the container repro after packaging the missing library. Also dump target/surefire-reports/*.dumpstream and hs_err_pid*.log inline in ci/test_java_static.sh on failure, and upload surefire-reports as a job artifact: this exact diagnosis depended on reading the dumpstream file's contents, which neither the console log nor any uploaded artifact previously exposed.
Collaborator
Author
|
/ok to test 4bc2d27 |
…se the crash Both were temporary: the fprintf diagnostics in logger.cpp/cuopt_jni.cpp confirmed NativeLogSink's callback routing works correctly on every solve (never falls back to raw std::cout, never skips reconfiguring an existing guard), which ruled out this PR's own logging path as the remaining cause of java-static-test's Surefire "Corrupted channel" crash. The actual cause -- cuDSS's missing OpenMP threading-layer companion library -- is fixed independently in 4bc2d27 and verified clean against a local reproduction of the real CI container three times, with and without this debug code. pr.yaml's non-java if:false gates were only there to keep iteration on java-static-test fast; restored to the normal changed-files-gated conditions, keeping the surefire-reports artifact upload added alongside the real fix.
Collaborator
Author
|
/ok to test d468b48 |
…d.yaml The java-static-* jobs are no longer exploratory, and the comments duplicated what's already explained at each script's own top-of-file #1817 reference. Kept the one bit of load-bearing rationale (why java-static-build-matrix skips conda-cpp-build) inline in pr.yaml. build.yaml also had "Combines every classifier into one Maven-repository-layout artifact... See rapidsai/build-infra#379" sitting above java-static-test, describing java-static-gather instead (25 lines further down, which had no comment of its own). Moved it to the job it actually documents.
java-static-test has repeatedly hit 429 Too Many Requests resolving plugins like maven-source-plugin from a cold repository -- the cuopt_mvn wrapper's retry loop (fixed in #1823) retries the whole mvn invocation with backoff, but that's compensating for Maven's own resolver never being tuned, and 4 attempts don't reliably outlast a sustained rate limit. cuDF and cuVS already carry this exact fix for their own Java/Maven Central builds: a project-level .mvn/maven.config (auto-applied to every mvn invocation, no wrapper needed) that caps concurrent downloads to reduce burst request rate and adds a real backoff inside Maven's own transport-layer retry handler, rather than only retrying around the outside of a failed process: -Daether.connector.basic.downstreamThreads=1 -Daether.transport.http.retryHandler.count=5 -Daether.transport.http.retryHandler.interval=10000 -Dmaven.wagon.http.retryHandler.count=5 cuopt_mvn's own -D flags target the connector-layer retry handler, which recent Maven resolver versions may no longer consult now that retry logic lives at the transport layer -- this adds the layer that actually gets read, verified via `mvn help:evaluate -Dexpression=aether.transport.http.retryHandler.interval` resolving to 10000. Verified the packaged-jar-tests suite still passes with this config present.
Collaborator
Author
|
/ok to test 88b49bd |
3 tasks
…rim pr.yaml to iterate Root cause investigation (see PR discussion): java-static-test's Surefire "Corrupted channel" crash was fixed by shipping cuDSS's missing threading-layer companion, but java-static-test also kept hitting Maven Central 429s that java-static-build didn't. The difference traced to test_java_static.sh doing a fresh `conda create` (openjdk+maven+cuda-version+libcublas+libcusparse) every run -- a slow solve that reliably lined up concurrent matrix jobs' cold Maven Central resolution moments apart, exactly the kind of synchronized retry collision a fixed backoff schedule can't escape. cuDF's java-tests job (same shared-workflows custom-job.yaml, same kind of packaged-jar test) doesn't hit this: it runs on rapidsai/ci-wheel, a plain CUDA-devel + dnf environment with no conda env-solve step at all. The test job only needs a JDK, Maven and the CUDA runtime (libcublas/libcusparse, already baked into that image); everything else the JAR needs is already embedded as a companion library, so there's nothing conda-specific about testing it. - test_java_static.sh: dnf-install a JDK (Rocky 8's own maven package is too old for cuOpt's plugins, so pin a modern Apache Maven tarball instead) rather than solving a conda env from scratch. - pr.yaml / build.yaml: point java-static-test's container_image at rapidsai/ci-wheel instead of rapidsai/ci-conda. java-static-build stays on conda unchanged -- unlike the test job, the build needs TBB, which has no clean dnf/pip source for both amd64 and arm64 (Rocky 8's dnf tbb-devel is the ancient 2018 API; the pip and GitHub release prebuilt binaries are x86_64-only). - Verified locally end to end against the actual rapidsai/ci-wheel image via Docker (not just -Dcuopt.native.dir): building the classifier JAR with conda unchanged, then running -Ppackaged-jar-tests inside a fresh rapidsai/ci-wheel container with zero conda. That surfaced two more build-toolchain/consumer-distro ABI gaps invisible on the current Ubuntu-based test image: libgomp, libstdc++ and libgcc_s all need newer symbol versions (OMP_5.0.1/GLIBCXX_3.4.30/GCC_14.0.0) than Rocky Linux 8's defaults ship. Fixed by shipping all three as companions, same pattern as librmm.so/libtbb.so.12/libcudss.so.0. Final local run: 38/38 tests, zero Corrupted channel, zero UnsatisfiedLinkError. pr.yaml's non-java if:false gates are back for the same reason as before: keep iterating on java-static-test alone instead of the full suite. Revert before merge.
Collaborator
Author
|
/ok to test 2dad31c |
Collaborator
Author
|
/ok to test 2dad31c |
… mvn test/verify Root cause of why cuDF's equivalent java-tests job never seems to hit the same Maven Central 429s ours does, even after moving to the same rapidsai/ci-wheel environment: cuDF gates maven-source-plugin and maven-javadoc-plugin behind a release-only profile, so their test path never resolves them. Ours declared both unconditionally in <build><plugins>, so every `mvn test`/`verify` invocation -- including test_java_static.sh's, which never packages anything -- still had to resolve maven-source-plugin:3.3.1 from Maven Central on every run. That's the exact artifact that's failed on 429 in every occurrence of this issue across this investigation. Move both plugins into a new attach-source-javadoc profile, activated explicitly by ci/build_cuopt_java_jar.sh (the only place that actually needs sources/javadoc jars, for Maven Central publishing) rather than left implicit everywhere. Verified locally: `mvn test -Ppackaged-jar-tests` and `mvn compile` no longer resolve maven-source-plugin/maven-javadoc-plugin at all (checked via -X debug output); `mvn package -Pattach-source-javadoc` still produces both jars; the full packaged-jar-tests suite still passes (38/38).
Collaborator
Author
|
/ok to test f373279 |
ramakrishnap-nv
added a commit
that referenced
this pull request
Sep 1, 2026
…y mvn test/verify maven-source-plugin and maven-javadoc-plugin were declared unconditionally in <build><plugins>, so every `mvn test`/`verify` invocation -- including ci/test_java.sh's, which never packages anything for publishing -- still had to resolve maven-source-plugin:3.3.1 from Maven Central on every run. That's the same artifact that's repeatedly failed on 429 in the java-static-test investigation on #1818, and there's no reason java-build's plain test path should pay for it either. No script on this branch currently packages a JAR for real publishing (only java/cuopt/scripts/test.sh's `mvn verify`, for testing, and build.sh's `mvn clean package`, for local dev -- neither needs sources/javadoc jars), so nothing needs the new attach-source-javadoc profile activated; this is a pure reduction in unnecessary Maven Central resolution. Verified locally: `mvn compile` no longer resolves maven-source-plugin/maven-javadoc-plugin at all (checked via -X debug output).
…rs-mergefix # Conflicts: # cpp/src/utilities/logger.cpp # cpp/src/utilities/logger.hpp
Collaborator
Author
|
/ok to test 458b062 |
ramakrishnap-nv
added a commit
that referenced
this pull request
Sep 1, 2026
The stray-infeasible-message fix is a real, separate bug (independently confirmed via a captured Surefire dumpstream, and fixed upstream at dance858/PSLP#55), not something the cuDSS fix on #1818 makes redundant -- but it's a distinct concern from this PR's own scope (routing cuOpt's console logging through System.out) and belongs in its own PR for review.
3 tasks
ramakrishnap-nv
added a commit
that referenced
this pull request
Sep 1, 2026
run_presolver() prints "PSLP declares problem as infeasible[.| or unbounded.]" unconditionally, unlike every other console message in that function, which are all gated on stgs->verbose. cuOpt sets verbose = false when calling PSLP (third_party_presolve.cpp) specifically to keep it silent, so this writes unexpectedly straight to the process's native stdout. Found and confirmed via a captured Surefire dumpstream while investigating the Java bindings' "Corrupted channel" crash (see #1818): a raw native write there bypasses System.out and corrupts Maven Surefire's forked-JVM protocol, which also uses stdout as its own channel. The infeasible/unbounded status itself is unaffected -- it already flows back to the caller through run_presolver()'s typed return value, not by parsing this printed text. Filed and fixed upstream: dance858/PSLP#55. Pin past v0.0.11 to the merge commit until a release containing it is available.
rapids-bot Bot
pushed a commit
that referenced
this pull request
Sep 1, 2026
#1825) ## Summary - cuOpt's C++ logger writes console output (`log_to_console` == true, the common case) directly to `std::cout`, which performs a raw write to the process's native stdout file descriptor -- completely bypassing Java's `System.out`. - In the Java bindings, that corrupts Maven Surefire's forked-JVM communication protocol, which also multiplexes over stdout. Depending on whether a log line happens to land mid-frame, this shows up as anything from a harmless `Corrupted channel by directly writing to native stream` warning to a full `VM crash or System.exit called?` failure that fails every remaining test in that fork. - This is not new: the same warning already fires (non-fatally, so far) in the existing `java-build` CI job today. It surfaced as a hard failure while investigating [#1818](#1818) (exploratory static-linked classifier JARs), whose `java-static-test` job hit the race far more reliably than `java-build` does. Root-caused and reproduced locally by rebuilding libcuopt + the JNI layer and running `NativeIntegrationTest` directly; see that PR's discussion for the investigation. ## Fix - Add a console-sink override hook to the shared logger, `cuopt::set_console_log_callback` (`cpp/src/utilities/logger.hpp`/`.cpp`). Unused by default, so behavior for the Python, C, CLI, and server bindings is unchanged. - The Java JNI layer (`cuopt_jni.cpp`) registers a callback (lazily, on first `SolverSettings` creation, so `FindClass` runs with the right classloader) that forwards each log line to a new `NativeLogSink.onLogLine`, which writes it through `System.out`. - Because the line now goes through `System.out`, Surefire's own interception of that stream (and any other consumer's -- a redirect, a logging bridge) sees it as ordinary Java output rather than a raw native write, so there is nothing left to corrupt its IPC channel. ## Second source found and fixed: vendored PSLP After the fix above, `java-static-test` still failed occasionally with the same signature. Reproduced locally (stress loop, hit on the first attempt) and read the exact corrupted text straight from Surefire's `.dumpstream` artifact: `PSLP declares problem as infeasible.` -- a raw `printf` in the vendored PSLP presolver (`github.com/dance858/PSLP`, pinned at `v0.0.11`). Root cause: `run_presolver()` gates every other console message behind `stgs->verbose` (`print_start_message`, `print_end_message`), but calls `print_infeas_or_unbnd_message()` unconditionally. cuOpt already sets `verbose = false` when calling PSLP (`third_party_presolve.cpp`) specifically to keep it silent -- this one line was just missed. The infeasible/unbounded status itself is unaffected: it already flows back to the caller through `run_presolver()`'s typed return value, not by parsing this printed text. Filed and fixed upstream: dance858/PSLP#55. Until a release containing it is available, `cpp/CMakeLists.txt` patches the vendored `v0.0.11` source at fetch time via a new `PATCH_COMMAND` on PSLP's `FetchContent_Declare` (patch file at `cpp/cmake/patches/pslp/respect_verbose_for_infeasible_message.patch`). ## Test plan - [x] Rebuilt libcuopt (static) and the JNI layer with the `NativeLogSink` change, in a clean CI-matched conda env; ran `NativeIntegrationTest` directly via `mvn test -Dcuopt.native.dir=...`. - Before: `Tests run: 11, Failures: 0, Errors: 0` followed by `Corrupted channel by directly writing to native stream in forked JVM 1` for every solver log line (`cuOpt version: ...`, `Setting parameter ...`, `Solving a problem with ...`). - After: same 11/11 pass, zero occurrences of `Corrupted channel`. - [x] Ran the full Java suite (35 tests) with just the `NativeLogSink` fix: all pass, but one residual `Corrupted channel` warning remained, traced to PSLP's `printf` (see above). - [x] After adding the PSLP patch: rebuilt libcuopt_static + the JNI layer with the patch applied (confirmed applied by inspecting the fetched source), and ran the full Java suite in a loop 50 times, including `ProblemIntegrationTest`'s infeasible-solve case which is what exercises this exact code path. All 50 runs passed with zero `Corrupted channel` occurrences. - [ ] `java-build` CI passes with no `Corrupted channel` warning at all. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Authors: - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv) Approvers: - Chris Maes (https://github.com/chris-maes) - Rajesh Gandham (https://github.com/rg20) - Trevor McKay (https://github.com/tmckayus) URL: #1825
…perty namespace Two independent fixes surfaced by the first real CI run of the rapidsai/ci-wheel migration: arm64 java-static-test failed with UnsatisfiedLinkError: libcublas.so.13 not found, despite the image shipping it and ldconfig registering it correctly (verified directly against the published image). arm64 images carry both a targets/aarch64-linux and a targets/sbsa-linux directory; only sbsa-linux actually has the libraries, and glob/find order put the empty aarch64-linux one first. Rather than depend on the image's implicit ldconfig setup at all (whatever the exact runner-side difference turns out to be), explicitly find the target directory that actually contains libcublas.so and put it on LD_LIBRARY_PATH. Separately, java-static-test hit a 429 on a different plugin (exec-maven-plugin, needed by generate-sources so it can't be moved into the attach-source-javadoc profile like the other two). Checked .mvn/maven.config's properties against the actual MRESOLVER-396 source (ConfigurationProperties.java) again: aether.transport.http.retryHandler.* (copied from cuDF/cuVS) isn't the namespace that feature's count/interval/serviceUnavailable properties live under -- it's aether.connector.http.retryHandler.*. Verified via `mvn help:evaluate` that all four now actually resolve, matching the real property names this time instead of a second guess.
Collaborator
Author
|
/ok to test c1f9a15 |
rapids-bot Bot
pushed a commit
that referenced
this pull request
Sep 1, 2026
… stdout (#1836) ## Summary - `run_presolver()` in vendored PSLP (`dance858/PSLP`, pinned at `v0.0.11`) prints `"PSLP declares problem as infeasible[.| or unbounded.]"` unconditionally, unlike every other console message in that function, which are all gated behind `stgs->verbose` (`print_start_message`, `print_end_message`, etc). - cuOpt already sets `verbose = false` when calling PSLP (`third_party_presolve.cpp`) specifically to keep it silent -- this one message was just missed upstream, so it writes straight to the process's native stdout regardless. - Found while investigating the Java bindings' "Corrupted channel" Surefire crash on #1818: a raw native write there bypasses `System.out` and corrupts Maven Surefire's forked-JVM protocol, which also multiplexes over stdout. Confirmed directly by reading the exact corrupted text out of a captured Surefire `.dumpstream` artifact. - The infeasible/unbounded status itself is unaffected by this fix -- it already flows back to the caller through `run_presolver()`'s typed return value, not by parsing this printed text. ## Fix Filed and fixed upstream: dance858/PSLP#55. Until a release containing it ships, pin `cpp/CMakeLists.txt`'s `GIT_TAG` past `v0.0.11` directly at the merge commit, rather than patching the vendored source locally. ## Test plan - [x] Confirmed via a captured `.dumpstream` artifact that this exact message was the corrupted text. - [x] `ProblemIntegrationTest`'s infeasible-solve test case (Java bindings) exercises this code path directly. - [ ] CI passes clean. Split out of #1825, which originally carried this alongside an unrelated fix (routing cuOpt's own console logging through `System.out`). Authors: - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv) Approvers: - Trevor McKay (https://github.com/tmckayus) URL: #1836
Without this, RAPIDS_CUDA_VERSION inside the container stayed at rapidsai/ci-conda:26.10-latest's baked-in default for every matrix entry, since custom-job.yaml has no other per-matrix env passthrough. All four build matrix entries were actually building against the same CUDA version regardless of label; ci/build_java_static.sh reads RAPIDS_CUDA_VERSION to pick the conda CUDA toolkit, so the cu12-labeled classifier JARs were really cu13 builds. That surfaced as an arm64 UnsatisfiedLinkError (libcublas.so.13 missing) when java-static-test correctly pulled a genuine CUDA 12.9 image to test what it thought was a CUDA 12.9 build.
Collaborator
Author
|
/ok to test 0002a06 |
…erride container-options: -e RAPIDS_CUDA_VERSION=... worked, but conda-cpp-build.yaml already establishes the idiomatic way to thread matrix.CUDA_VER into an image without custom-job.yaml's native support for it: pin the image tag itself, the same way java-static-test already does for rapidsai/ci-wheel. Confirmed the cuda-pinned ci-conda tags exist for both CUDA_VER values in this matrix.
Collaborator
Author
|
/ok to test ee920a8 |
NVIDIA-managed GitHub Actions runners egress through a small, shared NAT'd IP range, so every RAPIDS repo's Java CI shares the same rate-limit budget against Maven Central (rapidsai/build-infra#370): cuDF, cuVS, cuVS-lucene, and kvikio have all hit the same 429s we're seeing on exec-maven-plugin. kvikio#992 fixed it there by preferring the read-only GCS mirror of Central (the same one Apache ORC/Lucene/Spark use) with Central as fallback. Applying the same fix here. Verified locally: every plugin, including exec-maven-plugin, now resolves from the GCS mirror.
Collaborator
Author
|
/ok to test ed2fc5e |
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.
Description
Draft, opening the work for #1817. It now produces a working self-contained classifier JAR: a solve runs from the JAR alone, with no
libcuopt, no conda environment and no-Dcuopt.native.dir.Result
cuopt-26.10.0-cuda13.jar, 405 MB, built for CUDA arch 75 with routing and gRPC excluded:libnccl.so.2libcuopt_jni.solibcudss.so.0librmm.so,libtbb.so.12,librapids_logger.soAgainst the 1 GB bundle limit: one classifier fits comfortably; four classifiers as a single bundle would be ~1.6 GB. Whether classifiers upload separately is open on build-infra#379.
Note this is a single CUDA architecture. A real multi-arch fatbin grows
libcuopt_jni.so, though not the bundled.sofiles.What static linking actually removed, and what it did not
-DBUILD_SHARED_LIBS=OFFdoes not work here:cuoptis declaredadd_library(cuopt SHARED ...), so the flag is ignored.cuopt_staticalready existed but only insideif (BUILD_TESTS); it is now gated onBUILD_TESTS OR CUOPT_BUILD_STATIC_LIB.Linking that archive removed the dependency on
libcuopt.so, but not on the libraries cuOpt itself needs and conda ships only as shared objects. Each appeared as a separateUnsatisfiedLinkError, one per rebuild:_ZTIN3rmm10_RMM_26_109bad_allocE— rmm's exception typeinfotbb::detail::r1::throw_exception— TBB, via KaMinParncclGroupStart— NCCLcudssDestroy— cuDSSThey are linked and packaged beside the JNI library, which resolves them through its existing
$ORIGINRPATH.NCCL
NCCL is 69% of the JAR and cannot be reached from Java:
pdhg.hppholdsmulti_gpu_engine_t*as a forward-declared pointer defaulting tonullptr, and the whole surface is 14 symbols across 5 files undercpp/src/pdlp/distributed_pdlp/.Findings from investigating whether it could be linked statically instead:
ncclships no static archive — onlylibnccl.so{,.2,.2.30.7},libnccl_device.bcand a pkgconfig file.libnccl_static.aexists only in apt'slibnccl-dev, which would mean sourcing one dependency from apt while the rest comes from conda.nvidia/cuda:13.0.3-devel,libnccl_static.ais 190 MB against 181 MB for the shared library. NCCL's bulk is per-architecture device code, which comes along either way.nvidia/cuda:13.0.3-basehas none; the devel image gets it from thelibnccl2apt package at/lib/x86_64-linux-gnu, not/usr/local/cuda/lib64. So it cannot be treated as consumer-supplied the way cuBLAS can.dependencies.yamlfor both paths —nccl >=2.19underbuild_cpp,nvidia-nccl-cu1{2,3}>=2.19undercuda_wheels. There is no build in which cuOpt does without it.So it is bundled as a shared library for now, per the decision to keep it a dependency and revisit later. The size win is not in how it is linked but in not needing it: a switch to compile out distributed PDLP would take the JAR from 405 MB to roughly 126 MB, and loading NCCL lazily through
dlopenwould do the same for conda and wheel users, who currently carry ~280 MB for a feature most never use.cpp/CMakeLists.txthas no such switch today, unlikeSKIP_ROUTING_BUILDandSKIP_GRPC_BUILD.CI
java-static-buildrunsci/build_java_static.shend to end — static libcuopt, static link, package, verify — inbuild.yamlandpr.yaml, gated on the same file groups asjava-buildand part of thepr-builderaggregator.verify_jar_dependencies.shis the check worth having. It readsDT_NEEDEDfor every packaged library and allows only what is inside the JAR, provided by the CUDA toolkit, or part of the base system.Reading
DT_NEEDEDrather than resolving against a library directory is the point. The first version pointedlddat the conda prefix and passed a JAR withlibnccl.so.2deleted, because the prefix contains it either way — the build environment makes any JAR look self-contained. It now fails correctly:It also fails if
libcuopt.soreappears inDT_NEEDED, which would mean the static link silently fell back to shared.Not regressed
The shared
libcuoptpath is untouched.CUOPT_STATIC_BUILD_DIRandCUOPT_BUILD_STATIC_LIBare both empty/off by default,-Dcuopt.native.diris still the loader's first strategy, and./build.sh java --run-java-testspasses 35/35 producing an unclassified JAR.Open questions
libcuoptfailing to load on a plain CUDA runtime image.Checklist
verify_jar_dependencies.shtested against a deliberately broken JAR