Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8d7f3a5
fix(scope): bound and index PM sample attribution
CodingInAVan Jul 27, 2026
81aa157
feat(counter): named counters for rate-based triggers
CodingInAVan Jul 27, 2026
eeaf636
feat(counter): share the registry across modules via a C ABI runtime
CodingInAVan Jul 27, 2026
467fe7b
build(counter): ship the counter runtime beside every consumer
CodingInAVan Jul 27, 2026
0b75ec7
feat(metrics): freshness-aware metric sources for rule triggers
CodingInAVan Jul 27, 2026
805e6fc
test(metrics): name custom counters uniquely per test
CodingInAVan Jul 27, 2026
7e6f78c
feat(deep-window): rule evaluator with gates and validated config
CodingInAVan Jul 27, 2026
438ed40
fix(test): include <cmath> for std::nan on gcc
CodingInAVan Jul 27, 2026
d1030ea
feat(deep-window): wire the rule in and record what it concluded
CodingInAVan Jul 27, 2026
c42ace4
fix(deep-window): review fixes across the counter and rule paths
CodingInAVan Jul 27, 2026
115dd10
feat(deep-window): emit terminal rule outcomes at the transition
CodingInAVan Jul 27, 2026
bd1be9f
test(counter): make the cross-module check actually run, and fail
CodingInAVan Jul 27, 2026
f7efe6b
fix(deep-window): five review findings on discovery, arbitration and …
CodingInAVan Jul 27, 2026
cbc2a2e
fix(deep-window): five more review findings, and honest test coverage
CodingInAVan Jul 27, 2026
05be438
fix(deep-window): actually publish the queued flag under the lock
CodingInAVan Jul 27, 2026
3e0449a
fix(deep-window): close the consumer-side pending race, report partia…
CodingInAVan Jul 27, 2026
79bc016
fix(metrics): count bucket-level trimming, and report it on the same …
CodingInAVan Jul 27, 2026
f707311
feat(cli): --passes and --deep-* are separate execution modes
CodingInAVan Jul 27, 2026
09bb941
fix(deep-window): stop claiming PM is prepared before the window opens
CodingInAVan Jul 27, 2026
67e8354
fix(deep-window): prepare PM before bounded arm
CodingInAVan Jul 27, 2026
924a670
fix(deep-window): a refusal has to say why, and prepared has to be ea…
CodingInAVan Jul 27, 2026
c40a62f
fix(deep-window): a trigger the run did not ask for, and a verdict it…
CodingInAVan Jul 28, 2026
a18810b
fix(upload): require a successful agent exit
CodingInAVan Jul 28, 2026
021ff7a
replace GFL_LOG_ERROR in the upload related call with fprint
CodingInAVan Jul 28, 2026
80164d7
feat(deep-window): add counter telemetry and safe CUDA boundaries
CodingInAVan Jul 28, 2026
841de4f
fix(monitor): omit kernel rows when real activity records never arrived
CodingInAVan Jul 28, 2026
a5be209
fix(monitor): never infer real kernel timing from host gaps
CodingInAVan Jul 28, 2026
6d90f04
build: stop ignoring the cmake/ source directory
CodingInAVan Jul 29, 2026
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ build/
build-*/
build_tests/
cmake-build-*/
cmake/
CMakeFiles/
CMakeCache.txt
wget-log*
Expand Down
238 changes: 237 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,71 @@ option(BUILD_TESTING "Build the testing tree." ON)
add_library(gpufl STATIC)
add_library(gpufl::gpufl ALIAS gpufl)

# One counter registry per PROCESS, not per module. `gpufl` is static and is
# linked separately into gpufl_inject, the Python extension and any host
# application, so without this each would hold its own registry - a profiled
# target would tick one and the injected evaluator would read another. Only a C
# ABI crosses this boundary; see include/gpufl/abi/gpufl_counter_abi.h.
add_library(gpufl_counter_runtime SHARED
runtime/counter_runtime.cpp
include/gpufl/core/counter_registry.cpp
)
target_include_directories(gpufl_counter_runtime PUBLIC include)

# Installed unconditionally, NOT only with the Python bindings. A launcher-only
# install that omits it leaves the injected evaluator and the target application
# each falling back to their own in-process registry - the exact split this
# library exists to prevent, and one that shows up as a counter reading Missing
# forever rather than as a missing file.
install(TARGETS gpufl_counter_runtime
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
set_target_properties(gpufl_counter_runtime PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
)

# Counters WITHOUT the profiler. What an application links to tick counters a
# rule can watch (`gpufl::counter("tokens").add(n)`), while the profiling
# itself arrives from outside via `gpufl trace`. Linking gpufl::gpufl for two
# lines of instrumentation reads as "I am embedding the SDK" - and the target
# name is most of the answer to that confusion; nothing here starts a session,
# and none of the SDK's dependencies (CUPTI, NVML, zlib, OpenSSL, the
# uploader) are in it.
#
# `gpufl` layers ON TOP of this rather than compiling the same sources again:
# two archives both defining the registry is a duplicate-symbol error for
# anyone who links both.
add_library(gpufl_counters STATIC
include/gpufl/core/counter_registry.cpp
include/gpufl/core/counter_api.cpp
include/gpufl/core/counter_provider.cpp
include/gpufl/core/debug_logger.cpp
)
add_library(gpufl::counters ALIAS gpufl_counters)
# The ALIAS above only exists in this build tree. The INSTALLED name comes
# from EXPORT_NAME plus the export namespace, and without this the package
# would ship the target as gpufl::gpufl_counters while every document says
# gpufl::counters.
set_target_properties(gpufl_counters PROPERTIES EXPORT_NAME counters)
target_include_directories(gpufl_counters
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_compile_features(gpufl_counters INTERFACE cxx_std_17)
# The provider dlopens the shared runtime and the registry uses std::mutex;
# consumers must not have to know either. Threads::Threads rather than a raw
# `pthread`: the raw name would be exported verbatim into the targets file and
# bypass whatever the consumer's toolchain says threading means there.
find_package(Threads REQUIRED)
target_link_libraries(gpufl_counters PUBLIC Threads::Threads)
if(UNIX)
target_link_libraries(gpufl_counters PUBLIC ${CMAKE_DL_LIBS})
endif()
set_target_properties(gpufl_counters PROPERTIES POSITION_INDEPENDENT_CODE ON)

target_include_directories(gpufl
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
Expand Down Expand Up @@ -79,6 +144,11 @@ target_compile_definitions(gpufl PUBLIC
# Enable PIC for static library (required when linking into shared libraries like Python modules)
set_target_properties(gpufl PROPERTIES POSITION_INDEPENDENT_CODE ON)

# PUBLIC: consumers of the full SDK get the counter layer through this link,
# and the counter sources live in gpufl_counters ONLY - see that target for
# why they must not be compiled into both archives.
target_link_libraries(gpufl PUBLIC gpufl_counters)

target_sources(gpufl PRIVATE
include/gpufl/core/dictionary_manager.cpp
include/gpufl/core/sass_compressor.cpp
Expand Down Expand Up @@ -107,12 +177,16 @@ target_sources(gpufl PRIVATE
include/gpufl/core/runtime.cpp
include/gpufl/core/backend_factory.cpp
include/gpufl/core/monitor_adapter.cpp
include/gpufl/core/nvtx_counters.cpp
include/gpufl/core/metric_id.cpp
include/gpufl/core/deep_window_rule.cpp
include/gpufl/core/deep_window_rules.cpp
include/gpufl/core/metric_registry.cpp
include/gpufl/core/monitor_batch_manager.cpp
include/gpufl/core/monitor_record_builders.cpp
include/gpufl/core/monitor.cpp
include/gpufl/core/gpufl.cpp
include/gpufl/core/common.cpp
include/gpufl/core/debug_logger.cpp
include/gpufl/core/stack_trace.cpp
include/gpufl/core/itanium_demangle.cpp
include/gpufl/core/scope_registry.cpp
Expand Down Expand Up @@ -147,7 +221,15 @@ else()
GIT_REPOSITORY https://github.com/madler/zlib.git
GIT_TAG v1.3.1
)
# zlib's own install rules cache ABSOLUTE destinations at configure time
# (INSTALL_LIB_DIR = ${CMAKE_INSTALL_PREFIX}/lib), so they ignore
# `cmake --install --prefix` and try to write into Program Files - which is
# how the install-tree consumer test failed before this. Skip them all; the
# archive is installed below with a relative destination that the prefix
# override can move.
set(SKIP_INSTALL_ALL ON)
FetchContent_MakeAvailable(zlib)
unset(SKIP_INSTALL_ALL)
# zlib.h lives in the source dir; zconf.h is generated in the binary dir.
# Add both privately to gpufl - consumers never include zlib headers directly.
target_link_libraries(gpufl PRIVATE zlibstatic)
Expand Down Expand Up @@ -188,6 +270,12 @@ FetchContent_Declare(
# cpp-httplib's CMakeLists defines build flags for its own tests / examples.
# Turn them off so we only build the header-only interface target.
set(HTTPLIB_COMPILE OFF CACHE BOOL "" FORCE)
# HTTPLIB_INSTALL stays ON, deliberately: gpufl's link interface records
# httplib::httplib, and install(EXPORT gpufl_clientTargets) refuses to
# generate unless that target is in SOME export set - httplib's own is what
# satisfies the check. Turning it off fails the whole configure. The price is
# cpp-httplib's headers and package files landing in the install prefix; the
# fix that removes them is reworking the SDK export, not this switch.
FetchContent_MakeAvailable(httplib)

find_package(OpenSSL QUIET)
Expand Down Expand Up @@ -635,7 +723,28 @@ if(BUILD_GPUFL_INJECT AND ((UNIX AND NOT APPLE) OR WIN32))
add_library(gpufl_inject SHARED
include/gpufl/inject/inject_entry.cpp
)
if(UNIX AND NOT APPLE AND GPUFL_HAS_CUDA)
# Compile CUDA boundary wrappers against the toolkit's official ABI.
# inject_entry.cpp keeps its header-free fallback for no-CUDA builds.
target_sources(gpufl_inject PRIVATE
include/gpufl/inject/cuda_interpose_linux.cpp
)
target_compile_definitions(gpufl_inject PRIVATE
GPUFL_TYPED_CUDA_INTERPOSE=1
)
endif()
target_link_libraries(gpufl_inject PRIVATE gpufl::gpufl)

# The injected DLL is loaded by the CUDA driver into the profiled target,
# whose DLL search path has no reason to include our bin directory. The
# provider resolves the runtime from the injection DLL's own location, so
# it has to be there.
add_custom_command(TARGET gpufl_inject POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:gpufl_counter_runtime>
$<TARGET_FILE_DIR:gpufl_inject>
COMMENT "Colocating gpufl_counter_runtime with gpufl_inject"
)
if(CUDAToolkit_INCLUDE_DIRS)
target_include_directories(gpufl_inject PRIVATE ${CUDAToolkit_INCLUDE_DIRS})
endif()
Expand Down Expand Up @@ -731,6 +840,10 @@ endfunction()

if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR AND BUILD_TESTING)
enable_testing()
# Keep googletest's install rules out of the package: the install-tree
# consumer test installs this build into a scratch prefix, and gtest
# headers and archives are not something gpufl_client ships.
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
add_subdirectory(tests)
endif()

Expand Down Expand Up @@ -758,6 +871,15 @@ if(BUILD_PYTHON)

target_link_libraries(_gpufl_client PRIVATE gpufl::gpufl)

# Same for the Python extension: it may be the first module to bind, in
# which case it is the one that loads the runtime for the whole process.
add_custom_command(TARGET _gpufl_client POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:gpufl_counter_runtime>
$<TARGET_FILE_DIR:_gpufl_client>
COMMENT "Colocating gpufl_counter_runtime with _gpufl_client"
)

# If CUDA is available, link it to the Python module
if(GPUFL_HAS_CUDA)
target_link_libraries(_gpufl_client PRIVATE CUDA::cudart)
Expand All @@ -776,6 +898,28 @@ include(GNUInstallDirs)
# Install header files
install(DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
COMPONENT counters
)

# Two export sets, deliberately. The generated targets file REFUSES to load
# when any imported target it references is absent, and the SDK records its
# private dependencies (httplib, zlib, OpenSSL, CUDA) in its link interface -
# so shipping counters in the SDK's set made `find_package(gpufl_client)` fail
# for a counters-only consumer with "httplib::httplib is missing". Measured,
# not theoretical. The counters set references nothing but system dl/pthread
# and loads anywhere; the SDK set loads only for consumers that opt in AND
# have its dependencies.
install(TARGETS gpufl_counters
EXPORT gpufl_countersTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
COMPONENT counters
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(EXPORT gpufl_countersTargets
FILE gpufl_countersTargets.cmake
NAMESPACE gpufl::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/gpufl_client
COMPONENT counters
)

install(TARGETS gpufl
Expand All @@ -791,3 +935,95 @@ install(EXPORT gpufl_clientTargets
NAMESPACE gpufl::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/gpufl_client
)

# What makes `find_package(gpufl_client)` actually resolve: CMake looks for
# gpufl_clientConfig.cmake, and a bare Targets file is not one. Version file
# alongside so `find_package(gpufl_client 1.2)` can hold a floor.
include(CMakePackageConfigHelpers)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/cmake/gpufl_clientConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/gpufl_clientConfig.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/gpufl_client
)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/gpufl_clientConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/gpufl_clientConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/gpufl_clientConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/gpufl_client
COMPONENT counters
)

# Cross-module counter sharing. Cannot be proven from inside one executable - it
# needs the Python extension AND the shared runtime as separate modules - so it
# runs as its own process rather than as a gtest case.
#
# Lives here, not in tests/, because tests/ is processed before _gpufl_client is
# defined; gating on that target there is always false and the check silently
# never registers - which is how it went unrun in the first place.
#
# Staged into the build tree rather than run against the source tree: the
# extension has to sit INSIDE the package to be importable, and copying a build
# artifact into source would leave it there for every later run.
if(BUILD_TESTING AND BUILD_PYTHON AND TARGET _gpufl_client
AND TARGET gpufl_counter_runtime
AND CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
# The interpreter the EXTENSION was built for, not whichever one a fresh
# find_package picks. A different minor version cannot import a cp3XX
# module at all, so the check would silently run against the stub and
# report that the registries are split when nothing of the sort happened.
if(DEFINED Python_EXECUTABLE)
set(GPUFL_XMOD_PYTHON "${Python_EXECUTABLE}")
elseif(DEFINED PYTHON_EXECUTABLE)
set(GPUFL_XMOD_PYTHON "${PYTHON_EXECUTABLE}")
endif()
if(GPUFL_XMOD_PYTHON)
set(GPUFL_XMOD_STAGE ${CMAKE_BINARY_DIR}/xmod_stage)
add_custom_target(counter_xmod_stage ALL
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/python/gpufl
${GPUFL_XMOD_STAGE}/python/gpufl
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:_gpufl_client>
${GPUFL_XMOD_STAGE}/python/gpufl/
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:gpufl_counter_runtime>
${GPUFL_XMOD_STAGE}/python/gpufl/
COMMENT "Staging the cross-module counter check")
add_dependencies(counter_xmod_stage _gpufl_client gpufl_counter_runtime)

add_test(NAME counter_cross_module
COMMAND ${GPUFL_XMOD_PYTHON}
${CMAKE_SOURCE_DIR}/scripts/counter_cross_module_check.py)
set_tests_properties(counter_cross_module PROPERTIES
ENVIRONMENT "GPUFL_REPO=${GPUFL_XMOD_STAGE}")
endif()
endif()

# The install-tree consumer, as CTest rather than a manual run: install into a
# scratch prefix, configure the fixture consumer against it with find_package,
# build it, run it. This is the only test that can see EXPORT_NAME, the Config
# file install, and the counters export set's isolation from the SDK's
# dependencies - the unit suite links the build tree and passes no matter what
# the package ships.
if(BUILD_TESTING)
enable_testing()
add_test(NAME counters_package_consumer
COMMAND ${CMAKE_COMMAND}
-DGPUFL_BINARY_DIR=${CMAKE_BINARY_DIR}
-DGPUFL_SOURCE_DIR=${CMAKE_SOURCE_DIR}
-DGPUFL_CONFIG=$<CONFIG>
-DGPUFL_GENERATOR=${CMAKE_GENERATOR}
-DGPUFL_PLATFORM=${CMAKE_GENERATOR_PLATFORM}
-DGPUFL_TOOLSET=${CMAKE_GENERATOR_TOOLSET}
-P ${CMAKE_SOURCE_DIR}/tests/package/counters_consumer_check.cmake)
# Self-contained: the script builds gpufl_counters itself and installs
# ONLY the counters component, so this test does not depend on - and
# cannot be broken by - the rest of the project's install rules. A Debug
# run without gpufl_counter_runtime built proved why that isolation
# matters.
set_tests_properties(counters_package_consumer PROPERTIES TIMEOUT 300)
endif()
24 changes: 24 additions & 0 deletions cmake/gpufl_clientConfig.cmake.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@PACKAGE_INIT@

# find_package(gpufl_client) entry point. What it guarantees is the counters
# layer:
#
# find_package(gpufl_client REQUIRED)
# target_link_libraries(app PRIVATE gpufl::counters)
#
# Threads is the one real dependency in its link interface, resolved here so
# the consumer does not have to know.
#
# The full SDK target is NOT loaded by this file. Its targets file records
# private dependencies (httplib, zlib, OpenSSL, CUDA) and refuses to load
# unless the consumer already provides those imported targets - publishing it
# as a component would be documenting a flow that only works by accident.
# Consuming the SDK from an install tree needs find_dependency() wiring that
# does not exist yet.

include(CMakeFindDependencyMacro)
find_dependency(Threads)

include("${CMAKE_CURRENT_LIST_DIR}/gpufl_countersTargets.cmake")

check_required_components(gpufl_client)
13 changes: 13 additions & 0 deletions daemon/launcher/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ add_executable(gpufl_launcher
cli_parse.cpp
info_command.cpp
trace_command_common.cpp
deep_window_env.cpp
${GPUFL_LAUNCHER_TRACE_IMPL}
monitor_command.cpp
../monitor/monitor_runner.cpp
Expand Down Expand Up @@ -74,6 +75,18 @@ if(WIN32 AND TARGET gpufl_inject)
COMMENT "Colocating gpufl_inject.dll next to gpufl.exe")
endif()

# The counter runtime travels with the inject DLL. Once injected, gpufl_inject
# resolves it from its OWN directory - the target process's search path is not
# ours to rely on - so it has to be wherever the inject DLL ended up.
if(WIN32 AND TARGET gpufl_counter_runtime)
add_dependencies(gpufl_launcher gpufl_counter_runtime)
add_custom_command(TARGET gpufl_launcher POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:gpufl_counter_runtime>
$<TARGET_FILE_DIR:gpufl_launcher>
COMMENT "Colocating gpufl_counter_runtime.dll next to gpufl.exe")
endif()

# On Windows, copy CUPTI / NVPERF DLLs next to gpufl.exe (= beside the
# colocated gpufl_inject.dll) so the inject DLL's dependencies resolve when
# the driver loads it into the target. trace_command_win.cpp also prepends
Expand Down
Loading
Loading