diff --git a/CMakeLists.txt b/CMakeLists.txt index e2b70f0..b17a6be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,7 +189,11 @@ target_sources(gpufl PRIVATE include/gpufl/core/monitor_batch_manager.cpp include/gpufl/core/monitor_record_builders.cpp include/gpufl/core/monitor.cpp + include/gpufl/core/client_startup.cpp + include/gpufl/core/monitor_configuration.cpp include/gpufl/core/gpufl.cpp + include/gpufl/core/session_bootstrap.cpp + include/gpufl/core/startup_configuration.cpp include/gpufl/core/common.cpp include/gpufl/core/stack_trace.cpp include/gpufl/core/itanium_demangle.cpp diff --git a/daemon/launcher/CMakeLists.txt b/daemon/launcher/CMakeLists.txt index ee5b1f1..6779560 100644 --- a/daemon/launcher/CMakeLists.txt +++ b/daemon/launcher/CMakeLists.txt @@ -28,6 +28,7 @@ add_executable(gpufl_launcher cli_trace_options.cpp info_command.cpp trace_command_common.cpp + trace_run_plan.cpp deep_window_env.cpp segmentation_env.cpp ${GPUFL_LAUNCHER_TRACE_IMPL} diff --git a/daemon/launcher/trace_command_common.cpp b/daemon/launcher/trace_command_common.cpp index ee86e44..c5a7496 100644 --- a/daemon/launcher/trace_command_common.cpp +++ b/daemon/launcher/trace_command_common.cpp @@ -1,4 +1,5 @@ #include "trace_command_common.hpp" +#include "trace_run_plan.hpp" #include @@ -11,8 +12,6 @@ #include #include #include -#include -#include #include #include #include @@ -56,48 +55,6 @@ std::string firstEngineOfToken(const std::string& token) { return plus == std::string::npos ? token : token.substr(0, plus); } -std::string makeSessionId() { - static std::mt19937_64 rng{ - static_cast( - std::chrono::steady_clock::now().time_since_epoch().count())}; - uint64_t v = rng(); - std::ostringstream os; - os << std::hex << std::setw(8) << std::setfill('0') << (v & 0xffffffffu); - return os.str(); -} - -std::string makeAnalysisId() { - static std::mt19937_64 rng{ - static_cast( - std::chrono::steady_clock::now().time_since_epoch().count()) ^ - 0x9e3779b97f4a7c15ULL}; - const uint64_t a = rng(); - const uint64_t b = rng(); - char buf[40]; - std::snprintf(buf, sizeof(buf), "%08x-%04x-%04x-%04x-%012llx", - static_cast(a >> 32), - static_cast((a >> 16) & 0xffff), - static_cast(a & 0xffff), - static_cast((b >> 48) & 0xffff), - static_cast(b & 0xffffffffffffULL)); - return buf; -} - -// Filesystem-safe slug for a run-folder name: keep [A-Za-z0-9-_.], turn anything else into '-', -// trim leading/trailing dashes, cap length. Empty input falls back to "session". -std::string slugForPath(const std::string& in) { - auto safe = [](const char c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.'; - }; - std::string out; - for (const char c : in) out.push_back(safe(c) ? c : '-'); - while (!out.empty() && out.front() == '-') out.erase(out.begin()); - while (!out.empty() && out.back() == '-') out.pop_back(); - if (out.size() > 48) out.resize(48); - return out.empty() ? std::string("session") : out; -} - int64_t nowNs() { return std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); @@ -758,37 +715,13 @@ int runTraceCommon(const TraceArgs& args, const TracePlatform& platform) { return 3; } - const std::vector plan = resolvePassPlan(args); - const bool multipass = plan.size() > 1; - - const std::string analysis_id = multipass ? makeAnalysisId() : std::string(); - const std::string run_id = segmented ? generateRunId() : std::string(); - const std::string dir_tag = - multipass ? analysis_id : (segmented ? run_id : makeSessionId()); - - const std::string app_name = args.name.empty() - ? platform.defaultAppName(args.command.front()) - : args.name; - - // Where this run's session folder(s) land. The default output dir is already - // per-run (defaultOutputDir(dir_tag)), so sessions sit flat inside it. An - // explicit --output is a dir the user reuses across runs: - // - multi-pass nests its passes under a readable "run--" - // folder so the passes of one analysis stay grouped (a flat dir would lose - // which sessions belong to the same run); - // - single-pass is one session, so it lands flat as // - - // matching embedded gpufl and the upload agent's / - // discovery, with no redundant per-session wrapper. - fs::path output_dir; - if (args.output_dir.empty()) { - output_dir = platform.defaultOutputDir(dir_tag); - } else if (multipass) { - const std::string run_folder = - "run-" + slugForPath(app_name) + "-" + dir_tag.substr(0, 8); - output_dir = fs::path(args.output_dir) / run_folder; - } else { - output_dir = fs::path(args.output_dir); - } + const TraceRunPlan trace_plan = createTraceRunPlan(args, platform); + const std::vector& plan = trace_plan.passes; + const bool multipass = trace_plan.multipass; + const std::string& analysis_id = trace_plan.analysis_id; + const std::string& run_id = trace_plan.run_id; + const std::string& app_name = trace_plan.app_name; + const fs::path& output_dir = trace_plan.output_dir; std::error_code ec; fs::create_directories(output_dir, ec); @@ -838,17 +771,7 @@ int runTraceCommon(const TraceArgs& args, const TracePlatform& platform) { if (!applyDeepWindowEnv(args, platform)) return 2; if (!applySegmentationEnv(args, run_id, platform)) return 2; - // A bounded window stops the target after warmup+window wall-clock; - // run_ms == 0 keeps the historical "run until the target exits" behavior. - RunOptions run_opts; - if (args.window_ms > 0) { - run_opts.run_ms = args.warmup_ms + args.window_ms; - } - if (args.window_timeout_ms > 0) { - run_opts.run_ms = run_opts.run_ms > 0 - ? std::min(run_opts.run_ms, args.window_timeout_ms) - : args.window_timeout_ms; - } + const RunOptions& run_opts = trace_plan.run_options; AgentProcess agent; if (args.upload) { diff --git a/daemon/launcher/trace_run_plan.cpp b/daemon/launcher/trace_run_plan.cpp new file mode 100644 index 0000000..32c640f --- /dev/null +++ b/daemon/launcher/trace_run_plan.cpp @@ -0,0 +1,96 @@ +#include "trace_run_plan.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace gpufl::launcher { +namespace { + +std::string makeSessionId() { + static std::mt19937_64 rng{ + static_cast( + std::chrono::steady_clock::now().time_since_epoch().count())}; + const uint64_t value = rng(); + std::ostringstream out; + out << std::hex << std::setw(8) << std::setfill('0') + << (value & 0xffffffffu); + return out.str(); +} + +std::string makeAnalysisId() { + static std::mt19937_64 rng{ + static_cast( + std::chrono::steady_clock::now().time_since_epoch().count()) ^ + 0x9e3779b97f4a7c15ULL}; + const uint64_t a = rng(); + const uint64_t b = rng(); + char buffer[40]; + std::snprintf(buffer, sizeof(buffer), "%08x-%04x-%04x-%04x-%012llx", + static_cast(a >> 32), + static_cast((a >> 16) & 0xffff), + static_cast(a & 0xffff), + static_cast((b >> 48) & 0xffff), + static_cast(b & 0xffffffffffffULL)); + return buffer; +} + +// Filesystem-safe slug for a multi-pass run-folder name. Empty input falls +// back to "session" so planning never creates an invalid or invisible folder. +std::string slugForPath(const std::string& input) { + const auto safe = [](const char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.'; + }; + std::string output; + for (const char c : input) output.push_back(safe(c) ? c : '-'); + while (!output.empty() && output.front() == '-') output.erase(output.begin()); + while (!output.empty() && output.back() == '-') output.pop_back(); + if (output.size() > 48) output.resize(48); + return output.empty() ? std::string("session") : output; +} + +} // namespace + +TraceRunPlan createTraceRunPlan(const TraceArgs& args, + const TracePlatform& platform) { + TraceRunPlan plan; + plan.passes = resolvePassPlan(args); + plan.multipass = plan.passes.size() > 1; + plan.segmented = segmentationRequested(args); + plan.analysis_id = plan.multipass ? makeAnalysisId() : std::string(); + plan.run_id = plan.segmented ? generateRunId() : std::string(); + plan.directory_tag = plan.multipass + ? plan.analysis_id + : (plan.segmented ? plan.run_id : makeSessionId()); + plan.app_name = args.name.empty() + ? platform.defaultAppName(args.command.front()) + : args.name; + + if (args.output_dir.empty()) { + plan.output_dir = platform.defaultOutputDir(plan.directory_tag); + } else if (plan.multipass) { + const std::string run_folder = + "run-" + slugForPath(plan.app_name) + "-" + + plan.directory_tag.substr(0, 8); + plan.output_dir = fs::path(args.output_dir) / run_folder; + } else { + plan.output_dir = fs::path(args.output_dir); + } + + if (args.window_ms > 0) { + plan.run_options.run_ms = args.warmup_ms + args.window_ms; + } + if (args.window_timeout_ms > 0) { + plan.run_options.run_ms = plan.run_options.run_ms > 0 + ? std::min(plan.run_options.run_ms, args.window_timeout_ms) + : args.window_timeout_ms; + } + return plan; +} + +} // namespace gpufl::launcher diff --git a/daemon/launcher/trace_run_plan.hpp b/daemon/launcher/trace_run_plan.hpp new file mode 100644 index 0000000..7e838dc --- /dev/null +++ b/daemon/launcher/trace_run_plan.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "trace_command_common.hpp" + +#include +#include + +namespace gpufl::launcher { + +// The pure, per-invocation decisions made before trace starts mutating the +// target environment or filesystem. output_dir is intentionally only a path +// here; runTraceCommon owns creating and canonicalising it. +struct TraceRunPlan { + std::vector passes; + bool segmented = false; + bool multipass = false; + std::string analysis_id; + std::string run_id; + std::string directory_tag; + std::string app_name; + fs::path output_dir; + RunOptions run_options; +}; + +TraceRunPlan createTraceRunPlan(const TraceArgs& args, + const TracePlatform& platform); + +} // namespace gpufl::launcher diff --git a/include/gpufl/core/client_startup.cpp b/include/gpufl/core/client_startup.cpp new file mode 100644 index 0000000..154c79f --- /dev/null +++ b/include/gpufl/core/client_startup.cpp @@ -0,0 +1,324 @@ +#include "gpufl/core/client_startup.hpp" + +#include +#include +#include +#include +#include +#include + +#include "gpufl/gpufl.hpp" +#include "gpufl/backends/host_collector.hpp" +#include "gpufl/core/backend_factory.hpp" +#include "gpufl/core/common.hpp" +#include "gpufl/core/debug_logger.hpp" +#include "gpufl/core/env_vars.hpp" +#include "gpufl/core/events.hpp" +#include "gpufl/core/logger/logger.hpp" +#include "gpufl/core/model/lifecycle_model.hpp" +#include "gpufl/core/model/system_event_model.hpp" +#include "gpufl/core/monitor.hpp" +#include "gpufl/core/monitor_configuration.hpp" +#include "gpufl/core/remote_config.hpp" +#include "gpufl/core/runtime.hpp" +#include "gpufl/core/segment_runtime.hpp" +#include "gpufl/core/session_bootstrap.hpp" +#include "gpufl/core/startup_configuration.hpp" + +#if GPUFL_HAS_CUDA || defined(__CUDACC__) +#include +#endif + +namespace gpufl::detail { +namespace { + +bool windowsInjectedProcess() { +#if defined(_WIN32) + const char* injected = std::getenv(env::kInject); + return injected && std::string(injected) == "1"; +#else + return false; +#endif +} + +void configureEagerModuleLoading(const MonitorOptions& options) { + // EAGER module loading is opt-in. It avoids the known SASS lazy-patching + // deadlock, but carries a whole-process startup/memory cost, so it must run + // before the first CUDA call and must never override a user's own setting. + if (options.profiling_engine != ProfilingEngine::SassMetrics && + options.profiling_engine != ProfilingEngine::Deep) { + return; + } + const char* knob_env = std::getenv(env::kEagerModuleLoading); + const std::string knob = knob_env ? knob_env : ""; + const bool opted_in = knob == "1" || knob == "true" || + knob == "yes" || knob == "on"; + if (!opted_in || std::getenv(env::kCudaModuleLoading) != nullptr) return; +#if defined(_WIN32) + _putenv_s(env::kCudaModuleLoading, "EAGER"); +#else + setenv(env::kCudaModuleLoading, "EAGER", /*overwrite=*/0); +#endif + GFL_LOG_DEBUG("[gpufl] CUDA_MODULE_LOADING=EAGER set " + "(GPUFL_EAGER_MODULE_LOADING opt-in) for SASS/Deep."); +} + +void autoTuneKernelSampleRate(const InitOptions& init_options, + MonitorOptions& monitor_options) { +#if GPUFL_HAS_CUDA || defined(__CUDACC__) + if (monitor_options.kernel_sample_rate_ms <= 0 || + monitor_options.kernel_sample_rate_ms >= 200 || + (monitor_options.profiling_engine != ProfilingEngine::SassMetrics && + monitor_options.profiling_engine != ProfilingEngine::Deep)) { + return; + } + cudaDeviceProp prop{}; + int device_id = 0; + if (cudaGetDevice(&device_id) != cudaSuccess || + cudaGetDeviceProperties(&prop, device_id) != cudaSuccess) { + return; + } + if (prop.major < 12 && + monitor_options.kernel_sample_rate_ms == init_options.kernel_sample_rate_ms) { + GFL_LOG_DEBUG("[gpufl] Auto-tuning kernel_sample_rate_ms 50 -> 200 " + "on sm_", prop.major, prop.minor, + " (SASS metrics have significant per-launch overhead " + "on pre-sm_120 GPUs). Set the value explicitly to override."); + monitor_options.kernel_sample_rate_ms = 200; + } +#else + (void)init_options; + (void)monitor_options; +#endif +} + +} // namespace + +class ClientStartup::State { +public: + StartupSegmentationOptions segmentation; + InitialSessionLoggingState logging; + MonitorOptions monitor_options; + InitEvent initial_event; +}; + +ClientStartup::ClientStartup(InitOptions& active_options) + : options_(active_options), state_(std::make_unique()) {} + +ClientStartup::~ClientStartup() = default; + +bool ClientStartup::start() { + if (!resolveConfiguration()) return false; + if (!createRuntime()) return false; + + launchVersionProbe(); + set_runtime(std::move(pending_runtime_)); + return startMonitor(); +} + +bool ClientStartup::resolveConfiguration() { + resolveStartupOptions(options_); + DebugLogger::setEnabled(options_.enable_debug_output); + GFL_LOG_DEBUG("Initializing..."); + + std::string error; + if (!readStartupSegmentationOptions(state_->segmentation, error)) { + GFL_LOG_ERROR(error); + return false; + } + segmented_ = state_->segmentation.enabled(); + return true; +} + +bool ClientStartup::createRuntime() { + if (runtime()) { + GFL_LOG_DEBUG("Runtime already exists, shutting down first..."); + shutdown(); + } + + pending_runtime_ = std::make_unique(); + pending_runtime_->app_name = options_.app_name.empty() ? "gpufl" : options_.app_name; + pending_runtime_->session_id = GenerateSessionId(); + if (segmented_) { + pending_runtime_->run_id = state_->segmentation.run_id; + pending_runtime_->segment_index = 0; + pending_runtime_->segment_every_ms = + static_cast(state_->segmentation.segment_every_ms); + pending_runtime_->segment_max_rows = state_->segmentation.segment_max_rows; + pending_runtime_->run_roll_every_ms = + static_cast(state_->segmentation.run_roll_every_ms); + pending_runtime_->run_roll_max_bytes = + state_->segmentation.run_roll_max_bytes; + } + pending_runtime_->logger = std::make_shared(); + pending_runtime_->host_collector = std::make_unique(); + return openInitialSessionLogging(*pending_runtime_, options_, segmented_, + state_->logging); +} + +void ClientStartup::launchVersionProbe() const { + // Bounded, detached version discovery is advisory: offline/file-only use + // must never block or fail initialization. + std::string probe_url; + if (const char* value = std::getenv(env::kBackendUrl)) probe_url = value; + else if (const char* value = std::getenv(env::kRemoteConfig)) probe_url = value; + if (probe_url.empty()) return; + std::thread([url = std::move(probe_url), api_path = options_.api_path] { + probeBackendVersion(url, api_path); + }).detach(); +} + +bool ClientStartup::startMonitor() { + GFL_LOG_DEBUG("Initializing Monitor (CUPTI)..."); + state_->monitor_options = buildMonitorOptions(options_); + configureEagerModuleLoading(state_->monitor_options); + autoTuneKernelSampleRate(options_, state_->monitor_options); + Monitor::Initialize(state_->monitor_options); + + GFL_LOG_DEBUG("Starting Monitor..."); + Monitor::Start(); + GFL_LOG_DEBUG("Monitor started"); + return activateRuntime(); +} + +bool ClientStartup::activateRuntime() { + Runtime* const active_runtime = runtime(); + const auto segment = active_runtime + ? active_runtime->acquireSegmentContext("client_startup") + : nullptr; + if (!segment) { + GFL_LOG_ERROR("Missing active segment context before job_start"); + return false; + } + + configureCollectors(*active_runtime); + emitInitialEvent(*active_runtime, *segment); + if (!startSegmentRuntime(*active_runtime)) { + GFL_LOG_ERROR("Failed to start SegmentRuntime"); + shutdown(); + return false; + } + configureSampler(*active_runtime); + startContinuousSampling(*active_runtime, *segment); + return true; +} + +void ClientStartup::configureCollectors(Runtime& active_runtime) const { + std::string backend_reason; + auto collectors = CreateBackendCollectors(options_.backend, &backend_reason); + active_runtime.unified_gpu_collector = std::move(collectors.unified_collector); + active_runtime.collector = std::move(collectors.telemetry_collector); + active_runtime.static_info_collector = + std::move(collectors.static_info_collector); + if (!active_runtime.collector) { + GFL_LOG_ERROR("Failed to initialize GPU backend: ", backend_reason); + } +} + +void ClientStartup::emitInitialEvent(Runtime& active_runtime, + const SegmentContext& segment) { + InitEvent event; + event.pid = GetPid(); + event.session_id = segment.session_id; + event.app = active_runtime.app_name; + event.log_path = state_->logging.log_path; + event.ts_ns = GetTimestampNs(); + if (active_runtime.collector) { + event.devices = active_runtime.collector->sampleAll(); + } + const bool skip_static_info = windowsInjectedProcess(); + if (active_runtime.static_info_collector && !skip_static_info) { + event.gpu_static_device_infos = + active_runtime.static_info_collector->sampleStaticInfo(); + } else if (skip_static_info) { + GFL_LOG_DEBUG("Skipping CUDA static GPU inventory during Windows injection init."); + } + event.host = active_runtime.host_collector->sample(); + event.session_kind = ProfilingEngineSessionKind(state_->monitor_options.profiling_engine); + event.profiling_engine = ProfilingEngineWireName(state_->monitor_options.profiling_engine); + event.run_id = segment.run_id; + event.segment_index = segment.segment_index; + if (segment.run_part) { + event.roll_chain_id = segment.run_part->roll_chain_id; + event.previous_run_id = segment.run_part->previous_run_id; + event.part_index = segment.run_part->part_index; + } + + // The launcher tags each child of a multi-pass analysis through the + // environment; absent fields preserve the ordinary single-pass wire form. + if (const char* analysis_id = std::getenv(env::kAnalysisId); + analysis_id && *analysis_id) { + event.analysis_id = analysis_id; + if (const char* pass_index = std::getenv(env::kPassIndex)) { + event.pass_index = std::atoi(pass_index); + } + if (const char* pass_count = std::getenv(env::kPassCount)) { + event.pass_count = std::atoi(pass_count); + } + GFL_LOG_DEBUG("Multi-pass: analysis_id=", event.analysis_id, + " pass ", event.pass_index, "/", event.pass_count); + } + + segment.logger->write(model::InitEventModel(event)); + state_->initial_event = std::move(event); +} + +bool ClientStartup::startSegmentRuntime(Runtime& active_runtime) { + if (!segmented_) return true; + + SegmentRuntime::Options options; + options.runtime = &active_runtime; + options.logger_options = state_->logging.options; + options.logger_options.on_serialized_bytes = {}; + options.init_template = state_->initial_event; + options.segment_every_ms = active_runtime.segment_every_ms; + options.segment_max_rows = active_runtime.segment_max_rows; + options.run_roll_every_ms = active_runtime.run_roll_every_ms; + options.run_roll_max_bytes = active_runtime.run_roll_max_bytes; + active_runtime.segment_runtime = + std::make_shared(std::move(options)); + return active_runtime.segment_runtime->start(); +} + +void ClientStartup::configureSampler(Runtime& active_runtime) const { + if (options_.system_sample_rate_ms <= 0 || !active_runtime.collector) return; + Runtime* const runtime_ptr = &active_runtime; + active_runtime.sampler.configure( + active_runtime.app_name, + [runtime_ptr] { + return runtime_ptr->acquireSegmentContext("sampler"); + }, + [runtime_ptr] { + return runtime_ptr->peekSegmentContext(); + }, + active_runtime.collector, options_.system_sample_rate_ms, + active_runtime.host_collector.get(), + [runtime_ptr](const uint32_t index, const uint64_t rows, + const int64_t steady_ns, const int64_t event_ns) { + if (runtime_ptr->segment_runtime) { + runtime_ptr->segment_runtime->noteRows( + index, rows, steady_ns, event_ns); + } + }); +} + +void ClientStartup::startContinuousSampling( + Runtime& active_runtime, const SegmentContext& segment) const { + if (options_.continuous_system_sampling && segment.logger) { + SystemStartEvent event; + event.pid = GetPid(); + event.app = active_runtime.app_name; + event.name = "sampling_start"; + event.session_id = segment.session_id; + event.ts_ns = GetTimestampNs(); + if (active_runtime.collector) event.devices = active_runtime.collector->sampleAll(); + if (active_runtime.host_collector) event.host = active_runtime.host_collector->sample(); + segment.logger->write(model::SystemStartModel(event)); + } + if (options_.continuous_system_sampling && options_.system_sample_rate_ms > 0 && + active_runtime.collector) { + active_runtime.sampler.activate(); + } +} + +} // namespace gpufl::detail diff --git a/include/gpufl/core/client_startup.hpp b/include/gpufl/core/client_startup.hpp new file mode 100644 index 0000000..7a399b9 --- /dev/null +++ b/include/gpufl/core/client_startup.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include + +namespace gpufl { +struct InitOptions; +struct Runtime; +struct SegmentContext; + +namespace detail { + +/** + * Startup transaction for one public gpufl::init() invocation. + * + * It owns only state that exists while a runtime is being constructed. The + * process-wide InitOptions remain in g_opts because normal runtime APIs read + * them after startup; Runtime takes ownership only once logging is open and + * its first immutable SegmentContext has been published. + */ +class ClientStartup { +public: + explicit ClientStartup(InitOptions& active_options); + ~ClientStartup(); + + // Starts through continuous-sampling activation. The public init() wrapper + // remains responsible for the NVTX guard and deep-window installation that + // must happen after CUPTI has finished wiring its injection table. + bool start(); + +private: + bool resolveConfiguration(); + bool createRuntime(); + void launchVersionProbe() const; + bool startMonitor(); + bool activateRuntime(); + void configureCollectors(Runtime& runtime) const; + void emitInitialEvent(Runtime& runtime, const SegmentContext& segment); + bool startSegmentRuntime(Runtime& runtime); + void configureSampler(Runtime& runtime) const; + void startContinuousSampling(Runtime& runtime, + const SegmentContext& segment) const; + + InitOptions& options_; + std::unique_ptr pending_runtime_; + bool segmented_ = false; + + // Declared in the implementation because these domain types are only + // meaningful during startup. Keeping them there prevents this coordinator + // header from becoming another aggregate dependency hub. + class State; + std::unique_ptr state_; +}; + +} // namespace detail +} // namespace gpufl diff --git a/include/gpufl/core/gpufl.cpp b/include/gpufl/core/gpufl.cpp index 74d2db1..491eff5 100644 --- a/include/gpufl/core/gpufl.cpp +++ b/include/gpufl/core/gpufl.cpp @@ -4,33 +4,27 @@ #include #include -#include #include #include #include #include -#include #include #include #include -#include #include #include #include #include #include "gpufl/backends/host_collector.hpp" -#include "gpufl/core/backend_factory.hpp" +#include "gpufl/core/client_startup.hpp" #include "gpufl/core/common.hpp" #include "gpufl/core/teardown_flag.hpp" // detail::isProcessExitTeardown -#include "gpufl/core/config_file_loader.hpp" #include "gpufl/core/debug_logger.hpp" #include "gpufl/core/deep_window.hpp" #include "gpufl/core/deep_window_rules.hpp" -#include "gpufl/core/dictionary_manager.hpp" #include "gpufl/core/events.hpp" #include "gpufl/core/logger/logger.hpp" -#include "gpufl/core/remote_config.hpp" #include "gpufl/core/version.hpp" #include "gpufl/upload/upload_logs.hpp" // NOTE: we intentionally do NOT include in this TU. @@ -45,12 +39,9 @@ #include "gpufl/core/monitor_backend.hpp" #include "gpufl/core/runtime.hpp" #include "gpufl/core/segment_runtime.hpp" -#include "gpufl/core/segmentation_config.hpp" +#include "gpufl/core/session_bootstrap.hpp" #include "gpufl/core/scope_registry.hpp" #include "gpufl/report/text_report.hpp" -#if GPUFL_HAS_CUDA || defined(__CUDACC__) -#include -#endif // NVTX (NVIDIA Tools Extension) - zero-overhead annotation library. // When GPUFL_HAS_NVTX is defined (see CMakeLists NVTX block), GFL_SCOPE @@ -185,39 +176,6 @@ namespace gpufl { std::atomic g_systemSampleRateMs{0}; InitOptions g_opts; -namespace { - -MonitorBackendKind ToMonitorBackendKind(const BackendKind backend) { - switch (backend) { - case BackendKind::Nvidia: - return MonitorBackendKind::Nvidia; - case BackendKind::Amd: - return MonitorBackendKind::Amd; - case BackendKind::None: - return MonitorBackendKind::None; - case BackendKind::Auto: - default: - return MonitorBackendKind::Auto; - } -} - -} // namespace - -static std::string defaultLogPath_(const std::string& app) { - // v1.2: log_path is a directory (sessions nest inside it as - // `//.log`). The legacy convention - // returned ".log" which the rotator stripped down to "" - // anyway; explicitly return just "" so debug output and - // `clean_logs(log_path=...)` show the same value as what's on - // disk. - return app; -} - -// Remembered after init() for use by generateReport() after shutdown() -static std::string g_lastLogPath; -static std::string g_lastSessionId; -static std::string g_lastAppName; - static std::atomic g_nextScopeId{1}; static uint64_t nextScopeId_() { @@ -243,52 +201,6 @@ bool envDisabled_() { return s == "1" || s == "true" || s == "yes" || s == "on"; } -bool windowsInjectedProcess_() { -#if defined(_WIN32) - const char* injected = std::getenv(gpufl::env::kInject); - return injected && std::string(injected) == "1"; -#else - return false; -#endif -} - -bool parseNonNegativeEnv_(const char* key, uint64_t& value, - std::string& error) { - value = 0; - const char* raw = std::getenv(key); - if (!raw || !*raw) return true; - if (*raw == '-') { - error = std::string(key) + " must be a non-negative integer"; - return false; - } - errno = 0; - char* end = nullptr; - const unsigned long long parsed = std::strtoull(raw, &end, 10); - if (end == raw || *end != '\0' || errno == ERANGE) { - error = std::string(key) + "='" + raw + - "' is invalid (expected a non-negative integer)"; - return false; - } - value = static_cast(parsed); - return true; -} - -bool isUuidV4_(const char* value) { - if (!value || std::strlen(value) != 36) return false; - for (size_t i = 0; i < 36; ++i) { - if (i == 8 || i == 13 || i == 18 || i == 23) { - if (value[i] != '-') return false; - } else if (!std::isxdigit(static_cast(value[i]))) { - return false; - } - } - if (value[14] != '4') return false; - const char variant = - static_cast(std::tolower(static_cast(value[19]))); - return variant == '8' || variant == '9' || variant == 'a' || - variant == 'b'; -} - } // namespace bool init(const InitOptions& opts) { @@ -310,488 +222,8 @@ bool init(const InitOptions& opts) { } g_opts = opts; - - // Read config file early - before anything uses the options - { - std::string configPath = g_opts.config_file; - if (configPath.empty()) { - if (const char* env = std::getenv(env::kConfigFile)) configPath = env; - } - if (!configPath.empty()) { - ConfigFileLoader::apply(g_opts, configPath); - } - } - - { - // Resolve api_path (InitOptions value or GPUFL_API_PATH) and normalize - // once - the version-discovery probe below appends to it. Backend - // creds live on UploadOptions now, not InitOptions; the probe reads - // GPUFL_BACKEND_URL straight from the environment. - std::string apiPath = g_opts.api_path; - if (apiPath.empty()) { - if (const char* e = std::getenv(env::kApiPath)) apiPath = e; - } - g_opts.api_path = normalizeApiPath(apiPath); - } - - DebugLogger::setEnabled(g_opts.enable_debug_output); - GFL_LOG_DEBUG("Initializing..."); - - uint64_t segment_every_ms = 0; - uint64_t segment_max_rows = 0; - std::string segmentation_error; - if (!parseNonNegativeEnv_(env::kSegmentEveryMs, segment_every_ms, - segmentation_error) || - !parseNonNegativeEnv_(env::kSegmentMaxRows, segment_max_rows, - segmentation_error)) { - GFL_LOG_ERROR(segmentation_error); - return false; - } - const bool segmented = segment_every_ms > 0 || segment_max_rows > 0; - if (segment_every_ms > - static_cast((std::numeric_limits::max)())) { - GFL_LOG_ERROR(env::kSegmentEveryMs, - " exceeds the supported signed 64-bit millisecond range"); - return false; - } - - uint64_t run_roll_every_ms = 0; - uint64_t run_roll_max_bytes = 0; - if (!parseNonNegativeEnv_(env::kRunRollEveryMs, run_roll_every_ms, - segmentation_error) || - !parseNonNegativeEnv_(env::kRunRollMaxBytes, run_roll_max_bytes, - segmentation_error)) { - GFL_LOG_ERROR(segmentation_error); - return false; - } - if (run_roll_every_ms > - static_cast((std::numeric_limits::max)())) { - GFL_LOG_ERROR(env::kRunRollEveryMs, - " exceeds the supported signed 64-bit millisecond range"); - return false; - } - - const char* env_run_id = std::getenv(env::kRunId); - if (segmented && (!env_run_id || !*env_run_id)) { - GFL_LOG_ERROR("Session segmentation requires GPUFL_RUN_ID. The " - "launcher must generate one run ID before starting the " - "target."); - return false; - } - if (segmented && !isUuidV4_(env_run_id)) { - GFL_LOG_ERROR(env::kRunId, "='", env_run_id, - "' is invalid (expected a UUIDv4)"); - return false; - } - if (segmented && !segmentation::kRuntimeReady) { - GFL_LOG_ERROR( - "This build does not include executable session segmentation."); - return false; - } - - if (runtime()) { - GFL_LOG_DEBUG("Runtime already exists, shutting down first..."); - shutdown(); - } - - auto rt = std::make_unique(); - rt->app_name = g_opts.app_name.empty() ? "gpufl" : g_opts.app_name; - rt->session_id = detail::GenerateSessionId(); - if (segmented) { - rt->run_id = env_run_id; - rt->segment_index = 0; - rt->segment_every_ms = static_cast(segment_every_ms); - rt->segment_max_rows = segment_max_rows; - rt->run_roll_every_ms = static_cast(run_roll_every_ms); - rt->run_roll_max_bytes = run_roll_max_bytes; - } - rt->logger = std::make_shared(); - rt->host_collector = std::make_unique(); - - const std::string logPath = - g_opts.log_path.empty() ? defaultLogPath_(rt->app_name) : g_opts.log_path; - - Logger::Options logOpts; - logOpts.base_path = logPath; - // Threaded through so the rotator can write under - // `//.log` - v1.2 disk layout. The - // uploader uses the directory name to discover sessions instead of - // parsing job_start events out of flat log files. - logOpts.session_id = rt->session_id; - logOpts.system_sample_rate_ms = g_opts.system_sample_rate_ms; - logOpts.flush_always = g_opts.flush_logs_always; - if (const char* v = std::getenv(env::kFlushLogsAlways)) { - std::string flag(v); - std::transform(flag.begin(), flag.end(), flag.begin(), - [](unsigned char c) { - return static_cast(std::tolower(c)); - }); - if (flag == "1" || flag == "true" || flag == "yes" || - flag == "on") { - logOpts.flush_always = true; - } - } - if (const char* v = std::getenv(env::kLogRotateBytes)) { - if (const auto bytes = std::strtoull(v, nullptr, 10); bytes > 0) { - logOpts.rotate_bytes = static_cast(bytes); - } - } - if (const char* v = std::getenv(env::kLogRotateAfterMs)) { - if (const auto ms = std::strtoll(v, nullptr, 10); ms > 0) { - logOpts.rotate_after_ms = static_cast(ms); - } - } - if (const char* v = std::getenv(env::kLogMaxSpoolBytes)) { - logOpts.max_spool_bytes = - std::strtoull(v, nullptr, 10); - } - if (const char* v = std::getenv(env::kLogMinFreeBytes)) { - logOpts.min_free_bytes = - std::strtoull(v, nullptr, 10); - } - - g_lastLogPath = logPath; - g_lastSessionId = rt->session_id; - g_lastAppName = rt->app_name; - - - std::shared_ptr initial_run_part; - if (segmented && - (rt->run_roll_every_ms > 0 || rt->run_roll_max_bytes > 0)) { - initial_run_part = std::make_shared( - rt->run_id, rt->run_id, std::string(), 1u, - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count(), - 0u); - } - if (initial_run_part && rt->run_roll_max_bytes > 0) { - logOpts.on_serialized_bytes = - [part = initial_run_part](const uint64_t bytes) noexcept { - part->addSerializedBytes(bytes); - }; - } - - GFL_LOG_DEBUG("Opening log file: ", logPath); - if (!rt->logger->open(logOpts)) { - GFL_LOG_ERROR("Failed to open logger at: ", logPath); - return false; - } - auto initial_dictionary = - segmented ? std::make_shared() : nullptr; - - if (!rt->publishSegmentContext(std::make_shared( - rt->run_id, rt->session_id, rt->segment_index, - detail::GetTimestampNs(), rt->logger, initial_dictionary, - initial_run_part))) { - GFL_LOG_ERROR("Failed to publish the initial segment context"); - rt->logger->close(); - return false; - } - - // Fire-and-forget version-discovery probe. Hits - // /info/version with 2s timeouts to detect - // client/backend version drift early and emit a clear warning. - // Must NEVER block init - detached, bounded by httplib timeouts. - // Reads GPUFL_BACKEND_URL from the environment (creds live on - // UploadOptions now); skipped when unset (offline / file-only mode). - std::string probeUrl; - if (const char* e = std::getenv(env::kBackendUrl)) probeUrl = e; - else if (const char* e2 = std::getenv(env::kRemoteConfig)) probeUrl = e2; - if (!probeUrl.empty()) { - std::thread([url = probeUrl, ap = g_opts.api_path] { - probeBackendVersion(url, ap); - }).detach(); - } - - set_runtime(std::move(rt)); - rt = nullptr; // rt is now moved - - GFL_LOG_DEBUG("Initializing Monitor (CUPTI)..."); - MonitorOptions mOpts; - mOpts.enable_debug_output = g_opts.enable_debug_output; - mOpts.profiling_engine = g_opts.profiling_engine; - - // Allow environment variable override: GPUFL_PROFILING_ENGINE. - // Accepts exactly the six canonical engine names. Unrecognized - // values are logged and ignored (the engine stays at whatever - // g_opts set above) rather than silently doing nothing. - if (const char* envEngine = std::getenv(gpufl::env::kProfilingEngine)) { - const std::string val(envEngine); - bool matched = true; - if (val == "Monitor") mOpts.profiling_engine = ProfilingEngine::Monitor; - else if (val == "Trace") mOpts.profiling_engine = ProfilingEngine::Trace; - else if (val == "PcSampling") mOpts.profiling_engine = ProfilingEngine::PcSampling; - else if (val == "SassMetrics") mOpts.profiling_engine = ProfilingEngine::SassMetrics; - else if (val == "PmSampling") mOpts.profiling_engine = ProfilingEngine::PmSampling; - else if (val == "RangeProfiler") mOpts.profiling_engine = ProfilingEngine::RangeProfiler; - else if (val == "RangeProfilerKernelReplay") - mOpts.profiling_engine = ProfilingEngine::RangeProfilerKernelReplay; - else if (val == "Deep") mOpts.profiling_engine = ProfilingEngine::Deep; - else matched = false; - if (matched) { - GFL_LOG_DEBUG("GPUFL_PROFILING_ENGINE override: ", val); - } else { - GFL_LOG_ERROR( - "GPUFL_PROFILING_ENGINE='", val, "' is not a recognized " - "engine name. Valid values: Monitor, Trace, PcSampling, " - "SassMetrics, PmSampling, RangeProfiler, Deep. Keeping current engine " - "selection."); - } - } - - // Allow environment override of the PC sampling period (log2 cycles/sample), - // e.g. `gpufl trace --pc-sample-period`. The injection path has no other way - // to reach pc_sampling_period. CUPTI accepts 5..31; out-of-range/garbage is - // logged and ignored so a typo can't silently disable sampling. - if (const char* v = std::getenv(gpufl::env::kPcSamplingPeriod)) { - char* end = nullptr; - const unsigned long n = std::strtoul(v, &end, 10); - if (end != v && *end == '\0' && n >= 5 && n <= 31) { - mOpts.pc_sampling_period = static_cast(n); - GFL_LOG_DEBUG("GPUFL_PC_SAMPLING_PERIOD override: 2^", n, " = ", - (1ul << n), " cycles/sample"); - } else { - GFL_LOG_ERROR("GPUFL_PC_SAMPLING_PERIOD='", v, "' is invalid " - "(expected an integer 5..31). Keeping ", - mOpts.pc_sampling_period, "."); - } - } - - mOpts.kernel_sample_rate_ms = g_opts.kernel_sample_rate_ms; - mOpts.enable_stack_trace = g_opts.enable_stack_trace; - mOpts.enable_source_collection = g_opts.enable_source_collection; - // Propagate the framework-correlation flag to the backend so - // CuptiBackend::start can decide whether to enable - // CUPTI_ACTIVITY_KIND_EXTERNAL_CORRELATION. - mOpts.enable_external_correlation = g_opts.enable_external_correlation; - mOpts.enable_synchronization = g_opts.enable_synchronization; - mOpts.enable_memory_tracking = g_opts.enable_memory_tracking; - mOpts.enable_cuda_graphs_tracking = g_opts.enable_cuda_graphs_tracking; - mOpts.pm_sampling_interval_us = g_opts.pm_sampling_interval_us; - mOpts.pm_sampling_max_samples = g_opts.pm_sampling_max_samples; - mOpts.pm_sampling_preset = g_opts.pm_sampling_preset; - mOpts.pm_sampling_metrics = g_opts.pm_sampling_metrics; - mOpts.pm_sampling_scope_only = g_opts.pm_sampling_scope_only; - mOpts.deep_arm_mode = g_opts.deep_window_only ? DeepArmMode::WindowOnly - : DeepArmMode::Always; - // GPUFL_DEEP_ARM reaches the injection path, which can't set InitOptions. - if (const char* v = std::getenv(gpufl::env::kDeepArm)) { - const std::string val(v); - if (val == "window") { - mOpts.deep_arm_mode = DeepArmMode::WindowOnly; - } else if (val == "always") { - mOpts.deep_arm_mode = DeepArmMode::Always; - } else { - GFL_LOG_ERROR("GPUFL_DEEP_ARM='", val, - "' is not recognized. Valid values: always, window. " - "Keeping current deep arm mode."); - } - } - // WindowOnly subsumes the PM-specific gate: PM sampling already arms on - // scope start, which is exactly what a window is. - if (mOpts.deep_arm_mode == DeepArmMode::WindowOnly) { - mOpts.pm_sampling_scope_only = true; - } - mOpts.backend_kind = ToMonitorBackendKind(g_opts.backend); - - // EAGER module loading is OPT-IN. By default we leave CUDA on its normal - // LAZY loading; the per-architecture SASS exclusion gate - // (GPUFL_SASS_EXCLUDE_ARCHS, in SassMetricsEngine) is the default guard - // for the CUPTI lazy-patching deadlock - it disables SASS only on - // architectures confirmed to hang, rather than paying EAGER's - // whole-process startup/memory cost everywhere. EAGER remains available - // as a per-run alternative: GPUFL_EAGER_MODULE_LOADING=1 forces it (it - // finalizes every module up front, while the process is quiescent, so the - // concurrent-launch finalize that triggers the deadlock never happens). - // - // This MUST run before the first CUDA call below (cudaGetDevice creates - // the context, which reads CUDA_MODULE_LOADING). Honor a value the user - // already set. Python callers apply the same opt-in earlier in - // gpufl.init(); this covers the pure-C++ path. - if (mOpts.profiling_engine == ProfilingEngine::SassMetrics || - mOpts.profiling_engine == ProfilingEngine::Deep) { - const char* knobEnv = std::getenv(env::kEagerModuleLoading); - const std::string knob = knobEnv ? knobEnv : ""; - const bool optedIn = (knob == "1" || knob == "true" || - knob == "yes" || knob == "on"); - if (optedIn && std::getenv(env::kCudaModuleLoading) == nullptr) { -#if defined(_WIN32) - _putenv_s(env::kCudaModuleLoading, "EAGER"); -#else - setenv(gpufl::env::kCudaModuleLoading, "EAGER", /*overwrite=*/0); -#endif - GFL_LOG_DEBUG("[gpufl] CUDA_MODULE_LOADING=EAGER set " - "(GPUFL_EAGER_MODULE_LOADING opt-in) for SASS/Deep."); - } - } - - // Auto-tune kernel_sample_rate_ms on older NVIDIA GPUs where SASS metric - // overhead per kernel launch is much higher. A default 50ms on sm_86 can - // lead to hundreds of captured kernels per second each carrying - // instrumentation replay cost; bump to 200ms so users get a workable - // profile without wild slowdowns. Users can still explicitly set a lower - // value in InitOptions or via config file. -#if GPUFL_HAS_CUDA || defined(__CUDACC__) - if (mOpts.kernel_sample_rate_ms > 0 && mOpts.kernel_sample_rate_ms < 200 && - (mOpts.profiling_engine == ProfilingEngine::SassMetrics || - mOpts.profiling_engine == ProfilingEngine::Deep)) { - cudaDeviceProp prop{}; - int devId = 0; - if (cudaGetDevice(&devId) == cudaSuccess && - cudaGetDeviceProperties(&prop, devId) == cudaSuccess) { - const bool preSm120 = prop.major < 12; - if (preSm120 && mOpts.kernel_sample_rate_ms == g_opts.kernel_sample_rate_ms) { - GFL_LOG_DEBUG("[gpufl] Auto-tuning kernel_sample_rate_ms 50 -> 200 " - "on sm_", prop.major, prop.minor, - " (SASS metrics have significant per-launch overhead " - "on pre-sm_120 GPUs). Set the value explicitly to override."); - mOpts.kernel_sample_rate_ms = 200; - } - } - } -#endif - - Monitor::Initialize(mOpts); - - GFL_LOG_DEBUG("Starting Monitor..."); - Monitor::Start(); - GFL_LOG_DEBUG("Monitor started"); - - Runtime* rt_ptr = runtime(); - const auto segment = rt_ptr ? rt_ptr->acquireSegmentContext() : nullptr; - if (!segment) { - GFL_LOG_ERROR("Missing active segment context before job_start"); - return false; - } - - // Runtime backend selection - std::string backendReason; - auto backendCollectors = - CreateBackendCollectors(g_opts.backend, &backendReason); - rt_ptr->unified_gpu_collector = std::move(backendCollectors.unified_collector); - rt_ptr->collector = std::move(backendCollectors.telemetry_collector); - rt_ptr->static_info_collector = - std::move(backendCollectors.static_info_collector); - - if (!rt_ptr->collector) { - GFL_LOG_ERROR("Failed to initialize GPU backend: ", backendReason); - } - - // init event with inventory (optional) - InitEvent ie; - ie.pid = detail::GetPid(); - ie.session_id = segment->session_id; - ie.app = rt_ptr->app_name; - ie.log_path = logPath; - ie.ts_ns = detail::GetTimestampNs(); - // Collector may be unavailable on systems without NVML/ROCm. Guard usage. - if (rt_ptr->collector) { - ie.devices = rt_ptr->collector->sampleAll(); - } - const bool skipStaticInfoDuringInject = windowsInjectedProcess_(); - if (rt_ptr->static_info_collector && !skipStaticInfoDuringInject) { - ie.gpu_static_device_infos = - rt_ptr->static_info_collector->sampleStaticInfo(); - } else if (skipStaticInfoDuringInject) { - GFL_LOG_DEBUG("Skipping CUDA static GPU inventory during Windows injection init."); - } - ie.host = rt_ptr->host_collector->sample(); - - ie.session_kind = ProfilingEngineSessionKind(mOpts.profiling_engine); - ie.profiling_engine = ProfilingEngineWireName(mOpts.profiling_engine); - ie.run_id = segment->run_id; - ie.segment_index = segment->segment_index; - - if (segment->run_part) { - ie.roll_chain_id = segment->run_part->roll_chain_id; - ie.previous_run_id = segment->run_part->previous_run_id; - ie.part_index = segment->run_part->part_index; - } - - // Multi-pass grouping (P1): the launcher's multi-pass driver tags each - // child with GPUFL_ANALYSIS_ID + its 0-based GPUFL_PASS_INDEX and the - // GPUFL_PASS_COUNT total so the backend can stitch the isolated passes - // into one analysis. Read straight from the env here (same pattern as the - // GPUFL_PROFILING_ENGINE override above). Absent → an ordinary single-pass - // run: analysis_id stays empty and the three fields are omitted from - // job_start (see InitEventModel), keeping single runs wire-identical. - if (const char* envAnalysis = std::getenv(gpufl::env::kAnalysisId); - envAnalysis && *envAnalysis) { - ie.analysis_id = envAnalysis; - if (const char* envIdx = std::getenv(gpufl::env::kPassIndex)) - ie.pass_index = std::atoi(envIdx); - if (const char* envCnt = std::getenv(gpufl::env::kPassCount)) - ie.pass_count = std::atoi(envCnt); - GFL_LOG_DEBUG("Multi-pass: analysis_id=", ie.analysis_id, - " pass ", ie.pass_index, "/", ie.pass_count); - } - - segment->logger->write(model::InitEventModel(ie)); - - if (segmented) { - SegmentRuntime::Options segment_options; - segment_options.runtime = rt_ptr; - segment_options.logger_options = logOpts; - segment_options.logger_options.on_serialized_bytes = {}; - segment_options.init_template = ie; - segment_options.segment_every_ms = rt_ptr->segment_every_ms; - segment_options.segment_max_rows = rt_ptr->segment_max_rows; - segment_options.run_roll_every_ms = rt_ptr->run_roll_every_ms; - segment_options.run_roll_max_bytes = rt_ptr->run_roll_max_bytes; - rt_ptr->segment_runtime = - std::make_shared(std::move(segment_options)); - if (!rt_ptr->segment_runtime->start()) { - GFL_LOG_ERROR("Failed to start SegmentRuntime"); - shutdown(); - return false; - } - } - - // Configure the sampler with collectors / interval. This does NOT - // start the worker - that happens via activate(), driven either by - // the continuous-mode baseline activation below or by GFL_SCOPE - // entry / systemStart() at runtime. - if (g_opts.system_sample_rate_ms > 0 && rt_ptr->collector) { - rt_ptr->sampler.configure( - rt_ptr->app_name, - [rt_ptr] { - return rt_ptr->acquireSegmentContext("sampler"); - }, - [rt_ptr] { - return rt_ptr->peekSegmentContext(); - }, - rt_ptr->collector, g_opts.system_sample_rate_ms, - rt_ptr->host_collector.get(), - [rt_ptr](const uint32_t index, const uint64_t rows, - const int64_t steady_ns, const int64_t event_ns) { - if (rt_ptr->segment_runtime) { - rt_ptr->segment_runtime->noteRows( - index, rows, steady_ns, event_ns); - } - }); - } - - // Continuous mode: emit the SystemStart event and take the baseline - // activation that keeps the sampler running until shutdown(). - if (g_opts.continuous_system_sampling && segment->logger) { - SystemStartEvent e; - e.pid = gpufl::detail::GetPid(); - e.app = rt_ptr->app_name; - e.name = "sampling_start"; - e.session_id = segment->session_id; - e.ts_ns = gpufl::detail::GetTimestampNs(); - if (rt_ptr->collector) e.devices = rt_ptr->collector->sampleAll(); - if (rt_ptr->host_collector) e.host = rt_ptr->host_collector->sample(); - segment->logger->write(model::SystemStartModel(e)); - } - if (g_opts.continuous_system_sampling && g_opts.system_sample_rate_ms > 0 && - rt_ptr->collector) { - rt_ptr->sampler.activate(); - } - - // Intentionally disabled - shutdown order must be explicit to avoid CUPTI - // teardown races std::atexit(shutdown); + detail::ClientStartup startup(g_opts); + if (!startup.start()) return false; #if GPUFL_HAS_NVTX // Enable NVTX push/pop now that CUPTI has wired up its injection. @@ -1084,14 +516,15 @@ ScopedMonitor::~ScopedMonitor() { void generateReport(const std::string& output_path) { namespace fs = std::filesystem; - fs::path p(g_lastLogPath); + const auto report_source = detail::lastSessionReportSource(); + fs::path p(report_source.log_path); if (p.extension() == ".log") { p.replace_extension(); } report::TextReport::Options opts; - const fs::path sessionDir = p / g_lastSessionId; - if (!g_lastSessionId.empty() && fs::exists(sessionDir)) { + const fs::path sessionDir = p / report_source.session_id; + if (!report_source.session_id.empty() && fs::exists(sessionDir)) { opts.log_dir = sessionDir.string(); opts.log_prefix.clear(); } else { diff --git a/include/gpufl/core/logger/logger.hpp b/include/gpufl/core/logger/logger.hpp index d2f6bd0..0595e1e 100644 --- a/include/gpufl/core/logger/logger.hpp +++ b/include/gpufl/core/logger/logger.hpp @@ -145,7 +145,7 @@ class Logger { /** * Replace FileLogSink byte accounting before this logger's first write. - * The callback must remina non-blocking, non-throwing, and must not + * The callback must remain non-blocking, non-throwing, and must not * re-enter Logger. */ void setSerializedBytesCallbackBeforeFirstWrite( diff --git a/include/gpufl/core/monitor_configuration.cpp b/include/gpufl/core/monitor_configuration.cpp new file mode 100644 index 0000000..223ab33 --- /dev/null +++ b/include/gpufl/core/monitor_configuration.cpp @@ -0,0 +1,135 @@ +#include "gpufl/core/monitor_configuration.hpp" + +#include +#include + +#include "gpufl/core/debug_logger.hpp" +#include "gpufl/core/env_vars.hpp" + +namespace gpufl::detail { +namespace { + +MonitorBackendKind toMonitorBackendKind(const BackendKind backend) { + switch (backend) { + case BackendKind::Nvidia: + return MonitorBackendKind::Nvidia; + case BackendKind::Amd: + return MonitorBackendKind::Amd; + case BackendKind::None: + return MonitorBackendKind::None; + case BackendKind::Auto: + default: + return MonitorBackendKind::Auto; + } +} + +bool applyProfilingEngineOverride(MonitorOptions& monitor_options) { + const char* raw = std::getenv(env::kProfilingEngine); + if (!raw) return true; + + const std::string value(raw); + bool matched = true; + if (value == "Monitor") { + monitor_options.profiling_engine = ProfilingEngine::Monitor; + } else if (value == "Trace") { + monitor_options.profiling_engine = ProfilingEngine::Trace; + } else if (value == "PcSampling") { + monitor_options.profiling_engine = ProfilingEngine::PcSampling; + } else if (value == "SassMetrics") { + monitor_options.profiling_engine = ProfilingEngine::SassMetrics; + } else if (value == "PmSampling") { + monitor_options.profiling_engine = ProfilingEngine::PmSampling; + } else if (value == "RangeProfiler") { + monitor_options.profiling_engine = ProfilingEngine::RangeProfiler; + } else if (value == "RangeProfilerKernelReplay") { + monitor_options.profiling_engine = + ProfilingEngine::RangeProfilerKernelReplay; + } else if (value == "Deep") { + monitor_options.profiling_engine = ProfilingEngine::Deep; + } else { + matched = false; + } + + if (matched) { + GFL_LOG_DEBUG("GPUFL_PROFILING_ENGINE override: ", value); + } else { + // Preserve the existing user-visible text in this behavior-preserving + // refactor. The omission of RangeProfilerKernelReplay from this list + // is a separate copy-only defect, not part of this module move. + GFL_LOG_ERROR( + "GPUFL_PROFILING_ENGINE='", value, "' is not a recognized " + "engine name. Valid values: Monitor, Trace, PcSampling, " + "SassMetrics, PmSampling, RangeProfiler, Deep. Keeping current engine " + "selection."); + } + return matched; +} + +void applyPcSamplingPeriodOverride(MonitorOptions& monitor_options) { + const char* raw = std::getenv(env::kPcSamplingPeriod); + if (!raw) return; + + char* end = nullptr; + const unsigned long value = std::strtoul(raw, &end, 10); + if (end != raw && *end == '\0' && value >= 5 && value <= 31) { + monitor_options.pc_sampling_period = static_cast(value); + GFL_LOG_DEBUG("GPUFL_PC_SAMPLING_PERIOD override: 2^", value, " = ", + (1ul << value), " cycles/sample"); + return; + } + GFL_LOG_ERROR("GPUFL_PC_SAMPLING_PERIOD='", raw, "' is invalid " + "(expected an integer 5..31). Keeping ", + monitor_options.pc_sampling_period, "."); +} + +void applyDeepArmOverride(MonitorOptions& monitor_options) { + const char* raw = std::getenv(env::kDeepArm); + if (!raw) return; + + const std::string value(raw); + if (value == "window") { + monitor_options.deep_arm_mode = DeepArmMode::WindowOnly; + } else if (value == "always") { + monitor_options.deep_arm_mode = DeepArmMode::Always; + } else { + GFL_LOG_ERROR("GPUFL_DEEP_ARM='", value, + "' is not recognized. Valid values: always, window. " + "Keeping current deep arm mode."); + } +} + +} // namespace + +MonitorOptions buildMonitorOptions(const InitOptions& options) { + MonitorOptions monitor_options; + monitor_options.enable_debug_output = options.enable_debug_output; + monitor_options.profiling_engine = options.profiling_engine; + applyProfilingEngineOverride(monitor_options); + applyPcSamplingPeriodOverride(monitor_options); + + monitor_options.kernel_sample_rate_ms = options.kernel_sample_rate_ms; + monitor_options.enable_stack_trace = options.enable_stack_trace; + monitor_options.enable_source_collection = options.enable_source_collection; + monitor_options.enable_external_correlation = + options.enable_external_correlation; + monitor_options.enable_synchronization = options.enable_synchronization; + monitor_options.enable_memory_tracking = options.enable_memory_tracking; + monitor_options.enable_cuda_graphs_tracking = + options.enable_cuda_graphs_tracking; + monitor_options.pm_sampling_interval_us = options.pm_sampling_interval_us; + monitor_options.pm_sampling_max_samples = options.pm_sampling_max_samples; + monitor_options.pm_sampling_preset = options.pm_sampling_preset; + monitor_options.pm_sampling_metrics = options.pm_sampling_metrics; + monitor_options.pm_sampling_scope_only = options.pm_sampling_scope_only; + monitor_options.deep_arm_mode = options.deep_window_only + ? DeepArmMode::WindowOnly + : DeepArmMode::Always; + applyDeepArmOverride(monitor_options); + if (monitor_options.deep_arm_mode == DeepArmMode::WindowOnly) { + monitor_options.pm_sampling_scope_only = true; + } + monitor_options.backend_kind = toMonitorBackendKind(options.backend); + return monitor_options; +} + +} // namespace gpufl::detail diff --git a/include/gpufl/core/monitor_configuration.hpp b/include/gpufl/core/monitor_configuration.hpp new file mode 100644 index 0000000..b2a3340 --- /dev/null +++ b/include/gpufl/core/monitor_configuration.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "gpufl/core/monitor.hpp" +#include "gpufl/gpufl.hpp" + +namespace gpufl::detail { + +// Builds the deterministic MonitorOptions contract from InitOptions and the +// documented environment overrides. CUDA/process side effects deliberately do +// not belong here; eager module loading and device-specific tuning remain at +// the initialization boundary. +MonitorOptions buildMonitorOptions(const InitOptions& options); + +} // namespace gpufl::detail diff --git a/include/gpufl/core/segment_context.hpp b/include/gpufl/core/segment_context.hpp index 731fb2b..cf315f5 100644 --- a/include/gpufl/core/segment_context.hpp +++ b/include/gpufl/core/segment_context.hpp @@ -18,7 +18,7 @@ class SegmentRuntime; struct Runtime; /** - * Immutable identity shared by every SegmentContext of one run part. + * Identity shared by every SegmentContext of one run part. * * A run is a chain of parts joined by roll_chain_id. Each part owns a distinct * run_id and its own segment numbering; a rollover mints a new RunPartContext @@ -27,6 +27,9 @@ struct Runtime; * * job_start and run_end read run identity from here rather than from a mutable * process global, so a part still carries the correct identity after a roll. + * + * Identity is const; the byte counter is not. It lives here because the budget + * is scoped to this object, so a roll resets it by construction. */ struct RunPartContext { RunPartContext(std::string roll_chain_id_value, std::string run_id_value, @@ -48,6 +51,8 @@ struct RunPartContext { const int64_t run_started_mono_ns; const uint32_t first_segment_index; + /** Lock-free (logger write path). Saturates: a wrap would read as a tiny + * budget and roll continuously. */ void addSerializedBytes(const uint64_t bytes) const noexcept { auto current = serialized_bytes_.load(std::memory_order_relaxed); for (;;) { @@ -67,9 +72,8 @@ struct RunPartContext { return serialized_bytes_.load(std::memory_order_relaxed); } -private: + private: mutable std::atomic serialized_bytes_{0}; - }; /** diff --git a/include/gpufl/core/segment_runtime.hpp b/include/gpufl/core/segment_runtime.hpp index 2f80d4b..97cc132 100644 --- a/include/gpufl/core/segment_runtime.hpp +++ b/include/gpufl/core/segment_runtime.hpp @@ -87,6 +87,7 @@ class SegmentRuntime { std::deque retirement_queue_; bool retirement_stopping_ = false; std::thread retirement_thread_; + // How much of the current part's byte counter is already accounted. std::mutex serialized_bytes_mu_; std::shared_ptr observed_run_part_; uint64_t observed_run_part_bytes_ = 0; diff --git a/include/gpufl/core/session_bootstrap.cpp b/include/gpufl/core/session_bootstrap.cpp new file mode 100644 index 0000000..0d652ad --- /dev/null +++ b/include/gpufl/core/session_bootstrap.cpp @@ -0,0 +1,125 @@ +#include "gpufl/core/session_bootstrap.hpp" + +#include +#include +#include +#include +#include +#include + +#include "gpufl/gpufl.hpp" +#include "gpufl/core/common.hpp" +#include "gpufl/core/debug_logger.hpp" +#include "gpufl/core/dictionary_manager.hpp" +#include "gpufl/core/env_vars.hpp" +#include "gpufl/core/runtime.hpp" + +namespace gpufl::detail { +namespace { + +std::string g_last_log_path; +std::string g_last_session_id; + +std::string defaultLogPath(const std::string& app) { + // log_path is a directory: sessions nest beneath it as + // `//.log`. Returning just the app keeps + // this default aligned with the on-disk directory layout. + return app; +} + +void applyLoggingEnvironment(const InitOptions& options, + Logger::Options& logger_options) { + logger_options.system_sample_rate_ms = options.system_sample_rate_ms; + logger_options.flush_always = options.flush_logs_always; + if (const char* value = std::getenv(env::kFlushLogsAlways)) { + std::string flag(value); + std::transform(flag.begin(), flag.end(), flag.begin(), + [](const unsigned char c) { + return static_cast(std::tolower(c)); + }); + if (flag == "1" || flag == "true" || flag == "yes" || flag == "on") { + logger_options.flush_always = true; + } + } + if (const char* value = std::getenv(env::kLogRotateBytes)) { + if (const auto bytes = std::strtoull(value, nullptr, 10); bytes > 0) { + logger_options.rotate_bytes = static_cast(bytes); + } + } + if (const char* value = std::getenv(env::kLogRotateAfterMs)) { + if (const auto ms = std::strtoll(value, nullptr, 10); ms > 0) { + logger_options.rotate_after_ms = static_cast(ms); + } + } + if (const char* value = std::getenv(env::kLogMaxSpoolBytes)) { + logger_options.max_spool_bytes = std::strtoull(value, nullptr, 10); + } + if (const char* value = std::getenv(env::kLogMinFreeBytes)) { + logger_options.min_free_bytes = std::strtoull(value, nullptr, 10); + } +} + +std::shared_ptr makeInitialRunPart( + const Runtime& runtime, const bool segmented) { + if (!segmented || + (runtime.run_roll_every_ms <= 0 && runtime.run_roll_max_bytes == 0)) { + return nullptr; + } + const auto opened_mono_ns = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + return std::make_shared( + runtime.run_id, runtime.run_id, std::string(), 1u, opened_mono_ns, 0u); +} + +} // namespace + +bool openInitialSessionLogging(Runtime& runtime, const InitOptions& options, + const bool segmented, + InitialSessionLoggingState& state) { + state.log_path = options.log_path.empty() + ? defaultLogPath(runtime.app_name) + : options.log_path; + state.options.base_path = state.log_path; + // The uploader discovers sessions from this directory layout instead of + // parsing a job_start event out of a legacy flat log file. + state.options.session_id = runtime.session_id; + applyLoggingEnvironment(options, state.options); + + // Preserve the existing post-shutdown reporting state even if opening the + // logger itself fails. + g_last_log_path = state.log_path; + g_last_session_id = runtime.session_id; + + const auto initial_run_part = makeInitialRunPart(runtime, segmented); + if (initial_run_part && runtime.run_roll_max_bytes > 0) { + state.options.on_serialized_bytes = + [part = initial_run_part](const uint64_t bytes) noexcept { + part->addSerializedBytes(bytes); + }; + } + + GFL_LOG_DEBUG("Opening log file: ", state.log_path); + if (!runtime.logger->open(state.options)) { + GFL_LOG_ERROR("Failed to open logger at: ", state.log_path); + return false; + } + + auto dictionary = segmented + ? std::make_shared() + : nullptr; + if (!runtime.publishSegmentContext(std::make_shared( + runtime.run_id, runtime.session_id, runtime.segment_index, + GetTimestampNs(), runtime.logger, std::move(dictionary), + initial_run_part))) { + GFL_LOG_ERROR("Failed to publish the initial segment context"); + runtime.logger->close(); + return false; + } + return true; +} + +LastSessionReportSource lastSessionReportSource() { + return {g_last_log_path, g_last_session_id}; +} + +} // namespace gpufl::detail diff --git a/include/gpufl/core/session_bootstrap.hpp b/include/gpufl/core/session_bootstrap.hpp new file mode 100644 index 0000000..06f7421 --- /dev/null +++ b/include/gpufl/core/session_bootstrap.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include "gpufl/core/logger/logger.hpp" + +namespace gpufl { +struct InitOptions; +struct Runtime; + +namespace detail { + +// The values init() still needs after the initial logger/context transaction: +// job_start reports log_path and SegmentRuntime copies the resolved logger +// policy for later segments. +struct InitialSessionLoggingState { + std::string log_path; + Logger::Options options; +}; + +// Reporting happens after shutdown(), when Runtime is gone. Keep only the +// location needed to find the final session directory; this deliberately does +// not retain any live runtime object. +struct LastSessionReportSource { + std::string log_path; + std::string session_id; +}; + +// Open the first session logger and publish its immutable write context. +// Returns false with no published context when the logger cannot be opened. +bool openInitialSessionLogging(Runtime& runtime, const InitOptions& options, + bool segmented, + InitialSessionLoggingState& state); + +// Snapshot the source remembered by the most recent bootstrap attempt. +LastSessionReportSource lastSessionReportSource(); + +} // namespace detail +} // namespace gpufl diff --git a/include/gpufl/core/startup_configuration.cpp b/include/gpufl/core/startup_configuration.cpp new file mode 100644 index 0000000..e30a4c7 --- /dev/null +++ b/include/gpufl/core/startup_configuration.cpp @@ -0,0 +1,124 @@ +#include "gpufl/core/startup_configuration.hpp" + +#include +#include +#include +#include +#include + +#include "gpufl/core/config_file_loader.hpp" +#include "gpufl/core/env_vars.hpp" +#include "gpufl/core/segmentation_config.hpp" +#include "gpufl/core/version.hpp" + +namespace gpufl::detail { +namespace { + +bool parseNonNegativeEnv(const char* key, uint64_t& value, + std::string& error) { + value = 0; + const char* raw = std::getenv(key); + if (!raw || !*raw) return true; + if (*raw == '-') { + error = std::string(key) + " must be a non-negative integer"; + return false; + } + errno = 0; + char* end = nullptr; + const unsigned long long parsed = std::strtoull(raw, &end, 10); + if (end == raw || *end != '\0' || errno == ERANGE) { + error = std::string(key) + "='" + raw + + "' is invalid (expected a non-negative integer)"; + return false; + } + value = static_cast(parsed); + return true; +} + +bool isUuidV4(const char* value) { + if (!value || std::strlen(value) != 36) return false; + for (size_t i = 0; i < 36; ++i) { + if (i == 8 || i == 13 || i == 18 || i == 23) { + if (value[i] != '-') return false; + } else if (!std::isxdigit(static_cast(value[i]))) { + return false; + } + } + if (value[14] != '4') return false; + const char variant = + static_cast(std::tolower(static_cast(value[19]))); + return variant == '8' || variant == '9' || variant == 'a' || + variant == 'b'; +} + +} // namespace + +void resolveStartupOptions(InitOptions& options) { + std::string config_path = options.config_file; + if (config_path.empty()) { + if (const char* value = std::getenv(env::kConfigFile)) { + config_path = value; + } + } + if (!config_path.empty()) { + ConfigFileLoader::apply(options, config_path); + } + + std::string api_path = options.api_path; + if (api_path.empty()) { + if (const char* value = std::getenv(env::kApiPath)) { + api_path = value; + } + } + options.api_path = normalizeApiPath(api_path); +} + +bool readStartupSegmentationOptions(StartupSegmentationOptions& options, + std::string& error) { + if (!parseNonNegativeEnv(env::kSegmentEveryMs, options.segment_every_ms, + error) || + !parseNonNegativeEnv(env::kSegmentMaxRows, options.segment_max_rows, + error) || + !parseNonNegativeEnv(env::kRunRollEveryMs, + options.run_roll_every_ms, error) || + !parseNonNegativeEnv(env::kRunRollMaxBytes, + options.run_roll_max_bytes, error)) { + return false; + } + + constexpr uint64_t kMaxSignedMilliseconds = + static_cast((std::numeric_limits::max)()); + if (options.segment_every_ms > kMaxSignedMilliseconds) { + error = std::string(env::kSegmentEveryMs) + + " exceeds the supported signed 64-bit millisecond range"; + return false; + } + if (options.run_roll_every_ms > kMaxSignedMilliseconds) { + error = std::string(env::kRunRollEveryMs) + + " exceeds the supported signed 64-bit millisecond range"; + return false; + } + + if (!options.enabled()) return true; + + const char* run_id = std::getenv(env::kRunId); + if (!run_id || !*run_id) { + error = "Session segmentation requires GPUFL_RUN_ID. The launcher " + "must generate one run ID before starting the target."; + return false; + } + if (!isUuidV4(run_id)) { + error = std::string(env::kRunId) + "='" + run_id + + "' is invalid (expected a UUIDv4)"; + return false; + } + if (!segmentation::kRuntimeReady) { + error = "This build does not include executable session segmentation."; + return false; + } + + options.run_id = run_id; + return true; +} + +} // namespace gpufl::detail diff --git a/include/gpufl/core/startup_configuration.hpp b/include/gpufl/core/startup_configuration.hpp new file mode 100644 index 0000000..953ee08 --- /dev/null +++ b/include/gpufl/core/startup_configuration.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "gpufl/gpufl.hpp" + +namespace gpufl::detail { + +// Values accepted from the launcher/runtime environment before any Runtime is +// allocated. The launcher decides which values to export; this type records +// the validated contract the embedded runtime consumes. +struct StartupSegmentationOptions { + uint64_t segment_every_ms = 0; + uint64_t segment_max_rows = 0; + uint64_t run_roll_every_ms = 0; + uint64_t run_roll_max_bytes = 0; + std::string run_id; + + bool enabled() const { + return segment_every_ms > 0 || segment_max_rows > 0; + } +}; + +// Applies the configuration-file fallback and resolves a canonical API path. +// Call before any component observes InitOptions. +void resolveStartupOptions(InitOptions& options); + +// Reads and validates segmentation/rollover environment state without +// allocating a Runtime. On failure, `error` contains the user-facing reason. +bool readStartupSegmentationOptions(StartupSegmentationOptions& options, + std::string& error); + +} // namespace gpufl::detail diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index de0f8d6..6b43cc8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,10 +26,13 @@ set(GPUFL_TEST_SOURCES core/test_deep_window_rule.cpp core/test_deep_window_rules_install.cpp core/test_monitor.cpp + core/test_monitor_configuration.cpp core/test_itanium_demangle.cpp core/test_sampler.cpp core/test_segment_context.cpp core/test_segment_coordinator.cpp + core/test_session_bootstrap.cpp + core/test_startup_configuration.cpp upload/test_upload_logs.cpp # Launcher CLI parser test - portable (no CUDA / no POSIX). # The portable CLI parser sources are compiled directly into the test @@ -38,6 +41,7 @@ set(GPUFL_TEST_SOURCES launcher/test_info_command.cpp launcher/test_deep_window_env.cpp launcher/test_segmentation_env.cpp + launcher/test_trace_run_plan.cpp launcher/test_agent_launcher.cpp ${CMAKE_SOURCE_DIR}/daemon/launcher/cli_parse.cpp ${CMAKE_SOURCE_DIR}/daemon/launcher/cli_help.cpp @@ -45,8 +49,10 @@ set(GPUFL_TEST_SOURCES ${CMAKE_SOURCE_DIR}/daemon/launcher/cli_subcommand_options.cpp ${CMAKE_SOURCE_DIR}/daemon/launcher/cli_trace_options.cpp ${CMAKE_SOURCE_DIR}/daemon/launcher/info_command.cpp + ${CMAKE_SOURCE_DIR}/daemon/launcher/trace_command_common.cpp ${CMAKE_SOURCE_DIR}/daemon/launcher/deep_window_env.cpp ${CMAKE_SOURCE_DIR}/daemon/launcher/segmentation_env.cpp + ${CMAKE_SOURCE_DIR}/daemon/launcher/trace_run_plan.cpp ${CMAKE_SOURCE_DIR}/daemon/launcher/agent_launcher.cpp ) diff --git a/tests/core/test_monitor_configuration.cpp b/tests/core/test_monitor_configuration.cpp new file mode 100644 index 0000000..f48cbac --- /dev/null +++ b/tests/core/test_monitor_configuration.cpp @@ -0,0 +1,147 @@ +#include + +#include +#include +#include +#include + +#include "gpufl/core/env_vars.hpp" +#include "gpufl/core/monitor_configuration.hpp" + +namespace { + +void setEnv(const char* name, const char* value) { +#if defined(_WIN32) + _putenv_s(name, value); +#else + ::setenv(name, value, /*overwrite=*/1); +#endif +} + +void unsetEnv(const char* name) { +#if defined(_WIN32) + _putenv_s(name, ""); +#else + ::unsetenv(name); +#endif +} + +class MonitorConfigurationTest : public testing::Test { +protected: + void SetUp() override { + saveAndUnset_(gpufl::env::kProfilingEngine, profiling_engine_); + saveAndUnset_(gpufl::env::kPcSamplingPeriod, pc_sampling_period_); + saveAndUnset_(gpufl::env::kDeepArm, deep_arm_); + } + + void TearDown() override { + restore_(gpufl::env::kProfilingEngine, profiling_engine_); + restore_(gpufl::env::kPcSamplingPeriod, pc_sampling_period_); + restore_(gpufl::env::kDeepArm, deep_arm_); + } + +private: + static void saveAndUnset_(const char* name, std::optional& out) { + if (const char* value = std::getenv(name)) out = value; + unsetEnv(name); + } + + static void restore_(const char* name, + const std::optional& value) { + if (value) setEnv(name, value->c_str()); + else unsetEnv(name); + } + + std::optional profiling_engine_; + std::optional pc_sampling_period_; + std::optional deep_arm_; +}; + +TEST_F(MonitorConfigurationTest, CopiesEveryInitOptionWithNoEnvironmentOverride) { + gpufl::InitOptions options; + options.enable_debug_output = true; + options.enable_stack_trace = true; + options.enable_source_collection = false; + options.enable_external_correlation = false; + options.enable_synchronization = false; + options.enable_memory_tracking = false; + options.enable_cuda_graphs_tracking = true; + options.kernel_sample_rate_ms = 37; + options.pm_sampling_interval_us = 777; + options.pm_sampling_max_samples = 1234; + options.pm_sampling_preset = "compute"; + options.pm_sampling_metrics = {"sm__cycles_elapsed", "dram__bytes"}; + options.pm_sampling_scope_only = false; + options.profiling_engine = gpufl::ProfilingEngine::PmSampling; + options.backend = gpufl::BackendKind::Amd; + + const auto actual = gpufl::detail::buildMonitorOptions(options); + + EXPECT_TRUE(actual.enable_debug_output); + EXPECT_TRUE(actual.enable_stack_trace); + EXPECT_FALSE(actual.enable_source_collection); + EXPECT_FALSE(actual.enable_external_correlation); + EXPECT_FALSE(actual.enable_synchronization); + EXPECT_FALSE(actual.enable_memory_tracking); + EXPECT_TRUE(actual.enable_cuda_graphs_tracking); + EXPECT_EQ(actual.kernel_sample_rate_ms, 37); + EXPECT_EQ(actual.pm_sampling_interval_us, 777u); + EXPECT_EQ(actual.pm_sampling_max_samples, 1234u); + EXPECT_EQ(actual.pm_sampling_preset, "compute"); + EXPECT_EQ(actual.pm_sampling_metrics, + (std::vector{"sm__cycles_elapsed", "dram__bytes"})); + EXPECT_FALSE(actual.pm_sampling_scope_only); + EXPECT_EQ(actual.profiling_engine, gpufl::ProfilingEngine::PmSampling); + EXPECT_EQ(actual.backend_kind, gpufl::MonitorBackendKind::Amd); +} + +TEST_F(MonitorConfigurationTest, EnvironmentOverridesTakePrecedence) { + setEnv(gpufl::env::kProfilingEngine, "RangeProfilerKernelReplay"); + setEnv(gpufl::env::kPcSamplingPeriod, "17"); + setEnv(gpufl::env::kDeepArm, "window"); + + gpufl::InitOptions options; + options.profiling_engine = gpufl::ProfilingEngine::Trace; + options.deep_window_only = false; + options.pm_sampling_scope_only = false; + + const auto actual = gpufl::detail::buildMonitorOptions(options); + + EXPECT_EQ(actual.profiling_engine, + gpufl::ProfilingEngine::RangeProfilerKernelReplay); + EXPECT_EQ(actual.pc_sampling_period, 17u); + EXPECT_EQ(actual.deep_arm_mode, gpufl::DeepArmMode::WindowOnly); + EXPECT_TRUE(actual.pm_sampling_scope_only); +} + +TEST_F(MonitorConfigurationTest, InvalidOverridesPreserveConfiguredValues) { + setEnv(gpufl::env::kProfilingEngine, "not-an-engine"); + setEnv(gpufl::env::kPcSamplingPeriod, "32"); + setEnv(gpufl::env::kDeepArm, "sometimes"); + + gpufl::InitOptions options; + options.profiling_engine = gpufl::ProfilingEngine::SassMetrics; + options.deep_window_only = true; + + const auto actual = gpufl::detail::buildMonitorOptions(options); + + EXPECT_EQ(actual.profiling_engine, gpufl::ProfilingEngine::SassMetrics); + EXPECT_EQ(actual.pc_sampling_period, 10u); + EXPECT_EQ(actual.deep_arm_mode, gpufl::DeepArmMode::WindowOnly); + EXPECT_TRUE(actual.pm_sampling_scope_only); +} + +TEST_F(MonitorConfigurationTest, AlwaysOverrideCanDisableWindowOnlyArming) { + setEnv(gpufl::env::kDeepArm, "always"); + + gpufl::InitOptions options; + options.deep_window_only = true; + options.pm_sampling_scope_only = false; + + const auto actual = gpufl::detail::buildMonitorOptions(options); + + EXPECT_EQ(actual.deep_arm_mode, gpufl::DeepArmMode::Always); + EXPECT_FALSE(actual.pm_sampling_scope_only); +} + +} // namespace diff --git a/tests/core/test_session_bootstrap.cpp b/tests/core/test_session_bootstrap.cpp new file mode 100644 index 0000000..c22f89d --- /dev/null +++ b/tests/core/test_session_bootstrap.cpp @@ -0,0 +1,55 @@ +#include + +#include +#include +#include + +#include "gpufl/gpufl.hpp" +#include "gpufl/core/common.hpp" +#include "gpufl/core/logger/logger.hpp" +#include "gpufl/core/runtime.hpp" +#include "gpufl/core/session_bootstrap.hpp" + +namespace { + +TEST(SessionBootstrapTest, OpensInitialContextAndRemembersReportSource) { + namespace fs = std::filesystem; + const fs::path root = fs::temp_directory_path() / + ("gpufl_session_bootstrap_" + + std::to_string(gpufl::detail::GetPid())); + std::error_code ec; + fs::remove_all(root, ec); + + gpufl::Runtime runtime; + runtime.app_name = "bootstrap-test"; + runtime.session_id = "session-bootstrap"; + runtime.run_id = "run-bootstrap"; + runtime.logger = std::make_shared(); + + gpufl::InitOptions options; + options.log_path = root.string(); + gpufl::detail::InitialSessionLoggingState state; + ASSERT_TRUE(gpufl::detail::openInitialSessionLogging( + runtime, options, /*segmented=*/false, state)); + + EXPECT_EQ(state.log_path, root.string()); + EXPECT_EQ(state.options.base_path, root.string()); + EXPECT_EQ(state.options.session_id, "session-bootstrap"); + ASSERT_TRUE(runtime.hasSegmentContext()); + { + const auto context = runtime.acquireSegmentContext("bootstrap-test"); + ASSERT_TRUE(context); + EXPECT_EQ(context->session_id, "session-bootstrap"); + EXPECT_EQ(context->segment_index, 0u); + } + + const auto report_source = gpufl::detail::lastSessionReportSource(); + EXPECT_EQ(report_source.log_path, root.string()); + EXPECT_EQ(report_source.session_id, "session-bootstrap"); + + runtime.sealActiveSegmentContext(); + runtime.logger->close(); + fs::remove_all(root, ec); +} + +} // namespace diff --git a/tests/core/test_startup_configuration.cpp b/tests/core/test_startup_configuration.cpp new file mode 100644 index 0000000..97bc911 --- /dev/null +++ b/tests/core/test_startup_configuration.cpp @@ -0,0 +1,147 @@ +#include + +#include +#include +#include +#include +#include + +#include "gpufl/core/common.hpp" +#include "gpufl/core/env_vars.hpp" +#include "gpufl/core/startup_configuration.hpp" + +namespace { + +void setEnv(const char* name, const char* value) { +#if defined(_WIN32) + _putenv_s(name, value); +#else + ::setenv(name, value, /*overwrite=*/1); +#endif +} + +void unsetEnv(const char* name) { +#if defined(_WIN32) + _putenv_s(name, ""); +#else + ::unsetenv(name); +#endif +} + +class StartupConfigurationTest : public testing::Test { +protected: + void SetUp() override { + saveAndUnset_(gpufl::env::kConfigFile, config_file_); + saveAndUnset_(gpufl::env::kApiPath, api_path_); + saveAndUnset_(gpufl::env::kRunId, run_id_); + saveAndUnset_(gpufl::env::kSegmentEveryMs, segment_every_ms_); + saveAndUnset_(gpufl::env::kSegmentMaxRows, segment_max_rows_); + saveAndUnset_(gpufl::env::kRunRollEveryMs, run_roll_every_ms_); + saveAndUnset_(gpufl::env::kRunRollMaxBytes, run_roll_max_bytes_); + } + + void TearDown() override { + restore_(gpufl::env::kConfigFile, config_file_); + restore_(gpufl::env::kApiPath, api_path_); + restore_(gpufl::env::kRunId, run_id_); + restore_(gpufl::env::kSegmentEveryMs, segment_every_ms_); + restore_(gpufl::env::kSegmentMaxRows, segment_max_rows_); + restore_(gpufl::env::kRunRollEveryMs, run_roll_every_ms_); + restore_(gpufl::env::kRunRollMaxBytes, run_roll_max_bytes_); + } + +private: + static void saveAndUnset_(const char* name, std::optional& out) { + if (const char* value = std::getenv(name)) out = value; + unsetEnv(name); + } + + static void restore_(const char* name, + const std::optional& value) { + if (value) setEnv(name, value->c_str()); + else unsetEnv(name); + } + + std::optional config_file_; + std::optional api_path_; + std::optional run_id_; + std::optional segment_every_ms_; + std::optional segment_max_rows_; + std::optional run_roll_every_ms_; + std::optional run_roll_max_bytes_; +}; + +TEST_F(StartupConfigurationTest, ConfigFileApiPathIsNormalized) { + const auto temp_root = + std::filesystem::temp_directory_path() / + ("gpufl_startup_config_" + std::to_string(gpufl::detail::GetPid())); + const auto config_path = temp_root / "config.json"; + std::error_code ec; + std::filesystem::remove_all(temp_root, ec); + std::filesystem::create_directories(temp_root, ec); + ASSERT_FALSE(ec) << ec.message(); + { + std::ofstream config(config_path); + ASSERT_TRUE(config); + config << R"({"api_path":"configured/v1/"})"; + } + + gpufl::InitOptions options; + options.config_file = config_path.string(); + gpufl::detail::resolveStartupOptions(options); + EXPECT_EQ(options.api_path, "/configured/v1"); + + std::filesystem::remove_all(temp_root, ec); +} + +TEST_F(StartupConfigurationTest, ApiPathEnvironmentFallbackIsNormalized) { + setEnv(gpufl::env::kApiPath, "environment/v1/"); + + gpufl::InitOptions options; + gpufl::detail::resolveStartupOptions(options); + + EXPECT_EQ(options.api_path, "/environment/v1"); +} + +TEST_F(StartupConfigurationTest, ValidSegmentationAndRolloverAreReadTogether) { + setEnv(gpufl::env::kRunId, "12345678-1234-4123-8123-123456789abc"); + setEnv(gpufl::env::kSegmentEveryMs, "60000"); + setEnv(gpufl::env::kSegmentMaxRows, "2000000"); + setEnv(gpufl::env::kRunRollEveryMs, "180000"); + setEnv(gpufl::env::kRunRollMaxBytes, "4294967296"); + + gpufl::detail::StartupSegmentationOptions options; + std::string error; + ASSERT_TRUE(gpufl::detail::readStartupSegmentationOptions(options, error)) + << error; + EXPECT_TRUE(options.enabled()); + EXPECT_EQ(options.run_id, "12345678-1234-4123-8123-123456789abc"); + EXPECT_EQ(options.segment_every_ms, 60000u); + EXPECT_EQ(options.segment_max_rows, 2000000u); + EXPECT_EQ(options.run_roll_every_ms, 180000u); + EXPECT_EQ(options.run_roll_max_bytes, 4294967296u); +} + +TEST_F(StartupConfigurationTest, SegmentationWithoutRunIdFailsBeforeRuntimeSetup) { + setEnv(gpufl::env::kSegmentEveryMs, "60000"); + + gpufl::detail::StartupSegmentationOptions options; + std::string error; + EXPECT_FALSE(gpufl::detail::readStartupSegmentationOptions(options, error)); + EXPECT_EQ(error, + "Session segmentation requires GPUFL_RUN_ID. The launcher " + "must generate one run ID before starting the target."); +} + +TEST_F(StartupConfigurationTest, InvalidRolloverValueIsRejected) { + setEnv(gpufl::env::kRunRollMaxBytes, "not-a-number"); + + gpufl::detail::StartupSegmentationOptions options; + std::string error; + EXPECT_FALSE(gpufl::detail::readStartupSegmentationOptions(options, error)); + EXPECT_EQ(error, + "GPUFL_RUN_ROLL_MAX_BYTES='not-a-number' is invalid " + "(expected a non-negative integer)"); +} + +} // namespace diff --git a/tests/launcher/test_trace_run_plan.cpp b/tests/launcher/test_trace_run_plan.cpp new file mode 100644 index 0000000..6d8c34a --- /dev/null +++ b/tests/launcher/test_trace_run_plan.cpp @@ -0,0 +1,184 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "gpufl/core/common.hpp" +#include "gpufl/core/env_vars.hpp" +#include "trace_command_common.hpp" +#include "trace_run_plan.hpp" + +namespace { + +class PlanningPlatform : public gpufl::launcher::TracePlatform { +public: + const char* platformName() const override { return "planning"; } + const char* injectLibraryName() const override { return "inject"; } + gpufl::launcher::fs::path selfExe() const override { return {}; } + std::vector injectLibCandidates( + const gpufl::launcher::fs::path&) const override { return {}; } + gpufl::launcher::fs::path defaultOutputDir( + const std::string& tag) const override { + return gpufl::launcher::fs::path("captures") / tag; + } + std::string defaultAppName(const std::string&) const override { + return "inferred-app"; + } + bool setEnv(const char*, const std::string&, std::string&) const override { + return true; + } + bool unsetEnv(const char*, std::string&) const override { return true; } + bool prepareInjectionEnv(const gpufl::launcher::fs::path&, + std::string&) const override { + return true; + } + gpufl::launcher::TraceProcessResult runProcess( + const std::vector&, + const gpufl::launcher::RunOptions&) const override { + return {}; + } +}; + +using gpufl::launcher::TraceArgs; +using gpufl::launcher::createTraceRunPlan; + +TEST(TraceRunPlanTest, SinglePassUsesOneGeneratedDirectoryTag) { + PlanningPlatform platform; + TraceArgs args; + args.command = {"target"}; + + const auto plan = createTraceRunPlan(args, platform); + + ASSERT_EQ(plan.passes, std::vector({"Trace"})); + EXPECT_FALSE(plan.multipass); + EXPECT_FALSE(plan.segmented); + EXPECT_TRUE(plan.analysis_id.empty()); + EXPECT_TRUE(plan.run_id.empty()); + EXPECT_EQ(plan.app_name, "inferred-app"); + EXPECT_EQ(plan.output_dir, gpufl::launcher::fs::path("captures") / + plan.directory_tag); + EXPECT_EQ(plan.run_options.run_ms, 0); +} + +TEST(TraceRunPlanTest, MultipassNestsTheExplicitOutputUnderTheAnalysisFolder) { + PlanningPlatform platform; + TraceArgs args; + args.command = {"target"}; + args.name = "candidate"; + args.output_dir = "requested-output"; + args.passes = {"Trace", "PcSampling"}; + + const auto plan = createTraceRunPlan(args, platform); + + ASSERT_EQ(plan.passes.size(), 2u); + EXPECT_TRUE(plan.multipass); + EXPECT_FALSE(plan.segmented); + EXPECT_FALSE(plan.analysis_id.empty()); + EXPECT_EQ(plan.directory_tag, plan.analysis_id); + EXPECT_EQ(plan.output_dir.parent_path(), + gpufl::launcher::fs::path("requested-output")); + EXPECT_EQ(plan.output_dir.filename().string(), + "run-candidate-" + plan.analysis_id.substr(0, 8)); +} + +TEST(TraceRunPlanTest, SegmentedRunUsesItsRunIdAndAppliesTheTightestWindowCap) { + PlanningPlatform platform; + TraceArgs args; + args.command = {"target"}; + args.segment_every_ms = 60'000; + args.warmup_ms = 1'500; + args.window_ms = 1'000; + args.window_timeout_ms = 2'200; + + const auto plan = createTraceRunPlan(args, platform); + + EXPECT_FALSE(plan.multipass); + EXPECT_TRUE(plan.segmented); + EXPECT_TRUE(plan.analysis_id.empty()); + EXPECT_FALSE(plan.run_id.empty()); + EXPECT_EQ(plan.directory_tag, plan.run_id); + EXPECT_EQ(plan.output_dir, gpufl::launcher::fs::path("captures") / + plan.run_id); + EXPECT_EQ(plan.run_options.run_ms, 2'200); +} + +class ExecutingPlanningPlatform final : public PlanningPlatform { +public: + explicit ExecutingPlanningPlatform(gpufl::launcher::fs::path root) + : root_(std::move(root)) {} + + gpufl::launcher::fs::path selfExe() const override { + return root_ / "gpufl"; + } + std::vector injectLibCandidates( + const gpufl::launcher::fs::path&) const override { + return {root_ / "gpufl_inject.so"}; + } + gpufl::launcher::fs::path defaultOutputDir( + const std::string& tag) const override { + return root_ / "captures" / tag; + } + bool setEnv(const char* key, const std::string& value, + std::string&) const override { + env[key] = value; + return true; + } + bool unsetEnv(const char* key, std::string&) const override { + env.erase(key); + return true; + } + bool prepareInjectionEnv(const gpufl::launcher::fs::path&, + std::string&) const override { + return true; + } + gpufl::launcher::TraceProcessResult runProcess( + const std::vector& command, + const gpufl::launcher::RunOptions& options) const override { + seen_command = command; + seen_options = options; + gpufl::launcher::TraceProcessResult result; + result.rc = 0; + return result; + } + + mutable std::map env; + mutable std::vector seen_command; + mutable gpufl::launcher::RunOptions seen_options; + +private: + gpufl::launcher::fs::path root_; +}; + +TEST(TraceRunPlanTest, TraceCommonExecutesThePlannedSinglePass) { + namespace fs = std::filesystem; + const fs::path root = fs::temp_directory_path() / + ("gpufl_trace_plan_" + std::to_string(gpufl::detail::GetPid())); + std::error_code ec; + fs::remove_all(root, ec); + fs::create_directories(root, ec); + ASSERT_FALSE(ec) << ec.message(); + { std::ofstream inject(root / "gpufl_inject.so"); ASSERT_TRUE(inject); } + + ExecutingPlanningPlatform platform(root); + TraceArgs args; + args.command = {"target", "--work"}; + args.name = "trace-plan-test"; + args.output_dir = (root / "requested-output").string(); + args.warmup_ms = 100; + args.window_ms = 200; + + EXPECT_EQ(gpufl::launcher::runTraceCommon(args, platform), 0); + EXPECT_EQ(platform.seen_command, args.command); + EXPECT_EQ(platform.seen_options.run_ms, 300); + EXPECT_EQ(platform.env[gpufl::env::kAppName], "trace-plan-test"); + EXPECT_EQ(platform.env[gpufl::env::kLogDir], args.output_dir); + EXPECT_EQ(platform.env[gpufl::env::kProfilingEngine], "Trace"); + + fs::remove_all(root, ec); +} + +} // namespace