Repository: dask/dask

Issue:
enforce_type slowdown in meta computation in dask array
We have a LBFGS implementation where we are seeing a 40x increase in runtime during graph construction when using dask 2.0.0 compared to dask 1.2.2.

I have profiled it a bit and it seems all the additional time is spent resolving dtypes for the multiplications. I have not yet managed to reduce it to a smaller reproducer than our H^-1 * G function.

The attached code needs about 0.08 seconds per call to `calculate_hinvg` for 1.2.2 and about 3.4s for 2.0.0.

```python
import time

import numpy as np
import dask.array as da
import dask

M = 5
N= 10000
grad = da.zeros((N,), dtype=np.float64, chunks=N)
w = da.zeros((N,), dtype=np.float64, chunks=N)
search_steps = [da.zeros((N,), dtype=np.float64, chunks=N) for i in range(M)]
gradient_differences = [da.zeros((N,), dtype=np.float64, chunks=N) for i in range(M)]
mask = da.zeros((M,), dtype=np.float64, chunks=N)
rho = da.zeros((M,), dtype=np.float64, chunks=N)
stp = da.from_array([1], chunks=1)

def calculate_hinvg(grad, w, search_steps, gradient_differences, mask, stp, rho):
    """ Algorithm follows the explanation at
    https://en.wikipedia.org/wiki/Limited-memory_BFGS"""

    # For us the newest entry in the arrays is always at index 0

    # calculate the missing gradient difference and the missing search direction
    # search direction is the value that we calculated the last time multiplied
    # with the accepted step length
    grad_diff_new = [grad - w]
    grad_diff_new.extend(gradient_differences)
    search_steps_new = [stp * search_steps[0]]

    # don't add the entry that is the unscaled search direction
    search_steps_new.extend(search_steps[1:])

    ys = da.dot(grad_diff_new[0], search_steps_new[0])
    yy = da.dot(grad_diff_new[0], grad_diff_new[0])

    gamma = ys / yy
    rho = [1.0 / ys]

    work_array = -1 * grad
    for i in range(M - 1):
        rho.append(rho[i])

    alpha_list = []
    for i in range(0, M):
        sq = da.dot(search_steps_new[i], work_array)
        alpha = sq * rho[i]
        alpha_list.append(alpha)
        work_array = work_array - mask[i] * alpha * grad_diff_new[i]

    z = gamma * work_array

    for j in range(0, M):
        i = M - 1 - j
        yr = da.dot(grad_diff_new[i], z)
        beta = alpha_list[i] - rho[i] * yr
        z = z + mask[i] * beta * search_steps_new[i]

    # z is the proposed new search direction if we accept the iteration
    return z, rho, search_steps_new

for i in range(20):
    start = time.clock()
    w, rho, search_steps = calculate_hinvg(grad, w, search_steps, gradient_differences, mask, stp, rho)
    end = time.clock()
    print("Runtime: %f" % (end - start))
```

Below also a part of the cprofile that shows that the problem might be recursive resolving of the return type.

![dask2](https://user-images.githubusercontent.com/6321320/60490338-ec218700-9cd8-11e9-9237-1a69b7fb3ec4.png)



Discussion:
cc @pentschev 
This relates to meta computation. I'm not yet certain, but I think it relates specifically to the dtype enforcing in `elemwise`, https://github.com/dask/dask/blob/master/dask/array/core.py#L3686. Will investigate further.

Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -4759,7 +4759,7 @@ def map_partitions(func, *args, **kwargs):
         }
         graph = HighLevelGraph.from_collections(name, layer, dependencies=args)
         return Scalar(graph, name, meta)
-    elif not (has_parallel_type(meta) or is_arraylike(meta)):
+    elif not (has_parallel_type(meta) or is_arraylike(meta) and meta.shape):
         # If `meta` is not a pandas object, the concatenated results will be a
         # different type
         meta = make_meta(_concat([meta]), index=meta_index)
@@ -5841,7 +5841,7 @@ def new_dd_object(dsk, name, meta, divisions):
     """
     if has_parallel_type(meta):
         return get_parallel_type(meta)(dsk, name, meta, divisions)
-    elif is_arraylike(meta):
+    elif is_arraylike(meta) and meta.shape:
         import dask.array as da
 
         chunks = ((np.nan,) * (len(divisions) - 1),) + tuple(
diff --git a/dask/dataframe/utils.py b/dask/dataframe/utils.py
--- a/dask/dataframe/utils.py
+++ b/dask/dataframe/utils.py
@@ -317,7 +317,7 @@ def make_meta_object(x, index=None):
     """
     if hasattr(x, "_meta"):
         return x._meta
-    elif is_arraylike(x):
+    elif is_arraylike(x) and x.shape:
         return x[:0]
 
     if index is not None:
diff --git a/dask/utils.py b/dask/utils.py
--- a/dask/utils.py
+++ b/dask/utils.py
@@ -1051,8 +1051,9 @@ def is_arraylike(x):
     Examples
     --------
     >>> import numpy as np
-    >>> x = np.ones(5)
-    >>> is_arraylike(x)
+    >>> is_arraylike(np.ones(5))
+    True
+    >>> is_arraylike(np.ones(()))
     True
     >>> is_arraylike(5)
     False
@@ -1061,10 +1062,9 @@ def is_arraylike(x):
     """
     from .base import is_dask_collection
 
-    return (
+    return bool(
         hasattr(x, "shape")
         and isinstance(x.shape, tuple)
-        and x.shape
         and hasattr(x, "dtype")
         and not any(is_dask_collection(n) for n in x.shape)
     )


Test patch:
diff --git a/dask/tests/test_utils.py b/dask/tests/test_utils.py
--- a/dask/tests/test_utils.py
+++ b/dask/tests/test_utils.py
@@ -27,6 +27,7 @@
     derived_from,
     parse_timedelta,
     parse_bytes,
+    is_arraylike,
 )
 from dask.utils_test import inc
 from dask.highlevelgraph import HighLevelGraph
@@ -506,3 +507,15 @@ def test_parse_timedelta():
     assert parse_timedelta("1", default="seconds") == 1
     assert parse_timedelta("1", default="ms") == 0.001
     assert parse_timedelta(1, default="ms") == 0.001
+
+
+def test_is_arraylike():
+    assert is_arraylike(0) is False
+    assert is_arraylike(()) is False
+    assert is_arraylike((0)) is False
+    assert is_arraylike([]) is False
+    assert is_arraylike([0]) is False
+
+    assert is_arraylike(np.empty(())) is True
+    assert is_arraylike(np.empty((0,))) is True
+    assert is_arraylike(np.empty((0, 0))) is True