chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:35:51 +08:00
commit c36a561cd8
2172 changed files with 455595 additions and 0 deletions
@@ -0,0 +1 @@
""" DGL graphbolt API tests"""
@@ -0,0 +1,374 @@
import os
import dgl
import dgl.graphbolt as gb
import numpy as np
import pandas as pd
import scipy.sparse as sp
import torch
def rand_csc_graph(N, density, bidirection_edge=False):
adj = sp.random(N, N, density)
if bidirection_edge:
adj = adj + adj.T
adj = adj.tocsc()
indptr = torch.LongTensor(adj.indptr)
indices = torch.LongTensor(adj.indices)
graph = gb.fused_csc_sampling_graph(indptr, indices)
return graph
def random_homo_graph(num_nodes, num_edges):
csc_indptr = torch.randint(0, num_edges, (num_nodes + 1,))
csc_indptr = torch.sort(csc_indptr)[0]
csc_indptr[0] = 0
csc_indptr[-1] = num_edges
indices = torch.randint(0, num_nodes, (num_edges,))
return csc_indptr, indices
def get_type_to_id(num_ntypes, num_etypes):
ntypes = {f"n{i}": i for i in range(num_ntypes)}
etypes = {}
count = 0
for n1 in range(num_ntypes):
for n2 in range(n1, num_ntypes):
if count >= num_etypes:
break
etypes.update({f"n{n1}:e{count}:n{n2}": count})
count += 1
return ntypes, etypes
def get_ntypes_and_etypes(num_nodes, num_ntypes, num_etypes):
ntypes = {f"n{i}": num_nodes // num_ntypes for i in range(num_ntypes)}
if num_nodes % num_ntypes != 0:
ntypes["n0"] += num_nodes % num_ntypes
etypes = []
count = 0
while count < num_etypes:
for n1 in range(num_ntypes):
for n2 in range(num_ntypes):
if count >= num_etypes:
break
etypes.append((f"n{n1}", f"e{count}", f"n{n2}"))
count += 1
return ntypes, etypes
def random_hetero_graph(num_nodes, num_edges, num_ntypes, num_etypes):
ntypes, etypes = get_ntypes_and_etypes(num_nodes, num_ntypes, num_etypes)
edges = {}
for step, etype in enumerate(etypes):
src_ntype, _, dst_ntype = etype
num_e = num_edges // num_etypes + (
0 if step != 0 else num_edges % num_etypes
)
if ntypes[src_ntype] == 0 or ntypes[dst_ntype] == 0:
continue
src = torch.randint(0, ntypes[src_ntype], (num_e,))
dst = torch.randint(0, ntypes[dst_ntype], (num_e,))
edges[etype] = (src, dst)
gb_g = gb.from_dglgraph(dgl.heterograph(edges, ntypes))
return (
gb_g.csc_indptr,
gb_g.indices,
gb_g.node_type_offset,
gb_g.type_per_edge,
gb_g.node_type_to_id,
gb_g.edge_type_to_id,
)
def random_homo_graphbolt_graph(
test_dir, dataset_name, num_nodes, num_edges, num_classes, edge_fmt="csv"
):
"""Generate random graphbolt version homograph"""
# Generate random edges.
nodes = np.repeat(np.arange(num_nodes, dtype=np.int64), 5)
neighbors = np.random.randint(
0, num_nodes, size=(num_edges), dtype=np.int64
)
edges = np.stack([nodes, neighbors], axis=1)
os.makedirs(os.path.join(test_dir, "edges"), exist_ok=True)
assert edge_fmt in [
"numpy",
"csv",
], "Only numpy and csv are supported for edges."
if edge_fmt == "csv":
# Write into edges/edge.csv
edges_DataFrame = pd.DataFrame(edges, columns=["src", "dst"])
edge_path = os.path.join("edges", "edge.csv")
edges_DataFrame.to_csv(
os.path.join(test_dir, edge_path),
index=False,
header=False,
)
else:
# Write into edges/edge.npy
edges = edges.T
edge_path = os.path.join("edges", "edge.npy")
np.save(os.path.join(test_dir, edge_path), edges)
# Generate random graph edge-feats.
edge_feats = np.random.rand(num_edges, num_classes)
os.makedirs(os.path.join(test_dir, "data"), exist_ok=True)
edge_feat_path = os.path.join("data", "edge-feat.npy")
np.save(os.path.join(test_dir, edge_feat_path), edge_feats)
# Generate random node-feats.
if num_classes == 1:
node_feats = np.random.rand(num_nodes)
else:
node_feats = np.random.rand(num_nodes, num_classes)
node_feat_path = os.path.join("data", "node-feat.npy")
np.save(os.path.join(test_dir, node_feat_path), node_feats)
# Generate train/test/valid set.
assert num_nodes % 4 == 0, "num_nodes must be divisible by 4"
each_set_size = num_nodes // 4
os.makedirs(os.path.join(test_dir, "set"), exist_ok=True)
train_pairs = (
np.arange(each_set_size),
np.arange(each_set_size, 2 * each_set_size),
)
train_data = np.vstack(train_pairs).T.astype(edges.dtype)
train_path = os.path.join("set", "train.npy")
np.save(os.path.join(test_dir, train_path), train_data)
validation_pairs = (
np.arange(each_set_size, 2 * each_set_size),
np.arange(2 * each_set_size, 3 * each_set_size),
)
validation_data = np.vstack(validation_pairs).T.astype(edges.dtype)
validation_path = os.path.join("set", "validation.npy")
np.save(os.path.join(test_dir, validation_path), validation_data)
test_pairs = (
np.arange(2 * each_set_size, 3 * each_set_size),
np.arange(3 * each_set_size, 4 * each_set_size),
)
test_data = np.vstack(test_pairs).T.astype(edges.dtype)
test_path = os.path.join("set", "test.npy")
np.save(os.path.join(test_dir, test_path), test_data)
yaml_content = f"""
dataset_name: {dataset_name}
graph: # Graph structure and required attributes.
nodes:
- num: {num_nodes}
edges:
- format: {edge_fmt}
path: {edge_path}
feature_data:
- domain: node
type: null
name: feat
format: numpy
in_memory: true
path: {node_feat_path}
- domain: edge
type: null
name: feat
format: numpy
in_memory: true
path: {edge_feat_path}
feature_data:
- domain: node
type: null
name: feat
format: numpy
in_memory: true
path: {node_feat_path}
- domain: edge
type: null
name: feat
format: numpy
path: {edge_feat_path}
tasks:
- name: link_prediction
num_classes: {num_classes}
train_set:
- type: null
data:
- name: seeds
format: numpy
in_memory: true
path: {train_path}
validation_set:
- type: null
data:
- name: seeds
format: numpy
in_memory: true
path: {validation_path}
test_set:
- type: null
data:
- name: seeds
format: numpy
in_memory: true
path: {test_path}
"""
return yaml_content
def generate_raw_data_for_hetero_dataset(
test_dir, dataset_name, num_nodes, num_edges, num_classes, edge_fmt="csv"
):
# Generate edges.
edges_path = {}
for etype, num_edge in num_edges.items():
src_ntype, etype_str, dst_ntype = etype
src = torch.randint(0, num_nodes[src_ntype], (num_edge,))
dst = torch.randint(0, num_nodes[dst_ntype], (num_edge,))
os.makedirs(os.path.join(test_dir, "edges"), exist_ok=True)
assert edge_fmt in [
"numpy",
"csv",
], "Only numpy and csv are supported for edges."
if edge_fmt == "csv":
# Write into edges/edge.csv
edges = pd.DataFrame(
np.stack([src, dst], axis=1), columns=["src", "dst"]
)
edge_path = os.path.join("edges", f"{etype_str}.csv")
edges.to_csv(
os.path.join(test_dir, edge_path),
index=False,
header=False,
)
else:
edges = np.stack([src, dst], axis=1).T
edge_path = os.path.join("edges", f"{etype_str}.npy")
np.save(os.path.join(test_dir, edge_path), edges)
edges_path[etype_str] = edge_path
# Generate node features.
node_feats_path = {}
os.makedirs(os.path.join(test_dir, "data"), exist_ok=True)
for ntype, num_node in num_nodes.items():
node_feat_path = os.path.join("data", f"{ntype}-feat.npy")
node_feats = np.random.rand(num_node, num_classes)
np.save(os.path.join(test_dir, node_feat_path), node_feats)
node_feats_path[ntype] = node_feat_path
# Generate edge features.
edge_feats_path = {}
os.makedirs(os.path.join(test_dir, "data"), exist_ok=True)
for etype, num_edge in num_edges.items():
src_ntype, etype_str, dst_ntype = etype
edge_feat_path = os.path.join("data", f"{etype_str}-feat.npy")
edge_feats = np.random.rand(num_edge, num_classes)
np.save(os.path.join(test_dir, edge_feat_path), edge_feats)
edge_feats_path[etype_str] = edge_feat_path
# Generate train/test/valid set.
os.makedirs(os.path.join(test_dir, "set"), exist_ok=True)
user_ids = torch.arange(num_nodes["user"])
np.random.shuffle(user_ids.numpy())
num_train = int(num_nodes["user"] * 0.6)
num_validation = int(num_nodes["user"] * 0.2)
num_test = num_nodes["user"] - num_train - num_validation
train_path = os.path.join("set", "train.npy")
np.save(os.path.join(test_dir, train_path), user_ids[:num_train])
validation_path = os.path.join("set", "validation.npy")
np.save(
os.path.join(test_dir, validation_path),
user_ids[num_train : num_train + num_validation],
)
test_path = os.path.join("set", "test.npy")
np.save(
os.path.join(test_dir, test_path),
user_ids[num_train + num_validation :],
)
yaml_content = f"""
dataset_name: {dataset_name}
graph: # Graph structure and required attributes.
nodes:
- type: user
num: {num_nodes["user"]}
- type: item
num: {num_nodes["item"]}
edges:
- type: "user:follow:user"
format: {edge_fmt}
path: {edges_path["follow"]}
- type: "user:click:item"
format: {edge_fmt}
path: {edges_path["click"]}
feature_data:
- domain: node
type: user
name: feat
format: numpy
in_memory: true
path: {node_feats_path["user"]}
- domain: node
type: item
name: feat
format: numpy
in_memory: true
path: {node_feats_path["item"]}
- domain: edge
type: "user:follow:user"
name: feat
format: numpy
in_memory: true
path: {edge_feats_path["follow"]}
- domain: edge
type: "user:click:item"
name: feat
format: numpy
in_memory: true
path: {edge_feats_path["click"]}
feature_data:
- domain: node
type: user
name: feat
format: numpy
in_memory: true
path: {node_feats_path["user"]}
- domain: node
type: item
name: feat
format: numpy
in_memory: true
path: {node_feats_path["item"]}
tasks:
- name: node_classification
num_classes: {num_classes}
train_set:
- type: user
data:
- name: seeds
format: numpy
in_memory: true
path: {train_path}
validation_set:
- type: user
data:
- name: seeds
format: numpy
in_memory: true
path: {validation_path}
test_set:
- type: user
data:
- name: seeds
format: numpy
in_memory: true
path: {test_path}
"""
yaml_file = os.path.join(test_dir, "metadata.yaml")
with open(yaml_file, "w") as f:
f.write(yaml_content)
@@ -0,0 +1 @@
""" DGL graphbolt/impl tests"""
@@ -0,0 +1,151 @@
import pytest
import torch
from dgl import graphbolt as gb
def test_basic_feature_store_homo():
a = torch.tensor([[1, 2, 4], [2, 5, 3]])
b = torch.tensor([[[1, 2], [3, 4]], [[2, 5], [4, 3]]])
metadata = {"max_value": 3}
features = {}
features[("node", None, "a")] = gb.TorchBasedFeature(a, metadata=metadata)
features[("node", None, "b")] = gb.TorchBasedFeature(b)
feature_store = gb.BasicFeatureStore(features)
# Test __getitem__ to access the stored Feature.
feature = feature_store[("node", None, "a")]
assert isinstance(feature, gb.Feature)
assert torch.equal(
feature.read(),
torch.tensor([[1, 2, 4], [2, 5, 3]]),
)
# Test read the entire feature.
assert torch.equal(
feature_store.read("node", None, "a"),
torch.tensor([[1, 2, 4], [2, 5, 3]]),
)
assert torch.equal(
feature_store.read("node", None, "b"),
torch.tensor([[[1, 2], [3, 4]], [[2, 5], [4, 3]]]),
)
# Test read with ids.
assert torch.equal(
feature_store.read("node", None, "a", torch.tensor([0])),
torch.tensor([[1, 2, 4]]),
)
assert torch.equal(
feature_store.read("node", None, "b", torch.tensor([0])),
torch.tensor([[[1, 2], [3, 4]]]),
)
# Test get the size and count of the entire feature.
assert feature_store.size("node", None, "a") == torch.Size([3])
assert feature_store.size("node", None, "b") == torch.Size([2, 2])
assert feature_store.count("node", None, "a") == a.size(0)
assert feature_store.count("node", None, "b") == b.size(0)
# Test get metadata of the feature.
assert feature_store.metadata("node", None, "a") == metadata
assert feature_store.metadata("node", None, "b") == {}
# Test __setitem__ and __contains__ of FeatureStore.
assert ("node", None, "c") not in feature_store
feature_store[("node", None, "c")] = feature_store[("node", None, "a")]
assert ("node", None, "c") in feature_store
# Test get keys of the features.
assert feature_store.keys() == [
("node", None, "a"),
("node", None, "b"),
("node", None, "c"),
]
def test_basic_feature_store_hetero():
a = torch.tensor([[1, 2, 4], [2, 5, 3]])
b = torch.tensor([[[6], [8]], [[8], [9]]])
metadata = {"max_value": 3}
features = {}
features[("node", "author", "a")] = gb.TorchBasedFeature(
a, metadata=metadata
)
features[("edge", "paper:cites", "b")] = gb.TorchBasedFeature(b)
feature_store = gb.BasicFeatureStore(features)
# Test __getitem__ to access the stored Feature.
feature = feature_store[("node", "author", "a")]
assert isinstance(feature, gb.Feature)
assert torch.equal(
feature.read(),
torch.tensor([[1, 2, 4], [2, 5, 3]]),
)
# Test read the entire feature.
assert torch.equal(
feature_store.read("node", "author", "a"),
torch.tensor([[1, 2, 4], [2, 5, 3]]),
)
assert torch.equal(
feature_store.read("edge", "paper:cites", "b"),
torch.tensor([[[6], [8]], [[8], [9]]]),
)
# Test read with ids.
assert torch.equal(
feature_store.read("node", "author", "a", torch.tensor([0])),
torch.tensor([[1, 2, 4]]),
)
# Test get the size of the entire feature.
assert feature_store.size("node", "author", "a") == torch.Size([3])
assert feature_store.size("edge", "paper:cites", "b") == torch.Size([2, 1])
# Test get metadata of the feature.
assert feature_store.metadata("node", "author", "a") == metadata
assert feature_store.metadata("edge", "paper:cites", "b") == {}
# Test __setitem__ and __contains__ of FeatureStore.
assert ("node", "author", "c") not in feature_store
feature_store[("node", "author", "c")] = feature_store[
("node", "author", "a")
]
assert ("node", "author", "c") in feature_store
# Test get keys of the features.
assert feature_store.keys() == [
("node", "author", "a"),
("edge", "paper:cites", "b"),
("node", "author", "c"),
]
def test_basic_feature_store_errors():
a = torch.tensor([3, 2, 1])
b = torch.tensor([[1, 2, 4], [2, 5, 3]])
features = {}
# Test error when dimension of the value is illegal.
with pytest.raises(
AssertionError,
match=rf"dimension of torch_feature in TorchBasedFeature must be "
rf"greater than 1, but got {a.dim()} dimension.",
):
features[("node", "paper", "a")] = gb.TorchBasedFeature(a)
features[("node", "author", "b")] = gb.TorchBasedFeature(b)
feature_store = gb.BasicFeatureStore(features)
# Test error when key does not exist.
with pytest.raises(KeyError):
feature_store.read("node", "paper", "b")
# Test error when at least one id is out of bound.
with pytest.raises(IndexError):
feature_store.read("node", "author", "b", torch.tensor([0, 3]))
@@ -0,0 +1,68 @@
import unittest
from functools import partial
import backend as F
import dgl.graphbolt as gb
import pytest
import torch
WORLD_SIZE = 7
assert_equal = partial(torch.testing.assert_close, rtol=0, atol=0)
@unittest.skipIf(
F._default_context_str != "gpu",
reason="This test requires an NVIDIA GPU.",
)
@pytest.mark.parametrize("dtype", [torch.int32, torch.int64])
@pytest.mark.parametrize("rank", list(range(WORLD_SIZE)))
def test_rank_sort_and_unique_and_compact(dtype, rank):
torch.manual_seed(7)
nodes_list1 = [
torch.randint(0, 2111111111, [777], dtype=dtype, device=F.ctx())
for _ in range(10)
]
nodes_list2 = [nodes.sort()[0] for nodes in nodes_list1]
res1 = torch.ops.graphbolt.rank_sort(nodes_list1, rank, WORLD_SIZE)
res2 = torch.ops.graphbolt.rank_sort(nodes_list2, rank, WORLD_SIZE)
for i, ((nodes1, idx1, offsets1), (nodes2, idx2, offsets2)) in enumerate(
zip(res1, res2)
):
assert_equal(nodes_list1[i], nodes1[idx1])
assert_equal(nodes_list2[i], nodes2[idx2])
assert_equal(offsets1, offsets2)
assert offsets1.is_pinned() and offsets2.is_pinned()
res3 = torch.ops.graphbolt.rank_sort(nodes_list1, rank, WORLD_SIZE)
# This function is deterministic. Call with identical arguments and check.
for (nodes1, idx1, offsets1), (nodes3, idx3, offsets3) in zip(res1, res3):
assert_equal(nodes1, nodes3)
assert_equal(idx1, idx3)
assert_equal(offsets1, offsets3)
# The dependency on the rank argument is simply a permutation.
res4 = torch.ops.graphbolt.rank_sort(nodes_list1, 0, WORLD_SIZE)
for (nodes1, idx1, offsets1), (nodes4, idx4, offsets4) in zip(res1, res4):
off1 = offsets1.tolist()
off4 = offsets4.tolist()
assert_equal(nodes1[idx1], nodes4[idx4])
for i in range(WORLD_SIZE):
j = (i - rank + WORLD_SIZE) % WORLD_SIZE
assert_equal(
nodes1[off1[j] : off1[j + 1]], nodes4[off4[i] : off4[i + 1]]
)
unique, compacted, offsets = gb.unique_and_compact(
nodes_list1[:1], rank, WORLD_SIZE
)
nodes1, idx1, offsets1 = res1[0]
assert_equal(unique, nodes1)
assert_equal(compacted[0], idx1)
assert_equal(offsets, offsets1)
@@ -0,0 +1,184 @@
import os
import tempfile
import unittest
import backend as F
import numpy as np
import pytest
import torch
from dgl import graphbolt as gb
def to_on_disk_numpy(test_dir, name, t):
path = os.path.join(test_dir, name + ".npy")
np.save(path, t.numpy())
return path
@pytest.mark.parametrize(
"dtype",
[
torch.bool,
torch.uint8,
torch.int8,
torch.int16,
torch.int32,
torch.int64,
torch.float16,
torch.bfloat16,
torch.float32,
torch.float64,
],
)
@pytest.mark.parametrize("policy", ["s3-fifo", "sieve", "lru", "clock"])
def test_cpu_cached_feature(dtype, policy):
cache_size_a = 32
cache_size_b = 64
a = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=dtype)
b = torch.tensor([[[1, 2], [3, 4]], [[4, 5], [6, 7]]], dtype=dtype)
pin_memory = F._default_context_str == "gpu"
cache_size_a *= a[:1].nbytes
cache_size_b *= b[:1].nbytes
feat_store_a = gb.cpu_cached_feature(
gb.TorchBasedFeature(a), cache_size_a, policy, pin_memory
)
feat_store_b = gb.cpu_cached_feature(
gb.TorchBasedFeature(b), cache_size_b, policy, pin_memory
)
# Test read the entire feature.
assert torch.equal(feat_store_a.read(), a)
assert torch.equal(feat_store_b.read(), b)
# Test read with ids.
assert torch.equal(
# Test read when ids are on a different device.
feat_store_a.read(torch.tensor([0], device=F.ctx())),
torch.tensor([[1, 2, 3]], dtype=dtype, device=F.ctx()),
)
assert torch.equal(
feat_store_b.read(torch.tensor([1, 1])),
torch.tensor([[[4, 5], [6, 7]], [[4, 5], [6, 7]]], dtype=dtype),
)
assert torch.equal(
feat_store_a.read(torch.tensor([1, 1])),
torch.tensor([[4, 5, 6], [4, 5, 6]], dtype=dtype),
)
assert torch.equal(
feat_store_b.read(torch.tensor([0])),
torch.tensor([[[1, 2], [3, 4]]], dtype=dtype),
)
# The cache should be full now for the large cache sizes, %100 hit expected.
total_miss = feat_store_a._feature.total_miss
feat_store_a.read(torch.tensor([0, 1]))
assert total_miss == feat_store_a._feature.total_miss
total_miss = feat_store_b._feature.total_miss
feat_store_b.read(torch.tensor([0, 1]))
assert total_miss == feat_store_b._feature.total_miss
assert feat_store_a._feature.miss_rate == feat_store_a.miss_rate
# Test get the size and count of the entire feature.
assert feat_store_a.size() == torch.Size([3])
assert feat_store_b.size() == torch.Size([2, 2])
assert feat_store_a.count() == a.size(0)
assert feat_store_b.count() == b.size(0)
# Test update the entire feature.
feat_store_a.update(torch.tensor([[0, 1, 2], [3, 5, 2]], dtype=dtype))
assert torch.equal(
feat_store_a.read(),
torch.tensor([[0, 1, 2], [3, 5, 2]], dtype=dtype),
)
# Test update with ids.
feat_store_a.update(
torch.tensor([[2, 0, 1]], dtype=dtype),
torch.tensor([0]),
)
assert torch.equal(
feat_store_a.read(),
torch.tensor([[2, 0, 1], [3, 5, 2]], dtype=dtype),
)
# Test with different dimensionality
feat_store_a.update(b)
assert torch.equal(feat_store_a.read(), b)
@pytest.mark.parametrize(
"dtype",
[
torch.bool,
torch.uint8,
torch.int8,
torch.int16,
torch.int32,
torch.int64,
torch.float16,
torch.bfloat16,
torch.float32,
torch.float64,
],
)
def test_cpu_cached_feature_read_async(dtype):
a = torch.randint(0, 2, [1000, 13], dtype=dtype)
cache_size = 256 * a[:1].nbytes
feat_store = gb.cpu_cached_feature(gb.TorchBasedFeature(a), cache_size)
# Test read with ids.
ids1 = torch.tensor([0, 15, 71, 101])
ids2 = torch.tensor([71, 101, 202, 303])
for ids in [ids1, ids2]:
reader = feat_store.read_async(ids)
for _ in range(feat_store.read_async_num_stages(ids.device)):
values = next(reader)
assert torch.equal(values.wait(), a[ids])
@unittest.skipIf(
not torch.ops.graphbolt.detect_io_uring(),
reason="DiskBasedFeature is not available on this system.",
)
@pytest.mark.parametrize(
"dtype",
[
torch.bool,
torch.uint8,
torch.int8,
torch.int16,
torch.int32,
torch.int64,
torch.float16,
torch.float32,
torch.float64,
],
)
def test_cpu_cached_disk_feature_read_async(dtype):
a = torch.randint(0, 2, [1000, 13], dtype=dtype)
cache_size = 256 * a[:1].nbytes
ids1 = torch.tensor([0, 15, 71, 101])
ids2 = torch.tensor([71, 101, 202, 303])
with tempfile.TemporaryDirectory() as test_dir:
path = to_on_disk_numpy(test_dir, "tensor", a)
feat_store = gb.cpu_cached_feature(
gb.DiskBasedFeature(path=path), cache_size
)
# Test read feature.
for ids in [ids1, ids2]:
reader = feat_store.read_async(ids)
for _ in range(feat_store.read_async_num_stages(ids.device)):
values = next(reader)
assert torch.equal(values.wait(), a[ids])
feat_store = None
@@ -0,0 +1,192 @@
import os
import tempfile
import unittest
from functools import partial
import backend as F
import numpy as np
import pytest
import torch
from dgl import graphbolt as gb
def to_on_disk_numpy(test_dir, name, t):
path = os.path.join(test_dir, name + ".npy")
t = t.numpy()
np.save(path, t)
return path
assert_equal = partial(torch.testing.assert_close, rtol=0, atol=0)
@unittest.skipIf(
not torch.ops.graphbolt.detect_io_uring(),
reason="DiskBasedFeature is not available on this system.",
)
def test_disk_based_feature():
with tempfile.TemporaryDirectory() as test_dir:
a = torch.tensor([[1, 2, 3], [4, 5, 6]])
b = torch.tensor([[[1, 2], [3, 4]], [[4, 5], [6, 7]]])
c = torch.randn([4111, 47])
metadata = {"max_value": 3}
path_a = to_on_disk_numpy(test_dir, "a", a)
path_b = to_on_disk_numpy(test_dir, "b", b)
path_c = to_on_disk_numpy(test_dir, "c", c)
feature_a = gb.DiskBasedFeature(path=path_a, metadata=metadata)
feature_b = gb.DiskBasedFeature(path=path_b)
feature_c = gb.DiskBasedFeature(path=path_c)
# Read the entire feature.
assert_equal(feature_a.read(), torch.tensor([[1, 2, 3], [4, 5, 6]]))
assert_equal(
feature_b.read(), torch.tensor([[[1, 2], [3, 4]], [[4, 5], [6, 7]]])
)
# Test read the feature with ids.
assert_equal(
feature_a.read(torch.tensor([0])),
torch.tensor([[1, 2, 3]]),
)
assert_equal(
feature_b.read(torch.tensor([1])),
torch.tensor([[[4, 5], [6, 7]]]),
)
# Test reading into pin_memory
if F._default_context_str == "gpu":
res = feature_a.read(torch.tensor([0], pin_memory=True))
assert res.is_pinned()
# Test when the index tensor is large.
torch_based_feature_a = gb.TorchBasedFeature(a)
ind_a = torch.randint(low=0, high=a.size(0), size=(4111,))
assert_equal(
feature_a.read(ind_a),
torch_based_feature_a.read(ind_a),
)
# Test converting to torch_based_feature with read_into_memory()
torch_based_feature_b = feature_b.read_into_memory()
ind_b = torch.randint(low=0, high=b.size(0), size=(4111,))
assert_equal(
feature_b.read(ind_b),
torch_based_feature_b.read(ind_b),
)
# Test with larger stored feature tensor
ind_c = torch.randint(low=0, high=c.size(0), size=(4111,))
assert_equal(feature_c.read(ind_c), c[ind_c])
# Test get the size and count of the entire feature.
assert feature_a.size() == torch.Size([3])
assert feature_b.size() == torch.Size([2, 2])
assert feature_a.count() == a.size(0)
assert feature_b.count() == b.size(0)
# Test get metadata of the feature.
assert feature_a.metadata() == metadata
assert feature_b.metadata() == {}
with pytest.raises(IndexError):
feature_a.read(torch.tensor([0, 1, 2, 3]))
# Test loading a Fortran contiguous ndarray.
a_T = np.asfortranarray(a)
path_a_T = test_dir + "a_T.npy"
np.save(path_a_T, a_T)
with pytest.raises(
AssertionError,
match="DiskBasedFeature only supports C_CONTIGUOUS array.",
):
gb.DiskBasedFeature(path=path_a_T, metadata=metadata)
# For windows, the file is locked by the numpy.load. We need to delete
# it before closing the temporary directory.
a = b = c = None
feature_a = feature_b = feature_c = None
@unittest.skipIf(
not torch.ops.graphbolt.detect_io_uring(),
reason="DiskBasedFeature is not available on this system.",
)
@pytest.mark.parametrize(
"dtype",
[
torch.float32,
torch.float64,
torch.int32,
torch.int64,
torch.int8,
torch.float16,
torch.complex128,
],
)
@pytest.mark.parametrize("idtype", [torch.int32, torch.int64])
@pytest.mark.parametrize(
"shape", [(10, 20), (20, 10), (20, 25, 10), (137, 50, 30)]
)
@pytest.mark.parametrize("index", [[0], [1, 2, 3], [0, 6, 2, 8]])
def test_more_disk_based_feature(dtype, idtype, shape, index):
if dtype == torch.complex128:
tensor = torch.complex(
torch.randint(0, 127, shape, dtype=torch.float64),
torch.randint(0, 127, shape, dtype=torch.float64),
)
else:
tensor = torch.randint(0, 127, shape, dtype=dtype)
test_tensor = tensor.clone()
idx = torch.tensor(index, dtype=idtype)
with tempfile.TemporaryDirectory() as test_dir:
path = to_on_disk_numpy(test_dir, "tensor", tensor)
feature = gb.DiskBasedFeature(path=path)
# Test read feature.
assert_equal(feature.read(idx), test_tensor[idx.long()])
@unittest.skipIf(
not torch.ops.graphbolt.detect_io_uring(),
reason="DiskBasedFeature is not available on this system.",
)
def test_disk_based_feature_repr():
with tempfile.TemporaryDirectory() as test_dir:
a = torch.tensor([[1, 2, 3], [4, 5, 6]])
b = torch.tensor([[[1, 2], [3, 4]], [[4, 5], [6, 7]]])
metadata = {"max_value": 3}
path_a = to_on_disk_numpy(test_dir, "a", a)
path_b = to_on_disk_numpy(test_dir, "b", b)
feature_a = gb.DiskBasedFeature(path=path_a, metadata=metadata)
feature_b = gb.DiskBasedFeature(path=path_b)
expected_str_feature_a = str(
"DiskBasedFeature(\n"
" feature=tensor([[1, 2, 3],\n"
" [4, 5, 6]]),\n"
" metadata={'max_value': 3},\n"
")"
)
expected_str_feature_b = str(
"DiskBasedFeature(\n"
" feature=tensor([[[1, 2],\n"
" [3, 4]],\n"
"\n"
" [[4, 5],\n"
" [6, 7]]]),\n"
" metadata={},\n"
")"
)
assert str(feature_a) == expected_str_feature_a
assert str(feature_b) == expected_str_feature_b
a = b = metadata = None
feature_a = feature_b = None
expected_str_feature_a = expected_str_feature_b = None
@@ -0,0 +1,172 @@
import backend as F
import pytest
import torch
from dgl import graphbolt as gb
def _test_query_and_replace(policy1, policy2, keys, offset):
# Testing query_and_replace equivalence to query and then replace.
(
_,
index,
pointers,
missing_keys,
found_offsets,
missing_offsets,
) = policy1.query_and_replace(keys, offset)
found_cnt = keys.size(0) - missing_keys.size(0)
found_pointers = pointers[:found_cnt]
policy1.reading_completed(found_pointers, found_offsets)
missing_pointers = pointers[found_cnt:]
policy1.writing_completed(missing_pointers, missing_offsets)
(
_,
index2,
missing_keys2,
found_pointers2,
found_offsets2,
missing_offsets2,
) = policy2.query(keys + offset, 0)
policy2.reading_completed(found_pointers2, found_offsets2)
(_, missing_pointers2, missing_offsets2) = policy2.replace(
missing_keys2, missing_offsets2, 0
)
policy2.writing_completed(missing_pointers2, missing_offsets2)
assert torch.equal(index, index2)
assert torch.equal(missing_keys, missing_keys2 - offset)
@pytest.mark.parametrize("offsets", [False, True])
@pytest.mark.parametrize(
"dtype",
[
torch.bool,
torch.uint8,
torch.int8,
torch.int16,
torch.int32,
torch.int64,
torch.float16,
torch.bfloat16,
torch.float32,
torch.float64,
],
)
@pytest.mark.parametrize("feature_size", [2, 16])
@pytest.mark.parametrize("num_parts", [1, 2, None])
@pytest.mark.parametrize("policy", ["s3-fifo", "sieve", "lru", "clock"])
@pytest.mark.parametrize("offset", [0, 1111111])
def test_feature_cache(offsets, dtype, feature_size, num_parts, policy, offset):
cache_size = 32 * (
torch.get_num_threads() if num_parts is None else num_parts
)
a = torch.randint(0, 2, [1024, feature_size], dtype=dtype)
cache = gb.impl.CPUFeatureCache(
(cache_size,) + a.shape[1:], a.dtype, policy, num_parts
)
cache2 = gb.impl.CPUFeatureCache(
(cache_size,) + a.shape[1:], a.dtype, policy, num_parts
)
policy1 = gb.impl.CPUFeatureCache(
(cache_size,) + a.shape[1:], a.dtype, policy, num_parts
)._policy
policy2 = gb.impl.CPUFeatureCache(
(cache_size,) + a.shape[1:], a.dtype, policy, num_parts
)._policy
reader_fn = lambda keys: a[keys]
keys = torch.tensor([0, 1])
values, missing_index, missing_keys, missing_offsets = cache.query(
keys, offset
)
if not offsets:
missing_offsets = None
assert torch.equal(
missing_keys.flip([0]) if num_parts == 1 else missing_keys.sort()[0],
keys,
)
missing_values = a[missing_keys]
cache.replace(missing_keys, missing_values, missing_offsets, offset)
values[missing_index] = missing_values
assert torch.equal(values, a[keys])
assert torch.equal(
cache2.query_and_replace(keys, reader_fn, offset), a[keys]
)
_test_query_and_replace(policy1, policy2, keys, offset)
pin_memory = F._default_context_str == "gpu"
keys = torch.arange(1, 33, pin_memory=pin_memory)
values, missing_index, missing_keys, missing_offsets = cache.query(
keys, offset
)
if not offsets:
missing_offsets = None
assert torch.equal(
missing_keys.flip([0]) if num_parts == 1 else missing_keys.sort()[0],
torch.arange(2, 33),
)
assert not pin_memory or values.is_pinned()
missing_values = a[missing_keys]
cache.replace(missing_keys, missing_values, missing_offsets, offset)
values[missing_index] = missing_values
assert torch.equal(values, a[keys])
assert torch.equal(
cache2.query_and_replace(keys, reader_fn, offset), a[keys]
)
_test_query_and_replace(policy1, policy2, keys, offset)
values, missing_index, missing_keys, missing_offsets = cache.query(
keys, offset
)
if not offsets:
missing_offsets = None
assert torch.equal(missing_keys.flip([0]), torch.tensor([]))
missing_values = a[missing_keys]
cache.replace(missing_keys, missing_values, missing_offsets, offset)
values[missing_index] = missing_values
assert torch.equal(values, a[keys])
assert torch.equal(
cache2.query_and_replace(keys, reader_fn, offset), a[keys]
)
_test_query_and_replace(policy1, policy2, keys, offset)
values, missing_index, missing_keys, missing_offsets = cache.query(
keys, offset
)
if not offsets:
missing_offsets = None
assert torch.equal(missing_keys.flip([0]), torch.tensor([]))
missing_values = a[missing_keys]
cache.replace(missing_keys, missing_values, missing_offsets, offset)
values[missing_index] = missing_values
assert torch.equal(values, a[keys])
assert torch.equal(
cache2.query_and_replace(keys, reader_fn, offset), a[keys]
)
_test_query_and_replace(policy1, policy2, keys, offset)
assert cache.miss_rate == cache2.miss_rate
raw_feature_cache = torch.ops.graphbolt.feature_cache(
(cache_size,) + a.shape[1:], a.dtype, pin_memory
)
idx = torch.tensor([0, 1, 2])
raw_feature_cache.replace(idx, a[idx])
val = raw_feature_cache.index_select(idx)
assert torch.equal(val, a[idx])
if pin_memory:
val = raw_feature_cache.index_select(idx.to(F.ctx()))
assert torch.equal(val, a[idx].to(F.ctx()))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,219 @@
import os
import tempfile
import unittest
import backend as F
import numpy as np
import pytest
import torch
from dgl import graphbolt as gb
def to_on_disk_numpy(test_dir, name, t):
path = os.path.join(test_dir, name + ".npy")
np.save(path, t.cpu().numpy())
return path
def _skip_condition_cached_feature():
return (F._default_context_str != "gpu") or (
torch.cuda.get_device_capability()[0] < 7
)
def _reason_to_skip_cached_feature():
if F._default_context_str != "gpu":
return "GPUCachedFeature tests are available only when testing the GPU backend."
return "GPUCachedFeature requires a Volta or later generation NVIDIA GPU."
@unittest.skipIf(
_skip_condition_cached_feature(),
reason=_reason_to_skip_cached_feature(),
)
@pytest.mark.parametrize(
"dtype",
[
torch.bool,
torch.uint8,
torch.int8,
torch.int16,
torch.int32,
torch.int64,
torch.float16,
torch.bfloat16,
torch.float32,
torch.float64,
],
)
@pytest.mark.parametrize("cache_size_a", [1, 1024])
@pytest.mark.parametrize("cache_size_b", [1, 1024])
def test_gpu_cached_feature(dtype, cache_size_a, cache_size_b):
a = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=dtype, pin_memory=True)
b = torch.tensor(
[[[1, 2], [3, 4]], [[4, 5], [6, 7]]], dtype=dtype, pin_memory=True
)
cache_size_a *= a[:1].element_size() * a[:1].numel()
cache_size_b *= b[:1].element_size() * b[:1].numel()
feat_store_a = gb.gpu_cached_feature(gb.TorchBasedFeature(a), cache_size_a)
feat_store_b = gb.gpu_cached_feature(gb.TorchBasedFeature(b), cache_size_b)
# Test read the entire feature.
assert torch.equal(feat_store_a.read(), a.to("cuda"))
assert torch.equal(feat_store_b.read(), b.to("cuda"))
# Test read with ids.
assert torch.equal(
feat_store_a.read(torch.tensor([0]).to("cuda")),
torch.tensor([[1, 2, 3]], dtype=dtype).to("cuda"),
)
assert torch.equal(
feat_store_b.read(torch.tensor([1, 1]).to("cuda")),
torch.tensor([[[4, 5], [6, 7]], [[4, 5], [6, 7]]], dtype=dtype).to(
"cuda"
),
)
assert torch.equal(
feat_store_a.read(torch.tensor([1, 1]).to("cuda")),
torch.tensor([[4, 5, 6], [4, 5, 6]], dtype=dtype).to("cuda"),
)
assert torch.equal(
feat_store_b.read(torch.tensor([0]).to("cuda")),
torch.tensor([[[1, 2], [3, 4]]], dtype=dtype).to("cuda"),
)
# The cache should be full now for the large cache sizes, %100 hit expected.
if cache_size_a >= 1024:
total_miss = feat_store_a._feature.total_miss
feat_store_a.read(torch.tensor([0, 1]).to("cuda"))
assert total_miss == feat_store_a._feature.total_miss
if cache_size_b >= 1024:
total_miss = feat_store_b._feature.total_miss
feat_store_b.read(torch.tensor([0, 1]).to("cuda"))
assert total_miss == feat_store_b._feature.total_miss
assert feat_store_a._feature.miss_rate == feat_store_a.miss_rate
# Test get the size and count of the entire feature.
assert feat_store_a.size() == torch.Size([3])
assert feat_store_b.size() == torch.Size([2, 2])
assert feat_store_a.count() == a.size(0)
assert feat_store_b.count() == b.size(0)
# Test update the entire feature.
feat_store_a.update(
torch.tensor([[0, 1, 2], [3, 5, 2]], dtype=dtype).to("cuda")
)
assert torch.equal(
feat_store_a.read(),
torch.tensor([[0, 1, 2], [3, 5, 2]], dtype=dtype).to("cuda"),
)
# Test update with ids.
feat_store_a.update(
torch.tensor([[2, 0, 1]], dtype=dtype).to("cuda"),
torch.tensor([0]).to("cuda"),
)
assert torch.equal(
feat_store_a.read(),
torch.tensor([[2, 0, 1], [3, 5, 2]], dtype=dtype).to("cuda"),
)
# Test with different dimensionality
feat_store_a.update(b)
assert torch.equal(feat_store_a.read(), b.to("cuda"))
@unittest.skipIf(
_skip_condition_cached_feature(),
reason=_reason_to_skip_cached_feature(),
)
@pytest.mark.parametrize(
"dtype",
[
torch.bool,
torch.uint8,
torch.int8,
torch.int16,
torch.int32,
torch.int64,
torch.float16,
torch.bfloat16,
torch.float32,
torch.float64,
],
)
@pytest.mark.parametrize("pin_memory", [False, True])
def test_gpu_cached_feature_read_async(dtype, pin_memory):
a = torch.randint(0, 2, [1000, 13], dtype=dtype, pin_memory=pin_memory)
a_cuda = a.to(F.ctx())
cache_size = 256 * a[:1].nbytes
feat_store = gb.gpu_cached_feature(gb.TorchBasedFeature(a), cache_size)
# Test read with ids.
ids1 = torch.tensor([0, 15, 71, 101], device=F.ctx())
ids2 = torch.tensor([71, 101, 202, 303], device=F.ctx())
for ids in [ids1, ids2]:
reader = feat_store.read_async(ids)
for _ in range(feat_store.read_async_num_stages(ids.device)):
values = next(reader)
assert torch.equal(values.wait(), a_cuda[ids])
@unittest.skipIf(
_skip_condition_cached_feature(),
reason=_reason_to_skip_cached_feature(),
)
@unittest.skipIf(
not torch.ops.graphbolt.detect_io_uring(),
reason="DiskBasedFeature is not available on this system.",
)
@pytest.mark.parametrize(
"dtype",
[
torch.bool,
torch.uint8,
torch.int8,
torch.int16,
torch.int32,
torch.int64,
torch.float16,
torch.float32,
torch.float64,
],
)
def test_gpu_cached_nested_feature_async(dtype):
a = torch.randint(0, 2, [1000, 13], dtype=dtype, device=F.ctx())
cache_size = 256 * a[:1].nbytes
ids1 = torch.tensor([0, 15, 71, 101], device=F.ctx())
ids2 = torch.tensor([71, 101, 202, 303], device=F.ctx())
with tempfile.TemporaryDirectory() as test_dir:
path = to_on_disk_numpy(test_dir, "tensor", a)
disk_store = gb.DiskBasedFeature(path=path)
feat_store1 = gb.gpu_cached_feature(disk_store, cache_size)
feat_store2 = gb.gpu_cached_feature(
gb.cpu_cached_feature(disk_store, cache_size * 2), cache_size
)
feat_store3 = gb.gpu_cached_feature(
gb.cpu_cached_feature(disk_store, cache_size * 2, pin_memory=True),
cache_size,
)
# Test read feature.
for feat_store in [feat_store1, feat_store2, feat_store3]:
for ids in [ids1, ids2]:
reader = feat_store.read_async(ids)
for _ in range(feat_store.read_async_num_stages(ids.device)):
values = next(reader)
assert torch.equal(values.wait(), a[ids])
feat_store1 = feat_store2 = feat_store3 = disk_store = None
@@ -0,0 +1,82 @@
import unittest
import backend as F
import dgl.graphbolt as gb
import pytest
import torch
@unittest.skipIf(
F._default_context_str != "gpu"
or torch.cuda.get_device_capability()[0] < 7,
reason="GPUCachedFeature tests are available only when testing the GPU backend."
if F._default_context_str != "gpu"
else "GPUCachedFeature requires a Volta or later generation NVIDIA GPU.",
)
@pytest.mark.parametrize(
"indptr_dtype",
[
torch.int32,
torch.int64,
],
)
@pytest.mark.parametrize(
"dtype",
[
torch.bool,
torch.uint8,
torch.int8,
torch.int16,
torch.int32,
torch.int64,
torch.float16,
torch.bfloat16,
torch.float32,
torch.float64,
],
)
@pytest.mark.parametrize("cache_size", [4, 9, 11])
@pytest.mark.parametrize("with_edge_ids", [True, False])
def test_gpu_graph_cache(indptr_dtype, dtype, cache_size, with_edge_ids):
indices_dtype = torch.int32
indptr = torch.tensor([0, 3, 6, 10], dtype=indptr_dtype, pin_memory=True)
indices = torch.arange(0, indptr[-1], dtype=indices_dtype, pin_memory=True)
probs_or_mask = indices.to(dtype).pin_memory()
edge_tensors = [indices, probs_or_mask]
g = gb.GPUGraphCache(
cache_size,
2,
indptr.dtype,
[e.dtype for e in edge_tensors],
not with_edge_ids,
)
for i in range(10):
keys = (
torch.arange(2, dtype=indices_dtype, device=F.ctx()) + i * 2
) % (indptr.size(0) - 1)
missing_keys, replace = g.query(keys)
(
missing_indptr,
missing_edge_tensors,
) = torch.ops.graphbolt.index_select_csc_batched(
indptr, edge_tensors, missing_keys, with_edge_ids, None
)
output_indptr, output_edge_tensors = replace(
missing_indptr, missing_edge_tensors
)
(
reference_indptr,
reference_edge_tensors,
) = torch.ops.graphbolt.index_select_csc_batched(
indptr, edge_tensors, keys, with_edge_ids, None
)
assert torch.equal(output_indptr, reference_indptr)
assert len(output_edge_tensors) == len(reference_edge_tensors)
for e, ref in zip(output_edge_tensors, reference_edge_tensors):
assert torch.equal(e, ref)
@@ -0,0 +1,42 @@
import backend as F
import pytest
import torch
from dgl import graphbolt as gb
@pytest.mark.parametrize(
"cached_feature_type", [gb.cpu_cached_feature, gb.gpu_cached_feature]
)
def test_hetero_cached_feature(cached_feature_type):
if cached_feature_type == gb.gpu_cached_feature and (
F._default_context_str != "gpu"
or torch.cuda.get_device_capability()[0] < 7
):
pytest.skip(
"GPUCachedFeature tests are available only when testing the GPU backend."
if F._default_context_str != "gpu"
else "GPUCachedFeature requires a Volta or later generation NVIDIA GPU."
)
device = F.ctx() if cached_feature_type == gb.gpu_cached_feature else None
pin_memory = cached_feature_type == gb.gpu_cached_feature
a = {
("node", str(i), "feat"): gb.TorchBasedFeature(
torch.randn([(i + 1) * 10, 5], pin_memory=pin_memory)
)
for i in range(75)
}
cached_a = cached_feature_type(a, 2**18)
for i in range(1024):
etype = i % len(a)
ids = torch.randint(
0, (etype + 1) * 10 - 1, ((etype + 1) * 4,), device=device
)
feature_key = ("node", str(etype), "feat")
ref = a[feature_key].read(ids)
val = cached_a[feature_key].read(ids)
torch.testing.assert_close(ref, val, rtol=0, atol=0)
assert cached_a[feature_key].miss_rate < 0.69
@@ -0,0 +1,287 @@
import unittest
import backend as F
import dgl.graphbolt as gb
import pytest
import torch
from .. import gb_test_utils
@unittest.skipIf(
F._default_context_str == "cpu",
reason="Tests for pinned memory are only meaningful on GPU.",
)
@pytest.mark.parametrize(
"indptr_dtype",
[torch.int32, torch.int64],
)
@pytest.mark.parametrize(
"indices_dtype",
[
torch.int8,
torch.uint8,
torch.int16,
torch.int32,
torch.int64,
torch.float32,
torch.float64,
],
)
@pytest.mark.parametrize("idtype", [torch.int32, torch.int64])
@pytest.mark.parametrize("is_pinned", [False, True])
@pytest.mark.parametrize("with_edge_ids", [False, True])
@pytest.mark.parametrize("output_size", [None, True])
def test_index_select_csc(
indptr_dtype, indices_dtype, idtype, is_pinned, with_edge_ids, output_size
):
"""Original graph in COO:
1 0 1 0 1 0
1 0 0 1 0 1
0 1 0 1 0 0
0 1 0 0 1 0
1 0 0 0 0 1
0 0 1 0 1 0
"""
indptr = torch.tensor([0, 3, 5, 7, 9, 12, 14], dtype=indptr_dtype)
indices = torch.tensor(
[0, 1, 4, 2, 3, 0, 5, 1, 2, 0, 3, 5, 1, 4], dtype=indices_dtype
)
index = torch.tensor([0, 5, 3], dtype=idtype)
cpu_indptr, cpu_indices = torch.ops.graphbolt.index_select_csc(
indptr, indices, index, None
)
if is_pinned:
indptr = indptr.pin_memory()
indices = indices.pin_memory()
else:
indptr = indptr.cuda()
indices = indices.cuda()
index = index.cuda()
edge_ids = torch.tensor(
[0, 1, 2, 12, 13, 7, 8], dtype=indptr_dtype, device=index.device
)
if output_size:
output_size = len(cpu_indices)
gpu_indptr, gpu_indices = torch.ops.graphbolt.index_select_csc(
indptr, indices, index, output_size
)
assert not cpu_indptr.is_cuda
assert not cpu_indices.is_cuda
assert gpu_indptr.is_cuda
assert gpu_indices.is_cuda
assert torch.equal(cpu_indptr, gpu_indptr.cpu())
assert torch.equal(cpu_indices, gpu_indices.cpu())
for output_size_selection in [None, output_size]:
indices_list = [
indices,
indices.int().pin_memory() if is_pinned else indices.int(),
]
(
gpu_indptr2,
gpu_indices_list,
) = torch.ops.graphbolt.index_select_csc_batched(
indptr, indices_list, index, with_edge_ids, output_size_selection
)
assert torch.equal(gpu_indptr, gpu_indptr2)
assert torch.equal(gpu_indices_list[0], gpu_indices)
assert torch.equal(gpu_indices_list[1], gpu_indices.int())
if with_edge_ids:
assert torch.equal(gpu_indices_list[2], edge_ids)
def test_InSubgraphSampler_homo():
"""Original graph in COO:
1 0 1 0 1 0
1 0 0 1 0 1
0 1 0 1 0 0
0 1 0 0 1 0
1 0 0 0 0 1
0 0 1 0 1 0
"""
indptr = torch.LongTensor([0, 3, 5, 7, 9, 12, 14])
indices = torch.LongTensor([0, 1, 4, 2, 3, 0, 5, 1, 2, 0, 3, 5, 1, 4])
graph = gb.fused_csc_sampling_graph(indptr, indices).to(F.ctx())
seed_nodes = torch.LongTensor([0, 5, 3])
item_set = gb.ItemSet(seed_nodes, names="seeds")
batch_size = 1
item_sampler = gb.ItemSampler(item_set, batch_size=batch_size).copy_to(
F.ctx()
)
in_subgraph_sampler = gb.InSubgraphSampler(item_sampler, graph)
it = iter(in_subgraph_sampler)
def original_indices(minibatch):
sampled_subgraph = minibatch.sampled_subgraphs[0]
_indices = sampled_subgraph.original_row_node_ids[
sampled_subgraph.sampled_csc.indices
]
return _indices
mn = next(it)
assert torch.equal(mn.seeds, torch.LongTensor([0]).to(F.ctx()))
assert torch.equal(
mn.sampled_subgraphs[0].sampled_csc.indptr,
torch.tensor([0, 3]).to(F.ctx()),
)
mn = next(it)
assert torch.equal(mn.seeds, torch.LongTensor([5]).to(F.ctx()))
assert torch.equal(
mn.sampled_subgraphs[0].sampled_csc.indptr,
torch.tensor([0, 2]).to(F.ctx()),
)
assert torch.equal(original_indices(mn), torch.tensor([1, 4]).to(F.ctx()))
mn = next(it)
assert torch.equal(mn.seeds, torch.LongTensor([3]).to(F.ctx()))
assert torch.equal(
mn.sampled_subgraphs[0].sampled_csc.indptr,
torch.tensor([0, 2]).to(F.ctx()),
)
assert torch.equal(original_indices(mn), torch.tensor([1, 2]).to(F.ctx()))
def test_InSubgraphSampler_hetero():
"""Original graph in COO:
1 0 1 0 1 0
1 0 0 1 0 1
0 1 0 1 0 0
0 1 0 0 1 0
1 0 0 0 0 1
0 0 1 0 1 0
node_type_0: [0, 1, 2]
node_type_1: [3, 4, 5]
edge_type_0: node_type_0 -> node_type_0
edge_type_1: node_type_0 -> node_type_1
edge_type_2: node_type_1 -> node_type_0
edge_type_3: node_type_1 -> node_type_1
"""
ntypes = {
"N0": 0,
"N1": 1,
}
etypes = {
"N0:R0:N0": 0,
"N0:R1:N1": 1,
"N1:R2:N0": 2,
"N1:R3:N1": 3,
}
indptr = torch.LongTensor([0, 3, 5, 7, 9, 12, 14])
indices = torch.LongTensor([0, 1, 4, 2, 3, 0, 5, 1, 2, 0, 3, 5, 1, 4])
node_type_offset = torch.LongTensor([0, 3, 6])
type_per_edge = torch.LongTensor([0, 0, 2, 0, 2, 0, 2, 1, 1, 1, 3, 3, 1, 3])
graph = gb.fused_csc_sampling_graph(
csc_indptr=indptr,
indices=indices,
node_type_offset=node_type_offset,
type_per_edge=type_per_edge,
node_type_to_id=ntypes,
edge_type_to_id=etypes,
).to(F.ctx())
item_set = gb.HeteroItemSet(
{
"N0": gb.ItemSet(torch.LongTensor([1, 0, 2]), names="seeds"),
"N1": gb.ItemSet(torch.LongTensor([0, 2, 1]), names="seeds"),
}
)
batch_size = 2
item_sampler = gb.ItemSampler(item_set, batch_size=batch_size).copy_to(
F.ctx()
)
in_subgraph_sampler = gb.InSubgraphSampler(item_sampler, graph)
it = iter(in_subgraph_sampler)
mn = next(it)
assert torch.equal(mn.seeds["N0"], torch.LongTensor([1, 0]).to(F.ctx()))
expected_sampled_csc = {
"N0:R0:N0": gb.CSCFormatBase(
indptr=torch.LongTensor([0, 1, 3]),
indices=torch.LongTensor([2, 1, 0]),
),
"N0:R1:N1": gb.CSCFormatBase(
indptr=torch.LongTensor([0]), indices=torch.LongTensor([])
),
"N1:R2:N0": gb.CSCFormatBase(
indptr=torch.LongTensor([0, 1, 2]), indices=torch.LongTensor([0, 1])
),
"N1:R3:N1": gb.CSCFormatBase(
indptr=torch.LongTensor([0]), indices=torch.LongTensor([])
),
}
for etype, pairs in mn.sampled_subgraphs[0].sampled_csc.items():
assert torch.equal(
pairs.indices, expected_sampled_csc[etype].indices.to(F.ctx())
)
assert torch.equal(
pairs.indptr, expected_sampled_csc[etype].indptr.to(F.ctx())
)
mn = next(it)
assert mn.seeds == {
"N0": torch.LongTensor([2]).to(F.ctx()),
"N1": torch.LongTensor([0]).to(F.ctx()),
}
expected_sampled_csc = {
"N0:R0:N0": gb.CSCFormatBase(
indptr=torch.LongTensor([0, 1]), indices=torch.LongTensor([1])
),
"N0:R1:N1": gb.CSCFormatBase(
indptr=torch.LongTensor([0, 2]), indices=torch.LongTensor([2, 0])
),
"N1:R2:N0": gb.CSCFormatBase(
indptr=torch.LongTensor([0, 1]), indices=torch.LongTensor([1])
),
"N1:R3:N1": gb.CSCFormatBase(
indptr=torch.LongTensor([0, 0]), indices=torch.LongTensor([])
),
}
for etype, pairs in mn.sampled_subgraphs[0].sampled_csc.items():
assert torch.equal(
pairs.indices, expected_sampled_csc[etype].indices.to(F.ctx())
)
assert torch.equal(
pairs.indptr, expected_sampled_csc[etype].indptr.to(F.ctx())
)
mn = next(it)
assert torch.equal(mn.seeds["N1"], torch.LongTensor([2, 1]).to(F.ctx()))
expected_sampled_csc = {
"N0:R0:N0": gb.CSCFormatBase(
indptr=torch.LongTensor([0]), indices=torch.LongTensor([])
),
"N0:R1:N1": gb.CSCFormatBase(
indptr=torch.LongTensor([0, 1, 2]), indices=torch.LongTensor([0, 1])
),
"N1:R2:N0": gb.CSCFormatBase(
indptr=torch.LongTensor([0]), indices=torch.LongTensor([])
),
"N1:R3:N1": gb.CSCFormatBase(
indptr=torch.LongTensor([0, 1, 3]),
indices=torch.LongTensor([1, 2, 0]),
),
}
if graph.csc_indptr.is_cuda and torch.cuda.get_device_capability()[0] < 7:
expected_sampled_csc["N0:R1:N1"] = gb.CSCFormatBase(
indptr=torch.LongTensor([0, 1, 2]), indices=torch.LongTensor([1, 0])
)
for etype, pairs in mn.sampled_subgraphs[0].sampled_csc.items():
assert torch.equal(
pairs.indices, expected_sampled_csc[etype].indices.to(F.ctx())
)
assert torch.equal(
pairs.indptr, expected_sampled_csc[etype].indptr.to(F.ctx())
)
@@ -0,0 +1,36 @@
import dgl.graphbolt as gb
import pytest
import torch
from dgl import AddSelfLoop
from dgl.data import AsNodePredDataset, CoraGraphDataset
def test_LegacyDataset_homo_node_pred():
cora = CoraGraphDataset(transform=AddSelfLoop())
dataset = gb.LegacyDataset(cora)
# Check tasks.
assert len(dataset.tasks) == 1
task = dataset.tasks[0]
assert task.train_set.names == ("seeds", "labels")
assert len(task.train_set) == 140
assert task.validation_set.names == ("seeds", "labels")
assert len(task.validation_set) == 500
assert task.test_set.names == ("seeds", "labels")
assert len(task.test_set) == 1000
assert task.metadata["num_classes"] == 7
num_nodes = 2708
assert dataset.graph.num_nodes == num_nodes
assert len(dataset.all_nodes_set) == num_nodes
assert dataset.feature.size("node", None, "feat") == torch.Size([1433])
assert (
dataset.feature.read(
"node", None, "feat", torch.tensor([num_nodes - 1])
).size(dim=0)
== 1
)
# Out of bound indexing results in segmentation fault instead of exception
# in CI. This may be related to docker env. Skip it for now.
# with pytest.raises(IndexError):
# dataset.feature.read("node", None, "feat", torch.Tensor([num_nodes]))
@@ -0,0 +1,257 @@
import re
import backend as F
import dgl.graphbolt as gb
import pytest
import torch
from .. import gb_test_utils
def test_NegativeSampler_invoke():
# Instantiate graph and required datapipes.
num_seeds = 30
item_set = gb.ItemSet(
torch.arange(0, 2 * num_seeds).reshape(-1, 2), names="seeds"
)
batch_size = 10
item_sampler = gb.ItemSampler(item_set, batch_size=batch_size).copy_to(
F.ctx()
)
negative_ratio = 2
# Invoke NegativeSampler via class constructor.
negative_sampler = gb.NegativeSampler(
item_sampler,
negative_ratio,
)
with pytest.raises(NotImplementedError):
next(iter(negative_sampler))
# Invoke NegativeSampler via functional form.
negative_sampler = item_sampler.sample_negative(
negative_ratio,
)
with pytest.raises(NotImplementedError):
next(iter(negative_sampler))
def test_UniformNegativeSampler_invoke():
# Instantiate graph and required datapipes.
graph = gb_test_utils.rand_csc_graph(100, 0.05, bidirection_edge=True).to(
F.ctx()
)
num_seeds = 30
item_set = gb.ItemSet(
torch.arange(0, 2 * num_seeds).reshape(-1, 2), names="seeds"
)
batch_size = 10
item_sampler = gb.ItemSampler(item_set, batch_size=batch_size).copy_to(
F.ctx()
)
negative_ratio = 2
def _verify(negative_sampler):
for data in negative_sampler:
# Assertation
seeds_len = batch_size + batch_size * negative_ratio
assert data.seeds.size(0) == seeds_len
assert data.labels.size(0) == seeds_len
assert data.indexes.size(0) == seeds_len
# Invoke UniformNegativeSampler via class constructor.
negative_sampler = gb.UniformNegativeSampler(
item_sampler,
graph,
negative_ratio,
)
_verify(negative_sampler)
# Invoke UniformNegativeSampler via functional form.
negative_sampler = item_sampler.sample_uniform_negative(
graph,
negative_ratio,
)
_verify(negative_sampler)
@pytest.mark.parametrize("negative_ratio", [1, 5, 10, 20])
def test_Uniform_NegativeSampler(negative_ratio):
# Construct FusedCSCSamplingGraph.
graph = gb_test_utils.rand_csc_graph(100, 0.05, bidirection_edge=True).to(
F.ctx()
)
num_seeds = 30
item_set = gb.ItemSet(
torch.arange(0, num_seeds * 2).reshape(-1, 2), names="seeds"
)
batch_size = 10
item_sampler = gb.ItemSampler(item_set, batch_size=batch_size).copy_to(
F.ctx()
)
# Construct NegativeSampler.
negative_sampler = gb.UniformNegativeSampler(
item_sampler,
graph,
negative_ratio,
)
# Perform Negative sampling.
for data in negative_sampler:
seeds_len = batch_size + batch_size * negative_ratio
# Assertation
assert data.seeds.size(0) == seeds_len
assert data.labels.size(0) == seeds_len
assert data.indexes.size(0) == seeds_len
# Check negative seeds value.
pos_src = data.seeds[:batch_size, 0]
neg_src = data.seeds[batch_size:, 0]
assert torch.equal(pos_src.repeat_interleave(negative_ratio), neg_src)
# Check labels.
assert torch.equal(
data.labels[:batch_size], torch.ones(batch_size).to(F.ctx())
)
assert torch.equal(
data.labels[batch_size:],
torch.zeros(batch_size * negative_ratio).to(F.ctx()),
)
# Check indexes.
pos_indexes = torch.arange(0, batch_size).to(F.ctx())
neg_indexes = pos_indexes.repeat_interleave(negative_ratio)
expected_indexes = torch.cat((pos_indexes, neg_indexes))
assert torch.equal(data.indexes, expected_indexes)
def test_Uniform_NegativeSampler_error_shape():
# 1. seeds with shape N*3.
# Construct FusedCSCSamplingGraph.
graph = gb_test_utils.rand_csc_graph(100, 0.05, bidirection_edge=True).to(
F.ctx()
)
num_seeds = 30
item_set = gb.ItemSet(
torch.arange(0, num_seeds * 3).reshape(-1, 3), names="seeds"
)
batch_size = 10
item_sampler = gb.ItemSampler(item_set, batch_size=batch_size).copy_to(
F.ctx()
)
negative_ratio = 2
# Construct NegativeSampler.
negative_sampler = gb.UniformNegativeSampler(
item_sampler,
graph,
negative_ratio,
)
with pytest.raises(
AssertionError,
match=re.escape(
"Only tensor with shape N*2 is "
+ "supported for negative sampling, but got torch.Size([10, 3])."
),
):
next(iter(negative_sampler))
# 2. seeds with shape N*2*1.
# Construct FusedCSCSamplingGraph.
item_set = gb.ItemSet(
torch.arange(0, num_seeds * 2).reshape(-1, 2, 1), names="seeds"
)
item_sampler = gb.ItemSampler(item_set, batch_size=batch_size).copy_to(
F.ctx()
)
# Construct NegativeSampler.
negative_sampler = gb.UniformNegativeSampler(
item_sampler,
graph,
negative_ratio,
)
with pytest.raises(
AssertionError,
match=re.escape(
"Only tensor with shape N*2 is "
+ "supported for negative sampling, but got torch.Size([10, 2, 1])."
),
):
next(iter(negative_sampler))
# 3. seeds with shape N.
# Construct FusedCSCSamplingGraph.
item_set = gb.ItemSet(torch.arange(0, num_seeds), names="seeds")
item_sampler = gb.ItemSampler(item_set, batch_size=batch_size).copy_to(
F.ctx()
)
# Construct NegativeSampler.
negative_sampler = gb.UniformNegativeSampler(
item_sampler,
graph,
negative_ratio,
)
with pytest.raises(
AssertionError,
match=re.escape(
"Only tensor with shape N*2 is "
+ "supported for negative sampling, but got torch.Size([10])."
),
):
next(iter(negative_sampler))
def get_hetero_graph():
# COO graph:
# [0, 0, 1, 1, 2, 2, 3, 3, 4, 4]
# [2, 4, 2, 3, 0, 1, 1, 0, 0, 1]
# [1, 1, 1, 1, 0, 0, 0, 0, 0] - > edge type.
# num_nodes = 5, num_n1 = 2, num_n2 = 3
ntypes = {"n1": 0, "n2": 1}
etypes = {"n1:e1:n2": 0, "n2:e2:n1": 1}
indptr = torch.LongTensor([0, 2, 4, 6, 8, 10])
indices = torch.LongTensor([2, 4, 2, 3, 0, 1, 1, 0, 0, 1])
type_per_edge = torch.LongTensor([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
node_type_offset = torch.LongTensor([0, 2, 5])
return gb.fused_csc_sampling_graph(
indptr,
indices,
node_type_offset=node_type_offset,
type_per_edge=type_per_edge,
node_type_to_id=ntypes,
edge_type_to_id=etypes,
)
def test_NegativeSampler_Hetero_Data():
graph = get_hetero_graph().to(F.ctx())
itemset = gb.HeteroItemSet(
{
"n1:e1:n2": gb.ItemSet(
torch.LongTensor([[0, 0, 1, 1], [0, 2, 0, 1]]).T,
names="seeds",
),
"n2:e2:n1": gb.ItemSet(
torch.LongTensor([[0, 0, 1, 1, 2, 2], [0, 1, 1, 0, 0, 1]]).T,
names="seeds",
),
}
)
batch_size = 2
negative_ratio = 1
item_sampler = gb.ItemSampler(itemset, batch_size=batch_size).copy_to(
F.ctx()
)
negative_dp = gb.UniformNegativeSampler(item_sampler, graph, negative_ratio)
assert len(list(negative_dp)) == 5
# Perform negative sampling.
expected_neg_src = [
{"n1:e1:n2": torch.tensor([0, 0])},
{"n1:e1:n2": torch.tensor([1, 1])},
{"n2:e2:n1": torch.tensor([0, 0])},
{"n2:e2:n1": torch.tensor([1, 1])},
{"n2:e2:n1": torch.tensor([2, 2])},
]
for i, data in enumerate(negative_dp):
# Check negative seeds value.
for etype, seeds_data in data.seeds.items():
neg_src = seeds_data[batch_size:, 0]
neg_dst = seeds_data[batch_size:, 1]
assert torch.equal(expected_neg_src[i][etype].to(F.ctx()), neg_src)
assert (neg_dst < 3).all(), neg_dst
@@ -0,0 +1,161 @@
import unittest
from functools import partial
import backend as F
import dgl.graphbolt as gb
import pytest
import torch
def get_hetero_graph(include_original_edge_ids):
# COO graph:
# [0, 0, 1, 1, 2, 2, 3, 3, 4, 4]
# [2, 4, 2, 3, 0, 1, 1, 0, 0, 1]
# [1, 1, 1, 1, 0, 0, 0, 0, 0] - > edge type.
# num_nodes = 5, num_n1 = 2, num_n2 = 3
ntypes = {"n1": 0, "n2": 1, "n3": 2}
etypes = {"n2:e1:n3": 0, "n3:e2:n2": 1}
indptr = torch.LongTensor([0, 0, 2, 4, 6, 8, 10])
indices = torch.LongTensor([3, 5, 3, 4, 1, 2, 2, 1, 1, 2])
type_per_edge = torch.LongTensor([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
edge_attributes = {
"weight": torch.FloatTensor(
[2.5, 0, 8.4, 0, 0.4, 1.2, 2.5, 0, 8.4, 0.5]
),
"mask": torch.BoolTensor([1, 0, 1, 0, 1, 1, 1, 0, 1, 1]),
}
if include_original_edge_ids:
edge_attributes[gb.ORIGINAL_EDGE_ID] = (
torch.arange(indices.size(0), 0, -1) - 1
)
node_type_offset = torch.LongTensor([0, 1, 3, 6])
return gb.fused_csc_sampling_graph(
indptr,
indices,
node_type_offset=node_type_offset,
type_per_edge=type_per_edge,
node_type_to_id=ntypes,
edge_type_to_id=etypes,
edge_attributes=edge_attributes,
)
@unittest.skipIf(F._default_context_str != "gpu", reason="Enabled only on GPU.")
@pytest.mark.parametrize("hetero", [False, True])
@pytest.mark.parametrize("prob_name", [None, "weight", "mask"])
@pytest.mark.parametrize("sorted", [False, True])
@pytest.mark.parametrize("num_cached_edges", [0, 10])
@pytest.mark.parametrize("is_pinned", [False, True])
@pytest.mark.parametrize("has_orig_edge_ids", [False, True])
def test_NeighborSampler_GraphFetch(
hetero, prob_name, sorted, num_cached_edges, is_pinned, has_orig_edge_ids
):
if sorted:
items = torch.arange(3)
else:
items = torch.tensor([2, 0, 1])
names = "seeds"
itemset = gb.ItemSet(items, names=names)
graph = get_hetero_graph(has_orig_edge_ids)
graph = graph.pin_memory_() if is_pinned else graph.to(F.ctx())
if hetero:
itemset = gb.HeteroItemSet({"n3": itemset})
else:
graph.type_per_edge = None
item_sampler = gb.ItemSampler(itemset, batch_size=2).copy_to(F.ctx())
fanout = torch.LongTensor([2])
preprocess_fn = partial(
gb.SubgraphSampler._preprocess, cooperative=False, async_op=False
)
datapipe = item_sampler.map(preprocess_fn)
datapipe = datapipe.map(
partial(gb.NeighborSampler._prepare, graph.node_type_to_id)
)
sample_per_layer = gb.SamplePerLayer(
datapipe, graph.sample_neighbors, fanout, False, prob_name, False
)
compact_per_layer = sample_per_layer.compact_per_layer(True)
gb.seed(123)
expected_results = list(compact_per_layer)
if num_cached_edges > 0:
graph._initialize_gpu_graph_cache(num_cached_edges, 1, prob_name)
datapipe = datapipe.sample_per_layer(
graph.sample_neighbors, fanout, False, prob_name, True
)
datapipe = datapipe.compact_per_layer(True)
gb.seed(123)
new_results = list(datapipe)
assert len(expected_results) == len(new_results)
for a, b in zip(expected_results, new_results):
assert repr(a) == repr(b)
def remove_input_nodes(minibatch):
minibatch.input_nodes = None
return minibatch
datapipe = item_sampler.sample_neighbor(
graph, [fanout], False, prob_name=prob_name, overlap_fetch=True
)
datapipe = datapipe.transform(remove_input_nodes)
dataloader = gb.DataLoader(datapipe)
gb.seed(123)
new_results = list(dataloader)
assert len(expected_results) == len(new_results)
for a, b in zip(expected_results, new_results):
assert repr(a) == repr(b)
@pytest.mark.parametrize("layer_dependency", [False, True])
@pytest.mark.parametrize("overlap_graph_fetch", [False, True])
def test_labor_dependent_minibatching(layer_dependency, overlap_graph_fetch):
if F._default_context_str != "gpu" and overlap_graph_fetch:
pytest.skip("overlap_graph_fetch is only available for GPU.")
num_edges = 200
csc_indptr = torch.cat(
(
torch.zeros(1, dtype=torch.int64),
torch.ones(num_edges + 1, dtype=torch.int64) * num_edges,
)
)
indices = torch.arange(1, num_edges + 1)
graph = gb.fused_csc_sampling_graph(
csc_indptr.int(),
indices.int(),
).to(F.ctx())
torch.random.set_rng_state(torch.manual_seed(123).get_state())
batch_dependency = 100
itemset = gb.ItemSet(torch.zeros(batch_dependency + 1).int(), names="seeds")
datapipe = gb.ItemSampler(itemset, batch_size=1).copy_to(F.ctx())
fanouts = [5, 5]
datapipe = datapipe.sample_layer_neighbor(
graph,
fanouts,
overlap_fetch=overlap_graph_fetch,
layer_dependency=layer_dependency,
batch_dependency=batch_dependency,
)
dataloader = gb.DataLoader(datapipe)
res = list(dataloader)
assert len(res) == batch_dependency + 1
if layer_dependency:
assert torch.equal(
res[0].input_nodes,
res[0].sampled_subgraphs[1].original_row_node_ids,
)
else:
assert res[0].input_nodes.size(0) > res[0].sampled_subgraphs[
1
].original_row_node_ids.size(0)
delta = 0
for i in range(batch_dependency):
res_current = (
res[i].sampled_subgraphs[-1].original_row_node_ids.tolist()
)
res_next = (
res[i + 1].sampled_subgraphs[-1].original_row_node_ids.tolist()
)
intersect_len = len(set(res_current).intersection(set(res_next)))
assert intersect_len >= fanouts[-1]
delta += 1 + fanouts[-1] - intersect_len
assert delta >= fanouts[-1]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,735 @@
import unittest
import backend as F
import dgl
import dgl.graphbolt as gb
import pytest
import torch
from dgl.graphbolt.impl.sampled_subgraph_impl import SampledSubgraphImpl
def _assert_container_equal(lhs, rhs):
if isinstance(lhs, torch.Tensor):
assert isinstance(rhs, torch.Tensor)
assert torch.equal(lhs, rhs)
elif isinstance(lhs, tuple):
assert isinstance(rhs, tuple)
assert len(lhs) == len(rhs)
for l, r in zip(lhs, rhs):
_assert_container_equal(l, r)
elif isinstance(lhs, gb.CSCFormatBase):
assert isinstance(rhs, gb.CSCFormatBase)
assert len(lhs.indptr) == len(rhs.indptr)
assert len(lhs.indices) == len(rhs.indices)
_assert_container_equal(lhs.indptr, rhs.indptr)
_assert_container_equal(lhs.indices, rhs.indices)
elif isinstance(lhs, dict):
assert isinstance(rhs, dict)
assert len(lhs) == len(rhs)
for key, value in lhs.items():
assert key in rhs
_assert_container_equal(value, rhs[key])
@pytest.mark.parametrize("reverse_row", [True, False])
@pytest.mark.parametrize("reverse_column", [True, False])
def test_exclude_edges_homo_deduplicated(reverse_row, reverse_column):
csc_formats = gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 1, 2, 2, 3]), indices=torch.tensor([0, 3, 2])
)
if reverse_row:
original_row_node_ids = torch.tensor([10, 15, 11, 24, 9])
src_to_exclude = torch.tensor([11])
else:
original_row_node_ids = None
src_to_exclude = torch.tensor([2])
if reverse_column:
original_column_node_ids = torch.tensor([10, 15, 11, 24, 9])
dst_to_exclude = torch.tensor([9])
else:
original_column_node_ids = None
dst_to_exclude = torch.tensor([4])
original_edge_ids = torch.Tensor([5, 9, 10])
subgraph = SampledSubgraphImpl(
csc_formats,
original_column_node_ids,
original_row_node_ids,
original_edge_ids,
)
edges_to_exclude = torch.cat((src_to_exclude, dst_to_exclude)).view(2, -1).T
result = subgraph.exclude_edges(edges_to_exclude)
expected_csc_formats = gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 1, 2, 2, 2]), indices=torch.tensor([0, 3])
)
if reverse_row:
expected_row_node_ids = torch.tensor([10, 15, 11, 24, 9])
else:
expected_row_node_ids = None
if reverse_column:
expected_column_node_ids = torch.tensor([10, 15, 11, 24, 9])
else:
expected_column_node_ids = None
expected_edge_ids = torch.Tensor([5, 9])
_assert_container_equal(result.sampled_csc, expected_csc_formats)
_assert_container_equal(
result.original_column_node_ids, expected_column_node_ids
)
_assert_container_equal(result.original_row_node_ids, expected_row_node_ids)
_assert_container_equal(result.original_edge_ids, expected_edge_ids)
@pytest.mark.parametrize("reverse_row", [True, False])
@pytest.mark.parametrize("reverse_column", [True, False])
def test_exclude_edges_homo_duplicated(reverse_row, reverse_column):
csc_formats = gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 1, 3, 3, 5]),
indices=torch.tensor([0, 3, 3, 2, 2]),
)
if reverse_row:
original_row_node_ids = torch.tensor([10, 15, 11, 24, 9])
src_to_exclude = torch.tensor([24])
else:
original_row_node_ids = None
src_to_exclude = torch.tensor([3])
if reverse_column:
original_column_node_ids = torch.tensor([10, 15, 11, 24, 9])
dst_to_exclude = torch.tensor([11])
else:
original_column_node_ids = None
dst_to_exclude = torch.tensor([2])
original_edge_ids = torch.Tensor([5, 9, 9, 10, 10])
subgraph = SampledSubgraphImpl(
csc_formats,
original_column_node_ids,
original_row_node_ids,
original_edge_ids,
)
edges_to_exclude = torch.cat((src_to_exclude, dst_to_exclude)).view(2, -1).T
result = subgraph.exclude_edges(edges_to_exclude)
expected_csc_formats = gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 1, 1, 1, 3]), indices=torch.tensor([0, 2, 2])
)
if reverse_row:
expected_row_node_ids = torch.tensor([10, 15, 11, 24, 9])
else:
expected_row_node_ids = None
if reverse_column:
expected_column_node_ids = torch.tensor([10, 15, 11, 24, 9])
else:
expected_column_node_ids = None
expected_edge_ids = torch.Tensor([5, 10, 10])
_assert_container_equal(result.sampled_csc, expected_csc_formats)
_assert_container_equal(
result.original_column_node_ids, expected_column_node_ids
)
_assert_container_equal(result.original_row_node_ids, expected_row_node_ids)
_assert_container_equal(result.original_edge_ids, expected_edge_ids)
@pytest.mark.parametrize("reverse_row", [True, False])
@pytest.mark.parametrize("reverse_column", [True, False])
def test_exclude_edges_hetero_deduplicated(reverse_row, reverse_column):
csc_formats = {
"A:relation:B": gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 2, 3]),
indices=torch.tensor([2, 1, 0]),
)
}
if reverse_row:
original_row_node_ids = {
"A": torch.tensor([13, 14, 15]),
}
src_to_exclude = torch.tensor([15, 13])
else:
original_row_node_ids = None
src_to_exclude = torch.tensor([2, 0])
if reverse_column:
original_column_node_ids = {
"B": torch.tensor([10, 11, 12]),
}
dst_to_exclude = torch.tensor([10, 12])
else:
original_column_node_ids = None
dst_to_exclude = torch.tensor([0, 2])
original_edge_ids = {"A:relation:B": torch.tensor([19, 20, 21])}
subgraph = SampledSubgraphImpl(
sampled_csc=csc_formats,
original_column_node_ids=original_column_node_ids,
original_row_node_ids=original_row_node_ids,
original_edge_ids=original_edge_ids,
)
edges_to_exclude = {
"A:relation:B": torch.cat(
(
src_to_exclude,
dst_to_exclude,
)
)
.view(2, -1)
.T
}
result = subgraph.exclude_edges(edges_to_exclude)
expected_csc_formats = {
"A:relation:B": gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 1, 1]),
indices=torch.tensor([1]),
)
}
if reverse_row:
expected_row_node_ids = {
"A": torch.tensor([13, 14, 15]),
}
else:
expected_row_node_ids = None
if reverse_column:
expected_column_node_ids = {
"B": torch.tensor([10, 11, 12]),
}
else:
expected_column_node_ids = None
expected_edge_ids = {"A:relation:B": torch.tensor([20])}
_assert_container_equal(result.sampled_csc, expected_csc_formats)
_assert_container_equal(
result.original_column_node_ids, expected_column_node_ids
)
_assert_container_equal(result.original_row_node_ids, expected_row_node_ids)
_assert_container_equal(result.original_edge_ids, expected_edge_ids)
@pytest.mark.parametrize("reverse_row", [True, False])
@pytest.mark.parametrize("reverse_column", [True, False])
def test_exclude_edges_hetero_duplicated(reverse_row, reverse_column):
csc_formats = {
"A:relation:B": gb.CSCFormatBase(
indptr=torch.tensor([0, 2, 4, 5]),
indices=torch.tensor([2, 2, 1, 1, 0]),
)
}
if reverse_row:
original_row_node_ids = {
"A": torch.tensor([13, 14, 15]),
}
src_to_exclude = torch.tensor([15, 13])
else:
original_row_node_ids = None
src_to_exclude = torch.tensor([2, 0])
if reverse_column:
original_column_node_ids = {
"B": torch.tensor([10, 11, 12]),
}
dst_to_exclude = torch.tensor([10, 12])
else:
original_column_node_ids = None
dst_to_exclude = torch.tensor([0, 2])
original_edge_ids = {"A:relation:B": torch.tensor([19, 19, 20, 20, 21])}
subgraph = SampledSubgraphImpl(
sampled_csc=csc_formats,
original_column_node_ids=original_column_node_ids,
original_row_node_ids=original_row_node_ids,
original_edge_ids=original_edge_ids,
)
edges_to_exclude = {
"A:relation:B": torch.cat(
(
src_to_exclude,
dst_to_exclude,
)
)
.view(2, -1)
.T
}
result = subgraph.exclude_edges(edges_to_exclude)
expected_csc_formats = {
"A:relation:B": gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 2, 2]),
indices=torch.tensor([1, 1]),
)
}
if reverse_row:
expected_row_node_ids = {
"A": torch.tensor([13, 14, 15]),
}
else:
expected_row_node_ids = None
if reverse_column:
expected_column_node_ids = {
"B": torch.tensor([10, 11, 12]),
}
else:
expected_column_node_ids = None
expected_edge_ids = {"A:relation:B": torch.tensor([20, 20])}
_assert_container_equal(result.sampled_csc, expected_csc_formats)
_assert_container_equal(
result.original_column_node_ids, expected_column_node_ids
)
_assert_container_equal(result.original_row_node_ids, expected_row_node_ids)
_assert_container_equal(result.original_edge_ids, expected_edge_ids)
@pytest.mark.parametrize("reverse_row", [True, False])
@pytest.mark.parametrize("reverse_column", [True, False])
def test_exclude_edges_homo_deduplicated_tensor(reverse_row, reverse_column):
csc_formats = gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 1, 2, 2, 3]), indices=torch.tensor([0, 3, 2])
)
if reverse_row:
original_row_node_ids = torch.tensor([10, 15, 11, 24, 9])
src_to_exclude = torch.tensor([11])
else:
original_row_node_ids = None
src_to_exclude = torch.tensor([2])
if reverse_column:
original_column_node_ids = torch.tensor([10, 15, 11, 24, 9])
dst_to_exclude = torch.tensor([9])
else:
original_column_node_ids = None
dst_to_exclude = torch.tensor([4])
original_edge_ids = torch.Tensor([5, 9, 10])
subgraph = SampledSubgraphImpl(
csc_formats,
original_column_node_ids,
original_row_node_ids,
original_edge_ids,
)
edges_to_exclude = torch.cat((src_to_exclude, dst_to_exclude)).view(1, -1)
result = subgraph.exclude_edges(edges_to_exclude)
expected_csc_formats = gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 1, 2, 2, 2]), indices=torch.tensor([0, 3])
)
if reverse_row:
expected_row_node_ids = torch.tensor([10, 15, 11, 24, 9])
else:
expected_row_node_ids = None
if reverse_column:
expected_column_node_ids = torch.tensor([10, 15, 11, 24, 9])
else:
expected_column_node_ids = None
expected_edge_ids = torch.Tensor([5, 9])
_assert_container_equal(result.sampled_csc, expected_csc_formats)
_assert_container_equal(
result.original_column_node_ids, expected_column_node_ids
)
_assert_container_equal(result.original_row_node_ids, expected_row_node_ids)
_assert_container_equal(result.original_edge_ids, expected_edge_ids)
@pytest.mark.parametrize("reverse_row", [True, False])
@pytest.mark.parametrize("reverse_column", [True, False])
def test_exclude_edges_homo_duplicated_tensor(reverse_row, reverse_column):
csc_formats = gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 1, 3, 3, 5]),
indices=torch.tensor([0, 3, 3, 2, 2]),
)
if reverse_row:
original_row_node_ids = torch.tensor([10, 15, 11, 24, 9])
src_to_exclude = torch.tensor([24])
else:
original_row_node_ids = None
src_to_exclude = torch.tensor([3])
if reverse_column:
original_column_node_ids = torch.tensor([10, 15, 11, 24, 9])
dst_to_exclude = torch.tensor([11])
else:
original_column_node_ids = None
dst_to_exclude = torch.tensor([2])
original_edge_ids = torch.Tensor([5, 9, 9, 10, 10])
subgraph = SampledSubgraphImpl(
csc_formats,
original_column_node_ids,
original_row_node_ids,
original_edge_ids,
)
edges_to_exclude = torch.cat((src_to_exclude, dst_to_exclude)).view(1, -1)
result = subgraph.exclude_edges(edges_to_exclude)
expected_csc_formats = gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 1, 1, 1, 3]), indices=torch.tensor([0, 2, 2])
)
if reverse_row:
expected_row_node_ids = torch.tensor([10, 15, 11, 24, 9])
else:
expected_row_node_ids = None
if reverse_column:
expected_column_node_ids = torch.tensor([10, 15, 11, 24, 9])
else:
expected_column_node_ids = None
expected_edge_ids = torch.Tensor([5, 10, 10])
_assert_container_equal(result.sampled_csc, expected_csc_formats)
_assert_container_equal(
result.original_column_node_ids, expected_column_node_ids
)
_assert_container_equal(result.original_row_node_ids, expected_row_node_ids)
_assert_container_equal(result.original_edge_ids, expected_edge_ids)
@pytest.mark.parametrize("reverse_row", [True, False])
@pytest.mark.parametrize("reverse_column", [True, False])
def test_exclude_edges_hetero_deduplicated_tensor(reverse_row, reverse_column):
csc_formats = {
"A:relation:B": gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 2, 3]),
indices=torch.tensor([2, 1, 0]),
)
}
if reverse_row:
original_row_node_ids = {
"A": torch.tensor([13, 14, 15]),
}
src_to_exclude = torch.tensor([15, 13])
else:
original_row_node_ids = None
src_to_exclude = torch.tensor([2, 0])
if reverse_column:
original_column_node_ids = {
"B": torch.tensor([10, 11, 12]),
}
dst_to_exclude = torch.tensor([10, 12])
else:
original_column_node_ids = None
dst_to_exclude = torch.tensor([0, 2])
original_edge_ids = {"A:relation:B": torch.tensor([19, 20, 21])}
subgraph = SampledSubgraphImpl(
sampled_csc=csc_formats,
original_column_node_ids=original_column_node_ids,
original_row_node_ids=original_row_node_ids,
original_edge_ids=original_edge_ids,
)
edges_to_exclude = {
"A:relation:B": torch.cat((src_to_exclude, dst_to_exclude))
.view(2, -1)
.T
}
result = subgraph.exclude_edges(edges_to_exclude)
expected_csc_formats = {
"A:relation:B": gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 1, 1]),
indices=torch.tensor([1]),
)
}
if reverse_row:
expected_row_node_ids = {
"A": torch.tensor([13, 14, 15]),
}
else:
expected_row_node_ids = None
if reverse_column:
expected_column_node_ids = {
"B": torch.tensor([10, 11, 12]),
}
else:
expected_column_node_ids = None
expected_edge_ids = {"A:relation:B": torch.tensor([20])}
_assert_container_equal(result.sampled_csc, expected_csc_formats)
_assert_container_equal(
result.original_column_node_ids, expected_column_node_ids
)
_assert_container_equal(result.original_row_node_ids, expected_row_node_ids)
_assert_container_equal(result.original_edge_ids, expected_edge_ids)
@pytest.mark.parametrize("reverse_row", [True, False])
@pytest.mark.parametrize("reverse_column", [True, False])
def test_exclude_edges_hetero_duplicated_tensor(reverse_row, reverse_column):
csc_formats = {
"A:relation:B": gb.CSCFormatBase(
indptr=torch.tensor([0, 2, 4, 5]),
indices=torch.tensor([2, 2, 1, 1, 0]),
)
}
if reverse_row:
original_row_node_ids = {
"A": torch.tensor([13, 14, 15]),
}
src_to_exclude = torch.tensor([15, 13])
else:
original_row_node_ids = None
src_to_exclude = torch.tensor([2, 0])
if reverse_column:
original_column_node_ids = {
"B": torch.tensor([10, 11, 12]),
}
dst_to_exclude = torch.tensor([10, 12])
else:
original_column_node_ids = None
dst_to_exclude = torch.tensor([0, 2])
original_edge_ids = {"A:relation:B": torch.tensor([19, 19, 20, 20, 21])}
subgraph = SampledSubgraphImpl(
sampled_csc=csc_formats,
original_column_node_ids=original_column_node_ids,
original_row_node_ids=original_row_node_ids,
original_edge_ids=original_edge_ids,
)
edges_to_exclude = {
"A:relation:B": torch.cat((src_to_exclude, dst_to_exclude))
.view(2, -1)
.T
}
result = subgraph.exclude_edges(edges_to_exclude)
expected_csc_formats = {
"A:relation:B": gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 2, 2]),
indices=torch.tensor([1, 1]),
)
}
if reverse_row:
expected_row_node_ids = {
"A": torch.tensor([13, 14, 15]),
}
else:
expected_row_node_ids = None
if reverse_column:
expected_column_node_ids = {
"B": torch.tensor([10, 11, 12]),
}
else:
expected_column_node_ids = None
expected_edge_ids = {"A:relation:B": torch.tensor([20, 20])}
_assert_container_equal(result.sampled_csc, expected_csc_formats)
_assert_container_equal(
result.original_column_node_ids, expected_column_node_ids
)
_assert_container_equal(result.original_row_node_ids, expected_row_node_ids)
_assert_container_equal(result.original_edge_ids, expected_edge_ids)
def test_to_pyg_homo():
graph = dgl.graph(([5, 0, 7, 7, 2, 4], [0, 1, 2, 2, 3, 4]))
graph = gb.from_dglgraph(graph, is_homogeneous=True).to(F.ctx())
items = torch.LongTensor([[0, 3], [4, 4]])
names = "seeds"
itemset = gb.ItemSet(items, names=names)
datapipe = gb.ItemSampler(itemset, batch_size=4).copy_to(F.ctx())
num_layer = 2
fanouts = [torch.LongTensor([-1]) for _ in range(num_layer)]
sampler = gb.NeighborSampler
datapipe = sampler(
datapipe,
graph,
fanouts,
deduplicate=True,
)
for minibatch in datapipe:
x = torch.randn((minibatch.node_ids().size(0), 2), dtype=torch.float32)
for subgraph in minibatch.sampled_subgraphs:
(x_src, x_dst), edge_index, sizes = subgraph.to_pyg(x)
assert torch.equal(x_src, x)
dst_size = subgraph.original_column_node_ids.size(0)
assert torch.equal(x_dst, x[:dst_size])
src_size = subgraph.original_row_node_ids.size(0)
assert dst_size == sizes[1]
assert src_size == sizes[0]
assert torch.equal(edge_index[0], subgraph.sampled_csc.indices)
assert torch.equal(
edge_index[1],
gb.expand_indptr(
subgraph.sampled_csc.indptr,
subgraph.sampled_csc.indices.dtype,
),
)
x = x_dst
def test_to_pyg_hetero():
# COO graph:
# [0, 0, 1, 1, 2, 2, 3, 3, 4, 4]
# [2, 4, 2, 3, 0, 1, 1, 0, 0, 1]
# [1, 1, 1, 1, 0, 0, 0, 0, 0] - > edge type.
# num_nodes = 5, num_n1 = 2, num_n2 = 3
ntypes = {"n1": 0, "n2": 1}
etypes = {"n1:e1:n2": 0, "n2:e2:n1": 1}
indptr = torch.LongTensor([0, 2, 4, 6, 8, 10])
indices = torch.LongTensor([2, 4, 2, 3, 0, 1, 1, 0, 0, 1])
type_per_edge = torch.LongTensor([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
node_type_offset = torch.LongTensor([0, 2, 5])
graph = gb.fused_csc_sampling_graph(
indptr,
indices,
node_type_offset=node_type_offset,
type_per_edge=type_per_edge,
node_type_to_id=ntypes,
edge_type_to_id=etypes,
).to(F.ctx())
itemset = gb.HeteroItemSet(
{"n1:e1:n2": gb.ItemSet(torch.tensor([[0, 1]]), names="seeds")}
)
item_sampler = gb.ItemSampler(itemset, batch_size=2).copy_to(F.ctx())
num_layer = 2
fanouts = [torch.LongTensor([2]) for _ in range(num_layer)]
Sampler = gb.NeighborSampler
datapipe = Sampler(
item_sampler,
graph,
fanouts,
deduplicate=True,
)
for minibatch in datapipe:
x = {}
for key, ids in minibatch.node_ids().items():
x[key] = torch.randn((ids.size(0), 2), dtype=torch.float32)
for subgraph in minibatch.sampled_subgraphs:
(x_src, x_dst), edge_index, sizes = subgraph.to_pyg(x)
assert x_src == x
for ntype in x:
dst_size = subgraph.original_column_node_ids[ntype].size(0)
assert torch.equal(x_dst[ntype], x[ntype][:dst_size])
for etype in subgraph.sampled_csc:
src_ntype, _, dst_ntype = gb.etype_str_to_tuple(etype)
src_size = subgraph.original_row_node_ids[src_ntype].size(0)
dst_size = subgraph.original_column_node_ids[dst_ntype].size(0)
assert dst_size == sizes[etype][1]
assert src_size == sizes[etype][0]
assert torch.equal(
edge_index[etype][0], subgraph.sampled_csc[etype].indices
)
assert torch.equal(
edge_index[etype][1],
gb.expand_indptr(
subgraph.sampled_csc[etype].indptr,
subgraph.sampled_csc[etype].indices.dtype,
),
)
x = x_dst
@unittest.skipIf(
F._default_context_str == "cpu",
reason="`to` function needs GPU to test.",
)
def test_sampled_subgraph_to_device():
# Initialize data.
csc_format = {
"A:relation:B": gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 2, 3]),
indices=torch.tensor([0, 1, 2]),
)
}
original_row_node_ids = {
"A": torch.tensor([13, 14, 15]),
}
src_to_exclude = torch.tensor([15, 13])
original_column_node_ids = {
"B": torch.tensor([10, 11, 12]),
}
dst_to_exclude = torch.tensor([10, 12])
original_edge_ids = {"A:relation:B": torch.tensor([19, 20, 21])}
subgraph = SampledSubgraphImpl(
sampled_csc=csc_format,
original_column_node_ids=original_column_node_ids,
original_row_node_ids=original_row_node_ids,
original_edge_ids=original_edge_ids,
)
edges_to_exclude = {
"A:relation:B": torch.cat(
(
src_to_exclude,
dst_to_exclude,
)
)
.view(2, -1)
.T
}
graph = subgraph.exclude_edges(edges_to_exclude)
# Copy to device.
graph = graph.to("cuda")
# Check.
for key in graph.sampled_csc:
assert graph.sampled_csc[key].indices.device.type == "cuda"
assert graph.sampled_csc[key].indptr.device.type == "cuda"
for key in graph.original_column_node_ids:
assert graph.original_column_node_ids[key].device.type == "cuda"
for key in graph.original_row_node_ids:
assert graph.original_row_node_ids[key].device.type == "cuda"
for key in graph.original_edge_ids:
assert graph.original_edge_ids[key].device.type == "cuda"
def test_sampled_subgraph_impl_representation_homo():
sampled_subgraph_impl = SampledSubgraphImpl(
sampled_csc=gb.CSCFormatBase(
indptr=torch.arange(0, 101, 10),
indices=torch.arange(10, 110),
),
original_column_node_ids=torch.arange(0, 10),
original_row_node_ids=torch.arange(0, 110),
original_edge_ids=None,
)
expected_result = str(
"""SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([ 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]),
indices=tensor([ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93,
94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107,
108, 109]),
),
original_row_node_ids=tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55,
56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69,
70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83,
84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109]),
original_edge_ids=None,
original_column_node_ids=tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
)"""
)
assert str(sampled_subgraph_impl) == expected_result, print(
sampled_subgraph_impl
)
def test_sampled_subgraph_impl_representation_hetero():
sampled_subgraph_impl = SampledSubgraphImpl(
sampled_csc={
"n1:e1:n2": gb.CSCFormatBase(
indptr=torch.tensor([0, 2, 4]),
indices=torch.tensor([4, 5, 6, 7]),
),
"n2:e2:n1": gb.CSCFormatBase(
indptr=torch.tensor([0, 2, 4, 6, 8]),
indices=torch.tensor([2, 3, 4, 5, 6, 7, 8, 9]),
),
},
original_column_node_ids={
"n1": torch.tensor([1, 0, 0, 1]),
"n2": torch.tensor([1, 2]),
},
original_row_node_ids={
"n1": torch.tensor([1, 0, 0, 1, 1, 0, 0, 1]),
"n2": torch.tensor([1, 2, 0, 1, 0, 2, 0, 2, 0, 1]),
},
original_edge_ids=None,
)
expected_result = str(
"""SampledSubgraphImpl(sampled_csc={'n1:e1:n2': CSCFormatBase(indptr=tensor([0, 2, 4]),
indices=tensor([4, 5, 6, 7]),
), 'n2:e2:n1': CSCFormatBase(indptr=tensor([0, 2, 4, 6, 8]),
indices=tensor([2, 3, 4, 5, 6, 7, 8, 9]),
)},
original_row_node_ids={'n1': tensor([1, 0, 0, 1, 1, 0, 0, 1]), 'n2': tensor([1, 2, 0, 1, 0, 2, 0, 2, 0, 1])},
original_edge_ids=None,
original_column_node_ids={'n1': tensor([1, 0, 0, 1]), 'n2': tensor([1, 2])},
)"""
)
assert str(sampled_subgraph_impl) == expected_result, print(
sampled_subgraph_impl
)
@@ -0,0 +1,458 @@
import os
import tempfile
import unittest
import backend as F
import numpy as np
import pydantic
import pytest
import torch
from dgl import graphbolt as gb
def to_on_disk_tensor(test_dir, name, t):
path = os.path.join(test_dir, name + ".npy")
t = t.numpy()
np.save(path, t)
# The Pytorch tensor is a view of the numpy array on disk, which does not
# consume memory.
t = torch.as_tensor(np.load(path, mmap_mode="r+"))
return t
@pytest.mark.parametrize("in_memory", [True, False])
def test_torch_based_feature(in_memory):
with tempfile.TemporaryDirectory() as test_dir:
a = torch.tensor([[1, 2, 3], [4, 5, 6]])
b = torch.tensor([[[1, 2], [3, 4]], [[4, 5], [6, 7]]])
metadata = {"max_value": 3}
if not in_memory:
a = to_on_disk_tensor(test_dir, "a", a)
b = to_on_disk_tensor(test_dir, "b", b)
feature_a = gb.TorchBasedFeature(a, metadata=metadata)
feature_b = gb.TorchBasedFeature(b)
# Read the entire feature.
assert torch.equal(
feature_a.read(), torch.tensor([[1, 2, 3], [4, 5, 6]])
)
# Test read the feature with ids.
assert torch.equal(
feature_b.read(), torch.tensor([[[1, 2], [3, 4]], [[4, 5], [6, 7]]])
)
# Read the feature with ids.
assert torch.equal(
feature_a.read(torch.tensor([0])),
torch.tensor([[1, 2, 3]]),
)
assert torch.equal(
feature_b.read(torch.tensor([1])),
torch.tensor([[[4, 5], [6, 7]]]),
)
# Update the feature with ids.
feature_a.update(torch.tensor([[0, 1, 2]]), torch.tensor([0]))
assert torch.equal(
feature_a.read(), torch.tensor([[0, 1, 2], [4, 5, 6]])
)
feature_b.update(torch.tensor([[[1, 2], [3, 4]]]), torch.tensor([1]))
assert torch.equal(
feature_b.read(), torch.tensor([[[1, 2], [3, 4]], [[1, 2], [3, 4]]])
)
# Test update the feature.
feature_a.update(torch.tensor([[5, 1, 3]]))
assert torch.equal(
feature_a.read(),
torch.tensor([[5, 1, 3]]),
), print(feature_a.read())
feature_b.update(
torch.tensor([[[1, 3], [5, 7]], [[2, 4], [6, 8]], [[2, 4], [6, 8]]])
)
assert torch.equal(
feature_b.read(),
torch.tensor(
[[[1, 3], [5, 7]], [[2, 4], [6, 8]], [[2, 4], [6, 8]]]
),
)
# Test get the size and count of the entire feature.
assert feature_a.size() == torch.Size([3])
assert feature_b.size() == torch.Size([2, 2])
assert feature_a.count() == 1
assert feature_b.count() == 3
# Test get metadata of the feature.
assert feature_a.metadata() == metadata
assert feature_b.metadata() == {}
with pytest.raises(IndexError):
feature_a.read(torch.tensor([0, 1, 2, 3]))
# For windows, the file is locked by the numpy.load. We need to delete
# it before closing the temporary directory.
a = b = None
feature_a = feature_b = None
# Test loaded tensors' contiguity from C/Fortran contiguous ndarray.
contiguous_numpy = np.array([[1, 2, 3], [4, 5, 6]], order="C")
non_contiguous_numpy = np.array([[1, 2, 3], [4, 5, 6]], order="F")
assert contiguous_numpy.flags["C_CONTIGUOUS"]
assert non_contiguous_numpy.flags["F_CONTIGUOUS"]
np.save(
os.path.join(test_dir, "contiguous_numpy.npy"), contiguous_numpy
)
np.save(
os.path.join(test_dir, "non_contiguous_numpy.npy"),
non_contiguous_numpy,
)
cur_mmap_mode = None
if not in_memory:
cur_mmap_mode = "r+"
feature_a = gb.TorchBasedFeature(
torch.from_numpy(
np.load(
os.path.join(test_dir, "contiguous_numpy.npy"),
mmap_mode=cur_mmap_mode,
)
)
)
feature_b = gb.TorchBasedFeature(
torch.from_numpy(
np.load(
os.path.join(test_dir, "non_contiguous_numpy.npy"),
mmap_mode=cur_mmap_mode,
)
)
)
assert feature_a._tensor.is_contiguous()
assert feature_b._tensor.is_contiguous()
contiguous_numpy = non_contiguous_numpy = None
feature_a = feature_b = None
def is_feature_store_on_cuda(store):
for feature in store._features.values():
assert feature._tensor.is_cuda
def is_feature_store_on_cpu(store):
for feature in store._features.values():
assert not feature._tensor.is_cuda
@unittest.skipIf(
F._default_context_str == "cpu",
reason="Tests for pinned memory are only meaningful on GPU.",
)
@pytest.mark.parametrize("device", ["pinned", "cuda"])
def test_feature_store_to_device(device):
with tempfile.TemporaryDirectory() as test_dir:
a = torch.tensor([[1, 2, 4], [2, 5, 3]])
b = torch.tensor([[[1, 2], [3, 4]], [[2, 5], [3, 4]]])
write_tensor_to_disk(test_dir, "a", a, fmt="torch")
write_tensor_to_disk(test_dir, "b", b, fmt="numpy")
feature_data = [
gb.OnDiskFeatureData(
domain="node",
type="paper",
name="a",
format="torch",
path=os.path.join(test_dir, "a.pt"),
),
gb.OnDiskFeatureData(
domain="edge",
type="paper:cites:paper",
name="b",
format="numpy",
path=os.path.join(test_dir, "b.npy"),
),
]
feature_store = gb.TorchBasedFeatureStore(feature_data)
feature_store2 = feature_store.to(device)
if device == "pinned":
assert feature_store2.is_pinned()
elif device == "cuda":
is_feature_store_on_cuda(feature_store2)
# The original variable should be untouched.
is_feature_store_on_cpu(feature_store)
@unittest.skipIf(
F._default_context_str == "cpu",
reason="Tests for pinned memory are only meaningful on GPU.",
)
@pytest.mark.parametrize(
"dtype",
[
torch.float32,
torch.float64,
torch.int32,
torch.int64,
torch.int8,
torch.float16,
torch.complex128,
],
)
@pytest.mark.parametrize("idtype", [torch.int32, torch.int64])
@pytest.mark.parametrize("shape", [(2, 1), (2, 3), (2, 2, 2), (137, 13, 3)])
@pytest.mark.parametrize("in_place", [False, True])
def test_torch_based_pinned_feature(dtype, idtype, shape, in_place):
if dtype == torch.complex128:
tensor = torch.complex(
torch.randint(0, 13, shape, dtype=torch.float64),
torch.randint(0, 13, shape, dtype=torch.float64),
)
else:
tensor = torch.randint(0, 13, shape, dtype=dtype)
test_tensor = tensor.clone().detach()
test_tensor_cuda = test_tensor.cuda()
feature = gb.TorchBasedFeature(tensor)
if in_place:
if gb.is_wsl():
pytest.skip("In place pinning is not supported on WSL.")
feature.pin_memory_()
# Check if pinning is truly in-place.
assert feature._tensor.data_ptr() == tensor.data_ptr()
else:
feature = feature.to("pinned")
assert feature.is_pinned()
# Test read entire pinned feature, the result should be on cuda.
assert torch.equal(feature.read(), test_tensor_cuda)
assert feature.read().is_cuda
assert torch.equal(
feature.read(torch.tensor([0], dtype=idtype).cuda()),
test_tensor_cuda[[0]],
)
# Test read pinned feature with idx on cuda, the result should be on cuda.
assert feature.read(torch.tensor([0], dtype=idtype).cuda()).is_cuda
# Test read pinned feature with idx on cpu, the result should be on cpu.
assert torch.equal(
feature.read(torch.tensor([0], dtype=idtype)), test_tensor[[0]]
)
assert not feature.read(torch.tensor([0], dtype=idtype)).is_cuda
def write_tensor_to_disk(dir, name, t, fmt="torch"):
if fmt == "torch":
torch.save(t, os.path.join(dir, name + ".pt"))
elif fmt == "numpy":
t = t.numpy()
np.save(os.path.join(dir, name + ".npy"), t)
else:
raise ValueError(f"Unsupported format: {fmt}")
@pytest.mark.parametrize("in_memory", [True, False])
def test_torch_based_feature_store(in_memory):
with tempfile.TemporaryDirectory() as test_dir:
a = torch.tensor([[1, 2, 4], [2, 5, 3]])
b = torch.tensor([[[1, 2], [3, 4]], [[2, 5], [3, 4]]])
write_tensor_to_disk(test_dir, "a", a, fmt="torch")
write_tensor_to_disk(test_dir, "b", b, fmt="numpy")
feature_data = [
gb.OnDiskFeatureData(
domain="node",
type="paper",
name="a",
format="torch",
path=os.path.join(test_dir, "a.pt"),
in_memory=True,
),
gb.OnDiskFeatureData(
domain="edge",
type="paper:cites:paper",
name="b",
format="numpy",
path=os.path.join(test_dir, "b.npy"),
in_memory=in_memory,
),
]
feature_store = gb.TorchBasedFeatureStore(feature_data)
assert isinstance(
feature_store[("node", "paper", "a")], gb.TorchBasedFeature
)
assert isinstance(
feature_store[("edge", "paper:cites:paper", "b")],
gb.TorchBasedFeature if in_memory else gb.DiskBasedFeature,
)
# Test read the entire feature.
assert torch.equal(
feature_store.read("node", "paper", "a"),
torch.tensor([[1, 2, 4], [2, 5, 3]]),
)
assert torch.equal(
feature_store.read("edge", "paper:cites:paper", "b"),
torch.tensor([[[1, 2], [3, 4]], [[2, 5], [3, 4]]]),
)
# Test get the size of the entire feature.
assert feature_store.size("node", "paper", "a") == torch.Size([3])
assert feature_store.size(
"edge", "paper:cites:paper", "b"
) == torch.Size([2, 2])
# Test get the keys of the features.
assert feature_store.keys() == [
("node", "paper", "a"),
("edge", "paper:cites:paper", "b"),
]
# For windows, the file is locked by the numpy.load. We need to delete
# it before closing the temporary directory.
a = b = None
feature_store = None
# ``domain`` should be enum.
with pytest.raises(pydantic.ValidationError):
_ = gb.OnDiskFeatureData(
domain="invalid",
type="paper",
name="a",
format="torch",
path=os.path.join(test_dir, "a.pt"),
in_memory=True,
)
# ``type`` could be null.
feature_data = [
gb.OnDiskFeatureData(
domain="node",
name="a",
format="torch",
path=os.path.join(test_dir, "a.pt"),
in_memory=True,
),
]
feature_store = gb.TorchBasedFeatureStore(feature_data)
# Test read the entire feature.
assert torch.equal(
feature_store.read("node", None, "a"),
torch.tensor([[1, 2, 4], [2, 5, 3]]),
)
# Test get the size of the entire feature.
assert feature_store.size("node", None, "a") == torch.Size([3])
feature_store = None
@pytest.mark.parametrize("in_memory", [True, False])
def test_torch_based_feature_repr(in_memory):
with tempfile.TemporaryDirectory() as test_dir:
a = torch.tensor([[1, 2, 3], [4, 5, 6]])
b = torch.tensor([[[1, 2], [3, 4]], [[4, 5], [6, 7]]])
metadata = {"max_value": 3}
if not in_memory:
a = to_on_disk_tensor(test_dir, "a", a)
b = to_on_disk_tensor(test_dir, "b", b)
feature_a = gb.TorchBasedFeature(a, metadata=metadata)
feature_b = gb.TorchBasedFeature(b)
expected_str_feature_a = (
"TorchBasedFeature(\n"
" feature=tensor([[1, 2, 3],\n"
" [4, 5, 6]]),\n"
" metadata={'max_value': 3},\n"
")"
)
expected_str_feature_b = (
"TorchBasedFeature(\n"
" feature=tensor([[[1, 2],\n"
" [3, 4]],\n"
"\n"
" [[4, 5],\n"
" [6, 7]]]),\n"
" metadata={},\n"
")"
)
assert repr(feature_a) == expected_str_feature_a, feature_a
assert repr(feature_b) == expected_str_feature_b, feature_b
a = b = metadata = None
feature_a = feature_b = None
expected_str_feature_a = expected_str_feature_b = None
@pytest.mark.parametrize("in_memory", [True, False])
def test_torch_based_feature_store_repr(in_memory):
with tempfile.TemporaryDirectory() as test_dir:
a = torch.tensor([[1, 2, 4], [2, 5, 3]])
b = torch.tensor([[[1, 2], [3, 4]], [[2, 5], [3, 4]]])
write_tensor_to_disk(test_dir, "a", a, fmt="torch")
write_tensor_to_disk(test_dir, "b", b, fmt="numpy")
feature_data = [
gb.OnDiskFeatureData(
domain="node",
type="paper",
name="a",
format="torch",
path=os.path.join(test_dir, "a.pt"),
in_memory=True,
),
gb.OnDiskFeatureData(
domain="edge",
type="paper:cites:paper",
name="b",
format="numpy",
path=os.path.join(test_dir, "b.npy"),
in_memory=in_memory,
),
]
feature_store = gb.TorchBasedFeatureStore(feature_data)
expected_feature_store_str = (
(
"TorchBasedFeatureStore(\n"
" {(<OnDiskFeatureDataDomain.NODE: 'node'>, 'paper', 'a'): TorchBasedFeature(\n"
" feature=tensor([[1, 2, 4],\n"
" [2, 5, 3]]),\n"
" metadata={},\n"
" ), (<OnDiskFeatureDataDomain.EDGE: 'edge'>, 'paper:cites:paper', 'b'): TorchBasedFeature(\n"
" feature=tensor([[[1, 2],\n"
" [3, 4]],\n"
"\n"
" [[2, 5],\n"
" [3, 4]]]),\n"
" metadata={},\n"
" )}\n"
")"
)
if in_memory
else (
"TorchBasedFeatureStore(\n"
" {(<OnDiskFeatureDataDomain.NODE: 'node'>, 'paper', 'a'): TorchBasedFeature(\n"
" feature=tensor([[1, 2, 4],\n"
" [2, 5, 3]]),\n"
" metadata={},\n"
" ), (<OnDiskFeatureDataDomain.EDGE: 'edge'>, 'paper:cites:paper', 'b'): DiskBasedFeature(\n"
" feature=tensor([[[1, 2],\n"
" [3, 4]],\n"
"\n"
" [[2, 5],\n"
" [3, 4]]]),\n"
" metadata={},\n"
" )}\n"
")"
)
)
assert repr(feature_store) == expected_feature_store_str, feature_store
a = b = feature_data = None
feature_store = expected_feature_store_str = None
@@ -0,0 +1,273 @@
import backend as F
import dgl.graphbolt as gb
import pytest
import torch
def test_unique_and_compact_hetero():
N1 = torch.tensor(
[0, 5, 2, 7, 12, 7, 9, 5, 6, 2, 3, 4, 1, 0, 9], device=F.ctx()
)
N2 = torch.tensor([0, 3, 3, 5, 2, 7, 2, 8, 4, 9, 2, 3], device=F.ctx())
N3 = torch.tensor([1, 2, 6, 6, 1, 8, 3, 6, 3, 2], device=F.ctx())
expected_unique = {
"n1": torch.tensor([0, 5, 2, 7, 12, 9, 6, 3, 4, 1], device=F.ctx()),
"n2": torch.tensor([0, 3, 5, 2, 7, 8, 4, 9], device=F.ctx()),
"n3": torch.tensor([1, 2, 6, 8, 3], device=F.ctx()),
}
if N1.is_cuda and torch.cuda.get_device_capability()[0] < 7:
expected_reverse_id = {
k: v.sort()[1] for k, v in expected_unique.items()
}
expected_unique = {k: v.sort()[0] for k, v in expected_unique.items()}
else:
expected_reverse_id = {
k: torch.arange(0, v.shape[0], device=F.ctx())
for k, v in expected_unique.items()
}
nodes_dict = {
"n1": N1.split(5),
"n2": N2.split(4),
"n3": N3.split(2),
}
expected_nodes_dict = {
"n1": [
torch.tensor([0, 1, 2, 3, 4], device=F.ctx()),
torch.tensor([3, 5, 1, 6, 2], device=F.ctx()),
torch.tensor([7, 8, 9, 0, 5], device=F.ctx()),
],
"n2": [
torch.tensor([0, 1, 1, 2], device=F.ctx()),
torch.tensor([3, 4, 3, 5], device=F.ctx()),
torch.tensor([6, 7, 3, 1], device=F.ctx()),
],
"n3": [
torch.tensor([0, 1], device=F.ctx()),
torch.tensor([2, 2], device=F.ctx()),
torch.tensor([0, 3], device=F.ctx()),
torch.tensor([4, 2], device=F.ctx()),
torch.tensor([4, 1], device=F.ctx()),
],
}
unique, compacted, _ = gb.unique_and_compact(nodes_dict)
for ntype, nodes in unique.items():
expected_nodes = expected_unique[ntype]
assert torch.equal(nodes, expected_nodes)
for ntype, nodes in compacted.items():
expected_nodes = expected_nodes_dict[ntype]
assert isinstance(nodes, list)
for expected_node, node in zip(expected_nodes, nodes):
node = expected_reverse_id[ntype][node]
assert torch.equal(expected_node, node)
def test_unique_and_compact_homo():
N = torch.tensor(
[0, 5, 2, 7, 12, 7, 9, 5, 6, 2, 3, 4, 1, 0, 9], device=F.ctx()
)
expected_unique_N = torch.tensor(
[0, 5, 2, 7, 12, 9, 6, 3, 4, 1], device=F.ctx()
)
if N.is_cuda and torch.cuda.get_device_capability()[0] < 7:
expected_reverse_id_N = expected_unique_N.sort()[1]
expected_unique_N = expected_unique_N.sort()[0]
else:
expected_reverse_id_N = torch.arange(
0, expected_unique_N.shape[0], device=F.ctx()
)
nodes_list = N.split(5)
expected_nodes_list = [
torch.tensor([0, 1, 2, 3, 4], device=F.ctx()),
torch.tensor([3, 5, 1, 6, 2], device=F.ctx()),
torch.tensor([7, 8, 9, 0, 5], device=F.ctx()),
]
unique, compacted, _ = gb.unique_and_compact(nodes_list)
assert torch.equal(unique, expected_unique_N)
assert isinstance(compacted, list)
for expected_node, node in zip(expected_nodes_list, compacted):
node = expected_reverse_id_N[node]
assert torch.equal(expected_node, node)
def test_unique_and_compact_csc_formats_hetero():
dst_nodes = {
"n2": torch.tensor([2, 4, 1, 3]),
"n3": torch.tensor([1, 3, 2, 7]),
}
csc_formats = {
"n1:e1:n2": gb.CSCFormatBase(
indptr=torch.tensor([0, 3, 4, 7, 10]),
indices=torch.tensor([1, 3, 4, 6, 2, 7, 9, 4, 2, 6]),
),
"n1:e2:n3": gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 4, 7, 10]),
indices=torch.tensor([5, 2, 6, 4, 7, 2, 8, 1, 3, 0]),
),
"n2:e3:n3": gb.CSCFormatBase(
indptr=torch.tensor([0, 2, 4, 6, 8]),
indices=torch.tensor([2, 5, 4, 1, 4, 3, 6, 0]),
),
}
expected_unique_nodes = {
"n1": torch.tensor([1, 3, 4, 6, 2, 7, 9, 5, 8, 0]),
"n2": torch.tensor([2, 4, 1, 3, 5, 6, 0]),
"n3": torch.tensor([1, 3, 2, 7]),
}
expected_csc_formats = {
"n1:e1:n2": gb.CSCFormatBase(
indptr=torch.tensor([0, 3, 4, 7, 10]),
indices=torch.tensor([0, 1, 2, 3, 4, 5, 6, 2, 4, 3]),
),
"n1:e2:n3": gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 4, 7, 10]),
indices=torch.tensor([7, 4, 3, 2, 5, 4, 8, 0, 1, 9]),
),
"n2:e3:n3": gb.CSCFormatBase(
indptr=torch.tensor([0, 2, 4, 6, 8]),
indices=torch.tensor([0, 4, 1, 2, 1, 3, 5, 6]),
),
}
unique_nodes, compacted_csc_formats, _ = gb.unique_and_compact_csc_formats(
csc_formats, dst_nodes
)
for ntype, nodes in unique_nodes.items():
expected_nodes = expected_unique_nodes[ntype]
assert torch.equal(nodes, expected_nodes)
for etype, pair in compacted_csc_formats.items():
indices = pair.indices
indptr = pair.indptr
expected_indices = expected_csc_formats[etype].indices
expected_indptr = expected_csc_formats[etype].indptr
assert torch.equal(indices, expected_indices)
assert torch.equal(indptr, expected_indptr)
def test_unique_and_compact_csc_formats_homo():
seeds = torch.tensor([1, 3, 5, 2, 6])
indptr = torch.tensor([0, 2, 4, 6, 7, 11])
indices = torch.tensor([2, 3, 1, 4, 5, 2, 5, 1, 4, 4, 6])
csc_formats = gb.CSCFormatBase(indptr=indptr, indices=indices)
expected_unique_nodes = torch.tensor([1, 3, 5, 2, 6, 4])
expected_indptr = indptr
expected_indices = torch.tensor([3, 1, 0, 5, 2, 3, 2, 0, 5, 5, 4])
unique_nodes, compacted_csc_formats, _ = gb.unique_and_compact_csc_formats(
csc_formats, seeds
)
indptr = compacted_csc_formats.indptr
indices = compacted_csc_formats.indices
assert torch.equal(indptr, expected_indptr)
assert torch.equal(indices, expected_indices)
assert torch.equal(unique_nodes, expected_unique_nodes)
def test_unique_and_compact_incorrect_indptr():
seeds = torch.tensor([1, 3, 5, 2, 6, 7])
indptr = torch.tensor([0, 2, 4, 6, 7, 11])
indices = torch.tensor([2, 3, 1, 4, 5, 2, 5, 1, 4, 4, 6])
csc_formats = gb.CSCFormatBase(indptr=indptr, indices=indices)
# The number of seeds is not corresponding to indptr.
with pytest.raises(AssertionError):
gb.unique_and_compact_csc_formats(csc_formats, seeds)
def test_compact_csc_format_hetero():
dst_nodes = {
"n2": torch.tensor([2, 4, 1, 3]),
"n3": torch.tensor([1, 3, 2, 7]),
}
csc_formats = {
"n1:e1:n2": gb.CSCFormatBase(
indptr=torch.tensor([0, 3, 4, 7, 10]),
indices=torch.tensor([1, 3, 4, 6, 2, 7, 9, 4, 2, 6]),
),
"n1:e2:n3": gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 4, 7, 10]),
indices=torch.tensor([5, 2, 6, 4, 7, 2, 8, 1, 3, 0]),
),
"n2:e3:n3": gb.CSCFormatBase(
indptr=torch.tensor([0, 2, 4, 6, 8]),
indices=torch.tensor([2, 5, 4, 1, 4, 3, 6, 0]),
),
}
expected_original_row_ids = {
"n1": torch.tensor(
[1, 3, 4, 6, 2, 7, 9, 4, 2, 6, 5, 2, 6, 4, 7, 2, 8, 1, 3, 0]
),
"n2": torch.tensor([2, 4, 1, 3, 2, 5, 4, 1, 4, 3, 6, 0]),
"n3": torch.tensor([1, 3, 2, 7]),
}
expected_csc_formats = {
"n1:e1:n2": gb.CSCFormatBase(
indptr=torch.tensor([0, 3, 4, 7, 10]),
indices=torch.arange(0, 10),
),
"n1:e2:n3": gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 4, 7, 10]),
indices=torch.arange(0, 10) + 10,
),
"n2:e3:n3": gb.CSCFormatBase(
indptr=torch.tensor([0, 2, 4, 6, 8]),
indices=torch.arange(0, 8) + 4,
),
}
original_row_ids, compacted_csc_formats = gb.compact_csc_format(
csc_formats, dst_nodes
)
for ntype, nodes in original_row_ids.items():
expected_nodes = expected_original_row_ids[ntype]
assert torch.equal(nodes, expected_nodes)
for etype, csc_format in compacted_csc_formats.items():
indptr = csc_format.indptr
indices = csc_format.indices
expected_indptr = expected_csc_formats[etype].indptr
expected_indices = expected_csc_formats[etype].indices
assert torch.equal(indptr, expected_indptr)
assert torch.equal(indices, expected_indices)
def test_compact_csc_format_homo():
seeds = torch.tensor([1, 3, 5, 2, 6])
indptr = torch.tensor([0, 2, 4, 6, 7, 11])
indices = torch.tensor([2, 3, 1, 4, 5, 2, 5, 1, 4, 4, 6])
csc_formats = gb.CSCFormatBase(indptr=indptr, indices=indices)
expected_original_row_ids = torch.tensor(
[1, 3, 5, 2, 6, 2, 3, 1, 4, 5, 2, 5, 1, 4, 4, 6]
)
expected_indptr = indptr
expected_indices = torch.arange(0, len(indices)) + 5
original_row_ids, compacted_csc_formats = gb.compact_csc_format(
csc_formats, seeds
)
indptr = compacted_csc_formats.indptr
indices = compacted_csc_formats.indices
assert torch.equal(indptr, expected_indptr)
assert torch.equal(indices, expected_indices)
assert torch.equal(original_row_ids, expected_original_row_ids)
def test_compact_incorrect_indptr():
seeds = torch.tensor([1, 3, 5, 2, 6, 7])
indptr = torch.tensor([0, 2, 4, 6, 7, 11])
indices = torch.tensor([2, 3, 1, 4, 5, 2, 5, 1, 4, 4, 6])
csc_formats = gb.CSCFormatBase(indptr=indptr, indices=indices)
# The number of seeds is not corresponding to indptr.
with pytest.raises(AssertionError):
gb.compact_csc_format(csc_formats, seeds)
@@ -0,0 +1,286 @@
import json
import os
import re
import tempfile
from functools import partial
import dgl.graphbolt as gb
import dgl.graphbolt.internal as internal
import numpy as np
import pandas as pd
import pytest
import torch
def test_read_torch_data():
with tempfile.TemporaryDirectory() as test_dir:
save_tensor = torch.tensor([[1, 2, 4], [2, 5, 3]])
file_name = os.path.join(test_dir, "save_tensor.pt")
torch.save(save_tensor, file_name)
read_tensor = internal.utils._read_torch_data(file_name)
assert torch.equal(save_tensor, read_tensor)
save_tensor = read_tensor = None
@pytest.mark.parametrize("in_memory", [True, False])
def test_read_numpy_data(in_memory):
with tempfile.TemporaryDirectory() as test_dir:
save_numpy = np.array([[1, 2, 4], [2, 5, 3]])
file_name = os.path.join(test_dir, "save_numpy.npy")
np.save(file_name, save_numpy)
read_tensor = internal.utils._read_numpy_data(file_name, in_memory)
assert torch.equal(torch.from_numpy(save_numpy), read_tensor)
save_numpy = read_tensor = None
@pytest.mark.parametrize("fmt", ["torch", "numpy"])
def test_read_data(fmt):
with tempfile.TemporaryDirectory() as test_dir:
data = np.array([[1, 2, 4], [2, 5, 3]])
type_name = "pt" if fmt == "torch" else "npy"
file_name = os.path.join(test_dir, f"save_data.{type_name}")
if fmt == "numpy":
np.save(file_name, data)
elif fmt == "torch":
torch.save(torch.from_numpy(data), file_name)
read_tensor = internal.read_data(file_name, fmt)
assert torch.equal(torch.from_numpy(data), read_tensor)
@pytest.mark.parametrize(
"data_fmt, save_fmt, contiguous",
[
("torch", "torch", True),
("torch", "torch", False),
("torch", "numpy", True),
("torch", "numpy", False),
("numpy", "torch", True),
("numpy", "torch", False),
("numpy", "numpy", True),
("numpy", "numpy", False),
],
)
def test_save_data(data_fmt, save_fmt, contiguous):
with tempfile.TemporaryDirectory() as test_dir:
data = np.array([[1, 2, 4], [2, 5, 3]])
if not contiguous:
data = np.asfortranarray(data)
tensor_data = torch.from_numpy(data)
type_name = "pt" if save_fmt == "torch" else "npy"
save_file_name = os.path.join(test_dir, f"save_data.{type_name}")
# Step1. Save the data.
if data_fmt == "torch":
internal.save_data(tensor_data, save_file_name, save_fmt)
elif data_fmt == "numpy":
internal.save_data(data, save_file_name, save_fmt)
# Step2. Load the data.
if save_fmt == "torch":
loaded_data = torch.load(save_file_name, weights_only=False)
assert loaded_data.is_contiguous()
assert torch.equal(tensor_data, loaded_data)
elif save_fmt == "numpy":
loaded_data = np.load(save_file_name)
# Checks if the loaded data is C-contiguous.
assert loaded_data.flags["C_CONTIGUOUS"]
assert np.array_equal(tensor_data.numpy(), loaded_data)
data = tensor_data = loaded_data = None
@pytest.mark.parametrize("fmt", ["torch", "numpy"])
def test_get_npy_dim(fmt):
with tempfile.TemporaryDirectory() as test_dir:
data = np.array([[1, 2, 4], [2, 5, 3]])
type_name = "pt" if fmt == "torch" else "npy"
file_name = os.path.join(test_dir, f"save_data.{type_name}")
if fmt == "numpy":
np.save(file_name, data)
assert internal.get_npy_dim(file_name) == 2
elif fmt == "torch":
torch.save(torch.from_numpy(data), file_name)
with pytest.raises(ValueError):
internal.get_npy_dim(file_name)
data = None
@pytest.mark.parametrize("data_fmt", ["numpy", "torch"])
@pytest.mark.parametrize("save_fmt", ["numpy", "torch"])
@pytest.mark.parametrize("is_feature", [True, False])
def test_copy_or_convert_data(data_fmt, save_fmt, is_feature):
with tempfile.TemporaryDirectory() as test_dir:
data = np.arange(10)
tensor_data = torch.from_numpy(data)
in_type_name = "npy" if data_fmt == "numpy" else "pt"
input_path = os.path.join(test_dir, f"data.{in_type_name}")
out_type_name = "npy" if save_fmt == "numpy" else "pt"
output_path = os.path.join(test_dir, f"out_data.{out_type_name}")
if data_fmt == "numpy":
np.save(input_path, data)
else:
torch.save(tensor_data, input_path)
if save_fmt == "torch":
with pytest.raises(AssertionError):
internal.copy_or_convert_data(
input_path,
output_path,
data_fmt,
save_fmt,
is_feature=is_feature,
)
else:
internal.copy_or_convert_data(
input_path,
output_path,
data_fmt,
save_fmt,
is_feature=is_feature,
)
if is_feature:
data = data.reshape(-1, 1)
tensor_data = tensor_data.reshape(-1, 1)
if save_fmt == "numpy":
out_data = np.load(output_path)
assert (data == out_data).all()
data = None
tensor_data = None
out_data = None
@pytest.mark.parametrize("edge_fmt", ["csv", "numpy"])
def test_read_edges(edge_fmt):
with tempfile.TemporaryDirectory() as test_dir:
num_nodes = 40
num_edges = 200
nodes = np.repeat(np.arange(num_nodes), 5)
neighbors = np.random.randint(0, num_nodes, size=(num_edges))
edges = np.stack([nodes, neighbors], axis=1)
os.makedirs(os.path.join(test_dir, "edges"), exist_ok=True)
if edge_fmt == "csv":
# Wrtie into edges/edge.csv
edges = pd.DataFrame(edges, columns=["src", "dst"])
edge_path = os.path.join("edges", "edge.csv")
edges.to_csv(
os.path.join(test_dir, edge_path),
index=False,
header=False,
)
else:
# Wrtie into edges/edge.npy
edges = edges.T
edge_path = os.path.join("edges", "edge.npy")
np.save(os.path.join(test_dir, edge_path), edges)
src, dst = internal.read_edges(test_dir, edge_fmt, edge_path)
assert src.all() == nodes.all()
assert dst.all() == neighbors.all()
def test_read_edges_error():
# 1. Unsupported file format.
with pytest.raises(
AssertionError,
match="`numpy` or `csv` is expected when reading edges but got `fake-type`.",
):
internal.read_edges("test_dir", "fake-type", "edge_path")
# 2. Unexpected shape of numpy array
with tempfile.TemporaryDirectory() as test_dir:
num_nodes = 40
num_edges = 200
nodes = np.repeat(np.arange(num_nodes), 5)
neighbors = np.random.randint(0, num_nodes, size=(num_edges))
edges = np.stack([nodes, neighbors, nodes], axis=1)
os.makedirs(os.path.join(test_dir, "edges"), exist_ok=True)
# Wrtie into edges/edge.npy
edges = edges.T
edge_path = os.path.join("edges", "edge.npy")
np.save(os.path.join(test_dir, edge_path), edges)
with pytest.raises(
AssertionError,
match=re.escape(
"The shape of edges should be (2, N), but got torch.Size([3, 200])."
),
):
internal.read_edges(test_dir, "numpy", edge_path)
def test_calculate_file_hash():
with tempfile.TemporaryDirectory() as test_dir:
test_file_path = os.path.join(test_dir, "test.txt")
with open(test_file_path, "w") as file:
file.write("test content")
hash_value = internal.calculate_file_hash(
test_file_path, hash_algo="md5"
)
expected_hash_value = "9473fdd0d880a43c21b7778d34872157"
assert expected_hash_value == hash_value
with pytest.raises(
ValueError,
match=re.escape(
"Hash algorithm must be one of: ['md5', 'sha1', 'sha224', "
+ "'sha256', 'sha384', 'sha512'], but got `fake`."
),
):
hash_value = internal.calculate_file_hash(
test_file_path, hash_algo="fake"
)
def test_calculate_dir_hash():
with tempfile.TemporaryDirectory() as test_dir:
test_file_path_1 = os.path.join(test_dir, "test_1.txt")
test_file_path_2 = os.path.join(test_dir, "test_2.txt")
with open(test_file_path_1, "w") as file:
file.write("test content")
with open(test_file_path_2, "w") as file:
file.write("test contents of directory")
hash_value = internal.calculate_dir_hash(test_dir, hash_algo="md5")
expected_hash_value = [
"56e708a2bdf92887d4a7f25cbc13c555",
"9473fdd0d880a43c21b7778d34872157",
]
assert len(hash_value) == 2
for val in hash_value.values():
assert val in expected_hash_value
def test_check_dataset_change():
with tempfile.TemporaryDirectory() as test_dir:
# Generate directory and record its hash value.
test_file_path_1 = os.path.join(test_dir, "test_1.txt")
test_file_path_2 = os.path.join(test_dir, "test_2.txt")
with open(test_file_path_1, "w") as file:
file.write("test content")
with open(test_file_path_2, "w") as file:
file.write("test contents of directory")
hash_value = internal.calculate_dir_hash(test_dir, hash_algo="md5")
hash_value_file = "dataset_hash_value.txt"
hash_value_file_paht = os.path.join(
test_dir, "preprocessed", hash_value_file
)
os.makedirs(os.path.join(test_dir, "preprocessed"), exist_ok=True)
with open(hash_value_file_paht, "w") as file:
file.write(json.dumps(hash_value, indent=4))
# Modify the content of a file.
with open(test_file_path_2, "w") as file:
file.write("test contents of directory changed")
assert internal.check_dataset_change(test_dir, "preprocessed")
def test_numpy_save_aligned():
assert_equal = partial(torch.testing.assert_close, rtol=0, atol=0)
a = torch.randn(1024, dtype=torch.float32) # 4096 bytes
with tempfile.TemporaryDirectory() as test_dir:
aligned_path = os.path.join(test_dir, "aligned.npy")
gb.numpy_save_aligned(aligned_path, a.numpy())
nonaligned_path = os.path.join(test_dir, "nonaligned.npy")
np.save(nonaligned_path, a.numpy())
assert_equal(np.load(aligned_path), np.load(nonaligned_path))
# The size of the file should be 4K (aligned header) + 4K (tensor).
assert os.path.getsize(aligned_path) == 4096 * 2
+413
View File
@@ -0,0 +1,413 @@
import os
import re
import unittest
from collections.abc import Iterable, Mapping
import backend as F
import dgl.graphbolt as gb
import pytest
import torch
from torch.torch_version import TorchVersion
from . import gb_test_utils
def test_pytorch_cuda_allocator_conf():
env = os.getenv("PYTORCH_CUDA_ALLOC_CONF")
assert env is not None
config_list = env.split(",")
assert "expandable_segments:True" in config_list
@unittest.skipIf(F._default_context_str != "gpu", "CopyTo needs GPU to test")
@pytest.mark.parametrize("non_blocking", [False, True])
def test_CopyTo(non_blocking):
item_sampler = gb.ItemSampler(
gb.ItemSet(torch.arange(20), names="seeds"), 4
)
if non_blocking:
item_sampler = item_sampler.transform(lambda x: x.pin_memory())
# Invoke CopyTo via class constructor.
dp = gb.CopyTo(item_sampler, "cuda")
for data in dp:
assert data.seeds.device.type == "cuda"
dp = gb.CopyTo(item_sampler, "cuda", non_blocking)
for data in dp:
assert data.seeds.device.type == "cuda"
# Invoke CopyTo via functional form.
dp = item_sampler.copy_to("cuda", non_blocking)
for data in dp:
assert data.seeds.device.type == "cuda"
@pytest.mark.parametrize(
"task",
[
"node_classification",
"node_inference",
"link_prediction",
"edge_classification",
],
)
@unittest.skipIf(F._default_context_str == "cpu", "CopyTo needs GPU to test")
def test_CopyToWithMiniBatches(task):
N = 16
B = 2
if task == "node_classification":
itemset = gb.ItemSet(
(torch.arange(N), torch.arange(N)), names=("seeds", "labels")
)
elif task == "node_inference":
itemset = gb.ItemSet(torch.arange(N), names="seeds")
elif task == "link_prediction":
itemset = gb.ItemSet(
(
torch.arange(2 * N).reshape(-1, 2),
torch.arange(N),
),
names=("seeds", "labels"),
)
elif task == "edge_classification":
itemset = gb.ItemSet(
(torch.arange(2 * N).reshape(-1, 2), torch.arange(N)),
names=("seeds", "labels"),
)
graph = gb_test_utils.rand_csc_graph(100, 0.15, bidirection_edge=True)
features = {}
keys = [("node", None, "a"), ("node", None, "b")]
features[keys[0]] = gb.TorchBasedFeature(torch.randn(200, 4))
features[keys[1]] = gb.TorchBasedFeature(torch.randn(200, 4))
feature_store = gb.BasicFeatureStore(features)
datapipe = gb.ItemSampler(itemset, batch_size=B)
datapipe = gb.NeighborSampler(
datapipe,
graph,
fanouts=[torch.LongTensor([2]) for _ in range(2)],
)
if task != "node_inference":
datapipe = gb.FeatureFetcher(
datapipe,
feature_store,
["a"],
)
copied_attrs = [
"labels",
"compacted_seeds",
"sampled_subgraphs",
"indexes",
"node_features",
"edge_features",
"blocks",
"seeds",
"input_nodes",
]
def test_data_device(datapipe):
for data in datapipe:
for attr in dir(data):
var = getattr(data, attr)
if isinstance(var, Mapping):
var = var[next(iter(var))]
elif isinstance(var, Iterable):
var = next(iter(var))
if (
not callable(var)
and not attr.startswith("__")
and hasattr(var, "device")
and var is not None
):
if attr in copied_attrs:
assert var.device.type == "cuda", attr
else:
assert var.device.type == "cpu", attr
# Invoke CopyTo via class constructor.
test_data_device(gb.CopyTo(datapipe, "cuda"))
# Invoke CopyTo via functional form.
test_data_device(datapipe.copy_to("cuda"))
def test_etype_tuple_to_str():
"""Convert etype from tuple to string."""
# Test for expected input.
c_etype = ("user", "like", "item")
c_etype_str = gb.etype_tuple_to_str(c_etype)
assert c_etype_str == "user:like:item"
# Test for unexpected input: not a tuple.
c_etype = "user:like:item"
with pytest.raises(
AssertionError,
match=re.escape(
"Passed-in canonical etype should be in format of (str, str, str). "
"But got user:like:item."
),
):
_ = gb.etype_tuple_to_str(c_etype)
# Test for unexpected input: tuple with wrong length.
c_etype = ("user", "like")
with pytest.raises(
AssertionError,
match=re.escape(
"Passed-in canonical etype should be in format of (str, str, str). "
"But got ('user', 'like')."
),
):
_ = gb.etype_tuple_to_str(c_etype)
def test_etype_str_to_tuple():
"""Convert etype from string to tuple."""
# Test for expected input.
c_etype_str = "user:like:item"
c_etype = gb.etype_str_to_tuple(c_etype_str)
assert c_etype == ("user", "like", "item")
# Test for unexpected input: string with wrong format.
c_etype_str = "user:like"
with pytest.raises(
AssertionError,
match=re.escape(
"Passed-in canonical etype should be in format of 'str:str:str'. "
"But got user:like."
),
):
_ = gb.etype_str_to_tuple(c_etype_str)
def test_seed_type_str_to_ntypes():
"""Convert etype from string to tuple."""
# Test for node pairs.
seed_type_str = "user:like:item"
seed_size = 2
node_type = gb.seed_type_str_to_ntypes(seed_type_str, seed_size)
assert node_type == ["user", "item"]
# Test for node pairs.
seed_type_str = "user:item:user"
seed_size = 3
node_type = gb.seed_type_str_to_ntypes(seed_type_str, seed_size)
assert node_type == ["user", "item", "user"]
# Test for unexpected input: list.
seed_type_str = ["user", "item"]
with pytest.raises(
AssertionError,
match=re.escape(
"Passed-in seed type should be string, but got <class 'list'>"
),
):
_ = gb.seed_type_str_to_ntypes(seed_type_str, 2)
def test_isin():
elements = torch.tensor([2, 3, 5, 5, 20, 13, 11], device=F.ctx())
test_elements = torch.tensor([2, 5], device=F.ctx())
res = gb.isin(elements, test_elements)
expected = torch.tensor(
[True, False, True, True, False, False, False], device=F.ctx()
)
assert torch.equal(res, expected)
def test_isin_big_data():
elements = torch.randint(0, 10000, (10000000,), device=F.ctx())
test_elements = torch.randint(0, 10000, (500000,), device=F.ctx())
res = gb.isin(elements, test_elements)
expected = torch.isin(elements, test_elements)
assert torch.equal(res, expected)
def test_isin_non_1D_dim():
elements = torch.tensor([[2, 3], [5, 5], [20, 13]], device=F.ctx())
test_elements = torch.tensor([2, 5], device=F.ctx())
with pytest.raises(Exception):
gb.isin(elements, test_elements)
elements = torch.tensor([2, 3, 5, 5, 20, 13], device=F.ctx())
test_elements = torch.tensor([[2, 5]], device=F.ctx())
with pytest.raises(Exception):
gb.isin(elements, test_elements)
@pytest.mark.parametrize(
"dtype",
[
torch.bool,
torch.uint8,
torch.int8,
torch.int16,
torch.int32,
torch.int64,
torch.float16,
torch.bfloat16,
torch.float32,
torch.float64,
],
)
@pytest.mark.parametrize("idtype", [torch.int32, torch.int64])
@pytest.mark.parametrize("pinned", [False, True])
def test_index_select(dtype, idtype, pinned):
if F._default_context_str != "gpu" and pinned:
pytest.skip("Pinned tests are available only on GPU.")
tensor = torch.tensor([[2, 3], [5, 5], [20, 13]], dtype=dtype)
tensor = tensor.pin_memory() if pinned else tensor.to(F.ctx())
index = torch.tensor([0, 2], dtype=idtype, device=F.ctx())
gb_result = gb.index_select(tensor, index)
torch_result = tensor.to(F.ctx())[index.long()]
assert torch.equal(torch_result, gb_result)
if pinned:
gb_result = gb.index_select(tensor.cpu(), index.cpu().pin_memory())
assert torch.equal(torch_result.cpu(), gb_result)
assert gb_result.is_pinned()
# Test the internal async API
future = torch.ops.graphbolt.index_select_async(tensor.cpu(), index.cpu())
assert torch.equal(torch_result.cpu(), future.wait())
@pytest.mark.parametrize(
"dtype",
[
torch.bool,
torch.uint8,
torch.int8,
torch.int16,
torch.int32,
torch.int64,
torch.float16,
torch.bfloat16,
torch.float32,
torch.float64,
],
)
@pytest.mark.parametrize("idtype", [torch.int32, torch.int64])
def test_scatter_async(dtype, idtype):
input = torch.tensor([[2, 3], [5, 5], [20, 13]], dtype=dtype)
index = torch.ones([1], dtype=idtype)
res = torch.ops.graphbolt.scatter_async(input, index, input[2:3])
assert torch.equal(
torch.tensor([[2, 3], [20, 13], [20, 13]], dtype=dtype), res.wait()
)
def torch_expand_indptr(indptr, dtype, nodes=None):
if nodes is None:
nodes = torch.arange(len(indptr) - 1, dtype=dtype, device=indptr.device)
return nodes.to(dtype).repeat_interleave(indptr.diff())
@pytest.mark.parametrize("nodes", [None, True])
@pytest.mark.parametrize("dtype", [torch.int32, torch.int64])
def test_expand_indptr(nodes, dtype):
if nodes:
nodes = torch.tensor([1, 7, 3, 4, 5, 8], dtype=dtype, device=F.ctx())
indptr = torch.tensor([0, 2, 2, 7, 10, 12, 20], device=F.ctx())
torch_result = torch_expand_indptr(indptr, dtype, nodes)
gb_result = gb.expand_indptr(indptr, dtype, nodes)
assert torch.equal(torch_result, gb_result)
gb_result = gb.expand_indptr(indptr, dtype, nodes, indptr[-1].item())
assert torch.equal(torch_result, gb_result)
if TorchVersion(torch.__version__) >= TorchVersion("2.2.0a0"):
import torch._dynamo as dynamo
from torch.testing._internal.optests import opcheck
# Tests torch.compile compatibility
for output_size in [None, indptr[-1].item()]:
kwargs = {"node_ids": nodes, "output_size": output_size}
opcheck(
torch.ops.graphbolt.expand_indptr,
(indptr, dtype),
kwargs,
test_utils=[
"test_schema",
"test_autograd_registration",
"test_faketensor",
"test_aot_dispatch_dynamic",
],
raise_exception=True,
)
explanation = dynamo.explain(gb.expand_indptr)(
indptr, dtype, nodes, output_size
)
expected_breaks = -1 if output_size is None else 0
assert explanation.graph_break_count == expected_breaks
@unittest.skipIf(
F._default_context_str != "gpu", "Only GPU implementation is available."
)
@pytest.mark.parametrize("offset", [None, True])
@pytest.mark.parametrize("dtype", [torch.int32, torch.int64])
def test_indptr_edge_ids(offset, dtype):
indptr = torch.tensor([0, 2, 2, 7, 10, 12], device=F.ctx())
if offset:
offset = indptr[:-1]
ref_result = torch.arange(
0, indptr[-1].item(), dtype=dtype, device=F.ctx()
)
else:
ref_result = torch.tensor(
[0, 1, 0, 1, 2, 3, 4, 0, 1, 2, 0, 1], dtype=dtype, device=F.ctx()
)
gb_result = gb.indptr_edge_ids(indptr, dtype, offset)
assert torch.equal(ref_result, gb_result)
gb_result = gb.indptr_edge_ids(indptr, dtype, offset, indptr[-1].item())
assert torch.equal(ref_result, gb_result)
if TorchVersion(torch.__version__) >= TorchVersion("2.2.0a0"):
import torch._dynamo as dynamo
from torch.testing._internal.optests import opcheck
# Tests torch.compile compatibility
for output_size in [None, indptr[-1].item()]:
kwargs = {"offset": offset, "output_size": output_size}
opcheck(
torch.ops.graphbolt.indptr_edge_ids,
(indptr, dtype),
kwargs,
test_utils=[
"test_schema",
"test_autograd_registration",
"test_faketensor",
"test_aot_dispatch_dynamic",
],
raise_exception=True,
)
explanation = dynamo.explain(gb.indptr_edge_ids)(
indptr, dtype, offset, output_size
)
expected_breaks = -1 if output_size is None else 0
assert explanation.graph_break_count == expected_breaks
def test_csc_format_base_representation():
csc_format_base = gb.CSCFormatBase(
indptr=torch.tensor([0, 2, 4]),
indices=torch.tensor([4, 5, 6, 7]),
)
expected_result = str(
"""CSCFormatBase(indptr=tensor([0, 2, 4]),
indices=tensor([4, 5, 6, 7]),
)"""
)
assert str(csc_format_base) == expected_result, print(csc_format_base)
def test_csc_format_base_incorrect_indptr():
indptr = torch.tensor([0, 2, 4, 6, 7, 11])
indices = torch.tensor([2, 3, 1, 4, 5, 2, 5, 1, 4, 4])
with pytest.raises(AssertionError):
# The value of last element in indptr is not corresponding to indices.
csc_formats = gb.CSCFormatBase(indptr=indptr, indices=indices)
@@ -0,0 +1,220 @@
import os
import unittest
from sys import platform
import backend as F
import dgl
import dgl.graphbolt
import dgl.graphbolt as gb
import pytest
import torch
import torch.distributed as thd
from dgl.graphbolt.datapipes import find_dps, traverse_dps
from . import gb_test_utils
@pytest.mark.parametrize("overlap_feature_fetch", [False, True])
def test_DataLoader(overlap_feature_fetch):
N = 40
B = 4
itemset = dgl.graphbolt.ItemSet(torch.arange(N), names="seeds")
graph = gb_test_utils.rand_csc_graph(200, 0.15, bidirection_edge=True)
features = {}
keys = [("node", None, "a"), ("node", None, "b"), ("edge", None, "c")]
features[keys[0]] = dgl.graphbolt.TorchBasedFeature(torch.randn(200, 4))
features[keys[1]] = dgl.graphbolt.TorchBasedFeature(torch.randn(200, 4))
M = graph.total_num_edges
features[keys[2]] = dgl.graphbolt.TorchBasedFeature(torch.randn(M, 1))
feature_store = dgl.graphbolt.BasicFeatureStore(features)
item_sampler = dgl.graphbolt.ItemSampler(itemset, batch_size=B)
subgraph_sampler = dgl.graphbolt.NeighborSampler(
item_sampler,
graph,
fanouts=[torch.LongTensor([2]) for _ in range(2)],
)
feature_fetcher = dgl.graphbolt.FeatureFetcher(
subgraph_sampler,
feature_store,
["a", "b"],
["c"],
overlap_fetch=overlap_feature_fetch,
)
device_transferrer = dgl.graphbolt.CopyTo(feature_fetcher, F.ctx())
dataloader = dgl.graphbolt.DataLoader(
device_transferrer,
num_workers=4,
)
for i, minibatch in enumerate(dataloader):
assert "a" in minibatch.node_features
assert "b" in minibatch.node_features
for layer_id in range(minibatch.num_layers()):
assert "c" in minibatch.edge_features[layer_id]
assert i + 1 == N // B
@unittest.skipIf(
F._default_context_str != "gpu",
reason="This test requires the GPU.",
)
@pytest.mark.parametrize(
"sampler_name", ["NeighborSampler", "LayerNeighborSampler"]
)
@pytest.mark.parametrize("enable_feature_fetch", [True, False])
@pytest.mark.parametrize("overlap_feature_fetch", [True, False])
@pytest.mark.parametrize("overlap_graph_fetch", [True, False])
@pytest.mark.parametrize("cooperative", [True, False])
@pytest.mark.parametrize("asynchronous", [True, False])
@pytest.mark.parametrize("num_gpu_cached_edges", [0, 1024])
@pytest.mark.parametrize("gpu_cache_threshold", [1, 3])
def test_gpu_sampling_DataLoader(
sampler_name,
enable_feature_fetch,
overlap_feature_fetch,
overlap_graph_fetch,
cooperative,
asynchronous,
num_gpu_cached_edges,
gpu_cache_threshold,
):
if cooperative and not thd.is_initialized():
# On Windows, the init method can only be file.
init_method = (
f"file:///{os.path.join(os.getcwd(), 'dis_tempfile')}"
if platform == "win32"
else "tcp://127.0.0.1:12345"
)
thd.init_process_group(
init_method=init_method,
world_size=1,
rank=0,
)
N = 40
B = 4
num_layers = 2
itemset = dgl.graphbolt.ItemSet(torch.arange(N), names="seeds")
graph = gb_test_utils.rand_csc_graph(200, 0.15, bidirection_edge=True)
graph = graph.pin_memory_() if overlap_graph_fetch else graph.to(F.ctx())
features = {}
keys = [
("node", None, "a"),
("node", None, "b"),
("node", None, "c"),
("edge", None, "d"),
]
features[keys[0]] = dgl.graphbolt.TorchBasedFeature(
torch.randn(200, 4, pin_memory=True)
)
features[keys[1]] = dgl.graphbolt.TorchBasedFeature(
torch.randn(200, 4, pin_memory=True)
)
features[keys[2]] = dgl.graphbolt.TorchBasedFeature(
torch.randn(200, 4, device=F.ctx())
)
features[keys[3]] = dgl.graphbolt.TorchBasedFeature(
torch.randn(graph.total_num_edges, 1, device=F.ctx())
)
feature_store = dgl.graphbolt.BasicFeatureStore(features)
dataloaders = []
for i in range(2):
datapipe = dgl.graphbolt.ItemSampler(itemset, batch_size=B)
datapipe = datapipe.copy_to(F.ctx())
kwargs = {
"overlap_fetch": overlap_graph_fetch,
"num_gpu_cached_edges": num_gpu_cached_edges,
"gpu_cache_threshold": gpu_cache_threshold,
"cooperative": cooperative,
"asynchronous": asynchronous,
}
if i != 0:
kwargs = {}
datapipe = getattr(dgl.graphbolt, sampler_name)(
datapipe,
graph,
fanouts=[torch.LongTensor([2]) for _ in range(num_layers)],
**kwargs,
)
if enable_feature_fetch:
datapipe = dgl.graphbolt.FeatureFetcher(
datapipe,
feature_store,
["a", "b", "c"],
["d"],
overlap_fetch=overlap_feature_fetch and i == 0,
cooperative=asynchronous and cooperative and i == 0,
)
dataloaders.append(dgl.graphbolt.DataLoader(datapipe))
dataloader, dataloader2 = dataloaders
bufferer_cnt = int(enable_feature_fetch and overlap_feature_fetch)
if overlap_graph_fetch:
bufferer_cnt += num_layers
if num_gpu_cached_edges > 0:
bufferer_cnt += 2 * num_layers
if asynchronous:
bufferer_cnt += 2 * num_layers + 1 # _preprocess stage has 1.
if cooperative:
bufferer_cnt += 3 * num_layers
if enable_feature_fetch:
bufferer_cnt += 1 # feature fetch has 1.
if cooperative:
# _preprocess stage.
bufferer_cnt += 4
datapipe_graph = traverse_dps(dataloader)
bufferers = find_dps(
datapipe_graph,
dgl.graphbolt.Bufferer,
)
assert len(bufferers) == bufferer_cnt
# Fixes the randomness of LayerNeighborSampler
torch.manual_seed(1)
minibatches = list(dataloader)
assert len(minibatches) == N // B
for i, _ in enumerate(dataloader):
if i >= 1:
break
torch.manual_seed(1)
for minibatch, minibatch2 in zip(minibatches, dataloader2):
if enable_feature_fetch:
assert "a" in minibatch.node_features
assert "b" in minibatch.node_features
assert "c" in minibatch.node_features
if sampler_name == "LayerNeighborSampler":
assert torch.equal(
minibatch.node_features["a"], minibatch2.node_features["a"]
)
for layer_id in range(minibatch.num_layers()):
assert "d" in minibatch.edge_features[layer_id]
edge_feature = minibatch.edge_features[layer_id]["d"]
edge_feature_ref = minibatch2.edge_features[layer_id]["d"]
if sampler_name == "LayerNeighborSampler":
assert torch.equal(edge_feature, edge_feature_ref)
assert len(list(dataloader)) == N // B
if asynchronous and cooperative:
for minibatch in minibatches:
x = torch.ones((minibatch.node_ids().size(0), 1), device=F.ctx())
for subgraph in minibatch.sampled_subgraphs:
x = gb.CooperativeConvFunction.apply(subgraph, x)
x, edge_index, size = subgraph.to_pyg(x)
x = x[0]
one = torch.ones(
edge_index.shape[1], dtype=x.dtype, device=x.device
)
coo = torch.sparse_coo_tensor(
edge_index.flipud(), one, size=(size[1], size[0])
)
x = torch.sparse.mm(coo, x)
assert x.shape[0] == minibatch.seeds.shape[0]
assert x.shape[1] == 1
if thd.is_initialized():
thd.destroy_process_group()
@@ -0,0 +1,15 @@
import pytest
from dgl import graphbolt as gb
def test_Dataset():
dataset = gb.Dataset()
with pytest.raises(NotImplementedError):
_ = dataset.tasks
with pytest.raises(NotImplementedError):
_ = dataset.graph
with pytest.raises(NotImplementedError):
_ = dataset.feature
with pytest.raises(NotImplementedError):
_ = dataset.dataset_name
@@ -0,0 +1,258 @@
import random
from functools import partial
import dgl.graphbolt as gb
import torch
from torch.utils.data.datapipes.iter import Mapper
from . import gb_test_utils
def test_FeatureFetcher_invoke():
# Prepare graph and required datapipes.
graph = gb_test_utils.rand_csc_graph(20, 0.15, bidirection_edge=True)
a = torch.tensor(
[[random.randint(0, 10)] for _ in range(graph.total_num_nodes)]
)
b = torch.tensor(
[[random.randint(0, 10)] for _ in range(graph.total_num_edges)]
)
features = {}
keys = [("node", None, "a"), ("edge", None, "b")]
features[keys[0]] = gb.TorchBasedFeature(a)
features[keys[1]] = gb.TorchBasedFeature(b)
feature_store = gb.BasicFeatureStore(features)
itemset = gb.ItemSet(torch.arange(10), names="seeds")
item_sampler = gb.ItemSampler(itemset, batch_size=2)
num_layer = 2
fanouts = [torch.LongTensor([2]) for _ in range(num_layer)]
# Invoke FeatureFetcher via class constructor.
datapipe = gb.NeighborSampler(item_sampler, graph, fanouts)
datapipe = gb.FeatureFetcher(datapipe, feature_store, ["a"], ["b"])
assert len(list(datapipe)) == 5
# Invoke FeatureFetcher via functional form.
datapipe = item_sampler.sample_neighbor(graph, fanouts).fetch_feature(
feature_store, ["a"], ["b"]
)
assert len(list(datapipe)) == 5
def test_FeatureFetcher_homo():
graph = gb_test_utils.rand_csc_graph(20, 0.15, bidirection_edge=True)
a = torch.tensor(
[[random.randint(0, 10)] for _ in range(graph.total_num_nodes)]
)
b = torch.tensor(
[[random.randint(0, 10)] for _ in range(graph.total_num_edges)]
)
features = {}
keys = [("node", None, "a"), ("edge", None, "b")]
features[keys[0]] = gb.TorchBasedFeature(a)
features[keys[1]] = gb.TorchBasedFeature(b)
feature_store = gb.BasicFeatureStore(features)
itemset = gb.ItemSet(torch.arange(10), names="seeds")
item_sampler = gb.ItemSampler(itemset, batch_size=2)
num_layer = 2
fanouts = [torch.LongTensor([2]) for _ in range(num_layer)]
sampler_dp = gb.NeighborSampler(item_sampler, graph, fanouts)
fetcher_dp = gb.FeatureFetcher(sampler_dp, feature_store, ["a"], ["b"])
assert len(list(fetcher_dp)) == 5
def _func(fn, minibatch):
return fn(minibatch)
def test_FeatureFetcher_with_edges_homo():
graph = gb_test_utils.rand_csc_graph(20, 0.15, bidirection_edge=True)
a = torch.tensor(
[[random.randint(0, 10)] for _ in range(graph.total_num_nodes)]
)
b = torch.tensor(
[[random.randint(0, 10)] for _ in range(graph.total_num_edges)]
)
def add_node_and_edge_ids(minibatch):
seeds = minibatch.seeds
subgraphs = []
for _ in range(3):
sampled_csc = gb.CSCFormatBase(
indptr=torch.arange(11),
indices=torch.arange(10),
)
subgraphs.append(
gb.SampledSubgraphImpl(
sampled_csc=sampled_csc,
original_column_node_ids=torch.arange(10),
original_row_node_ids=torch.arange(10),
original_edge_ids=torch.randint(
0, graph.total_num_edges, (10,)
),
)
)
data = gb.MiniBatch(input_nodes=seeds, sampled_subgraphs=subgraphs)
return data
features = {}
keys = [("node", None, "a"), ("edge", None, "b")]
features[keys[0]] = gb.TorchBasedFeature(a)
features[keys[1]] = gb.TorchBasedFeature(b)
feature_store = gb.BasicFeatureStore(features)
itemset = gb.ItemSet(torch.arange(10), names="seeds")
item_sampler_dp = gb.ItemSampler(itemset, batch_size=2)
fn = partial(_func, add_node_and_edge_ids)
converter_dp = Mapper(item_sampler_dp, fn)
fetcher_dp = gb.FeatureFetcher(converter_dp, feature_store, ["a"], ["b"])
assert len(list(fetcher_dp)) == 5
for data in fetcher_dp:
assert data.node_features["a"].size(0) == 2
assert len(data.edge_features) == 3
for edge_feature in data.edge_features:
assert edge_feature["b"].size(0) == 10
def get_hetero_graph():
# COO graph:
# [0, 0, 1, 1, 2, 2, 3, 3, 4, 4]
# [2, 4, 2, 3, 0, 1, 1, 0, 0, 1]
# [1, 1, 1, 1, 0, 0, 0, 0, 0] - > edge type.
# num_nodes = 5, num_n1 = 2, num_n2 = 3
ntypes = {"n1": 0, "n2": 1}
etypes = {"n1:e1:n2": 0, "n2:e2:n1": 1}
indptr = torch.LongTensor([0, 2, 4, 6, 8, 10])
indices = torch.LongTensor([2, 4, 2, 3, 0, 1, 1, 0, 0, 1])
type_per_edge = torch.LongTensor([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
node_type_offset = torch.LongTensor([0, 2, 5])
return gb.fused_csc_sampling_graph(
indptr,
indices,
node_type_offset=node_type_offset,
type_per_edge=type_per_edge,
node_type_to_id=ntypes,
edge_type_to_id=etypes,
)
def test_FeatureFetcher_hetero():
graph = get_hetero_graph()
a = torch.tensor([[random.randint(0, 10)] for _ in range(2)])
b = torch.tensor([[random.randint(0, 10)] for _ in range(3)])
features = {}
keys = [("node", "n1", "a"), ("node", "n2", "a")]
features[keys[0]] = gb.TorchBasedFeature(a)
features[keys[1]] = gb.TorchBasedFeature(b)
feature_store = gb.BasicFeatureStore(features)
itemset = gb.HeteroItemSet(
{
"n1": gb.ItemSet(torch.LongTensor([0, 1]), names="seeds"),
"n2": gb.ItemSet(torch.LongTensor([0, 1, 2]), names="seeds"),
}
)
item_sampler = gb.ItemSampler(itemset, batch_size=2)
num_layer = 2
fanouts = [torch.LongTensor([2]) for _ in range(num_layer)]
sampler_dp = gb.NeighborSampler(item_sampler, graph, fanouts)
# "n3" is not in the sampled input nodes.
node_feature_keys = {"n1": ["a"], "n2": ["a"], "n3": ["a"]}
fetcher_dp = gb.FeatureFetcher(
sampler_dp, feature_store, node_feature_keys=node_feature_keys
)
assert len(list(fetcher_dp)) == 3
# Do not fetch feature for "n1".
node_feature_keys = {"n2": ["a"]}
fetcher_dp = gb.FeatureFetcher(
sampler_dp, feature_store, node_feature_keys=node_feature_keys
)
for mini_batch in fetcher_dp:
assert ("n1", "a") not in mini_batch.node_features
def test_FeatureFetcher_with_edges_hetero():
a = torch.tensor([[random.randint(0, 10)] for _ in range(20)])
b = torch.tensor([[random.randint(0, 10)] for _ in range(50)])
def add_node_and_edge_ids(minibatch):
seeds = minibatch.seeds
subgraphs = []
original_edge_ids = {
"n1:e1:n2": torch.randint(0, 50, (10,)),
"n2:e2:n1": torch.randint(0, 50, (10,)),
}
original_column_node_ids = {
"n1": torch.randint(0, 20, (10,)),
"n2": torch.randint(0, 20, (10,)),
}
original_row_node_ids = {
"n1": torch.randint(0, 20, (10,)),
"n2": torch.randint(0, 20, (10,)),
}
for _ in range(3):
subgraphs.append(
gb.SampledSubgraphImpl(
sampled_csc={
"n1:e1:n2": gb.CSCFormatBase(
indptr=torch.arange(11),
indices=torch.arange(10),
),
"n2:e2:n1": gb.CSCFormatBase(
indptr=torch.arange(11),
indices=torch.arange(10),
),
},
original_column_node_ids=original_column_node_ids,
original_row_node_ids=original_row_node_ids,
original_edge_ids=original_edge_ids,
)
)
data = gb.MiniBatch(input_nodes=seeds, sampled_subgraphs=subgraphs)
return data
features = {}
keys = [
("node", "n1", "a"),
("edge", "n1:e1:n2", "a"),
("edge", "n2:e2:n1", "a"),
]
features[keys[0]] = gb.TorchBasedFeature(a)
features[keys[1]] = gb.TorchBasedFeature(b)
feature_store = gb.BasicFeatureStore(features)
itemset = gb.HeteroItemSet(
{
"n1": gb.ItemSet(torch.randint(0, 20, (10,)), names="seeds"),
}
)
item_sampler_dp = gb.ItemSampler(itemset, batch_size=2)
fn = partial(_func, add_node_and_edge_ids)
converter_dp = Mapper(item_sampler_dp, fn)
# "n3:e3:n3" is not in the sampled edges.
# Do not fetch feature for "n2:e2:n1".
node_feature_keys = {"n1": ["a"]}
edge_feature_keys = {"n1:e1:n2": ["a"], "n3:e3:n3": ["a"]}
fetcher_dp = gb.FeatureFetcher(
converter_dp,
feature_store,
node_feature_keys=node_feature_keys,
edge_feature_keys=edge_feature_keys,
)
assert len(list(fetcher_dp)) == 5
for data in fetcher_dp:
assert data.node_features[("n1", "a")].size(0) == 2
assert len(data.edge_features) == 3
for edge_feature in data.edge_features:
assert edge_feature[("n1:e1:n2", "a")].size(0) == 10
assert ("n2:e2:n1", "a") not in edge_feature
@@ -0,0 +1,60 @@
import backend as F
import dgl.graphbolt as gb
import pytest
import torch
def test_find_reverse_edges_homo():
edges = torch.tensor([[1, 3, 5], [2, 4, 5]]).T
edges = gb.add_reverse_edges(edges)
expected_edges = torch.tensor([[1, 3, 5, 2, 4, 5], [2, 4, 5, 1, 3, 5]]).T
assert torch.equal(edges, expected_edges)
assert torch.equal(edges[1], expected_edges[1])
def test_find_reverse_edges_hetero():
edges = {
"A:r:B": torch.tensor([[1, 5], [2, 5]]).T,
"B:rr:A": torch.tensor([[3], [3]]).T,
}
edges = gb.add_reverse_edges(edges, {"A:r:B": "B:rr:A"})
expected_edges = {
"A:r:B": torch.tensor([[1, 5], [2, 5]]).T,
"B:rr:A": torch.tensor([[3, 2, 5], [3, 1, 5]]).T,
}
assert torch.equal(edges["A:r:B"], expected_edges["A:r:B"])
assert torch.equal(edges["B:rr:A"], expected_edges["B:rr:A"])
def test_find_reverse_edges_bi_reverse_types():
edges = {
"A:r:B": torch.tensor([[1, 5], [2, 5]]).T,
"B:rr:A": torch.tensor([[3], [3]]).T,
}
edges = gb.add_reverse_edges(edges, {"A:r:B": "B:rr:A", "B:rr:A": "A:r:B"})
expected_edges = {
"A:r:B": torch.tensor([[1, 5, 3], [2, 5, 3]]).T,
"B:rr:A": torch.tensor([[3, 2, 5], [3, 1, 5]]).T,
}
assert torch.equal(edges["A:r:B"], expected_edges["A:r:B"])
assert torch.equal(edges["B:rr:A"], expected_edges["B:rr:A"])
def test_find_reverse_edges_circual_reverse_types():
edges = {
"A:r1:B": torch.tensor([[1, 1]]),
"B:r2:C": torch.tensor([[2, 2]]),
"C:r3:A": torch.tensor([[3, 3]]),
}
edges = gb.add_reverse_edges(
edges, {"A:r1:B": "B:r2:C", "B:r2:C": "C:r3:A", "C:r3:A": "A:r1:B"}
)
expected_edges = {
"A:r1:B": torch.tensor([[1, 3], [1, 3]]).T,
"B:r2:C": torch.tensor([[2, 1], [2, 1]]).T,
"C:r3:A": torch.tensor([[3, 2], [3, 2]]).T,
}
assert torch.equal(edges["A:r1:B"], expected_edges["A:r1:B"])
assert torch.equal(edges["B:r2:C"], expected_edges["B:r2:C"])
assert torch.equal(edges["A:r1:B"], expected_edges["A:r1:B"])
assert torch.equal(edges["C:r3:A"], expected_edges["C:r3:A"])
@@ -0,0 +1,360 @@
import dgl
import dgl.graphbolt as gb
import dgl.sparse as dglsp
import torch
def test_integration_link_prediction():
torch.manual_seed(926)
indptr = torch.tensor([0, 0, 1, 3, 6, 8, 10])
indices = torch.tensor([5, 3, 3, 3, 3, 4, 4, 0, 5, 4])
matrix_a = dglsp.from_csc(indptr, indices)
seeds = torch.t(torch.stack(matrix_a.coo()))
node_feature_data = torch.tensor(
[
[0.9634, 0.2294],
[0.6172, 0.7865],
[0.2109, 0.1089],
[0.8672, 0.2276],
[0.5503, 0.8223],
[0.5160, 0.2486],
]
)
edge_feature_data = torch.tensor(
[
[0.5123, 0.1709, 0.6150],
[0.1476, 0.1902, 0.1314],
[0.2582, 0.5203, 0.6228],
[0.3708, 0.7631, 0.2683],
[0.2126, 0.7878, 0.7225],
[0.7885, 0.3414, 0.5485],
[0.4088, 0.8200, 0.1851],
[0.0056, 0.9469, 0.4432],
[0.8972, 0.7511, 0.3617],
[0.5773, 0.2199, 0.3366],
]
)
item_set = gb.ItemSet(seeds, names="seeds")
graph = gb.fused_csc_sampling_graph(indptr, indices)
node_feature = gb.TorchBasedFeature(node_feature_data)
edge_feature = gb.TorchBasedFeature(edge_feature_data)
features = {
("node", None, "feat"): node_feature,
("edge", None, "feat"): edge_feature,
}
feature_store = gb.BasicFeatureStore(features)
datapipe = gb.ItemSampler(item_set, batch_size=4)
datapipe = datapipe.sample_uniform_negative(graph, 2)
fanouts = torch.LongTensor([1])
datapipe = datapipe.sample_neighbor(graph, [fanouts, fanouts], replace=True)
datapipe = datapipe.transform(gb.exclude_seed_edges)
datapipe = datapipe.fetch_feature(
feature_store, node_feature_keys=["feat"], edge_feature_keys=["feat"]
)
dataloader = gb.DataLoader(
datapipe,
)
expected = [
str(
"""MiniBatch(seeds=tensor([[5, 1],
[3, 2],
[3, 2],
[3, 3],
[5, 2],
[5, 1],
[3, 4],
[3, 3],
[3, 5],
[3, 2],
[3, 0],
[3, 4]]),
sampled_subgraphs=[SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 1, 1, 1, 1, 2, 2], dtype=torch.int32),
indices=tensor([4, 5], dtype=torch.int32),
),
original_row_node_ids=tensor([5, 1, 3, 2, 4, 0]),
original_edge_ids=tensor([9, 7]),
original_column_node_ids=tensor([5, 1, 3, 2, 4, 0]),
),
SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 1, 1, 1, 1, 2, 2], dtype=torch.int32),
indices=tensor([0, 5], dtype=torch.int32),
),
original_row_node_ids=tensor([5, 1, 3, 2, 4, 0]),
original_edge_ids=tensor([8, 7]),
original_column_node_ids=tensor([5, 1, 3, 2, 4, 0]),
)],
node_features={'feat': tensor([[0.5160, 0.2486],
[0.6172, 0.7865],
[0.8672, 0.2276],
[0.2109, 0.1089],
[0.5503, 0.8223],
[0.9634, 0.2294]])},
labels=tensor([1., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0.]),
input_nodes=tensor([5, 1, 3, 2, 4, 0]),
indexes=tensor([0, 1, 2, 3, 0, 0, 1, 1, 2, 2, 3, 3]),
edge_features=[{'feat': tensor([[0.5773, 0.2199, 0.3366],
[0.0056, 0.9469, 0.4432]])},
{'feat': tensor([[0.8972, 0.7511, 0.3617],
[0.0056, 0.9469, 0.4432]])}],
compacted_seeds=tensor([[0, 1],
[2, 3],
[2, 3],
[2, 2],
[0, 3],
[0, 1],
[2, 4],
[2, 2],
[2, 0],
[2, 3],
[2, 5],
[2, 4]]),
blocks=[Block(num_src_nodes=6, num_dst_nodes=6, num_edges=2),
Block(num_src_nodes=6, num_dst_nodes=6, num_edges=2)],
)"""
),
str(
"""MiniBatch(seeds=tensor([[3, 3],
[4, 3],
[4, 4],
[0, 4],
[3, 4],
[3, 5],
[4, 1],
[4, 4],
[4, 4],
[4, 5],
[0, 1],
[0, 3]]),
sampled_subgraphs=[SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 0, 0, 0, 0, 1], dtype=torch.int32),
indices=tensor([3], dtype=torch.int32),
),
original_row_node_ids=tensor([3, 4, 0, 5, 1]),
original_edge_ids=tensor([0]),
original_column_node_ids=tensor([3, 4, 0, 5, 1]),
),
SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 0, 0, 0, 1, 2], dtype=torch.int32),
indices=tensor([3, 3], dtype=torch.int32),
),
original_row_node_ids=tensor([3, 4, 0, 5, 1]),
original_edge_ids=tensor([8, 0]),
original_column_node_ids=tensor([3, 4, 0, 5, 1]),
)],
node_features={'feat': tensor([[0.8672, 0.2276],
[0.5503, 0.8223],
[0.9634, 0.2294],
[0.5160, 0.2486],
[0.6172, 0.7865]])},
labels=tensor([1., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0.]),
input_nodes=tensor([3, 4, 0, 5, 1]),
indexes=tensor([0, 1, 2, 3, 0, 0, 1, 1, 2, 2, 3, 3]),
edge_features=[{'feat': tensor([[0.5123, 0.1709, 0.6150]])},
{'feat': tensor([[0.8972, 0.7511, 0.3617],
[0.5123, 0.1709, 0.6150]])}],
compacted_seeds=tensor([[0, 0],
[1, 0],
[1, 1],
[2, 1],
[0, 1],
[0, 3],
[1, 4],
[1, 1],
[1, 1],
[1, 3],
[2, 4],
[2, 0]]),
blocks=[Block(num_src_nodes=5, num_dst_nodes=5, num_edges=1),
Block(num_src_nodes=5, num_dst_nodes=5, num_edges=2)],
)"""
),
str(
"""MiniBatch(seeds=tensor([[5, 5],
[4, 5],
[5, 5],
[5, 5],
[4, 0],
[4, 0]]),
sampled_subgraphs=[SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 0, 1, 1], dtype=torch.int32),
indices=tensor([1], dtype=torch.int32),
),
original_row_node_ids=tensor([5, 4, 0]),
original_edge_ids=tensor([6]),
original_column_node_ids=tensor([5, 4, 0]),
),
SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 0, 1, 1], dtype=torch.int32),
indices=tensor([2], dtype=torch.int32),
),
original_row_node_ids=tensor([5, 4, 0]),
original_edge_ids=tensor([7]),
original_column_node_ids=tensor([5, 4, 0]),
)],
node_features={'feat': tensor([[0.5160, 0.2486],
[0.5503, 0.8223],
[0.9634, 0.2294]])},
labels=tensor([1., 1., 0., 0., 0., 0.]),
input_nodes=tensor([5, 4, 0]),
indexes=tensor([0, 1, 0, 0, 1, 1]),
edge_features=[{'feat': tensor([[0.4088, 0.8200, 0.1851]])},
{'feat': tensor([[0.0056, 0.9469, 0.4432]])}],
compacted_seeds=tensor([[0, 0],
[1, 0],
[0, 0],
[0, 0],
[1, 2],
[1, 2]]),
blocks=[Block(num_src_nodes=3, num_dst_nodes=3, num_edges=1),
Block(num_src_nodes=3, num_dst_nodes=3, num_edges=1)],
)"""
),
]
for step, data in enumerate(dataloader):
assert expected[step] == str(data), print(step, data)
def test_integration_node_classification():
torch.manual_seed(926)
indptr = torch.tensor([0, 0, 1, 3, 6, 8, 10])
indices = torch.tensor([5, 3, 3, 3, 3, 4, 4, 0, 5, 4])
seeds = torch.tensor([5, 1, 2, 4, 3, 0])
node_feature_data = torch.tensor(
[
[0.9634, 0.2294],
[0.6172, 0.7865],
[0.2109, 0.1089],
[0.8672, 0.2276],
[0.5503, 0.8223],
[0.5160, 0.2486],
]
)
edge_feature_data = torch.tensor(
[
[0.5123, 0.1709, 0.6150],
[0.1476, 0.1902, 0.1314],
[0.2582, 0.5203, 0.6228],
[0.3708, 0.7631, 0.2683],
[0.2126, 0.7878, 0.7225],
[0.7885, 0.3414, 0.5485],
[0.4088, 0.8200, 0.1851],
[0.0056, 0.9469, 0.4432],
[0.8972, 0.7511, 0.3617],
[0.5773, 0.2199, 0.3366],
]
)
item_set = gb.ItemSet(seeds, names="seeds")
graph = gb.fused_csc_sampling_graph(indptr, indices)
node_feature = gb.TorchBasedFeature(node_feature_data)
edge_feature = gb.TorchBasedFeature(edge_feature_data)
features = {
("node", None, "feat"): node_feature,
("edge", None, "feat"): edge_feature,
}
feature_store = gb.BasicFeatureStore(features)
datapipe = gb.ItemSampler(item_set, batch_size=2)
fanouts = torch.LongTensor([1])
datapipe = datapipe.sample_neighbor(graph, [fanouts, fanouts], replace=True)
datapipe = datapipe.fetch_feature(
feature_store, node_feature_keys=["feat"], edge_feature_keys=["feat"]
)
dataloader = gb.DataLoader(
datapipe,
)
expected = [
str(
"""MiniBatch(seeds=tensor([5, 1]),
sampled_subgraphs=[SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 1, 2], dtype=torch.int32),
indices=tensor([0, 0], dtype=torch.int32),
),
original_row_node_ids=tensor([5, 1]),
original_edge_ids=tensor([8, 0]),
original_column_node_ids=tensor([5, 1]),
),
SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 1, 2], dtype=torch.int32),
indices=tensor([0, 0], dtype=torch.int32),
),
original_row_node_ids=tensor([5, 1]),
original_edge_ids=tensor([8, 0]),
original_column_node_ids=tensor([5, 1]),
)],
node_features={'feat': tensor([[0.5160, 0.2486],
[0.6172, 0.7865]])},
labels=None,
input_nodes=tensor([5, 1]),
indexes=None,
edge_features=[{'feat': tensor([[0.8972, 0.7511, 0.3617],
[0.5123, 0.1709, 0.6150]])},
{'feat': tensor([[0.8972, 0.7511, 0.3617],
[0.5123, 0.1709, 0.6150]])}],
compacted_seeds=None,
blocks=[Block(num_src_nodes=2, num_dst_nodes=2, num_edges=2),
Block(num_src_nodes=2, num_dst_nodes=2, num_edges=2)],
)"""
),
str(
"""MiniBatch(seeds=tensor([2, 4]),
sampled_subgraphs=[SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 1, 2, 3], dtype=torch.int32),
indices=tensor([2, 1, 2], dtype=torch.int32),
),
original_row_node_ids=tensor([2, 4, 3]),
original_edge_ids=tensor([1, 6, 3]),
original_column_node_ids=tensor([2, 4, 3]),
),
SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 1, 2], dtype=torch.int32),
indices=tensor([2, 1], dtype=torch.int32),
),
original_row_node_ids=tensor([2, 4, 3]),
original_edge_ids=tensor([2, 6]),
original_column_node_ids=tensor([2, 4]),
)],
node_features={'feat': tensor([[0.2109, 0.1089],
[0.5503, 0.8223],
[0.8672, 0.2276]])},
labels=None,
input_nodes=tensor([2, 4, 3]),
indexes=None,
edge_features=[{'feat': tensor([[0.1476, 0.1902, 0.1314],
[0.4088, 0.8200, 0.1851],
[0.3708, 0.7631, 0.2683]])},
{'feat': tensor([[0.2582, 0.5203, 0.6228],
[0.4088, 0.8200, 0.1851]])}],
compacted_seeds=None,
blocks=[Block(num_src_nodes=3, num_dst_nodes=3, num_edges=3),
Block(num_src_nodes=3, num_dst_nodes=2, num_edges=2)],
)"""
),
str(
"""MiniBatch(seeds=tensor([3, 0]),
sampled_subgraphs=[SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 1, 1], dtype=torch.int32),
indices=tensor([0], dtype=torch.int32),
),
original_row_node_ids=tensor([3, 0]),
original_edge_ids=tensor([3]),
original_column_node_ids=tensor([3, 0]),
),
SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 1, 1], dtype=torch.int32),
indices=tensor([0], dtype=torch.int32),
),
original_row_node_ids=tensor([3, 0]),
original_edge_ids=tensor([3]),
original_column_node_ids=tensor([3, 0]),
)],
node_features={'feat': tensor([[0.8672, 0.2276],
[0.9634, 0.2294]])},
labels=None,
input_nodes=tensor([3, 0]),
indexes=None,
edge_features=[{'feat': tensor([[0.3708, 0.7631, 0.2683]])},
{'feat': tensor([[0.3708, 0.7631, 0.2683]])}],
compacted_seeds=None,
blocks=[Block(num_src_nodes=2, num_dst_nodes=2, num_edges=1),
Block(num_src_nodes=2, num_dst_nodes=2, num_edges=1)],
)"""
),
]
for step, data in enumerate(dataloader):
assert expected[step] == str(data), print(step, data)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,660 @@
import re
import dgl
import pytest
import torch
from dgl import graphbolt as gb
def test_ItemSet_names():
# ItemSet with single name.
item_set = gb.ItemSet(torch.arange(0, 5), names="seeds")
assert item_set.names == ("seeds",)
# ItemSet with multiple names.
item_set = gb.ItemSet(
(torch.arange(0, 5), torch.arange(5, 10)),
names=("seeds", "labels"),
)
assert item_set.names == ("seeds", "labels")
# ItemSet without name.
item_set = gb.ItemSet(torch.arange(0, 5))
assert item_set.names is None
# Integer-initiated ItemSet with excessive names.
with pytest.raises(
AssertionError,
match=re.escape("Number of items (1) and names (2) don't match."),
):
_ = gb.ItemSet(5, names=("seeds", "labels"))
# ItemSet with mismatched items and names.
with pytest.raises(
AssertionError,
match=re.escape("Number of items (1) and names (2) don't match."),
):
_ = gb.ItemSet(torch.arange(0, 5), names=("seeds", "labels"))
@pytest.mark.parametrize("dtype", [torch.int32, torch.int64])
def test_ItemSet_scalar_dtype(dtype):
item_set = gb.ItemSet(torch.tensor(5, dtype=dtype), names="seeds")
for i, item in enumerate(item_set):
assert i == item
assert item.dtype == dtype
assert item_set[2] == torch.tensor(2, dtype=dtype)
assert torch.equal(
item_set[slice(1, 4, 2)], torch.arange(1, 4, 2, dtype=dtype)
)
def test_ItemSet_length():
# Integer with valid length
num = 10
item_set = gb.ItemSet(num)
assert len(item_set) == 10
# Test __iter__() method. Same as below.
for i, item in enumerate(item_set):
assert i == item
# Single iterable with valid length.
ids = torch.arange(0, 5)
item_set = gb.ItemSet(ids)
assert len(item_set) == 5
for i, item in enumerate(item_set):
assert i == item.item()
# Tuple of iterables with valid length.
item_set = gb.ItemSet((torch.arange(0, 5), torch.arange(5, 10)))
assert len(item_set) == 5
for i, (item1, item2) in enumerate(item_set):
assert i == item1.item()
assert i + 5 == item2.item()
class InvalidLength:
def __iter__(self):
return iter([0, 1, 2])
# Single iterable with invalid length.
with pytest.raises(
TypeError, match="object of type 'InvalidLength' has no len()"
):
item_set = gb.ItemSet(InvalidLength())
# Tuple of iterables with invalid length.
with pytest.raises(
TypeError, match="object of type 'InvalidLength' has no len()"
):
item_set = gb.ItemSet((InvalidLength(), InvalidLength()))
def test_ItemSet_seed_nodes():
# Node IDs with tensor.
item_set = gb.ItemSet(torch.arange(0, 5), names="seeds")
assert item_set.names == ("seeds",)
# Iterating over ItemSet and indexing one by one.
for i, item in enumerate(item_set):
assert i == item.item()
assert i == item_set[i]
# Indexing with a slice.
assert torch.equal(item_set[::2], torch.tensor([0, 2, 4]))
# Indexing with an Iterable.
assert torch.equal(item_set[torch.arange(0, 5)], torch.arange(0, 5))
# Node IDs with single integer.
item_set = gb.ItemSet(5, names="seeds")
assert item_set.names == ("seeds",)
# Iterating over ItemSet and indexing one by one.
for i, item in enumerate(item_set):
assert i == item.item()
assert i == item_set[i]
# Indexing with a slice.
assert torch.equal(item_set[::2], torch.tensor([0, 2, 4]))
assert torch.equal(item_set[torch.arange(0, 5)], torch.arange(0, 5))
# Indexing with an integer.
assert item_set[0] == 0
assert item_set[-1] == 4
# Indexing that is out of range.
with pytest.raises(IndexError, match="ItemSet index out of range."):
_ = item_set[5]
with pytest.raises(IndexError, match="ItemSet index out of range."):
_ = item_set[-10]
# Indexing with invalid input type.
with pytest.raises(
TypeError,
match="ItemSet indices must be int, slice, or torch.Tensor, not <class 'float'>.",
):
_ = item_set[1.5]
def test_ItemSet_seed_nodes_labels():
# Node IDs and labels.
seed_nodes = torch.arange(0, 5)
labels = torch.randint(0, 3, (5,))
item_set = gb.ItemSet((seed_nodes, labels), names=("seeds", "labels"))
assert item_set.names == ("seeds", "labels")
# Iterating over ItemSet and indexing one by one.
for i, (seed_node, label) in enumerate(item_set):
assert seed_node == seed_nodes[i]
assert label == labels[i]
assert seed_node == item_set[i][0]
assert label == item_set[i][1]
# Indexing with a slice.
assert torch.equal(item_set[:][0], seed_nodes)
assert torch.equal(item_set[:][1], labels)
# Indexing with an Iterable.
assert torch.equal(item_set[torch.arange(0, 5)][0], seed_nodes)
assert torch.equal(item_set[torch.arange(0, 5)][1], labels)
def test_ItemSet_node_pairs():
# Node pairs.
node_pairs = torch.arange(0, 10).reshape(-1, 2)
item_set = gb.ItemSet(node_pairs, names="seeds")
assert item_set.names == ("seeds",)
# Iterating over ItemSet and indexing one by one.
for i, (src, dst) in enumerate(item_set):
assert node_pairs[i][0] == src
assert node_pairs[i][1] == dst
assert node_pairs[i][0] == item_set[i][0]
assert node_pairs[i][1] == item_set[i][1]
# Indexing with a slice.
assert torch.equal(item_set[:], node_pairs)
# Indexing with an Iterable.
assert torch.equal(item_set[torch.arange(0, 5)], node_pairs)
def test_ItemSet_node_pairs_labels():
# Node pairs and labels
node_pairs = torch.arange(0, 10).reshape(-1, 2)
labels = torch.randint(0, 3, (5,))
item_set = gb.ItemSet((node_pairs, labels), names=("seeds", "labels"))
assert item_set.names == ("seeds", "labels")
# Iterating over ItemSet and indexing one by one.
for i, (node_pair, label) in enumerate(item_set):
assert torch.equal(node_pairs[i], node_pair)
assert labels[i] == label
assert torch.equal(node_pairs[i], item_set[i][0])
assert labels[i] == item_set[i][1]
# Indexing with a slice.
assert torch.equal(item_set[:][0], node_pairs)
assert torch.equal(item_set[:][1], labels)
# Indexing with an Iterable.
assert torch.equal(item_set[torch.arange(0, 5)][0], node_pairs)
assert torch.equal(item_set[torch.arange(0, 5)][1], labels)
def test_ItemSet_node_pairs_labels_indexes():
# Node pairs and negative destinations.
node_pairs = torch.arange(0, 10).reshape(-1, 2)
labels = torch.tensor([1, 1, 0, 0, 0])
indexes = torch.tensor([0, 1, 0, 0, 1])
item_set = gb.ItemSet(
(node_pairs, labels, indexes), names=("seeds", "labels", "indexes")
)
assert item_set.names == ("seeds", "labels", "indexes")
# Iterating over ItemSet and indexing one by one.
for i, (node_pair, label, index) in enumerate(item_set):
assert torch.equal(node_pairs[i], node_pair)
assert torch.equal(labels[i], label)
assert torch.equal(indexes[i], index)
assert torch.equal(node_pairs[i], item_set[i][0])
assert torch.equal(labels[i], item_set[i][1])
assert torch.equal(indexes[i], item_set[i][2])
# Indexing with a slice.
assert torch.equal(item_set[:][0], node_pairs)
assert torch.equal(item_set[:][1], labels)
assert torch.equal(item_set[:][2], indexes)
# Indexing with an Iterable.
assert torch.equal(item_set[torch.arange(0, 5)][0], node_pairs)
assert torch.equal(item_set[torch.arange(0, 5)][1], labels)
assert torch.equal(item_set[torch.arange(0, 5)][2], indexes)
def test_ItemSet_graphs():
# Graphs.
graphs = [dgl.rand_graph(10, 20) for _ in range(5)]
item_set = gb.ItemSet(graphs)
assert item_set.names is None
# Iterating over ItemSet and indexing one by one.
for i, item in enumerate(item_set):
assert graphs[i] == item
assert graphs[i] == item_set[i]
# Indexing with a slice.
assert item_set[:] == graphs
def test_HeteroItemSet_names():
# HeteroItemSet with single name.
item_set = gb.HeteroItemSet(
{
"user": gb.ItemSet(torch.arange(0, 5), names="seeds"),
"item": gb.ItemSet(torch.arange(5, 10), names="seeds"),
}
)
assert item_set.names == ("seeds",)
# HeteroItemSet with multiple names.
item_set = gb.HeteroItemSet(
{
"user": gb.ItemSet(
(torch.arange(0, 5), torch.arange(5, 10)),
names=("seeds", "labels"),
),
"item": gb.ItemSet(
(torch.arange(5, 10), torch.arange(10, 15)),
names=("seeds", "labels"),
),
}
)
assert item_set.names == ("seeds", "labels")
# HeteroItemSet with no name.
item_set = gb.HeteroItemSet(
{
"user": gb.ItemSet(torch.arange(0, 5)),
"item": gb.ItemSet(torch.arange(5, 10)),
}
)
assert item_set.names is None
# HeteroItemSet with mismatched items and names.
with pytest.raises(
AssertionError,
match=re.escape("All itemsets must have the same names."),
):
_ = gb.HeteroItemSet(
{
"user": gb.ItemSet(
(torch.arange(0, 5), torch.arange(5, 10)),
names=("seeds", "labels"),
),
"item": gb.ItemSet((torch.arange(5, 10),), names=("seeds",)),
}
)
def test_HeteroItemSet_length():
# Single iterable with valid length.
user_ids = torch.arange(0, 5)
item_ids = torch.arange(0, 5)
item_set = gb.HeteroItemSet(
{
"user": gb.ItemSet(user_ids),
"item": gb.ItemSet(item_ids),
}
)
assert len(item_set) == len(user_ids) + len(item_ids)
# Tuple of iterables with valid length.
node_pairs_like = torch.arange(0, 10).reshape(-1, 2)
neg_dsts_like = torch.arange(10, 20).reshape(-1, 2)
node_pairs_follow = torch.arange(0, 10).reshape(-1, 2)
neg_dsts_follow = torch.arange(10, 20).reshape(-1, 2)
item_set = gb.HeteroItemSet(
{
"user:like:item": gb.ItemSet((node_pairs_like, neg_dsts_like)),
"user:follow:user": gb.ItemSet(
(node_pairs_follow, neg_dsts_follow)
),
}
)
assert len(item_set) == node_pairs_like.size(0) + node_pairs_follow.size(0)
class InvalidLength:
def __iter__(self):
return iter([0, 1, 2])
# Single iterable with invalid length.
with pytest.raises(
TypeError, match="object of type 'InvalidLength' has no len()"
):
item_set = gb.HeteroItemSet(
{
"user": gb.ItemSet(InvalidLength()),
"item": gb.ItemSet(InvalidLength()),
}
)
# Tuple of iterables with invalid length.
with pytest.raises(
TypeError, match="object of type 'InvalidLength' has no len()"
):
item_set = gb.HeteroItemSet(
{
"user:like:item": gb.ItemSet(
(InvalidLength(), InvalidLength())
),
"user:follow:user": gb.ItemSet(
(InvalidLength(), InvalidLength())
),
}
)
def test_HeteroItemSet_iteration_seed_nodes():
# Node IDs.
user_ids = torch.arange(0, 5)
item_ids = torch.arange(5, 10)
ids = {
"user": gb.ItemSet(user_ids, names="seeds"),
"item": gb.ItemSet(item_ids, names="seeds"),
}
chained_ids = []
for key, value in ids.items():
chained_ids += [(key, v) for v in value]
item_set = gb.HeteroItemSet(ids)
assert item_set.names == ("seeds",)
# Iterating over HeteroItemSet and indexing one by one.
for i, item in enumerate(item_set):
assert len(item) == 1
assert isinstance(item, dict)
assert chained_ids[i][0] in item
assert item[chained_ids[i][0]] == chained_ids[i][1]
assert item_set[i] == item
assert item_set[i - len(item_set)] == item
# Indexing all with a slice.
assert torch.equal(item_set[:]["user"], user_ids)
assert torch.equal(item_set[:]["item"], item_ids)
# Indexing partial with a slice.
partial_data = item_set[:3]
assert len(list(partial_data.keys())) == 1
assert torch.equal(partial_data["user"], user_ids[:3])
partial_data = item_set[7:]
assert len(list(partial_data.keys())) == 1
assert torch.equal(partial_data["item"], item_ids[2:])
partial_data = item_set[3:8:2]
assert len(list(partial_data.keys())) == 2
assert torch.equal(partial_data["user"], user_ids[3:-1:2])
assert torch.equal(partial_data["item"], item_ids[0:3:2])
# Indexing with an iterable of int.
partial_data = item_set[torch.tensor([1, 0, 4])]
assert len(list(partial_data.keys())) == 1
assert torch.equal(partial_data["user"], torch.tensor([1, 0, 4]))
partial_data = item_set[torch.tensor([9, 8, 5])]
assert len(list(partial_data.keys())) == 1
assert torch.equal(partial_data["item"], torch.tensor([9, 8, 5]))
partial_data = item_set[torch.tensor([8, 1, 0, 9, 7, 5])]
assert len(list(partial_data.keys())) == 2
assert torch.equal(partial_data["user"], torch.tensor([1, 0]))
assert torch.equal(partial_data["item"], torch.tensor([8, 9, 7, 5]))
# Exception cases.
with pytest.raises(
AssertionError, match="Start must be smaller than stop."
):
_ = item_set[5:3]
with pytest.raises(
AssertionError, match="Start must be smaller than stop."
):
_ = item_set[-1:3]
with pytest.raises(IndexError, match="HeteroItemSet index out of range."):
_ = item_set[20]
with pytest.raises(IndexError, match="HeteroItemSet index out of range."):
_ = item_set[-20]
with pytest.raises(
TypeError,
match="HeteroItemSet indices must be int, slice, or iterable of int, not <class 'float'>.",
):
_ = item_set[1.5]
def test_HeteroItemSet_iteration_seed_nodes_labels():
# Node IDs and labels.
user_ids = torch.arange(0, 5)
user_labels = torch.randint(0, 3, (5,))
item_ids = torch.arange(5, 10)
item_labels = torch.randint(0, 3, (5,))
ids_labels = {
"user": gb.ItemSet((user_ids, user_labels), names=("seeds", "labels")),
"item": gb.ItemSet((item_ids, item_labels), names=("seeds", "labels")),
}
chained_ids = []
for key, value in ids_labels.items():
chained_ids += [(key, v) for v in value]
item_set = gb.HeteroItemSet(ids_labels)
assert item_set.names == ("seeds", "labels")
# Iterating over HeteroItemSet and indexing one by one.
for i, item in enumerate(item_set):
assert len(item) == 1
assert isinstance(item, dict)
assert chained_ids[i][0] in item
assert item[chained_ids[i][0]] == chained_ids[i][1]
assert item_set[i] == item
# Indexing with a slice.
assert torch.equal(item_set[:]["user"][0], user_ids)
assert torch.equal(item_set[:]["user"][1], user_labels)
assert torch.equal(item_set[:]["item"][0], item_ids)
assert torch.equal(item_set[:]["item"][1], item_labels)
def test_HeteroItemSet_iteration_node_pairs():
# Node pairs.
node_pairs = torch.arange(0, 10).reshape(-1, 2)
node_pairs_dict = {
"user:like:item": gb.ItemSet(node_pairs, names="seeds"),
"user:follow:user": gb.ItemSet(node_pairs, names="seeds"),
}
expected_data = []
for key, value in node_pairs_dict.items():
expected_data += [(key, v) for v in value]
item_set = gb.HeteroItemSet(node_pairs_dict)
assert item_set.names == ("seeds",)
# Iterating over HeteroItemSet and indexing one by one.
for i, item in enumerate(item_set):
assert len(item) == 1
assert isinstance(item, dict)
assert expected_data[i][0] in item
assert torch.equal(item[expected_data[i][0]], expected_data[i][1])
assert item_set[i].keys() == item.keys()
key = list(item.keys())[0]
assert torch.equal(item_set[i][key], item[key])
# Indexing with a slice.
assert torch.equal(item_set[:]["user:like:item"], node_pairs)
assert torch.equal(item_set[:]["user:follow:user"], node_pairs)
def test_HeteroItemSet_iteration_node_pairs_labels():
# Node pairs and labels
node_pairs = torch.arange(0, 10).reshape(-1, 2)
labels = torch.randint(0, 3, (5,))
node_pairs_labels = {
"user:like:item": gb.ItemSet(
(node_pairs, labels), names=("seeds", "labels")
),
"user:follow:user": gb.ItemSet(
(node_pairs, labels), names=("seeds", "labels")
),
}
expected_data = []
for key, value in node_pairs_labels.items():
expected_data += [(key, v) for v in value]
item_set = gb.HeteroItemSet(node_pairs_labels)
assert item_set.names == ("seeds", "labels")
# Iterating over HeteroItemSet and indexing one by one.
for i, item in enumerate(item_set):
assert len(item) == 1
assert isinstance(item, dict)
key, value = expected_data[i]
assert key in item
assert torch.equal(item[key][0], value[0])
assert item[key][1] == value[1]
assert item_set[i].keys() == item.keys()
key = list(item.keys())[0]
assert torch.equal(item_set[i][key][0], item[key][0])
assert torch.equal(item_set[i][key][1], item[key][1])
# Indexing with a slice.
assert torch.equal(item_set[:]["user:like:item"][0], node_pairs)
assert torch.equal(item_set[:]["user:like:item"][1], labels)
assert torch.equal(item_set[:]["user:follow:user"][0], node_pairs)
assert torch.equal(item_set[:]["user:follow:user"][1], labels)
def test_HeteroItemSet_iteration_node_pairs_labels_indexes():
# Node pairs and negative destinations.
node_pairs = torch.arange(0, 10).reshape(-1, 2)
labels = torch.tensor([1, 1, 0, 0, 0])
indexes = torch.tensor([0, 1, 0, 0, 1])
node_pairs_neg_dsts = {
"user:like:item": gb.ItemSet(
(node_pairs, labels, indexes), names=("seeds", "labels", "indexes")
),
"user:follow:user": gb.ItemSet(
(node_pairs, labels, indexes), names=("seeds", "labels", "indexes")
),
}
expected_data = []
for key, value in node_pairs_neg_dsts.items():
expected_data += [(key, v) for v in value]
item_set = gb.HeteroItemSet(node_pairs_neg_dsts)
assert item_set.names == ("seeds", "labels", "indexes")
# Iterating over HeteroItemSet and indexing one by one.
for i, item in enumerate(item_set):
assert len(item) == 1
assert isinstance(item, dict)
key, value = expected_data[i]
assert key in item
assert torch.equal(item[key][0], value[0])
assert torch.equal(item[key][1], value[1])
assert torch.equal(item[key][2], value[2])
assert item_set[i].keys() == item.keys()
key = list(item.keys())[0]
assert torch.equal(item_set[i][key][0], item[key][0])
assert torch.equal(item_set[i][key][1], item[key][1])
assert torch.equal(item_set[i][key][2], item[key][2])
# Indexing with a slice.
assert torch.equal(item_set[:]["user:like:item"][0], node_pairs)
assert torch.equal(item_set[:]["user:like:item"][1], labels)
assert torch.equal(item_set[:]["user:like:item"][2], indexes)
assert torch.equal(item_set[:]["user:follow:user"][0], node_pairs)
assert torch.equal(item_set[:]["user:follow:user"][1], labels)
assert torch.equal(item_set[:]["user:follow:user"][2], indexes)
def test_ItemSet_repr():
# ItemSet with single name.
item_set = gb.ItemSet(torch.arange(0, 5), names="seeds")
expected_str = (
"ItemSet(\n"
" items=(tensor([0, 1, 2, 3, 4]),),\n"
" names=('seeds',),\n"
")"
)
assert str(item_set) == expected_str, item_set
# ItemSet with multiple names.
item_set = gb.ItemSet(
(torch.arange(0, 5), torch.arange(5, 10)),
names=("seeds", "labels"),
)
expected_str = (
"ItemSet(\n"
" items=(tensor([0, 1, 2, 3, 4]), tensor([5, 6, 7, 8, 9])),\n"
" names=('seeds', 'labels'),\n"
")"
)
assert str(item_set) == expected_str, item_set
def test_HeteroItemSet_repr():
# HeteroItemSet with single name.
item_set = gb.HeteroItemSet(
{
"user": gb.ItemSet(torch.arange(0, 5), names="seeds"),
"item": gb.ItemSet(torch.arange(5, 10), names="seeds"),
}
)
expected_str = (
"HeteroItemSet(\n"
" itemsets={'user': ItemSet(\n"
" items=(tensor([0, 1, 2, 3, 4]),),\n"
" names=('seeds',),\n"
" ), 'item': ItemSet(\n"
" items=(tensor([5, 6, 7, 8, 9]),),\n"
" names=('seeds',),\n"
" )},\n"
" names=('seeds',),\n"
")"
)
assert str(item_set) == expected_str, item_set
# HeteroItemSet with multiple names.
item_set = gb.HeteroItemSet(
{
"user": gb.ItemSet(
(torch.arange(0, 5), torch.arange(5, 10)),
names=("seeds", "labels"),
),
"item": gb.ItemSet(
(torch.arange(5, 10), torch.arange(10, 15)),
names=("seeds", "labels"),
),
}
)
expected_str = (
"HeteroItemSet(\n"
" itemsets={'user': ItemSet(\n"
" items=(tensor([0, 1, 2, 3, 4]), tensor([5, 6, 7, 8, 9])),\n"
" names=('seeds', 'labels'),\n"
" ), 'item': ItemSet(\n"
" items=(tensor([5, 6, 7, 8, 9]), tensor([10, 11, 12, 13, 14])),\n"
" names=('seeds', 'labels'),\n"
" )},\n"
" names=('seeds', 'labels'),\n"
")"
)
assert str(item_set) == expected_str, item_set
def test_deprecation_alias():
"""Test `ItemSetDict` as the alias for `HeteroItemSet`."""
user_ids = torch.arange(0, 5)
item_ids = torch.arange(5, 10)
ids = {
"user": gb.ItemSet(user_ids, names="seeds"),
"item": gb.ItemSet(item_ids, names="seeds"),
}
with pytest.warns(
DeprecationWarning,
match="ItemSetDict is deprecated and will be removed in the future. Please use HeteroItemSet instead.",
):
item_set_dict = gb.ItemSetDict(ids)
hetero_item_set = gb.HeteroItemSet(ids)
assert len(item_set_dict) == len(hetero_item_set)
assert item_set_dict.names == hetero_item_set.names
assert item_set_dict._keys == hetero_item_set._keys
assert torch.equal(item_set_dict._offsets, hetero_item_set._offsets)
assert (
repr(item_set_dict)[len("ItemSetDict") :]
== repr(hetero_item_set)[len("HeteroItemSet") :]
)
# Indexing all with a slice.
assert torch.equal(item_set_dict[:]["user"], hetero_item_set[:]["user"])
assert torch.equal(item_set_dict[:]["item"], hetero_item_set[:]["item"])
# Indexing partial with a slice.
partial_data = item_set_dict[:3]
assert len(list(partial_data.keys())) == 1
assert torch.equal(partial_data["user"], hetero_item_set[:3]["user"])
partial_data = item_set_dict[7:]
assert len(list(partial_data.keys())) == 1
assert torch.equal(partial_data["item"], hetero_item_set[7:]["item"])
partial_data = item_set_dict[3:8:2]
assert len(list(partial_data.keys())) == 2
assert torch.equal(partial_data["user"], hetero_item_set[3:8:2]["user"])
assert torch.equal(partial_data["item"], hetero_item_set[3:8:2]["item"])
# Indexing with an iterable of int.
partial_data = item_set_dict[torch.tensor([1, 0, 4])]
assert len(list(partial_data.keys())) == 1
assert torch.equal(partial_data["user"], hetero_item_set[1, 0, 4]["user"])
partial_data = item_set_dict[torch.tensor([9, 8, 5])]
assert len(list(partial_data.keys())) == 1
assert torch.equal(partial_data["item"], hetero_item_set[9, 8, 5]["item"])
partial_data = item_set_dict[torch.tensor([8, 1, 0, 9, 7, 5])]
assert len(list(partial_data.keys())) == 2
assert torch.equal(partial_data["user"], hetero_item_set[1, 0]["user"])
assert torch.equal(
partial_data["item"], hetero_item_set[8, 9, 7, 5]["item"]
)
@@ -0,0 +1,755 @@
import dgl
import dgl.graphbolt as gb
import pytest
import torch
relation = "A:r:B"
reverse_relation = "B:rr:A"
@pytest.mark.parametrize("indptr_dtype", [torch.int32, torch.int64])
@pytest.mark.parametrize("indices_dtype", [torch.int32, torch.int64])
def test_minibatch_representation_homo(indptr_dtype, indices_dtype):
seeds = torch.tensor([10, 11])
csc_formats = [
gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 3, 5, 6], dtype=indptr_dtype),
indices=torch.tensor([0, 1, 2, 2, 1, 2], dtype=indices_dtype),
),
gb.CSCFormatBase(
indptr=torch.tensor([0, 2, 3], dtype=indptr_dtype),
indices=torch.tensor([1, 2, 0], dtype=indices_dtype),
),
]
original_column_node_ids = [
torch.tensor([10, 11, 12, 13]),
torch.tensor([10, 11]),
]
original_row_node_ids = [
torch.tensor([10, 11, 12, 13]),
torch.tensor([10, 11, 12]),
]
original_edge_ids = [
torch.tensor([19, 20, 21, 22, 25, 30]),
torch.tensor([10, 15, 17]),
]
node_features = {"x": torch.tensor([5, 0, 2, 1])}
edge_features = [
{"x": torch.tensor([9, 0, 1, 1, 7, 4])},
{"x": torch.tensor([0, 2, 2])},
]
subgraphs = []
for i in range(2):
subgraphs.append(
gb.SampledSubgraphImpl(
sampled_csc=csc_formats[i],
original_column_node_ids=original_column_node_ids[i],
original_row_node_ids=original_row_node_ids[i],
original_edge_ids=original_edge_ids[i],
)
)
input_nodes = torch.tensor([8, 1, 6, 5, 9, 0, 2, 4])
compacted_seeds = torch.tensor([0, 1])
labels = torch.tensor([1.0, 2.0])
# Test minibatch without data.
minibatch = gb.MiniBatch()
expect_result = str(
"""MiniBatch(seeds=None,
sampled_subgraphs=None,
node_features=None,
labels=None,
input_nodes=None,
indexes=None,
edge_features=None,
compacted_seeds=None,
blocks=None,
)"""
)
result = str(minibatch)
assert result == expect_result, print(expect_result, result)
# Test minibatch with all attributes.
minibatch = gb.MiniBatch(
seeds=seeds,
sampled_subgraphs=subgraphs,
labels=labels,
node_features=node_features,
edge_features=edge_features,
compacted_seeds=compacted_seeds,
input_nodes=input_nodes,
)
expect_result = str(
"""MiniBatch(seeds=tensor([10, 11]),
sampled_subgraphs=[SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 1, 3, 5, 6], dtype=torch.int32),
indices=tensor([0, 1, 2, 2, 1, 2], dtype=torch.int32),
),
original_row_node_ids=tensor([10, 11, 12, 13]),
original_edge_ids=tensor([19, 20, 21, 22, 25, 30]),
original_column_node_ids=tensor([10, 11, 12, 13]),
),
SampledSubgraphImpl(sampled_csc=CSCFormatBase(indptr=tensor([0, 2, 3], dtype=torch.int32),
indices=tensor([1, 2, 0], dtype=torch.int32),
),
original_row_node_ids=tensor([10, 11, 12]),
original_edge_ids=tensor([10, 15, 17]),
original_column_node_ids=tensor([10, 11]),
)],
node_features={'x': tensor([5, 0, 2, 1])},
labels=tensor([1., 2.]),
input_nodes=tensor([8, 1, 6, 5, 9, 0, 2, 4]),
indexes=None,
edge_features=[{'x': tensor([9, 0, 1, 1, 7, 4])},
{'x': tensor([0, 2, 2])}],
compacted_seeds=tensor([0, 1]),
blocks=[Block(num_src_nodes=4, num_dst_nodes=4, num_edges=6),
Block(num_src_nodes=3, num_dst_nodes=2, num_edges=3)],
)"""
)
result = str(minibatch)
assert result == expect_result, print(expect_result, result)
@pytest.mark.parametrize("indptr_dtype", [torch.int32, torch.int64])
@pytest.mark.parametrize("indices_dtype", [torch.int32, torch.int64])
def test_minibatch_representation_hetero(indptr_dtype, indices_dtype):
seeds = {relation: torch.tensor([10, 11])}
csc_formats = [
{
relation: gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 2, 3], dtype=indptr_dtype),
indices=torch.tensor([0, 1, 1], dtype=indices_dtype),
),
reverse_relation: gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 0, 1, 2], dtype=indptr_dtype),
indices=torch.tensor([1, 0], dtype=indices_dtype),
),
},
{
relation: gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 2], dtype=indptr_dtype),
indices=torch.tensor([1, 0], dtype=indices_dtype),
),
reverse_relation: gb.CSCFormatBase(
indptr=torch.tensor([0, 2], dtype=indptr_dtype),
indices=torch.tensor([1, 0], dtype=indices_dtype),
),
},
]
original_column_node_ids = [
{"B": torch.tensor([10, 11, 12]), "A": torch.tensor([5, 7, 9, 11])},
{"B": torch.tensor([10, 11]), "A": torch.tensor([5])},
]
original_row_node_ids = [
{
"A": torch.tensor([5, 7, 9, 11]),
"B": torch.tensor([10, 11, 12]),
},
{
"A": torch.tensor([5, 7]),
"B": torch.tensor([10, 11]),
},
]
original_edge_ids = [
{
relation: torch.tensor([19, 20, 21]),
reverse_relation: torch.tensor([23, 26]),
},
{relation: torch.tensor([10, 12])},
]
node_features = {
("A", "x"): torch.tensor([6, 4, 0, 1]),
}
edge_features = [
{(relation, "x"): torch.tensor([4, 2, 4])},
{(relation, "x"): torch.tensor([0, 6])},
]
subgraphs = []
for i in range(2):
subgraphs.append(
gb.SampledSubgraphImpl(
sampled_csc=csc_formats[i],
original_column_node_ids=original_column_node_ids[i],
original_row_node_ids=original_row_node_ids[i],
original_edge_ids=original_edge_ids[i],
)
)
compacted_seeds = {relation: torch.tensor([0, 1])}
# Test minibatch with all attributes.
minibatch = gb.MiniBatch(
seeds=seeds,
sampled_subgraphs=subgraphs,
node_features=node_features,
edge_features=edge_features,
labels={"B": torch.tensor([2, 5])},
compacted_seeds=compacted_seeds,
input_nodes={
"A": torch.tensor([5, 7, 9, 11]),
"B": torch.tensor([10, 11, 12]),
},
)
expect_result = str(
"""MiniBatch(seeds={'A:r:B': tensor([10, 11])},
sampled_subgraphs=[SampledSubgraphImpl(sampled_csc={'A:r:B': CSCFormatBase(indptr=tensor([0, 1, 2, 3], dtype=torch.int32),
indices=tensor([0, 1, 1], dtype=torch.int32),
), 'B:rr:A': CSCFormatBase(indptr=tensor([0, 0, 0, 1, 2], dtype=torch.int32),
indices=tensor([1, 0], dtype=torch.int32),
)},
original_row_node_ids={'A': tensor([ 5, 7, 9, 11]), 'B': tensor([10, 11, 12])},
original_edge_ids={'A:r:B': tensor([19, 20, 21]), 'B:rr:A': tensor([23, 26])},
original_column_node_ids={'B': tensor([10, 11, 12]), 'A': tensor([ 5, 7, 9, 11])},
),
SampledSubgraphImpl(sampled_csc={'A:r:B': CSCFormatBase(indptr=tensor([0, 1, 2], dtype=torch.int32),
indices=tensor([1, 0], dtype=torch.int32),
), 'B:rr:A': CSCFormatBase(indptr=tensor([0, 2], dtype=torch.int32),
indices=tensor([1, 0], dtype=torch.int32),
)},
original_row_node_ids={'A': tensor([5, 7]), 'B': tensor([10, 11])},
original_edge_ids={'A:r:B': tensor([10, 12])},
original_column_node_ids={'B': tensor([10, 11]), 'A': tensor([5])},
)],
node_features={('A', 'x'): tensor([6, 4, 0, 1])},
labels={'B': tensor([2, 5])},
input_nodes={'A': tensor([ 5, 7, 9, 11]), 'B': tensor([10, 11, 12])},
indexes=None,
edge_features=[{('A:r:B', 'x'): tensor([4, 2, 4])},
{('A:r:B', 'x'): tensor([0, 6])}],
compacted_seeds={'A:r:B': tensor([0, 1])},
blocks=[Block(num_src_nodes={'A': 4, 'B': 3},
num_dst_nodes={'A': 4, 'B': 3},
num_edges={('A', 'r', 'B'): 3, ('B', 'rr', 'A'): 2},
metagraph=[('A', 'B', 'r'), ('B', 'A', 'rr')]),
Block(num_src_nodes={'A': 2, 'B': 2},
num_dst_nodes={'A': 1, 'B': 2},
num_edges={('A', 'r', 'B'): 2, ('B', 'rr', 'A'): 2},
metagraph=[('A', 'B', 'r'), ('B', 'A', 'rr')])],
)"""
)
result = str(minibatch)
assert result == expect_result, print(result)
@pytest.mark.parametrize("indptr_dtype", [torch.int32, torch.int64])
@pytest.mark.parametrize("indices_dtype", [torch.int32, torch.int64])
def test_get_dgl_blocks_homo(indptr_dtype, indices_dtype):
csc_formats = [
gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 3, 5, 6], dtype=indptr_dtype),
indices=torch.tensor([0, 1, 2, 2, 1, 2], dtype=indices_dtype),
),
gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 3], dtype=indptr_dtype),
indices=torch.tensor([0, 1, 2], dtype=indices_dtype),
),
]
original_column_node_ids = [
torch.tensor([10, 11, 12, 13]),
torch.tensor([10, 11]),
]
original_row_node_ids = [
torch.tensor([10, 11, 12, 13]),
torch.tensor([10, 11, 12]),
]
original_edge_ids = [
torch.tensor([19, 20, 21, 22, 25, 30]),
torch.tensor([10, 15, 17]),
]
subgraphs = []
for i in range(2):
subgraphs.append(
gb.SampledSubgraphImpl(
sampled_csc=csc_formats[i],
original_column_node_ids=original_column_node_ids[i],
original_row_node_ids=original_row_node_ids[i],
original_edge_ids=original_edge_ids[i],
)
)
# Test minibatch with all attributes.
minibatch = gb.MiniBatch(
sampled_subgraphs=subgraphs,
)
dgl_blocks = minibatch.blocks
expect_result = str(
"""[Block(num_src_nodes=4, num_dst_nodes=4, num_edges=6), Block(num_src_nodes=3, num_dst_nodes=2, num_edges=3)]"""
)
result = str(dgl_blocks)
assert result == expect_result
def test_get_dgl_blocks_hetero():
csc_formats = [
{
relation: gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 2, 3]),
indices=torch.tensor([0, 1, 1]),
),
reverse_relation: gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 0, 1, 2]),
indices=torch.tensor([1, 0]),
),
},
{
relation: gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 2]), indices=torch.tensor([1, 0])
),
reverse_relation: gb.CSCFormatBase(
indptr=torch.tensor([0, 1]),
indices=torch.tensor([1]),
),
},
]
original_column_node_ids = [
{"B": torch.tensor([10, 11, 12]), "A": torch.tensor([5, 7, 9, 11])},
{"B": torch.tensor([10, 11]), "A": torch.tensor([5])},
]
original_row_node_ids = [
{
"A": torch.tensor([5, 7, 9, 11]),
"B": torch.tensor([10, 11, 12]),
},
{
"A": torch.tensor([5, 7]),
"B": torch.tensor([10, 11]),
},
]
original_edge_ids = [
{
relation: torch.tensor([19, 20, 21]),
reverse_relation: torch.tensor([23, 26]),
},
{relation: torch.tensor([10, 12])},
]
subgraphs = []
for i in range(2):
subgraphs.append(
gb.SampledSubgraphImpl(
sampled_csc=csc_formats[i],
original_column_node_ids=original_column_node_ids[i],
original_row_node_ids=original_row_node_ids[i],
original_edge_ids=original_edge_ids[i],
)
)
# Test minibatch with all attributes.
minibatch = gb.MiniBatch(
sampled_subgraphs=subgraphs,
)
dgl_blocks = minibatch.blocks
expect_result = str(
"""[Block(num_src_nodes={'A': 4, 'B': 3},
num_dst_nodes={'A': 4, 'B': 3},
num_edges={('A', 'r', 'B'): 3, ('B', 'rr', 'A'): 2},
metagraph=[('A', 'B', 'r'), ('B', 'A', 'rr')]), Block(num_src_nodes={'A': 2, 'B': 2},
num_dst_nodes={'A': 1, 'B': 2},
num_edges={('A', 'r', 'B'): 2, ('B', 'rr', 'A'): 1},
metagraph=[('A', 'B', 'r'), ('B', 'A', 'rr')])]"""
)
result = str(dgl_blocks)
assert result == expect_result
def test_get_dgl_blocks_hetero_partial_empty_edges():
hg = dgl.heterograph(
{
("n1", "e1", "n1"): ([0, 1, 1], [1, 2, 0]),
("n1", "e2", "n2"): ([0, 1, 2], [1, 0, 2]),
}
)
gb_g = gb.from_dglgraph(hg, is_homogeneous=False)
train_set = gb.HeteroItemSet(
{"n1:e2:n2": gb.ItemSet(torch.LongTensor([[0, 1]]), names="seeds")}
)
datapipe = gb.ItemSampler(train_set, batch_size=1)
datapipe = datapipe.sample_neighbor(gb_g, fanouts=[-1, -1])
dataloader = gb.DataLoader(datapipe)
blocks_str = str(next(iter(dataloader)).blocks)
expected_str = """[Block(num_src_nodes={'n1': 2, 'n2': 0},
num_dst_nodes={'n1': 2, 'n2': 0},
num_edges={('n1', 'e1', 'n1'): 2, ('n1', 'e2', 'n2'): 0},
metagraph=[('n1', 'n1', 'e1'), ('n1', 'n2', 'e2')]), Block(num_src_nodes={'n1': 2, 'n2': 0},
num_dst_nodes={'n1': 1, 'n2': 1},
num_edges={('n1', 'e1', 'n1'): 1, ('n1', 'e2', 'n2'): 1},
metagraph=[('n1', 'n1', 'e1'), ('n1', 'n2', 'e2')])]"""
assert expected_str == blocks_str
def test_get_dgl_blocks_hetero_empty_edges():
hg = dgl.heterograph(
{
("n3", "e1", "n1"): ([0, 1, 1], [1, 2, 0]),
("n3", "e2", "n2"): ([0, 1, 2], [1, 0, 2]),
}
)
gb_g = gb.from_dglgraph(hg, is_homogeneous=False)
train_set = gb.HeteroItemSet(
{"n3:e1:n1": gb.ItemSet(torch.LongTensor([[2, 1]]), names="seeds")}
)
datapipe = gb.ItemSampler(train_set, batch_size=1)
datapipe = datapipe.sample_neighbor(gb_g, fanouts=[-1, -1])
dataloader = gb.DataLoader(datapipe)
blocks_str = str(next(iter(dataloader)).blocks)
expected_str = """[Block(num_src_nodes={'n1': 0, 'n2': 0, 'n3': 2},
num_dst_nodes={'n1': 0, 'n2': 0, 'n3': 2},
num_edges={('n3', 'e1', 'n1'): 0, ('n3', 'e2', 'n2'): 0},
metagraph=[('n3', 'n1', 'e1'), ('n3', 'n2', 'e2')]), Block(num_src_nodes={'n1': 0, 'n2': 0, 'n3': 2},
num_dst_nodes={'n1': 1, 'n2': 0, 'n3': 1},
num_edges={('n3', 'e1', 'n1'): 1, ('n3', 'e2', 'n2'): 0},
metagraph=[('n3', 'n1', 'e1'), ('n3', 'n2', 'e2')])]"""
assert expected_str == blocks_str
def test_get_dgl_blocks_homo_empty_edges():
g = dgl.graph(([2, 3, 4], [3, 4, 5]))
gb_g = gb.from_dglgraph(g, is_homogeneous=True)
train_set = gb.ItemSet(torch.LongTensor([[0, 1]]), names="seeds")
datapipe = gb.ItemSampler(train_set, batch_size=1)
datapipe = datapipe.sample_neighbor(gb_g, fanouts=[-1, -1])
dataloader = gb.DataLoader(datapipe)
blocks_str = str(next(iter(dataloader)).blocks)
expected_str = "[Block(num_src_nodes=2, num_dst_nodes=2, num_edges=0), Block(num_src_nodes=2, num_dst_nodes=2, num_edges=0)]"
assert expected_str == blocks_str
def test_seeds_ntype_being_passed():
hg = dgl.heterograph({("n1", "e1", "n2"): ([0, 1, 2], [2, 0, 1])})
gb_g = gb.from_dglgraph(hg, is_homogeneous=False)
train_set = gb.HeteroItemSet(
{"n2": gb.ItemSet(torch.LongTensor([0, 1]), names="seeds")}
)
datapipe = gb.ItemSampler(train_set, batch_size=2)
datapipe = datapipe.sample_neighbor(gb_g, [-1, -1, -1])
dataloader = gb.DataLoader(datapipe)
blocks = next(iter(dataloader)).blocks
for block in blocks:
assert "n2" in block.srctypes
def create_homo_minibatch():
csc_formats = [
gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 3, 5, 6]),
indices=torch.tensor([0, 1, 2, 2, 1, 2]),
),
gb.CSCFormatBase(
indptr=torch.tensor([0, 2, 3]),
indices=torch.tensor([1, 2, 0]),
),
]
original_column_node_ids = [
torch.tensor([10, 11, 12, 13]),
torch.tensor([10, 11]),
]
original_row_node_ids = [
torch.tensor([10, 11, 12, 13]),
torch.tensor([10, 11, 12]),
]
original_edge_ids = [
torch.tensor([19, 20, 21, 22, 25, 30]),
torch.tensor([10, 15, 17]),
]
node_features = {"x": torch.randint(0, 10, (4,))}
edge_features = [
{"x": torch.randint(0, 10, (6,))},
{"x": torch.randint(0, 10, (3,))},
]
subgraphs = []
for i in range(2):
subgraphs.append(
gb.SampledSubgraphImpl(
sampled_csc=csc_formats[i],
original_column_node_ids=original_column_node_ids[i],
original_row_node_ids=original_row_node_ids[i],
original_edge_ids=original_edge_ids[i],
)
)
return gb.MiniBatch(
sampled_subgraphs=subgraphs,
node_features=node_features,
edge_features=edge_features,
input_nodes=torch.tensor([10, 11, 12, 13]),
)
def create_hetero_minibatch():
sampled_csc = [
{
relation: gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 2, 3]),
indices=torch.tensor([0, 1, 1]),
),
reverse_relation: gb.CSCFormatBase(
indptr=torch.tensor([0, 0, 0, 1, 2]),
indices=torch.tensor([1, 0]),
),
},
{
relation: gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 2]), indices=torch.tensor([1, 0])
)
},
]
original_column_node_ids = [
{"B": torch.tensor([10, 11, 12]), "A": torch.tensor([5, 7, 9, 11])},
{"B": torch.tensor([10, 11])},
]
original_row_node_ids = [
{
"A": torch.tensor([5, 7, 9, 11]),
"B": torch.tensor([10, 11, 12]),
},
{
"A": torch.tensor([5, 7]),
"B": torch.tensor([10, 11]),
},
]
original_edge_ids = [
{
relation: torch.tensor([19, 20, 21]),
reverse_relation: torch.tensor([23, 26]),
},
{relation: torch.tensor([10, 12])},
]
node_features = {
("A", "x"): torch.randint(0, 10, (4,)),
}
edge_features = [
{(relation, "x"): torch.randint(0, 10, (3,))},
{(relation, "x"): torch.randint(0, 10, (2,))},
]
subgraphs = []
for i in range(2):
subgraphs.append(
gb.SampledSubgraphImpl(
sampled_csc=sampled_csc[i],
original_column_node_ids=original_column_node_ids[i],
original_row_node_ids=original_row_node_ids[i],
original_edge_ids=original_edge_ids[i],
)
)
return gb.MiniBatch(
sampled_subgraphs=subgraphs,
node_features=node_features,
edge_features=edge_features,
input_nodes={
"A": torch.tensor([5, 7, 9, 11]),
"B": torch.tensor([10, 11, 12]),
},
)
def check_dgl_blocks_hetero(minibatch, blocks):
etype = gb.etype_str_to_tuple(relation)
sampled_csc = [
subgraph.sampled_csc for subgraph in minibatch.sampled_subgraphs
]
original_edge_ids = [
subgraph.original_edge_ids for subgraph in minibatch.sampled_subgraphs
]
original_row_node_ids = [
subgraph.original_row_node_ids
for subgraph in minibatch.sampled_subgraphs
]
for i, block in enumerate(blocks):
edges = block.edges(etype=etype)
dst_ndoes = torch.arange(
0, len(sampled_csc[i][relation].indptr) - 1
).repeat_interleave(sampled_csc[i][relation].indptr.diff())
assert torch.equal(edges[0], sampled_csc[i][relation].indices)
assert torch.equal(edges[1], dst_ndoes)
assert torch.equal(
block.edges[etype].data[dgl.EID], original_edge_ids[i][relation]
)
edges = blocks[0].edges(etype=gb.etype_str_to_tuple(reverse_relation))
dst_ndoes = torch.arange(
0, len(sampled_csc[0][reverse_relation].indptr) - 1
).repeat_interleave(sampled_csc[0][reverse_relation].indptr.diff())
assert torch.equal(edges[0], sampled_csc[0][reverse_relation].indices)
assert torch.equal(edges[1], dst_ndoes)
assert torch.equal(
blocks[0].srcdata[dgl.NID]["A"], original_row_node_ids[0]["A"]
)
assert torch.equal(
blocks[0].srcdata[dgl.NID]["B"], original_row_node_ids[0]["B"]
)
def check_dgl_blocks_homo(minibatch, blocks):
sampled_csc = [
subgraph.sampled_csc for subgraph in minibatch.sampled_subgraphs
]
original_edge_ids = [
subgraph.original_edge_ids for subgraph in minibatch.sampled_subgraphs
]
original_row_node_ids = [
subgraph.original_row_node_ids
for subgraph in minibatch.sampled_subgraphs
]
for i, block in enumerate(blocks):
dst_ndoes = torch.arange(
0, len(sampled_csc[i].indptr) - 1
).repeat_interleave(sampled_csc[i].indptr.diff())
assert torch.equal(block.edges()[0], sampled_csc[i].indices)
assert torch.equal(block.edges()[1], dst_ndoes)
assert torch.equal(block.edata[dgl.EID], original_edge_ids[i])
assert torch.equal(blocks[0].srcdata[dgl.NID], original_row_node_ids[0])
def test_dgl_node_classification_without_feature():
# Arrange
minibatch = create_homo_minibatch()
minibatch.node_features = None
minibatch.labels = None
minibatch.seeds = torch.tensor([10, 15])
# Act
dgl_blocks = minibatch.blocks
# Assert
assert len(dgl_blocks) == 2
assert minibatch.node_features is None
assert minibatch.labels is None
check_dgl_blocks_homo(minibatch, dgl_blocks)
def test_dgl_node_classification_homo():
# Arrange
minibatch = create_homo_minibatch()
minibatch.seeds = torch.tensor([10, 15])
minibatch.labels = torch.tensor([2, 5])
# Act
dgl_blocks = minibatch.blocks
# Assert
assert len(dgl_blocks) == 2
check_dgl_blocks_homo(minibatch, dgl_blocks)
def test_dgl_node_classification_hetero():
minibatch = create_hetero_minibatch()
minibatch.labels = {"B": torch.tensor([2, 5])}
minibatch.seeds = {"B": torch.tensor([10, 15])}
# Act
dgl_blocks = minibatch.blocks
# Assert
assert len(dgl_blocks) == 2
check_dgl_blocks_hetero(minibatch, dgl_blocks)
def test_dgl_link_predication_homo():
# Arrange
minibatch = create_homo_minibatch()
minibatch.compacted_seeds = (
torch.tensor([[0, 1, 0, 0, 1, 1], [1, 0, 1, 1, 0, 0]]).T,
)
minibatch.labels = torch.tensor([1, 1, 0, 0, 0, 0])
# Act
dgl_blocks = minibatch.blocks
# Assert
assert len(dgl_blocks) == 2
check_dgl_blocks_homo(minibatch, dgl_blocks)
def test_dgl_link_predication_hetero():
# Arrange
minibatch = create_hetero_minibatch()
minibatch.compacted_seeds = {
relation: (torch.tensor([[1, 1, 2, 0, 1, 2], [1, 0, 1, 1, 0, 0]]).T,),
reverse_relation: (
torch.tensor([[0, 1, 1, 2, 0, 2], [1, 0, 1, 1, 0, 0]]).T,
),
}
minibatch.labels = {
relation: (torch.tensor([1, 1, 0, 0, 0, 0]),),
reverse_relation: (torch.tensor([1, 1, 0, 0, 0, 0]),),
}
# Act
dgl_blocks = minibatch.blocks
# Assert
assert len(dgl_blocks) == 2
check_dgl_blocks_hetero(minibatch, dgl_blocks)
def test_to_pyg_data():
test_minibatch = create_homo_minibatch()
test_minibatch.seeds = torch.tensor([0, 1])
test_minibatch.labels = torch.tensor([7, 8])
expected_edge_index = torch.tensor(
[[0, 0, 1, 1, 1, 2, 2, 2, 2], [0, 1, 0, 1, 2, 0, 1, 2, 3]]
)
expected_node_features = next(iter(test_minibatch.node_features.values()))
expected_labels = torch.tensor([7, 8])
expected_batch_size = 2
expected_n_id = torch.tensor([10, 11, 12, 13])
pyg_data = test_minibatch.to_pyg_data()
pyg_data.validate()
assert torch.equal(pyg_data.edge_index, expected_edge_index)
assert torch.equal(pyg_data.x, expected_node_features)
assert torch.equal(pyg_data.y, expected_labels)
assert pyg_data.batch_size == expected_batch_size
assert torch.equal(pyg_data.n_id, expected_n_id)
test_minibatch.seeds = torch.tensor([[0, 1], [2, 3]])
assert pyg_data.batch_size == expected_batch_size
test_minibatch.seeds = {"A": torch.tensor([0, 1])}
assert pyg_data.batch_size == expected_batch_size
test_minibatch.seeds = {"A": torch.tensor([[0, 1], [2, 3]])}
assert pyg_data.batch_size == expected_batch_size
subgraph = test_minibatch.sampled_subgraphs[0]
# Test with sampled_csc as None.
test_minibatch = gb.MiniBatch(
sampled_subgraphs=None,
node_features={"feat": expected_node_features},
labels=expected_labels,
)
pyg_data = test_minibatch.to_pyg_data()
assert pyg_data.edge_index is None, "Edge index should be none."
# Test with node_features as None.
test_minibatch = gb.MiniBatch(
sampled_subgraphs=[subgraph],
node_features=None,
labels=expected_labels,
)
pyg_data = test_minibatch.to_pyg_data()
assert pyg_data.x is None, "Node features should be None."
# Test with labels as None.
test_minibatch = gb.MiniBatch(
sampled_subgraphs=[subgraph],
node_features={"feat": expected_node_features},
labels=None,
)
pyg_data = test_minibatch.to_pyg_data()
assert pyg_data.y is None, "Labels should be None."
# Test with multiple features.
test_minibatch = gb.MiniBatch(
sampled_subgraphs=[subgraph],
node_features={
"feat": expected_node_features,
"extra_feat": torch.tensor([[3], [4]]),
},
labels=expected_labels,
)
try:
pyg_data = test_minibatch.to_pyg_data()
assert (
pyg_data.x is None
), "Multiple features case should raise an error."
except AssertionError as e:
assert (
str(e)
== "`to_pyg_data` only supports single feature homogeneous graph."
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,308 @@
import re
import unittest
from functools import partial
import backend as F
import dgl
import dgl.graphbolt as gb
import pytest
import torch
def test_add_reverse_edges_homo():
edges = torch.tensor([[0, 1, 2, 3], [4, 5, 6, 7]]).T
combined_edges = gb.add_reverse_edges(edges)
assert torch.equal(
combined_edges,
torch.tensor([[0, 1, 2, 3, 4, 5, 6, 7], [4, 5, 6, 7, 0, 1, 2, 3]]).T,
)
# Tensor with uncorrect dimensions.
edges = torch.tensor([0, 1, 2, 3])
with pytest.raises(
AssertionError,
match=re.escape(
"Only tensor with shape N*2 is supported now, but got torch.Size([4])."
),
):
gb.add_reverse_edges(edges)
def test_add_reverse_edges_hetero():
# reverse_etype doesn't exist in original etypes.
edges = {"n1:e1:n2": torch.tensor([[0, 1, 2], [4, 5, 6]]).T}
reverse_etype_mapping = {"n1:e1:n2": "n2:e2:n1"}
combined_edges = gb.add_reverse_edges(edges, reverse_etype_mapping)
assert torch.equal(
combined_edges["n1:e1:n2"], torch.tensor([[0, 1, 2], [4, 5, 6]]).T
)
assert torch.equal(
combined_edges["n2:e2:n1"], torch.tensor([[4, 5, 6], [0, 1, 2]]).T
)
# reverse_etype exists in original etypes.
edges = {
"n1:e1:n2": torch.tensor([[0, 1, 2], [4, 5, 6]]).T,
"n2:e2:n1": torch.tensor([[7, 8, 9], [10, 11, 12]]).T,
}
reverse_etype_mapping = {"n1:e1:n2": "n2:e2:n1"}
combined_edges = gb.add_reverse_edges(edges, reverse_etype_mapping)
assert torch.equal(
combined_edges["n1:e1:n2"], torch.tensor([[0, 1, 2], [4, 5, 6]]).T
)
assert torch.equal(
combined_edges["n2:e2:n1"],
torch.tensor([[7, 8, 9, 4, 5, 6], [10, 11, 12, 0, 1, 2]]).T,
)
# Tensor with uncorrect dimensions.
edges = {
"n1:e1:n2": torch.tensor([0, 1, 2]),
"n2:e2:n1": torch.tensor([7, 8, 9]),
}
with pytest.raises(
AssertionError,
match=re.escape(
"Only tensor with shape N*2 is supported now, but got torch.Size([3])."
),
):
gb.add_reverse_edges(edges, reverse_etype_mapping)
@unittest.skipIf(
F._default_context_str == "gpu",
reason="Fails due to different result on the GPU.",
)
@pytest.mark.parametrize("use_datapipe", [False, True])
def test_exclude_seed_edges_homo_cpu(use_datapipe):
graph = dgl.graph(([5, 0, 6, 7, 2, 2, 4], [0, 1, 2, 2, 3, 4, 4]))
graph = gb.from_dglgraph(graph, True).to(F.ctx())
items = torch.LongTensor([[0, 3], [4, 4]])
names = "seeds"
itemset = gb.ItemSet(items, names=names)
datapipe = gb.ItemSampler(itemset, batch_size=2).copy_to(F.ctx())
num_layer = 2
fanouts = [torch.LongTensor([2]) for _ in range(num_layer)]
sampler = gb.NeighborSampler
datapipe = sampler(datapipe, graph, fanouts)
if use_datapipe:
datapipe = datapipe.exclude_seed_edges()
else:
datapipe = datapipe.transform(partial(gb.exclude_seed_edges))
original_row_node_ids = [
torch.tensor([0, 3, 4, 5, 2, 6, 7]).to(F.ctx()),
torch.tensor([0, 3, 4, 5, 2]).to(F.ctx()),
]
compacted_indices = [
torch.tensor([3, 4, 4, 5, 6]).to(F.ctx()),
torch.tensor([3, 4, 4]).to(F.ctx()),
]
indptr = [
torch.tensor([0, 1, 2, 3, 3, 5]).to(F.ctx()),
torch.tensor([0, 1, 2, 3]).to(F.ctx()),
]
seeds = [
torch.tensor([0, 3, 4, 5, 2]).to(F.ctx()),
torch.tensor([0, 3, 4]).to(F.ctx()),
]
for data in datapipe:
for step, sampled_subgraph in enumerate(data.sampled_subgraphs):
assert torch.equal(
sampled_subgraph.original_row_node_ids,
original_row_node_ids[step],
)
assert torch.equal(
sampled_subgraph.sampled_csc.indices, compacted_indices[step]
)
assert torch.equal(
sampled_subgraph.sampled_csc.indptr, indptr[step]
)
assert torch.equal(
sampled_subgraph.original_column_node_ids, seeds[step]
)
@unittest.skipIf(
F._default_context_str == "cpu",
reason="Fails due to different result on the CPU.",
)
@pytest.mark.parametrize("use_datapipe", [False, True])
@pytest.mark.parametrize("async_op", [False, True])
def test_exclude_seed_edges_gpu(use_datapipe, async_op):
graph = dgl.graph(([5, 0, 7, 7, 2, 4], [0, 1, 2, 2, 3, 4]))
graph = gb.from_dglgraph(graph, is_homogeneous=True).to(F.ctx())
items = torch.LongTensor([[0, 3], [4, 4]])
names = "seeds"
itemset = gb.ItemSet(items, names=names)
datapipe = gb.ItemSampler(itemset, batch_size=4).copy_to(F.ctx())
num_layer = 2
fanouts = [torch.LongTensor([-1]) for _ in range(num_layer)]
sampler = gb.NeighborSampler
datapipe = sampler(
datapipe,
graph,
fanouts,
deduplicate=True,
)
if use_datapipe:
datapipe = datapipe.exclude_seed_edges(asynchronous=async_op)
else:
datapipe = datapipe.transform(
partial(gb.exclude_seed_edges, async_op=async_op)
)
if torch.cuda.get_device_capability()[0] < 7:
original_row_node_ids = [
torch.tensor([0, 3, 4, 2, 5, 7]).to(F.ctx()),
torch.tensor([0, 3, 4, 2, 5]).to(F.ctx()),
]
compacted_indices = [
torch.tensor([4, 3, 5, 5]).to(F.ctx()),
torch.tensor([4, 3]).to(F.ctx()),
]
indptr = [
torch.tensor([0, 1, 2, 2, 5, 5]).to(F.ctx()),
torch.tensor([0, 1, 2, 2]).to(F.ctx()),
]
seeds = [
torch.tensor([0, 3, 4, 2, 5]).to(F.ctx()),
torch.tensor([0, 3, 4]).to(F.ctx()),
]
else:
original_row_node_ids = [
torch.tensor([0, 3, 4, 5, 2, 7]).to(F.ctx()),
torch.tensor([0, 3, 4, 5, 2]).to(F.ctx()),
]
compacted_indices = [
torch.tensor([3, 4, 5, 5]).to(F.ctx()),
torch.tensor([3, 4]).to(F.ctx()),
]
indptr = [
torch.tensor([0, 1, 2, 2, 2, 4]).to(F.ctx()),
torch.tensor([0, 1, 2, 2]).to(F.ctx()),
]
seeds = [
torch.tensor([0, 3, 4, 5, 2]).to(F.ctx()),
torch.tensor([0, 3, 4]).to(F.ctx()),
]
for data in datapipe:
for step, sampled_subgraph in enumerate(data.sampled_subgraphs):
if async_op and not use_datapipe:
sampled_subgraph = sampled_subgraph.wait()
assert torch.equal(
sampled_subgraph.original_row_node_ids,
original_row_node_ids[step],
)
assert torch.equal(
(sampled_subgraph.sampled_csc.indices), compacted_indices[step]
)
assert torch.equal(
sampled_subgraph.sampled_csc.indptr, indptr[step]
)
assert torch.equal(
sampled_subgraph.original_column_node_ids, seeds[step]
)
def get_hetero_graph():
# COO graph:
# [0, 0, 1, 1, 2, 2, 3, 3, 4, 4]
# [2, 4, 2, 3, 0, 1, 1, 0, 0, 1]
# [1, 1, 1, 1, 0, 0, 0, 0, 0] - > edge type.
# num_nodes = 5, num_n1 = 2, num_n2 = 3
ntypes = {"n1": 0, "n2": 1}
etypes = {"n1:e1:n2": 0, "n2:e2:n1": 1}
indptr = torch.LongTensor([0, 2, 4, 6, 8, 10])
indices = torch.LongTensor([2, 4, 2, 3, 0, 1, 1, 0, 0, 1])
type_per_edge = torch.LongTensor([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
node_type_offset = torch.LongTensor([0, 2, 5])
return gb.fused_csc_sampling_graph(
indptr,
indices,
node_type_offset=node_type_offset,
type_per_edge=type_per_edge,
node_type_to_id=ntypes,
edge_type_to_id=etypes,
)
def test_exclude_seed_edges_hetero():
graph = get_hetero_graph().to(F.ctx())
itemset = gb.HeteroItemSet(
{"n1:e1:n2": gb.ItemSet(torch.tensor([[0, 1]]), names="seeds")}
)
item_sampler = gb.ItemSampler(itemset, batch_size=2).copy_to(F.ctx())
num_layer = 2
fanouts = [torch.LongTensor([2]) for _ in range(num_layer)]
Sampler = gb.NeighborSampler
datapipe = Sampler(
item_sampler,
graph,
fanouts,
deduplicate=True,
)
datapipe = datapipe.transform(partial(gb.exclude_seed_edges))
csc_formats = [
{
"n1:e1:n2": gb.CSCFormatBase(
indptr=torch.tensor([0, 1, 3, 5]),
indices=torch.tensor([1, 0, 1, 0, 1]),
),
"n2:e2:n1": gb.CSCFormatBase(
indptr=torch.tensor([0, 2, 4]),
indices=torch.tensor([1, 2, 1, 0]),
),
},
{
"n1:e1:n2": gb.CSCFormatBase(
indptr=torch.tensor([0, 1]),
indices=torch.tensor([1]),
),
"n2:e2:n1": gb.CSCFormatBase(
indptr=torch.tensor([0, 2]),
indices=torch.tensor([1, 2], dtype=torch.int64),
),
},
]
original_column_node_ids = [
{
"n1": torch.tensor([0, 1]),
"n2": torch.tensor([0, 1, 2]),
},
{
"n1": torch.tensor([0]),
"n2": torch.tensor([1]),
},
]
original_row_node_ids = [
{
"n1": torch.tensor([0, 1]),
"n2": torch.tensor([0, 1, 2]),
},
{
"n1": torch.tensor([0, 1]),
"n2": torch.tensor([0, 1, 2]),
},
]
for data in datapipe:
for step, sampled_subgraph in enumerate(data.sampled_subgraphs):
for ntype in ["n1", "n2"]:
assert torch.equal(
torch.sort(sampled_subgraph.original_row_node_ids[ntype])[
0
],
original_row_node_ids[step][ntype].to(F.ctx()),
)
assert torch.equal(
torch.sort(
sampled_subgraph.original_column_node_ids[ntype]
)[0],
original_column_node_ids[step][ntype].to(F.ctx()),
)
for etype in ["n1:e1:n2", "n2:e2:n1"]:
assert torch.equal(
sampled_subgraph.sampled_csc[etype].indices,
csc_formats[step][etype].indices.to(F.ctx()),
)
assert torch.equal(
sampled_subgraph.sampled_csc[etype].indptr,
csc_formats[step][etype].indptr.to(F.ctx()),
)