mirror of
https://github.com/zebrajr/pytorch.git
synced 2025-12-07 00:21:07 +01:00
* Allow `__constant__` values in a ScriptModule to be used as attributes for builtin functions * Fix bugs in @script loops 1. while loops run shape propagation multiple times until the shapes have converged. There were two bugs here. (a) First the 'changed' condition was not checking if it actually changed the output, and instead would mark changed = true if the two inputs were different. This incorrect because the output of the block and the input of the block may always have different shapes. Now it actually checks if it is about to change the output entry that it is writing to. (b) expand nodes were being inserted into the graph even inside the while loop body. However, if we iteratively discover that the input shape to one of these expands is actual dynamic, then it was incorrect to insert the expand in the first place. This changes it so that we only insert expands after we have converged on the shapes. 2. the way deleteExtraInputs removed loop-carried dependencies was unsafe because it would lookup Value* elements in the loop body's environment that were previously invalidated when deleteExtraInputs remove another input to the loop. This changes the way deleteExtraInputs works so that it never has to read a value out of the loop body's environment to avoid using the invalidated pointers.
402 lines
15 KiB
C++
402 lines
15 KiB
C++
#include "torch/csrc/jit/script/init.h"
|
|
#include "torch/csrc/jit/script/compiler.h"
|
|
|
|
namespace torch {
|
|
namespace jit {
|
|
namespace script {
|
|
|
|
using ResolutionCallback = std::function<py::function(std::string)>;
|
|
|
|
// The visibility attribute is to avoid a warning about storing a field in the
|
|
// struct that has a different visibility (from pybind) than the struct.
|
|
#ifdef _WIN32
|
|
#define VISIBILITY_HIDDEN
|
|
#else
|
|
#define VISIBILITY_HIDDEN __attribute__((visibility("hidden")))
|
|
#endif
|
|
|
|
static std::string typeString(py::handle h) {
|
|
return py::str(h.get_type().attr("__name__"));
|
|
}
|
|
|
|
struct VISIBILITY_HIDDEN PythonValue : public SugaredValue {
|
|
PythonValue(py::object self)
|
|
: self(std::move(self)) {}
|
|
|
|
// call it like a function, e.g. `outputs = this(inputs)`
|
|
virtual std::shared_ptr<SugaredValue> call(SourceRange loc, Method & m, at::ArrayRef<Value*> inputs, at::ArrayRef<NamedValue> attributes, size_t n_binders) override {
|
|
ensureTensors(loc, inputs);
|
|
|
|
if (attributes.size() > 0)
|
|
throw ErrorReport(loc) << "keyword arguments in Python calls aren't supported";
|
|
Graph& g = *m.graph();
|
|
|
|
// this python object might be a @trace or @script stand-alone function
|
|
// if so, inline the graph rather than calling the python
|
|
if(py::isinstance<GraphExecutor>(self)) {
|
|
GraphExecutor& ge = py::cast<GraphExecutor&>(self);
|
|
ensureSizeMatches(loc, ge.graph()->inputs().size(), inputs.size(), "arguments");
|
|
return packOutputs(*m.graph(),inlineCallTo(*m.graph(), *ge.graph(), inputs));
|
|
}
|
|
|
|
// Release the function object so we can wrap it in a PythonOp
|
|
py::object func = self;
|
|
std::string cconv(inputs.size(), 't');
|
|
Node* new_node = g.insertNode(g.createPythonOp(
|
|
THPObjectPtr(func.release().ptr()), cconv, {}, {}, false));
|
|
new_node->setSourceLocation(std::make_shared<SourceRange>(loc));
|
|
for(auto i : inputs)
|
|
new_node->addInput(i);
|
|
std::vector<Value*> outputs;
|
|
for(size_t i = 0; i < n_binders; ++i)
|
|
outputs.push_back(new_node->addOutput());
|
|
return packOutputs(*m.graph(), outputs);
|
|
}
|
|
|
|
virtual std::shared_ptr<SugaredValue> attr(SourceRange loc, Method & m, const std::string& field) override {
|
|
// We generally don't want to allow traversing arbitrary Python objects, but we
|
|
// make an exception for traversing modules because we want to be access
|
|
// torch, torch.nn.functional, and the functions they expose.
|
|
py::object member = getattr(loc, field);
|
|
if (isBuiltinModule() && py::isinstance<py::function>(member)) {
|
|
return std::make_shared<BuiltinFunction>(field);
|
|
}
|
|
if (py::isinstance<py::module>(self) && py::isinstance<py::module>(member)) {
|
|
return std::make_shared<PythonValue>(member);
|
|
}
|
|
throw ErrorReport(loc) << "unsupported attribute lookup on " << py::repr(self) << ".";
|
|
}
|
|
|
|
virtual std::string kind() const override {
|
|
std::stringstream ss;
|
|
ss << "python value of type '" << typeString(self) << "'";
|
|
return ss.str();
|
|
}
|
|
|
|
protected:
|
|
bool isBuiltinModule() {
|
|
// XXX: these can't be static, or they will be destructed after the Python interpreter
|
|
// exits and that generally sounds like a bad idea
|
|
py::object torch = py::module::import("torch");
|
|
py::object functional = py::module::import("torch.nn.functional");
|
|
return self.is(torch) || self.is(functional);
|
|
}
|
|
|
|
py::object getattr(SourceRange loc, const std::string& name) {
|
|
try {
|
|
return py::getattr(self, name.c_str());
|
|
} catch (py::error_already_set& e) {
|
|
throw ErrorReport(loc) << "object has no attribute " << name;
|
|
}
|
|
}
|
|
|
|
py::object self;
|
|
};
|
|
|
|
// by using torch.jit.Const, a user can mark a python value constant
|
|
// we then make that value immutable.
|
|
// once marked constant, we enable additional behavior such as
|
|
// 1. conversion via asValue to a constant Tensor
|
|
// 2. unrolling of for loops
|
|
struct VISIBILITY_HIDDEN ConstantPythonValue : public PythonValue {
|
|
using PythonValue::PythonValue;
|
|
virtual Value * asValue(SourceRange loc, Method & m) override {
|
|
|
|
return PythonValue::asValue(loc, m);
|
|
}
|
|
virtual std::vector<std::shared_ptr<SugaredValue>> asTuple(SourceRange loc, Method& m) override {
|
|
if(!py::isinstance<py::tuple>(self))
|
|
return PythonValue::asTuple(loc, m);
|
|
|
|
py::tuple tup = self;
|
|
std::vector<std::shared_ptr<SugaredValue>> result;
|
|
for(size_t i = 0; i < tup.size(); ++i) {
|
|
result.push_back(create(loc, m, tup[i]));
|
|
}
|
|
return result;
|
|
}
|
|
static std::shared_ptr<SugaredValue> create(SourceRange loc, Method& m, py::object self) {
|
|
// directly create SimpleValues when possible, because they are first-class
|
|
// and can be re-assigned. Otherwise, this would be invalid:
|
|
// f = python_constant
|
|
// while ...
|
|
// f = f + 1
|
|
if(py::isinstance<py::int_>(self)) {
|
|
return createConstant(loc, m, at::CPU(at::kLong).scalarTensor(py::cast<int64_t>(self)));
|
|
} else if(py::isinstance<py::float_>(self)) {
|
|
return createConstant(loc, m, at::CPU(at::kFloat).scalarTensor(py::cast<float>(self)));
|
|
} else if(py::isinstance<py::bool_>(self)) {
|
|
return createConstant(loc, m, at::CPU(at::kByte).scalarTensor(py::cast<bool>(self)));
|
|
}
|
|
return std::make_shared<ConstantPythonValue>(self);
|
|
}
|
|
private:
|
|
static std::shared_ptr<SugaredValue> createConstant(SourceRange loc, Method& m, const at::Tensor& val) {
|
|
auto n = m.graph()->createConstant(val);
|
|
n->setSourceLocation(std::make_shared<SourceRange>(loc));
|
|
return std::make_shared<SimpleValue>(m.graph()->insertNode(n)->output());
|
|
}
|
|
};
|
|
|
|
Resolver pythonResolver(ResolutionCallback rcb) {
|
|
return [=](const std::string& name) -> std::shared_ptr<SugaredValue> {
|
|
AutoGIL ag;
|
|
py::object obj = rcb(name);
|
|
if(obj.is(py::none())) {
|
|
return nullptr;
|
|
}
|
|
return std::make_shared<PythonValue>(obj);
|
|
};
|
|
}
|
|
|
|
// defines how modules/methods behave inside the script subset.
|
|
// for now this does not have any interaction with python.
|
|
// in the future, we will add the ability to resolve `self.foo` to python
|
|
// {functions, modules, contants} so this SugaredValue is defined here
|
|
// anticipating we will eventually need to replace Module with a py::object
|
|
// holding the actual nn.Module class.
|
|
|
|
// defines how a method obtained from a module behaves in script
|
|
struct MethodValue : public SugaredValue {
|
|
MethodValue(std::shared_ptr<Module> module, Method& method)
|
|
: module(std::move(module)) //insurance that method stays alive
|
|
, method(method) {}
|
|
std::string kind() const override {
|
|
return "method";
|
|
}
|
|
virtual std::shared_ptr<SugaredValue> call(SourceRange loc, Method & caller, at::ArrayRef<Value*> inputs, at::ArrayRef<NamedValue> attributes, size_t n_binders) override {
|
|
if(attributes.size() != 0) {
|
|
throw ErrorReport(loc) << "not yet implemented - calls to script methods using keyword arguments";
|
|
}
|
|
return packOutputs(*caller.graph(), caller.emit_call_to(loc, method, inputs));
|
|
}
|
|
private:
|
|
std::shared_ptr<Module> module;
|
|
Method& method;
|
|
|
|
};
|
|
|
|
|
|
struct ModuleValue : public SugaredValue {
|
|
ModuleValue(std::shared_ptr<Module> module)
|
|
: module(std::move(module)) {}
|
|
|
|
virtual std::string kind() const override {
|
|
return "module";
|
|
}
|
|
|
|
// select an attribute on it, e.g. `this.field`
|
|
virtual std::shared_ptr<SugaredValue> attr(SourceRange loc, Method & m, const std::string& field) override {
|
|
if(at::optional<NamedModule&> v = module->find_module(field)) {
|
|
return std::make_shared<ModuleValue>(v->module);
|
|
} else if(at::optional<Method&> v = module->find_method(field)) {
|
|
return std::make_shared<MethodValue>(module, *v);
|
|
} else if(at::optional<NamedParameter&> v = module->find_parameter(field)) {
|
|
return std::make_shared<SimpleValue>(m.get_or_add_parameter(v->slot()));
|
|
}
|
|
// This can also be a call to a non-script module, or a plain
|
|
// python method. If so return this as a python value.
|
|
py::object py_module = py::cast(module);
|
|
if(py::object attr = py::getattr(py_module, field.c_str(), py::none())) {
|
|
if(py::isinstance<py::function>(attr) ||
|
|
py::isinstance(attr, py::module::import("torch.nn").attr("Module"))) {
|
|
return std::make_shared<PythonValue>(attr);
|
|
} else if(py_module.attr("_constants_set").contains(field.c_str())) {
|
|
return ConstantPythonValue::create(loc, m, attr);
|
|
} else {
|
|
throw ErrorReport(loc) << "attribute '" << field << "' of type '" << typeString(attr) << "' is not usable in a script method (did you forget to add it __constants__?)";
|
|
}
|
|
}
|
|
throw ErrorReport(loc) << "module has no attribute '" << field << "'";
|
|
}
|
|
// call module.forward
|
|
virtual std::shared_ptr<SugaredValue> call(SourceRange loc, Method & caller, at::ArrayRef<Value*> inputs, at::ArrayRef<NamedValue> attributes, size_t n_binders) override {
|
|
return attr(loc, caller, "forward")->call(loc, caller, inputs, attributes, n_binders);
|
|
}
|
|
|
|
virtual std::vector<std::shared_ptr<SugaredValue>> asTuple(SourceRange loc, Method& m) override {
|
|
py::object py_module = py::cast(module);
|
|
if(!py::isinstance(py_module, py::module::import("torch.jit").attr("_ConstModuleList")))
|
|
return SugaredValue::asTuple(loc, m);
|
|
std::vector<std::shared_ptr<SugaredValue>> result;
|
|
for(py::handle module : py_module) {
|
|
py::object obj = py::reinterpret_borrow<py::object>(module);
|
|
if(py::isinstance<Module>(obj)) {
|
|
auto r = py::cast<std::shared_ptr<Module>>(obj);
|
|
result.push_back(std::make_shared<ModuleValue>(r));
|
|
} else {
|
|
result.push_back(ConstantPythonValue::create(loc, m, obj));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<Module> module;
|
|
};
|
|
|
|
|
|
// TODO: dedup with other init
|
|
|
|
// we cannot use the default py:cast<autograd::Variable> because it currently
|
|
// unwraps the data tensor in the conversion process
|
|
|
|
variable_tensor_list createVariableTensorList(py::tuple tuple, size_t reserve_extra_space = 0) {
|
|
variable_tensor_list result;
|
|
result.reserve(tuple.size() + reserve_extra_space);
|
|
for(auto e : tuple) {
|
|
result.push_back(py::cast<autograd::Variable>(e));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
py::object unpackVariableTensorList(std::vector<at::Tensor> outputs) {
|
|
// if we don't tell pybind these are variables it chokes on the
|
|
// conversion.
|
|
// TODO: fix conversions to be sane and make sure this works.
|
|
if (outputs.size() == 0) {
|
|
return py::none();
|
|
} else if (outputs.size() == 1) {
|
|
return py::cast(static_cast<autograd::Variable&>(outputs[0]));
|
|
} else {
|
|
py::tuple tuple(outputs.size());
|
|
for(size_t i = 0; i < outputs.size(); i++) {
|
|
tuple[i] = py::cast(static_cast<autograd::Variable&>(outputs[i]));
|
|
}
|
|
return tuple;
|
|
}
|
|
}
|
|
|
|
static void gatherParametersAndBuffers(std::vector<at::Tensor*> & values, const Module & m) {
|
|
for(auto & params : m.get_parameters()) {
|
|
values.push_back(params.slot());
|
|
}
|
|
for(const auto & sub : m.get_modules()) {
|
|
gatherParametersAndBuffers(values, *sub.module);
|
|
}
|
|
}
|
|
|
|
void initJitScriptBindings(PyObject* module) {
|
|
auto m = py::handle(module).cast<py::module>();
|
|
// torch.jit.ScriptModule is a subclass of this C++ object.
|
|
// Methods here are prefixed with _ since they should not be
|
|
// public.
|
|
py::class_<Module, std::shared_ptr<Module>>(m, "ScriptModule")
|
|
.def(py::init<>())
|
|
.def("_set_optimized", &Module::set_optimized)
|
|
.def(
|
|
"_define",
|
|
[](Module& m,
|
|
const std::string& script,
|
|
ResolutionCallback rcb, bool has_self) {
|
|
auto self = has_self ? std::make_shared<ModuleValue>(m.shared_from_this()) : nullptr;
|
|
return defineMethodsInModule(m, script, pythonResolver(rcb), self);
|
|
})
|
|
.def("_create_methods", [](Module& m, const std::vector<Def>& defs, const std::vector<ResolutionCallback>& rcbs) {
|
|
std::vector<Resolver> resolvers;
|
|
for(auto & callback : rcbs) {
|
|
resolvers.push_back(pythonResolver(callback));
|
|
}
|
|
defineMethodsInModule(
|
|
m,
|
|
defs,
|
|
resolvers,
|
|
std::make_shared<ModuleValue>(m.shared_from_this()));
|
|
})
|
|
.def("_get_method",
|
|
[](Module& self, const std::string& name) -> const Method& {
|
|
return self.get_method(name);
|
|
}, py::return_value_policy::reference_internal)
|
|
.def("_register_parameter", &Module::register_parameter)
|
|
.def("_register_module", &Module::register_module)
|
|
.def("_set_parameter", &Module::set_parameter)
|
|
.def("_get_parameter", &Module::get_parameter)
|
|
.def("_get_module", &Module::get_module)
|
|
.def("_get_modules", [](Module& self) -> py::tuple {
|
|
auto & modules = self.get_modules();
|
|
py::tuple result(modules.size());
|
|
for(size_t i = 0; i < modules.size(); ++i) {
|
|
auto & nm = modules[i];
|
|
result[i] = std::make_pair(nm.name, nm.module);
|
|
}
|
|
return result;
|
|
})
|
|
.def("_get_parameters", [](Module& self) -> py::tuple {
|
|
auto & parameters = self.get_parameters();
|
|
py::tuple result(parameters.size());
|
|
for(size_t i = 0; i < parameters.size(); ++i) {
|
|
auto & p = parameters[i];
|
|
py::tuple r(3);
|
|
result[i] = std::make_tuple(
|
|
p.name,
|
|
static_cast<const autograd::Variable&>(*p.slot()),
|
|
p.is_buffer);
|
|
|
|
}
|
|
return result;
|
|
})
|
|
.def("_has_parameter", [](Module& self, const std::string& name) {
|
|
if(auto r = self.find_parameter(name)) {
|
|
return !r->is_buffer;
|
|
}
|
|
return false;
|
|
})
|
|
.def("_has_buffer", [](Module& self, const std::string& name) {
|
|
if(auto r = self.find_parameter(name)) {
|
|
return r->is_buffer;
|
|
}
|
|
return false;
|
|
})
|
|
.def("_has_module", [](Module& self, const std::string& name) {
|
|
return bool(self.find_module(name));
|
|
})
|
|
.def("_has_method", [](Module& self, const std::string& name) {
|
|
return bool(self.find_method(name));
|
|
})
|
|
.def("_method_names", [](Module& self) {
|
|
return fmap(self.get_methods(), [](const std::unique_ptr<Method> & m) {
|
|
return m->name();
|
|
});
|
|
})
|
|
.def("_create_method_from_trace", [](
|
|
Module& self,
|
|
const std::string& name,
|
|
py::function func,
|
|
tracer::variable_list inputs) {
|
|
size_t num_inputs = inputs.size();
|
|
// prereq: Module's buffers and parameters are unique
|
|
// this was ensured in python before calling this function
|
|
std::vector<at::Tensor*> parameters;
|
|
gatherParametersAndBuffers(parameters, self);
|
|
for(at::Tensor* param : parameters) {
|
|
inputs.push_back(static_cast<autograd::Variable&>(*param));
|
|
}
|
|
auto graph = tracer::createGraphByTracing(func, std::move(inputs), num_inputs);
|
|
self.create_method(name, std::move(graph), std::move(parameters));
|
|
});
|
|
|
|
py::class_<Method>(m, "ScriptMethod")
|
|
.def("graph", [&](Method& self) {
|
|
return self.graph();
|
|
})
|
|
.def("__call__", [](Method& m, py::args args) -> py::object {
|
|
auto inputs = createVariableTensorList(args);
|
|
auto outputs = m.run(std::move(inputs));
|
|
return unpackVariableTensorList(std::move(outputs));
|
|
})
|
|
.def_property_readonly("graph", [](Method& m) {
|
|
return m.graph();
|
|
})
|
|
.def("propagate_shapes", &Method::propagate_shapes)
|
|
.def("params", &Method::params);
|
|
|
|
m.def("_jit_script_compile", [](Def def, ResolutionCallback rcb) {
|
|
return compileFunction(def, pythonResolver(rcb));
|
|
});
|
|
|
|
}
|
|
|
|
} // namespace script
|
|
} // namespace jit
|
|
} // namespace torch
|