diff --git a/CMakeLists.txt b/CMakeLists.txt index f8121dace8..1faec1824e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -606,6 +606,7 @@ set(SOURCES_CORE src/cpp/server/utils/version_utils.cpp src/cpp/server/utils/wmi_helper.cpp src/cpp/server/utils/network_beacon.cpp + src/cpp/server/utils/rocm_arch_utils.cpp src/cpp/server/utils/tcp_jsonl_client.cpp src/cpp/server/backends/cloud_server.cpp src/cpp/server/backends/llamacpp_server.cpp diff --git a/src/cpp/cli/CMakeLists.txt b/src/cpp/cli/CMakeLists.txt index bd58c60ba1..8a02445364 100644 --- a/src/cpp/cli/CMakeLists.txt +++ b/src/cpp/cli/CMakeLists.txt @@ -93,6 +93,7 @@ set(COMMON_SOURCES ../server/utils/path_utils.cpp ../server/utils/json_utils.cpp ../server/utils/http_client.cpp + ../server/utils/rocm_arch_utils.cpp agent_launcher.cpp agent_config_file.cpp opencode_profile.cpp diff --git a/src/cpp/cli/lemonade_client.cpp b/src/cpp/cli/lemonade_client.cpp index 66a334ec9b..2c25a77a7e 100644 --- a/src/cpp/cli/lemonade_client.cpp +++ b/src/cpp/cli/lemonade_client.cpp @@ -7,6 +7,9 @@ #include #include +#include "lemon/utils/json_utils.h" +#include "lemon/utils/rocm_arch_utils.h" + namespace lemonade { using json = nlohmann::json; @@ -472,6 +475,138 @@ int LemonadeClient::list_models(bool show_all, const std::string& name_filter) c } } +int LemonadeClient::set_rocm_arch(const std::string& rocm_arch) const{ + std::vector rocm_devs; + + if (rocm_arch.empty()) { + std::cerr << "Error Rocm arch value is empty" << std::endl; + return 1; + } + + // Check if the rocm_arch var exists at server config.json + try { + std::string config_response = make_request("/internal/config"); + auto json_config_response = json::parse(config_response); + if (!json_config_response.contains("rocm_arch")) { + std::cout << "You do NOT have the rocm_arch config var in your config.json" << std::endl; + return 0; + } + } catch (const HttpError& e) { + std::cerr << "Error checking rocm_arch config var: " << extract_server_error_message(e) << std::endl; + return 1; + } catch (const std::exception& e) { + std::cerr << "Error checking rocm_arch config var: " << e.what() << std::endl; + return 1; + } + + // Check if an active gpu with Rocm arch exists at server. We only check if it is NOT auto. + if (!rocm_arch.empty() && rocm_arch != "auto") { + bool rocm_arch_found = false; + try { + std::string response = make_request("/api/v1/system-info"); + auto json_response = json::parse(response); + + if (json_response.contains("devices")) { + rocm_devs = lemon::ROCmArchUtils::rocm_arch_get_active_devices(json_response["devices"]); + for (const auto& rocm_dev : rocm_devs) { + if (rocm_arch == rocm_dev.name) { + rocm_arch_found = true; + break; + } + } + } + if (!rocm_arch_found) { + std::cerr << "Error there is not any active gpu with that ROCm arch " << std::endl; + return 1; + } + } catch (const HttpError& e) { + std::cerr << "Error setting ROCm arch: " << extract_server_error_message(e) << std::endl; + return 1; + } catch (const json::exception& e) { + std::cerr << "Error parsing system-info JSON: " << e.what() << std::endl; + return 1; + } catch (const std::exception& e) { + std::cerr << "Error setting ROCm arch: " << e.what() << std::endl; + return 1; + } + } + + //Set the config var. + try { + json request_body = {{"rocm_arch", rocm_arch}}; + std::string response = make_request("/internal/set", "POST", request_body.dump(), "application/json"); + + auto response_json = json::parse(response); + if (response_json.contains("status") && response_json["status"] == "success") { + std::cout << "ROCm arch setted successfully: " << rocm_arch << std::endl; + return 0; + } else { + std::cerr << "Failed to set rocm_arch config var" << std::endl; + return 1; + } + } catch (const HttpError& e) { + std::cerr << "Error setting ROCm architecture: " << extract_server_error_message(e) << std::endl; + return 1; + } catch (const json::exception& e) { + std::cerr << "Error parsing rocm_arch set response JSON: " << e.what() << std::endl; + return 1; + } catch (const std::exception& e) { + std::cerr << "Error setting ROCm architecture: " << e.what() << std::endl; + return 1; + } +} + +int LemonadeClient::list_rocm_archs() const { + + std::vector rocm_devs; + + // Check if rocm_archs config var exists at server. + try { + std::string config_response = make_request("/internal/config"); + auto json_config_response = json::parse(config_response); + if (!json_config_response.contains("rocm_arch")) { + std::cout << "You do NOT have the rocm_arch config var in your config.json" << std::endl; + return 0; + } + + } catch (const HttpError& e) { + std::cerr << "Error listing ROCm architectures: " << extract_server_error_message(e) << std::endl; + return 1; + } catch (const std::exception& e) { + std::cerr << "Error listing ROCm architectures: " << e.what() << std::endl; + return 1; + } + + try { + std::cout << "List of avaible ROCm architectures." << std::endl; + std::cout << "----------------------------------------------" << std::endl; + std::string response = make_request("/api/v1/system-info"); + auto json_response = json::parse(response); + if (json_response.contains("devices")) { + rocm_devs = lemon::ROCmArchUtils::rocm_arch_get_active_devices(json_response["devices"]); + } + std::cout << "architecture\t| vram (GB)\t|" << std::endl; + for (const auto& rocm_dev : rocm_devs) { + + std::cout << " " << rocm_dev.name << "\t" << " " << rocm_dev.vram_gb << std::endl; + } + std::cout << "----------------------------------------------" << std::endl << std::endl; + return 0; + + } catch (const HttpError& e) { + std::cerr << "Error listing gpus: " << extract_server_error_message(e) << std::endl; + //return {}; + return 1; + } catch (const json::exception& e) { + std::cerr << "Error parsing gpus JSON: " << e.what() << std::endl; + return 1; + } catch (const std::exception& e) { + std::cerr << "Error listing gpus: " << e.what() << std::endl; + return 1; + } + +} + // Helper function to parse SSE progress events static std::string format_speed(double bytes_per_sec) { if (bytes_per_sec < 1024.0) { diff --git a/src/cpp/cli/main.cpp b/src/cpp/cli/main.cpp index c6c2e24594..04f3b13960 100644 --- a/src/cpp/cli/main.cpp +++ b/src/cpp/cli/main.cpp @@ -169,6 +169,7 @@ struct CliConfig { bool codex_use_user_config = false; std::string codex_model_provider = "lemonade"; std::string agent_args; + std::string rocm_arch = ""; // Cloud provider commands std::string cloud_provider; @@ -1106,6 +1107,16 @@ static int handle_scan_command(const CliConfig& config) { return 0; } +static int handle_rocm_arch_command(lemonade::LemonadeClient& client, + const CliConfig& config, + bool set_requested) { + if (set_requested) { + return client.set_rocm_arch(config.rocm_arch); + } else { + return client.list_rocm_archs(); + } +} + int main(int argc, char* argv[]) { // CLI11 configuration CLI::App app{"Lemonade CLI - HTTP client for Lemonade Server"}; @@ -1143,6 +1154,14 @@ int main(int argc, char* argv[]) { CLI::App* logs_cmd = app.add_subcommand("logs", "Open server logs in the web UI")->group("Server"); CLI::App* scan_cmd = app.add_subcommand("scan", "Scan for network beacons")->group("Server"); + // List rocm arch + CLI::App* rocm_arch_cmd = app.add_subcommand("rocm-arch", "View or modify ROCm architectures")->group("Server"); + // We add the subcommand set + CLI::App* rocm_arch_set_cmd = rocm_arch_cmd->add_subcommand("set", "Set ROCm architecture to use (e.g. gfx1200, gfx1150, ...)")->group("Subcommands"); + rocm_arch_set_cmd->add_option("rocm_arch", config.rocm_arch, "ROCm architecture to use")->required(); + rocm_arch_set_cmd->allow_extras(true); + rocm_arch_set_cmd->fallthrough(false); + // Config commands CLI::App* config_cmd = app.add_subcommand("config", "View or modify server configuration")->group("Server"); CLI::App* config_set_cmd = config_cmd->add_subcommand("set", "Set configuration values (e.g., llamacpp.backend=rocm port=8123)")->group("Subcommands"); @@ -1456,6 +1475,9 @@ int main(int argc, char* argv[]) { return handle_config_view(client); } else if (cleanup_cmd->count() > 0) { return client.cleanup_cache(config.dry_run); + } else if (rocm_arch_cmd->count() > 0) { + return handle_rocm_arch_command(client, config, + rocm_arch_set_cmd->count() > 0); } else if (bench_cmd->count() > 0) { auto bench_config = lemon_cli::build_bench_config(config.output_file, config.bench); return lemon_cli::handle_bench_command(client, bench_config); diff --git a/src/cpp/include/lemon/runtime_config.h b/src/cpp/include/lemon/runtime_config.h index 9dbc422f68..d343e88bde 100644 --- a/src/cpp/include/lemon/runtime_config.h +++ b/src/cpp/include/lemon/runtime_config.h @@ -43,6 +43,7 @@ class RuntimeConfig { bool enable_dgpu_gtt() const; std::string rocm_channel() const; std::string rocm_channel_for_recipe(const std::string& recipe) const; + std::string rocm_arch() const; // Backend settings (nested) json backend_config(const std::string& backend_name) const; @@ -101,6 +102,8 @@ class RuntimeConfig { const std::string& key, const std::string& value); + static void validate_rocm_arch(const std::string& rocm_arch); + private: // Validate a single key/value pair. Throws std::invalid_argument on failure. void validate(const std::string& key, const json& value) const; diff --git a/src/cpp/include/lemon/utils/rocm_arch_utils.h b/src/cpp/include/lemon/utils/rocm_arch_utils.h new file mode 100644 index 0000000000..80c8c77241 --- /dev/null +++ b/src/cpp/include/lemon/utils/rocm_arch_utils.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +namespace lemon { + +using json = nlohmann::json; + +//ROCm Device information structure +struct ROCmDeviceInfo { + std::string family; + std::string name; + float vram_gb; +}; + +class ROCmArchUtils { + +public: + static std::string transform_isakfd_to_gfx(const std::string& isa); + static std::string get_gfx_from_device_name(const std::string& device_name); + static bool rocm_arch_is_valid_gfx(const std::string& gfx_arch); + static std::vector rocm_arch_get_active_devices(const json& json_devices); +}; + +} // namespace lemon diff --git a/src/cpp/include/lemon_cli/lemonade_client.h b/src/cpp/include/lemon_cli/lemonade_client.h index da1fb19d1e..a09d53aa92 100644 --- a/src/cpp/include/lemon_cli/lemonade_client.h +++ b/src/cpp/include/lemon_cli/lemonade_client.h @@ -97,6 +97,10 @@ class LemonadeClient { int install_backend(const std::string& recipe, const std::string& backend, bool force = false); int uninstall_backend(const std::string& recipe, const std::string& backend); + // Rocm-arch selector commands + int list_rocm_archs() const; + int set_rocm_arch(const std::string& rocm_arch) const; + // Cloud provider commands. Each maps to one /v1/cloud/* or /v1/{install, // uninstall} request. api_key is optional on install — when omitted the // server relies on env var or a later /v1/cloud/auth POST. diff --git a/src/cpp/server/runtime_config.cpp b/src/cpp/server/runtime_config.cpp index a33a4098ed..cc04cdf45c 100644 --- a/src/cpp/server/runtime_config.cpp +++ b/src/cpp/server/runtime_config.cpp @@ -2,6 +2,7 @@ #include "lemon/system_info.h" #include "lemon/utils/aixlog.hpp" #include "lemon/utils/path_utils.h" +#include "lemon/utils/rocm_arch_utils.h" #include #include #include @@ -132,6 +133,32 @@ void RuntimeConfig::validate_bin_path(const std::string& config_section, // error if the tag does not exist on GitHub. } +void RuntimeConfig::validate_rocm_arch(const std::string& rocm_arch) { + std::vector rocm_devs; + bool rocm_arch_found = false; + + // We check if rocm_arch has the correct format and if we have an active gpu with that architecture. + if (rocm_arch.empty() || rocm_arch == "auto" ) { + return ; + } + if (!lemon::ROCmArchUtils::rocm_arch_is_valid_gfx(rocm_arch)) { + throw std::invalid_argument("'rocm_arch' has NOT valid format"); + } + json system_info = SystemInfoCache::get_system_info_with_cache(); + if (system_info.contains("devices")) { + rocm_devs = lemon::ROCmArchUtils::rocm_arch_get_active_devices(system_info["devices"]); + for (const auto& rocm_dev : rocm_devs) { + if (rocm_arch == rocm_dev.name) { + rocm_arch_found = true; + break; + } + } + } + if (!rocm_arch_found) { + throw std::invalid_argument("There is NOT active GPU with selected 'rocm_arch'"); + } +} + RuntimeConfig::RuntimeConfig(const json& config) : config_(config) { // Config is expected to already have defaults merged in (by ConfigFile::load). @@ -251,6 +278,14 @@ std::string RuntimeConfig::rocm_channel_for_recipe(const std::string& recipe) co return channel; } +std::string RuntimeConfig::rocm_arch() const{ + std::shared_lock lock(mutex_); + if (config_.contains("rocm_arch")){ + return config_["rocm_arch"].get(); + } + return ""; +} + json RuntimeConfig::backend_config(const std::string& backend_name) const { std::shared_lock lock(mutex_); if (config_.contains(backend_name) && config_[backend_name].is_object()) { @@ -461,6 +496,11 @@ void RuntimeConfig::validate(const std::string& key, const json& value) const { if (channel != "stable" && channel != "nightly") { throw std::invalid_argument("'rocm_channel' must be either 'stable', or 'nightly'"); } + } else if (key == "rocm_arch") { + if (!value.is_string()) { + throw std::invalid_argument("'rocm_arch' must be a string"); + } + validate_rocm_arch(value); } else if (is_backend_name(key)) { if (!value.is_object()) { throw std::invalid_argument("'" + key + "' must be an object"); diff --git a/src/cpp/server/system_info.cpp b/src/cpp/server/system_info.cpp index fa6d5874ec..b7cfffa795 100644 --- a/src/cpp/server/system_info.cpp +++ b/src/cpp/server/system_info.cpp @@ -6,6 +6,7 @@ #include "lemon/utils/version_utils.h" #include "lemon/utils/json_utils.h" #include "lemon/utils/process_manager.h" +#include "lemon/utils/rocm_arch_utils.h" #include "lemon/backends/backend_utils.h" #include #include @@ -1836,33 +1837,20 @@ std::string identify_cuda_arch_from_name(const std::string& device_name) { // Helper to identify ROCm architecture from GPU name. std::string identify_rocm_arch_from_name(const std::string& device_name) { std::string device_lower = device_name; + std::string gfx; std::transform(device_lower.begin(), device_lower.end(), device_lower.begin(), ::tolower); - std::smatch gfx_match; - // Match 3- or 4-digit gfx tokens; the trailing nibble can be hex (e.g. gfx90a). - if (std::regex_search(device_lower, gfx_match, std::regex(R"((gfx[0-9a-f]{3,4}))"))) { - return gfx_match[1].str(); + gfx = ROCmArchUtils::get_gfx_from_device_name(device_lower); + if (!gfx.empty()) { + return gfx; } // Linux will pass the ISA from KFD, transform it to what the rest of lemonade expects - if (!device_lower.empty() && - std::all_of(device_lower.begin(), device_lower.end(), ::isdigit)) { - int v; - try { - v = std::stoi(device_lower); - } catch (const std::exception& e) { - throw std::runtime_error( - "Failed to parse gfx_target_version '" + device_lower + "': " + e.what()); - } - int major = v / 10000; - int minor = (v / 100) % 100; - int step = v % 100; - - char buf[16]; - std::snprintf(buf, sizeof(buf), "gfx%d%x%x", major, minor, step); - return std::string(buf); + gfx = ROCmArchUtils::transform_isakfd_to_gfx(device_lower); + if (!gfx.empty()) { + return gfx; } - + if (device_lower.find("radeon") == std::string::npos && device_lower.find("amd") == std::string::npos) { return ""; @@ -2085,6 +2073,13 @@ std::string identify_npu_arch() { std::string SystemInfo::get_rocm_arch() { // Returns the ROCm architecture for the best available AMD GPU on this system + // Check if user has defined rocm_arch config var. + if (auto* cfg = RuntimeConfig::global()) { + std::string rocm_arch = cfg->rocm_arch(); + if (!rocm_arch.empty() && rocm_arch != "auto") { + return cfg->rocm_arch(); + } + } // Checks iGPU first, then dGPUs. Returns empty string if no compatible GPU found. try { // Use cached system info to avoid re-detecting GPUs @@ -2115,6 +2110,7 @@ std::string SystemInfo::get_rocm_arch() { } return ""; // No supported architecture found + } static int cuda_sm_value(const std::string& arch) { diff --git a/src/cpp/server/utils/rocm_arch_utils.cpp b/src/cpp/server/utils/rocm_arch_utils.cpp new file mode 100644 index 0000000000..d1ae0e6962 --- /dev/null +++ b/src/cpp/server/utils/rocm_arch_utils.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include "lemon/utils/rocm_arch_utils.h" + +namespace lemon{ + + +std::string ROCmArchUtils::transform_isakfd_to_gfx(const std::string& isa) { + if (!isa.empty() && + std::all_of(isa.begin(), isa.end(), ::isdigit)) { + int v; + try { + v = std::stoi(isa); + } catch (const std::exception& e) { + throw std::runtime_error( + "Failed to parse gfx_target_version '" + isa + "': " + e.what()); + } + int major = v / 10000; + int minor = (v / 100) % 100; + int step = v % 100; + + char buf[16]; + std::snprintf(buf, sizeof(buf), "gfx%d%x%x", major, minor, step); + return std::string(buf); + } else { + return ""; + } +} + +std::string ROCmArchUtils::get_gfx_from_device_name(const std::string& device_name) { + std::string device_lower = device_name; + std::transform(device_lower.begin(), device_lower.end(), device_lower.begin(), ::tolower); + + std::smatch gfx_match; + // Match 3- or 4-digit gfx tokens; the trailing nibble can be hex (e.g. gfx90a). + if (std::regex_search(device_lower, gfx_match, std::regex(R"((gfx[0-9a-f]{3,4}))"))) { + return gfx_match[1].str(); + } + return ""; +} + + +bool ROCmArchUtils::rocm_arch_is_valid_gfx(const std::string& gfx_arch) { + std::string gfx = ROCmArchUtils::get_gfx_from_device_name(gfx_arch); + return !gfx.empty(); +} + +std::vector ROCmArchUtils::rocm_arch_get_active_devices(const json& devices) { + std::vector active_devs; + + if (devices.contains("amd_gpu")) { + if (devices["amd_gpu"].is_array()) { + for (const auto& amd_gpu : devices["amd_gpu"]) { + ROCmDeviceInfo dev; + if (amd_gpu.contains("available") && amd_gpu["available"].is_boolean() && amd_gpu["available"]) { + dev.name = ROCmArchUtils::transform_isakfd_to_gfx(amd_gpu["name"]); + dev.vram_gb = amd_gpu["vram_gb"]; + active_devs.push_back(dev); + } + } + } + } + return active_devs; +} + +} // namespace lemon