Repository: dask/dask
Issue category: Feature Request

Issue:
Use tree reduction in `da.bincount`
Currently `da.bincount` does a [reduction over `np.bincount`s from each chunk]( https://github.com/dask/dask/blob/2f308e72a46de818055819108429e8f7ce7ba03b/dask/array/routines.py#L549 ). It would be nice to change this to use [a tree reduction]( https://github.com/dask/dask/blob/master/dask/array/reductions.py ) instead.


Hints:
This could be a fun task if it is interesting to you, @madsbk. Would provide an opportunity to learn about how reductions work in Dask and how to construct task graphs.
How important is it to do a tree reduction here?  I understand the value of tree reductions generally, I'm mostly just curious about when this becomes awkward.
How important is it to do a tree reduction here?  I understand the value of tree reductions generally, I'm mostly just curious about when this becomes awkward.
Running `bincount` on the AOLLSM m4 dataset now is pretty slow. My hope is this would help speed it up, but I could be wrong about this.
If someone wants to pick this up, the next steps appear to be implementing a tree reduction and running some benchmarks to add to this conversation.
Did you ever get a chance to do some benchmarking on this @jakirkham?
I’m no longer looking at this. That doesn’t mean it’s not useful to people using `bincount`. Just have different work today.

Patch:
diff --git a/dask/array/routines.py b/dask/array/routines.py
index b250e3ff44a..29542f431ce 100644
--- a/dask/array/routines.py
+++ b/dask/array/routines.py
@@ -620,7 +620,10 @@ def gradient(f, *varargs, **kwargs):
     return results
 
 
-def _bincount_sum(bincounts, dtype=int):
+def _bincount_agg(bincounts, dtype, **kwargs):
+    if not isinstance(bincounts, list):
+        return bincounts
+
     n = max(map(len, bincounts))
     out = zeros_like_safe(bincounts[0], shape=n, dtype=dtype)
     for b in bincounts:
@@ -629,7 +632,7 @@ def _bincount_sum(bincounts, dtype=int):
 
 
 @derived_from(np)
-def bincount(x, weights=None, minlength=0):
+def bincount(x, weights=None, minlength=0, split_every=None):
     if x.ndim != 1:
         raise ValueError("Input array must be one dimensional. Try using x.ravel()")
     if weights is not None:
@@ -637,35 +640,30 @@ def bincount(x, weights=None, minlength=0):
             raise ValueError("Chunks of input array x and weights must match.")
 
     token = tokenize(x, weights, minlength)
-    name = "bincount-" + token
-    final_name = "bincount-sum" + token
-    # Call np.bincount on each block, possibly with weights
+    args = [x, "i"]
     if weights is not None:
-        dsk = {
-            (name, i): (np.bincount, (x.name, i), (weights.name, i), minlength)
-            for i, _ in enumerate(x.__dask_keys__())
-        }
-        dtype = np.bincount([1], weights=[1]).dtype
+        meta = np.bincount([1], weights=[1])
+        args.extend([weights, "i"])
     else:
-        dsk = {
-            (name, i): (np.bincount, (x.name, i), None, minlength)
-            for i, _ in enumerate(x.__dask_keys__())
-        }
-        dtype = np.bincount([]).dtype
+        meta = np.bincount([])
 
-    dsk[(final_name, 0)] = (_bincount_sum, list(dsk), dtype)
-    graph = HighLevelGraph.from_collections(
-        final_name, dsk, dependencies=[x] if weights is None else [x, weights]
+    chunked_counts = blockwise(
+        partial(np.bincount, minlength=minlength), "i", *args, token=token, meta=meta
     )
 
-    if minlength == 0:
-        chunks = ((np.nan,),)
-    else:
-        chunks = ((minlength,),)
-
-    meta = meta_from_array(x, 1, dtype=dtype)
+    from .reductions import _tree_reduce
 
-    return Array(graph, final_name, chunks, meta=meta)
+    output = _tree_reduce(
+        chunked_counts,
+        aggregate=partial(_bincount_agg, dtype=meta.dtype),
+        axis=(0,),
+        keepdims=False,
+        dtype=meta.dtype,
+        split_every=split_every,
+        concatenate=False,
+    )
+    output._meta = meta
+    return output
 
 
 @derived_from(np)


Test patch:
diff --git a/dask/array/tests/test_routines.py b/dask/array/tests/test_routines.py
index 91139b83aab..3cfc023ef7b 100644
--- a/dask/array/tests/test_routines.py
+++ b/dask/array/tests/test_routines.py
@@ -535,10 +535,16 @@ def test_bincount():
     assert da.bincount(d, minlength=6).name == da.bincount(d, minlength=6).name
 
 
-def test_bincount_with_weights():
+@pytest.mark.parametrize(
+    "weights",
+    [
+        np.array([1, 2, 1, 0.5, 1], dtype=np.float32),
+        np.array([1, 2, 1, 0, 1], dtype=np.int32),
+    ],
+)
+def test_bincount_with_weights(weights):
     x = np.array([2, 1, 5, 2, 1])
     d = da.from_array(x, chunks=2)
-    weights = np.array([1, 2, 1, 0.5, 1])
 
     dweights = da.from_array(weights, chunks=2)
     e = da.bincount(d, weights=dweights, minlength=6)