From 6075139f553a6a4cbdbc28f9c5c57e1d00cfc2ae Mon Sep 17 00:00:00 2001 From: Myoungho Shin Date: Tue, 27 Oct 2026 16:30:49 -0700 Subject: [PATCH 1/2] Let the agent choose the channels it uploads gpufl trace --upload and gpufl upload pinned the agent to device,scope,system,sass. The agent is becoming a channel-agnostic transport that uploads every channel with published windows, so other producers (such as the serving writer) need no launcher change, and the launcher should not carry their channel names. - TraceArgs::log_types is now empty by default and gpufl upload passes no list. --log-types still narrows a trace upload. - With an empty list the agent starts without GPUFL_LOG_TYPES: an inherited value is removed, so the agent's own default applies. The agent never took the list as an argument. - gpufl monitor still passes system. Uploads do not change: native sessions hold only those four channels, every released agent defaults to the same four, and an older launcher's list acts as a filter on a newer agent. --- CHANGELOG.md | 9 ++++ daemon/launcher/agent_launcher.cpp | 19 +++++--- daemon/launcher/agent_launcher.hpp | 2 + daemon/launcher/cli_parse.hpp | 2 +- daemon/launcher/cli_trace_options.cpp | 3 +- daemon/launcher/upload_command.cpp | 2 +- tests/launcher/test_agent_launcher.cpp | 60 ++++++++++++++++++++++++++ tests/launcher/test_cli_parse.cpp | 2 +- 8 files changed, 90 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f87f367..28793e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -188,6 +188,15 @@ types are additive. `--agent-drain-ms`, `--agent-cursor` and `--all-sessions` control it, and the launcher sends upload-complete only after a clean agent drain. `--upload` is scoped to a per-run folder rather than the shared `--output` directory. +- **The launcher no longer picks the agent's channels.** `gpufl trace --upload` + and `gpufl upload` used to pass `device,scope,system,sass` to the agent they + start. They now pass no list, so the agent uploads every channel it finds in + the session directory, and `gpufl trace --log-types` only narrows that. A + `GPUFL_LOG_TYPES` inherited from the shell is removed from the agent's + environment rather than passed on. `gpufl monitor` still passes `system`. + Uploads do not change: a native session holds only those four channels, an + older agent with no list uses its own default (the same four), and an older + launcher's list acts as a filter on a newer agent. - **Linux injection lifecycle (`gpufl trace`).** - **Warmup.** `--warmup` now sets `GPUFL_INJECT_WARMUP_MS` instead of `GPUFL_INJECT_INIT_DELAY_MS`. Nothing waits for a warmup, so a warmup run diff --git a/daemon/launcher/agent_launcher.cpp b/daemon/launcher/agent_launcher.cpp index 42cc67e..4cf988a 100644 --- a/daemon/launcher/agent_launcher.cpp +++ b/daemon/launcher/agent_launcher.cpp @@ -78,6 +78,16 @@ bool setEnv(const char* name, const std::string& value) { #endif } +// Removing a variable that is not set succeeds: that is the state asked for. +bool unsetEnv(const char* name) { +#ifdef _WIN32 + return SetEnvironmentVariableA(name, nullptr) != 0 || + GetLastError() == ERROR_ENVVAR_NOT_FOUND; +#else + return ::unsetenv(name) == 0; +#endif +} + void appendQuotedArg(const std::string& arg, std::string& cmd) { if (!arg.empty() && arg.find_first_of(" \t\n\v\"") == std::string::npos) { cmd += arg; @@ -258,10 +268,6 @@ bool configureAgentEnvironment(const AgentOptions& opts, std::string& error) { error = "agent source folder is empty"; return false; } - if (opts.log_types.empty()) { - error = "agent log types are empty"; - return false; - } if (opts.cursor_file.empty()) { error = "agent cursor file is empty"; return false; @@ -276,7 +282,10 @@ bool configureAgentEnvironment(const AgentOptions& opts, std::string& error) { } if (!setEnv("GPUFL_SOURCE_FOLDER", opts.source_folders) || !setEnv("GPUFL_SOURCE_FOLDERS", opts.source_folders) || - !setEnv("GPUFL_LOG_TYPES", opts.log_types) || + // An empty list leaves the channels to the agent (every channel it finds), so + // a GPUFL_LOG_TYPES inherited from the caller's shell is removed, not passed on. + !(opts.log_types.empty() ? unsetEnv("GPUFL_LOG_TYPES") + : setEnv("GPUFL_LOG_TYPES", opts.log_types)) || !setEnv("GPUFL_CURSOR_FILE", opts.cursor_file) || !setEnv("GPUFL_PUBLISHER_TYPE", "http") || !setEnv("GPUFL_HTTP_HOST", opts.backend_url) || diff --git a/daemon/launcher/agent_launcher.hpp b/daemon/launcher/agent_launcher.hpp index cbdc968..a268e96 100644 --- a/daemon/launcher/agent_launcher.hpp +++ b/daemon/launcher/agent_launcher.hpp @@ -7,6 +7,8 @@ namespace gpufl::launcher { struct AgentOptions { std::string source_folders; + // Channel filter (GPUFL_LOG_TYPES). Empty: no filter, the agent uploads every + // channel it finds. std::string log_types; std::string cursor_file; std::string backend_url; diff --git a/daemon/launcher/cli_parse.hpp b/daemon/launcher/cli_parse.hpp index 0cd86b2..5a87a53 100644 --- a/daemon/launcher/cli_parse.hpp +++ b/daemon/launcher/cli_parse.hpp @@ -32,7 +32,7 @@ struct TraceArgs { std::string api_version = "v1"; // --api-version std::string agent_jar; // --agent-jar; else GPUFL_AGENT_JAR std::string agent_cursor; // --agent-cursor; default /cursor.json - std::string log_types = "device,scope,system,sass"; // --log-types (sass carries cubin disassembly + source artifacts) + std::string log_types; // --log-types; empty: the agent uploads every channel it finds int agent_drain_ms = 60000; // --agent-drain-ms: cap on waiting for the agent to finish uploading // Bounded window profiling (`gpufl trace` only): bound a capture of a // long-running target that never exits on its own. warmup defers the diff --git a/daemon/launcher/cli_trace_options.cpp b/daemon/launcher/cli_trace_options.cpp index 86933a1..ccb2d19 100644 --- a/daemon/launcher/cli_trace_options.cpp +++ b/daemon/launcher/cli_trace_options.cpp @@ -516,7 +516,8 @@ const CliOptionManager& traceOptions() { kSection(TraceHelpSection::Runtime), &parseNonEmptyStringOption<&TraceArgs::agent_cursor>) .add({"--log-types"}, "", - "Agent channels to upload (default: device,scope,system,sass)", + "Upload only these agent channels (default: every channel " + "the agent finds)", kSection(TraceHelpSection::Runtime), &parseNonEmptyStringOption<&TraceArgs::log_types>) .add({"--agent-drain-ms"}, "", diff --git a/daemon/launcher/upload_command.cpp b/daemon/launcher/upload_command.cpp index c507571..7fd3ddf 100644 --- a/daemon/launcher/upload_command.cpp +++ b/daemon/launcher/upload_command.cpp @@ -91,7 +91,7 @@ int runUpload(const UploadArgs& args) { AgentOptions agent_opts; agent_opts.source_folders = args.log_path; - agent_opts.log_types = "device,scope,system,sass"; + // log_types stays empty: the agent uploads every channel it finds. agent_opts.backend_url = backend_url; agent_opts.api_key = api_key; agent_opts.api_version = apiVersionFrom(resolve(args.api_path, gpufl::env::kApiPath)); diff --git a/tests/launcher/test_agent_launcher.cpp b/tests/launcher/test_agent_launcher.cpp index a777764..242777b 100644 --- a/tests/launcher/test_agent_launcher.cpp +++ b/tests/launcher/test_agent_launcher.cpp @@ -1,5 +1,6 @@ #include +#include #include #include @@ -15,6 +16,41 @@ std::vector exitCommand(const int code) { #endif } +// A child that exits 0 only when GPUFL_LOG_TYPES is absent from its environment. +std::vector logTypesAbsentCommand() { +#ifdef _WIN32 + return {"cmd.exe", "/d", "/s", "/c", + "if defined GPUFL_LOG_TYPES (exit 1) else (exit 0)"}; +#else + return {"/bin/sh", "-c", "test -z \"${GPUFL_LOG_TYPES+set}\""}; +#endif +} + +void setEnv(const char* name, const char* value) { +#ifdef _WIN32 + _putenv_s(name, value); +#else + ::setenv(name, value, /*overwrite=*/1); +#endif +} + +void unsetEnv(const char* name) { +#ifdef _WIN32 + _putenv_s(name, ""); +#else + ::unsetenv(name); +#endif +} + +// The child's exit code, or -1 when it could not be started or did not exit. +int childExitCode(const std::vector& command) { + gpufl::launcher::AgentProcess process; + std::string error; + if (!process.start(command, error)) return -1; + const auto result = process.waitForExit(5000); + return result.exited ? result.exit_code : -1; +} + } // namespace TEST(AgentProcessTest, AZeroExitIsACompletedUpload) { @@ -40,3 +76,27 @@ TEST(AgentProcessTest, ANonzeroExitIsNotACompletedUpload) { EXPECT_EQ(result.exit_code, 7); EXPECT_FALSE(result.succeeded()); } + +TEST(AgentEnvironmentTest, AnEmptyChannelListRemovesAnInheritedFilter) { + setEnv("GPUFL_LOG_TYPES", "device,scope"); // as if exported by the caller's shell + ASSERT_EQ(childExitCode(logTypesAbsentCommand()), 1); // the child sees it + + gpufl::launcher::AgentOptions opts; + opts.source_folders = "session-dir"; + opts.cursor_file = "cursor.json"; + opts.backend_url = "http://localhost:1"; + opts.api_key = "test-key"; + std::string error; + ASSERT_TRUE(gpufl::launcher::configureAgentEnvironment(opts, error)) << error; + + EXPECT_EQ(childExitCode(logTypesAbsentCommand()), 0); + + // configureAgentEnvironment publishes into this process; leave it clean. + for (const char* name : + {"GPUFL_SOURCE_FOLDER", "GPUFL_SOURCE_FOLDERS", "GPUFL_CURSOR_FILE", + "GPUFL_PUBLISHER_TYPE", "GPUFL_HTTP_HOST", "GPUFL_HTTP_TOKEN", + "GPUFL_HTTP_API_VERSION", "GPUFL_AGENT_UPLOAD_MODE", + "GPUFL_AGENT_EXIT_WHEN_DRAINED"}) { + unsetEnv(name); + } +} diff --git a/tests/launcher/test_cli_parse.cpp b/tests/launcher/test_cli_parse.cpp index 1bbc544..92d34a2 100644 --- a/tests/launcher/test_cli_parse.cpp +++ b/tests/launcher/test_cli_parse.cpp @@ -398,7 +398,7 @@ TEST(CliParseTrace, UploadFlag) { ASSERT_TRUE(r.args.has_value()) << r.error; EXPECT_TRUE(r.args->upload); EXPECT_EQ(r.args->api_version, "v1"); - EXPECT_EQ(r.args->log_types, "device,scope,system,sass"); + EXPECT_TRUE(r.args->log_types.empty()); // the agent uploads every channel EXPECT_EQ(r.args->agent_drain_ms, 60000); } From 500036c908f1ebcf1e8d475c3fdfe25aef35387d Mon Sep 17 00:00:00 2001 From: Myoungho Shin Date: Tue, 27 Oct 2026 16:31:18 -0700 Subject: [PATCH 2/2] Describe the agent's channel default in the daemon README The table still listed device,scope,system. With a channel-agnostic agent, an unset GPUFL_LOG_TYPES means every channel it finds. --- daemon/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/README.md b/daemon/README.md index a062e41..5df0318 100644 --- a/daemon/README.md +++ b/daemon/README.md @@ -216,7 +216,7 @@ The named volume persists the agent's read cursor so it resumes from where it le |---|---|---| | `GPUFL_SOURCE_FOLDER` | same as `GPUFL_MONITOR_LOG_DIR` | Directory the agent watches for log files | | `GPUFL_SOURCE_PREFIX` | `session` | Filename prefix of the log files (e.g. `session.device.log`) | -| `GPUFL_LOG_TYPES` | `device,scope,system` | Comma-separated log channels to tail | +| `GPUFL_LOG_TYPES` | unset: every channel the agent finds (agents before gpufl-agent#31 default to `device,scope,system,sass`) | Comma-separated log channels to tail; only narrows the set | | `GPUFL_CURSOR_FILE` | `/var/gpufl/monitor/cursor.json` | Path to the cursor file for resume-on-restart | ### Agent - HTTP publisher