Repository: pyg-team/pytorch_geometric (python)

Issue:
`MetaPath2Vec` fails in a heterogeneous graph with zero-degree nodes
### 🐛 Describe the bug

Just found that `MetaPath2Vec` does not work well on a heterogeneous graph with zero-degree nodes.
Here is the example to reproduce the bug:

```python
import torch
from torch_geometric.data import HeteroData
from torch_geometric.nn.models import MetaPath2Vec

data = HeteroData()
data['a'].x = torch.ones(3, 2)
data['b'].x = torch.ones(4, 2)
data[('a', 'to', 'b')].edge_index = torch.tensor([[0, 2], [0, 2]])
data[('b', 'to', 'a')].edge_index = torch.tensor([[0, 2], [0, 2]])

metapath = [('a', 'to', 'b'), ('b', 'to', 'a')]

model = MetaPath2Vec(data.edge_index_dict, embedding_dim=16,
                     metapath=metapath, walk_length=10, context_size=7,
                     walks_per_node=5, num_negative_samples=5,
                     num_nodes_dict=data.num_nodes_dict,
                     sparse=True)

loader = model.loader(batch_size=16, shuffle=True)
next(iter(loader))
```
It throws 
```
    248 def sample(rowptr: Tensor, col: Tensor, rowcount: Tensor, subset: Tensor,
    249            num_neighbors: int, dummy_idx: int) -> Tensor:
    251     rand = torch.rand((subset.size(0), num_neighbors), device=subset.device)
--> 252     rand *= rowcount[subset].to(rand.dtype).view(-1, 1)
    253     rand = rand.to(torch.long) + rowptr[subset].view(-1, 1)
    255     col = col[rand]

IndexError: index 7 is out of bounds for dimension 0 with size 4
```

That's because `MetaPath2Vec` assigns invalid sampled nodes with a `dummy_idx` (here `7`) during each sampling step. However, the `dummy_idx` is out-of-index for each (sub)graph, leading to the `IndexError` at the next sampleing step.
https://github.com/pyg-team/pytorch_geometric/blob/114ddcac8dc8a46b96734f55416750474b290666/torch_geometric/nn/models/metapath2vec.py#L256


### Environment

* PyG version: master
* PyTorch version: 2.0.0
* OS: macos
* Python version: 3.10
* CUDA/cuDNN version: N/A
* How you installed PyTorch and PyG (`conda`, `pip`, source): pip
* Any other relevant information (*e.g.*, version of `torch-scatter`): N/A


Pull request:
Fix empty edge handling in `MetaPath2Vec`
Fixes #8246

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 22598fb75..0a9136eac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 
 ### Fixed
 
+- Fixed empty edge handling in `MetaPath2Vec` ([#8248](https://github.com/pyg-team/pytorch_geometric/pull/8248))
 - Fixed `AttentionExplainer` usage within `AttentiveFP` ([#8244](https://github.com/pyg-team/pytorch_geometric/pull/8244))
 - Fixed `load_from_state_dict` in lazy `Linear` modules ([#8242](https://github.com/pyg-team/pytorch_geometric/pull/8242))
 - Fixed pre-trained `DimeNet++` performance on `QM9` ([#8239](https://github.com/pyg-team/pytorch_geometric/pull/8239))
diff --git a/torch_geometric/nn/models/metapath2vec.py b/torch_geometric/nn/models/metapath2vec.py
index 0936f713e..854a8442f 100644
--- a/torch_geometric/nn/models/metapath2vec.py
+++ b/torch_geometric/nn/models/metapath2vec.py
@@ -248,10 +248,14 @@ class MetaPath2Vec(torch.nn.Module):
 def sample(rowptr: Tensor, col: Tensor, rowcount: Tensor, subset: Tensor,
            num_neighbors: int, dummy_idx: int) -> Tensor:
 
+    mask = subset >= dummy_idx
+    subset = subset.clamp(min=0, max=rowptr.numel() - 2)
+    count = rowcount[subset]
+
     rand = torch.rand((subset.size(0), num_neighbors), device=subset.device)
-    rand *= rowcount[subset].to(rand.dtype).view(-1, 1)
+    rand *= count.to(rand.dtype).view(-1, 1)
     rand = rand.to(torch.long) + rowptr[subset].view(-1, 1)
 
-    col = col[rand]
-    col[(subset >= dummy_idx) | (rowcount[subset] == 0)] = dummy_idx
+    col = col[rand] if col.numel() > 0 else rand
+    col[mask | (count == 0)] = dummy_idx
     return col


Test patch:
diff --git a/test/nn/models/test_metapath2vec.py b/test/nn/models/test_metapath2vec.py
index 7f00515c6..208319a8d 100644
--- a/test/nn/models/test_metapath2vec.py
+++ b/test/nn/models/test_metapath2vec.py
@@ -39,3 +39,25 @@ def test_metapath2vec(device):
     acc = model.test(torch.ones(20, 16), torch.randint(10, (20, )),
                      torch.ones(20, 16), torch.randint(10, (20, )))
     assert 0 <= acc and acc <= 1
+
+
+def test_metapath2vec_empty_edges():
+    num_nodes_dict = {'a': 3, 'b': 4}
+    edge_index_dict = {
+        ('a', 'to', 'b'): torch.empty((2, 0), dtype=torch.long),
+        ('b', 'to', 'a'): torch.empty((2, 0), dtype=torch.long),
+    }
+    metapath = [('a', 'to', 'b'), ('b', 'to', 'a')]
+
+    model = MetaPath2Vec(
+        edge_index_dict,
+        embedding_dim=16,
+        metapath=metapath,
+        walk_length=10,
+        context_size=7,
+        walks_per_node=5,
+        num_negative_samples=5,
+        num_nodes_dict=num_nodes_dict,
+    )
+    loader = model.loader(batch_size=16, shuffle=True)
+    next(iter(loader))