Skip to content
Draft
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
2 changes: 2 additions & 0 deletions src/migraphx/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ add_library(migraphx-ep MODULE
mgx_interop.h
mgx_kernel_reg.cc
mgx_kernel_reg.h
mgx_mlss_heuristics.cc
mgx_mlss_heuristics.h
mgx_options.h
mgx_utils.cc
mgx_utils.h
Expand Down
39 changes: 13 additions & 26 deletions src/migraphx/mgx_ep.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: MIT

#include <algorithm>
#include <array>
#include <charconv>
#include <cstdio>
#include <set>
Expand All @@ -29,6 +28,7 @@
#include "mgx_ep_ctx.h"
#include "mgx_hip_graph.h"
#include "mgx_info.h"
#include "mgx_mlss_heuristics.h"
#include "mgx_utils.h"

namespace mgx_ep {
Expand Down Expand Up @@ -556,26 +556,6 @@ ExecutionProvider::ExecutionProvider(const ProviderFactory& factory, std::string
PARSE_ENV_VAR(env_var::kCpuControlFlow, cpu_control_flow_enable_);
PARSE_ENV_VAR(env_var::kModelArch, model_arch_);

// Per-architecture ops to force onto AMDMLSS.
// Add a row here to enable specific ops on additional architectures.
struct arch_mlss_ops {
std::string_view arch;
std::string_view ops; // comma-separated op names
};
static constexpr std::array<arch_mlss_ops, 2> kArchMlssOps{{
{"gfx1200", "conv"},
{"gfx1201", "conv"},
}};

for (const auto& [arch, ops] : kArchMlssOps) {
if (compute_capability_.rfind(arch, 0) == 0) {
if (!mlss_use_specific_ops_.empty()) {
mlss_use_specific_ops_ += ",";
}
mlss_use_specific_ops_ += ops;
}
}

auto compute_mode{platform::GetEnvironmentVar(env_var::kComputeMode)};
if (!compute_mode.empty()) {
std::transform(compute_mode.begin(), compute_mode.end(), compute_mode.begin(), ::tolower);
Expand Down Expand Up @@ -953,6 +933,13 @@ Ort::Status ExecutionProvider::CreateNodeComputeInfoFromGraph(const Ort::ConstGr
Ort::Graph sorted_graph{graph.GetGraphView(sorted_nodes)};
ONNX_NAMESPACE::ModelProto model_proto{};
RETURN_IF_ERROR(GraphToProto(sorted_graph, model_proto));
const auto mlss_graph_features{AnalyzeMlssGraph(model_proto)};
const std::string effective_mlss_use_specific_ops{
!mlss_use_specific_ops_.empty()
? mlss_use_specific_ops_
: (ShouldForceMlssConv(compute_capability_, mlss_graph_features) ? "conv" : "")};
const std::string effective_mxr_prefix{
mxr_prefix + hash::ToHex(std::string_view{effective_mlss_use_specific_ops}) + "-"};
std::string onnx_string;
if (!model_proto.SerializeToString(&onnx_string) || onnx_string.empty()) {
return Ort::Status{"Serializing a model proto to string failed!", ORT_EP_FAIL};
Expand Down Expand Up @@ -1000,7 +987,7 @@ Ort::Status ExecutionProvider::CreateNodeComputeInfoFromGraph(const Ort::ConstGr

fs::path mxr_path;
if (!effective_cache_dir.empty()) {
mxr_path = effective_cache_dir / (mxr_prefix + input_shapes_hash_hex + ".mxr");
mxr_path = effective_cache_dir / (effective_mxr_prefix + input_shapes_hash_hex + ".mxr");
}
loaded_from_cache = !force_recompile_ && load_compiled_program(program, mxr_path);
backend_telemetry_.loaded_from_cache = loaded_from_cache;
Expand All @@ -1012,7 +999,7 @@ Ort::Status ExecutionProvider::CreateNodeComputeInfoFromGraph(const Ort::ConstGr
migraphx::program_parameters params;
calibrate_and_quantize(program, t_, params, enable_fp16_, enable_bf16_, enable_int8_,
enable_fp8_, int8_calibration_cache_available_, dynamic_ranges_);
compile_program(program, t_, exhaustive_tune_, mlss_use_specific_ops_);
compile_program(program, t_, exhaustive_tune_, effective_mlss_use_specific_ops);
// context_enable needs this file on disk even if caching is otherwise disabled.
if (!disable_compiled_model_caching_ || context_enable_) {
save_compiled_program(program, mxr_path);
Expand All @@ -1026,7 +1013,7 @@ Ort::Status ExecutionProvider::CreateNodeComputeInfoFromGraph(const Ort::ConstGr

if (context_enable_) {
// input_shapes_hash_hex is non-empty here: the RETURN_IF above requires has_input_shape.
const fs::path ep_context_mxr_path{mxr_prefix + input_shapes_hash_hex + ".mxr"};
const fs::path ep_context_mxr_path{effective_mxr_prefix + input_shapes_hash_hex + ".mxr"};

EpContextNodeHelper ep_context_helper{*this, sorted_graph, fused_node};
RETURN_IF_ERROR(ep_context_helper.CreateEpContextNode(ep_context_mxr_path, effective_cache_dir,
Expand All @@ -1049,7 +1036,7 @@ Ort::Status ExecutionProvider::CreateNodeComputeInfoFromGraph(const Ort::ConstGr
has_input_shape,
dump_subgraphs_,
exhaustive_tune_,
mlss_use_specific_ops_,
effective_mlss_use_specific_ops,
dynamic_ranges_,
input_name_indices,
output_name_indices,
Expand All @@ -1060,7 +1047,7 @@ Ort::Status ExecutionProvider::CreateNodeComputeInfoFromGraph(const Ort::ConstGr
disable_compiled_model_caching_,
force_recompile_,
external_data_dir_,
mxr_prefix,
effective_mxr_prefix,
});

// Propagate hipGraph / dynamic-batch configuration onto the compute state.
Expand Down
145 changes: 145 additions & 0 deletions src/migraphx/mgx_mlss_heuristics.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (c) Advanced Micro Devices, Inc.
// SPDX-License-Identifier: MIT

#include "mgx_mlss_heuristics.h"

#include <algorithm>
#include <limits>
#include <string>
#include <unordered_map>
#include <vector>

namespace mgx_ep {
namespace {

std::uint64_t SaturatingMultiply(std::uint64_t left, std::uint64_t right) {
if (left == 0 || right == 0) {
return 0;
}
if (left > std::numeric_limits<std::uint64_t>::max() / right) {
return std::numeric_limits<std::uint64_t>::max();
}
return left * right;
}

template <typename Range>
std::uint64_t PositiveProduct(const Range& values, std::size_t begin = 0) {
if (begin >= static_cast<std::size_t>(values.size())) {
return 0;
}
std::uint64_t result{1};
for (std::size_t index = begin; index < static_cast<std::size_t>(values.size()); ++index) {
if (values[index] <= 0) {
return 0;
}
result = SaturatingMultiply(result,
static_cast<std::uint64_t>(values[index]));
}
return result;
}

const ONNX_NAMESPACE::AttributeProto* FindAttribute(
const ONNX_NAMESPACE::NodeProto& node, std::string_view name) {
for (const auto& attribute : node.attribute()) {
if (attribute.name() == name) {
return &attribute;
}
}
return nullptr;
}

std::uint64_t AttributeProduct(
const ONNX_NAMESPACE::NodeProto& node, std::string_view name) {
const auto* attribute{FindAttribute(node, name)};
if (attribute == nullptr || attribute->ints().empty()) {
return 1;
}
return PositiveProduct(attribute->ints());
}

} // namespace

MlssGraphFeatures AnalyzeMlssGraph(const ONNX_NAMESPACE::ModelProto& model) {
MlssGraphFeatures features{};
const auto& graph{model.graph()};
std::unordered_map<std::string, const ONNX_NAMESPACE::TensorProto*> tensors;
tensors.reserve(static_cast<std::size_t>(graph.initializer_size() + graph.node_size()));
for (const auto& initializer : graph.initializer()) {
tensors.emplace(initializer.name(), &initializer);
}
for (const auto& node : graph.node()) {
if (node.op_type() != "Constant" || node.output().empty()) {
continue;
}
if (const auto* value{FindAttribute(node, "value")}; value != nullptr && value->has_t()) {
tensors.emplace(node.output(0), &value->t());
}
}

for (const auto& input : graph.input()) {
if (!input.type().has_tensor_type() || !input.type().tensor_type().has_shape()) {
continue;
}
const auto& dimensions{input.type().tensor_type().shape().dim()};
std::vector<std::int64_t> shape;
shape.reserve(static_cast<std::size_t>(dimensions.size()));
for (const auto& dimension : dimensions) {
shape.push_back(dimension.has_dim_value() ? dimension.dim_value() : 0);
}
features.input_elements_max =
std::max(features.input_elements_max, PositiveProduct(shape));
if (shape.size() >= 2 && shape[1] > 0) {
features.input_channels_max =
std::max(features.input_channels_max, static_cast<std::uint64_t>(shape[1]));
}
if (shape.size() >= 4) {
features.input_spatial_max =
std::max(features.input_spatial_max, PositiveProduct(shape, 2));
}
}

for (const auto& node : graph.node()) {
if (node.op_type() != "Conv" || node.input_size() < 2) {
continue;
}
const auto tensor{tensors.find(node.input(1))};
if (tensor == tensors.end() || tensor->second->dims_size() < 3) {
continue;
}
const auto& weight{*tensor->second};
const std::uint64_t group = [&] {
const auto* attribute{FindAttribute(node, "group")};
return attribute != nullptr && attribute->i() > 0
? static_cast<std::uint64_t>(attribute->i())
: std::uint64_t{1};
}();
const auto weight_elements{PositiveProduct(weight.dims())};
const auto kernel_area{PositiveProduct(weight.dims(), 2)};
const auto output_channels{
static_cast<std::uint64_t>(std::max<std::int64_t>(weight.dims(0), 0))};
const auto input_channels_per_group{
static_cast<std::uint64_t>(std::max<std::int64_t>(weight.dims(1), 0))};
const auto input_channels{SaturatingMultiply(input_channels_per_group, group)};

++features.convolution_count;
features.convolution_weight_elements += weight_elements;
features.convolution_weight_elements_max =
std::max(features.convolution_weight_elements_max, weight_elements);
features.input_channels_sum += input_channels;
features.output_channels_sum += output_channels;
features.channels_max =
std::max(features.channels_max, std::max(input_channels, output_channels));
features.kernel_area_sum += kernel_area;
features.one_by_one_count += kernel_area == 1;
features.three_by_three_count += kernel_area == 9;
features.strided_count += AttributeProduct(node, "strides") > 1;
features.dilated_count += AttributeProduct(node, "dilations") > 1;
features.grouped_count += group > 1;
features.depthwise_count += group > 1 && group == input_channels;
features.fp16_count += weight.data_type() == ONNX_NAMESPACE::TensorProto_DataType_FLOAT16;
features.fp32_count += weight.data_type() == ONNX_NAMESPACE::TensorProto_DataType_FLOAT;
}
return features;
}

} // namespace mgx_ep
97 changes: 97 additions & 0 deletions src/migraphx/mgx_mlss_heuristics.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) Advanced Micro Devices, Inc.
// SPDX-License-Identifier: MIT

#pragma once

#include <cstdint>
#include <string_view>

#include "onnx/onnx_pb.h"

namespace mgx_ep {

struct MlssGraphFeatures {
std::uint64_t convolution_count{};
std::uint64_t convolution_weight_elements{};
std::uint64_t convolution_weight_elements_max{};
std::uint64_t input_channels_sum{};
std::uint64_t output_channels_sum{};
std::uint64_t channels_max{};
std::uint64_t kernel_area_sum{};
std::uint64_t one_by_one_count{};
std::uint64_t three_by_three_count{};
std::uint64_t strided_count{};
std::uint64_t dilated_count{};
std::uint64_t grouped_count{};
std::uint64_t depthwise_count{};
std::uint64_t fp16_count{};
std::uint64_t fp32_count{};
std::uint64_t input_elements_max{};
std::uint64_t input_spatial_max{};
std::uint64_t input_channels_max{};
};

MlssGraphFeatures AnalyzeMlssGraph(const ONNX_NAMESPACE::ModelProto& model);

constexpr bool IsMlssArchPrefix(std::string_view value, std::string_view prefix) {
return value.substr(0, prefix.size()) == prefix;
}

// The gfx115x thresholds were selected from grouped validation of STXH model
// measurements. Only leaves containing no measured >=5% regressions are
// enabled; unknown graph shapes remain on the MIGraphX default path.
constexpr bool ShouldForceMlssConv(std::string_view gfx, const MlssGraphFeatures& features) {
if (features.convolution_count == 0) {
return false;
}
// Preserve the existing gfx1200/gfx1201 policy.
if (IsMlssArchPrefix(gfx, "gfx1200") || IsMlssArchPrefix(gfx, "gfx1201")) {
return true;
}
if (!IsMlssArchPrefix(gfx, "gfx1150") && !IsMlssArchPrefix(gfx, "gfx1151")) {
return false;
}

const bool fp32_low_pointwise =
features.fp32_count == features.convolution_count &&
features.input_elements_max > 2 &&
features.one_by_one_count * 60 <= features.convolution_count * 19;
return fp32_low_pointwise;
}

namespace detail {

constexpr MlssGraphFeatures TestFeatures(std::uint64_t convolutions,
std::uint64_t one_by_one,
std::uint64_t fp16,
std::uint64_t fp32,
std::uint64_t input_channels,
std::uint64_t three_by_three = 0) {
MlssGraphFeatures features{};
features.convolution_count = convolutions;
features.one_by_one_count = one_by_one;
features.fp16_count = fp16;
features.fp32_count = fp32;
features.input_channels_max = input_channels;
features.input_elements_max = 1024;
features.three_by_three_count = three_by_three;
return features;
}

static_assert(ShouldForceMlssConv("gfx1151", TestFeatures(4, 1, 0, 4, 3)));
static_assert(!ShouldForceMlssConv("gfx1151", TestFeatures(3, 1, 0, 3, 3)));
static_assert(!ShouldForceMlssConv("gfx1150", TestFeatures(2, 0, 2, 0, 949)));
static_assert(!ShouldForceMlssConv("gfx1150", TestFeatures(2, 0, 2, 0, 936)));
static_assert(!ShouldForceMlssConv("gfx1151", TestFeatures(10, 4, 0, 10, 3, 6)));
static_assert(!ShouldForceMlssConv("gfx1151", TestFeatures(10, 4, 0, 10, 3, 5)));
static_assert(!ShouldForceMlssConv("gfx1151", TestFeatures(10, 0, 5, 5, 1024)));
static_assert([] {
auto features = TestFeatures(4, 1, 0, 4, 3);
features.input_elements_max = 0;
return !ShouldForceMlssConv("gfx1151", features);
}());
static_assert(ShouldForceMlssConv("gfx1201", TestFeatures(1, 0, 0, 0, 0)));
static_assert(!ShouldForceMlssConv("gfx1100", TestFeatures(10, 0, 0, 0, 0)));

} // namespace detail
} // namespace mgx_ep