From aa85dddb19f71a4d9786151b0a55e24b0b3f6ca0 Mon Sep 17 00:00:00 2001 From: Luke Hutton Date: Wed, 27 Mar 2024 15:53:46 +0000 Subject: [PATCH] [Target] Use LLVM target parser for determining Arm(R) A-Profile Architecture features (#16425) Currently, target features are determined by a set of fixed checks on the target string. This works well for checking support of a small number of simple features, but it doesn't scale. Some problems include: - There are many non-trivial conditions for which a feature may(not) be available. It is easy to miss these with the current implementation. - The inclusion of some features in a target string can imply other features. For example, "+sve" implies "+neon". This currently isn't taken into account. - The tests in tests/cpp/target/parsers/aprofile_test.c suggest that targets such as "llvm -mcpu=cortex-a+neon" and "llvm -mattr=+noneon" are supported target strings. The features will be correctly parsed in TVM, however, they are not valid in LLVM. Therefore, it's possible that TVM and LLVM have different understanding of the features available. This commit uses the more robust LLVM target parser to determine support for the features in TVM. It leverages previous infrastructure added to TVM for obtaining a list of all supported features given an input target, and uses this to check the existance of certain features we're interested in. It should be trivial to grow this list over time. As a result of this change, the problems mentioned above are solved. In the current form, this commit drops support for target strings such as "llvm -mcpu=cortex-a+neon" and "llvm -mattr=+noneon". A scan of the codebase suggests this functionality is not in use (only in test cases). Should we feel the need to support them, or have a smoother migration for downstream users of TVM we can add a translator to the parser to convert these into LLVM compatible targets. --- python/tvm/target/codegen.py | 3 +- src/target/llvm/llvm_instance.cc | 95 +++---- src/target/llvm/llvm_instance.h | 13 +- src/target/llvm/llvm_module.cc | 7 +- src/target/parsers/aprofile.cc | 88 +++--- tests/cpp/target/parsers/aprofile_test.cc | 263 +++++++++++------- .../strategy/test_select_implementation.py | 12 +- .../python/target/test_llvm_features_info.py | 24 +- 8 files changed, 282 insertions(+), 223 deletions(-) diff --git a/python/tvm/target/codegen.py b/python/tvm/target/codegen.py index b2a92c2ca21b4..82385e3b684f1 100644 --- a/python/tvm/target/codegen.py +++ b/python/tvm/target/codegen.py @@ -183,7 +183,8 @@ def llvm_get_cpu_features(target=None): List of available CPU features. """ assert isinstance(target, Target) or target is None - return _ffi_api.llvm_get_cpu_features(target) + feature_map = _ffi_api.llvm_get_cpu_features(target) + return set(feature_map.keys()) def llvm_cpu_has_features(cpu_features, target=None): diff --git a/src/target/llvm/llvm_instance.cc b/src/target/llvm/llvm_instance.cc index a1359b7850a4d..b3f55594a25f0 100644 --- a/src/target/llvm/llvm_instance.cc +++ b/src/target/llvm/llvm_instance.cc @@ -199,32 +199,37 @@ std::ostream& operator<<(std::ostream& os, const LLVMTargetInfo::Option& opt) { return os; } -LLVMTargetInfo::LLVMTargetInfo(LLVMInstance& instance, const Target& target) { - triple_ = target->GetAttr("mtriple").value_or("default"); +LLVMTargetInfo::LLVMTargetInfo(LLVMInstance& instance, const Target& target) + : LLVMTargetInfo(instance, target->Export()) {} +LLVMTargetInfo::LLVMTargetInfo(LLVMInstance& instance, const TargetJSON& target) { + triple_ = Downcast(target.Get("mtriple").value_or(String("default"))); if (triple_.empty() || triple_ == "default") { triple_ = llvm::sys::getDefaultTargetTriple(); } - cpu_ = target->GetAttr("mcpu").value_or(defaults::cpu); + cpu_ = Downcast(target.Get("mcpu").value_or(String(defaults::cpu))); - if (const Optional>& v = target->GetAttr>("mattr")) { + if (const auto& v = Downcast>>(target.Get("mattr"))) { for (const String& s : v.value()) { attrs_.push_back(s); } } // llvm module target - if (target->kind->name == "llvm") { + if (Downcast(target.Get("kind")) == "llvm") { // legalize -mcpu with the target -mtriple auto arches = GetAllLLVMTargetArches(); bool has_arch = std::any_of(arches.begin(), arches.end(), [&](const auto& var) { return var == cpu_; }); if (!has_arch) { - LOG(FATAL) << "LLVM cpu architecture `-mcpu=" << cpu_ - << "` is not valid in `-mtriple=" << triple_ << "`"; + // Flag an error, but don't abort. This mimicks the behaviour of 'llc' to + // give the code a chance to run with a less-specific target. + LOG(ERROR) << "LLVM cpu architecture `-mcpu=" << cpu_ + << "` is not valid in `-mtriple=" << triple_ << "`" + << ", using default `-mcpu=" << String(defaults::cpu) << "`"; } } - if (const Optional>& v = target->GetAttr>("cl-opt")) { + if (const auto& v = Downcast>>(target.Get("cl-opt"))) { llvm::StringMap& options = llvm::cl::getRegisteredOptions(); bool parse_error = false; for (const String& s : v.value()) { @@ -245,7 +250,7 @@ LLVMTargetInfo::LLVMTargetInfo(LLVMInstance& instance, const Target& target) { } llvm::FloatABI::ABIType float_abi = llvm::FloatABI::Default; - if (const Optional& v = target->GetAttr("mfloat-abi")) { + if (const auto& v = Downcast>(target.Get("mfloat-abi"))) { String value = v.value(); if (value == "hard") { float_abi = llvm::FloatABI::Hard; @@ -257,7 +262,7 @@ LLVMTargetInfo::LLVMTargetInfo(LLVMInstance& instance, const Target& target) { } // LLVM JIT engine options - if (const Optional& v = target->GetAttr("jit")) { + if (const auto& v = Downcast>(target.Get("jit"))) { String value = v.value(); if ((value == "mcjit") || (value == "orcjit")) { jit_engine_ = value; @@ -283,14 +288,14 @@ LLVMTargetInfo::LLVMTargetInfo(LLVMInstance& instance, const Target& target) { target_options_.NoInfsFPMath = false; target_options_.NoNaNsFPMath = true; target_options_.FloatABIType = float_abi; - if (const Optional& v = target->GetAttr("mabi")) { - target_options_.MCOptions.ABIName = v.value(); + if (target.find("mabi") != target.end()) { + target_options_.MCOptions.ABIName = Downcast(target.Get("mabi")); } - auto maybe_level = target->GetAttr("opt-level"); + auto maybe_level = Downcast(target.Get("opt-level")); #if TVM_LLVM_VERSION <= 170 if (maybe_level.defined()) { - int level = maybe_level.value()->value; + int level = maybe_level->value; if (level <= 0) { opt_level_ = llvm::CodeGenOpt::None; } else if (level == 1) { @@ -327,7 +332,7 @@ LLVMTargetInfo::LLVMTargetInfo(LLVMInstance& instance, const Target& target) { // Fast math options auto GetBoolFlag = [&target](llvm::StringRef flag) -> bool { - return target->GetAttr(flag.str()).value_or(Bool(false)); + return Downcast(target.Get(flag.str()).value_or(Bool(false))); }; if (GetBoolFlag("fast-math")) { #if TVM_LLVM_VERSION >= 60 @@ -381,41 +386,21 @@ static const llvm::Target* CreateLLVMTargetInstance(const std::string triple, return llvm_instance; } -static llvm::TargetMachine* CreateLLVMTargetMachine( +static std::unique_ptr CreateLLVMTargetMachine( const llvm::Target* llvm_instance, const std::string& triple, const std::string& cpu, - const std::string& features, const llvm::TargetOptions& target_options, - const llvm::Reloc::Model& reloc_model, const llvm::CodeModel::Model& code_model, + const std::string& features, const llvm::TargetOptions& target_options = {}, + const llvm::Reloc::Model& reloc_model = llvm::Reloc::Static, + const llvm::CodeModel::Model& code_model = llvm::CodeModel::Small, #if TVM_LLVM_VERSION <= 170 - const llvm::CodeGenOpt::Level& opt_level) { + const llvm::CodeGenOpt::Level& opt_level = llvm::CodeGenOpt::Level(0)) { #else - const llvm::CodeGenOptLevel& opt_level) { + const llvm::CodeGenOptLevel& opt_level = llvm::CodeGenOptLevel(0)) { #endif llvm::TargetMachine* tm = llvm_instance->createTargetMachine( triple, cpu, features, target_options, reloc_model, code_model, opt_level); ICHECK(tm != nullptr); - return tm; -} - -static const llvm::MCSubtargetInfo* GetLLVMSubtargetInfo(const std::string& triple, - const std::string& cpu_name, - const std::string& feats) { - // create a LLVM instance - auto llvm_instance = CreateLLVMTargetInstance(triple, true); - // create a target machine - // required minimum: llvm::InitializeAllTargetMCs() - llvm::TargetOptions target_options; - auto tm = CreateLLVMTargetMachine(llvm_instance, triple, cpu_name, feats, target_options, - llvm::Reloc::Static, llvm::CodeModel::Small, -#if TVM_LLVM_VERSION <= 170 - llvm::CodeGenOpt::Level(0)); -#else - llvm::CodeGenOptLevel(0)); -#endif - // create subtarget info module - const llvm::MCSubtargetInfo* MCInfo = tm->getMCSubtargetInfo(); - - return MCInfo; + return std::unique_ptr(tm); } llvm::TargetMachine* LLVMTargetInfo::GetOrCreateTargetMachine(bool allow_missing) { @@ -423,10 +408,9 @@ llvm::TargetMachine* LLVMTargetInfo::GetOrCreateTargetMachine(bool allow_missing std::string error; if (const llvm::Target* llvm_instance = CreateLLVMTargetInstance(triple_, allow_missing)) { - llvm::TargetMachine* tm = + target_machine_ = CreateLLVMTargetMachine(llvm_instance, triple_, cpu_, GetTargetFeatureString(), target_options_, reloc_model_, code_model_, opt_level_); - target_machine_ = std::unique_ptr(tm); } ICHECK(target_machine_ != nullptr); return target_machine_.get(); @@ -832,7 +816,11 @@ const Array LLVMTargetInfo::GetAllLLVMTargets() const { const Array LLVMTargetInfo::GetAllLLVMTargetArches() const { Array cpu_arches; // get the subtarget info module - const auto MCInfo = GetLLVMSubtargetInfo(triple_, "", ""); + auto llvm_instance = CreateLLVMTargetInstance(triple_, true); + std::unique_ptr target_machine = + CreateLLVMTargetMachine(llvm_instance, triple_, "", ""); + const auto MCInfo = target_machine->getMCSubtargetInfo(); + if (!MCInfo) { return cpu_arches; } @@ -850,13 +838,17 @@ const Array LLVMTargetInfo::GetAllLLVMTargetArches() const { return cpu_arches; } -const Array LLVMTargetInfo::GetAllLLVMCpuFeatures() const { +const Map LLVMTargetInfo::GetAllLLVMCpuFeatures() const { std::string feats = ""; for (const auto& attr : attrs_) { feats += feats.empty() ? attr : ("," + attr); } // get the subtarget info module - const auto MCInfo = GetLLVMSubtargetInfo(triple_, cpu_.c_str(), feats); + auto llvm_instance = CreateLLVMTargetInstance(triple_, true); + std::unique_ptr target_machine = + CreateLLVMTargetMachine(llvm_instance, triple_, cpu_.c_str(), feats); + const auto MCInfo = target_machine->getMCSubtargetInfo(); + // get all features for CPU llvm::ArrayRef llvm_features = #if TVM_LLVM_VERSION < 180 @@ -864,10 +856,11 @@ const Array LLVMTargetInfo::GetAllLLVMCpuFeatures() const { #else MCInfo->getAllProcessorFeatures(); #endif - Array cpu_features; + // TVM doesn't have an FFI friendly Set, so use a Map instead for now + Map cpu_features; for (const auto& feat : llvm_features) { if (MCInfo->checkFeatures("+" + std::string(feat.Key))) { - cpu_features.push_back(feat.Key); + cpu_features.Set(feat.Key, ""); } } @@ -877,9 +870,7 @@ const Array LLVMTargetInfo::GetAllLLVMCpuFeatures() const { const bool LLVMTargetInfo::TargetHasCPUFeature(const std::string& feature) const { // lookup features for `-mcpu` auto feats = GetAllLLVMCpuFeatures(); - bool has_feature = - std::any_of(feats.begin(), feats.end(), [&](const auto& var) { return var == feature; }); - + bool has_feature = feats.find(feature) != feats.end(); return has_feature; } diff --git a/src/target/llvm/llvm_instance.h b/src/target/llvm/llvm_instance.h index f3948b7a01d29..fd63140a0b37d 100644 --- a/src/target/llvm/llvm_instance.h +++ b/src/target/llvm/llvm_instance.h @@ -156,6 +156,14 @@ class LLVMTargetInfo { */ // NOLINTNEXTLINE(runtime/references) LLVMTargetInfo(LLVMInstance& scope, const std::string& target_str); + /*! + * \brief Constructs LLVMTargetInfo from `Target` + * \param scope LLVMInstance object + * \param target TVM JSON Target object for target "llvm" + */ + // NOLINTNEXTLINE(runtime/references) + LLVMTargetInfo(LLVMInstance& instance, const TargetJSON& target); + /*! * \brief Destroys LLVMTargetInfo object */ @@ -290,11 +298,12 @@ class LLVMTargetInfo { /*! * \brief Get all CPU features from target - * \return list with all valid cpu features + * \return Map with all valid cpu features as keys and empty string as value. The Map + * is intended to be used as a Set, which TVM does not currently support. * \note The features are fetched from the LLVM backend using the target `-mtriple` * and the `-mcpu` architecture, but also consider the `-mattr` attributes. */ - const Array GetAllLLVMCpuFeatures() const; + const Map GetAllLLVMCpuFeatures() const; /*! * \brief Check the target if has a specific cpu feature diff --git a/src/target/llvm/llvm_module.cc b/src/target/llvm/llvm_module.cc index c332314a3e6c7..baa68feedfa21 100644 --- a/src/target/llvm/llvm_module.cc +++ b/src/target/llvm/llvm_module.cc @@ -697,12 +697,12 @@ TVM_REGISTER_GLOBAL("target.llvm_get_cpu_archlist") }); TVM_REGISTER_GLOBAL("target.llvm_get_cpu_features") - .set_body_typed([](const Target& target) -> Array { + .set_body_typed([](const Target& target) -> Map { auto use_target = target.defined() ? target : Target::Current(false); // ignore non "llvm" target if (target.defined()) { if (target->kind->name != "llvm") { - return Array{}; + return {}; } } auto llvm_instance = std::make_unique(); @@ -722,8 +722,7 @@ TVM_REGISTER_GLOBAL("target.llvm_cpu_has_feature") auto llvm_instance = std::make_unique(); LLVMTargetInfo llvm_backend(*llvm_instance, use_target); auto cpu_features = llvm_backend.GetAllLLVMCpuFeatures(); - bool has_feature = std::any_of(cpu_features.begin(), cpu_features.end(), - [&](auto& var) { return var == feature; }); + bool has_feature = cpu_features.find(feature) != cpu_features.end(); return has_feature; }); diff --git a/src/target/parsers/aprofile.cc b/src/target/parsers/aprofile.cc index 622ec5cc3fbf1..907e0cae72d27 100644 --- a/src/target/parsers/aprofile.cc +++ b/src/target/parsers/aprofile.cc @@ -24,9 +24,11 @@ #include "aprofile.h" +#include #include #include "../../support/utils.h" +#include "../llvm/llvm_instance.h" namespace tvm { namespace target { @@ -52,33 +54,6 @@ double GetArchVersion(Optional> attr) { return GetArchVersion(attr.value()); } -static inline bool HasFlag(String attr, std::string flag) { - std::string attr_str = attr; - return attr_str.find(flag) != std::string::npos; -} - -static inline bool HasFlag(Optional attr, std::string flag) { - if (!attr) { - return false; - } - return HasFlag(attr.value(), flag); -} - -static inline bool HasFlag(Optional> attr, std::string flag) { - if (!attr) { - return false; - } - Array attr_array = attr.value(); - - auto matching_attr = std::find_if(attr_array.begin(), attr_array.end(), - [flag](String attr_str) { return HasFlag(attr_str, flag); }); - return matching_attr != attr_array.end(); -} - -static bool HasFlag(Optional mcpu, Optional> mattr, std::string flag) { - return HasFlag(mcpu, flag) || HasFlag(mattr, flag); -} - bool IsAArch32(Optional mtriple, Optional mcpu) { if (mtriple) { bool is_mprofile = mcpu && support::StartsWith(mcpu.value(), "cortex-m"); @@ -101,39 +76,46 @@ bool IsArch(TargetJSON attrs) { return IsAArch32(mtriple, mcpu) || IsAArch64(mtriple); } -static TargetFeatures GetFeatures(TargetJSON target) { - Optional mcpu = Downcast>(target.Get("mcpu")); - Optional mtriple = Downcast>(target.Get("mtriple")); - Optional> mattr = Downcast>>(target.Get("mattr")); +bool CheckContains(Array array, String predicate) { + return std::any_of(array.begin(), array.end(), [&](String var) { return var == predicate; }); +} - const double arch_version = GetArchVersion(mattr); +static TargetFeatures GetFeatures(TargetJSON target) { +#ifdef TVM_LLVM_VERSION + String kind = Downcast(target.Get("kind")); + ICHECK_EQ(kind, "llvm") << "Expected target kind 'llvm', but got '" << kind << "'"; - const bool is_aarch64 = IsAArch64(mtriple); + Optional mtriple = Downcast>(target.Get("mtriple")); + Optional mcpu = Downcast>(target.Get("mcpu")); - const bool simd_flag = HasFlag(mcpu, mattr, "+neon") || HasFlag(mcpu, mattr, "+simd"); - const bool has_asimd = is_aarch64 || simd_flag; - const bool has_sve = HasFlag(mcpu, mattr, "+sve"); + // Check that LLVM has been compiled with the correct target support + auto llvm_instance = std::make_unique(); + codegen::LLVMTargetInfo llvm_backend(*llvm_instance, {{"kind", String("llvm")}}); + Array targets = llvm_backend.GetAllLLVMTargets(); + if ((IsAArch64(mtriple) && !CheckContains(targets, "aarch64")) || + (IsAArch32(mtriple, mcpu) && !CheckContains(targets, "arm"))) { + LOG(WARNING) << "Cannot parse target features. LLVM was not compiled with support for " + "Arm(R)-based targets."; + return {}; + } - const bool i8mm_flag = HasFlag(mcpu, mattr, "+i8mm"); - const bool i8mm_disable = HasFlag(mcpu, mattr, "+noi8mm"); - const bool i8mm_default = arch_version >= 8.6; - const bool i8mm_support = arch_version >= 8.2 && arch_version <= 8.5; - const bool has_i8mm = (i8mm_default && !i8mm_disable) || (i8mm_support && i8mm_flag); + codegen::LLVMTargetInfo llvm_target(*llvm_instance, target); + Map features = llvm_target.GetAllLLVMCpuFeatures(); - const bool dotprod_flag = HasFlag(mcpu, mattr, "+dotprod"); - const bool dotprod_disable = HasFlag(mcpu, mattr, "+nodotprod"); - const bool dotprod_default = arch_version >= 8.4; - const bool dotprod_support = arch_version >= 8.2 && arch_version <= 8.3; - const bool has_dotprod = - (dotprod_default && !dotprod_disable) || (dotprod_support && dotprod_flag); + auto has_feature = [features](const String& feature) { + return features.find(feature) != features.end(); + }; - const bool fp16_flag = HasFlag(mcpu, mattr, "+fullfp16"); - const bool fp16_support = arch_version >= 8.2; - const bool has_fp16_simd = fp16_support && (fp16_flag || has_sve); + return {{"is_aarch64", Bool(IsAArch64(mtriple))}, + {"has_asimd", Bool(has_feature("neon"))}, + {"has_sve", Bool(has_feature("sve"))}, + {"has_dotprod", Bool(has_feature("dotprod"))}, + {"has_matmul_i8", Bool(has_feature("i8mm"))}, + {"has_fp16_simd", Bool(has_feature("fullfp16"))}}; +#endif - return {{"is_aarch64", Bool(is_aarch64)}, {"has_asimd", Bool(has_asimd)}, - {"has_sve", Bool(has_sve)}, {"has_dotprod", Bool(has_dotprod)}, - {"has_matmul_i8", Bool(has_i8mm)}, {"has_fp16_simd", Bool(has_fp16_simd)}}; + LOG(WARNING) << "Cannot parse Arm(R)-based target features without LLVM support."; + return {}; } static Array MergeKeys(Optional> existing_keys) { diff --git a/tests/cpp/target/parsers/aprofile_test.cc b/tests/cpp/target/parsers/aprofile_test.cc index fa85d1c32989c..a134e162fc2d6 100644 --- a/tests/cpp/target/parsers/aprofile_test.cc +++ b/tests/cpp/target/parsers/aprofile_test.cc @@ -19,42 +19,89 @@ #include "../src/target/parsers/aprofile.h" +#include #include #include #include +#include "../src/target/llvm/llvm_instance.h" + namespace tvm { namespace target { namespace parsers { namespace aprofile { +using ::testing::HasSubstr; + static float defaultI8MM = 8.6; static float optionalI8MM[] = {8.2, 8.3, 8.4, 8.5}; static float defaultDotProd = 8.4; static float optionalDotProd[] = {8.2, 8.3}; -class AProfileOptionalI8MM : public testing::TestWithParam {}; -class AProfileOptionalDotProd : public testing::TestWithParam {}; +static bool CheckArchitectureAvailability() { +#if TVM_LLVM_VERSION > 120 + auto llvm_instance = std::make_unique(); + codegen::LLVMTargetInfo llvm_backend(*llvm_instance, "llvm"); + Array targets = llvm_backend.GetAllLLVMTargets(); + int expected_target_count = 0; + for (String target : targets) { + if (target == "aarch64" || target == "arm") { + expected_target_count += 1; + } + } + if (expected_target_count >= 2) { + return true; + } +#endif + return false; +} +static bool has_aarch64_and_arm_targets = CheckArchitectureAvailability(); + +class AProfileParser : public ::testing::Test { + public: + // Check that LLVM has been compiled with the required targets, otherwise skip the test. + // Unfortunately, googletest doesn't let you call GTEST_SKIP in SetUpTestSuite() to skip + // the whole suite of tests, so a cached result is checked before each test is run instead. + void SetUp() override { + if (!has_aarch64_and_arm_targets) { + GTEST_SKIP() << "Skipping as LLVM has not been built for Arm(R)-based targets."; + } + } +}; + +class AProfileParserTestWithParam : public AProfileParser, + public testing::WithParamInterface {}; static TargetFeatures ParseTargetWithAttrs(String mcpu, String mtriple, Array mattr) { - return ParseTarget({ - {"mcpu", mcpu}, + TargetJSON target_json = { + {"kind", String("llvm")}, {"mtriple", mtriple}, {"mattr", mattr}, - }); + }; + if (mcpu != "") { + target_json.Set("mcpu", mcpu); + } + return ParseTarget(target_json); +} + +std::string FloatToStringWithoutTrailingZeros(float value) { + std::stringstream ss; + ss << value; + return ss.str(); } -TEST(AProfileParser, ParseTargetKeys) { - TargetJSON target = ParseTarget({}); +TEST_F(AProfileParser, ParseTargetKeys) { + TargetJSON target = ParseTarget({{"kind", String("llvm")}}); Array keys = Downcast>(target.at("keys")); ASSERT_EQ(keys.size(), 2); ASSERT_EQ(keys[0], "arm_cpu"); ASSERT_EQ(keys[1], "cpu"); } -TEST(AProfileParser, ParseTargetWithExistingKeys) { +TEST_F(AProfileParser, ParseTargetWithExistingKeys) { TargetJSON target = ParseTarget({ + {"kind", String("llvm")}, {"keys", Array{"cpu"}}, }); TargetFeatures features = Downcast(target.at("features")); @@ -64,8 +111,9 @@ TEST(AProfileParser, ParseTargetWithExistingKeys) { ASSERT_EQ(keys[1], "arm_cpu"); } -TEST(AProfileParser, ParseTargetWithDuplicateKey) { +TEST_F(AProfileParser, ParseTargetWithDuplicateKey) { TargetJSON target = ParseTarget({ + {"kind", String("llvm")}, {"keys", Array{"cpu", "arm_cpu"}}, }); TargetFeatures features = Downcast(target.at("features")); @@ -75,24 +123,21 @@ TEST(AProfileParser, ParseTargetWithDuplicateKey) { ASSERT_EQ(keys[1], "arm_cpu"); } -TEST(AProfileParser, ParseTargetDefaults) { - TargetJSON target = ParseTarget({}); +TEST_F(AProfileParser, ParseTargetDefaults) { + TargetJSON target = ParseTarget({{"kind", String("llvm")}}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(Downcast(features.at("is_aarch64")), false); - ASSERT_EQ(Downcast(features.at("has_asimd")), false); - ASSERT_EQ(Downcast(features.at("has_dotprod")), false); - ASSERT_EQ(Downcast(features.at("has_matmul_i8")), false); } -TEST(AProfileParser, IsAArch64Triple) { +TEST_F(AProfileParser, IsAArch64Triple) { TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {""}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); ASSERT_EQ(Downcast(features.at("is_aarch64")), true); } -TEST(AProfileParser, IsAArch32Triple) { +TEST_F(AProfileParser, IsAArch32Triple) { TargetJSON target = ParseTargetWithAttrs("", "armv7a-arm-none-eabi", {""}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); @@ -109,15 +154,16 @@ TEST(AProfileParser, IsAArch32Triple) { ASSERT_EQ(Downcast(features.at("is_aarch64")), false); } -TEST(AProfileParser, IsAArch32BlankCPU) { +TEST_F(AProfileParser, IsAArch32BlankCPU) { TargetJSON target = ParseTarget({ + {"kind", String("llvm")}, {"mtriple", String("arm-unknown-linux-gnu")}, }); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); } -TEST(AProfileParser, IsAArch32TripleWithAProfile) { +TEST_F(AProfileParser, IsAArch32TripleWithAProfile) { TargetJSON target = ParseTargetWithAttrs("cortex-a53", "armv7a-arm-none-eabi", {""}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); @@ -134,7 +180,7 @@ TEST(AProfileParser, IsAArch32TripleWithAProfile) { ASSERT_EQ(Downcast(features.at("is_aarch64")), false); } -TEST(AProfileParser, IsAArch32TripleWithMProfile) { +TEST_F(AProfileParser, IsAArch32TripleWithMProfile) { TargetJSON target = ParseTargetWithAttrs("cortex-m33", "armv7a-arm-none-eabi", {""}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), false); @@ -148,75 +194,53 @@ TEST(AProfileParser, IsAArch32TripleWithMProfile) { ASSERT_EQ(IsArch(target), false); } -TEST(AProfileParser, AArch64HasASIMD) { +TEST_F(AProfileParser, AArch64HasASIMD) { TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {""}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); ASSERT_EQ(Downcast(features.at("has_asimd")), true); } -TEST(AProfileParser, AArch32NoASIMD) { +TEST_F(AProfileParser, AArch32ASIMD) { TargetJSON target = ParseTargetWithAttrs("", "armv8a-arm-none-eabi", {}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); - ASSERT_EQ(Downcast(features.at("has_asimd")), false); + ASSERT_EQ(Downcast(features.at("has_asimd")), true); } -TEST(AProfileParser, AArch32HasASIMDWithOption) { +TEST_F(AProfileParser, AArch32HasASIMDWithOption) { TargetJSON target = ParseTargetWithAttrs("", "armv8a-arm-none-eabi", {"+simd"}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); ASSERT_EQ(Downcast(features.at("has_asimd")), true); - - target = ParseTargetWithAttrs("cortex-a+simd", "armv8a-arm-none-eabi", {""}); - features = Downcast(target.at("features")); - ASSERT_EQ(IsArch(target), true); - ASSERT_EQ(Downcast(features.at("has_asimd")), true); } -TEST(AProfileParser, AArch32HasASIMDWithAlternativeOption) { +TEST_F(AProfileParser, AArch32HasASIMDWithAlternativeOption) { TargetJSON target = ParseTargetWithAttrs("", "armv8a-arm-none-eabi", {"+neon"}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); ASSERT_EQ(Downcast(features.at("has_asimd")), true); - - target = ParseTargetWithAttrs("cortex-a+neon", "armv8a-arm-none-eabi", {""}); - features = Downcast(target.at("features")); - ASSERT_EQ(IsArch(target), true); - ASSERT_EQ(Downcast(features.at("has_asimd")), true); -} - -TEST(AProfileParser, NoI8MMSupport) { - std::string attr = "+v8.0a"; - TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {attr, "+i8mm"}); - TargetFeatures features = Downcast(target.at("features")); - ASSERT_EQ(IsArch(target), true); - ASSERT_EQ(Downcast(features.at("has_matmul_i8")), false); } -TEST(AProfileParser, DefaultI8MMSupport) { - std::string arch_attr = "+v" + std::to_string(defaultI8MM) + "a"; +TEST_F(AProfileParser, DefaultI8MMSupport) { + std::string arch_attr = "+v" + FloatToStringWithoutTrailingZeros(defaultI8MM) + "a"; TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); ASSERT_EQ(Downcast(features.at("has_matmul_i8")), true); } -TEST(AProfileParser, DefaultI8MMSupportDisable) { - std::string arch_attr = "+v" + std::to_string(defaultI8MM) + "a"; - TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr, "+noi8mm"}); +TEST_F(AProfileParser, DefaultI8MMSupportDisable) { + std::string arch_attr = "+v" + FloatToStringWithoutTrailingZeros(defaultI8MM) + "a"; + TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr, "-i8mm"}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); ASSERT_EQ(Downcast(features.at("has_matmul_i8")), false); - - target = ParseTargetWithAttrs("cortex-a+noi8mm", "aarch64-arm-none-eabi", {arch_attr}); - features = Downcast(target.at("features")); - ASSERT_EQ(IsArch(target), true); - ASSERT_EQ(Downcast(features.at("has_matmul_i8")), false); } +using AProfileOptionalI8MM = AProfileParserTestWithParam; TEST_P(AProfileOptionalI8MM, OptionalI8MMSupport) { - std::string arch_attr = "+v" + std::to_string(GetParam()) + "a"; + std::string arch_attr = "+v" + FloatToStringWithoutTrailingZeros(GetParam()) + "a"; TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr}); TargetFeatures features = Downcast(target.at("features")); @@ -227,44 +251,27 @@ TEST_P(AProfileOptionalI8MM, OptionalI8MMSupport) { features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); ASSERT_EQ(Downcast(features.at("has_matmul_i8")), true); - - target = ParseTargetWithAttrs("cortex-a+i8mm", "aarch64-arm-none-eabi", {arch_attr}); - features = Downcast(target.at("features")); - ASSERT_EQ(IsArch(target), true); - ASSERT_EQ(Downcast(features.at("has_matmul_i8")), true); -} - -TEST(AProfileParser, NoDotProdSupport) { - std::string attr = "+v8.0a"; - TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {attr, "+dotprod"}); - TargetFeatures features = Downcast(target.at("features")); - ASSERT_EQ(IsArch(target), true); - ASSERT_EQ(Downcast(features.at("has_dotprod")), false); } -TEST(AProfileParser, DefaultDotProdSupport) { - std::string arch_attr = "+v" + std::to_string(defaultDotProd) + "a"; +TEST_F(AProfileParser, DefaultDotProdSupport) { + std::string arch_attr = "+v" + FloatToStringWithoutTrailingZeros(defaultDotProd) + "a"; TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); ASSERT_EQ(Downcast(features.at("has_dotprod")), true); } -TEST(AProfileParser, DefaultDotProdSupportDisable) { - std::string arch_attr = "+v" + std::to_string(defaultDotProd) + "a"; - TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr, "+nodotprod"}); +TEST_F(AProfileParser, DefaultDotProdSupportDisable) { + std::string arch_attr = "+v" + FloatToStringWithoutTrailingZeros(defaultDotProd) + "a"; + TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr, "-dotprod"}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); ASSERT_EQ(Downcast(features.at("has_dotprod")), false); - - target = ParseTargetWithAttrs("cortex-a+nodotprod", "aarch64-arm-none-eabi", {arch_attr}); - features = Downcast(target.at("features")); - ASSERT_EQ(IsArch(target), true); - ASSERT_EQ(Downcast(features.at("has_dotprod")), false); } +using AProfileOptionalDotProd = AProfileParserTestWithParam; TEST_P(AProfileOptionalDotProd, OptionalDotProdSupport) { - std::string arch_attr = "+v" + std::to_string(GetParam()) + "a"; + std::string arch_attr = "+v" + FloatToStringWithoutTrailingZeros(GetParam()) + "a"; TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr}); TargetFeatures features = Downcast(target.at("features")); @@ -275,24 +282,19 @@ TEST_P(AProfileOptionalDotProd, OptionalDotProdSupport) { features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); ASSERT_EQ(Downcast(features.at("has_dotprod")), true); - - target = ParseTargetWithAttrs("cortex-a+dotprod", "aarch64-arm-none-eabi", {arch_attr}); - features = Downcast(target.at("features")); - ASSERT_EQ(IsArch(target), true); - ASSERT_EQ(Downcast(features.at("has_dotprod")), true); } -TEST(AProfileParser, ArchVersionInvalidLetter) { - std::string arch_attr = "+v" + std::to_string(defaultDotProd) + "b"; +TEST_F(AProfileParser, ArchVersionInvalidLetter) { + std::string arch_attr = "+v" + FloatToStringWithoutTrailingZeros(defaultDotProd) + "b"; TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr}); TargetFeatures features = Downcast(target.at("features")); ASSERT_EQ(IsArch(target), true); ASSERT_EQ(Downcast(features.at("has_dotprod")), false); } -using AProfileOptionalSVE = testing::TestWithParam; +using AProfileOptionalSVE = AProfileParserTestWithParam; TEST_P(AProfileOptionalSVE, OptionalSVESupport) { - const std::string arch_attr = "+v" + std::to_string(GetParam()) + "a"; + const std::string arch_attr = "+v" + FloatToStringWithoutTrailingZeros(GetParam()) + "a"; // Check that the "has_sve" feature is not set by default when "+sve" isn't set as an attribute. TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr}); @@ -307,9 +309,25 @@ TEST_P(AProfileOptionalSVE, OptionalSVESupport) { EXPECT_TRUE(Downcast(features.at("has_sve"))); } -using AProfileOptionalFP16 = testing::TestWithParam; +TEST_F(AProfileParser, DefaultSVESupportSVESupport) { + const std::string arch_attr = "+v9a"; + + // Check that the "has_sve" feature is not set by default when "+sve" isn't set as an attribute. + TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr}); + TargetFeatures features = Downcast(target.at("features")); + EXPECT_TRUE(IsArch(target)); + EXPECT_TRUE(Downcast(features.at("has_sve"))); + + // Check that the "has_sve" feature is set when "+sve" is explicitly set as an attribute. + target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr, "+sve"}); + features = Downcast(target.at("features")); + EXPECT_TRUE(IsArch(target)); + EXPECT_TRUE(Downcast(features.at("has_sve"))); +} + +using AProfileOptionalFP16 = AProfileParserTestWithParam; TEST_P(AProfileOptionalFP16, OptionalFP16Support) { - const std::string arch_attr = "+v" + std::to_string(GetParam()) + "a"; + const std::string arch_attr = "+v" + FloatToStringWithoutTrailingZeros(GetParam()) + "a"; // Check that the "has_fp16_simd" feature is not set by default when "+fullfp16" isn't set as an // attribute. @@ -332,13 +350,68 @@ TEST_P(AProfileOptionalFP16, OptionalFP16Support) { EXPECT_TRUE(Downcast(features.at("has_fp16_simd"))); } -INSTANTIATE_TEST_CASE_P(AProfileParser, AProfileOptionalI8MM, ::testing::ValuesIn(optionalI8MM)); -INSTANTIATE_TEST_CASE_P(AProfileParser, AProfileOptionalDotProd, - ::testing::ValuesIn(optionalDotProd)); -INSTANTIATE_TEST_CASE_P(AProfileParser, AProfileOptionalSVE, - ::testing::Values(8.0, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9, 9.0)); -INSTANTIATE_TEST_CASE_P(AProfileParser, AProfileOptionalFP16, - ::testing::Values(8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9, 9.0)); +TEST_F(AProfileParser, DefaultFP16Support) { + const std::string arch_attr = "+v9a"; + + // Check that the "has_fp16_simd" feature is not set by default when "+fullfp16" isn't set as an + // attribute. + TargetJSON target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr}); + TargetFeatures features = Downcast(target.at("features")); + EXPECT_TRUE(IsArch(target)); + EXPECT_TRUE(Downcast(features.at("has_fp16_simd"))); + + // Check that the "has_fp16_simd" feature is set when "+fullfp16" is explicitly set as an + // attribute. + target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr, "+fullfp16"}); + features = Downcast(target.at("features")); + EXPECT_TRUE(IsArch(target)); + EXPECT_TRUE(Downcast(features.at("has_fp16_simd"))); + + // Check that the "has_fp16_simd" feature is set when "+sve" is explicitly set as an attribute. + target = ParseTargetWithAttrs("", "aarch64-arm-none-eabi", {arch_attr, "+sve"}); + features = Downcast(target.at("features")); + EXPECT_TRUE(IsArch(target)); + EXPECT_TRUE(Downcast(features.at("has_fp16_simd"))); +} + +TEST_F(AProfileParser, ImpliedFeature) { + TargetJSON target = ParseTargetWithAttrs("", "aarch64-linux-gnu", {"+sve2"}); + TargetFeatures features = Downcast(target.at("features")); + EXPECT_TRUE(Downcast(features.at("has_sve"))); + EXPECT_TRUE(Downcast(features.at("has_asimd"))); +} + +TEST_F(AProfileParser, UnexpectedTargetKind) { + EXPECT_THROW( + { + try { + ParseTarget({{"kind", String("c")}}); + } catch (const tvm::InternalError& e) { + EXPECT_THAT(e.what(), HasSubstr("Expected target kind 'llvm', but got 'c'")); + throw; + } + }, + tvm::InternalError); +} + +TEST(AProfileParserInvalid, LLVMUnsupportedArchitecture) { + if (has_aarch64_and_arm_targets) { + GTEST_SKIP() << "LLVM has been compiled for the correct targets."; + } + TargetJSON target = ParseTarget({{"kind", String("llvm")}}); + TargetFeatures features = Downcast(target.at("features")); + for (auto feature : features) { + ASSERT_EQ(Downcast(feature.second), false); + } +} + +INSTANTIATE_TEST_SUITE_P(AProfileParser, AProfileOptionalI8MM, ::testing::ValuesIn(optionalI8MM)); +INSTANTIATE_TEST_SUITE_P(AProfileParser, AProfileOptionalDotProd, + ::testing::ValuesIn(optionalDotProd)); +INSTANTIATE_TEST_SUITE_P(AProfileParser, AProfileOptionalSVE, + ::testing::Values(8.0, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9)); +INSTANTIATE_TEST_SUITE_P(AProfileParser, AProfileOptionalFP16, + ::testing::Values(8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9)); } // namespace aprofile } // namespace parsers diff --git a/tests/python/relay/strategy/test_select_implementation.py b/tests/python/relay/strategy/test_select_implementation.py index 0ab00e550895e..d0767175d3d8d 100644 --- a/tests/python/relay/strategy/test_select_implementation.py +++ b/tests/python/relay/strategy/test_select_implementation.py @@ -27,6 +27,7 @@ from tvm.relay.testing import run_infer_type, run_opt_pass import tvm.testing from tvm import topi +from tvm.target.codegen import llvm_version_major @pytest.mark.parametrize( @@ -90,6 +91,9 @@ def _get_conv2d_impl(dtype, target): return impl.name +@pytest.mark.skipif( + llvm_version_major() < 15, reason=f"Requires LLVM 15+, got {llvm_version_major()}" +) @pytest.mark.parametrize( "target,expected_impl", [ @@ -119,7 +123,7 @@ def _get_conv2d_impl(dtype, target): ), ( "llvm --device=arm_cpu --mtriple=aarch64-linux-gnu -mattr=+v9a", - "conv2d_NHWC_quantized_interleaved_without_transform.arm_cpu", + "conv2d_NHWC_quantized_native_without_transform.arm_cpu", ), ], ) @@ -131,6 +135,9 @@ def test_int8_conv2d(target, expected_impl): assert selected_impl == expected_impl +@pytest.mark.skipif( + llvm_version_major() < 15, reason=f"Requires LLVM 15+, got {llvm_version_major()}" +) @pytest.mark.parametrize( "target,expected_impl", [ @@ -164,6 +171,9 @@ def test_fp32_conv2d(target, expected_impl): assert selected_impl == expected_impl +@pytest.mark.skipif( + llvm_version_major() < 15, reason=f"Requires LLVM 15+, got {llvm_version_major()}" +) @pytest.mark.parametrize( "target,expected_impl", [ diff --git a/tests/python/target/test_llvm_features_info.py b/tests/python/target/test_llvm_features_info.py index edcbc891c90d2..34e9a582313ad 100644 --- a/tests/python/target/test_llvm_features_info.py +++ b/tests/python/target/test_llvm_features_info.py @@ -22,7 +22,7 @@ LLVM_VERSION = codegen.llvm_version_major() -def test_llvm_targets(): +def test_llvm_targets(capfd): ## ## check LLVM backend @@ -39,20 +39,14 @@ def test_llvm_targets(): assert codegen.llvm_get_system_x86_vendor() == _ffi_api.llvm_get_system_x86_vendor() assert str(codegen.llvm_get_targets()) == str(_ffi_api.llvm_get_targets()) - # check LLVM target -mcpu legality - try: - tvm.target.codegen.llvm_get_cpu_features( - tvm.target.Target("llvm -mtriple=x86_64-linux-gnu -mcpu=dummy") - ) - assert False - except tvm.error.TVMError as e: - msg = str(e) - assert ( - msg.find( - "TVMError: LLVM cpu architecture `-mcpu=dummy` is not valid in `-mtriple=x86_64-linux-gnu`" - ) - != -1 - ) + tvm.target.codegen.llvm_get_cpu_features( + tvm.target.Target("llvm -mtriple=x86_64-linux-gnu -mcpu=dummy") + ) + expected_str = ( + "Error: LLVM cpu architecture `-mcpu=dummy` is not valid in " + "`-mtriple=x86_64-linux-gnu`, using default `-mcpu=generic`" + ) + assert expected_str in capfd.readouterr().err min_llvm_version, llvm_target, cpu_arch, cpu_features, is_supported = tvm.testing.parameters(