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/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