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
36 changes: 36 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ option(
"Build the S3 parquet benchmark (requires aws-sdk-cpp; NOT a cuCascade dependency)"
OFF)

# Compile-time floor for the CUCASCADE_LOG_* macros, as the underlying value of
# a cucascade::log::level (0 = trace ... 6 = off). The default keeps every call
# site and leaves filtering to the runtime threshold; raising it strips the
# cheaper levels from the binary entirely.
#
# Propagated PUBLIC because consumers expand the same macros. That is safe even
# if a consumer overrides it: the definition is read only inside the macros, not
# inside any inline function body or type, so a mismatch prunes call sites
# differently rather than violating the ODR.
set(CUCASCADE_MIN_LOG_LEVEL
"0"
CACHE
STRING
"Compile out CUCASCADE_LOG_* call sites below this level (0=trace, 1=debug, 2=info, 3=warn, 4=error, 5=fatal, 6=off)"
)

# Swappable third-party backends for the io layer. The io code was ported from a
# codebase with its own vendored dependencies. These knobs let an embedding host
# (e.g. sirius) reuse the copies it already links instead of the
Expand Down Expand Up @@ -340,6 +356,7 @@ if(NOT CUCASCADE_TOPOLOGY_ONLY)
add_subdirectory(src/cuda)
endif()
add_subdirectory(src/memory)
add_subdirectory(src/log)
if(NOT CUCASCADE_TOPOLOGY_ONLY)
add_subdirectory(src/data)
endif()
Expand All @@ -356,13 +373,22 @@ set(CUCASCADE_PUBLIC_INCLUDE_DIRS
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)

# Carried by every target that exposes include/cucascade/log/logging.hpp. The
# object libraries need it for cuCascade's own call sites; the installable
# static/shared targets are assembled from $<TARGET_OBJECTS:...> rather than by
# linking, so PUBLIC usage requirements do not flow across and must be repeated.
set(CUCASCADE_PUBLIC_COMPILE_DEFS
CUCASCADE_MIN_LOG_LEVEL=${CUCASCADE_MIN_LOG_LEVEL})

if(NOT CUCASCADE_TOPOLOGY_ONLY)
set(CUCASCADE_PUBLIC_LINK_LIBS rmm::rmm CUDA::cudart_static Threads::Threads
Numa::Numa)

# Set include directories for the object library
target_include_directories(cucascade_objects
PUBLIC ${CUCASCADE_PUBLIC_INCLUDE_DIRS})
target_compile_definitions(cucascade_objects
PUBLIC ${CUCASCADE_PUBLIC_COMPILE_DEFS})

# Link dependencies to object library
target_link_libraries(cucascade_objects PUBLIC ${CUCASCADE_PUBLIC_LINK_LIBS})
Expand Down Expand Up @@ -424,6 +450,8 @@ endif()

target_include_directories(cucascade_topology_discovery_objects
PUBLIC ${CUCASCADE_PUBLIC_INCLUDE_DIRS})
target_compile_definitions(cucascade_topology_discovery_objects
PUBLIC ${CUCASCADE_PUBLIC_COMPILE_DEFS})
target_link_libraries(cucascade_topology_discovery_objects
PUBLIC CUDA::nvml_static rmm::rmm)
target_compile_features(cucascade_topology_discovery_objects PUBLIC cxx_std_20)
Expand All @@ -446,6 +474,8 @@ if(CUCASCADE_BUILD_STATIC_LIBS)
PRIVATE CUDA::nvml_static rmm::rmm)
target_include_directories(cucascade_topology_discovery_static
PUBLIC ${CUCASCADE_PUBLIC_INCLUDE_DIRS})
target_compile_definitions(cucascade_topology_discovery_static
PUBLIC ${CUCASCADE_PUBLIC_COMPILE_DEFS})
target_compile_features(cucascade_topology_discovery_static PUBLIC cxx_std_20)
# Work around a bug in libnvidia-ml.so - it attempts to call back into the
# stub library (https://nvbugspro.nvidia.com/bug/6174166)
Expand All @@ -463,6 +493,8 @@ if(CUCASCADE_BUILD_STATIC_LIBS)

target_include_directories(cucascade_static
PUBLIC ${CUCASCADE_PUBLIC_INCLUDE_DIRS})
target_compile_definitions(cucascade_static
PUBLIC ${CUCASCADE_PUBLIC_COMPILE_DEFS})
target_link_libraries(
cucascade_static PUBLIC ${CUCASCADE_PUBLIC_LINK_LIBS}
cucascade_topology_discovery_static)
Expand Down Expand Up @@ -524,6 +556,8 @@ if(CUCASCADE_BUILD_SHARED_LIBS)
PRIVATE CUDA::nvml_static rmm::rmm)
target_include_directories(cucascade_topology_discovery_shared
PUBLIC ${CUCASCADE_PUBLIC_INCLUDE_DIRS})
target_compile_definitions(cucascade_topology_discovery_shared
PUBLIC ${CUCASCADE_PUBLIC_COMPILE_DEFS})
target_compile_features(cucascade_topology_discovery_shared PUBLIC cxx_std_20)
# Work around a bug in libnvidia-ml.so - it attempts to call back into the
# stub library (https://nvbugspro.nvidia.com/bug/6174166)
Expand All @@ -539,6 +573,8 @@ if(CUCASCADE_BUILD_SHARED_LIBS)

target_include_directories(cucascade_shared
PUBLIC ${CUCASCADE_PUBLIC_INCLUDE_DIRS})
target_compile_definitions(cucascade_shared
PUBLIC ${CUCASCADE_PUBLIC_COMPILE_DEFS})
target_link_libraries(
cucascade_shared PUBLIC ${CUCASCADE_PUBLIC_LINK_LIBS}
cucascade_topology_discovery_shared)
Expand Down
244 changes: 231 additions & 13 deletions include/cucascade/log/logging.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,244 @@

#pragma once

// Logging is compiled out: the CUCASCADE_LOG_* macros are no-ops. The
// argument expressions are placed in an unevaluated (sizeof) context so they
// are type-checked and their operands count as used (no -Wunused-variable
// fallout at call sites), but they are never evaluated at runtime.
/**
* @file
* @brief std::format-based logging delivered to a host-installed sink.
*
* cuCascade ships no logging backend; an embedding host installs a @ref cucascade::log::sink_fn
* via @ref cucascade::log::set_sink. With no sink installed a call site costs one relaxed atomic
* load and a never-taken branch, so logging is silent and free by default.
*
* @code
* void to_host_logger(void* user_data, cucascade::log::record const& rec) noexcept
* {
* static_cast<my_logger*>(user_data)->write(rec.file, rec.line,
* std::string_view{rec.message, rec.message_len});
* }
* cucascade::log::set_sink(&to_host_logger, &my_logger, cucascade::log::level::debug);
* @endcode
*/

#include <atomic>
#include <cstddef>
#include <cstdint>
#include <format>
#include <source_location>
#include <string_view>

/// Forces default visibility on the process-wide sink state. Without it a `-fvisibility=hidden`
/// build gives each shared object its own sink slot, so a host's @ref cucascade::log::set_sink
/// would be invisible to cuCascade's own call sites.
#if defined(__GNUC__) || defined(__clang__)
#define CUCASCADE_LOG_SHARED_STATE __attribute__((visibility("default")))
#else
#define CUCASCADE_LOG_SHARED_STATE
#endif

namespace cucascade::log {
inline namespace v1 {

/// Record severity, ordered ascending.
enum class level : int {
trace = 0,
debug,
info,
warn,
error,
fatal, ///< Severity only; cuCascade never aborts on its own behalf.
off, ///< Not a message level; pass to @ref set_min_level to mute everything.
};

/// @return Static, human-readable name of @p lvl, e.g. `"WARN"`.
[[nodiscard]] inline char const* to_string(level lvl) noexcept
{
switch (lvl) {
case level::trace: return "TRACE";
case level::debug: return "DEBUG";
case level::info: return "INFO";
case level::warn: return "WARN";
case level::error: return "ERROR";
case level::fatal: return "FATAL";
case level::off: return "OFF";
}
return "?";
}

/// A formatted log record as passed to a sink.
///
/// A POD of pointers and integers, so it can cross into separately-compiled host code without
/// depending on standard library layout choices. All pointers are borrowed for the duration of
/// the sink call; @ref file and @ref function have static storage duration, @ref message does not.
///
/// @note Fields are only ever appended. @ref struct_size is the producer's `sizeof(record)`; a
/// sink must check that it covers a field before reading one it was not compiled against.
struct record {
std::size_t struct_size; ///< `sizeof(record)` as seen by the producer.
level lvl;
std::uint_least32_t line;
std::uint_least32_t column;
char const* message; ///< Not NUL-terminated; pair with @ref message_len.
std::size_t message_len;
char const* file; ///< Originating call site, not the sink.
char const* function;
std::uint64_t thread_id; ///< Linux `gettid()`; 0 where unavailable.
std::int64_t unix_time_ns; ///< Stamped when formatted, not when delivered.
};

/// Host-installed destination for a log record.
///
/// @param user_data Whatever was passed to @ref set_sink; must outlive the last log call.
/// @note May throw — the caller catches and drops the record — but a throwing sink formats
/// messages nobody sees.
using sink_fn = void (*)(void* user_data, record const&);

namespace detail {

/// Immutable once published, so a reader never sees a mismatched fn/user_data pair.
struct sink_state {
sink_fn fn;
void* user_data;
};

CUCASCADE_LOG_SHARED_STATE inline std::atomic<sink_state const*>& sink_slot() noexcept
{
static std::atomic<sink_state const*> slot{nullptr};
return slot;
}

CUCASCADE_LOG_SHARED_STATE inline std::atomic<level>& min_level_slot() noexcept
{
static std::atomic<level> slot{level::info};
return slot;
}

/// Publishes a sink pair, or null for a null @p fn.
///
/// @note The result is never freed: a concurrent @ref vemit may still hold the previous pointer.
/// The first install is served from a static, so the usual single-install case neither
/// allocates nor reports a leak.
sink_state const* publish_sink(sink_fn fn, void* user_data) noexcept;

namespace cucascade::log::detail {
/// Formats and delivers one record. Never throws: these macros run in destructors and noexcept
/// paths, where an escaping exception is `std::terminate`.
void vemit(level lvl,
std::source_location const& location,
std::string_view fmt,
std::format_args args) noexcept;

/// Declared only — used strictly inside an unevaluated context.
/// Type-erases the arguments, so a call site expands to one `make_format_args` and one
/// non-template call rather than a fresh instantiation per argument pack.
template <typename... Args>
void emit(level lvl,
std::source_location const& location,
std::format_string<Args...> fmt,
Args&&... args) noexcept
{
vemit(lvl, location, fmt.get(), std::make_format_args(args...));
}

/// Declared only; used in an unevaluated context to keep compiled-out arguments type-checked.
template <typename... Args>
int ignore(Args&&...) noexcept;

} // namespace cucascade::log::detail
} // namespace detail

/// Installs the process-wide sink and sets the severity threshold.
///
/// Intended to be called once during host start-up. @p fn and whatever @p user_data points at
/// must stay valid while any cuCascade thread can log; see @ref clear_sink for teardown.
///
/// @note Always writes the threshold, so it overrides an earlier @ref set_min_level.
inline void set_sink(sink_fn fn, void* user_data = nullptr, level min_level = level::info) noexcept
{
auto const* const state = detail::publish_sink(fn, user_data);
detail::min_level_slot().store(min_level, std::memory_order_relaxed);
detail::sink_slot().store(state, std::memory_order_release);
}

/// Removes the installed sink. Call before destroying whatever `user_data` pointed at.
inline void clear_sink() noexcept { detail::sink_slot().store(nullptr, std::memory_order_release); }

/// Sets the severity threshold without touching the installed sink.
///
/// Mirror the host logger's own level here so filtering happens once, in @ref enabled, rather
/// than formatting records the backend will discard.
inline void set_min_level(level min_level) noexcept
{
detail::min_level_slot().store(min_level, std::memory_order_relaxed);
}

/// @return The installed sink, or null if none.
[[nodiscard]] inline sink_fn sink() noexcept
{
auto const* const state = detail::sink_slot().load(std::memory_order_acquire);
return state != nullptr ? state->fn : nullptr;
}

/// @return The `user_data` passed to @ref set_sink, or null if no sink is installed.
[[nodiscard]] inline void* sink_user_data() noexcept
{
auto const* const state = detail::sink_slot().load(std::memory_order_acquire);
return state != nullptr ? state->user_data : nullptr;
}

/// @return The current severity threshold.
[[nodiscard]] inline level min_level() noexcept
{
return detail::min_level_slot().load(std::memory_order_relaxed);
}

/// @return Whether a record at @p lvl would reach a sink.
/// @note The guard on every call site, so it stays one relaxed load and two integer compares.
[[nodiscard]] inline bool enabled(level lvl) noexcept
{
return detail::sink_slot().load(std::memory_order_relaxed) != nullptr && lvl >= min_level();
}

/// Built-in sink writing `<timestamp> [LEVEL] <tid> file:line: message` to stderr. Ignores
/// @p user_data. Not installed by default; see @ref use_stderr_sink.
void stderr_sink(void* user_data, record const& rec) noexcept;

/// Routes records to @ref stderr_sink, for debugging cuCascade standalone. An embedding host
/// should install its own sink instead.
inline void use_stderr_sink(level min_level = level::info) noexcept
{
set_sink(&stderr_sink, nullptr, min_level);
}

} // namespace v1
} // namespace cucascade::log

// clang-format off

/// Compile-time severity floor, as the underlying value of a cucascade::log::level (0 = trace
/// ... 6 = off). Call sites below it are removed by the preprocessor, arguments included.
///
/// @note Read only inside the macros below, never in an inline function body or a type, so the
/// library and a consumer may define it differently without an ODR violation.
#ifndef CUCASCADE_MIN_LOG_LEVEL
#define CUCASCADE_MIN_LOG_LEVEL 0
#endif

/// Discards the arguments in an unevaluated context: still type-checked and "used", never run.
#define CUCASCADE_LOG_NOOP(...) static_cast<void>(sizeof(::cucascade::log::detail::ignore(__VA_ARGS__)))

#define CUCASCADE_LOG_TRACE(...) CUCASCADE_LOG_NOOP(__VA_ARGS__)
#define CUCASCADE_LOG_DEBUG(...) CUCASCADE_LOG_NOOP(__VA_ARGS__)
#define CUCASCADE_LOG_INFO(...) CUCASCADE_LOG_NOOP(__VA_ARGS__)
#define CUCASCADE_LOG_WARN(...) CUCASCADE_LOG_NOOP(__VA_ARGS__)
#define CUCASCADE_LOG_ERROR(...) CUCASCADE_LOG_NOOP(__VA_ARGS__)
#define CUCASCADE_LOG_FATAL(...) CUCASCADE_LOG_NOOP(__VA_ARGS__)
#define CUCASCADE_LOG_IMPL(lvl, ...) \
do { \
if constexpr (static_cast<int>(lvl) >= (CUCASCADE_MIN_LOG_LEVEL)) { \
if (::cucascade::log::enabled(lvl)) { \
::cucascade::log::detail::emit( \
(lvl), std::source_location::current(), __VA_ARGS__); \
} \
} else { \
CUCASCADE_LOG_NOOP(__VA_ARGS__); \
} \
} while (false)

#define CUCASCADE_LOG_TRACE(...) CUCASCADE_LOG_IMPL(::cucascade::log::level::trace, __VA_ARGS__)
#define CUCASCADE_LOG_DEBUG(...) CUCASCADE_LOG_IMPL(::cucascade::log::level::debug, __VA_ARGS__)
#define CUCASCADE_LOG_INFO(...) CUCASCADE_LOG_IMPL(::cucascade::log::level::info, __VA_ARGS__)
#define CUCASCADE_LOG_WARN(...) CUCASCADE_LOG_IMPL(::cucascade::log::level::warn, __VA_ARGS__)
#define CUCASCADE_LOG_ERROR(...) CUCASCADE_LOG_IMPL(::cucascade::log::level::error, __VA_ARGS__)
#define CUCASCADE_LOG_FATAL(...) CUCASCADE_LOG_IMPL(::cucascade::log::level::fatal, __VA_ARGS__)
// clang-format on
28 changes: 28 additions & 0 deletions src/log/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# =============================================================================
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES.
# All rights reserved. SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
# =============================================================================

# Record delivery (vemit), sink publication and the built-in stderr sink. Only
# the level check and the sink slot stay inline in
# include/cucascade/log/logging.hpp, so the hot path remains header-only while
# <format>, <chrono> and <ctime> stay out of every translation unit that logs.
#
# Logging therefore requires linking cuCascade, and is unavailable in a
# topology-only build, which does not produce cucascade_objects.
if(TARGET cucascade_objects)
target_sources(cucascade_objects
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/logging.cpp)

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.

seems like we only build logging.cpp but no stderr_sink.cpp?

endif()
Loading