diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index b737c6abe..610f45c1e 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -148,4 +148,13 @@ if (UMPIRE_ENABLE_BENCHMARKS) blt_add_benchmark( NAME inspector_benchmarks COMMAND inspector_benchmarks) + + blt_add_executable( + NAME introspection_level_benchmarks + SOURCES introspection_level_benchmarks.cpp + DEPENDS_ON ${benchmark_depends}) + + blt_add_benchmark( + NAME introspection_level_benchmarks + COMMAND introspection_level_benchmarks) endif() diff --git a/benchmarks/introspection_level_benchmarks.cpp b/benchmarks/introspection_level_benchmarks.cpp new file mode 100644 index 000000000..d4c10635a --- /dev/null +++ b/benchmarks/introspection_level_benchmarks.cpp @@ -0,0 +1,248 @@ +////////////////////////////////////////////////////////////////////////////// +// Copyright (c) 2016-26, Lawrence Livermore National Security, LLC and Umpire +// project contributors. See the COPYRIGHT file for details. +// +// SPDX-License-Identifier: (MIT) +////////////////////////////////////////////////////////////////////////////// +#include +#include + +#include "benchmark/benchmark.h" + +#include "umpire/Allocator.hpp" +#include "umpire/ResourceManager.hpp" +#include "umpire/Introspection.hpp" + +static const std::size_t NUM_ALLOCATIONS = 10000; +static const std::size_t ALLOC_SIZE = 1024; + +// Get current introspection level (set via UMPIRE_INTROSPECTION_LEVEL env var) +static umpire::IntrospectionLevel getCurrentLevel() { + auto& rm = umpire::ResourceManager::getInstance(); + return rm.getIntrospectionLevel(); +} + +// Benchmark allocation/deallocation overhead +static void BM_Allocate(benchmark::State& state) { + auto& rm = umpire::ResourceManager::getInstance(); + auto alloc = rm.getAllocator("HOST"); + std::vector ptrs; + ptrs.reserve(NUM_ALLOCATIONS); + + for (auto _ : state) { + state.PauseTiming(); + ptrs.clear(); + state.ResumeTiming(); + + for (std::size_t i = 0; i < NUM_ALLOCATIONS; ++i) { + void* ptr = alloc.allocate(ALLOC_SIZE); + ptrs.push_back(ptr); + } + + state.PauseTiming(); + for (auto ptr : ptrs) { + alloc.deallocate(ptr); + } + state.ResumeTiming(); + } + + state.SetItemsProcessed(state.iterations() * NUM_ALLOCATIONS); +} +BENCHMARK(BM_Allocate); + +// Benchmark hasAllocator queries (Off mode: always returns false, skip) +static void BM_HasAllocator(benchmark::State& state) { + auto& rm = umpire::ResourceManager::getInstance(); + + if (getCurrentLevel() == umpire::IntrospectionLevel::Off) { + state.SkipWithError("hasAllocator not available in Off mode"); + return; + } + + auto alloc = rm.getAllocator("HOST"); + std::vector ptrs; + ptrs.reserve(NUM_ALLOCATIONS); + + for (std::size_t i = 0; i < NUM_ALLOCATIONS; ++i) { + ptrs.push_back(alloc.allocate(ALLOC_SIZE)); + } + + for (auto _ : state) { + for (auto ptr : ptrs) { + benchmark::DoNotOptimize(rm.hasAllocator(ptr)); + } + } + + for (auto ptr : ptrs) { + alloc.deallocate(ptr); + } + + state.SetItemsProcessed(state.iterations() * NUM_ALLOCATIONS); +} +BENCHMARK(BM_HasAllocator); + +// Benchmark getAllocator queries (Off mode: throws, skip) +static void BM_GetAllocator(benchmark::State& state) { + auto& rm = umpire::ResourceManager::getInstance(); + + if (getCurrentLevel() == umpire::IntrospectionLevel::Off) { + state.SkipWithError("getAllocator not available in Off mode"); + return; + } + + auto alloc = rm.getAllocator("HOST"); + std::vector ptrs; + ptrs.reserve(NUM_ALLOCATIONS); + + for (std::size_t i = 0; i < NUM_ALLOCATIONS; ++i) { + ptrs.push_back(alloc.allocate(ALLOC_SIZE)); + } + + for (auto _ : state) { + for (auto ptr : ptrs) { + benchmark::DoNotOptimize(rm.getAllocator(ptr)); + } + } + + for (auto ptr : ptrs) { + alloc.deallocate(ptr); + } + + state.SetItemsProcessed(state.iterations() * NUM_ALLOCATIONS); +} +BENCHMARK(BM_GetAllocator); + +// Benchmark getSize queries (only available in On mode) +static void BM_GetSize(benchmark::State& state) { + auto& rm = umpire::ResourceManager::getInstance(); + + if (getCurrentLevel() != umpire::IntrospectionLevel::On) { + state.SkipWithError("getSize only available in On mode"); + return; + } + + auto alloc = rm.getAllocator("HOST"); + std::vector ptrs; + ptrs.reserve(NUM_ALLOCATIONS); + + for (std::size_t i = 0; i < NUM_ALLOCATIONS; ++i) { + ptrs.push_back(alloc.allocate(ALLOC_SIZE)); + } + + for (auto _ : state) { + for (auto ptr : ptrs) { + benchmark::DoNotOptimize(rm.getSize(ptr)); + } + } + + for (auto ptr : ptrs) { + alloc.deallocate(ptr); + } + + state.SetItemsProcessed(state.iterations() * NUM_ALLOCATIONS); +} +BENCHMARK(BM_GetSize); + +// Benchmark copy operations (Off mode: not available, skip) +static void BM_Copy(benchmark::State& state) { + auto& rm = umpire::ResourceManager::getInstance(); + + if (getCurrentLevel() == umpire::IntrospectionLevel::Off) { + state.SkipWithError("copy not available in Off mode"); + return; + } + + auto alloc = rm.getAllocator("HOST"); + void* src = alloc.allocate(ALLOC_SIZE); + void* dst = alloc.allocate(ALLOC_SIZE); + + for (auto _ : state) { + rm.copy(dst, src, ALLOC_SIZE); + } + + alloc.deallocate(src); + alloc.deallocate(dst); + + state.SetItemsProcessed(state.iterations()); + state.SetBytesProcessed(state.iterations() * ALLOC_SIZE); +} +BENCHMARK(BM_Copy); + +// Benchmark memset operations (Off mode: not available, skip) +static void BM_Memset(benchmark::State& state) { + auto& rm = umpire::ResourceManager::getInstance(); + + if (getCurrentLevel() == umpire::IntrospectionLevel::Off) { + state.SkipWithError("memset not available in Off mode"); + return; + } + + auto alloc = rm.getAllocator("HOST"); + void* ptr = alloc.allocate(ALLOC_SIZE); + + for (auto _ : state) { + rm.memset(ptr, 0, ALLOC_SIZE); + } + + alloc.deallocate(ptr); + + state.SetItemsProcessed(state.iterations()); + state.SetBytesProcessed(state.iterations() * ALLOC_SIZE); +} +BENCHMARK(BM_Memset); + +// Benchmark memory overhead (allocation records storage) +// Tests how performance scales with increasing allocation count +static void BM_MemoryOverhead(benchmark::State& state) { + auto& rm = umpire::ResourceManager::getInstance(); + auto alloc = rm.getAllocator("HOST"); + std::vector ptrs; + + for (auto _ : state) { + state.PauseTiming(); + ptrs.clear(); + ptrs.reserve(state.range(0)); + state.ResumeTiming(); + + for (int64_t i = 0; i < state.range(0); ++i) { + ptrs.push_back(alloc.allocate(ALLOC_SIZE)); + } + + state.PauseTiming(); + for (auto ptr : ptrs) { + alloc.deallocate(ptr); + } + state.ResumeTiming(); + } + + state.SetItemsProcessed(state.iterations() * state.range(0)); +} +BENCHMARK(BM_MemoryOverhead)->Range(100, 100000); + +int main(int argc, char** argv) { + auto& rm = umpire::ResourceManager::getInstance(); + auto level = rm.getIntrospectionLevel(); + + std::cout << "============================================\n"; + std::cout << "Introspection Level Benchmarks\n"; + std::cout << "============================================\n"; + std::cout << "Current level: "; + switch (level) { + case umpire::IntrospectionLevel::Off: + std::cout << "Off (zero overhead, no introspection)\n"; + break; + case umpire::IntrospectionLevel::Basic: + std::cout << "Basic (runtime API inference, zero storage)\n"; + break; + case umpire::IntrospectionLevel::On: + std::cout << "On (full tracking with metadata)\n"; + break; + } + std::cout << "============================================\n\n"; + + ::benchmark::Initialize(&argc, argv); + if (::benchmark::ReportUnrecognizedArguments(argc, argv)) return 1; + ::benchmark::RunSpecifiedBenchmarks(); + ::benchmark::Shutdown(); + return 0; +} diff --git a/benchmarks/run_introspection_benchmarks.sh b/benchmarks/run_introspection_benchmarks.sh new file mode 100755 index 000000000..7bfe784ff --- /dev/null +++ b/benchmarks/run_introspection_benchmarks.sh @@ -0,0 +1,119 @@ +#!/bin/bash +############################################################################## +# Copyright (c) 2016-26, Lawrence Livermore National Security, LLC and Umpire +# project contributors. See the COPYRIGHT file for details. +# +# SPDX-License-Identifier: (MIT) +############################################################################## + +# Script to run introspection level benchmarks and generate comparison report + +BENCHMARK_BIN="${1:-./bin/introspection_level_benchmarks}" + +if [ ! -f "$BENCHMARK_BIN" ]; then + echo "Error: Benchmark binary not found at $BENCHMARK_BIN" + echo "Usage: $0 [path_to_benchmark_binary]" + echo "Example: $0 ./build/bin/introspection_level_benchmarks" + exit 1 +fi + +OUTPUT_DIR="benchmark_results_$(date +%Y%m%d_%H%M%S)" +mkdir -p "$OUTPUT_DIR" + +echo "============================================" +echo "Introspection Level Benchmark Suite" +echo "============================================" +echo "" +echo "Output directory: $OUTPUT_DIR" +echo "" +echo "Running benchmarks for all three levels..." +echo "" + +# Run benchmarks for each introspection level +for LEVEL in off basic on; do + echo "============================================" + echo "Running: Level = $LEVEL" + echo "============================================" + + UMPIRE_INTROSPECTION_LEVEL="$LEVEL" "$BENCHMARK_BIN" \ + --benchmark_out="$OUTPUT_DIR/results_${LEVEL}.json" \ + --benchmark_out_format=json \ + --benchmark_counters_tabular=true \ + | tee "$OUTPUT_DIR/results_${LEVEL}.txt" + + echo "" +done + +echo "" +echo "============================================" +echo "Results saved to $OUTPUT_DIR/" +echo " - results_off.txt/json: Off mode results" +echo " - results_basic.txt/json: Basic mode results" +echo " - results_on.txt/json: On mode results" +echo "============================================" +echo "" + +# Generate comparison summary +echo "Performance Comparison Summary" | tee "$OUTPUT_DIR/comparison.txt" +echo "==============================" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "" | tee -a "$OUTPUT_DIR/comparison.txt" + +echo "ALLOCATION PERFORMANCE (lower is better):" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "-----------------------------------------" | tee -a "$OUTPUT_DIR/comparison.txt" +for LEVEL in off basic on; do + echo "" | tee -a "$OUTPUT_DIR/comparison.txt" + echo "[$LEVEL]:" | tee -a "$OUTPUT_DIR/comparison.txt" + grep "^BM_Allocate " "$OUTPUT_DIR/results_${LEVEL}.txt" 2>/dev/null | head -1 | tee -a "$OUTPUT_DIR/comparison.txt" +done +echo "" | tee -a "$OUTPUT_DIR/comparison.txt" + +echo "QUERY PERFORMANCE - hasAllocator (lower is better):" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "---------------------------------------------------" | tee -a "$OUTPUT_DIR/comparison.txt" +for LEVEL in basic on; do + echo "" | tee -a "$OUTPUT_DIR/comparison.txt" + echo "[$LEVEL]:" | tee -a "$OUTPUT_DIR/comparison.txt" + grep "^BM_HasAllocator " "$OUTPUT_DIR/results_${LEVEL}.txt" 2>/dev/null | head -1 | tee -a "$OUTPUT_DIR/comparison.txt" +done +echo "(Off mode: N/A - always returns false)" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "" | tee -a "$OUTPUT_DIR/comparison.txt" + +echo "QUERY PERFORMANCE - getAllocator (lower is better):" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "----------------------------------------------------" | tee -a "$OUTPUT_DIR/comparison.txt" +for LEVEL in basic on; do + echo "" | tee -a "$OUTPUT_DIR/comparison.txt" + echo "[$LEVEL]:" | tee -a "$OUTPUT_DIR/comparison.txt" + grep "^BM_GetAllocator " "$OUTPUT_DIR/results_${LEVEL}.txt" 2>/dev/null | head -1 | tee -a "$OUTPUT_DIR/comparison.txt" +done +echo "(Off mode: N/A - throws exception)" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "" | tee -a "$OUTPUT_DIR/comparison.txt" + +echo "COPY PERFORMANCE (lower is better):" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "-----------------------------------" | tee -a "$OUTPUT_DIR/comparison.txt" +for LEVEL in basic on; do + echo "" | tee -a "$OUTPUT_DIR/comparison.txt" + echo "[$LEVEL]:" | tee -a "$OUTPUT_DIR/comparison.txt" + grep "^BM_Copy " "$OUTPUT_DIR/results_${LEVEL}.txt" 2>/dev/null | head -1 | tee -a "$OUTPUT_DIR/comparison.txt" +done +echo "(Off mode: N/A - not available)" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "" | tee -a "$OUTPUT_DIR/comparison.txt" + +echo "MEMORY OVERHEAD SCALING (time for 100K allocations):" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "-----------------------------------------------------" | tee -a "$OUTPUT_DIR/comparison.txt" +for LEVEL in off basic on; do + echo "" | tee -a "$OUTPUT_DIR/comparison.txt" + echo "[$LEVEL]:" | tee -a "$OUTPUT_DIR/comparison.txt" + grep "^BM_MemoryOverhead/100000 " "$OUTPUT_DIR/results_${LEVEL}.txt" 2>/dev/null | tee -a "$OUTPUT_DIR/comparison.txt" +done +echo "" | tee -a "$OUTPUT_DIR/comparison.txt" + +echo "" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "KEY FINDINGS:" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "------------" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "- Off/Basic should have similar allocation performance (no tracking overhead)" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "- On mode has tracking overhead (Judy array insertions)" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "- Basic mode queries are SLOWER (runtime API calls) vs On mode (map lookups)" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "- Basic mode has ZERO storage overhead, On mode has ~24-48 bytes per allocation" | tee -a "$OUTPUT_DIR/comparison.txt" +echo "" | tee -a "$OUTPUT_DIR/comparison.txt" + +echo "" +echo "See $OUTPUT_DIR/comparison.txt for side-by-side comparison" diff --git a/docs/sphinx/cookbook/no_introspection.rst b/docs/sphinx/cookbook/no_introspection.rst index a1e308bc1..2d1bdbbc7 100644 --- a/docs/sphinx/cookbook/no_introspection.rst +++ b/docs/sphinx/cookbook/no_introspection.rst @@ -1,17 +1,55 @@ .. _no_introspection:: -===================== -Disable Introspection -===================== +==================================== +Introspection Control and Performance +==================================== -If you know that you won't be using any of Umpire's introspection capabalities -for allocations that come from a particular :class:`umpire::Allocator`, you can -turn off the introspection and avoid the overhead of tracking the associated -metadata. +Umpire provides multiple ways to control introspection overhead, both globally +and per-allocator. + +Global Introspection Levels +---------------------------- + +The simplest way to control introspection is via the global introspection level, +which affects all allocations. Set the ``UMPIRE_INTROSPECTION_LEVEL`` environment +variable to one of: + +- ``off``: No tracking at all (maximum performance, zero overhead) +- ``basic``: Lightweight range tracking (fast, but unsafe operations) +- ``on``: Full tracking with safety checks (default) + +For example: + +.. code-block:: bash + + export UMPIRE_INTROSPECTION_LEVEL=basic + ./my_application + +Or programmatically (must be set before any allocations): + +.. code-block:: cpp + + auto& rm = umpire::ResourceManager::getInstance(); + rm.setIntrospectionLevel(umpire::IntrospectionLevel::Basic); + +See the :ref:`introspection` tutorial for detailed information on each level. + +Per-Allocator Introspection Control +------------------------------------ + +If you need finer-grained control, you can disable introspection for specific +allocators while keeping it enabled globally. This is useful when you have +one hot path that needs maximum performance but still want introspection elsewhere. + +.. note:: + Disabling introspection turns off *all* allocation metadata tracking for + that Allocator. If you still need exact-pointer ownership tracking but want + to reduce overhead, consider using the global ``basic`` introspection level via + ``UMPIRE_INTROSPECTION_LEVEL``. .. warning:: - Disabling introspection means that allocations from this Allocator cannot - be used for operations, or size and location queries. + Allocations from an allocator with introspection disabled cannot be used + for operations like ``copy()``, or size and location queries. In this recipe, we look at disabling introspection for a pool. To turn off introspection, you pass a boolean as the second template parameter to the diff --git a/docs/sphinx/tutorial/introspection.rst b/docs/sphinx/tutorial/introspection.rst index d787efaf1..a03ee6f16 100644 --- a/docs/sphinx/tutorial/introspection.rst +++ b/docs/sphinx/tutorial/introspection.rst @@ -6,9 +6,24 @@ Introspection When writing code to run on computers with a complex memory hierarchy, one of the most difficult things can be keeping track of where each pointer has been -allocated. Umpire's instrospection capability keeps track of this information, +allocated. Umpire's introspection capability keeps track of this information, as well as other useful bits and pieces you might want to know. +Umpire supports multiple *introspection levels* that control both the overhead +and tracking strategy used for allocations: + +- ``off``: disable public introspection queries +- ``basic``: track only exact allocation-pointer ownership +- ``on``: track full allocation metadata and backtraces (if enabled via ``UMPIRE_BACKTRACE``) + +The level can be set via the ``UMPIRE_INTROSPECTION_LEVEL`` environment +variable, or programmatically with ``umpire::ResourceManager::setIntrospectionLevel()``. + +.. note:: + The introspection level is **locked after the first allocation**. You cannot + change the level once any allocation has been made. This ensures consistency + in tracking behavior throughout the program's lifetime. + The :class:`umpire::ResourceManager` can be used to find the allocator associated with an address: @@ -33,7 +48,8 @@ You can also find out how big the allocation is, in case you forgot: :end-before: _sphinx_tag_tut_getsize_end :language: C++ -Remember that these functions will work on any allocation made using an -Allocator or :class:`umpire::TypedAllocator`. +These functions require the global introspection level to be ``on``. With +``basic``, only exact-pointer ownership checks such as +``umpire::ResourceManager::hasAllocator`` remain available. .. literalinclude:: ../../../examples/tutorial/tut_introspection.cpp diff --git a/src/umpire/CMakeLists.txt b/src/umpire/CMakeLists.txt index 276380396..37cdef3a0 100644 --- a/src/umpire/CMakeLists.txt +++ b/src/umpire/CMakeLists.txt @@ -21,6 +21,7 @@ endif() set (umpire_headers Allocator.hpp Allocator.inl + Introspection.hpp ResourceManager.hpp ResourceManager.inl Tracking.hpp diff --git a/src/umpire/Introspection.hpp b/src/umpire/Introspection.hpp new file mode 100644 index 000000000..7ca4252ae --- /dev/null +++ b/src/umpire/Introspection.hpp @@ -0,0 +1,38 @@ +////////////////////////////////////////////////////////////////////////////// +// Copyright (c) 2016-26, Lawrence Livermore National Security, LLC and Umpire +// project contributors. See the COPYRIGHT file for details. +// +// SPDX-License-Identifier: (MIT) +////////////////////////////////////////////////////////////////////////////// +#ifndef UMPIRE_Introspection_HPP +#define UMPIRE_Introspection_HPP + +#include + +namespace umpire { + +/*! + * \brief Controls the tracking strategy Umpire uses for allocations. + * + * - Off: Disable public introspection tracking. + * - Basic: Use runtime API queries (zero storage overhead). + * - On: Track full allocation metadata and backtraces (if enabled). + */ +enum class IntrospectionLevel { Off, Basic, On }; + +inline std::string to_string(IntrospectionLevel level) +{ + switch (level) { + case IntrospectionLevel::Off: + return "off"; + case IntrospectionLevel::Basic: + return "basic"; + case IntrospectionLevel::On: + return "on"; + } + return "on"; +} + +} // end namespace umpire + +#endif // UMPIRE_Introspection_HPP diff --git a/src/umpire/ResourceManager.cpp b/src/umpire/ResourceManager.cpp index 8c31492a4..aadb0ec2d 100644 --- a/src/umpire/ResourceManager.cpp +++ b/src/umpire/ResourceManager.cpp @@ -6,6 +6,8 @@ ////////////////////////////////////////////////////////////////////////////// #include "umpire/ResourceManager.hpp" +#include +#include #include #include #include @@ -23,6 +25,7 @@ #include "umpire/util/Macros.hpp" #include "umpire/util/io.hpp" #include "umpire/util/make_unique.hpp" +#include "umpire/util/pointer_utils.hpp" #include "umpire/util/wrap_allocator.hpp" #if defined(UMPIRE_ENABLE_CUDA) @@ -42,9 +45,52 @@ static const char* s_null_resource_name{"__umpire_internal_null"}; static const char* s_zero_byte_pool_name{"__umpire_internal_0_byte_pool"}; +static const char* s_introspection_level_env_name{"UMPIRE_INTROSPECTION_LEVEL"}; namespace umpire { +namespace { +bool case_insensitive_match(const std::string& s1, const std::string& s2) +{ + return (s1.size() == s2.size()) && std::equal(s1.begin(), s1.end(), s2.begin(), [](char c1, char c2) { + return (std::toupper(c1) == std::toupper(c2)); + }); +} + +IntrospectionLevel parse_introspection_level(const char* enval) +{ + if (!enval) { + return IntrospectionLevel::On; + } + + const std::string val{enval}; + if (case_insensitive_match(val, "OFF")) { + return IntrospectionLevel::Off; + } else if (case_insensitive_match(val, "BASIC")) { + return IntrospectionLevel::Basic; + } else if (case_insensitive_match(val, "ON")) { + return IntrospectionLevel::On; + } + + UMPIRE_ERROR(runtime_error, + fmt::format("Invalid value \"{}\" for {}. Expected one of: on, basic, off.", val, + s_introspection_level_env_name)); + return IntrospectionLevel::On; +} + +bool requires_full_introspection(IntrospectionLevel level) noexcept +{ + return level == IntrospectionLevel::On; +} + +void throw_requires_full_introspection(const char* operation, IntrospectionLevel level) +{ + UMPIRE_ERROR(runtime_error, + fmt::format("{} requires introspection level \"on\". Current level is \"{}\".", operation, + to_string(level))); +} +} // namespace + ResourceManager& ResourceManager::getInstance() { static ResourceManager resource_manager; @@ -56,9 +102,14 @@ ResourceManager& ResourceManager::getInstance() ResourceManager::ResourceManager() : m_allocations(), m_allocators(), + m_shared_allocator_names(), m_allocators_by_id(), m_allocators_by_name(), m_memory_resources(), + m_default_allocator(nullptr), + m_null_allocator(nullptr), + m_zero_byte_pool(nullptr), + m_introspection_level{parse_introspection_level(std::getenv(s_introspection_level_env_name))}, m_id(0), m_mutex() { @@ -80,7 +131,9 @@ ResourceManager::~ResourceManager() if (allocator->getCurrentSize() != 0) { std::stringstream ss; - umpire::print_allocator_records(Allocator{allocator.get()}, ss); + if (getIntrospectionLevel() == IntrospectionLevel::On) { + printTrackedAllocationRecords(allocator.get(), ss); + } UMPIRE_LOG(Error, allocator->getName() << " Allocator still has " << allocator->getCurrentSize() << " bytes allocated" << std::endl @@ -129,6 +182,27 @@ void ResourceManager::initialize() UMPIRE_LOG(Debug, "() leaving"); } +void ResourceManager::setIntrospectionLevel(IntrospectionLevel level) +{ + const auto current_level = getIntrospectionLevel(); + + // Only block lowering from On mode if tracked allocations exist in AllocationMap + // (can't switch to Basic/Off if we have tracked allocations) + if (current_level == IntrospectionLevel::On && + static_cast(level) < static_cast(current_level) && + m_allocations.size() > 0) { + UMPIRE_ERROR(runtime_error, + fmt::format("Cannot lower introspection level from \"on\" to \"{}\" while tracked allocations exist", + to_string(level))); + } + m_introspection_level.store(level, std::memory_order_relaxed); +} + +IntrospectionLevel ResourceManager::getIntrospectionLevel() const noexcept +{ + return m_introspection_level.load(std::memory_order_relaxed); +} + Allocator ResourceManager::makeResource(const std::string& name) { resource::MemoryResourceRegistry& registry{resource::MemoryResourceRegistry::getInstance()}; @@ -167,6 +241,7 @@ Allocator ResourceManager::makeResource(const std::string& name, MemoryResourceT .category(event::category::operation) .arg("allocator_ref", (void*)allocator.get()) .arg("introspection", traits.tracking) + .arg("introspection_level", to_string(getIntrospectionLevel())) .tag("allocator_name", name) .tag("replay", "true"); }); @@ -365,7 +440,7 @@ void ResourceManager::destroyAllocator(const std::string& name, bool free_alloca fmt::format("Cannot destroy builtin allocator \"{}\"", name)); } - auto records = umpire::get_allocator_records(Allocator(strategy)); + auto records = getTrackedAllocationRecords(strategy); if (isStrictDestructionMode()) { if (!records.empty() && !free_allocations) { @@ -485,6 +560,11 @@ void ResourceManager::destroyAllocator(int id, bool free_allocations) Allocator ResourceManager::getAllocator(void* ptr) { UMPIRE_LOG(Debug, "(ptr=" << ptr << ")"); + const auto level = getIntrospectionLevel(); + if (level == IntrospectionLevel::Off) { + UMPIRE_ERROR(runtime_error, + "ResourceManager::getAllocator(void*) requires introspection to be enabled (basic or on mode)"); + } return Allocator(findAllocatorForPointer(ptr)); } @@ -506,7 +586,41 @@ bool ResourceManager::hasAllocator(void* ptr) { UMPIRE_LOG(Debug, "(ptr=" << ptr << ")"); - return m_allocations.contains(ptr); + const auto level = getIntrospectionLevel(); + if (level == IntrospectionLevel::On) { + return m_allocations.contains(ptr); + } + if (level == IntrospectionLevel::Basic) { + return util::inferAllocatorFromPointer(ptr, *this) != nullptr; + } + + return false; // Off mode +} + +std::vector +ResourceManager::getTrackedAllocationRecords(strategy::AllocationStrategy* strategy) const +{ + if (getIntrospectionLevel() != IntrospectionLevel::On) { + UMPIRE_ERROR(runtime_error, + "getTrackedAllocationRecords() requires IntrospectionLevel::On"); + } + + std::vector records; + std::copy_if(m_allocations.begin(), m_allocations.end(), std::back_inserter(records), + [strategy](const util::AllocationRecord& rec) { return rec.strategy == strategy; }); + return records; +} + +void ResourceManager::printTrackedAllocationRecords(strategy::AllocationStrategy* strategy, std::ostream& os) const +{ + if (getIntrospectionLevel() != IntrospectionLevel::On) { + UMPIRE_ERROR(runtime_error, + "printTrackedAllocationRecords() requires IntrospectionLevel::On"); + } + + m_allocations.print([strategy](const util::AllocationRecord& rec) { + return rec.strategy == strategy; + }, os); } void ResourceManager::registerAllocation(void* ptr, util::AllocationRecord record) @@ -518,19 +632,33 @@ void ResourceManager::registerAllocation(void* ptr, util::AllocationRecord recor UMPIRE_LOG(Debug, "(ptr=" << ptr << ", size=" << record.size << ", strategy=" << record.strategy << ") with " << this); - UMPIRE_RECORD_BACKTRACE(record); - - m_allocations.insert(ptr, record); + const auto level = getIntrospectionLevel(); + if (level == IntrospectionLevel::On) { + UMPIRE_RECORD_BACKTRACE(record); + m_allocations.insert(ptr, record); + } + // Off/Basic modes: no storage, just stats updated by caller } util::AllocationRecord ResourceManager::deregisterAllocation(void* ptr) { UMPIRE_LOG(Debug, "(ptr=" << ptr << ")"); - return m_allocations.remove(ptr); + const auto level = getIntrospectionLevel(); + if (level == IntrospectionLevel::On) { + return m_allocations.remove(ptr); + } + + // Basic/Off modes: return empty record (caller must infer strategy if needed) + return util::AllocationRecord{}; } const util::AllocationRecord* ResourceManager::findAllocationRecord(void* ptr) const { + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::findAllocationRecord(void*)", level); + } + auto alloc_record = m_allocations.find(ptr); if (!alloc_record->strategy) { @@ -545,6 +673,10 @@ const util::AllocationRecord* ResourceManager::findAllocationRecord(void* ptr) c void ResourceManager::copy(void* dst_ptr, void* src_ptr, std::size_t size) { UMPIRE_LOG(Debug, "(src_ptr=" << src_ptr << ", dst_ptr=" << dst_ptr << ", size=" << size << ")"); + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::copy", level); + } auto& op_registry = op::MemoryOperationRegistry::getInstance(); @@ -590,6 +722,10 @@ camp::resources::EventProxy ResourceManager::copy(voi std::size_t size) { UMPIRE_LOG(Debug, "(src_ptr=" << src_ptr << ", dst_ptr=" << dst_ptr << ", size=" << size << ")"); + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::copy", level); + } auto& op_registry = op::MemoryOperationRegistry::getInstance(); @@ -633,6 +769,10 @@ camp::resources::EventProxy ResourceManager::copy(voi void ResourceManager::memset(void* ptr, int value, std::size_t length) { UMPIRE_LOG(Debug, "(ptr=" << ptr << ", value=" << value << ", length=" << length << ")"); + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::memset", level); + } auto& op_registry = op::MemoryOperationRegistry::getInstance(); @@ -670,6 +810,10 @@ camp::resources::EventProxy ResourceManager::memset(v std::size_t length) { UMPIRE_LOG(Debug, "(ptr=" << ptr << ", value=" << value << ", length=" << length << ")"); + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::memset", level); + } auto& op_registry = op::MemoryOperationRegistry::getInstance(); @@ -705,6 +849,11 @@ camp::resources::EventProxy ResourceManager::memset(v void* ResourceManager::reallocate(void* current_ptr, std::size_t new_size) { + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::reallocate", level); + } + strategy::AllocationStrategy* strategy; if (current_ptr != nullptr) { @@ -739,6 +888,11 @@ void* ResourceManager::reallocate(void* current_ptr, std::size_t new_size) void* ResourceManager::reallocate(void* current_ptr, std::size_t new_size, camp::resources::Resource& ctx) { + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::reallocate", level); + } + strategy::AllocationStrategy* strategy; if (current_ptr != nullptr) { @@ -774,6 +928,11 @@ void* ResourceManager::reallocate(void* current_ptr, std::size_t new_size, camp: void* ResourceManager::reallocate(void* current_ptr, std::size_t new_size, Allocator alloc) { + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::reallocate", level); + } + umpire::event::record([&](auto& event) { event.name("reallocate") .category(event::category::operation) @@ -800,6 +959,11 @@ void* ResourceManager::reallocate(void* current_ptr, std::size_t new_size, Alloc void* ResourceManager::reallocate(void* current_ptr, std::size_t new_size, Allocator alloc, camp::resources::Resource& ctx) { + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::reallocate", level); + } + umpire::event::record([&](auto& event) { event.name("reallocate") .category(event::category::operation) @@ -828,6 +992,10 @@ void* ResourceManager::reallocate_impl(void* current_ptr, std::size_t new_size, { UMPIRE_LOG(Debug, "(current_ptr=" << current_ptr << ", new_size=" << new_size << ", with Allocator " << allocator.getName() << ")"); + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::reallocate", level); + } void* new_ptr; @@ -879,6 +1047,10 @@ void* ResourceManager::reallocate_impl(void* current_ptr, std::size_t new_size, { UMPIRE_LOG(Debug, "(current_ptr=" << current_ptr << ", new_size=" << new_size << ", with Allocator " << allocator.getName() << ")"); + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::reallocate", level); + } void* new_ptr; @@ -928,6 +1100,10 @@ void* ResourceManager::reallocate_impl(void* current_ptr, std::size_t new_size, void* ResourceManager::move(void* ptr, Allocator allocator) { UMPIRE_LOG(Debug, "(src_ptr=" << ptr << ", allocator=" << allocator.getName() << ")"); + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::move", level); + } auto alloc_record = m_allocations.find(ptr); @@ -1005,6 +1181,10 @@ camp::resources::EventProxy ResourceManager::prefetch camp::resources::Resource& ctx) { UMPIRE_LOG(Debug, "(ptr=" << ptr << ", device=" << device << ")"); + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::prefetch", level); + } auto& op_registry = op::MemoryOperationRegistry::getInstance(); auto alloc_record = m_allocations.find(ptr); @@ -1030,6 +1210,11 @@ void ResourceManager::deallocate(void* ptr) std::size_t ResourceManager::getSize(void* ptr) const { + const auto level = getIntrospectionLevel(); + if (!requires_full_introspection(level)) { + throw_requires_full_introspection("ResourceManager::getSize(void*)", level); + } + auto record = m_allocations.find(ptr); UMPIRE_LOG(Debug, "(ptr=" << ptr << ") returning " << record->size); return record->size; @@ -1054,12 +1239,27 @@ strategy::AllocationStrategy* ResourceManager::findAllocatorForId(int id) strategy::AllocationStrategy* ResourceManager::findAllocatorForPointer(void* ptr) { - auto allocation_record = m_allocations.find(ptr); + const auto level = getIntrospectionLevel(); + if (level == IntrospectionLevel::Off) { + UMPIRE_ERROR(runtime_error, + "findAllocatorForPointer() requires introspection to be enabled"); + } + + if (level == IntrospectionLevel::Basic) { + auto* strategy = util::inferAllocatorFromPointer(ptr, *this); + if (!strategy) { + UMPIRE_ERROR(runtime_error, fmt::format("Cannot infer allocator for pointer: {}", ptr)); + } + UMPIRE_LOG(Debug, "(ptr=" << ptr << ") returning " << strategy); + return strategy; + } + + // IntrospectionLevel::On + auto allocation_record = m_allocations.find(ptr); if (!allocation_record->strategy) { UMPIRE_ERROR(runtime_error, fmt::format("Cannot find allocator for pointer: {}", ptr)); } - UMPIRE_LOG(Debug, "(ptr=" << ptr << ") returning " << allocation_record->strategy); return allocation_record->strategy; } diff --git a/src/umpire/ResourceManager.hpp b/src/umpire/ResourceManager.hpp index 345f686b5..83c7aea12 100644 --- a/src/umpire/ResourceManager.hpp +++ b/src/umpire/ResourceManager.hpp @@ -7,15 +7,18 @@ #ifndef UMPIRE_ResourceManager_HPP #define UMPIRE_ResourceManager_HPP +#include #include #include #include +#include #include #include #include #include "camp/resource.hpp" #include "umpire/Allocator.hpp" +#include "umpire/Introspection.hpp" #include "umpire/Tracking.hpp" #include "umpire/resource/MemoryResourceTypes.hpp" #include "umpire/strategy/AllocationStrategy.hpp" @@ -52,6 +55,18 @@ class ResourceManager { */ void initialize(); + /*! + * \brief Set the global introspection level for tracked allocations. + * + * \throws runtime_error if allocations already exist + */ + void setIntrospectionLevel(IntrospectionLevel level); + + /*! + * \brief Get the global introspection level for tracked allocations. + */ + IntrospectionLevel getIntrospectionLevel() const noexcept; + /*! * \brief Get the names of all available Allocator objects. */ @@ -369,6 +384,8 @@ class ResourceManager { strategy::AllocationStrategy* findAllocatorForPointer(void* ptr); strategy::AllocationStrategy* findAllocatorForId(int id); strategy::AllocationStrategy* getAllocationStrategy(const std::string& name); + std::vector getTrackedAllocationRecords(strategy::AllocationStrategy* strategy) const; + void printTrackedAllocationRecords(strategy::AllocationStrategy* strategy, std::ostream& os) const; bool isBuiltinAllocator(strategy::AllocationStrategy* strategy); @@ -407,6 +424,8 @@ class ResourceManager { strategy::AllocationStrategy* m_null_allocator{nullptr}; strategy::AllocationStrategy* m_zero_byte_pool{nullptr}; + std::atomic m_introspection_level{IntrospectionLevel::On}; + int m_id; std::mutex m_mutex; diff --git a/src/umpire/ResourceManager.inl b/src/umpire/ResourceManager.inl index 2c96be943..7d2585fcd 100644 --- a/src/umpire/ResourceManager.inl +++ b/src/umpire/ResourceManager.inl @@ -43,6 +43,7 @@ Allocator ResourceManager::makeAllocator(const std::string& name, Tracking track .arg("allocator_ref", (void*)allocator.get()) .arg("type", typeid(Strategy).name()) .arg("introspection", is_tracked) + .arg("introspection_level", to_string(getIntrospectionLevel())) .args(args...) .tag("allocator_name", allocator->getName()) .tag("replay", "true"); diff --git a/src/umpire/Umpire.cpp b/src/umpire/Umpire.cpp index 30e0de03c..6e57e5b09 100644 --- a/src/umpire/Umpire.cpp +++ b/src/umpire/Umpire.cpp @@ -41,12 +41,19 @@ namespace umpire { void print_allocator_records(Allocator allocator, std::ostream& os) { - std::stringstream ss; auto& rm = umpire::ResourceManager::getInstance(); + const auto level = rm.getIntrospectionLevel(); + + if (level != IntrospectionLevel::On) { + UMPIRE_ERROR(runtime_error, + fmt::format("print_allocator_records requires introspection level \"on\". Current level is \"{}\".", + to_string(level))); + } + std::stringstream ss; auto strategy = allocator.getAllocationStrategy(); - rm.m_allocations.print([strategy](const util::AllocationRecord& rec) { return rec.strategy == strategy; }, ss); + rm.printTrackedAllocationRecords(strategy, ss); if (!ss.str().empty()) { os << "Allocations for " << allocator.getName() << " allocator:" << std::endl << ss.str() << std::endl; @@ -56,19 +63,26 @@ void print_allocator_records(Allocator allocator, std::ostream& os) std::vector get_allocator_records(Allocator allocator) { auto& rm = umpire::ResourceManager::getInstance(); - auto strategy = allocator.getAllocationStrategy(); - - std::vector recs; - std::copy_if(rm.m_allocations.begin(), rm.m_allocations.end(), std::back_inserter(recs), - [strategy](const util::AllocationRecord& rec) { return rec.strategy == strategy; }); + const auto level = rm.getIntrospectionLevel(); + if (level != IntrospectionLevel::On) { + UMPIRE_ERROR(runtime_error, + fmt::format("get_allocator_records requires introspection level \"on\". Current level is \"{}\".", + to_string(level))); + } - return recs; + auto strategy = allocator.getAllocationStrategy(); + return rm.getTrackedAllocationRecords(strategy); } bool pointer_overlaps(void* left_ptr, void* right_ptr) { auto& rm = umpire::ResourceManager::getInstance(); + if (rm.getIntrospectionLevel() != IntrospectionLevel::On) { + UMPIRE_ERROR(runtime_error, + "pointer_overlaps() requires IntrospectionLevel::On"); + } + try { auto left_record = rm.findAllocationRecord(left_ptr); auto right_record = rm.findAllocationRecord(right_ptr); @@ -88,6 +102,11 @@ bool pointer_contains(void* left_ptr, void* right_ptr) { auto& rm = umpire::ResourceManager::getInstance(); + if (rm.getIntrospectionLevel() != IntrospectionLevel::On) { + UMPIRE_ERROR(runtime_error, + "pointer_contains() requires IntrospectionLevel::On"); + } + try { auto left_record = rm.findAllocationRecord(left_ptr); auto right_record = rm.findAllocationRecord(right_ptr); @@ -120,6 +139,12 @@ std::string get_backtrace(void* ptr) { #if defined(UMPIRE_ENABLE_BACKTRACE) auto& rm = umpire::ResourceManager::getInstance(); + + if (rm.getIntrospectionLevel() != IntrospectionLevel::On) { + UMPIRE_ERROR(runtime_error, + "get_backtrace() requires IntrospectionLevel::On"); + } + auto record = rm.findAllocationRecord(ptr); return umpire::util::backtracer<>::print(record->allocation_backtrace); #else diff --git a/src/umpire/strategy/mixins/Inspector.cpp b/src/umpire/strategy/mixins/Inspector.cpp index 9330965b7..4c091713e 100644 --- a/src/umpire/strategy/mixins/Inspector.cpp +++ b/src/umpire/strategy/mixins/Inspector.cpp @@ -35,7 +35,12 @@ void Inspector::registerAllocation(void* ptr, std::size_t size, strategy::Alloca s->m_high_watermark = s->m_current_size; } - ResourceManager::getInstance().registerAllocation(ptr, {ptr, size, s, name}); + auto& rm = ResourceManager::getInstance(); + if (rm.getIntrospectionLevel() == IntrospectionLevel::On) { + rm.registerAllocation(ptr, {ptr, size, s, name}); + } else { + rm.registerAllocation(ptr, {ptr, size, s}); + } } util::AllocationRecord @@ -43,7 +48,8 @@ Inspector::deregisterAllocation(void* ptr, strategy::AllocationStrategy* s) { auto record = ResourceManager::getInstance().deregisterAllocation(ptr); - if (record.strategy == s) { + // In Basic/Off modes, record.strategy will be nullptr (no tracking) + if (record.strategy == nullptr || record.strategy == s) { s->m_current_size -= record.size; s->m_allocation_count--; } else { diff --git a/src/umpire/util/CMakeLists.txt b/src/umpire/util/CMakeLists.txt index 20893c2b2..fc8ff8cf3 100644 --- a/src/umpire/util/CMakeLists.txt +++ b/src/umpire/util/CMakeLists.txt @@ -23,6 +23,7 @@ set (umpire_util_headers MemoryMap.hpp MemoryMap.inl OutputBuffer.hpp + pointer_utils.hpp Platform.hpp allocation_statistics.hpp detect_vendor.hpp @@ -43,6 +44,7 @@ set (umpire_util_sources io.cpp Logger.cpp MPI.cpp + pointer_utils.cpp OutputBuffer.cpp allocation_statistics.cpp detect_vendor.cpp) diff --git a/src/umpire/util/backtrace.inl b/src/umpire/util/backtrace.inl index 4712ee2a3..855382d0d 100644 --- a/src/umpire/util/backtrace.inl +++ b/src/umpire/util/backtrace.inl @@ -73,7 +73,13 @@ std::string stringify(const std::vector& frames) { std::ostringstream backtrace_stream; #if !defined(_MSC_VER) - int num_frames = frames.size(); + const int num_frames = static_cast(frames.size()); + + if (num_frames == 0) { + backtrace_stream << " Backtrace: 0 frames" << std::endl; + return backtrace_stream.str(); + } + char** symbols = ::backtrace_symbols(&frames[0], num_frames); backtrace_stream << " Backtrace: " << num_frames << " frames" << std::endl; diff --git a/src/umpire/util/pointer_utils.cpp b/src/umpire/util/pointer_utils.cpp new file mode 100644 index 000000000..a7902989b --- /dev/null +++ b/src/umpire/util/pointer_utils.cpp @@ -0,0 +1,198 @@ +////////////////////////////////////////////////////////////////////////////// +// Copyright (c) 2016-26, Lawrence Livermore National Security, LLC and Umpire +// project contributors. See the COPYRIGHT file for details. +// +// SPDX-License-Identifier: (MIT) +////////////////////////////////////////////////////////////////////////////// +#include "umpire/util/pointer_utils.hpp" + +#include + +#include "umpire/ResourceManager.hpp" +#include "umpire/util/Macros.hpp" + +#if defined(UMPIRE_ENABLE_CUDA) +#include +#endif + +#if defined(UMPIRE_ENABLE_HIP) +#include +#endif + +namespace umpire { +namespace util { + +strategy::AllocationStrategy* inferAllocatorFromPointer(void* ptr, ResourceManager& rm) +{ + if (!ptr) { + return nullptr; + } + +#if defined(UMPIRE_ENABLE_CUDA) + { + cudaPointerAttributes attrs; + cudaError_t err = cudaPointerGetAttributes(&attrs, ptr); + + if (err == cudaSuccess) { + // Determine allocator based on memory type +#if CUDART_VERSION >= 10000 + switch (attrs.type) { + case cudaMemoryTypeUnregistered: + // Host memory not registered with CUDA - use HOST allocator + try { + return rm.getAllocator("HOST").getAllocationStrategy(); + } catch (...) { + return nullptr; + } + + case cudaMemoryTypeHost: + // Pinned host memory + try { + return rm.getAllocator("PINNED").getAllocationStrategy(); + } catch (...) { + // Fallback to HOST if PINNED not available + try { + return rm.getAllocator("HOST").getAllocationStrategy(); + } catch (...) { + return nullptr; + } + } + + case cudaMemoryTypeDevice: + // Device memory + try { + if (attrs.device == 0) { + return rm.getAllocator("DEVICE").getAllocationStrategy(); + } else { + // Try device-specific allocator + std::string device_name = "DEVICE::" + std::to_string(attrs.device); + try { + return rm.getAllocator(device_name).getAllocationStrategy(); + } catch (...) { + // Fallback to default DEVICE + return rm.getAllocator("DEVICE").getAllocationStrategy(); + } + } + } catch (...) { + return nullptr; + } + + case cudaMemoryTypeManaged: + // Unified memory + try { + return rm.getAllocator("UM").getAllocationStrategy(); + } catch (...) { + return nullptr; + } + + default: + return nullptr; + } +#else + // Pre-CUDA 10.0: use older API + if (attrs.memoryType == cudaMemoryTypeDevice) { + try { + return rm.getAllocator("DEVICE").getAllocationStrategy(); + } catch (...) { + return nullptr; + } + } else if (attrs.memoryType == cudaMemoryTypeHost) { + try { + return rm.getAllocator("HOST").getAllocationStrategy(); + } catch (...) { + return nullptr; + } + } +#endif + } else { + // cudaPointerGetAttributes failed - likely host memory + cudaGetLastError(); // Clear error + try { + return rm.getAllocator("HOST").getAllocationStrategy(); + } catch (...) { + return nullptr; + } + } + } +#elif defined(UMPIRE_ENABLE_HIP) + { + hipPointerAttribute_t attrs; + hipError_t err = hipPointerGetAttributes(&attrs, ptr); + + if (err == hipSuccess) { + switch (attrs.type) { + case hipMemoryTypeHost: + // Pinned host memory or regular host + if (attrs.isManaged) { + try { + return rm.getAllocator("UM").getAllocationStrategy(); + } catch (...) { + return nullptr; + } + } else { + try { + return rm.getAllocator("PINNED").getAllocationStrategy(); + } catch (...) { + // Fallback to HOST + try { + return rm.getAllocator("HOST").getAllocationStrategy(); + } catch (...) { + return nullptr; + } + } + } + + case hipMemoryTypeDevice: + // Device memory + try { + if (attrs.device == 0) { + return rm.getAllocator("DEVICE").getAllocationStrategy(); + } else { + // Try device-specific allocator + std::string device_name = "DEVICE::" + std::to_string(attrs.device); + try { + return rm.getAllocator(device_name).getAllocationStrategy(); + } catch (...) { + // Fallback to default DEVICE + return rm.getAllocator("DEVICE").getAllocationStrategy(); + } + } + } catch (...) { + return nullptr; + } + + case hipMemoryTypeUnified: + // Unified memory + try { + return rm.getAllocator("UM").getAllocationStrategy(); + } catch (...) { + return nullptr; + } + + default: + return nullptr; + } + } else { + // hipPointerGetAttributes failed - likely host memory + hipGetLastError(); // Clear error + try { + return rm.getAllocator("HOST").getAllocationStrategy(); + } catch (...) { + return nullptr; + } + } + } +#else + // No GPU support - assume HOST + try { + return rm.getAllocator("HOST").getAllocationStrategy(); + } catch (...) { + return nullptr; + } +#endif + + return nullptr; +} + +} // end of namespace util +} // end of namespace umpire diff --git a/src/umpire/util/pointer_utils.hpp b/src/umpire/util/pointer_utils.hpp new file mode 100644 index 000000000..70bde05b8 --- /dev/null +++ b/src/umpire/util/pointer_utils.hpp @@ -0,0 +1,41 @@ +////////////////////////////////////////////////////////////////////////////// +// Copyright (c) 2016-26, Lawrence Livermore National Security, LLC and Umpire +// project contributors. See the COPYRIGHT file for details. +// +// SPDX-License-Identifier: (MIT) +////////////////////////////////////////////////////////////////////////////// +#ifndef UMPIRE_pointer_utils_HPP +#define UMPIRE_pointer_utils_HPP + +#include "umpire/config.hpp" + +namespace umpire { + +class ResourceManager; + +namespace strategy { +class AllocationStrategy; +} + +namespace util { + +/*! + * \brief Infer which allocator owns a pointer by querying runtime APIs + * + * This function uses platform-specific APIs to determine the memory type + * of a pointer and returns the corresponding default allocator. + * + * For CUDA: uses cudaPointerGetAttributes + * For HIP: uses hipPointerGetAttributes + * For host-only: assumes HOST allocator + * + * \param ptr Pointer to query + * \param rm ResourceManager instance (for allocator lookup) + * \return Allocator strategy if determinable, nullptr otherwise + */ +strategy::AllocationStrategy* inferAllocatorFromPointer(void* ptr, ResourceManager& rm); + +} // end of namespace util +} // end of namespace umpire + +#endif // UMPIRE_pointer_utils_HPP diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt index 767512fd1..0c9ed7cbb 100644 --- a/tests/integration/CMakeLists.txt +++ b/tests/integration/CMakeLists.txt @@ -239,10 +239,30 @@ blt_add_executable( SOURCES introspection_tests.cpp DEPENDS_ON ${integration_tests_depends}) +# Run introspection tests with Off mode blt_add_test( - NAME introspection_tests + NAME introspection_tests_off + COMMAND introspection_tests) + +set_property(TEST introspection_tests_off + PROPERTY ENVIRONMENT "UMPIRE_INTROSPECTION_LEVEL=off") + +# Run introspection tests with Basic mode +blt_add_test( + NAME introspection_tests_basic COMMAND introspection_tests) +set_property(TEST introspection_tests_basic + PROPERTY ENVIRONMENT "UMPIRE_INTROSPECTION_LEVEL=basic") + +# Run introspection tests with On mode +blt_add_test( + NAME introspection_tests_on + COMMAND introspection_tests) + +set_property(TEST introspection_tests_on + PROPERTY ENVIRONMENT "UMPIRE_INTROSPECTION_LEVEL=on") + blt_add_executable( NAME destroy_allocator_tests SOURCES destroy_allocator_tests.cpp diff --git a/tests/integration/introspection_tests.cpp b/tests/integration/introspection_tests.cpp index 2be1a655f..71e454b97 100644 --- a/tests/integration/introspection_tests.cpp +++ b/tests/integration/introspection_tests.cpp @@ -7,10 +7,22 @@ #include "gtest/gtest.h" #include "umpire/Umpire.hpp" #include "umpire/config.hpp" +#include "umpire/strategy/QuickPool.hpp" + +namespace { +umpire::IntrospectionLevel getCurrentLevel() { + return umpire::ResourceManager::getInstance().getIntrospectionLevel(); +} +} // namespace TEST(IntrospectionTest, Overlaps) { auto& rm = umpire::ResourceManager::getInstance(); + + if (getCurrentLevel() != umpire::IntrospectionLevel::On) { + GTEST_SKIP() << "Overlaps test requires introspection level 'on'"; + } + umpire::Allocator allocator{rm.getAllocator("HOST")}; umpire::strategy::AllocationStrategy* strategy{rm.getAllocator("HOST").getAllocationStrategy()}; @@ -70,6 +82,11 @@ TEST(IntrospectionTest, Overlaps) TEST(IntrospectionTest, Contains) { auto& rm = umpire::ResourceManager::getInstance(); + + if (getCurrentLevel() != umpire::IntrospectionLevel::On) { + GTEST_SKIP() << "Contains test requires introspection level 'on'"; + } + umpire::Allocator allocator{rm.getAllocator("HOST")}; umpire::strategy::AllocationStrategy* strategy{rm.getAllocator("HOST").getAllocationStrategy()}; @@ -92,9 +109,310 @@ TEST(IntrospectionTest, RegisterNull) { auto& rm = umpire::ResourceManager::getInstance(); + if (getCurrentLevel() != umpire::IntrospectionLevel::On) { + GTEST_SKIP() << "RegisterNull test requires introspection level 'on'"; + } + umpire::strategy::AllocationStrategy* strategy{rm.getAllocator("HOST").getAllocationStrategy()}; auto record = umpire::util::AllocationRecord{nullptr, 0, strategy}; EXPECT_THROW(rm.registerAllocation(nullptr, record), umpire::runtime_error); } + +TEST(IntrospectionLevelTest, OnTracksNamedAllocationMetadata) +{ + auto& rm = umpire::ResourceManager::getInstance(); + + if (getCurrentLevel() != umpire::IntrospectionLevel::On) { + GTEST_SKIP() << "OnTracksNamedAllocationMetadata test requires introspection level 'on'"; + } + + umpire::Allocator allocator{rm.getAllocator("HOST")}; + + const std::string alloc_name{"my_named_alloc"}; + constexpr std::size_t size{64}; + + void* p = allocator.allocate(alloc_name, size); + ASSERT_TRUE(rm.hasAllocator(p)); + EXPECT_NO_THROW(rm.getAllocator(p)); + EXPECT_EQ(rm.getSize(p), size); + EXPECT_EQ(rm.findAllocationRecord(p)->name, alloc_name); + allocator.deallocate(p); +} + +TEST(IntrospectionLevelTest, AllocationQueries) +{ + auto& rm = umpire::ResourceManager::getInstance(); + const auto level = getCurrentLevel(); + + umpire::Allocator allocator{rm.getAllocator("HOST")}; + const std::string alloc_name{"my_named_alloc"}; + constexpr std::size_t size{64}; + + void* p = allocator.allocate(alloc_name, size); + + if (level == umpire::IntrospectionLevel::Off) { + // Off mode: no introspection available + EXPECT_FALSE(rm.hasAllocator(p)); + EXPECT_THROW(rm.findAllocationRecord(p), umpire::runtime_error); + EXPECT_THROW(rm.getAllocator(p), umpire::runtime_error); + EXPECT_THROW(rm.getSize(p), umpire::runtime_error); + EXPECT_THROW(umpire::get_allocator_records(allocator), umpire::runtime_error); + + } else if (level == umpire::IntrospectionLevel::Basic) { + // Basic mode: API inference, no metadata + ASSERT_TRUE(rm.hasAllocator(p)); + EXPECT_TRUE(rm.hasAllocator(static_cast(p) + 1)); // API can't track offsets + EXPECT_THROW(rm.findAllocationRecord(p), umpire::runtime_error); + EXPECT_NO_THROW(rm.getAllocator(p)); // Works via API inference + EXPECT_THROW(rm.getSize(p), umpire::runtime_error); + EXPECT_THROW(umpire::get_allocator_records(allocator), umpire::runtime_error); + + } else { // IntrospectionLevel::On + // On mode: full tracking with metadata + ASSERT_TRUE(rm.hasAllocator(p)); + EXPECT_NO_THROW(rm.getAllocator(p)); + EXPECT_EQ(rm.getSize(p), size); + EXPECT_EQ(rm.findAllocationRecord(p)->name, alloc_name); + EXPECT_NO_THROW(umpire::get_allocator_records(allocator)); + } + + allocator.deallocate(p); +} + +TEST(IntrospectionLevelTest, EdgeCaseStackPointer) +{ + auto& rm = umpire::ResourceManager::getInstance(); + const auto level = getCurrentLevel(); + + int stack_var = 42; + void* stack_ptr = &stack_var; + + if (level == umpire::IntrospectionLevel::Off) { + EXPECT_FALSE(rm.hasAllocator(stack_ptr)); + + } else if (level == umpire::IntrospectionLevel::Basic) { + // May return true (infers HOST) or false (API fails) + // Either is acceptable for non-Umpire pointer + bool has_alloc = rm.hasAllocator(stack_ptr); + (void)has_alloc; // Should not crash + + } else { // On + EXPECT_FALSE(rm.hasAllocator(stack_ptr)); + EXPECT_THROW(rm.getAllocator(stack_ptr), umpire::runtime_error); + } +} + +TEST(IntrospectionLevelTest, EdgeCaseFreedPointer) +{ + auto& rm = umpire::ResourceManager::getInstance(); + const auto level = getCurrentLevel(); + + if (level == umpire::IntrospectionLevel::Off) { + GTEST_SKIP() << "EdgeCaseFreedPointer test requires introspection (basic or on)"; + } + + umpire::Allocator allocator{rm.getAllocator("HOST")}; + void* ptr = allocator.allocate(256); + + if (level == umpire::IntrospectionLevel::On) { + EXPECT_TRUE(rm.hasAllocator(ptr)); + allocator.deallocate(ptr); + // After deallocation, should not be tracked + EXPECT_FALSE(rm.hasAllocator(ptr)); + EXPECT_THROW(rm.getAllocator(ptr), umpire::runtime_error); + + } else { // Basic + allocator.deallocate(ptr); + // Behavior after free is undefined in Basic mode but shouldn't crash + bool has_alloc = rm.hasAllocator(ptr); + (void)has_alloc; + } +} + +#if defined(UMPIRE_ENABLE_CUDA) || defined(UMPIRE_ENABLE_HIP) +TEST(IntrospectionLevelTest, AsyncCopy) +{ + auto& rm = umpire::ResourceManager::getInstance(); + const auto level = getCurrentLevel(); + + if (level != umpire::IntrospectionLevel::Basic) { + GTEST_SKIP() << "AsyncCopy test specific to Basic mode"; + } + + umpire::Allocator device_alloc{rm.getAllocator("DEVICE")}; + umpire::Allocator host_alloc{rm.getAllocator("HOST")}; + + void* device_ptr = device_alloc.allocate(256); + void* host_ptr = host_alloc.allocate(256); + +#if defined(UMPIRE_ENABLE_CUDA) + auto ctx = camp::resources::Cuda(); +#elif defined(UMPIRE_ENABLE_HIP) + auto ctx = camp::resources::Hip(); +#endif + + // Async copy should work (unsafe, no validation) + EXPECT_NO_THROW(rm.copy(device_ptr, host_ptr, ctx, 256)); + ctx.wait(); + + // Async copy with size=0 should throw + EXPECT_THROW(rm.copy(device_ptr, host_ptr, ctx, 0), umpire::runtime_error); + + device_alloc.deallocate(device_ptr); + host_alloc.deallocate(host_ptr); +} +#endif + +#if defined(UMPIRE_ENABLE_CUDA) || defined(UMPIRE_ENABLE_HIP) +TEST(IntrospectionLevelTest, AsyncMemset) +{ + auto& rm = umpire::ResourceManager::getInstance(); + const auto level = getCurrentLevel(); + + if (level != umpire::IntrospectionLevel::Basic) { + GTEST_SKIP() << "AsyncMemset test specific to Basic mode"; + } + + umpire::Allocator device_alloc{rm.getAllocator("DEVICE")}; + void* device_ptr = device_alloc.allocate(256); + +#if defined(UMPIRE_ENABLE_CUDA) + auto ctx = camp::resources::Cuda(); +#elif defined(UMPIRE_ENABLE_HIP) + auto ctx = camp::resources::Hip(); +#endif + + // Async memset should work + EXPECT_NO_THROW(rm.memset(device_ptr, 0, ctx, 256)); + ctx.wait(); + + // Async memset with length=0 should throw + EXPECT_THROW(rm.memset(device_ptr, 0, ctx, 0), umpire::runtime_error); + + device_alloc.deallocate(device_ptr); +} +#endif + +TEST(IntrospectionLevelTest, PoolAllocatorIdentification) +{ + auto& rm = umpire::ResourceManager::getInstance(); + const auto level = getCurrentLevel(); + + if (level == umpire::IntrospectionLevel::Off) { + GTEST_SKIP() << "PoolAllocatorIdentification requires introspection (basic or on)"; + } + + umpire::Allocator host_alloc{rm.getAllocator("HOST")}; + auto pool = rm.makeAllocator("TestPool", host_alloc); + void* pool_ptr = pool.allocate(256); + + auto retrieved = rm.getAllocator(pool_ptr); + + if (level == umpire::IntrospectionLevel::On) { + // On mode returns the specific pool + EXPECT_EQ(retrieved.getName(), "TestPool"); + } else { // Basic + // Basic mode returns backing resource + EXPECT_EQ(retrieved.getName(), "HOST"); + } + + pool.deallocate(pool_ptr); +} + +TEST(IntrospectionLevelTest, ErrorMessageQuality) +{ + auto& rm = umpire::ResourceManager::getInstance(); + const auto level = getCurrentLevel(); + + umpire::Allocator allocator{rm.getAllocator("HOST")}; + + if (level == umpire::IntrospectionLevel::Basic) { + // In Basic mode, test that operations requiring full introspection give clear error + void* p1 = allocator.allocate(256); + + try { + rm.copy(p1, p1, 0); + FAIL() << "Expected runtime_error"; + } catch (const umpire::runtime_error& e) { + std::string msg = e.what(); + EXPECT_TRUE(msg.find("introspection") != std::string::npos && + (msg.find("on") != std::string::npos || msg.find("On") != std::string::npos)) + << "Error message should mention introspection level: " << msg; + } + + allocator.deallocate(p1); + + } else if (level == umpire::IntrospectionLevel::Off) { + // Test Off mode operation error + void* p = allocator.allocate(256); + + try { + rm.getAllocator(p); + FAIL() << "Expected runtime_error"; + } catch (const umpire::runtime_error& e) { + std::string msg = e.what(); + EXPECT_TRUE(msg.find("introspection") != std::string::npos) + << "Error message should mention introspection: " << msg; + } + + allocator.deallocate(p); + + } else { // On mode + // Test On mode size=0 auto-sizing error + void* p1 = allocator.allocate(256); + void* p2 = allocator.allocate(256); + + // Size=0 with On mode should work (auto-sizing from allocation record) + EXPECT_NO_THROW(rm.copy(p2, p1, 0)); + + allocator.deallocate(p1); + allocator.deallocate(p2); + } +} + +#if defined(UMPIRE_ENABLE_CUDA) || defined(UMPIRE_ENABLE_HIP) +TEST(IntrospectionLevelTest, MultiGPU) +{ + auto& rm = umpire::ResourceManager::getInstance(); + const auto level = getCurrentLevel(); + + int num_devices = rm.getNumDevices(); + if (num_devices < 2) { + GTEST_SKIP() << "Test requires multiple GPUs"; + } + + if (level != umpire::IntrospectionLevel::Basic) { + GTEST_SKIP() << "MultiGPU test specific to Basic mode"; + } + + // Test device 0 + { + umpire::Allocator device0{rm.getAllocator("DEVICE::0")}; + void* ptr = device0.allocate(256); + + EXPECT_TRUE(rm.hasAllocator(ptr)); + auto retrieved = rm.getAllocator(ptr); + // Should return DEVICE or DEVICE::0 + EXPECT_TRUE(retrieved.getName() == "DEVICE" || + retrieved.getName() == "DEVICE::0"); + + device0.deallocate(ptr); + } + + // Test device 1 + { + umpire::Allocator device1{rm.getAllocator("DEVICE::1")}; + void* ptr = device1.allocate(256); + + EXPECT_TRUE(rm.hasAllocator(ptr)); + auto retrieved = rm.getAllocator(ptr); + // Should return DEVICE::1 or fallback to DEVICE + EXPECT_TRUE(retrieved.getName() == "DEVICE::1" || + retrieved.getName() == "DEVICE"); + + device1.deallocate(ptr); + } +} +#endif