diff --git a/emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/config/effective_config.cpp b/emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/config/effective_config.cpp index 560be4849a8f..ba2f640c094d 100644 --- a/emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/config/effective_config.cpp +++ b/emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/config/effective_config.cpp @@ -9,12 +9,14 @@ #include "embedded_schema.h" #include "simulation_config_generated.h" +#include #include #include #include #include #include #include +#include namespace rocjitsu { namespace config { @@ -22,10 +24,12 @@ namespace { constexpr std::string_view kBudgetField = "cpu_thread_budget"; -bool is_field_name_char(char c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; +bool is_identifier_start(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; } +bool is_identifier_char(char c) { return is_identifier_start(c) || (c >= '0' && c <= '9'); } + bool starts_comment(std::string_view json, size_t at) { return json[at] == '/' && at + 1 < json.size() && (json[at + 1] == '/' || json[at + 1] == '*'); } @@ -37,7 +41,7 @@ void skip_filler(std::string_view json, size_t &at) { if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { ++at; } else if (json.compare(at, 2, "//") == 0) { - const size_t line_end = json.find('\n', at); + const size_t line_end = json.find_first_of("\r\n", at + 2); at = line_end == std::string_view::npos ? json.size() : line_end + 1; } else if (json.compare(at, 2, "/*") == 0) { const size_t block_end = json.find("*/", at + 2); @@ -48,36 +52,170 @@ void skip_filler(std::string_view json, size_t &at) { } } -/// @brief Advance past a quoted string, honoring backslash escapes. -void skip_string(std::string_view json, size_t &at) { - for (++at; at < json.size(); ++at) { - if (json[at] == '\\') { - ++at; +FailureOr parse_hex_digits(std::string_view json, size_t &at, size_t count, + const util::DiagnosticEmitter &emit_error) { + if (json.size() - at < count) + return emit_error.emit() << "unterminated escape sequence"; + + const std::string_view digits = json.substr(at, count); + uint32_t value = 0; + const auto [ptr, ec] = std::from_chars(digits.data(), digits.data() + digits.size(), value, 16); + if (ec != std::errc{} || ptr != digits.data() + digits.size()) + return emit_error.emit() << "escape code must be followed by hexadecimal digits"; + + at += count; + return value; +} + +Result append_utf8(uint32_t code_point, std::string &decoded, + const util::DiagnosticEmitter &emit_error) { + if (code_point <= 0x7f) { + decoded += static_cast(code_point); + } else if (code_point <= 0x7ff) { + decoded += static_cast(0xc0 | (code_point >> 6)); + decoded += static_cast(0x80 | (code_point & 0x3f)); + } else if (code_point <= 0xffff) { + decoded += static_cast(0xe0 | (code_point >> 12)); + decoded += static_cast(0x80 | ((code_point >> 6) & 0x3f)); + decoded += static_cast(0x80 | (code_point & 0x3f)); + } else if (code_point <= 0x10ffff) { + decoded += static_cast(0xf0 | (code_point >> 18)); + decoded += static_cast(0x80 | ((code_point >> 12) & 0x3f)); + decoded += static_cast(0x80 | ((code_point >> 6) & 0x3f)); + decoded += static_cast(0x80 | (code_point & 0x3f)); + } else { + return emit_error.emit() << "Unicode code point is out of range"; + } + return Result::success(); +} + +/// @brief Decode a quoted string and advance past it. +FailureOr parse_quoted_string(std::string_view json, size_t &at, + const util::DiagnosticEmitter &emit_error) { + if (at >= json.size() || (json[at] != '"' && json[at] != '\'')) + return emit_error.emit() << "expected a quoted string"; + + const char quote = json[at++]; + std::string decoded; + int unicode_high_surrogate = -1; + while (at < json.size()) { + const char c = json[at++]; + if (c == quote) { + if (unicode_high_surrogate != -1) + return emit_error.emit() << "illegal Unicode sequence"; + return decoded; + } + + if (static_cast(c) < ' ') + return emit_error.emit() << "illegal character in string constant"; + if (c != '\\') { + if (unicode_high_surrogate != -1) + return emit_error.emit() << "illegal Unicode sequence"; + decoded += c; continue; } - if (json[at] == '"') { - ++at; - return; + + if (at >= json.size()) + return emit_error.emit() << "unterminated escape sequence"; + const char escape = json[at++]; + if (unicode_high_surrogate != -1 && escape != 'u') + return emit_error.emit() << "illegal Unicode sequence"; + + switch (escape) { + case 'n': + decoded += '\n'; + break; + case 't': + decoded += '\t'; + break; + case 'r': + decoded += '\r'; + break; + case 'b': + decoded += '\b'; + break; + case 'f': + decoded += '\f'; + break; + case '"': + decoded += '"'; + break; + case '\'': + decoded += '\''; + break; + case '\\': + decoded += '\\'; + break; + case '/': + decoded += '/'; + break; + case 'x': { + FailureOr digits = parse_hex_digits(json, at, 2, emit_error); + if (digits.failed()) + return Result::failure(); + decoded += static_cast(digits.value()); + break; + } + case 'u': { + FailureOr digits = parse_hex_digits(json, at, 4, emit_error); + if (digits.failed()) + return Result::failure(); + const uint32_t value = digits.value(); + if (value >= 0xd800 && value <= 0xdbff) { + if (unicode_high_surrogate != -1) + return emit_error.emit() << "illegal Unicode sequence"; + unicode_high_surrogate = static_cast(value); + } else if (value >= 0xdc00 && value <= 0xdfff) { + if (unicode_high_surrogate == -1) + return emit_error.emit() << "illegal Unicode sequence"; + const uint32_t code_point = + 0x10000 + ((static_cast(unicode_high_surrogate) & 0x3ff) << 10) + + (value & 0x3ff); + Result appended = append_utf8(code_point, decoded, emit_error); + if (appended.failed()) + return Result::failure(); + unicode_high_surrogate = -1; + } else { + if (unicode_high_surrogate != -1) + return emit_error.emit() << "illegal Unicode sequence"; + Result appended = append_utf8(value, decoded, emit_error); + if (appended.failed()) + return Result::failure(); + } + break; + } + default: + return emit_error.emit() << "unknown escape code in string constant"; } } + + return emit_error.emit() << "unterminated string constant"; +} + +/// @brief Advance past a quoted string, honoring the loader's escapes. +Result skip_string(std::string_view json, size_t &at, const util::DiagnosticEmitter &emit_error) { + FailureOr parsed = parse_quoted_string(json, at, emit_error); + if (parsed.failed()) + return Result::failure(); + return Result::success(); } /// @brief Advance past one value, descending through nested objects and arrays. -void skip_value(std::string_view json, size_t &at) { +Result skip_value(std::string_view json, size_t &at, const util::DiagnosticEmitter &emit_error) { if (at >= json.size()) - return; + return Result::success(); - if (json[at] == '"') { - skip_string(json, at); - return; - } + if (json[at] == '"' || json[at] == '\'') + return skip_string(json, at, emit_error); if (json[at] == '{' || json[at] == '[') { size_t depth = 0; while (at < json.size()) { const char c = json[at]; - if (c == '"') { - skip_string(json, at); + if (c == '"' || c == '\'') { + Result skipped = skip_string(json, at, emit_error); + if (skipped.failed()) + return skipped; continue; } if (starts_comment(json, at)) { @@ -90,17 +228,21 @@ void skip_value(std::string_view json, size_t &at) { --depth; if (depth == 0) { ++at; - return; + return Result::success(); } } ++at; } - return; + return Result::success(); } while (at < json.size() && json[at] != ',' && json[at] != '}' && json[at] != ']' && - json[at] != ' ' && json[at] != '\t' && json[at] != '\r' && json[at] != '\n') + json[at] != ' ' && json[at] != '\t' && json[at] != '\r' && json[at] != '\n') { + if (starts_comment(json, at)) + break; ++at; + } + return Result::success(); } /// @brief Return @p json with top-level @p field set to @p value, inserting it when absent. @@ -108,12 +250,13 @@ void skip_value(std::string_view json, size_t &at) { /// @details Only the one field's text changes, so every other field keeps the /// spelling, ordering and formatting the author gave it and stays readable when a /// failing run is reproduced from the launch copy. -std::string set_top_level_field(std::string_view json, std::string_view field, - std::string_view value) { +FailureOr set_top_level_field(std::string_view json, std::string_view field, + std::string_view value, + const util::DiagnosticEmitter &emit_error) { size_t at = 0; skip_filler(json, at); if (at >= json.size() || json[at] != '{') - throw std::runtime_error("simulation config must be a JSON object"); + return emit_error.emit() << "simulation config must be a JSON object"; const size_t fields_begin = ++at; skip_filler(json, at); @@ -124,29 +267,33 @@ std::string set_top_level_field(std::string_view json, std::string_view field, if (at >= json.size() || json[at] == '}') break; - std::string_view name; - if (json[at] == '"') { - const size_t name_begin = at + 1; - skip_string(json, at); - if (at > name_begin) - name = json.substr(name_begin, at - 1 - name_begin); + std::string name; + if (json[at] == '"' || json[at] == '\'') { + FailureOr parsed_name = parse_quoted_string(json, at, emit_error); + if (parsed_name.failed()) + return Result::failure(); + name = std::move(parsed_name.value()); } else { const size_t name_begin = at; - while (at < json.size() && is_field_name_char(json[at])) + if (at >= json.size() || !is_identifier_start(json[at])) + return emit_error.emit() << "simulation config has an unreadable field name"; + while (at < json.size() && is_identifier_char(json[at])) ++at; - name = json.substr(name_begin, at - name_begin); + name.assign(json.substr(name_begin, at - name_begin)); } if (name.empty()) - throw std::runtime_error("simulation config has an unreadable field name"); + return emit_error.emit() << "simulation config has an empty field name"; skip_filler(json, at); if (at >= json.size() || json[at] != ':') - throw std::runtime_error("simulation config field '" + std::string(name) + "' has no value"); + return emit_error.emit() << "simulation config field '" << name << "' has no value"; ++at; skip_filler(json, at); const size_t value_begin = at; - skip_value(json, at); + Result skipped_value = skip_value(json, at, emit_error); + if (skipped_value.failed()) + return Result::failure(); if (name == field) { std::string rewritten(json.substr(0, value_begin)); rewritten.append(value); @@ -169,38 +316,59 @@ std::string set_top_level_field(std::string_view json, std::string_view field, } // namespace -std::string json_with_cpu_thread_budget(std::string_view json, uint32_t budget) { - std::string rewritten = set_top_level_field(json, kBudgetField, std::to_string(budget)); +FailureOr json_with_cpu_thread_budget(std::string_view json, uint32_t budget, + const util::DiagnosticEmitter &emit_error) { + FailureOr rewritten = + set_top_level_field(json, kBudgetField, std::to_string(budget), emit_error); + if (rewritten.failed()) + return Result::failure(); - // Rewriting text must never be able to start a simulation under a budget nobody - // asked for, so the result is parsed and the field read back before it is used. - const uint32_t applied = with_parsed_simulation_config_json( - rewritten, rocjitsu::kEmbeddedSchema, - [](const fb::SimulationConfig *config) { return config->cpu_thread_budget(); }); + // The schema parser reports a bad document by throwing. Callers of this function + // handle failure through the returned result, so that message is emitted here. + uint32_t applied = 0; + try { + applied = with_parsed_simulation_config_json( + rewritten.value(), rocjitsu::kEmbeddedSchema, + [](const fb::SimulationConfig *config) { return config->cpu_thread_budget(); }); + } catch (const std::runtime_error &error) { + return emit_error.emit() << error.what(); + } if (applied != budget) - throw std::runtime_error("cannot apply cpu_thread_budget to the simulation config"); - return rewritten; + return emit_error.emit() << "cannot apply cpu_thread_budget to the simulation config"; + return std::move(rewritten.value()); } -std::string write_effective_config(const std::string &source_path, uint32_t budget, pid_t pid) { - const std::string json = json_with_cpu_thread_budget(read_config_file(source_path), budget); +FailureOr write_effective_config(const std::string &source_path, uint32_t budget, + pid_t pid, + const util::DiagnosticEmitter &emit_error) { + // Reading the source throws when the file cannot be opened. The launch path + // reports that through the same result as a rewrite or publish failure. + std::string source_json; + try { + source_json = read_config_file(source_path); + } catch (const std::runtime_error &error) { + return emit_error.emit() << error.what(); + } + FailureOr json = json_with_cpu_thread_budget(source_json, budget, emit_error); + if (json.failed()) + return Result::failure(); const std::filesystem::path directory(rocjitsu::rpc_invocation_runtime_dir(pid)); std::error_code directory_error; std::filesystem::create_directories(directory, directory_error); if (directory_error) - throw std::runtime_error("cannot create runtime directory " + directory.string() + ": " + - directory_error.message()); + return emit_error.emit() << "cannot create runtime directory " << directory.string() << ": " + << directory_error.message(); const std::filesystem::path target = directory / kEffectiveConfigName; const std::filesystem::path temporary = target.string() + ".tmp"; std::ofstream output(temporary); - output << json; + output << json.value(); output.close(); if (!output.good()) { std::error_code remove_error; std::filesystem::remove(temporary, remove_error); - throw std::runtime_error("cannot write effective config " + target.string()); + return emit_error.emit() << "cannot write effective config " << target.string(); } std::error_code rename_error; @@ -208,8 +376,8 @@ std::string write_effective_config(const std::string &source_path, uint32_t budg if (rename_error) { std::error_code remove_error; std::filesystem::remove(temporary, remove_error); - throw std::runtime_error("cannot publish effective config " + target.string() + ": " + - rename_error.message()); + return emit_error.emit() << "cannot publish effective config " << target.string() << ": " + << rename_error.message(); } return target.string(); } diff --git a/emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/config/effective_config.h b/emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/config/effective_config.h index aa4cac470898..95ad099d3bcf 100644 --- a/emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/config/effective_config.h +++ b/emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/config/effective_config.h @@ -7,6 +7,9 @@ #ifndef ROCJITSU_CONFIG_EFFECTIVE_CONFIG_H_ #define ROCJITSU_CONFIG_EFFECTIVE_CONFIG_H_ +#include "rocjitsu/result.h" +#include "util/diagnostic.h" + #include #include #include @@ -25,17 +28,22 @@ inline constexpr char kEffectiveConfigName[] = "effective_config.json"; /// setting process-wide state, so simulations built from other configs in the same /// process keep the budgets their own configs ask for. The field is replaced where /// it already appears and inserted otherwise; all other text is preserved verbatim. -/// @throws std::runtime_error when @p json is not a simulation-config object, or -/// when the rewritten document does not parse back with @p budget. -std::string json_with_cpu_thread_budget(std::string_view json, uint32_t budget); +/// Failure means @p json is not a simulation-config object, or the rewritten document +/// does not parse back with @p budget. The reason is emitted through @p emit_error. +[[nodiscard]] FailureOr +json_with_cpu_thread_budget(std::string_view json, uint32_t budget, + const util::DiagnosticEmitter &emit_error = {}); /// @brief Write the effective config for one invocation and return its path. /// /// @details The copy is placed in the invocation runtime directory next to the /// config-path handoff an exec'd workload already reads, so it is reclaimed by the /// same cleanup and never lands beside a config file the launcher does not own. -/// @throws std::runtime_error when the source cannot be read or the copy written. -std::string write_effective_config(const std::string &source_path, uint32_t budget, pid_t pid); +/// Failure means the source cannot be read, the budget cannot be applied, or the +/// copy cannot be written. The reason is emitted through @p emit_error. +[[nodiscard]] FailureOr +write_effective_config(const std::string &source_path, uint32_t budget, pid_t pid, + const util::DiagnosticEmitter &emit_error = {}); } // namespace config } // namespace rocjitsu diff --git a/emulation/rocjitsu/tests/config_test.cpp b/emulation/rocjitsu/tests/config_test.cpp index a553f4c77d73..1cf402dd9fb1 100644 --- a/emulation/rocjitsu/tests/config_test.cpp +++ b/emulation/rocjitsu/tests/config_test.cpp @@ -26,6 +26,7 @@ #include "rocjitsu/vm/rj_vm.h" #include "rocjitsu/vm/rj_vm_impl.h" #include "rocjitsu/vm/soc.h" +#include "util/diagnostic.h" #include "simdojo/sim/simulation.h" @@ -1178,7 +1179,9 @@ TEST(EffectiveConfigTest, ReplacesTheBudgetAndLeavesEveryOtherFieldAsWritten) { "num_threads": 2 })"; - EXPECT_EQ(config::json_with_cpu_thread_budget(json, 4), R"({ + FailureOr rewritten = config::json_with_cpu_thread_budget(json, 4); + ASSERT_TRUE(rewritten.succeeded()); + EXPECT_EQ(rewritten.value(), R"({ "max_ticks": 4, "cpu_thread_budget": 4, "num_threads": 2 @@ -1186,26 +1189,70 @@ TEST(EffectiveConfigTest, ReplacesTheBudgetAndLeavesEveryOtherFieldAsWritten) { } TEST(EffectiveConfigTest, AddsTheBudgetToConfigsThatDoNotAskForOne) { - EXPECT_EQ(config::json_with_cpu_thread_budget("{}", 8), R"({"cpu_thread_budget": 8})"); - EXPECT_EQ(config::json_with_cpu_thread_budget(R"({"num_threads": 2})", 8), - R"({"cpu_thread_budget": 8,"num_threads": 2})"); + FailureOr inserted = config::json_with_cpu_thread_budget("{}", 8); + ASSERT_TRUE(inserted.succeeded()); + EXPECT_EQ(inserted.value(), R"({"cpu_thread_budget": 8})"); + + FailureOr prefixed = config::json_with_cpu_thread_budget(R"({"num_threads": 2})", 8); + ASSERT_TRUE(prefixed.succeeded()); + EXPECT_EQ(prefixed.value(), R"({"cpu_thread_budget": 8,"num_threads": 2})"); } // The nested name is an unknown field the config parser discards, so only the scanner // can tell the two apart -- and picking the wrong one would silently launch under the // config's own budget instead of the requested one. TEST(EffectiveConfigTest, SetsTheTopLevelBudgetAndNotANestedFieldOfTheSameName) { - EXPECT_EQ(config::json_with_cpu_thread_budget(R"({"vm": {"cpu_thread_budget": 1}})", 6), - R"({"cpu_thread_budget": 6,"vm": {"cpu_thread_budget": 1}})"); + FailureOr rewritten = + config::json_with_cpu_thread_budget(R"({"vm": {"cpu_thread_budget": 1}})", 6); + ASSERT_TRUE(rewritten.succeeded()); + EXPECT_EQ(rewritten.value(), R"({"cpu_thread_budget": 6,"vm": {"cpu_thread_budget": 1}})"); } TEST(EffectiveConfigTest, AcceptsTheCommentsAndBareKeysTheConfigParserAccepts) { - EXPECT_EQ(config::json_with_cpu_thread_budget("{\n // ceiling\n cpu_thread_budget: 32\n}", 2), - "{\n // ceiling\n cpu_thread_budget: 2\n}"); + FailureOr rewritten = + config::json_with_cpu_thread_budget("{\n // ceiling\n cpu_thread_budget: 32\n}", 2); + ASSERT_TRUE(rewritten.succeeded()); + EXPECT_EQ(rewritten.value(), "{\n // ceiling\n cpu_thread_budget: 2\n}"); +} + +TEST(EffectiveConfigTest, PreservesCommentsAdjacentToScalarValues) { + FailureOr block_comment = + config::json_with_cpu_thread_budget(R"({"cpu_thread_budget":32/*,}*/})", 4); + ASSERT_TRUE(block_comment.succeeded()); + EXPECT_EQ(block_comment.value(), R"({"cpu_thread_budget":4/*,}*/})"); + + FailureOr line_comment = + config::json_with_cpu_thread_budget("{\"cpu_thread_budget\":32// ceiling\n}", 4); + ASSERT_TRUE(line_comment.succeeded()); + EXPECT_EQ(line_comment.value(), "{\"cpu_thread_budget\":4// ceiling\n}"); +} + +TEST(EffectiveConfigTest, MatchesSingleQuotedAndEscapedQuotedFieldNames) { + FailureOr single_quoted = + config::json_with_cpu_thread_budget(R"({'cpu_thread_budget':32})", 4); + ASSERT_TRUE(single_quoted.succeeded()); + EXPECT_EQ(single_quoted.value(), R"({'cpu_thread_budget':4})"); + + FailureOr escaped = + config::json_with_cpu_thread_budget(R"({"cpu_thread_budge\u0074":32})", 4); + ASSERT_TRUE(escaped.succeeded()); + EXPECT_EQ(escaped.value(), R"({"cpu_thread_budge\u0074":4})"); +} + +TEST(EffectiveConfigTest, ReportsAnUnknownFieldNameEscape) { + util::StringDiagnostic diagnostic; + FailureOr rewritten = + config::json_with_cpu_thread_budget(R"({"cpu_thread_budget\q": 1})", 4, diagnostic.emitter()); + EXPECT_TRUE(rewritten.failed()); + EXPECT_EQ(diagnostic.message(), "unknown escape code in string constant"); } TEST(EffectiveConfigTest, RejectsInputThatIsNotASimulationConfigObject) { - EXPECT_THROW((void)config::json_with_cpu_thread_budget("[1]", 1), std::runtime_error); + util::StringDiagnostic diagnostic; + FailureOr rewritten = + config::json_with_cpu_thread_budget("[1]", 1, diagnostic.emitter()); + EXPECT_TRUE(rewritten.failed()); + EXPECT_EQ(diagnostic.message(), "simulation config must be a JSON object"); } TEST(EffectiveConfigTest, WritesTheLaunchCopyBesideTheInvocationHandoff) { @@ -1214,11 +1261,14 @@ TEST(EffectiveConfigTest, WritesTheLaunchCopyBesideTheInvocationHandoff) { const std::string source = test::config_path("gfx942_cdna3.json"); const std::string source_before = config::read_config_file(source); - const std::string copy = config::write_effective_config(source, 12, getpid()); + FailureOr copy = config::write_effective_config(source, 12, getpid()); + ASSERT_TRUE(copy.succeeded()); - EXPECT_EQ(copy, rocjitsu::rpc_invocation_runtime_dir(getpid()) + "/effective_config.json"); + EXPECT_EQ(copy.value(), + rocjitsu::rpc_invocation_runtime_dir(getpid()) + "/effective_config.json"); EXPECT_EQ(config::read_config_file(source), source_before); - EXPECT_EQ(config::load_execution_thread_settings(copy, rocjitsu::kEmbeddedSchema).request.budget, + EXPECT_EQ(config::load_execution_thread_settings(copy.value(), rocjitsu::kEmbeddedSchema) + .request.budget, 12u); } @@ -1228,9 +1278,11 @@ TEST(EffectiveConfigTest, ReportsAnUnwritableRuntimeDirectory) { std::ofstream(blocked_root) << "not a directory"; test::ScopedEnvironmentVariable runtime_dir("ROCJITSU_RUNTIME_DIR", blocked_root.string()); - EXPECT_THROW( - (void)config::write_effective_config(test::config_path("gfx942_cdna3.json"), 12, getpid()), - std::runtime_error); + util::StringDiagnostic diagnostic; + FailureOr written = config::write_effective_config( + test::config_path("gfx942_cdna3.json"), 12, getpid(), diagnostic.emitter()); + EXPECT_TRUE(written.failed()); + EXPECT_THAT(diagnostic.message(), testing::HasSubstr("cannot create runtime directory")); } // This case is registered as a native CLI test because it expects the launcher @@ -1248,16 +1300,22 @@ TEST(EffectiveConfigTest, ReadsNativeLaunchConfigHandoff) { ASSERT_TRUE(std::getline(handoff, effective_path)); ASSERT_FALSE(effective_path.empty()); - EXPECT_EQ(std::filesystem::absolute(effective_path).lexically_normal(), - (invocation_path / config::kEffectiveConfigName).lexically_normal()); + EXPECT_EQ( + std::filesystem::absolute(effective_path).lexically_normal(), + std::filesystem::absolute(invocation_path / config::kEffectiveConfigName).lexically_normal()); ASSERT_TRUE(std::filesystem::exists(effective_path)); const std::string source_path = test::config_path("gfx942_cdna3.json"); const std::string source_before = config::read_config_file(source_path); - const auto settings = + const auto source_settings = + config::load_execution_thread_settings(source_path, rocjitsu::kEmbeddedSchema); + // Default config json does not specify a budget, so 0 is the pre-launch value. + EXPECT_EQ(source_settings.request.budget, 0u); + + const auto effective_settings = config::load_execution_thread_settings(effective_path, rocjitsu::kEmbeddedSchema); - EXPECT_EQ(settings.request.budget, 4u); + EXPECT_EQ(effective_settings.request.budget, 4u); EXPECT_EQ(config::read_config_file(source_path), source_before); } diff --git a/emulation/rocjitsu/tests/partitioning_test.cpp b/emulation/rocjitsu/tests/partitioning_test.cpp index 070dd12b4562..6a1de3edb54e 100644 --- a/emulation/rocjitsu/tests/partitioning_test.cpp +++ b/emulation/rocjitsu/tests/partitioning_test.cpp @@ -395,10 +395,11 @@ TEST(ExecutionThreadBudgetTest, ShippedServerChoicesAndExplicitSerial) { TEST(ExecutionThreadBudgetTest, LaunchBudgetReplanesOnlyTheConfigItRewrites) { std::string json = config_json_with_num_threads(CONFIG_PATH, 0); json.insert(json.find('{') + 1, R"("cpu_thread_budget":32,)"); - const std::string relaunched = config::json_with_cpu_thread_budget(json, 4); + FailureOr relaunched = config::json_with_cpu_thread_budget(json, 4); + ASSERT_TRUE(relaunched.succeeded()); const config::LoadedConfig loaded = - config::load_config_from_string(relaunched, rocjitsu::kEmbeddedSchema, 64); + config::load_config_from_string(relaunched.value(), rocjitsu::kEmbeddedSchema, 64); EXPECT_EQ(loaded.cpu_thread_budget, 4u); EXPECT_EQ(loaded.execution_threads.engines, 2u); EXPECT_EQ(loaded.execution_threads.dispatch, (std::vector{3})); diff --git a/emulation/rocjitsu/tools/rocjitsu/main.cpp b/emulation/rocjitsu/tools/rocjitsu/main.cpp index cf33ad787048..afd1a687531d 100644 --- a/emulation/rocjitsu/tools/rocjitsu/main.cpp +++ b/emulation/rocjitsu/tools/rocjitsu/main.cpp @@ -25,6 +25,7 @@ #include "rocjitsu/kmd/linux/amdgpu_properties.h" #include "rocjitsu/kmd/linux/rpc.h" #include "rocjitsu/vm/amdgpu/partitioning.h" +#include "util/diagnostic.h" #include "embedded_schema.h" #include "launch_preload.h" @@ -506,11 +507,13 @@ int main(int argc, char *argv[]) { // Applying the budget to a copy keeps the override inside this launch: the // config the user named is never rewritten, and simulations built from other // configs in the same process tree keep the budgets their own configs ask for. - auto config_json_for_this_launch = [&]() { + util::StringDiagnostic launch_config_diagnostic; + auto config_json_for_this_launch = [&]() -> rocjitsu::FailureOr { std::string json = rocjitsu::config::read_config_file(abs_config); - if (cpu_thread_budget) - json = rocjitsu::config::json_with_cpu_thread_budget(json, *cpu_thread_budget); - return json; + if (!cpu_thread_budget) + return json; + return rocjitsu::config::json_with_cpu_thread_budget(json, *cpu_thread_budget, + launch_config_diagnostic.emitter()); }; if (thread_budget_table) { @@ -519,8 +522,14 @@ int main(int argc, char *argv[]) { return 1; } try { + rocjitsu::FailureOr json = config_json_for_this_launch(); + if (json.failed()) { + std::cerr << std::format("rocjitsu: thread allocation failed: {}\n", + launch_config_diagnostic.message()); + return 1; + } auto settings = rocjitsu::config::load_execution_thread_settings_from_string( - config_json_for_this_launch(), rocjitsu::kEmbeddedSchema); + json.value(), rocjitsu::kEmbeddedSchema); const uint32_t host = rocjitsu::amdgpu::available_host_threads(); std::cout << "Budget | num_threads | cpu_dispatch_threads per GPU | async_helper_threads | Total\n"; @@ -561,13 +570,15 @@ int main(int argc, char *argv[]) { auto write_config_for_this_launch = [&]() { if (!cpu_thread_budget) return true; - try { - abs_config = rocjitsu::config::write_effective_config(abs_config, *cpu_thread_budget, my_pid); - } catch (const std::exception &e) { - std::cerr << std::format("rocjitsu: {}\n", e.what()); + util::StringDiagnostic diagnostic; + rocjitsu::FailureOr written = rocjitsu::config::write_effective_config( + abs_config, *cpu_thread_budget, my_pid, diagnostic.emitter()); + if (written.failed()) { + std::cerr << std::format("rocjitsu: {}\n", diagnostic.message()); cleanup_runtime_files(my_pid); return false; } + abs_config = std::move(written.value()); return true; };