Files
TheRock/rocm-libraries/projects/hipdnn/frontend/include/hipdnn_frontend/autotune/AutotuneFileWriter.hpp
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

503 lines
19 KiB
C++

// Copyright © Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
/**
* @file AutotuneFileWriter.hpp
* @brief JSON file writer for persisting autotuning results
*
* Writes autotune results in heuristic config JSON format, allowing the
* results to be loaded on subsequent runs via HIPDNN_HEUR_CONFIG_PATH.
* Supports append/replace semantics and atomic file writes via
* temp file + rename.
*/
#pragma once
#ifndef HIPDNN_FRONTEND_SKIP_JSON_LIB
#include <hipdnn_data_sdk/detail/AutotuneConfigNames.hpp>
#include <hipdnn_frontend/Error.hpp>
#include <hipdnn_frontend/Logging.hpp>
#include <hipdnn_frontend/autotune/AutotuneTypes.hpp>
#include <hipdnn_frontend/knob/KnobSetting.hpp>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <ctime>
#include <exception>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace hipdnn_frontend
{
namespace autotune
{
namespace detail
{
namespace config_json = hipdnn_data_sdk::detail::autotune_config::json;
namespace config_version = hipdnn_data_sdk::detail::autotune_config::version;
using Criteria = std::vector<std::pair<std::string, int64_t>>;
inline nlohmann::json criteriaToJson(const Criteria& criteria)
{
nlohmann::json result = nlohmann::json::object();
for(const auto& [key, value] : criteria)
{
result[key] = value;
}
return result;
}
inline nlohmann::json criteriaOrEmpty(const nlohmann::json& entry)
{
if(entry.contains(config_json::CRITERIA) && entry[config_json::CRITERIA].is_object())
{
return entry[config_json::CRITERIA];
}
return nlohmann::json::object();
}
enum class TensorIdValidationResult
{
HIPDNN_AUTOTUNE_TENSOR_IDS_VALID,
HIPDNN_AUTOTUNE_TENSOR_ID_MISSING,
HIPDNN_AUTOTUNE_TENSOR_ID_DUPLICATE
};
inline TensorIdValidationResult validateTensorIds(size_t tensorCount,
const std::vector<std::string>& tensorIds)
{
if(tensorCount == 0 || tensorIds.size() < tensorCount)
{
return TensorIdValidationResult::HIPDNN_AUTOTUNE_TENSOR_ID_MISSING;
}
std::unordered_set<std::string_view> seen;
seen.reserve(tensorCount);
for(size_t i = 0; i < tensorCount; ++i)
{
const auto& tensorId = tensorIds[i];
if(tensorId.empty())
{
return TensorIdValidationResult::HIPDNN_AUTOTUNE_TENSOR_ID_MISSING;
}
if(!seen.emplace(tensorId.data(), tensorId.size()).second)
{
return TensorIdValidationResult::HIPDNN_AUTOTUNE_TENSOR_ID_DUPLICATE;
}
}
return TensorIdValidationResult::HIPDNN_AUTOTUNE_TENSOR_IDS_VALID;
}
inline std::optional<std::string_view> tensorIdOf(const nlohmann::json& tensor)
{
if(!tensor.contains(config_json::TENSOR_ID) || !tensor[config_json::TENSOR_ID].is_string())
{
return std::nullopt;
}
const auto& tensorId = tensor[config_json::TENSOR_ID].get_ref<const std::string&>();
return std::string_view(tensorId.data(), tensorId.size());
}
inline nlohmann::json tensorEntryWithoutId(nlohmann::json entry)
{
entry.erase(config_json::TENSOR_ID);
return entry;
}
inline bool tensorsMatchByIdIgnoringOrder(const nlohmann::json& existing,
const nlohmann::json& replacement)
{
std::unordered_map<std::string_view, const nlohmann::json*> existingById;
existingById.reserve(existing.size());
for(const auto& existingTensor : existing)
{
const auto existingId = tensorIdOf(existingTensor);
if(!existingId.has_value() || !existingById.emplace(*existingId, &existingTensor).second)
{
return false;
}
}
return std::all_of(
replacement.begin(), replacement.end(), [&](const nlohmann::json& replacementTensor) {
const auto replacementId = tensorIdOf(replacementTensor);
if(!replacementId.has_value())
{
return false;
}
const auto existingIt = existingById.find(*replacementId);
return existingIt != existingById.end()
&& tensorEntryWithoutId(*existingIt->second)
== tensorEntryWithoutId(replacementTensor);
});
}
inline bool tensorSignaturesMatch(const nlohmann::json& existing, const nlohmann::json& replacement)
{
return existing.is_array() && replacement.is_array() && existing.size() == replacement.size()
&& tensorsMatchByIdIgnoringOrder(existing, replacement);
}
// Build a single JSON engine_overrides entry from an AutotuneResult.
// tensorDims/tensorStrides hold one vector<int64_t> per tensor. opName is the
// operation name for the entry (e.g. "conv_fprop"). Returns a nlohmann::json
// object representing the entry.
inline nlohmann::json buildOverrideEntry(const AutotuneResult& result,
const std::string& opName,
const std::vector<std::vector<int64_t>>& tensorDims,
const std::vector<std::vector<int64_t>>& tensorStrides,
const Criteria& criteria = {},
const std::vector<std::string>& tensorIds = {})
{
nlohmann::json entry;
entry[config_json::OP] = opName;
if(!criteria.empty())
{
entry[config_json::CRITERIA] = criteriaToJson(criteria);
}
entry[config_json::ENGINE_NAME] = result.engineName;
// Tensor patterns (dimensions and strides)
nlohmann::json tensors = nlohmann::json::array();
for(size_t i = 0; i < tensorDims.size(); ++i)
{
nlohmann::json t;
t[config_json::DIM] = tensorDims[i];
if(i < tensorIds.size() && !tensorIds[i].empty())
{
t[config_json::TENSOR_ID] = tensorIds[i];
}
if(i < tensorStrides.size() && !tensorStrides[i].empty())
{
t[config_json::STRIDE] = tensorStrides[i];
}
tensors.push_back(std::move(t));
}
entry[config_json::TENSORS] = std::move(tensors);
// Autotune metadata
nlohmann::json metadata;
metadata["min_time_ms"] = result.minTimeMs;
metadata["avg_time_ms"] = result.avgTimeMs;
metadata["stddev_ms"] = result.stddevMs;
metadata["iterations_run"] = result.iterationsRun;
metadata["mode"] = tuneModeToLowerString(result.modeUsed);
metadata["strategy"] = strategyToLowerString(result.strategyUsed);
metadata["rank"] = result.rank;
metadata["workspace_size"] = result.workspaceSize;
// Timestamp in ISO 8601 format, captured at write time
{
const auto now = std::chrono::system_clock::now();
const auto timeT = std::chrono::system_clock::to_time_t(now);
std::tm utcTm{};
#if defined(_WIN32)
gmtime_s(&utcTm, &timeT);
#else
gmtime_r(&timeT, &utcTm);
#endif
std::ostringstream oss;
oss << std::put_time(&utcTm, "%Y-%m-%dT%H:%M:%SZ");
metadata["timestamp"] = oss.str();
}
metadata["supports_exhaustive"] = result.supportsExhaustive;
metadata["ran_exhaustive"] = result.ranExhaustive;
metadata["exhaustive_not_run_reason"] = result.exhaustiveNotRunReason;
if(result.strategyUsed == AutotuneStrategy::RUN_UNTIL_STABLE)
{
metadata["converged"] = result.converged;
}
// Knob settings (informational; nested under autotune_metadata, omitted when empty)
if(!result.knobSettings.empty())
{
nlohmann::json knobs = nlohmann::json::array();
for(const auto& setting : result.knobSettings)
{
// KnobSetting -> JSON via the ADL-found to_json
knobs.push_back(setting);
}
metadata["knobs"] = std::move(knobs);
}
entry["autotune_metadata"] = std::move(metadata);
return entry;
}
// Write autotuning results to a JSON file in heuristic config format.
//
// The file format uses the versioned engine_overrides JSON schema. A single
// rank-0 winner entry looks like:
// {
// "version": 2,
// "engine_overrides": [
// {
// "op": "conv_fprop",
// "engine_name": "MIOPEN_ENGINE",
// "criteria": { "pointwise_mode": 1 },
// "tensors": [
// { "tensor_id": "X", "dim": [1, 3, 224, 224], "stride": [150528, 50176, 224, 1] },
// { "tensor_id": "W", "dim": [64, 3, 7, 7], "stride": [147, 49, 7, 1] }
// ],
// "autotune_metadata": {
// "rank": 0,
// "min_time_ms": 1.23,
// "avg_time_ms": 1.25,
// "stddev_ms": 0.02,
// "workspace_size": 16777216,
// "mode": "exhaustive",
// "supports_exhaustive": true,
// "ran_exhaustive": true,
// "exhaustive_not_run_reason": "",
// "strategy": "run_until_stable",
// "iterations_run": 37,
// "converged": true,
// "timestamp": "2026-04-21T10:30:00Z",
// "knobs": [ { "knob_id": "SPLIT_K", "type": "int", "value": 2 } ]
// }
// }
// ]
// }
//
// op is the canonical core-operation string (conv_fprop, conv_dgrad, matmul,
// sdpa_fwd, batchnorm_training, layernorm, pointwise, etc.). criteria is
// present only when the graph supplies discriminating criteria. Per tensor,
// tensor_id is required for the v2 named-id format and stride is present when
// strides are supplied. In autotune_metadata, converged is present only for
// the run_until_stable strategy, and knobs is omitted entirely for
// default-knob entries.
//
// Writes a single entry: the rank-0 winner (the first succeeded result in the
// rank-ordered input). If no result succeeded, nothing is written and OK is
// returned. When deleteAllExisting is true the file starts empty; when false,
// existing entries are loaded and the one matching (op, criteria, tensors) is
// replaced. Returns an Error on I/O failure.
inline Error writeAutotuneResults(const std::filesystem::path& filePath,
const std::string& opName,
const std::vector<AutotuneResult>& results,
bool deleteAllExisting,
const std::vector<std::vector<int64_t>>& tensorDims,
const std::vector<std::vector<int64_t>>& tensorStrides,
const Criteria& criteria = {},
const std::vector<std::string>& tensorIds = {})
{
try
{
nlohmann::json root;
bool loadedExistingConfig = false;
// Load existing file content unless we're overwriting it all.
if(!deleteAllExisting && std::filesystem::exists(filePath))
{
try
{
std::ifstream existingFile(filePath);
if(existingFile.is_open())
{
root = nlohmann::json::parse(existingFile);
loadedExistingConfig = true;
}
}
catch(const nlohmann::json::exception& e)
{
// The existing file is corrupt. Attempt to move aside.
const auto epochSeconds = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
std::filesystem::path corruptPath = filePath;
corruptPath += ".corrupt-" + std::to_string(epochSeconds);
std::error_code renameEc;
std::filesystem::rename(filePath, corruptPath, renameEc);
if(renameEc)
{
HIPDNN_FE_LOG_ERROR("autotune: existing config file "
<< filePath << " contains invalid JSON (" << e.what()
<< ") and could not be moved aside (" << renameEc.message()
<< "); overwriting it with new results");
}
else
{
HIPDNN_FE_LOG_WARN(
"autotune: existing config file "
<< filePath << " contains invalid JSON and could not be read: " << e.what()
<< ". Moved aside to " << corruptPath << "; writing fresh results.");
}
root = nlohmann::json::object();
}
}
if(loadedExistingConfig)
{
if(!root.is_object())
{
return {ErrorCode::INVALID_VALUE,
"AutotuneFileWriter: existing config file is not a versioned object"};
}
if(!root.contains(config_json::VERSION))
{
return {ErrorCode::INVALID_VALUE,
"AutotuneFileWriter: refusing to update legacy autotune config file"};
}
const auto& existingConfigVersion = root.at(config_json::VERSION);
if(!existingConfigVersion.is_number_integer())
{
return {ErrorCode::INVALID_VALUE,
"AutotuneFileWriter: existing config version is not an integer"};
}
if(existingConfigVersion.get<int64_t>() != config_version::CURRENT)
{
return {ErrorCode::INVALID_VALUE,
"AutotuneFileWriter: refusing to update non-current autotune config file"};
}
}
else
{
root = nlohmann::json::object();
}
if(!root.contains(config_json::ENGINE_OVERRIDES)
|| !root[config_json::ENGINE_OVERRIDES].is_array())
{
root[config_json::ENGINE_OVERRIDES] = nlohmann::json::array();
}
// Build the single new entry from the rank-0 winner (the first succeeded
// result). Only one entry per (op, tensor shape) is ever produced.
const auto rank0Result
= std::find_if(results.begin(), results.end(), [](const AutotuneResult& result) {
return result.succeeded;
});
if(rank0Result == results.end())
{
HIPDNN_FE_LOG_WARN("autotune: no successful results to write");
return {ErrorCode::OK, ""};
}
switch(validateTensorIds(tensorDims.size(), tensorIds))
{
case TensorIdValidationResult::HIPDNN_AUTOTUNE_TENSOR_IDS_VALID:
break;
case TensorIdValidationResult::HIPDNN_AUTOTUNE_TENSOR_ID_MISSING:
return {ErrorCode::INVALID_VALUE,
"AutotuneFileWriter: tensor IDs are required for versioned config files"};
case TensorIdValidationResult::HIPDNN_AUTOTUNE_TENSOR_ID_DUPLICATE:
return {ErrorCode::INVALID_VALUE,
"AutotuneFileWriter: tensor IDs must be unique for versioned config files"};
default:
return {ErrorCode::INVALID_VALUE,
"AutotuneFileWriter: unknown tensor ID validation result"};
}
const auto newEntry = buildOverrideEntry(
*rank0Result, opName, tensorDims, tensorStrides, criteria, tensorIds);
root[config_json::VERSION] = config_version::CURRENT;
// Remove the pre-existing entry that matches the new entry's (op, tensors)
// signature, then append the new entry. The knob configuration is
// unconditionally replaced.
// Replace-match is exact (operation, tensor shape) only. It does NOT match the
// reader's -1 wildcard patterns (TensorPattern::matches).
auto& overrides = root[config_json::ENGINE_OVERRIDES];
if(!overrides.empty())
{
overrides.erase(
std::remove_if(overrides.begin(),
overrides.end(),
[&](const nlohmann::json& existing) {
return existing.contains(config_json::OP)
&& existing[config_json::OP] == newEntry[config_json::OP]
&& criteriaOrEmpty(existing) == criteriaOrEmpty(newEntry)
&& existing.contains(config_json::TENSORS)
&& tensorSignaturesMatch(existing[config_json::TENSORS],
newEntry[config_json::TENSORS]);
}),
overrides.end());
}
overrides.push_back(newEntry);
// Atomic write: write to temp file, then rename.
// If the process crashes or power is lost between creating the temp file
// and completing the rename, the temp file (filePath + ".tmp") is left
// on disk. This is understood and accepted: the alternative (deleting
// the temp file on failure) risks silently losing the only copy of the
// data if the rename target was already removed.
std::filesystem::path tempPath = filePath;
tempPath += ".tmp";
{
std::ofstream outFile(tempPath);
if(!outFile.is_open())
{
return {ErrorCode::INVALID_VALUE,
"AutotuneFileWriter: cannot open temp file for writing: "
+ tempPath.string()};
}
outFile << root.dump(2) << '\n';
outFile.flush();
if(!outFile.good())
{
return {ErrorCode::INVALID_VALUE,
"AutotuneFileWriter: write to temp file failed: " + tempPath.string()};
}
}
// Rename temp file to target (atomic on POSIX, best-effort on Windows)
std::error_code ec;
std::filesystem::rename(tempPath, filePath, ec);
if(ec.value() != 0)
{
// Fallback: remove the existing target, then retry the rename. If the
// remove itself fails there is no point retrying - report it.
std::filesystem::remove(filePath, ec);
if(ec.value() != 0)
{
return {ErrorCode::INVALID_VALUE,
"AutotuneFileWriter: failed to remove existing file " + filePath.string()
+ " before rename: " + ec.message()};
}
std::filesystem::rename(tempPath, filePath, ec);
if(ec.value() != 0)
{
return {ErrorCode::INVALID_VALUE,
"AutotuneFileWriter: failed to rename temp file to " + filePath.string()
+ ": " + ec.message()};
}
}
HIPDNN_FE_LOG_INFO("autotune: wrote 1 entry to " << filePath);
return {ErrorCode::OK, ""};
}
catch(const std::exception& e)
{
return {ErrorCode::INVALID_VALUE, e.what()};
}
}
} // namespace detail
} // namespace autotune
} // namespace hipdnn_frontend
#endif // HIPDNN_FRONTEND_SKIP_JSON_LIB