diff --git a/scripts/generate_wrappers.py b/scripts/generate_wrappers.py index 9891ad7d9..ef54e4073 100644 --- a/scripts/generate_wrappers.py +++ b/scripts/generate_wrappers.py @@ -614,8 +614,9 @@ def _generate_call(op_name, call, method=True): f" handle.set_stream(reinterpret_cast(stream));\n" f" }}\n" f" Config config;\n" - f" config.set_implementation_index(\n" - f" implementation_index.value_or({default_impl_index}));\n" + f" if (implementation_index.has_value()) {{\n" + f" config.set_implementation_index(*implementation_index);\n" + f" }}\n" f" return generated_dispatch::Call{symbol_name}(handle, config, {call_args});\n" f' }}, {py_args_str}py::kw_only(), py::arg("stream") = 0, py::arg("implementation_index") = py::none());' ) @@ -1671,9 +1672,16 @@ def _dispatch_gen_batch_size(): // Generated with `INFINI_OPS_MONOLITHIC_BINDINGS=1`. {op_includes} +#include "tuning.h" + namespace infini::ops {{ PYBIND11_MODULE(ops, m) {{ + const char* tuning_path = std::getenv("INFINI_OPS_TUNING_PATH"); + if (!tuning_path) {{ + tuning_path = "tuning.json"; + }} + infini::ops::TuningManager::Instance().LoadTuningCache(tuning_path); {textwrap.indent(bind_func_calls, _INDENTATION)} }} @@ -1686,11 +1694,18 @@ def _dispatch_gen_batch_size(): ) ops_source = f"""#include +#include "tuning.h" + namespace infini::ops {{ {bind_func_declarations} PYBIND11_MODULE(ops, m) {{ + const char* tuning_path = std::getenv("INFINI_OPS_TUNING_PATH"); + if (!tuning_path) {{ + tuning_path = "tuning.json"; + }} + infini::ops::TuningManager::Instance().LoadTuningCache(tuning_path); {textwrap.indent(bind_func_calls, _INDENTATION)} }} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fb022422c..213163f91 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,6 +37,7 @@ include(GNUInstallDirs) file(GLOB BASE_SRCS CONFIGURE_DEPENDS "*.cc") list(FILTER BASE_SRCS EXCLUDE REGEX ".*tensor\\.cc$") + target_sources(infiniops PRIVATE ${BASE_SRCS}) target_link_libraries(infiniops PUBLIC infinirt) diff --git a/src/config.h b/src/config.h index a8b59a4fd..5c4bfa71a 100644 --- a/src/config.h +++ b/src/config.h @@ -11,10 +11,14 @@ class Config { void set_implementation_index(std::size_t implementation_index) { implementation_index_ = implementation_index; + auto_select_ = false; } + bool auto_select() const { return auto_select_; } + private: std::size_t implementation_index_{0}; + bool auto_select_{true}; }; } // namespace infini::ops diff --git a/src/operator.h b/src/operator.h index dc34d25bc..4893cedce 100644 --- a/src/operator.h +++ b/src/operator.h @@ -15,6 +15,17 @@ #include "handle.h" #include "tensor.h" +#include +#include +#include +#include +#include +#include + +#include "runtime.h" +#include "tuning.h" +#include "tuning_utils.h" + namespace infini::ops::detail { struct CacheKey { @@ -74,6 +85,19 @@ bool ListContains(ValueType value, List) { return ((value == static_cast(values)) || ...); } +inline void SyncDevice(Device::Type dev_type) { + if (!ListContains(dev_type, ActiveDevices{})) { + return; + } + DispatchFunc>( + dev_type, + [](auto device_tag) { + constexpr Device::Type kDev = decltype(device_tag)::value; + infini::rt::runtime::Runtime::DeviceSynchronize(); + }, + "SyncDevice"); +} + template class IsTensorLike : public std::false_type {}; @@ -151,6 +175,14 @@ struct CacheKeyBuilder { } }; +template +Config ResolveConfig(const Config& config, Device::Type dev_type, + const Args&... args); + +template +Config ResolveConfigOnline(const Handle& handle, const Config& config, + const Args&... args); + template struct ActiveImplementations; @@ -196,7 +228,8 @@ class Operator : public OperatorBase { template static std::unique_ptr Make(const Config& config, const Tensor tensor, Args&&... args) { - return MakeWithDevice(config, tensor.device().type(), tensor, + Config resolved = ResolveConfig(config, tensor.device().type(), tensor, args...); + return MakeWithDevice(resolved, tensor.device().type(), tensor, std::forward(args)...); } @@ -211,7 +244,8 @@ class Operator : public OperatorBase { Args&&... args) { assert(!tensors.empty() && "operator tensor list input cannot be empty"); - return MakeWithDevice(config, tensors.front().device().type(), tensors, + Config resolved = ResolveConfig(config, tensors.front().device().type(), tensors, args...); + return MakeWithDevice(resolved, tensors.front().device().type(), tensors, std::forward(args)...); } @@ -234,12 +268,15 @@ class Operator : public OperatorBase { generation = cache_generation_; } - auto key = CacheKeyBuilder{}(config, args...); + const Config effective_config = + ResolveConfigOnline(handle, config, args...); + + auto key = CacheKeyBuilder{}(effective_config, args...); auto it{cache.find(key)}; if (it == cache.end()) { - it = cache.emplace(std::move(key), Make(config, args...)).first; + it = cache.emplace(std::move(key), Make(effective_config, args...)).first; } auto& op{it->second}; @@ -393,6 +430,126 @@ struct ActiveImplementations { Key, kDev, std::make_index_sequence>::type; }; +template +Config ResolveConfig(const Config& config, Device::Type dev_type, + const Args&... args) { + if (config.auto_select()) { + auto indices = Operator::active_implementation_indices(dev_type); + if (!indices.empty()) { + auto signature = TuningSignature::Build(args...); + + auto op_name = detail::ExtractOperatorName(); + auto tuned_index = + TuningManager::Instance().Lookup(op_name, dev_type, signature); + + Config resolved = config; + if (tuned_index.has_value()) { + bool is_valid = std::find(indices.begin(), indices.end(), + *tuned_index) != indices.end(); + if (is_valid) { + resolved.set_implementation_index(*tuned_index); + } else { + std::cerr << "[Tuning] Warning: tuned implementation " << *tuned_index + << " for " << op_name << " on " + << Device::StringFromType(dev_type) + << " is not available (compiled indices:"; + for (auto idx : indices) std::cerr << " " << idx; + std::cerr << "), falling back to " << indices.front() << std::endl; + resolved.set_implementation_index(indices.front()); + } + } else { + resolved.set_implementation_index(indices.front()); + } + return resolved; + } + } + return config; +} + +template +double BenchmarkImplementation(const Handle& handle, Device::Type dev_type, + std::size_t impl_index, const Args&... args) { + Config fixed; + fixed.set_implementation_index(impl_index); + + auto op = Operator::Make(fixed, args...); + if (!op) { + return std::numeric_limits::infinity(); + } + + const int warmup = detail::EnvInt("INFINI_OPS_TUNING_WARMUP", 1); + const int repeat = detail::EnvInt("INFINI_OPS_TUNING_REPEAT", 5); + + for (int i = 0; i < warmup; ++i) { + (*op)(handle, args...); + } + detail::SyncDevice(dev_type); + + double best = std::numeric_limits::infinity(); + for (int i = 0; i < repeat; ++i) { + auto start = std::chrono::steady_clock::now(); + (*op)(handle, args...); + detail::SyncDevice(dev_type); + auto end = std::chrono::steady_clock::now(); + double elapsed = std::chrono::duration(end - start).count(); + best = std::min(best, elapsed); + } + return best; +} + +template +Config ResolveConfigOnline(const Handle& handle, const Config& config, + const Args&... args) { + if (config.auto_select() && TuningManager::Instance().IsEnabled()) { + Device::Type dev_type = detail::FirstDeviceType(args...); + auto indices = Operator::active_implementation_indices(dev_type); + + if (!indices.empty()) { + auto signature = TuningSignature::Build(args...); + auto op_name = detail::ExtractOperatorName(); + + auto tuned = + TuningManager::Instance().Lookup(op_name, dev_type, signature); + + std::size_t chosen; + if (tuned.has_value() && + std::find(indices.begin(), indices.end(), *tuned) != indices.end()) { + chosen = *tuned; + } else { + if (indices.size() == 1) { + chosen = indices.front(); + TuningManager::Instance().Record(op_name, dev_type, signature, chosen); + std::cout << "[Tuning] " << op_name << " on " + << Device::StringFromType(dev_type) + << ": single impl, chose index " << chosen << std::endl; + } else { + chosen = indices.front(); + double best_time = std::numeric_limits::infinity(); + for (auto idx : indices) { + double t = + BenchmarkImplementation(handle, dev_type, idx, args...); + if (t < best_time) { + best_time = t; + chosen = idx; + } + } + TuningManager::Instance().Record(op_name, dev_type, signature, chosen); + std::cout << "[Tuning] " << op_name << " on " + << Device::StringFromType(dev_type) << ": benchmarked " + << indices.size() << " impls, chose index " << chosen << " (" + << best_time * 1e6 << " us)" << std::endl; + } + } + + Config resolved = config; + resolved.set_implementation_index(chosen); + return resolved; + } + } + (void)handle; + return config; +} + } // namespace infini::ops #endif diff --git a/src/tuning.cc b/src/tuning.cc new file mode 100644 index 000000000..8c5c4a672 --- /dev/null +++ b/src/tuning.cc @@ -0,0 +1,294 @@ +#include "tuning.h" + +#include +#include +#include + +namespace { + +void SkipWhitespace(std::istream& in) { + while (in && std::isspace(in.peek())) { + in.get(); + } +} + +std::string ParseString(std::istream& in) { + SkipWhitespace(in); + if (in.get() != '"') return ""; + std::string result; + while (in) { + char c = in.get(); + if (c == '"') break; + if (c == '\\') { + c = in.get(); + } + result += c; + } + return result; +} + +double ParseNumber(std::istream& in) { + SkipWhitespace(in); + double val = 0; + in >> val; + return val; +} + +int64_t ParseInteger(std::istream& in) { + SkipWhitespace(in); + int64_t val = 0; + in >> val; + return val; +} + +void SkipTo(std::istream& in, char target) { + while (in && in.get() != target) { + } +} + +std::string NextKey(std::istream& in) { + SkipWhitespace(in); + if (in.peek() == '}' || in.peek() == ']') return ""; + if (in.peek() == ',') in.get(); + SkipWhitespace(in); + if (in.peek() == '"') { + auto key = ParseString(in); + SkipTo(in, ':'); + return key; + } + return ""; +} + +} // namespace + +namespace infini::ops { + +TuningManager& TuningManager::Instance() { + static TuningManager instance; + return instance; +} + +void TuningManager::LoadTuningCache(const std::string& json_path) { + std::lock_guard lock(mutex_); + + json_path_ = json_path; + enabled_ = true; + + std::ifstream file(json_path); + if (!file.is_open()) { + return; + } + + try { + std::stringstream buffer; + buffer << file.rdbuf(); + std::istringstream in(buffer.str()); + + SkipTo(in, '{'); + std::string key; + while ((key = NextKey(in)) != "") { + if (key == "version") { + int version = static_cast(ParseInteger(in)); + if (version != 1) { + std::cerr << "[TuningManager] Warning: tuning.json version " + << version << " not supported (expected 1)" << std::endl; + return; + } + } else if (key == "entries") { + SkipTo(in, '['); + SkipWhitespace(in); + while (in && in.peek() != ']') { + SkipTo(in, '{'); + std::string op_name; + Device::Type device = Device::Type::kCount; + TuningSignature sig; + std::size_t best_impl = 0; + + while ((key = NextKey(in)) != "") { + if (key == "operator") { + op_name = ParseString(in); + } else if (key == "device") { + std::string dev_str = ParseString(in); + if (dev_str == "cpu") + device = Device::Type::kCpu; + else if (dev_str == "nvidia") + device = Device::Type::kNvidia; + else if (dev_str == "cambricon") + device = Device::Type::kCambricon; + else if (dev_str == "ascend") + device = Device::Type::kAscend; + else if (dev_str == "metax") + device = Device::Type::kMetax; + else if (dev_str == "moore") + device = Device::Type::kMoore; + else if (dev_str == "iluvatar") + device = Device::Type::kIluvatar; + else if (dev_str == "hygon") + device = Device::Type::kHygon; + } else if (key == "signature") { + SkipTo(in, '{'); + while ((key = NextKey(in)) != "") { + if (key == "tensors") { + SkipTo(in, '['); + SkipWhitespace(in); + while (in && in.peek() != ']') { + SkipTo(in, '{'); + TuningSignature::TensorSig tsig; + while ((key = NextKey(in)) != "") { + if (key == "shape") { + SkipTo(in, '['); + SkipWhitespace(in); + while (in && in.peek() != ']') { + tsig.shape.push_back(ParseInteger(in)); + SkipWhitespace(in); + if (in.peek() == ',') in.get(); + SkipWhitespace(in); + } + if (in.peek() == ']') in.get(); + } else if (key == "dtype") { + tsig.dtype = static_cast(ParseInteger(in)); + } else { + SkipTo(in, ','); + } + } + if (in.peek() == '}') in.get(); + sig.tensors.push_back(tsig); + SkipWhitespace(in); + if (in.peek() == ',') in.get(); + SkipWhitespace(in); + } + if (in.peek() == ']') in.get(); + } else if (key == "scalars") { + SkipTo(in, '['); + SkipWhitespace(in); + while (in && in.peek() != ']') { + sig.scalars.push_back(ParseNumber(in)); + SkipWhitespace(in); + if (in.peek() == ',') in.get(); + SkipWhitespace(in); + } + if (in.peek() == ']') in.get(); + } else { + SkipTo(in, ','); + } + } + if (in.peek() == '}') in.get(); + } else if (key == "best_implementation") { + best_impl = static_cast(ParseInteger(in)); + } else if (key == "metadata") { + int depth = 0; + SkipWhitespace(in); + char c = in.get(); + if (c == '{') depth = 1; + while (depth > 0 && in) { + c = in.get(); + if (c == '{') + depth++; + else if (c == '}') + depth--; + } + } else { + SkipTo(in, ','); + } + } + + if (in.peek() == '}') in.get(); + + if (!op_name.empty() && device != Device::Type::kCount) { + CacheKey cache_key{op_name, device, sig}; + cache_[cache_key] = best_impl; + } + + SkipWhitespace(in); + if (in.peek() == ',') in.get(); + SkipWhitespace(in); + } + } else { + SkipTo(in, ','); + } + } + + std::cout << "[TuningManager] Loaded " << cache_.size() + << " tuning entries from " << json_path << std::endl; + + } catch (...) { + std::cerr << "[TuningManager] Warning: failed to parse " << json_path + << ", starting with an empty cache" << std::endl; + cache_.clear(); + } +} + +std::optional TuningManager::Lookup( + const std::string& operator_name, Device::Type device, + const TuningSignature& signature) const { + if (!enabled_) return std::nullopt; + + std::lock_guard lock(mutex_); + CacheKey key{operator_name, device, signature}; + auto it = cache_.find(key); + if (it != cache_.end()) { + return it->second; + } + return std::nullopt; +} + +void TuningManager::Record(const std::string& operator_name, + Device::Type device, + const TuningSignature& signature, + std::size_t best_index) { + if (!enabled_) return; + + std::lock_guard lock(mutex_); + CacheKey key{operator_name, device, signature}; + cache_[key] = best_index; + FlushToDiskLocked(); +} + +void TuningManager::FlushToDiskLocked() const { + std::ofstream out(json_path_, std::ios::trunc); + if (!out.is_open()) { + std::cerr << "[TuningManager] Warning: cannot write tuning cache to " + << json_path_ << std::endl; + return; + } + + out << "{\n"; + out << " \"version\": 1,\n"; + out << " \"entries\": [\n"; + + std::size_t entry_index = 0; + for (const auto& [key, best_impl] : cache_) { + out << " {\n"; + out << " \"operator\": \"" << key.operator_name << "\",\n"; + out << " \"device\": \"" << Device::StringFromType(key.device) + << "\",\n"; + out << " \"signature\": {\n"; + + out << " \"tensors\": ["; + for (std::size_t i = 0; i < key.signature.tensors.size(); ++i) { + const auto& t = key.signature.tensors[i]; + out << (i == 0 ? "\n" : ",\n"); + out << " {\"shape\": ["; + for (std::size_t d = 0; d < t.shape.size(); ++d) { + out << (d == 0 ? "" : ", ") << t.shape[d]; + } + out << "], \"dtype\": " << static_cast(t.dtype) << "}"; + } + out << (key.signature.tensors.empty() ? "" : "\n ") << "],\n"; + + out << " \"scalars\": ["; + for (std::size_t i = 0; i < key.signature.scalars.size(); ++i) { + out << (i == 0 ? "" : ", ") << key.signature.scalars[i]; + } + out << "]\n"; + + out << " },\n"; + out << " \"best_implementation\": " << best_impl << "\n"; + out << " }" << (++entry_index < cache_.size() ? "," : "") << "\n"; + } + + out << " ]\n"; + out << "}\n"; +} + +} // namespace infini::ops diff --git a/src/tuning.h b/src/tuning.h new file mode 100644 index 000000000..691e3bc5d --- /dev/null +++ b/src/tuning.h @@ -0,0 +1,167 @@ +#ifndef INFINI_OPS_TUNING_H_ +#define INFINI_OPS_TUNING_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "data_type.h" +#include "device.h" +#include "tensor.h" + +namespace infini::ops { + +struct TuningSignature { + struct TensorSig { + std::vector shape; + DataType dtype; + + bool operator==(const TensorSig& other) const { + return shape == other.shape && dtype == other.dtype; + } + }; + + std::vector tensors; + std::vector scalars; + + template + static TuningSignature Build(const Args&... args) { + TuningSignature sig; + (sig.Absorb(args), ...); + return sig; + } + + bool operator==(const TuningSignature& other) const { + return tensors == other.tensors && scalars == other.scalars; + } + + std::size_t Hash() const { + std::size_t h = 0; + for (const auto& t : tensors) { + for (auto dim : t.shape) { + h ^= std::hash{}(dim) + 0x9e3779b9 + (h << 6) + (h >> 2); + } + h ^= std::hash{}(static_cast(t.dtype)) + 0x9e3779b9 + + (h << 6) + (h >> 2); + } + for (auto s : scalars) { + h ^= std::hash{}(s) + 0x9e3779b9 + (h << 6) + (h >> 2); + } + return h; + } + + private: + void Absorb(const Tensor& t) { + std::vector shape_vec; + for (std::size_t i = 0; i < t.shape().size(); ++i) { + shape_vec.push_back(static_cast(t.shape()[i])); + } + tensors.push_back({shape_vec, t.dtype()}); + } + + void Absorb(const std::optional& t) { + if (t.has_value()) { + Absorb(*t); + } + } + + void Absorb(const std::vector& ts) { + for (const auto& t : ts) { + Absorb(t); + } + } + + template + void Absorb(const T& v) { + if constexpr (std::is_arithmetic_v) { + scalars.push_back(static_cast(v)); + } else if constexpr (std::is_enum_v) { + scalars.push_back(static_cast(static_cast(v))); + } + } + + template + void Absorb(const std::optional& v) { + if (v.has_value()) { + Absorb(*v); + } + } +}; + +} // namespace infini::ops + +namespace std { + +template <> +struct hash { + std::size_t operator()(const infini::ops::TuningSignature& sig) const { + return sig.Hash(); + } +}; + +} // namespace std + +namespace infini::ops { + +class TuningManager { + public: + static TuningManager& Instance(); + + void LoadTuningCache(const std::string& json_path); + + std::optional Lookup(const std::string& operator_name, + Device::Type device, + const TuningSignature& signature) const; + + void Record(const std::string& operator_name, Device::Type device, + const TuningSignature& signature, std::size_t best_index); + + bool IsEnabled() const { return enabled_; } + + private: + TuningManager() = default; + + TuningManager(const TuningManager&) = delete; + + TuningManager& operator=(const TuningManager&) = delete; + + struct CacheKey { + std::string operator_name; + Device::Type device; + TuningSignature signature; + + bool operator==(const CacheKey& other) const { + return operator_name == other.operator_name && device == other.device && + signature == other.signature; + } + }; + + struct CacheKeyHash { + std::size_t operator()(const CacheKey& key) const { + std::size_t h = std::hash{}(key.operator_name); + h ^= std::hash{}(static_cast(key.device)) + 0x9e3779b9 + + (h << 6) + (h >> 2); + h ^= key.signature.Hash() + 0x9e3779b9 + (h << 6) + (h >> 2); + return h; + } + }; + + void FlushToDiskLocked() const; + + std::unordered_map cache_; + + bool enabled_{false}; + + std::string json_path_{"tuning.json"}; + + mutable std::mutex mutex_; +}; + +} // namespace infini::ops + +#endif // INFINI_OPS_TUNING_H_ diff --git a/src/tuning_utils.h b/src/tuning_utils.h new file mode 100644 index 000000000..f1d55e522 --- /dev/null +++ b/src/tuning_utils.h @@ -0,0 +1,91 @@ +#ifndef INFINI_OPS_TUNING_UTILS_H_ +#define INFINI_OPS_TUNING_UTILS_H_ + +#include +#include +#include +#include +#include + +#include "device.h" +#include "tensor.h" + +namespace infini::ops { + +namespace detail { + +template +std::string ExtractOperatorName() { +#if defined(__GNUC__) || defined(__clang__) + std::string_view sig = __PRETTY_FUNCTION__; + + auto key_pos = sig.find("Key = "); + if (key_pos == std::string_view::npos) return "UnknownOp"; + + key_pos += 6; + auto end_pos = sig.find_first_of("]>;", key_pos); + std::string full_name(sig.substr(key_pos, end_pos - key_pos)); + + auto last_colon = full_name.rfind("::"); + if (last_colon != std::string::npos) { + return full_name.substr(last_colon + 2); + } + return full_name; +#elif defined(_MSC_VER) + std::string_view sig = __FUNCSIG__; + auto key_pos = sig.find("Key="); + if (key_pos == std::string_view::npos) return "UnknownOp"; + key_pos += 4; + auto end_pos = sig.find_first_of("]>,", key_pos); + std::string full_name(sig.substr(key_pos, end_pos - key_pos)); + auto last_colon = full_name.rfind("::"); + if (last_colon != std::string::npos) { + return full_name.substr(last_colon + 2); + } + return full_name; +#else + return "UnknownOp"; +#endif +} + +inline int EnvInt(const char* name, int fallback) { + const char* v = std::getenv(name); + if (!v || !*v) return fallback; + int parsed = std::atoi(v); + return parsed > 0 ? parsed : fallback; +} + +inline Device::Type FirstDeviceTypeHelper(bool& found) { + found = false; + return Device::Type::kCount; +} + +template +Device::Type FirstDeviceTypeHelper(bool& found, const First& first, + const Rest&... rest) { + if constexpr (std::is_same_v, Tensor>) { + found = true; + return first.device().type(); + } else if constexpr (std::is_same_v, + std::vector>) { + if (!first.empty()) { + found = true; + return first.front().device().type(); + } + return FirstDeviceTypeHelper(found, rest...); + } else { + return FirstDeviceTypeHelper(found, rest...); + } +} + +template +Device::Type FirstDeviceType(const Args&... args) { + bool found = false; + return FirstDeviceTypeHelper(found, args...); +} + +} // namespace detail + +} // namespace infini::ops + +#endif // INFINI_OPS_TUNING_UTILS_H_ diff --git a/tests/test_generate_wrappers.py b/tests/test_generate_wrappers.py index e571f6c90..9f3385789 100644 --- a/tests/test_generate_wrappers.py +++ b/tests/test_generate_wrappers.py @@ -164,15 +164,15 @@ class Mul { text = module._generate_pybind11(operator) assert "std::size_t DefaultImplementationIndexForMul" in text + # Constructor still uses DefaultImplementationIndex directly assert ( "config.set_implementation_index(" "DefaultImplementationIndexForMul(DeviceFromPybind11Handle(input).type()))" ) in text assert "std::optional implementation_index" in text - assert ( - "implementation_index.value_or(" - "DefaultImplementationIndexForMul(DeviceFromPybind11Handle(input).type()))" - ) in text + # Free function now uses has_value() to support auto-tuning + assert "if (implementation_index.has_value())" in text + assert "config.set_implementation_index(*implementation_index)" in text assert 'py::arg("implementation_index") = py::none()' in text