| // Copyright 2024 The IREE Authors | |
| // | |
| // Licensed under the Apache License v2.0 with LLVM Exceptions. | |
| // See https://llvm.org/LICENSE.txt for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| #include "compiler/plugins/target/LLVMCPU/ResolveCPUAndCPUFeatures.h" | |
| #include "llvm/ADT/SmallVector.h" | |
| #include "llvm/ADT/StringMap.h" | |
| #include "llvm/ADT/StringRef.h" | |
| #include "llvm/Support/FormatVariadic.h" | |
| #include "llvm/TargetParser/AArch64TargetParser.h" | |
| #include "llvm/TargetParser/Host.h" | |
| #include "llvm/TargetParser/RISCVTargetParser.h" | |
| #include "llvm/TargetParser/SubtargetFeature.h" | |
| #include "llvm/TargetParser/Triple.h" | |
| #include "llvm/TargetParser/X86TargetParser.h" | |
| namespace mlir::iree_compiler::IREE::HAL { | |
| namespace { | |
| ResolveCPUAndCPUFeaturesStatus | |
| resolveHostCPUAndCPUFeatures(std::string &cpu, std::string &cpuFeatures) { | |
| if (cpu != "host" && cpuFeatures != "host") { | |
| return ResolveCPUAndCPUFeaturesStatus::OK; | |
| } | |
| if ((!cpu.empty() && cpu != "host") || | |
| (!cpuFeatures.empty() && cpuFeatures != "host")) { | |
| return ResolveCPUAndCPUFeaturesStatus::InconsistentHost; | |
| } | |
| cpu = llvm::sys::getHostCPUName(); | |
| // Sort features to ensure deterministic iteration order. The StringMap | |
| // returned by getHostCPUFeatures() has non-deterministic iteration order. | |
| llvm::StringMap<bool, llvm::MallocAllocator> hostFeatures = | |
| llvm::sys::getHostCPUFeatures(); | |
| auto sortedFeatures = | |
| llvm::to_vector_of<llvm::StringRef>(hostFeatures.keys()); | |
| llvm::sort(sortedFeatures); | |
| // Add all features in lexicographically sorted order. | |
| llvm::SubtargetFeatures features; | |
| for (llvm::StringRef feature : sortedFeatures) { | |
| features.AddFeature(feature, hostFeatures.lookup(feature)); | |
| } | |
| cpuFeatures = features.getString(); | |
| return ResolveCPUAndCPUFeaturesStatus::OK; | |
| } | |
| ResolveCPUAndCPUFeaturesStatus | |
| resolveCPUFeaturesForCPU(const llvm::Triple &triple, std::string &cpu, | |
| std::string &cpuFeatures) { | |
| if (cpu.empty() || cpu == "generic" || | |
| llvm::StringRef(cpu).starts_with("generic-")) { | |
| // Implicitly (default) or explicitly specified generic CPU: no features. | |
| // Logging (on unspecified CPU) was already handled, no need for it here. | |
| return ResolveCPUAndCPUFeaturesStatus::OK; | |
| } | |
| llvm::SubtargetFeatures originalCpuFeatures(cpuFeatures); | |
| llvm::SubtargetFeatures targetCpuFeatures; | |
| if (triple.isX86()) { | |
| if (!llvm::X86::parseArchX86(cpu, /*Only64Bit=*/true)) { | |
| return ResolveCPUAndCPUFeaturesStatus::UnknownCPU; | |
| } | |
| llvm::SmallVector<llvm::StringRef> features; | |
| llvm::X86::getFeaturesForCPU(cpu, features, /*NeedPlus=*/false); | |
| // llvm::X86::getFeaturesForCPU returns only the features literally stored | |
| // on the ProcInfo entry (i.e. the "leaf" features). For microarchitecture | |
| // levels like x86-64-v{2,3,4}, that notably excludes the transitively | |
| // implied features: e.g. x86-64-v4 lists {avx512bw, avx512cd, avx512dq, | |
| // avx512vl} but not avx512f, avx2, avx, the earlier sse versions, etc. | |
| // Clang's X86TargetInfo::initFeatureMap expands those implications by | |
| // running each feature through llvm::X86::updateImpliedFeatures; do the | |
| // same here so downstream consumers (including IREE's own hasFeature | |
| // string-based checks) see the full transitive set, matching the behavior | |
| // of `clang -march=<cpu>`. | |
| llvm::StringMap<bool> featureMap; | |
| for (llvm::StringRef feature : features) { | |
| featureMap[feature] = true; | |
| llvm::X86::updateImpliedFeatures(feature, /*Enabled=*/true, featureMap); | |
| } | |
| // Sort for a deterministic feature string. | |
| llvm::SmallVector<llvm::StringRef> sortedFeatures = | |
| llvm::to_vector_of<llvm::StringRef>(featureMap.keys()); | |
| llvm::sort(sortedFeatures); | |
| for (llvm::StringRef feature : sortedFeatures) { | |
| if (featureMap.lookup(feature)) { | |
| targetCpuFeatures.AddFeature(feature); | |
| } | |
| } | |
| } else if (triple.isRISCV64()) { | |
| if (!llvm::RISCV::parseCPU(cpu, triple.isRISCV64())) { | |
| return ResolveCPUAndCPUFeaturesStatus::UnknownCPU; | |
| } | |
| llvm::SmallVector<std::string> features; | |
| llvm::RISCV::getFeaturesForCPU(cpu, features, /*NeedPlus=*/false); | |
| for (const auto &feature : features) { | |
| targetCpuFeatures.AddFeature(feature); | |
| } | |
| } else if (triple.isAArch64()) { | |
| std::optional<llvm::AArch64::CpuInfo> cpuInfo = | |
| llvm::AArch64::parseCpu(cpu); | |
| if (!cpuInfo) { | |
| return ResolveCPUAndCPUFeaturesStatus::UnknownCPU; | |
| } | |
| llvm::AArch64::ExtensionSet extensions; | |
| extensions.addCPUDefaults(*cpuInfo); | |
| std::vector<std::string> features; | |
| extensions.toLLVMFeatureList(features); | |
| for (const auto &feature : features) { | |
| targetCpuFeatures.AddFeature(feature); | |
| } | |
| } else { | |
| return ResolveCPUAndCPUFeaturesStatus::UnimplementedMapping; | |
| } | |
| targetCpuFeatures.addFeaturesVector(originalCpuFeatures.getFeatures()); | |
| cpuFeatures = targetCpuFeatures.getString(); | |
| return ResolveCPUAndCPUFeaturesStatus::OK; | |
| } | |
| void tweakCPUFeatures(const llvm::Triple &triple, std::string &cpu, | |
| std::string &cpuFeatures) { | |
| if (triple.isAArch64()) { | |
| llvm::SubtargetFeatures targetCpuFeatures(cpuFeatures); | |
| // x18 is platform-reserved per the Aarch64 procedure call specification. | |
| targetCpuFeatures.AddFeature("reserve-x18", true); | |
| cpuFeatures = targetCpuFeatures.getString(); | |
| } | |
| } | |
| std::string getImplicitGenericFallbackMessage(std::string_view triple_str) { | |
| llvm::StringRef triple_str_ref(triple_str); | |
| llvm::Triple triple(triple_str_ref); | |
| std::string msg = R"MSG( | |
| Defaulting to targeting a generic CPU for the target architecture will result in poor performance. Please specify a target CPU and/or a target CPU feature set. If it is intended to target a generic CPU, specify "generic" as the CPU. | |
| This can be done in two ways: | |
| 1. With command-line flags: | |
| --iree-llvmcpu-target-cpu=... | |
| --iree-llvmcpu-target-cpu-features=... | |
| 2. Within the IR: | |
| #hal.executable.target< ... , cpu="...", cpu_features="..."> | |
| In the rest of this message, these fields are referred to as just `cpu` and `cpu_features`. | |
| Examples: | |
| cpu=generic | |
| Target a generic CPU of the target architecture. The generated code will have poor performance, but will run on any CPU. | |
| cpu=host | |
| Target the host CPU. The generated code will have optimal performance on the host CPU but will crash on other CPUs not supporting the same CPU features. | |
| cpu="name" | |
| Target a specific CPU. This is mostly used on x86. The accepted values are the same as in Clang command lines.)MSG"; | |
| if (triple.isX86()) { | |
| msg += R"MSG( | |
| List of accepted x86 CPUs: )MSG"; | |
| llvm::SmallVector<llvm::StringRef> allAcceptedCpus; | |
| llvm::X86::fillValidCPUArchList(allAcceptedCpus, /*Only64Bit=*/true); | |
| llvm::raw_string_ostream s(msg); | |
| llvm::interleaveComma(allAcceptedCpus, s); | |
| msg += "\n"; | |
| } else { | |
| msg += R"MSG( | |
| CAVEAT: Outside of x86, this may only set the instruction scheduling model but may not enable CPU features. That's why when targeting non-x86 CPUs, it is usually preferred to pass cpu_features, see below. | |
| )MSG"; | |
| } | |
| msg += R"MSG( | |
| cpu_features="+feature1,..." | |
| Target a CPU supporting the comma-separated of (+-prefixed) features. The accepted values are the same as in Clang command lines. | |
| )MSG"; | |
| if (triple.isAArch64()) { | |
| msg += R"MSG( | |
| Example: cpu_features="+dotprod,+i8mm,+bf16 | |
| )MSG"; | |
| } | |
| if (triple.isRISCV()) { | |
| msg += R"MSG( | |
| Example: cpu_features="+m,+a,+f,+d,+c | |
| )MSG"; | |
| } | |
| return msg; | |
| } | |
| std::string getUnknownCPUMessage(std::string_view triple_str) { | |
| llvm::StringRef triple_str_ref(triple_str); | |
| llvm::Triple triple(triple_str_ref); | |
| std::string msg = llvm::formatv("Unknown CPU for target architecture {}.\n", | |
| triple.getArchName()); | |
| if (triple.isX86()) { | |
| msg += "List of accepted CPUs: "; | |
| llvm::SmallVector<llvm::StringRef> allAcceptedCpus; | |
| llvm::X86::fillValidCPUArchList(allAcceptedCpus, /*Only64Bit=*/true); | |
| llvm::raw_string_ostream s(msg); | |
| llvm::interleaveComma(allAcceptedCpus, s); | |
| msg += "\n"; | |
| } else if (triple.isRISCV()) { | |
| msg += "List of accepted CPUs: "; | |
| llvm::SmallVector<llvm::StringRef> allAcceptedCpus; | |
| llvm::RISCV::fillValidCPUArchList(allAcceptedCpus, /*IsRV64=*/true); | |
| llvm::raw_string_ostream s(msg); | |
| llvm::interleaveComma(allAcceptedCpus, s); | |
| msg += "\n"; | |
| } else if (triple.isAArch64()) { | |
| msg += "List of accepted CPUs: "; | |
| llvm::SmallVector<llvm::StringRef> allAcceptedCpus; | |
| llvm::AArch64::fillValidCPUArchList(allAcceptedCpus); | |
| llvm::raw_string_ostream s(msg); | |
| llvm::interleaveComma(allAcceptedCpus, s); | |
| msg += "\n"; | |
| } | |
| return msg; | |
| } | |
| } // namespace | |
| ResolveCPUAndCPUFeaturesStatus | |
| resolveCPUAndCPUFeatures(std::string_view triple_str, std::string &cpu, | |
| std::string &cpuFeatures) { | |
| // Helper to return the first non-OK status. | |
| auto combine = [](ResolveCPUAndCPUFeaturesStatus a, | |
| ResolveCPUAndCPUFeaturesStatus b) { | |
| return a == ResolveCPUAndCPUFeaturesStatus::OK ? b : a; | |
| }; | |
| llvm::StringRef triple_str_ref(triple_str); | |
| llvm::Triple triple(triple_str_ref); | |
| // No early-return on error status. The caller may treat these errors as | |
| // non-fatal and will carry on with whichever `cpu` and `cpuFeatures` we | |
| // produce. | |
| auto status1 = resolveHostCPUAndCPUFeatures(cpu, cpuFeatures); | |
| auto status2 = resolveCPUFeaturesForCPU(triple, cpu, cpuFeatures); | |
| auto status12 = combine(status1, status2); | |
| if (status12 == ResolveCPUAndCPUFeaturesStatus::OK) { | |
| tweakCPUFeatures(triple, cpu, cpuFeatures); | |
| } | |
| std::string defaultTweakedCpu; | |
| std::string defaultTweakedCpuFeatures; | |
| tweakCPUFeatures(triple, defaultTweakedCpu, defaultTweakedCpuFeatures); | |
| auto status3 = | |
| (cpu == defaultTweakedCpu && cpuFeatures == defaultTweakedCpuFeatures) | |
| ? ResolveCPUAndCPUFeaturesStatus::ImplicitGenericFallback | |
| : ResolveCPUAndCPUFeaturesStatus::OK; | |
| return combine(status12, status3); | |
| } | |
| std::string getMessage(ResolveCPUAndCPUFeaturesStatus status, | |
| std::string_view triple_str) { | |
| switch (status) { | |
| case ResolveCPUAndCPUFeaturesStatus::ImplicitGenericFallback: | |
| return getImplicitGenericFallbackMessage(triple_str); | |
| case ResolveCPUAndCPUFeaturesStatus::InconsistentHost: | |
| return "If either CPU or CPU-features is `host`, the other must " | |
| "be either also `host` or the default value.\n"; | |
| case ResolveCPUAndCPUFeaturesStatus::UnimplementedMapping: | |
| return "Resolution of CPU to CPU-features is not implemented on this " | |
| "target architecture. Pass explicit " | |
| "CPU-features, or implement the missing mapping.\n"; | |
| case ResolveCPUAndCPUFeaturesStatus::UnknownCPU: | |
| return getUnknownCPUMessage(triple_str); | |
| default: | |
| assert(false); | |
| return ""; | |
| } | |
| } | |
| } // namespace mlir::iree_compiler::IREE::HAL |