Repository: dmlc/dgl

Issue:
[Bug] Edges bucketed wrong in group_apply_edges after triggering `g.in_degrees()`
## 🐛 Bug

Apparently the edges being bucketed and sent into UDF of `group_apply_edges`

See the following test case.

## To Reproduce

```python
import dgl
import torch
import scipy.sparse as ssp

m = ssp.random(10, 10, 0.2)
g = dgl.DGLGraph(m, readonly=True)
g.ndata['deg'] = g.in_degrees()     # removing this line would pass the test
g.ndata['id'] = torch.arange(g.number_of_nodes())
g.edata['id'] = torch.arange(g.number_of_edges())

# This function checks whether the edge IDs in the bucket indeed match those of the
# inbound edges
def apply(edges):
    w = edges.data['id']
    n_nodes, deg = w.shape

    dst = edges.dst['id'][:, 0]
    eid1 = g.in_edges(dst, 'eid').view(n_nodes, deg).sort(1)[0]
    eid2 = edges.data['id'].sort(1)[0]
    print(dst)
    print(eid1)
    print(eid2)
    assert torch.equal(eid1, eid2)
    return {'id2': w}

print(g.all_edges(order='eid'))
g.group_apply_edges('dst', apply, inplace=True)
```

## Expected behavior

The test above should pass.

## Environment

 - DGL Version (e.g., 1.0): master
 - Backend Library & Version (e.g., PyTorch 0.4.1, MXNet/Gluon 1.3):
 - OS (e.g., Linux):
 - How you installed DGL (`conda`, `pip`, source):
 - Build command you used (if compiling from source):
 - Python version:
 - CUDA/cuDNN version (if applicable):
 - GPU models and configuration (e.g. V100):
 - Any other relevant information:

## Additional context

In https://github.com/dmlc/dgl/blob/master/python/dgl/graph.py#L2211, adding `'eid'` to the `edges()` call would solve the problem.  But could anybody explain why this happens only if `in_degrees()` is there (before I figure it out myself)?

`send()` and those in heterographs may also have the same problem.
[Bug] Edges bucketed wrong in group_apply_edges after triggering `g.in_degrees()`
## 🐛 Bug

Apparently the edges being bucketed and sent into UDF of `group_apply_edges`

See the following test case.

## To Reproduce

```python
import dgl
import torch
import scipy.sparse as ssp

m = ssp.random(10, 10, 0.2)
g = dgl.DGLGraph(m, readonly=True)
g.ndata['deg'] = g.in_degrees()     # removing this line would pass the test
g.ndata['id'] = torch.arange(g.number_of_nodes())
g.edata['id'] = torch.arange(g.number_of_edges())

# This function checks whether the edge IDs in the bucket indeed match those of the
# inbound edges
def apply(edges):
    w = edges.data['id']
    n_nodes, deg = w.shape

    dst = edges.dst['id'][:, 0]
    eid1 = g.in_edges(dst, 'eid').view(n_nodes, deg).sort(1)[0]
    eid2 = edges.data['id'].sort(1)[0]
    print(dst)
    print(eid1)
    print(eid2)
    assert torch.equal(eid1, eid2)
    return {'id2': w}

print(g.all_edges(order='eid'))
g.group_apply_edges('dst', apply, inplace=True)
```

## Expected behavior

The test above should pass.

## Environment

 - DGL Version (e.g., 1.0): master
 - Backend Library & Version (e.g., PyTorch 0.4.1, MXNet/Gluon 1.3):
 - OS (e.g., Linux):
 - How you installed DGL (`conda`, `pip`, source):
 - Build command you used (if compiling from source):
 - Python version:
 - CUDA/cuDNN version (if applicable):
 - GPU models and configuration (e.g. V100):
 - Any other relevant information:

## Additional context

In https://github.com/dmlc/dgl/blob/master/python/dgl/graph.py#L2211, adding `'eid'` to the `edges()` call would solve the problem.  But could anybody explain why this happens only if `in_degrees()` is there (before I figure it out myself)?

`send()` and those in heterographs may also have the same problem.


Discussion:

Resolving patch:
diff --git a/python/dgl/graph.py b/python/dgl/graph.py
--- a/python/dgl/graph.py
+++ b/python/dgl/graph.py
@@ -2208,7 +2208,7 @@ def group_apply_edges(self, group_by, func, edges=ALL, inplace=False):
             raise DGLError("Group_by should be either src or dst")
 
         if is_all(edges):
-            u, v, _ = self._graph.edges()
+            u, v, _ = self._graph.edges('eid')
             eid = utils.toindex(slice(0, self.number_of_edges()))
         elif isinstance(edges, tuple):
             u, v = edges
@@ -2270,7 +2270,7 @@ def send(self, edges=ALL, message_func="default"):
 
         if is_all(edges):
             eid = utils.toindex(slice(0, self.number_of_edges()))
-            u, v, _ = self._graph.edges()
+            u, v, _ = self._graph.edges('eid')
         elif isinstance(edges, tuple):
             u, v = edges
             u = utils.toindex(u)
diff --git a/python/dgl/heterograph.py b/python/dgl/heterograph.py
--- a/python/dgl/heterograph.py
+++ b/python/dgl/heterograph.py
@@ -2383,7 +2383,7 @@ def group_apply_edges(self, group_by, func, edges=ALL, etype=None, inplace=False
         etid = self.get_etype_id(etype)
         stid, dtid = self._graph.metagraph.find_edge(etid)
         if is_all(edges):
-            u, v, _ = self._graph.edges(etid)
+            u, v, _ = self._graph.edges(etid, 'eid')
             eid = utils.toindex(slice(0, self.number_of_edges(etype)))
         elif isinstance(edges, tuple):
             u, v = edges
@@ -2468,7 +2468,7 @@ def send(self, edges, message_func, etype=None):
 
         if is_all(edges):
             eid = utils.toindex(slice(0, self._graph.number_of_edges(etid)))
-            u, v, _ = self._graph.edges(etid)
+            u, v, _ = self._graph.edges(etid, 'eid')
         elif isinstance(edges, tuple):
             u, v = edges
             u = utils.toindex(u)


Test patch:
diff --git a/tests/compute/test_basics.py b/tests/compute/test_basics.py
--- a/tests/compute/test_basics.py
+++ b/tests/compute/test_basics.py
@@ -1,5 +1,7 @@
 import backend as F
 import dgl
+import numpy as np
+import scipy.sparse as ssp
 import networkx as nx
 from dgl import DGLGraph
 from collections import defaultdict as ddict
@@ -654,6 +656,28 @@ def _test(group_by):
     # test group by destination nodes
     _test('dst')
 
+
+# GitHub issue #1036
+def test_group_apply_edges2():
+    m = ssp.random(10, 10, 0.2)
+    g = DGLGraph(m, readonly=True)
+    g.ndata['deg'] = g.in_degrees()
+    g.ndata['id'] = F.arange(0, g.number_of_nodes())
+    g.edata['id'] = F.arange(0, g.number_of_edges())
+
+    def apply(edges):
+        w = edges.data['id']
+        n_nodes, deg = w.shape
+
+        dst = edges.dst['id'][:, 0]
+        eid1 = F.asnumpy(g.in_edges(dst, 'eid')).reshape(n_nodes, deg).sort(1)
+        eid2 = F.asnumpy(edges.data['id']).sort(1)
+        assert np.array_equal(eid1, eid2)
+        return {'id2': w}
+
+    g.group_apply_edges('dst', apply, inplace=True)
+
+
 def test_local_var():
     g = DGLGraph(nx.path_graph(5))
     g.ndata['h'] = F.zeros((g.number_of_nodes(), 3))
@@ -803,5 +827,6 @@ def foo(g):
     test_dynamic_addition()
     test_repr()
     test_group_apply_edges()
+    test_group_apply_edges2()
     test_local_var()
     test_local_scope()