Repository: mars-project/mars

Issue:
[BUG] Add failed with specified `out`
<!--
Thank you for your contribution!

Please review https://github.com/mars-project/mars/blob/master/CONTRIBUTING.rst before opening an issue.
-->

**Describe the bug**
Failed to add a scalar when specify an `IndexTensor` as `out`.

**To Reproduce**
``` Python
In [5]: import mars.tensor as mt                                                                                                                     

In [6]: a = mt.random.rand(1, 10, chunk_size=3)                                                                                                      

In [7]: b = mt.add(a[:, :5], 1, out=a[:, 5:])                                                                                                        

In [8]: b.execute()                                                                                                                                  
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~/Documents/mars_dev/mars/mars/tiles.py in _dispatch(self, op)
    110         try:
--> 111             handler = self._handlers[op_cls]
    112             return handler(op)

KeyError: <class 'mars.tensor.expressions.arithmetic.add.TensorAdd'>

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
<ipython-input-8-be8c8c42b9b4> in <module>
----> 1 b.execute()

~/Documents/mars_dev/mars/mars/tensor/core.py in execute(self, session, **kw)
    394 
    395     def execute(self, session=None, **kw):
--> 396         return self._data.execute(session, **kw)
    397 
    398 

~/Documents/mars_dev/mars/mars/core.py in execute(self, session, **kw)
    553         if session is None:
    554             session = Session.default_or_local()
--> 555         return session.run(self, **kw)
    556 
    557     def fetch(self, session=None, **kw):

~/Documents/mars_dev/mars/mars/session.py in run(self, *tileables, **kw)
    113         tileables = tuple(mt.tensor(t) if not isinstance(t, (Entity, Base)) else t
    114                           for t in tileables)
--> 115         result = self._sess.run(*tileables, **kw)
    116 
    117         for t in tileables:

~/Documents/mars_dev/mars/mars/session.py in run(self, *tileables, **kw)
     54         if 'n_parallel' not in kw:
     55             kw['n_parallel'] = cpu_count()
---> 56         res = self._executor.execute_tileables(tileables, **kw)
     57         return res
     58 

~/Documents/mars_dev/mars/mars/utils.py in _wrapped(*args, **kwargs)
    387         try:
    388             _kernel_mode.eager = False
--> 389             return func(*args, **kwargs)
    390         finally:
    391             _kernel_mode.eager = None

~/Documents/mars_dev/mars/mars/executor.py in execute_tileables(self, tileables, fetch, n_parallel, n_thread, print_progress, mock, compose)
    523         concat_keys = []
    524         for tileable in tileables:
--> 525             tileable.tiles()
    526             chunk_keys = [c.key for c in tileable.chunks]
    527             result_keys.extend(chunk_keys)

~/Documents/mars_dev/mars/mars/core.py in tiles(self)
    472 
    473     def tiles(self):
--> 474         return handler.tiles(self)
    475 
    476     def single_tiles(self):

~/Documents/mars_dev/mars/mars/tiles.py in tiles(self, tiles_obj)
    172             if not preds or accessible:
    173                 if node.is_coarse() and node.op:
--> 174                     tiled = self._dispatch(node.op)
    175                     self._assign_to([t.data for t in tiled], node.op.outputs)
    176                 visited.add(node)

~/Documents/mars_dev/mars/mars/utils.py in _wrapped(*args, **kwargs)
    387         try:
    388             _kernel_mode.eager = False
--> 389             return func(*args, **kwargs)
    390         finally:
    391             _kernel_mode.eager = None

~/Documents/mars_dev/mars/mars/tiles.py in _dispatch(self, op)
    114             if hasattr(op_cls, 'tile'):
    115                 # has tile implementation
--> 116                 return op_cls.tile(op)
    117             for op_clz in self._handlers.keys():
    118                 if issubclass(op_cls, op_clz):

~/Documents/mars_dev/mars/mars/tensor/expressions/arithmetic/core.py in tile(cls, op)
     36 
     37         chunk_shapes = [t.chunk_shape for t in inputs]
---> 38         out_chunk_shape = broadcast_shape(*chunk_shapes)
     39 
     40         out_chunks = [list() for _ in op.outputs]

~/Documents/mars_dev/mars/mars/tensor/expressions/utils.py in broadcast_shape(*shapes)
     76         if any(i != -1 and i != 1 and i != shape and not np.isnan(i) for i in ss):
     77             raise ValueError('Operands could not be broadcast together '
---> 78                              'with shape {0}'.format(' '.join(map(str, shapes))))
     79         out_shapes.append(shape)
     80     return tuple(reversed(out_shapes))

ValueError: Operands could not be broadcast together with shape (1, 2) () (1, 3)
```


Discussion:

Resolving patch:
diff --git a/mars/tensor/expressions/utils.py b/mars/tensor/expressions/utils.py
--- a/mars/tensor/expressions/utils.py
+++ b/mars/tensor/expressions/utils.py
@@ -359,8 +359,8 @@ def unify_nsplits(*tensor_axes):
     from .rechunk import rechunk
 
     tensor_splits = [dict((a, split) for a, split in izip(axes, t.nsplits) if split != (1,))
-                     for t, axes in tensor_axes]
-    common_axes = reduce(operator.and_, [set(lkeys(ts)) for ts in tensor_splits])
+                     for t, axes in tensor_axes if t.nsplits]
+    common_axes = reduce(operator.and_, [set(lkeys(ts)) for ts in tensor_splits]) if tensor_splits else set()
     axes_unified_splits = dict((ax, decide_unify_split(*(t[ax] for t in tensor_splits)))
                                for ax in common_axes)
 


Test patch:
diff --git a/mars/tensor/execution/tests/test_arithmetic_execute.py b/mars/tensor/execution/tests/test_arithmetic_execute.py
--- a/mars/tensor/execution/tests/test_arithmetic_execute.py
+++ b/mars/tensor/execution/tests/test_arithmetic_execute.py
@@ -262,6 +262,13 @@ def testAddWithOutExecution(self):
         expected = np.add(data1, 1, where=data1 > .5)
         self.assertTrue(np.array_equal(res[data1 > .5], expected[data1 > .5]))
 
+        arr1 = tensor(data2.copy(), chunk_size=3)
+
+        arr3 = add(arr1[:5, :], 1, out=arr1[-5:, :])
+        res = self.executor.execute_tensor(arr3, concat=True)[0]
+        expected = np.add(data2[:5, :], 1)
+        self.assertTrue(np.array_equal(res, expected))
+
     def testFrexpExecution(self):
         data1 = np.random.random((5, 9, 4))