Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions daemon/launcher/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ endif()
add_executable(gpufl_launcher
main.cpp
agent_launcher.cpp
cli_help.cpp
cli_parse.cpp
cli_parse_internal.cpp
cli_subcommand_options.cpp
cli_trace_options.cpp
info_command.cpp
trace_command_common.cpp
deep_window_env.cpp
Expand Down
149 changes: 149 additions & 0 deletions daemon/launcher/cli_help.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#include "cli_parse.hpp"
#include "cli_trace_options.hpp"

#include <string>

namespace gpufl::launcher {

const char* topLevelHelp() {
return R"HELP(gpufl - GPUFlight launcher

USAGE:
gpufl <SUBCOMMAND> [OPTIONS]

SUBCOMMANDS:
trace Inject GPUFlight into a target process and capture telemetry
monitor Run long-lived GPU/host telemetry collection
info Print local GPU device capabilities
upload Upload a captured session's NDJSON logs to the backend
version Print version + build info

Run `gpufl <subcommand> --help` for subcommand-specific help.
)HELP";
}

const char* traceHelp() {
static const std::string help = [] {
std::string out = R"HELP(gpufl trace - Capture telemetry from a target process

USAGE:
gpufl trace [OPTIONS] -- <COMMAND>...

OPTIONS:
)HELP";
// Every option below is rendered from the registry in
// cli_trace_options.cpp, so a new flag documents itself. Only prose
// that belongs to no single option is written here.
out += formatTraceSimpleOptions(TraceHelpSection::Capture);
out += formatTraceSimpleOptions(TraceHelpSection::Runtime);
out += formatTraceSimpleOptions(TraceHelpSection::Segmentation);
out += formatTraceSimpleOptions(TraceHelpSection::Window);
out += R"HELP( A deep window is ONE adaptive run: gpufl picks the deep engine, so
--deep-* cannot be combined with --passes. Which engine was selected
is printed at startup rather than promised here - it depends on the
GPU, and today only PM sampling is selected.

)HELP";
out += formatTraceSimpleOptions(TraceHelpSection::Deep);
out += formatTraceSimpleOptions(TraceHelpSection::Sampling);
out += R"HELP( -h, --help Print this help

EXAMPLES:
gpufl trace -- python train.py
gpufl trace --name=quantize -- ./inference_server
gpufl trace --passes=Trace,PmSampling -- python train.py
gpufl trace --passes=Deep -- python train.py # multi-pass
gpufl trace --passes=Trace,SassMetrics -- ./app # custom plan
gpufl trace --passes=Trace+PcSampling -- ./app # one-process composite
gpufl trace --passes=Trace+PcSampling --warmup=60s --window=5m -- ./serve
)HELP";
return out;
}();
return help.c_str();
}

const char* uploadHelp() {
return R"HELP(gpufl upload - Upload a captured session's NDJSON logs to the backend

USAGE:
gpufl upload <LOG_PATH> [OPTIONS]

ARGS:
<LOG_PATH> Output directory written by `gpufl trace`, or
the InitOptions log_path directory. Looks for
'<LOG_PATH>/<session_id>/<channel>.log[.gz]'.
A trace dir works directly:
e.g. ~/.gpufl/traces/20260603-101500_ab12cd34

OPTIONS:
--backend-url=<URL> Backend base URL. Env: GPUFL_BACKEND_URL
--api-key=<KEY> Bearer token. Env: GPUFL_API_KEY
--api-path=<PATH> Reverse-proxy mount. Defaults to /api/v1
--agent-jar=<PATH> Run the uploader as `java -jar <PATH>`.
Env: GPUFL_AGENT_JAR (else gpufl-agent on PATH)
--timeout=<SECS> Cap on waiting for the upload to finish. Default 300
--retries=<N> Accepted for compatibility; the agent retries internally
-q, --quiet Suppress periodic progress lines
--all-sessions Upload every session in the dir (this is the default)
--force Re-upload even if the cursor says it shipped
-h, --help Print this help

EXAMPLES:
gpufl upload ~/.gpufl/traces/20260603-101500_ab12cd34
gpufl upload ./logs --force
GPUFL_API_KEY=gpfl_… GPUFL_BACKEND_URL=https://api.gpuflight.com \
gpufl upload ./logs
)HELP";
}

const char* monitorHelp() {
return R"HELP(gpufl monitor - Run long-lived GPU/host telemetry collection

USAGE:
gpufl monitor [OPTIONS]

OPTIONS:
-n, --name=<NAME> Monitor session name. Default: gpufl-monitor
-o, --output=<DIR> Local NDJSON output dir
(default: ~/.gpufl/monitor/{ts}_{session_id}/)
--interval=<MS> Sampling interval in milliseconds. Default: 5000
--upload Start gpufl-agent as the live uploader
--backend-url=<URL> Backend base URL for --upload
Env fallback: GPUFL_BACKEND_URL
--api-key=<KEY> Bearer token for --upload
Env fallback: GPUFL_API_KEY
--api-version=<VER> Agent HTTP API version. Default: v1
--agent-jar=<PATH> Run agent as `java -jar <PATH>`
Env fallback: GPUFL_AGENT_JAR
--agent-cursor=<P> Agent cursor file. Default: <output>/cursor.json
--log-types=<LIST> Agent channels to upload. Default: system
-q, --quiet Suppress launcher chatter
-v, --verbose Verbose launcher logging
-h, --help Print this help

EXAMPLES:
gpufl monitor
gpufl monitor --interval=1000
gpufl monitor --name=llm-node-1 --upload
)HELP";
}

const char* infoHelp() {
return R"HELP(gpufl info - Print local GPU device capabilities

USAGE:
gpufl info [OPTIONS]

OPTIONS:
--json Emit stable machine-readable JSON
--device=<ID> Limit output to one zero-based device ID
-h, --help Print this help

EXAMPLES:
gpufl info
gpufl info --json
gpufl info --device=0 --json
)HELP";
}

} // namespace gpufl::launcher
182 changes: 182 additions & 0 deletions daemon/launcher/cli_option_manager.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#pragma once

#include <algorithm>
#include <initializer_list>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "cli_parse_internal.hpp"

namespace gpufl::launcher::detail {

/**
* Alias-to-handler dispatch for one subcommand's option table, plus the help
* text for those options.
*
* Help lives next to the option on purpose: a flag and its documentation are
* added in one place, so it is not possible to ship a flag nobody can discover.
* An option with an empty description is deliberately undocumented (removed
* flags kept only to print a migration hint).
*/
template <typename Args>
class CliOptionManager {
public:
using Handler = std::string (*)(
const FlagBreak&, const std::vector<std::string>&, std::size_t&, Args&);

/** Undocumented option: dispatch only, never rendered into help. */
CliOptionManager& add(std::initializer_list<std::string_view> aliases,
const Handler handler) {
return add(aliases, "", "", 0, handler);
}

/**
* Documented option. `value_name` is the metavariable ("<DUR>") or "" for a
* boolean flag; `help_section` groups the option in help output and must be
* nonzero for the option to be rendered.
*
* Aliases MUST have static storage duration - string literals. Only the
* views are stored, so a temporary std::string would dangle.
*/
CliOptionManager& add(std::initializer_list<std::string_view> aliases,
const std::string_view value_name,
const std::string_view description,
const int help_section,
const Handler handler) {
if (aliases.size() == 0 || handler == nullptr) {
throw std::logic_error("CLI option requires an alias and handler");
}
for (const std::string_view alias : aliases) {
if (find(alias) != nullptr) {
throw std::logic_error(
"duplicate CLI option alias: " + std::string(alias));
}
}
options_.push_back({std::vector<std::string_view>(aliases), value_name,
description, help_section, handler});
return *this;
}

bool parse(const FlagBreak& flag,
const std::vector<std::string>& argv,
std::size_t& index,
Args& args,
std::string& error) const {
const Option* option = find(flag.key);
if (option == nullptr) return false;
error = option->handler(flag, argv, index, args);
return true;
}

/** Render every documented option of one section, in registration order. */
std::string formatHelp(const int help_section) const {
std::string out;
for (const Option& option : options_) {
if (option.help_section != help_section) continue;
if (option.description.empty()) continue;
appendHelpLine(out, option);
}
return out;
}

/**
* Every alias in the table, documented or not. Lets a test assert that the
* table and the help output agree instead of trusting review.
*/
std::vector<std::string_view> aliases() const {
std::vector<std::string_view> all;
for (const Option& option : options_) {
all.insert(all.end(), option.aliases.begin(), option.aliases.end());
}
return all;
}

private:
struct Option {
std::vector<std::string_view> aliases;
std::string_view value_name;
std::string_view description;
int help_section = 0;
Handler handler = nullptr;
};

const Option* find(const std::string_view key) const {
const auto option = std::find_if(
options_.begin(), options_.end(),
[key](const Option& candidate) {
return std::find(candidate.aliases.begin(),
candidate.aliases.end(), key) !=
candidate.aliases.end();
});
return option == options_.end() ? nullptr : &*option;
}

// Matches the launcher's long-standing help layout: descriptions start at
// one column, and a long-only option indents to where a "-x, " prefix would
// have ended so both kinds line up.
static void appendHelpLine(std::string& out, const Option& option) {
constexpr std::size_t kShortIndent = 4;
constexpr std::size_t kLongOnlyIndent = 8;
constexpr std::size_t kDescriptionColumn = 28;
constexpr std::size_t kLineWidth = 96;

const bool has_short = std::any_of(
option.aliases.begin(), option.aliases.end(),
[](const std::string_view alias) {
return alias.size() > 1 && alias[0] == '-' && alias[1] != '-';
});
const std::size_t indent = has_short ? kShortIndent : kLongOnlyIndent;

std::string spelling;
for (std::size_t i = 0; i < option.aliases.size(); ++i) {
if (i > 0) spelling += ", ";
spelling += option.aliases[i];
}
if (!option.value_name.empty()) {
spelling += "=";
spelling += option.value_name;
}

out.append(indent, ' ');
out += spelling;
std::size_t column = indent + spelling.size();
// A spelling that reaches the description column takes the next line,
// so a long flag name never pushes its description out of alignment.
if (column >= kDescriptionColumn) {
out += '\n';
out.append(kDescriptionColumn, ' ');
} else {
out.append(kDescriptionColumn - column, ' ');
}
column = kDescriptionColumn;

std::istringstream words{std::string(option.description)};
std::string word;
bool first = true;
while (words >> word) {
const std::size_t separator = first ? 0 : 1;
if (column + separator + word.size() > kLineWidth) {
out += '\n';
out.append(kDescriptionColumn, ' ');
column = kDescriptionColumn;
first = true;
}
if (!first) {
out += ' ';
++column;
}
out += word;
column += word.size();
first = false;
}
out += '\n';
}

std::vector<Option> options_;
};

} // namespace gpufl::launcher::detail
Loading
Loading