Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,21 @@ FetchContent_MakeAvailable(papilo)

# PSLP - Lightweight C presolver for linear programs
# https://github.com/dance858/PSLP
#
# v0.0.11 ignores its own verbose=false setting for one message: run_presolver() prints
# "PSLP declares problem as infeasible[.| or unbounded.]" unconditionally, unlike every other
# console message in that function, which are all gated on verbose. cuOpt sets verbose = false
# (see third_party_presolve.cpp) precisely so PSLP stays silent, so this writes unexpectedly
# straight to the process's native stdout -- observed corrupting Maven Surefire's forked-JVM
# protocol in the Java bindings, which also uses stdout as its own channel. Patched upstream at
# https://github.com/dance858/PSLP/pull/55; drop this patch once a release containing it is
# available and this GIT_TAG is bumped past it.
FetchContent_Declare(
pslp
GIT_REPOSITORY "https://github.com/dance858/PSLP.git"
GIT_TAG "v0.0.11"
GIT_PROGRESS TRUE
PATCH_COMMAND sh -c "git apply --check '${CMAKE_CURRENT_SOURCE_DIR}/cmake/patches/pslp/respect_verbose_for_infeasible_message.patch' 2>/dev/null && git apply '${CMAKE_CURRENT_SOURCE_DIR}/cmake/patches/pslp/respect_verbose_for_infeasible_message.patch'; true"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
EXCLUDE_FROM_ALL
SYSTEM
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
diff --git a/src/core/Presolver.c b/src/core/Presolver.c
index c0bdc9e..426008e 100644
--- a/src/core/Presolver.c
+++ b/src/core/Presolver.c
@@ -720,7 +720,10 @@ PresolveStatus run_presolver(Presolver *presolver)
if (status != UNCHANGED)
{
// problem detected to be infeasible or unbounded
- print_infeas_or_unbnd_message(status);
+ if (stgs->verbose)
+ {
+ print_infeas_or_unbnd_message(status);
+ }
return status;
}

30 changes: 27 additions & 3 deletions cpp/src/utilities/logger.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* clang-format off */
/*
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* clang-format on */
Expand Down Expand Up @@ -55,6 +55,25 @@ log_buffer& global_log_buffer()
return buffer;
}

// Overrides the sink used when log_to_console is true. Null (the default) keeps writing to
// std::cout; set by language bindings whose host runtime cannot safely receive writes to the
// native stdout stream -- for example Java, where a raw write there bypasses System.out and can
// corrupt tools that intercept it, such as Maven Surefire's forked-process protocol.
static std::mutex g_console_callback_mutex;
static log_console_callback_t g_console_callback = nullptr;

void set_console_log_callback(log_console_callback_t callback)
{
std::lock_guard<std::mutex> lock(g_console_callback_mutex);
g_console_callback = callback;
}

static log_console_callback_t console_log_callback()
{
std::lock_guard<std::mutex> lock(g_console_callback_mutex);
return g_console_callback;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Callback function for the buffer sink
static void buffer_log_callback(int lvl, const char* msg)
{
Expand Down Expand Up @@ -161,8 +180,13 @@ init_logger_t::init_logger_t(std::string log_file, bool log_to_console)

// re-initialize sinks
if (log_to_console) {
cuopt::default_logger().sinks().push_back(
std::make_shared<rapids_logger::ostream_sink_mt>(std::cout));
if (auto callback = console_log_callback(); callback != nullptr) {
cuopt::default_logger().sinks().push_back(
std::make_shared<rapids_logger::callback_sink_mt>(callback));
} else {
cuopt::default_logger().sinks().push_back(
std::make_shared<rapids_logger::ostream_sink_mt>(std::cout));
}
}
if (!log_file.empty()) {
cuopt::default_logger().sinks().push_back(
Expand Down
13 changes: 13 additions & 0 deletions cpp/src/utilities/logger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ rapids_logger::logger& default_logger();
*/
void reset_default_logger();

using log_console_callback_t = void (*)(int level, const char* message);

/**
* @brief Overrides the sink used for console logging (settings.log_to_console == true).
*
* Passing nullptr (the default) restores writing to std::cout. Intended for language bindings
* whose host runtime cannot safely receive a raw write to the native stdout stream -- see the
* definition site in logger.cpp for why that matters.
*
* @param callback The callback to invoke for each logged line, or nullptr to restore std::cout.
*/
void set_console_log_callback(log_console_callback_t callback);

// Ref-counted logger initializer
class init_logger_t {
// Using shared_ptr for ref-counting
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.nvidia.cuopt.mathematicaloptimization;

/**
* Receives cuOpt's console log lines from native code and writes them through {@link
* System#out}, rather than the native library writing to the process's stdout stream directly.
*
* <p>A direct native write bypasses {@code System.out}, so it is invisible to anything that
* intercepts or redirects it -- {@link System#setOut}, a logging framework bridge, or Maven
* Surefire, which uses the forked JVM's stdout as its own communication channel and can
* misinterpret an unexpected raw write on it as the forked process having crashed.
*
* <p>Called from {@code cuopt_jni.cpp}; not part of the public API.
*/
final class NativeLogSink {
private NativeLogSink() {}

static void onLogLine(String message) {
System.out.print(message);
}
}
53 changes: 53 additions & 0 deletions java/cuopt/src/main/native/cuopt_jni.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <cuopt/mathematical_optimization/io/parser.hpp>
#include <cuopt/mathematical_optimization/optimization_problem_utils.hpp>
#include <pdlp/cuopt_c_internal.hpp>
#include <utilities/logger.hpp>

#include <jni.h>

Expand Down Expand Up @@ -378,6 +379,57 @@ void mip_set_solution_callback(cuopt_float_t* solution,
if (detach) { g_jvm->DetachCurrentThread(); }
}

jclass g_log_sink_class = nullptr;
jmethodID g_log_sink_method = nullptr;
std::once_flag g_log_sink_once;

// cuopt::log_console_callback_t: forwards a console log line to NativeLogSink.onLogLine, so it
// is written through System.out instead of directly to the native stdout stream. See
// register_console_log_sink for why that distinction matters.
void console_log_callback(int /* level */, const char* message)
{
if (g_log_sink_class == nullptr || g_log_sink_method == nullptr) { return; }

bool detach = false;
JNIEnv* env = get_callback_env(detach);
if (env == nullptr) { return; }

jstring line = env->NewStringUTF(message);
if (line != nullptr) {
env->CallStaticVoidMethod(g_log_sink_class, g_log_sink_method, line);
// A logging call is not the place to raise a Java exception; drop it rather than leave it
// pending for whatever JNI call happens to run next on this thread.
if (env->ExceptionCheck() == JNI_TRUE) { env->ExceptionClear(); }
env->DeleteLocalRef(line);
}

if (detach) { g_jvm->DetachCurrentThread(); }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Registers console_log_callback with the native logger, once. Done lazily on first use (rather
// than in JNI_OnLoad) because FindClass needs the caller's classloader, which JNI_OnLoad does not
// reliably have.
void register_console_log_sink(JNIEnv* env)
{
std::call_once(g_log_sink_once, [env]() {
jclass local_cls = env->FindClass("com/nvidia/cuopt/mathematicaloptimization/NativeLogSink");
if (local_cls == nullptr) {
env->ExceptionClear();
return;
}
jmethodID method = env->GetStaticMethodID(local_cls, "onLogLine", "(Ljava/lang/String;)V");
if (method == nullptr) {
env->ExceptionClear();
env->DeleteLocalRef(local_cls);
return;
}
g_log_sink_class = static_cast<jclass>(env->NewGlobalRef(local_cls));
g_log_sink_method = method;
env->DeleteLocalRef(local_cls);
cuopt::set_console_log_callback(&console_log_callback);
});
}

} // namespace

extern "C" jint JNI_OnLoad(JavaVM* vm, void*)
Expand Down Expand Up @@ -421,6 +473,7 @@ Java_com_nvidia_cuopt_mathematicaloptimization_NativeCuOpt_readProblemWithFormat
extern "C" JNIEXPORT jlong JNICALL
Java_com_nvidia_cuopt_mathematicaloptimization_NativeCuOpt_createSolverSettings(JNIEnv* env, jclass)
{
register_console_log_sink(env);
cuOptSolverSettings settings = nullptr;
if (!check_status(env, cuOptCreateSolverSettings(&settings), "cuOptCreateSolverSettings")) {
return 0;
Expand Down
Loading