Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
35 changes: 35 additions & 0 deletions cpp/include/cuopt/mathematical_optimization/cuopt_c.h
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,41 @@ cuopt_int_t cuOptGetFloatParameter(cuOptSolverSettings settings,
const char* parameter_name,
cuopt_float_t* parameter_value);

/**
* @brief Type of callback invoked once per log line emitted by the solver.
*
* @param level Severity of the log line, increasing with value
* (0=trace, 1=debug, 2=info, 3=warn, 4=error, 5=critical). Intended only for
* display/filtering of output, not as a stable programmatic API.
* @param message Null-terminated log line without trailing newline.
* @param user_data Opaque pointer passed to cuOptSetLogCallback.
*
* @note The callback is invoked from the solver thread. Do not call back into
* cuOpt from inside the callback.
* @warning Log message formatting is not part of the stable API and may change
* between releases. The callback is intended for display purposes (GUI integration,
* log forwarding, stdout capture) — do not parse message content for programmatic
* control flow.
*/
typedef void (*cuOptLogCallback)(int level, const char* message, void* user_data);

/**
* @brief Register a callback to receive solver log messages.
*
* The callback is invoked once per log line. It is called in addition to any
* file or console sink already enabled via ``log_to_console`` / ``log_file``
* parameters. Pass NULL to remove a previously registered callback.
*
* @param[in] settings The solver settings object.
* @param[in] callback Callback function, or NULL to clear.
* @param[in] user_data Opaque pointer forwarded to the callback unchanged.
*
* @return A status code indicating success or failure.
*/
cuopt_int_t cuOptSetLogCallback(cuOptSolverSettings settings,
cuOptLogCallback callback,
void* user_data);

/**
* @brief Type of callback for receiving incumbent MIP solutions with user context.
*
Expand Down
2 changes: 1 addition & 1 deletion cpp/src/mip_heuristics/diversity/diversity_manager.cu
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ void diversity_manager_t<i_t, f_t>::add_user_given_solutions(
*problem_ptr->original_problem_ptr, h_original, h_crushed);
init_sol_assignment = cuopt::device_copy(h_crushed, sol.handle_ptr->get_stream());

#if CUOPT_LOG_ACTIVE_LEVEL <= CUOPT_LOG_LEVEL_DEBUG
#if CUOPT_LOG_ACTIVE_LEVEL <= RAPIDS_LOGGER_LOG_LEVEL_DEBUG
const auto& reduced_problem = *problem_ptr->original_problem_ptr;
const std::vector<f_t> h_red_obj = reduced_problem.get_objective_coefficients_host();
const std::vector<f_t>& h_ori_obj = presolver_ptr->get_original_objective_coefficients();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,7 @@ i_t fj_t<i_t, f_t>::host_loop(solution_t<i_t, f_t>& solution, i_t climber_idx)
}
}
}
#if CUOPT_LOG_ACTIVE_LEVEL == CUOPT_LOG_LEVEL_TRACE
#if CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_TRACE
auto h_sol = cuopt::host_copy(solution.assignment, climber_stream);
static std::set<std::vector<f_t>> solutions_set;
bool same_sol = solutions_set.count(h_sol) > 0;
Expand Down
31 changes: 31 additions & 0 deletions cpp/src/pdlp/cuopt_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ struct solver_settings_handle_t {
~solver_settings_handle_t() { delete settings; }
solver_settings_t<cuopt_int_t, cuopt_float_t>* settings;
std::vector<std::unique_ptr<cuopt::internals::base_solution_callback_t>> callbacks;
// Log callback registered via cuOptSetLogCallback
cuOptLogCallback log_callback{nullptr};
void* log_callback_user_data{nullptr};
};

solver_settings_handle_t* get_settings_handle(cuOptSolverSettings settings)
Expand Down Expand Up @@ -1069,6 +1072,17 @@ cuopt_int_t cuOptSetMIPSetSolutionCallback(cuOptSolverSettings settings,
return CUOPT_SUCCESS;
}

cuopt_int_t cuOptSetLogCallback(cuOptSolverSettings settings,
cuOptLogCallback callback,
void* user_data)
{
if (settings == nullptr) { return CUOPT_INVALID_ARGUMENT; }
solver_settings_handle_t* handle = get_settings_handle(settings);
handle->log_callback = callback;
handle->log_callback_user_data = user_data;
return CUOPT_SUCCESS;
}

cuopt_int_t cuOptSetInitialPrimalSolution(cuOptSolverSettings settings,
const cuopt_float_t* primal_solution,
cuopt_int_t num_variables)
Expand Down Expand Up @@ -1145,6 +1159,23 @@ cuopt_int_t cuOptSolve(cuOptOptimizationProblem problem,
if (settings == nullptr) { return CUOPT_INVALID_ARGUMENT; }
if (solution_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; }

// Install user log callback so init_logger_t inside the solver picks it up.
// The RAII guard clears it on scope exit (whether by return or exception).
solver_settings_handle_t* handle = get_settings_handle(settings);
struct log_scope_guard_t {
bool has_callback;
~log_scope_guard_t()
{
if (has_callback) { cuopt::clear_pending_log_callback(); }
}
} log_scope{false};

if (handle->log_callback) {
// cuOptLogCallback and log_callback_with_data_t share the same signature.
cuopt::set_pending_log_callback(handle->log_callback, handle->log_callback_user_data);
log_scope.has_callback = true;
}

problem_and_stream_view_t* problem_and_stream_view =
static_cast<problem_and_stream_view_t*>(problem);

Expand Down
103 changes: 97 additions & 6 deletions cpp/src/utilities/logger.cpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/* 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 */

#include <utilities/logger.hpp>
#include <utilities/version_info.hpp>

#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <optional>

namespace cuopt {

struct buffered_entry {
Expand Down Expand Up @@ -82,13 +87,44 @@ rapids_logger::sink_ptr default_sink()
*/
inline std::string default_pattern() { return "[%Y-%m-%d %H:%M:%S:%f] [%n] [%-6l] %v"; }

/**
* @brief Runtime log-level override from the `CUOPT_LOG_LEVEL` environment variable.
*
* Accepts a level name (case-insensitive): TRACE, DEBUG, INFO, WARN, ERROR, CRITICAL, OFF.
* Returns std::nullopt if the variable is unset or holds an unrecognised value.
*
* @note Statements below the compile-time `CUOPT_LOG_ACTIVE_LEVEL` (default INFO) are
* removed at build time, so raising verbosity above the build level has no effect;
* lowering it (e.g. WARN/ERROR/OFF to suppress output) always works.
*/
inline std::optional<rapids_logger::level_enum> env_log_level()
{
const char* env = std::getenv("CUOPT_LOG_LEVEL");
if (env == nullptr) { return std::nullopt; }
std::string level{env};
std::transform(level.begin(), level.end(), level.begin(), [](unsigned char c) {
return static_cast<char>(std::toupper(c));
});
if (level == "TRACE") { return rapids_logger::level_enum::trace; }
if (level == "DEBUG") { return rapids_logger::level_enum::debug; }
if (level == "INFO") { return rapids_logger::level_enum::info; }
if (level == "WARN") { return rapids_logger::level_enum::warn; }
if (level == "ERROR") { return rapids_logger::level_enum::error; }
if (level == "CRITICAL") { return rapids_logger::level_enum::critical; }
if (level == "OFF") { return rapids_logger::level_enum::off; }
return std::nullopt; // unrecognised value: keep the compiled default
}

/**
* @brief Returns the default log level for the global logger.
*
* The `CUOPT_LOG_LEVEL` environment variable, when set, overrides the compile-time default.
*
* @return rapids_logger::level_enum The default log level.
*/
inline rapids_logger::level_enum default_level()
{
if (auto lvl = env_log_level()) { return *lvl; }
#if CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_TRACE
return rapids_logger::level_enum::trace;
#elif CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_DEBUG
Expand Down Expand Up @@ -137,14 +173,62 @@ void reset_default_logger()
default_logger().flush_on(rapids_logger::level_enum::debug);
}

// Guard object whose destructor resets the logger
// Forward declarations needed by logger_config_guard destructor.
static std::mutex g_guard_mutex;
static const struct captured_log_callback_t* g_active_log_callback;

// Captured (immutable) callback state owned by the active logger guard.
struct captured_log_callback_t {
log_callback_with_data_t callback;
void* user_data;
};

// Guard object whose destructor resets the logger.
// Owns the captured callback state to guarantee its lifetime.
struct logger_config_guard {
~logger_config_guard() { cuopt::reset_default_logger(); }
std::unique_ptr<captured_log_callback_t> callback_state;
~logger_config_guard()
{
cuopt::reset_default_logger(); // removes the sink; blocks until in-flight log calls finish
std::lock_guard<std::mutex> lock(g_guard_mutex);
g_active_log_callback = nullptr; // safe: the sink (and the bridge) are already gone
}
};

// Weak reference to detect if any init_logger_t instance is still alive
static std::weak_ptr<logger_config_guard> g_active_guard;
static std::mutex g_guard_mutex;

// g_active_log_callback: written only under g_guard_mutex (at guard create/destroy time).
// Read lock-free by user_log_bridge — safe because the bridge is only reachable
// while the sink is alive, and the sink is removed (in reset_default_logger) before
// this pointer is cleared.

// Pending user log callback set by the C API before cuOptSolve.
// Consumed once (under g_guard_mutex) by init_logger_t to build the guard state.
static log_callback_with_data_t g_pending_callback = nullptr;
static void* g_pending_callback_data = nullptr;

static void user_log_bridge(int lvl, const char* msg)
{
// g_active_log_callback is stable for the duration of any bridge call:
// it points into the guard's callback_state, which outlives the sink.
const captured_log_callback_t* state = g_active_log_callback;
if (state) { state->callback(lvl, msg, state->user_data); }
}

void set_pending_log_callback(log_callback_with_data_t cb, void* user_data)
{
std::lock_guard<std::mutex> lock(g_guard_mutex);
g_pending_callback = cb;
g_pending_callback_data = user_data;
}

void clear_pending_log_callback()
{
std::lock_guard<std::mutex> lock(g_guard_mutex);
g_pending_callback = nullptr;
g_pending_callback_data = nullptr;
}

init_logger_t::init_logger_t(std::string log_file, bool log_to_console)
{
Expand All @@ -169,6 +253,15 @@ init_logger_t::init_logger_t(std::string log_file, bool log_to_console)
std::make_shared<rapids_logger::basic_file_sink_mt>(log_file, true));
cuopt::default_logger().flush_on(rapids_logger::level_enum::debug);
}
// Capture pending callback into the guard so the bridge reads stable (immutable) state.
auto guard = std::make_shared<logger_config_guard>();
if (g_pending_callback) {
guard->callback_state = std::make_unique<captured_log_callback_t>(
captured_log_callback_t{g_pending_callback, g_pending_callback_data});
g_active_log_callback = guard->callback_state.get();
cuopt::default_logger().sinks().push_back(
std::make_shared<rapids_logger::callback_sink_mt>(user_log_bridge));
}

#if CUOPT_LOG_ACTIVE_LEVEL >= RAPIDS_LOGGER_LOG_LEVEL_INFO
cuopt::default_logger().set_pattern("%v");
Expand All @@ -182,8 +275,6 @@ init_logger_t::init_logger_t(std::string log_file, bool log_to_console)
cuopt::default_logger().log(entry.level, entry.msg.c_str());
}

// Create guard and store weak reference for future instances to find
auto guard = std::make_shared<logger_config_guard>();
g_active_guard = guard;
guard_ = guard;
}
Expand Down
14 changes: 14 additions & 0 deletions cpp/src/utilities/logger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ rapids_logger::logger& default_logger();
*/
void reset_default_logger();

// C-compatible log callback type: void callback(int level, const char* msg, void* user_data)
// Matches cuOptLogCallback in cuopt_c.h — layout-compatible, no dependency on that header.
using log_callback_with_data_t = void (*)(int level, const char* message, void* user_data);

/**
* @brief Install a user log callback to be picked up by the next init_logger_t.
*
* Must be called before the init_logger_t that starts the targeted solve.
* Protected by the same mutex as init_logger_t so it is safe to call from
* any thread, but do not call from inside the callback itself.
*/
void set_pending_log_callback(log_callback_with_data_t cb, void* user_data);
void clear_pending_log_callback();

// Ref-counted logger initializer
class init_logger_t {
// Using shared_ptr for ref-counting
Expand Down
Loading
Loading