Files
paddlepaddle--paddle/paddle/fluid/pybind/torch_compat.h
T
2026-07-13 12:40:42 +08:00

420 lines
15 KiB
C++

// Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <torch/library.h>
#include <vector>
#include "paddle/common/exception.h"
#include "paddle/fluid/pybind/eager_utils.h"
#include "paddle/fluid/pybind/op_function_common.h"
#include "paddle/phi/api/include/compat/utils/scalar_type_conversion.h"
#include "paddle/utils/pybind.h"
namespace py = pybind11;
namespace torch {
class OperationInvoker {
public:
static py::object invoke_operator_from_python(
const std::string& qualified_name,
const py::args& args,
const py::kwargs& kwargs);
static std::pair<const CppFunction*, FunctionArgs> get_op_with_args(
const std::string& qualified_name,
const py::args& args,
const py::kwargs& kwargs);
static py::object to_py_object(const torch::IValue& value);
static torch::IValue to_ivalue(py::handle obj);
static py::object create_python_callable(const std::string& qualified_name);
static FunctionArgs convert_args_kwargs_to_function_args(
const py::args& args, const py::kwargs& kwargs);
static py::object convert_result_to_python(const FunctionResult& result);
};
inline py::object OperationInvoker::invoke_operator_from_python(
const std::string& qualified_name,
const py::args& args,
const py::kwargs& kwargs) {
try {
auto [found_op, function_args] =
get_op_with_args(qualified_name, args, kwargs);
FunctionResult result;
{
py::gil_scoped_release no_gil_guard;
result = found_op->call_with_args(function_args);
}
return convert_result_to_python(result);
} catch (const std::exception& e) {
PADDLE_THROW(common::errors::PreconditionNotMet(
"Error in operator '%s': %s", qualified_name.c_str(), e.what()));
}
}
inline std::pair<const CppFunction*, FunctionArgs>
OperationInvoker::get_op_with_args(const std::string& qualified_name,
const py::args& args,
const py::kwargs& kwargs) {
auto* op = OperatorRegistry::instance().find_operator(qualified_name);
if (!op) {
PADDLE_THROW(common::errors::NotFound(
"Operator '%s' not found in the registry", qualified_name.c_str()));
}
auto impl_it = op->implementations.end();
const std::vector<c10::DispatchKey> preferred_keys = {
c10::DispatchKey::CPU,
c10::DispatchKey::BackendSelect,
c10::DispatchKey::CatchAll};
for (const auto& key : preferred_keys) {
impl_it = op->implementations.find(key);
if (impl_it != op->implementations.end()) {
break;
}
}
// If no preferred dispatch key was found, allow the call only when exactly
// one implementation is registered (deterministic). With multiple unknown
// keys the choice would be arbitrary (unordered_map has no stable iteration
// order), so we surface an Ambiguous error instead.
if (impl_it == op->implementations.end()) {
if (op->implementations.size() == 1) {
impl_it = op->implementations.begin();
} else if (op->implementations.empty()) {
PADDLE_THROW(common::errors::NotFound(
"No implementation found for operator '%s'", qualified_name.c_str()));
} else {
std::string available_keys;
for (const auto& kv : op->implementations) {
if (!available_keys.empty()) available_keys += ", ";
available_keys += c10::toString(kv.first);
}
PADDLE_THROW(common::errors::InvalidArgument(
"Operator '%s' has multiple implementations [%s] but none matches "
"the preferred dispatch keys (CPU, BackendSelect, CatchAll). "
"Register under one of those keys to make the operator callable "
"from Python.",
qualified_name.c_str(),
available_keys.c_str()));
}
}
FunctionArgs function_args =
convert_args_kwargs_to_function_args(args, kwargs);
return std::make_pair(&impl_it->second, std::move(function_args));
}
inline py::object OperationInvoker::to_py_object(const torch::IValue& value) {
if (value.is_none()) {
return py::none();
} else if (value.is_bool()) {
return py::cast(value.to_bool());
} else if (value.is_int()) {
return py::cast(value.to_int());
} else if (value.is_double()) {
return py::cast(value.to_double());
} else if (value.is_string()) {
return py::cast(value.to_string());
} else if (value.is_tensor()) {
return py::reinterpret_borrow<py::object>(
paddle::pybind::ToPyObject(value.to_tensor()._PD_GetInner()));
} else if (value.is_list()) {
auto ivalue_list = value.to_list();
py::list py_list;
for (const auto& item : ivalue_list) {
py_list.append(to_py_object(item));
}
return py_list;
} else if (value.is_tuple()) {
auto ivalue_tuple = value.to_tuple();
size_t size = ivalue_tuple.size();
py::tuple py_tuple(size);
for (size_t i = 0; i < size; ++i) {
py_tuple[i] = to_py_object(ivalue_tuple[i]);
}
return py_tuple;
} else {
PADDLE_THROW(common::errors::Unimplemented(
"Conversion of torch::IValue to Python object for type %s is not "
"implemented yet.",
value.type_string()));
}
}
inline torch::IValue OperationInvoker::to_ivalue(py::handle obj) {
if (obj.is_none()) {
return torch::IValue(); // None
} else if (py::isinstance<py::bool_>(obj)) {
return torch::IValue(py::cast<bool>(obj));
} else if (py::isinstance<py::int_>(obj)) {
return torch::IValue(py::cast<int64_t>(obj));
} else if (py::isinstance<py::float_>(obj)) {
return torch::IValue(py::cast<double>(obj));
} else if (py::isinstance<py::str>(obj)) {
return torch::IValue(py::cast<std::string>(obj));
} else if (paddle::pybind::PyCheckTensor(obj.ptr())) {
return torch::IValue(paddle::pybind::CastPyArg2Tensor(obj.ptr(), 0));
} else if (paddle::pybind::PyObject_CheckDataType(obj.ptr())) {
return torch::IValue(compat::_PD_PhiDataTypeToAtenScalarType(
paddle::pybind::CastPyArg2DataType(obj.ptr(), "to_ivalue", 0)));
} else if (py::isinstance<py::list>(obj)) {
auto list = obj.cast<py::list>();
std::vector<torch::IValue> ivalue_list;
ivalue_list.reserve(list.size());
for (auto item : list) {
ivalue_list.push_back(to_ivalue(item));
}
return torch::IValue(ivalue_list);
} else {
PADDLE_THROW(common::errors::Unimplemented(
"Conversion of Python object to torch::IValue for type %s is not "
"implemented yet.",
std::string(py::str(py::type::of(obj))).c_str()));
}
}
inline FunctionArgs OperationInvoker::convert_args_kwargs_to_function_args(
const py::args& args, const py::kwargs& kwargs) {
FunctionArgs function_args;
for (const auto& arg : args) {
torch::IValue value = to_ivalue(arg);
function_args.add_arg(std::move(value));
}
for (const auto& item : kwargs) {
std::string key = py::cast<std::string>(item.first);
torch::arg keyword(std::move(key));
keyword = to_ivalue(item.second);
function_args.add_arg(std::move(keyword));
}
return function_args;
}
inline py::object OperationInvoker::convert_result_to_python(
const FunctionResult& result) {
if (!result.has_value()) {
return py::none();
}
const torch::IValue& value = result.get_value();
return to_py_object(value);
}
inline py::object OperationInvoker::create_python_callable(
const std::string& qualified_name) {
return py::cpp_function(
[qualified_name](py::args args, py::kwargs kwargs) -> py::object {
return invoke_operator_from_python(qualified_name, args, kwargs);
},
py::name(qualified_name.c_str()),
py::is_method(py::none()));
}
class CustomClassProxyInstance {
public:
CustomClassProxyInstance(const std::string& qualified_name,
const IValue& instance)
: qualified_name_(qualified_name), instance_(instance) {}
// Get instance method
py::object __getattr__(const std::string& method_name) {
if (ClassRegistry::instance().has_method(qualified_name_, method_name)) {
return py::cpp_function(
[this, method_name](py::args args, py::kwargs kwargs) -> py::object {
FunctionArgs converted =
OperationInvoker::convert_args_kwargs_to_function_args(args,
kwargs);
FunctionArgs function_args;
function_args.add_arg(instance_); // this pointer
for (size_t i = 0; i < converted.size(); ++i) {
function_args.add_arg(converted.get_value(i));
}
for (const auto& [name, value] : converted.named_args()) {
torch::arg keyword(name);
keyword = value;
function_args.add_arg(std::move(keyword));
}
auto result = ClassRegistry::instance().call_method_with_args(
qualified_name_, method_name, function_args);
return OperationInvoker::convert_result_to_python(result);
},
py::name(method_name.c_str()));
}
PADDLE_THROW(common::errors::NotFound("Method '%s' not found in class %s",
method_name.c_str(),
qualified_name_.c_str()));
}
const IValue& get_instance() const { return instance_; }
private:
std::string qualified_name_;
IValue instance_;
};
class CustomClassProxy {
public:
CustomClassProxy(const std::string& qualified_name) // NOLINT
: qualified_name_(qualified_name) {}
// Create a new instance of the class
py::object __call__(const py::args& args, const py::kwargs& kwargs) {
try {
FunctionArgs function_args =
OperationInvoker::convert_args_kwargs_to_function_args(args, kwargs);
// Call the constructor
auto result = ClassRegistry::instance().call_constructor_with_args(
qualified_name_, function_args);
// Wrap the result in a CustomClassProxyInstance
if (result.has_value()) {
const IValue& value = result.get_value();
// Create proxy object for the custom class instance
return py::cast(CustomClassProxyInstance(qualified_name_, value));
} else {
PADDLE_THROW(common::errors::PreconditionNotMet(
"Constructor did not return an instance"));
}
} catch (const std::exception& e) {
PADDLE_THROW(common::errors::PreconditionNotMet(
"Failed to construct %s: %s", qualified_name_.c_str(), e.what()));
}
}
// Get static method
py::object __getattr__(const std::string& method_name) {
// Check if the method name is a dunder method
if (method_name.size() >= 2 && method_name.substr(0, 2) == "__") {
PADDLE_THROW(common::errors::InvalidArgument(
"Dunder methods are not supported: %s", method_name.c_str()));
}
// Check if the class has the static method
if (ClassRegistry::instance().has_static_method(qualified_name_,
method_name)) {
return py::cpp_function(
[this, method_name](py::args args, py::kwargs kwargs) -> py::object {
// Convert args and kwargs to FunctionArgs
FunctionArgs function_args =
OperationInvoker::convert_args_kwargs_to_function_args(args,
kwargs);
// Call the static method
auto result =
ClassRegistry::instance().call_static_method_with_args(
qualified_name_, method_name, function_args);
return OperationInvoker::convert_result_to_python(result);
},
py::name(method_name.c_str()));
}
PADDLE_THROW(
common::errors::NotFound("Static method '%s' not found in class %s",
method_name.c_str(),
qualified_name_.c_str()));
}
private:
std::string qualified_name_;
};
inline py::object get_custom_class_python_wrapper(
const std::string& namespace_name, const std::string& class_name) {
std::string qualified_name = namespace_name + "::" + class_name;
if (!ClassRegistry::instance().has_class(qualified_name)) {
PADDLE_THROW(common::errors::NotFound(
"Class '%s' not found in the registry", qualified_name.c_str()));
}
return py::cast(CustomClassProxy(qualified_name));
}
inline py::object get_operation(const std::string& qualified_name) {
return OperationInvoker::create_python_callable(qualified_name);
}
} // namespace torch
namespace paddle::pybind {
void BindTorchCompat(pybind11::module* m) {
py::class_<torch::IValue>(*m, "IValue")
.def(py::init<>())
.def(py::init<int>())
.def(py::init<double>())
.def(py::init<bool>())
.def(py::init<std::string>())
.def("is_none", &torch::IValue::is_none)
.def("is_int", &torch::IValue::is_int)
.def("is_double", &torch::IValue::is_double)
.def("is_bool", &torch::IValue::is_bool)
.def("is_string", &torch::IValue::is_string)
.def("to_int", &torch::IValue::to_int)
.def("to_double", &torch::IValue::to_double)
.def("to_bool", &torch::IValue::to_bool)
.def("to_string", &torch::IValue::to_string)
.def("__repr__", [](const torch::IValue& v) {
if (v.is_none()) return std::string("IValue(None)");
if (v.is_int())
return std::string("IValue(") + std::to_string(v.to_int()) + ")";
if (v.is_double())
return std::string("IValue(") + std::to_string(v.to_double()) + ")";
if (v.is_bool())
return std::string("IValue(") + (v.to_bool() ? "True" : "False") +
")";
if (v.is_string())
return std::string("IValue(\"") + v.to_string() + "\")";
return std::string("IValue(unknown)");
});
py::class_<torch::CustomClassProxy>(*m, "CustomClassProxy")
.def("__call__", &torch::CustomClassProxy::__call__)
.def("__getattr__", &torch::CustomClassProxy::__getattr__);
py::class_<torch::CustomClassProxyInstance>(*m, "CustomClassProxyInstance")
.def("__getattr__", &torch::CustomClassProxyInstance::__getattr__);
m->def("_get_operation",
&torch::get_operation,
"Get a callable for the specified operation",
py::arg("qualified_name"));
m->def("_get_custom_class_python_wrapper",
&torch::get_custom_class_python_wrapper,
"Get a Python wrapper for the specified custom class",
py::arg("namespace_name"),
py::arg("class_name"));
}
} // namespace paddle::pybind