basic testing of builtin alias annotations (#14588)
authorMichael Suo <suo@fb.com>
Tue, 4 Dec 2018 06:29:01 +0000 (22:29 -0800)
committerFacebook Github Bot <facebook-github-bot@users.noreply.github.com>
Tue, 4 Dec 2018 06:31:02 +0000 (22:31 -0800)
Summary:
Check whether the codegen'd alias annotations actually track alias creation and writes correctly. This could be made more exhaustive, but it's good enough for now.
Pull Request resolved: https://github.com/pytorch/pytorch/pull/14588

Differential Revision: D13312653

Pulled By: suo

fbshipit-source-id: 98de1610ea86deada71957c75c222fff331a0888

aten/src/ATen/core/Tensor.h
aten/src/ATen/core/alias_info.h
aten/src/ATen/core/ivalue.h
aten/src/ATen/templates/Tensor.h
c10/core/Storage.h
test/test_jit.py
torch/CMakeLists.txt
torch/csrc/jit/constants.cpp
torch/csrc/jit/init.cpp
torch/csrc/jit/passes/utils/check_alias_annotation.cpp [new file with mode: 0644]
torch/csrc/jit/passes/utils/check_alias_annotation.h [new file with mode: 0644]

index 3991708..fef6bbe 100644 (file)
@@ -157,6 +157,9 @@ public:
   const Storage& storage() const {
     return impl_->storage();
   }
+  bool is_alias_of(const at::Tensor& other) const{
+    return impl_->storage().is_alias_of(other.storage());
+  }
   Tensor toType(const Type & t, bool non_blocking=false) const;
   Tensor & copy_(const Tensor & src, bool non_blocking=false);
   Tensor toType(ScalarType t) const;
index 821a624..5cdeb1e 100644 (file)
@@ -50,7 +50,8 @@ class AliasInfo {
   }
 
   // TODO this doesn't check any contained types yet
-  bool isSubsetOf(const AliasInfo& other) {
+  // non-strict: returns true if self.sets() == other.sets()
+  bool isSubsetOf(const AliasInfo& other) const {
     for (const auto& alias : this->sets()) {
       if (other.sets().count(alias) == 0) {
         return false;
index 78985c5..e358573 100644 (file)
@@ -133,11 +133,36 @@ struct CAFFE2_API IValue final {
     IValue(rhs).swap(*this);
     return *this;
   }
+
+  bool isAliasOf(const IValue& rhs) const {
+    if (this->tag != rhs.tag) {
+      // Trivially don't alias if the type is different
+      return false;
+    }
+
+    if (!this->is_intrusive_ptr) {
+      // Primitive types don't alias anything
+      return false;
+    }
+
+    AT_ASSERT(rhs.is_intrusive_ptr);
+
+    // Tensors should be compared based on internal storage
+    if (this->isTensor()) {
+      const auto thisTensor = this->toTensor();
+      const auto rhsTensor = rhs.toTensor();
+      return thisTensor.is_alias_of(rhsTensor);
+    }
+
+    // Other types can be compared by their ptr value
+    return this->payload.as_intrusive_ptr == rhs.payload.as_intrusive_ptr;
+  }
   void swap(IValue & rhs) noexcept {
     std::swap(payload, rhs.payload);
     std::swap(is_intrusive_ptr, rhs.is_intrusive_ptr);
     std::swap(tag, rhs.tag);
   }
+
   // Accessors for subtypes are arranged together below
   // While some of these accessors could be generated through templates,
   // we prefer to write them manually for clarity
@@ -424,6 +449,10 @@ struct CAFFE2_API IValue final {
       std::ostream& out,
       const IValue& v);
 
+  bool isPtrType() const {
+    return is_intrusive_ptr;
+  }
+
  private:
   // NOTE: IValue tags are intentionally private. In the future we may encode
   // this value different (e.g. using NaN boxing), and this would make it more
index 45be061..8bea0e4 100644 (file)
@@ -157,6 +157,9 @@ public:
   const Storage& storage() const {
     return impl_->storage();
   }
+  bool is_alias_of(const at::Tensor& other) const{
+    return impl_->storage().is_alias_of(other.storage());
+  }
   Tensor toType(const Type & t, bool non_blocking=false) const;
   Tensor & copy_(const Tensor & src, bool non_blocking=false);
   Tensor toType(ScalarType t) const;
index 3eb7847..227e540 100644 (file)
@@ -151,6 +151,10 @@ struct C10_API Storage {
     return storage_impl_.unique();
   }
 
+  bool is_alias_of(const Storage& other) const {
+    return storage_impl_ == other.storage_impl_;
+  }
+
   void UniqueStorageShareExternalPointer(
       void* src,
       const caffe2::TypeMeta& data_type,
index b883099..e03debd 100644 (file)
@@ -9801,6 +9801,17 @@ def create_script_fn(self, method_name, func_type, output_process_fn,
     return script_fn
 
 
+def check_alias_annotation(method_name, args, kwargs):
+    formals, tensors, actuals = get_script_args(args)
+    kwargs_str = ''
+    for k, v in kwargs.items():
+        kwargs_str += ', ' + k + '=' + str(v)
+    call = '{}.{}({}{})'.format(actuals[0], method_name, ', '.join(actuals[1:]), kwargs_str)
+    script = script_template.format(', '.join(formals), call)
+    CU = torch.jit.CompilationUnit(script)
+    torch._C._jit_check_alias_annotation(CU.the_method.graph, tuple(tensors), method_name)
+
+
 def check_output_types(self, func, ref_outputs, args, kwargs):
     graph = getattr(func, 'last_graph', None)
     if not isinstance(ref_outputs, tuple):
@@ -10439,6 +10450,10 @@ def add_autograd_test(
                                                 fn, f_args_variable, kwargs_variable,
                                                 check_types=check_types)
 
+                # alias annotation testing
+                if is_inplace and test_name not in EXCLUDE_SCRIPT:
+                    check_alias_annotation(name, (self_variable,) + args_variable, kwargs_variable)
+
             check(name)
             inplace_name = name + '_'
             # can't broadcast inplace to left hand side
index efd44d6..90cc3d4 100644 (file)
@@ -183,6 +183,7 @@ set(TORCH_SRCS
   ${TORCH_SRC_DIR}/csrc/jit/passes/specialize_undef.cpp
   ${TORCH_SRC_DIR}/csrc/jit/passes/python_print.cpp
   ${TORCH_SRC_DIR}/csrc/jit/passes/utils/subgraph_utils.cpp
+  ${TORCH_SRC_DIR}/csrc/jit/passes/utils/check_alias_annotation.cpp
   ${TORCH_SRC_DIR}/csrc/jit/fuser/interface.cpp
   ${TORCH_SRC_DIR}/csrc/jit/register_prim_ops.cpp
   ${TORCH_SRC_DIR}/csrc/jit/register_special_ops.cpp
index 4eda293..99d4a0a 100644 (file)
@@ -2,6 +2,7 @@
 #include "torch/csrc/jit/operator.h"
 #include "torch/csrc/jit/custom_operator.h"
 #include "torch/csrc/autograd/variable.h"
+#include "torch/csrc/utils/functional.h"
 
 namespace torch { namespace jit {
 
@@ -140,8 +141,9 @@ RegisterOperators reg({
 });
 
 c10::optional<IValue> toIValue(const Value* v) {
-  if(v->node()->kind() != prim::Constant)
+  if (v->node()->kind() != prim::Constant) {
     return c10::nullopt;
+  }
   // use implemenation of prim::Constant to compute the output IValue
   auto op = getOperation(v->node());
   Stack stack;
index 3932a7e..308f70a 100644 (file)
@@ -29,6 +29,7 @@
 #include "torch/csrc/jit/passes/to_batch.h"
 #include "torch/csrc/jit/passes/lower_tuples.h"
 #include "torch/csrc/jit/passes/specialize_undef.h"
+#include "torch/csrc/jit/passes/utils/check_alias_annotation.h"
 #include "torch/csrc/jit/graph_executor.h"
 #include "torch/csrc/jit/script/init.h"
 #include "torch/csrc/jit/script/python_tree_views.h"
@@ -38,6 +39,7 @@
 #include "torch/csrc/jit/operator.h"
 #include "torch/csrc/jit/fuser/interface.h"
 #include "torch/csrc/jit/script/jit_exception.h"
+#include "torch/csrc/jit/script/jit_exception.h"
 
 #include "caffe2/serialize/inline_container.h"
 
@@ -161,6 +163,13 @@ void initJITBindings(PyObject *module) {
        // jit::differentiate mutates the input Graph
        auto g_clone = g.copy();
        return differentiate(g_clone);
+   })
+   .def("_jit_check_alias_annotation", [](
+         std::shared_ptr<Graph> g,
+         py::tuple args,
+         const std::string& unqualified_op_name) {
+       auto stack = toStack(args);
+       checkAliasAnnotation(g, std::move(stack), unqualified_op_name);
    });
 
   py::class_<CompleteArgumentSpec>(m, "CompleteArgumentSpec")
diff --git a/torch/csrc/jit/passes/utils/check_alias_annotation.cpp b/torch/csrc/jit/passes/utils/check_alias_annotation.cpp
new file mode 100644 (file)
index 0000000..20a9d88
--- /dev/null
@@ -0,0 +1,244 @@
+#include "check_alias_annotation.h"
+
+namespace torch {
+namespace jit {
+namespace {
+
+IValue deepCopy(const IValue& self) {
+  // primitive types can be copied directly
+  if (!self.isPtrType()) {
+    return self;
+  }
+
+  // Tensors need special handling, since copy assignment creates an alias
+  if (self.isTensor()) {
+    return IValue(self.toTensor().clone());
+  }
+  if (self.isTensorList()) {
+    std::vector<at::Tensor> newList;
+    for (const auto& oldTensor : self.toTensorListRef()) {
+      newList.push_back(oldTensor.clone());
+    }
+    return newList;
+  }
+
+  // Lists of ivalues should recursively deep copy their contents
+  if (self.isGenericList()) {
+    std::vector<IValue> newList;
+    for (const auto& value : self.toGenericListRef()) {
+      newList.push_back(deepCopy(value));
+    }
+    return newList;
+  }
+
+  // Regular lists can copy assign
+  if (self.isIntList()) {
+    return IValue(self.toIntListRef());
+  } else if (self.isDoubleList()) {
+    return IValue(self.toDoubleListRef());
+  } else if (self.isBoolList()) {
+    return IValue(self.toBoolListRef());
+  } else if (self.isString()) {
+    return IValue(self.toStringRef());
+  }
+
+  // If in the future we add more reference types that are used in aten ops,
+  // we'll have to add them as cases here.
+  AT_ASSERT(false);
+}
+
+Stack deepCopy(const Stack& stack) {
+  Stack ret;
+  for (const auto& v : stack) {
+    ret.push_back(deepCopy(v));
+  }
+  return ret;
+}
+
+bool deepEquals(const IValue& lhs, const IValue& rhs) {
+  // only check tensors for now
+  if (!lhs.isTensor() || !rhs.isTensor()) {
+    return true;
+  }
+
+  return lhs.toTensor().equal(rhs.toTensor());
+}
+
+struct AliasAndIValue {
+  AliasAndIValue(
+      const c10::optional<at::AliasInfo>& aliasInfo,
+      const IValue& iValue)
+      : aliasInfo(aliasInfo), iValue(iValue) {}
+
+  const c10::optional<at::AliasInfo>& aliasInfo;
+  const IValue& iValue;
+};
+
+// No inputs should alias each other
+void checkInputPreconditions(const Stack& inputs) {
+  for (size_t i = 0; i < inputs.size(); i++) {
+    for (size_t j = 0; j < inputs.size(); j++) {
+      if (i == j) {
+        continue;
+      }
+      const auto& lhs = inputs.at(i);
+      const auto& rhs = inputs.at(j);
+      JIT_ASSERT(!lhs.isAliasOf(rhs));
+    }
+  }
+}
+
+// If two ivalues alias, they must share an alias set
+void checkAliases(
+    const std::vector<AliasAndIValue>& inputs,
+    const std::vector<AliasAndIValue>& outputs) {
+  for (const auto& output : outputs) {
+    // if this output aliases any input, make sure that they share an alias set
+    for (const auto& input : inputs) {
+      if (output.iValue.isAliasOf(input.iValue)) {
+        const auto inputSet = input.aliasInfo;
+        const auto outputSet = output.aliasInfo;
+        JIT_ASSERT(inputSet && outputSet);
+        JIT_ASSERT(inputSet->isSubsetOf(*outputSet));
+      }
+    }
+  }
+}
+
+// If we didn't specify that we write to an input value, it must have not
+// changed
+void checkWrites(
+    const std::vector<AliasAndIValue>& inputs,
+    const std::vector<IValue>& deepCopiedInputs) {
+  JIT_ASSERT(inputs.size() == deepCopiedInputs.size());
+  for (size_t i = 0; i < inputs.size(); i++) {
+    const auto& input = inputs[i];
+    const auto& deepCopiedInput = deepCopiedInputs[i];
+    if (!input.aliasInfo || !input.aliasInfo->isWrite()) {
+      JIT_ASSERT(deepEquals(input.iValue, deepCopiedInput));
+    }
+  }
+}
+
+const Node* findNodeForOp(
+    const Graph& g,
+    const std::string& unqualifiedOpName) {
+  const auto opName = Symbol::fromQualString("aten::" + unqualifiedOpName);
+  for (const auto node : g.nodes()) {
+    if (node->kind() == opName) {
+      return node;
+    }
+  }
+  JIT_ASSERT(false);
+}
+
+// Handle a few special cases where we need to propagate constants
+// manually
+// TODO(suo): we should be able to move this stuff to constant prop
+c10::optional<IValue> toIValueProp(const Value* v) {
+  if (v->node()->kind() == prim::ListConstruct) {
+    std::vector<IValue> genericList;
+    for (auto input : v->node()->inputs()) {
+      if (auto elem = toIValue(input)) {
+        genericList.push_back(*elem);
+      } else {
+        // One of the list elements isn't constant.
+        return c10::nullopt;
+      }
+    }
+
+    // Specialize the list based on ListConstruct's return type
+    auto listType = v->node()->output()->type();
+    auto containedType = listType->containedTypes().at(0);
+    if (containedType == IntType::get()) {
+      return fmap(genericList, [](const IValue& v) { return v.toInt(); });
+    } else if (containedType == FloatType::get()) {
+      return fmap(genericList, [](const IValue& v) { return v.toDouble(); });
+    } else if (containedType->isSubtypeOf(DynamicType::get())) {
+      return fmap(genericList, [](const IValue& v) { return v.toTensor(); });
+    } else {
+      return c10::nullopt;
+    }
+  }
+
+  if (v->node()->kind() == prim::StringToFloat) {
+    auto op = getOperation(v->node());
+    if (auto input = toIValue(v->node()->input())) {
+      auto op = getOperation(v->node());
+      Stack stack;
+      push(stack, *input);
+      op(stack);
+      return stack.back();
+    } else {
+      return c10::nullopt;
+    }
+  }
+  return c10::nullopt;
+}
+} // namespace
+
+void checkAliasAnnotation(
+    std::shared_ptr<Graph> graph,
+    std::vector<IValue> pythonInputs,
+    const std::string& unqualifiedOpName) {
+  // Find the node that corresponds to our op name
+  const auto node = findNodeForOp(*graph, unqualifiedOpName);
+
+  // Build the stack to use as input to the op
+  Stack stack;
+  for (const auto input : node->inputs()) {
+    if (input->node() == graph->param_node()) {
+      // This value was passed as an input in python
+      push(stack, pythonInputs.at(input->offset()));
+    } else {
+      // This a generated constant, which we need to evaluate
+      auto inputValue = toIValue(input);
+      if (!inputValue) {
+        inputValue = toIValueProp(input);
+      }
+
+      if (inputValue) {
+        push(stack, *inputValue);
+      } else {
+        JIT_ASSERT(input->type()->kind() == TypeKind::OptionalType);
+        push(stack, IValue());
+      }
+    }
+  }
+
+  // Precondition: no inputs should alias each other. So if we find an alias,
+  // it was created by the op.
+  checkInputPreconditions(stack);
+
+  const auto schema = node->schema();
+
+  std::vector<AliasAndIValue> inputsToCheck;
+  for (size_t i = 0; i < schema.arguments().size(); i++) {
+    inputsToCheck.emplace_back(
+        schema.arguments().at(i).alias_info(), stack.at(i));
+  }
+
+  // Save a copy of the inputs so we can check whether the original inputs were
+  // written to.
+  const auto inputsDeepCopy = deepCopy(stack);
+
+  // Run the op
+  getOperation(node)(stack);
+
+  const auto outputs = std::move(stack);
+
+  std::vector<AliasAndIValue> outputsToCheck;
+  for (size_t i = 0; i < schema.returns().size(); i++) {
+    outputsToCheck.emplace_back(
+        schema.returns().at(i).alias_info(), outputs.at(i));
+  }
+
+  // Check that if any alias was created, we annotated it properly.
+  checkAliases(inputsToCheck, outputsToCheck);
+
+  // Check that if nothing was accidentally written to.
+  checkWrites(inputsToCheck, inputsDeepCopy);
+}
+
+} // namespace jit
+} // namespace torch
diff --git a/torch/csrc/jit/passes/utils/check_alias_annotation.h b/torch/csrc/jit/passes/utils/check_alias_annotation.h
new file mode 100644 (file)
index 0000000..aec1542
--- /dev/null
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "torch/csrc/jit/graph_executor.h"
+#include "torch/csrc/jit/ir.h"
+#include "torch/csrc/jit/operator.h"
+
+namespace torch {
+namespace jit {
+
+// Verify that alias annotations are correct. See impl for definition of
+// "correct".
+//
+// This function expects a graph with a single op with `unqualifiedOpName`, plus
+// the inputs that you would otherwise have passed to the graph executor.
+TORCH_API void checkAliasAnnotation(
+    std::shared_ptr<Graph> graph,
+    std::vector<IValue> pythonInputs,
+    const std::string& unqualifiedOpName);
+} // namespace jit
+} // namespace torch