chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import unittest
|
||||
|
||||
import backend as F
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
|
||||
from dgl.cuda import nccl
|
||||
from dgl.partition import NDArrayPartition
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
F._default_context_str == "cpu", reason="NCCL only runs on GPU."
|
||||
)
|
||||
def test_nccl_sparse_push_single_remainder():
|
||||
torch.cuda.set_device("cuda:0")
|
||||
dist.init_process_group(
|
||||
backend="nccl",
|
||||
init_method="tcp://127.0.0.1:12345",
|
||||
world_size=1,
|
||||
rank=0,
|
||||
)
|
||||
|
||||
index = F.randint([10000], F.int32, F.ctx(), 0, 10000)
|
||||
value = F.uniform([10000, 100], F.float32, F.ctx(), -1.0, 1.0)
|
||||
|
||||
part = NDArrayPartition(10000, 1, "remainder")
|
||||
|
||||
ri, rv = nccl.sparse_all_to_all_push(index, value, part)
|
||||
assert F.array_equal(ri, index)
|
||||
assert F.array_equal(rv, value)
|
||||
|
||||
dist.destroy_process_group()
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
F._default_context_str == "cpu", reason="NCCL only runs on GPU."
|
||||
)
|
||||
def test_nccl_sparse_pull_single_remainder():
|
||||
torch.cuda.set_device("cuda:0")
|
||||
dist.init_process_group(
|
||||
backend="nccl",
|
||||
init_method="tcp://127.0.0.1:12345",
|
||||
world_size=1,
|
||||
rank=0,
|
||||
)
|
||||
|
||||
req_index = F.randint([10000], F.int64, F.ctx(), 0, 100000)
|
||||
value = F.uniform([100000, 100], F.float32, F.ctx(), -1.0, 1.0)
|
||||
|
||||
part = NDArrayPartition(100000, 1, "remainder")
|
||||
|
||||
rv = nccl.sparse_all_to_all_pull(req_index, value, part)
|
||||
exp_rv = F.gather_row(value, req_index)
|
||||
assert F.array_equal(rv, exp_rv)
|
||||
|
||||
dist.destroy_process_group()
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
F._default_context_str == "cpu", reason="NCCL only runs on GPU."
|
||||
)
|
||||
def test_nccl_sparse_push_single_range():
|
||||
torch.cuda.set_device("cuda:0")
|
||||
dist.init_process_group(
|
||||
backend="nccl",
|
||||
init_method="tcp://127.0.0.1:12345",
|
||||
world_size=1,
|
||||
rank=0,
|
||||
)
|
||||
|
||||
index = F.randint([10000], F.int32, F.ctx(), 0, 10000)
|
||||
value = F.uniform([10000, 100], F.float32, F.ctx(), -1.0, 1.0)
|
||||
|
||||
part_ranges = F.copy_to(
|
||||
F.tensor([0, value.shape[0]], dtype=F.int64), F.ctx()
|
||||
)
|
||||
part = NDArrayPartition(10000, 1, "range", part_ranges=part_ranges)
|
||||
|
||||
ri, rv = nccl.sparse_all_to_all_push(index, value, part)
|
||||
assert F.array_equal(ri, index)
|
||||
assert F.array_equal(rv, value)
|
||||
|
||||
dist.destroy_process_group()
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
F._default_context_str == "cpu", reason="NCCL only runs on GPU."
|
||||
)
|
||||
def test_nccl_sparse_pull_single_range():
|
||||
torch.cuda.set_device("cuda:0")
|
||||
dist.init_process_group(
|
||||
backend="nccl",
|
||||
init_method="tcp://127.0.0.1:12345",
|
||||
world_size=1,
|
||||
rank=0,
|
||||
)
|
||||
|
||||
req_index = F.randint([10000], F.int64, F.ctx(), 0, 100000)
|
||||
value = F.uniform([100000, 100], F.float32, F.ctx(), -1.0, 1.0)
|
||||
|
||||
part_ranges = F.copy_to(
|
||||
F.tensor([0, value.shape[0]], dtype=F.int64), F.ctx()
|
||||
)
|
||||
part = NDArrayPartition(100000, 1, "range", part_ranges=part_ranges)
|
||||
|
||||
rv = nccl.sparse_all_to_all_pull(req_index, value, part)
|
||||
exp_rv = F.gather_row(value, req_index)
|
||||
assert F.array_equal(rv, exp_rv)
|
||||
|
||||
dist.destroy_process_group()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_nccl_sparse_push_single_remainder()
|
||||
test_nccl_sparse_pull_single_remainder()
|
||||
test_nccl_sparse_push_single_range()
|
||||
test_nccl_sparse_pull_single_range()
|
||||
@@ -0,0 +1,854 @@
|
||||
import os
|
||||
import unittest
|
||||
from collections.abc import Iterator, Mapping
|
||||
from functools import partial
|
||||
|
||||
import backend as F
|
||||
|
||||
import dgl
|
||||
import dgl.ops as OPS
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.multiprocessing as mp
|
||||
from utils import parametrize_idtype
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", [None, 16])
|
||||
def test_graph_dataloader(batch_size):
|
||||
num_batches = 2
|
||||
num_samples = num_batches * (batch_size if batch_size is not None else 1)
|
||||
minigc_dataset = dgl.data.MiniGCDataset(num_samples, 10, 20)
|
||||
data_loader = dgl.dataloading.GraphDataLoader(
|
||||
minigc_dataset, batch_size=batch_size, shuffle=True
|
||||
)
|
||||
assert isinstance(iter(data_loader), Iterator)
|
||||
for graph, label in data_loader:
|
||||
assert isinstance(graph, dgl.DGLGraph)
|
||||
if batch_size is not None:
|
||||
assert F.asnumpy(label).shape[0] == batch_size
|
||||
else:
|
||||
# If batch size is None, the label element will be a single scalar following
|
||||
# PyTorch's practice.
|
||||
assert F.asnumpy(label).ndim == 0
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@pytest.mark.parametrize("num_workers", [0, 4])
|
||||
def test_cluster_gcn(num_workers):
|
||||
dataset = dgl.data.CoraFullDataset()
|
||||
g = dataset[0]
|
||||
sampler = dgl.dataloading.ClusterGCNSampler(g, 100)
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g, torch.arange(100), sampler, batch_size=4, num_workers=num_workers
|
||||
)
|
||||
assert len(dataloader) == 25
|
||||
for i, sg in enumerate(dataloader):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_workers", [0, 4])
|
||||
def test_shadow(num_workers):
|
||||
g = dgl.data.CoraFullDataset()[0]
|
||||
sampler = dgl.dataloading.ShaDowKHopSampler([5, 10, 15])
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g,
|
||||
torch.arange(g.num_nodes()),
|
||||
sampler,
|
||||
batch_size=5,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
num_workers=num_workers,
|
||||
)
|
||||
for i, (input_nodes, output_nodes, subgraph) in enumerate(dataloader):
|
||||
assert torch.equal(input_nodes, subgraph.ndata[dgl.NID])
|
||||
assert torch.equal(input_nodes[: output_nodes.shape[0]], output_nodes)
|
||||
assert torch.equal(
|
||||
subgraph.ndata["label"], g.ndata["label"][input_nodes]
|
||||
)
|
||||
assert torch.equal(subgraph.ndata["feat"], g.ndata["feat"][input_nodes])
|
||||
if i == 5:
|
||||
break
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_workers", [0, 4])
|
||||
@pytest.mark.parametrize("mode", ["node", "edge", "walk"])
|
||||
def test_saint(num_workers, mode):
|
||||
g = dgl.data.CoraFullDataset()[0]
|
||||
|
||||
if mode == "node":
|
||||
budget = 100
|
||||
elif mode == "edge":
|
||||
budget = 200
|
||||
elif mode == "walk":
|
||||
budget = (3, 2)
|
||||
|
||||
sampler = dgl.dataloading.SAINTSampler(mode, budget)
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g, torch.arange(100), sampler, num_workers=num_workers
|
||||
)
|
||||
assert len(dataloader) == 100
|
||||
for sg in dataloader:
|
||||
pass
|
||||
|
||||
|
||||
@parametrize_idtype
|
||||
@pytest.mark.parametrize(
|
||||
"mode", ["cpu", "uva_cuda_indices", "uva_cpu_indices", "pure_gpu"]
|
||||
)
|
||||
@pytest.mark.parametrize("use_ddp", [False, True])
|
||||
@pytest.mark.parametrize("use_mask", [False, True])
|
||||
def test_neighbor_nonuniform(idtype, mode, use_ddp, use_mask):
|
||||
if mode != "cpu" and F.ctx() == F.cpu():
|
||||
pytest.skip("UVA and GPU sampling require a GPU.")
|
||||
if mode != "cpu" and use_mask:
|
||||
pytest.skip("Masked sampling only works on CPU.")
|
||||
if use_ddp:
|
||||
if os.name == "nt":
|
||||
pytest.skip("PyTorch 1.13.0+ has problems in Windows DDP...")
|
||||
dist.init_process_group(
|
||||
"gloo" if F.ctx() == F.cpu() else "nccl",
|
||||
"tcp://127.0.0.1:12347",
|
||||
world_size=1,
|
||||
rank=0,
|
||||
)
|
||||
g = dgl.graph(([1, 2, 3, 4, 5, 6, 7, 8], [0, 0, 0, 0, 1, 1, 1, 1])).astype(
|
||||
idtype
|
||||
)
|
||||
g.edata["p"] = torch.FloatTensor([1, 1, 0, 0, 1, 1, 0, 0])
|
||||
g.edata["mask"] = g.edata["p"] != 0
|
||||
if mode in ("cpu", "uva_cpu_indices"):
|
||||
indices = F.copy_to(F.tensor([0, 1], idtype), F.cpu())
|
||||
else:
|
||||
indices = F.copy_to(F.tensor([0, 1], idtype), F.cuda())
|
||||
if mode == "pure_gpu":
|
||||
g = g.to(F.cuda())
|
||||
use_uva = mode.startswith("uva")
|
||||
|
||||
if use_mask:
|
||||
prob, mask = None, "mask"
|
||||
else:
|
||||
prob, mask = "p", None
|
||||
|
||||
sampler = dgl.dataloading.MultiLayerNeighborSampler(
|
||||
[2], prob=prob, mask=mask
|
||||
)
|
||||
for num_workers in [0, 1, 2] if mode == "cpu" else [0]:
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g,
|
||||
indices,
|
||||
sampler,
|
||||
batch_size=1,
|
||||
device=F.ctx(),
|
||||
num_workers=num_workers,
|
||||
use_uva=use_uva,
|
||||
use_ddp=use_ddp,
|
||||
)
|
||||
for input_nodes, output_nodes, blocks in dataloader:
|
||||
seed = output_nodes.item()
|
||||
neighbors = set(input_nodes[1:].cpu().numpy())
|
||||
if seed == 1:
|
||||
assert neighbors == {5, 6}
|
||||
elif seed == 0:
|
||||
assert neighbors == {1, 2}
|
||||
|
||||
g = dgl.heterograph(
|
||||
{
|
||||
("B", "BA", "A"): (
|
||||
[1, 2, 3, 4, 5, 6, 7, 8],
|
||||
[0, 0, 0, 0, 1, 1, 1, 1],
|
||||
),
|
||||
("C", "CA", "A"): (
|
||||
[1, 2, 3, 4, 5, 6, 7, 8],
|
||||
[0, 0, 0, 0, 1, 1, 1, 1],
|
||||
),
|
||||
}
|
||||
).astype(idtype)
|
||||
g.edges["BA"].data["p"] = torch.FloatTensor([1, 1, 0, 0, 1, 1, 0, 0])
|
||||
g.edges["BA"].data["mask"] = g.edges["BA"].data["p"] != 0
|
||||
g.edges["CA"].data["p"] = torch.FloatTensor([0, 0, 1, 1, 0, 0, 1, 1])
|
||||
g.edges["CA"].data["mask"] = g.edges["CA"].data["p"] != 0
|
||||
if mode == "pure_gpu":
|
||||
g = g.to(F.cuda())
|
||||
for num_workers in [0, 1, 2] if mode == "cpu" else [0]:
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g,
|
||||
{"A": indices},
|
||||
sampler,
|
||||
batch_size=1,
|
||||
device=F.ctx(),
|
||||
num_workers=num_workers,
|
||||
use_uva=use_uva,
|
||||
use_ddp=use_ddp,
|
||||
)
|
||||
for input_nodes, output_nodes, blocks in dataloader:
|
||||
seed = output_nodes["A"].item()
|
||||
# Seed and neighbors are of different node types so slicing is not necessary here.
|
||||
neighbors = set(input_nodes["B"].cpu().numpy())
|
||||
if seed == 1:
|
||||
assert neighbors == {5, 6}
|
||||
elif seed == 0:
|
||||
assert neighbors == {1, 2}
|
||||
|
||||
neighbors = set(input_nodes["C"].cpu().numpy())
|
||||
if seed == 1:
|
||||
assert neighbors == {7, 8}
|
||||
elif seed == 0:
|
||||
assert neighbors == {3, 4}
|
||||
|
||||
if use_ddp:
|
||||
dist.destroy_process_group()
|
||||
|
||||
|
||||
def _check_dtype(data, dtype, attr_name):
|
||||
if isinstance(data, dict):
|
||||
for k, v in data.items():
|
||||
assert getattr(v, attr_name) == dtype
|
||||
elif isinstance(data, list):
|
||||
for v in data:
|
||||
assert getattr(v, attr_name) == dtype
|
||||
else:
|
||||
assert getattr(data, attr_name) == dtype
|
||||
|
||||
|
||||
def _check_device(data):
|
||||
if isinstance(data, dict):
|
||||
for k, v in data.items():
|
||||
assert v.device == F.ctx()
|
||||
elif isinstance(data, list):
|
||||
for v in data:
|
||||
assert v.device == F.ctx()
|
||||
else:
|
||||
assert data.device == F.ctx()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sampler_name", ["full", "neighbor"])
|
||||
@pytest.mark.parametrize(
|
||||
"mode", ["cpu", "uva_cuda_indices", "uva_cpu_indices", "pure_gpu"]
|
||||
)
|
||||
@pytest.mark.parametrize("nprocs", [1, 4])
|
||||
@pytest.mark.parametrize("drop_last", [True, False])
|
||||
def test_ddp_dataloader_decompose_dataset(
|
||||
sampler_name, mode, nprocs, drop_last
|
||||
):
|
||||
if torch.cuda.device_count() < nprocs and mode != "cpu":
|
||||
pytest.skip(
|
||||
"DDP dataloader needs sufficient GPUs for UVA and GPU sampling."
|
||||
)
|
||||
if mode != "cpu" and F.ctx() == F.cpu():
|
||||
pytest.skip("UVA and GPU sampling require a GPU.")
|
||||
|
||||
if os.name == "nt":
|
||||
pytest.skip("PyTorch 1.13.0+ has problems in Windows DDP...")
|
||||
g, _, _, _ = _create_homogeneous()
|
||||
g = g.to(F.cpu())
|
||||
|
||||
sampler = {
|
||||
"full": dgl.dataloading.MultiLayerFullNeighborSampler(2),
|
||||
"neighbor": dgl.dataloading.MultiLayerNeighborSampler([3, 3]),
|
||||
}[sampler_name]
|
||||
indices = F.copy_to(F.arange(0, g.num_nodes()), F.cpu())
|
||||
data = indices, sampler
|
||||
arguments = mode, drop_last
|
||||
g.create_formats_()
|
||||
os.environ["OMP_NUM_THREADS"] = str(mp.cpu_count() // 2 // nprocs)
|
||||
mp.spawn(_ddp_runner, args=(nprocs, g, data, arguments), nprocs=nprocs)
|
||||
|
||||
|
||||
def _ddp_runner(proc_id, nprocs, g, data, args):
|
||||
mode, drop_last = args
|
||||
indices, sampler = data
|
||||
if mode == "cpu":
|
||||
device = torch.device("cpu")
|
||||
else:
|
||||
device = torch.device(proc_id)
|
||||
torch.cuda.set_device(device)
|
||||
if mode == "pure_gpu":
|
||||
g = g.to(F.cuda())
|
||||
if mode in ("cpu", "uva_cpu_indices"):
|
||||
indices = indices.cpu()
|
||||
else:
|
||||
indices = indices.cuda()
|
||||
|
||||
dist.init_process_group(
|
||||
"nccl" if mode != "cpu" else "gloo",
|
||||
"tcp://127.0.0.1:12347",
|
||||
world_size=nprocs,
|
||||
rank=proc_id,
|
||||
)
|
||||
use_uva = mode.startswith("uva")
|
||||
batch_size = g.num_nodes()
|
||||
shuffle = False
|
||||
for num_workers in [1, 4] if mode == "cpu" else [0]:
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g,
|
||||
indices,
|
||||
sampler,
|
||||
device=device,
|
||||
batch_size=batch_size, # g1.num_nodes(),
|
||||
num_workers=num_workers,
|
||||
use_uva=use_uva,
|
||||
use_ddp=True,
|
||||
drop_last=drop_last,
|
||||
shuffle=shuffle,
|
||||
)
|
||||
max_nid = [0]
|
||||
for i, (input_nodes, output_nodes, blocks) in enumerate(dataloader):
|
||||
block = blocks[-1]
|
||||
o_src, o_dst = block.edges()
|
||||
src_nodes_id = block.srcdata[dgl.NID][o_src]
|
||||
dst_nodes_id = block.dstdata[dgl.NID][o_dst]
|
||||
max_nid.append(np.max(dst_nodes_id.cpu().numpy()))
|
||||
|
||||
local_max = torch.tensor(np.max(max_nid))
|
||||
if torch.distributed.get_backend() == "nccl":
|
||||
local_max = local_max.cuda()
|
||||
dist.reduce(local_max, 0, op=dist.ReduceOp.MAX)
|
||||
if proc_id == 0:
|
||||
if drop_last and not shuffle and local_max > 0:
|
||||
assert (
|
||||
local_max.item()
|
||||
== len(indices)
|
||||
- len(indices) % nprocs
|
||||
- 1
|
||||
- (len(indices) // nprocs) % batch_size
|
||||
)
|
||||
elif not drop_last:
|
||||
assert local_max == len(indices) - 1
|
||||
dist.destroy_process_group()
|
||||
|
||||
|
||||
@parametrize_idtype
|
||||
@pytest.mark.parametrize(
|
||||
"sampler_name", ["full", "neighbor", "neighbor2", "labor"]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"mode", ["cpu", "uva_cuda_indices", "uva_cpu_indices", "pure_gpu"]
|
||||
)
|
||||
@pytest.mark.parametrize("use_ddp", [False, True])
|
||||
def test_node_dataloader(idtype, sampler_name, mode, use_ddp):
|
||||
if mode != "cpu" and F.ctx() == F.cpu():
|
||||
pytest.skip("UVA and GPU sampling require a GPU.")
|
||||
if use_ddp:
|
||||
if os.name == "nt":
|
||||
pytest.skip("PyTorch 1.13.0+ has problems in Windows DDP...")
|
||||
dist.init_process_group(
|
||||
"gloo" if F.ctx() == F.cpu() else "nccl",
|
||||
"tcp://127.0.0.1:12347",
|
||||
world_size=1,
|
||||
rank=0,
|
||||
)
|
||||
g1 = dgl.graph(([0, 0, 0, 1, 1], [1, 2, 3, 3, 4])).astype(idtype)
|
||||
g1.ndata["feat"] = F.copy_to(F.randn((5, 8)), F.cpu())
|
||||
g1.ndata["label"] = F.copy_to(F.randn((g1.num_nodes(),)), F.cpu())
|
||||
if mode in ("cpu", "uva_cpu_indices"):
|
||||
indices = F.copy_to(F.arange(0, g1.num_nodes(), idtype), F.cpu())
|
||||
else:
|
||||
indices = F.copy_to(F.arange(0, g1.num_nodes(), idtype), F.cuda())
|
||||
if mode == "pure_gpu":
|
||||
g1 = g1.to(F.cuda())
|
||||
|
||||
use_uva = mode.startswith("uva")
|
||||
|
||||
sampler = {
|
||||
"full": dgl.dataloading.MultiLayerFullNeighborSampler(2),
|
||||
"neighbor": dgl.dataloading.MultiLayerNeighborSampler([3, 3]),
|
||||
"neighbor2": dgl.dataloading.MultiLayerNeighborSampler([3, 3]),
|
||||
"labor": dgl.dataloading.LaborSampler([3, 3]),
|
||||
}[sampler_name]
|
||||
for num_workers in [0, 1, 2] if mode == "cpu" else [0]:
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g1,
|
||||
indices,
|
||||
sampler,
|
||||
device=F.ctx(),
|
||||
batch_size=g1.num_nodes(),
|
||||
num_workers=num_workers,
|
||||
use_uva=use_uva,
|
||||
use_ddp=use_ddp,
|
||||
)
|
||||
for input_nodes, output_nodes, blocks in dataloader:
|
||||
_check_device(input_nodes)
|
||||
_check_device(output_nodes)
|
||||
_check_device(blocks)
|
||||
_check_dtype(input_nodes, idtype, "dtype")
|
||||
_check_dtype(output_nodes, idtype, "dtype")
|
||||
_check_dtype(blocks, idtype, "idtype")
|
||||
|
||||
g2 = dgl.heterograph(
|
||||
{
|
||||
("user", "follow", "user"): (
|
||||
[0, 0, 0, 1, 1, 1, 2],
|
||||
[1, 2, 3, 0, 2, 3, 0],
|
||||
),
|
||||
("user", "followed-by", "user"): (
|
||||
[1, 2, 3, 0, 2, 3, 0],
|
||||
[0, 0, 0, 1, 1, 1, 2],
|
||||
),
|
||||
("user", "play", "game"): ([0, 1, 1, 3, 5], [0, 1, 2, 0, 2]),
|
||||
("game", "played-by", "user"): ([0, 1, 2, 0, 2], [0, 1, 1, 3, 5]),
|
||||
}
|
||||
).astype(idtype)
|
||||
for ntype in g2.ntypes:
|
||||
g2.nodes[ntype].data["feat"] = F.copy_to(
|
||||
F.randn((g2.num_nodes(ntype), 8)), F.cpu()
|
||||
)
|
||||
if mode in ("cpu", "uva_cpu_indices"):
|
||||
indices = {nty: F.copy_to(g2.nodes(nty), F.cpu()) for nty in g2.ntypes}
|
||||
else:
|
||||
indices = {nty: F.copy_to(g2.nodes(nty), F.cuda()) for nty in g2.ntypes}
|
||||
if mode == "pure_gpu":
|
||||
g2 = g2.to(F.cuda())
|
||||
|
||||
batch_size = max(g2.num_nodes(nty) for nty in g2.ntypes)
|
||||
sampler = {
|
||||
"full": dgl.dataloading.MultiLayerFullNeighborSampler(2),
|
||||
"neighbor": dgl.dataloading.MultiLayerNeighborSampler(
|
||||
[{etype: 3 for etype in g2.etypes}] * 2
|
||||
),
|
||||
"neighbor2": dgl.dataloading.MultiLayerNeighborSampler([3, 3]),
|
||||
"labor": dgl.dataloading.LaborSampler([3, 3]),
|
||||
}[sampler_name]
|
||||
for num_workers in [0, 1, 2] if mode == "cpu" else [0]:
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g2,
|
||||
indices,
|
||||
sampler,
|
||||
device=F.ctx(),
|
||||
batch_size=batch_size,
|
||||
num_workers=num_workers,
|
||||
use_uva=use_uva,
|
||||
use_ddp=use_ddp,
|
||||
)
|
||||
assert isinstance(iter(dataloader), Iterator)
|
||||
for input_nodes, output_nodes, blocks in dataloader:
|
||||
_check_device(input_nodes)
|
||||
_check_device(output_nodes)
|
||||
_check_device(blocks)
|
||||
_check_dtype(input_nodes, idtype, "dtype")
|
||||
_check_dtype(output_nodes, idtype, "dtype")
|
||||
_check_dtype(blocks, idtype, "idtype")
|
||||
|
||||
if use_ddp:
|
||||
dist.destroy_process_group()
|
||||
|
||||
|
||||
@parametrize_idtype
|
||||
@pytest.mark.parametrize("sampler_name", ["full", "neighbor"])
|
||||
@pytest.mark.parametrize(
|
||||
"neg_sampler",
|
||||
[
|
||||
dgl.dataloading.negative_sampler.Uniform(2),
|
||||
dgl.dataloading.negative_sampler.GlobalUniform(15, False, 3),
|
||||
dgl.dataloading.negative_sampler.GlobalUniform(15, True, 3),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("mode", ["cpu", "uva", "pure_gpu"])
|
||||
@pytest.mark.parametrize("use_ddp", [False, True])
|
||||
def test_edge_dataloader(idtype, sampler_name, neg_sampler, mode, use_ddp):
|
||||
if mode != "cpu" and F.ctx() == F.cpu():
|
||||
pytest.skip("UVA and GPU sampling require a GPU.")
|
||||
if mode == "uva" and isinstance(
|
||||
neg_sampler, dgl.dataloading.negative_sampler.GlobalUniform
|
||||
):
|
||||
pytest.skip("GlobalUniform don't support UVA yet.")
|
||||
if use_ddp:
|
||||
if os.name == "nt":
|
||||
pytest.skip("PyTorch 1.13.0+ has problems in Windows DDP...")
|
||||
dist.init_process_group(
|
||||
"gloo" if F.ctx() == F.cpu() else "nccl",
|
||||
"tcp://127.0.0.1:12347",
|
||||
world_size=1,
|
||||
rank=0,
|
||||
)
|
||||
g1 = dgl.graph(([0, 0, 0, 1, 1], [1, 2, 3, 3, 4])).astype(idtype)
|
||||
g1.ndata["feat"] = F.copy_to(F.randn((5, 8)), F.cpu())
|
||||
if mode == "pure_gpu":
|
||||
g1 = g1.to(F.cuda())
|
||||
|
||||
sampler = {
|
||||
"full": dgl.dataloading.MultiLayerFullNeighborSampler(2),
|
||||
"neighbor": dgl.dataloading.MultiLayerNeighborSampler([3, 3]),
|
||||
}[sampler_name]
|
||||
|
||||
# no negative sampler
|
||||
edge_sampler = dgl.dataloading.as_edge_prediction_sampler(sampler)
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g1,
|
||||
g1.edges(form="eid"),
|
||||
edge_sampler,
|
||||
device=F.ctx(),
|
||||
batch_size=g1.num_edges(),
|
||||
use_uva=(mode == "uva"),
|
||||
use_ddp=use_ddp,
|
||||
)
|
||||
for input_nodes, pos_pair_graph, blocks in dataloader:
|
||||
_check_device(input_nodes)
|
||||
_check_device(pos_pair_graph)
|
||||
_check_device(blocks)
|
||||
|
||||
# negative sampler
|
||||
edge_sampler = dgl.dataloading.as_edge_prediction_sampler(
|
||||
sampler, negative_sampler=neg_sampler
|
||||
)
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g1,
|
||||
g1.edges(form="eid"),
|
||||
edge_sampler,
|
||||
device=F.ctx(),
|
||||
batch_size=g1.num_edges(),
|
||||
use_uva=(mode == "uva"),
|
||||
use_ddp=use_ddp,
|
||||
)
|
||||
for input_nodes, pos_pair_graph, neg_pair_graph, blocks in dataloader:
|
||||
_check_device(input_nodes)
|
||||
_check_device(pos_pair_graph)
|
||||
_check_device(neg_pair_graph)
|
||||
_check_device(blocks)
|
||||
|
||||
g2 = dgl.heterograph(
|
||||
{
|
||||
("user", "follow", "user"): (
|
||||
[0, 0, 0, 1, 1, 1, 2],
|
||||
[1, 2, 3, 0, 2, 3, 0],
|
||||
),
|
||||
("user", "followed-by", "user"): (
|
||||
[1, 2, 3, 0, 2, 3, 0],
|
||||
[0, 0, 0, 1, 1, 1, 2],
|
||||
),
|
||||
("user", "play", "game"): ([0, 1, 1, 3, 5], [0, 1, 2, 0, 2]),
|
||||
("game", "played-by", "user"): ([0, 1, 2, 0, 2], [0, 1, 1, 3, 5]),
|
||||
}
|
||||
).astype(idtype)
|
||||
for ntype in g2.ntypes:
|
||||
g2.nodes[ntype].data["feat"] = F.copy_to(
|
||||
F.randn((g2.num_nodes(ntype), 8)), F.cpu()
|
||||
)
|
||||
if mode == "pure_gpu":
|
||||
g2 = g2.to(F.cuda())
|
||||
|
||||
batch_size = max(g2.num_edges(ety) for ety in g2.canonical_etypes)
|
||||
sampler = {
|
||||
"full": dgl.dataloading.MultiLayerFullNeighborSampler(2),
|
||||
"neighbor": dgl.dataloading.MultiLayerNeighborSampler(
|
||||
[{etype: 3 for etype in g2.etypes}] * 2
|
||||
),
|
||||
}[sampler_name]
|
||||
|
||||
# no negative sampler
|
||||
edge_sampler = dgl.dataloading.as_edge_prediction_sampler(sampler)
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g2,
|
||||
{ety: g2.edges(form="eid", etype=ety) for ety in g2.canonical_etypes},
|
||||
edge_sampler,
|
||||
device=F.ctx(),
|
||||
batch_size=batch_size,
|
||||
use_uva=(mode == "uva"),
|
||||
use_ddp=use_ddp,
|
||||
)
|
||||
for input_nodes, pos_pair_graph, blocks in dataloader:
|
||||
_check_device(input_nodes)
|
||||
_check_device(pos_pair_graph)
|
||||
_check_device(blocks)
|
||||
|
||||
# negative sampler
|
||||
edge_sampler = dgl.dataloading.as_edge_prediction_sampler(
|
||||
sampler, negative_sampler=neg_sampler
|
||||
)
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g2,
|
||||
{ety: g2.edges(form="eid", etype=ety) for ety in g2.canonical_etypes},
|
||||
edge_sampler,
|
||||
device=F.ctx(),
|
||||
batch_size=batch_size,
|
||||
use_uva=(mode == "uva"),
|
||||
use_ddp=use_ddp,
|
||||
)
|
||||
|
||||
assert isinstance(iter(dataloader), Iterator)
|
||||
for input_nodes, pos_pair_graph, neg_pair_graph, blocks in dataloader:
|
||||
_check_device(input_nodes)
|
||||
_check_device(pos_pair_graph)
|
||||
_check_device(neg_pair_graph)
|
||||
_check_device(blocks)
|
||||
|
||||
if use_ddp:
|
||||
dist.destroy_process_group()
|
||||
|
||||
|
||||
def _create_homogeneous():
|
||||
s = torch.randint(0, 200, (1000,), device=F.ctx())
|
||||
d = torch.randint(0, 200, (1000,), device=F.ctx())
|
||||
src = torch.cat([s, d])
|
||||
dst = torch.cat([d, s])
|
||||
g = dgl.graph((s, d), num_nodes=200)
|
||||
reverse_eids = torch.cat(
|
||||
[torch.arange(1000, 2000), torch.arange(0, 1000)]
|
||||
).to(F.ctx())
|
||||
always_exclude = torch.randint(0, 1000, (50,), device=F.ctx())
|
||||
seed_edges = torch.arange(0, 1000, device=F.ctx())
|
||||
return g, reverse_eids, always_exclude, seed_edges
|
||||
|
||||
|
||||
def _create_heterogeneous():
|
||||
edges = {}
|
||||
for utype, etype, vtype in [("A", "AA", "A"), ("A", "AB", "B")]:
|
||||
s = torch.randint(0, 200, (1000,), device=F.ctx())
|
||||
d = torch.randint(0, 200, (1000,), device=F.ctx())
|
||||
edges[utype, etype, vtype] = (s, d)
|
||||
edges[vtype, "rev-" + etype, utype] = (d, s)
|
||||
g = dgl.heterograph(edges, num_nodes_dict={"A": 200, "B": 200})
|
||||
reverse_etypes = {
|
||||
"AA": "rev-AA",
|
||||
"AB": "rev-AB",
|
||||
"rev-AA": "AA",
|
||||
"rev-AB": "AB",
|
||||
}
|
||||
always_exclude = {
|
||||
"AA": torch.randint(0, 1000, (50,), device=F.ctx()),
|
||||
"AB": torch.randint(0, 1000, (50,), device=F.ctx()),
|
||||
}
|
||||
seed_edges = {
|
||||
"AA": torch.arange(0, 1000, device=F.ctx()),
|
||||
"AB": torch.arange(0, 1000, device=F.ctx()),
|
||||
}
|
||||
return g, reverse_etypes, always_exclude, seed_edges
|
||||
|
||||
|
||||
def _remove_duplicates(s, d):
|
||||
s, d = list(zip(*list(set(zip(s.tolist(), d.tolist())))))
|
||||
return torch.tensor(s, device=F.ctx()), torch.tensor(d, device=F.ctx())
|
||||
|
||||
|
||||
def _find_edges_to_exclude(g, exclude, always_exclude, pair_eids):
|
||||
if exclude == None:
|
||||
return always_exclude
|
||||
elif exclude == "self":
|
||||
return (
|
||||
torch.cat([pair_eids, always_exclude])
|
||||
if always_exclude is not None
|
||||
else pair_eids
|
||||
)
|
||||
elif exclude == "reverse_id":
|
||||
pair_eids = torch.cat([pair_eids, pair_eids + 1000])
|
||||
return (
|
||||
torch.cat([pair_eids, always_exclude])
|
||||
if always_exclude is not None
|
||||
else pair_eids
|
||||
)
|
||||
elif exclude == "reverse_types":
|
||||
pair_eids = {g.to_canonical_etype(k): v for k, v in pair_eids.items()}
|
||||
if ("A", "AA", "A") in pair_eids:
|
||||
pair_eids[("A", "rev-AA", "A")] = pair_eids[("A", "AA", "A")]
|
||||
if ("A", "AB", "B") in pair_eids:
|
||||
pair_eids[("B", "rev-AB", "A")] = pair_eids[("A", "AB", "B")]
|
||||
if always_exclude is not None:
|
||||
always_exclude = {
|
||||
g.to_canonical_etype(k): v for k, v in always_exclude.items()
|
||||
}
|
||||
for k in always_exclude.keys():
|
||||
if k in pair_eids:
|
||||
pair_eids[k] = torch.cat([pair_eids[k], always_exclude[k]])
|
||||
else:
|
||||
pair_eids[k] = always_exclude[k]
|
||||
return pair_eids
|
||||
|
||||
|
||||
@pytest.mark.parametrize("always_exclude_flag", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
"exclude", [None, "self", "reverse_id", "reverse_types"]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"sampler",
|
||||
[
|
||||
dgl.dataloading.MultiLayerFullNeighborSampler(1),
|
||||
dgl.dataloading.ShaDowKHopSampler([5]),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("batch_size", [1, 50])
|
||||
def test_edge_dataloader_excludes(
|
||||
exclude, always_exclude_flag, batch_size, sampler
|
||||
):
|
||||
if exclude == "reverse_types":
|
||||
g, reverse_etypes, always_exclude, seed_edges = _create_heterogeneous()
|
||||
else:
|
||||
g, reverse_eids, always_exclude, seed_edges = _create_homogeneous()
|
||||
g = g.to(F.ctx())
|
||||
if not always_exclude_flag:
|
||||
always_exclude = None
|
||||
|
||||
kwargs = {}
|
||||
kwargs["exclude"] = (
|
||||
partial(_find_edges_to_exclude, g, exclude, always_exclude)
|
||||
if always_exclude_flag
|
||||
else exclude
|
||||
)
|
||||
kwargs["reverse_eids"] = reverse_eids if exclude == "reverse_id" else None
|
||||
kwargs["reverse_etypes"] = (
|
||||
reverse_etypes if exclude == "reverse_types" else None
|
||||
)
|
||||
sampler = dgl.dataloading.as_edge_prediction_sampler(sampler, **kwargs)
|
||||
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g,
|
||||
seed_edges,
|
||||
sampler,
|
||||
batch_size=batch_size,
|
||||
device=F.ctx(),
|
||||
use_prefetch_thread=False,
|
||||
)
|
||||
for i, (input_nodes, pair_graph, blocks) in enumerate(dataloader):
|
||||
if isinstance(blocks, list):
|
||||
subg = blocks[0]
|
||||
else:
|
||||
subg = blocks
|
||||
pair_eids = pair_graph.edata[dgl.EID]
|
||||
block_eids = subg.edata[dgl.EID]
|
||||
|
||||
edges_to_exclude = _find_edges_to_exclude(
|
||||
g, exclude, always_exclude, pair_eids
|
||||
)
|
||||
if edges_to_exclude is None:
|
||||
continue
|
||||
edges_to_exclude = dgl.utils.recursive_apply(
|
||||
edges_to_exclude, lambda x: x.cpu().numpy()
|
||||
)
|
||||
block_eids = dgl.utils.recursive_apply(
|
||||
block_eids, lambda x: x.cpu().numpy()
|
||||
)
|
||||
|
||||
if isinstance(edges_to_exclude, Mapping):
|
||||
for k in edges_to_exclude.keys():
|
||||
assert not np.isin(edges_to_exclude[k], block_eids[k]).any()
|
||||
else:
|
||||
assert not np.isin(edges_to_exclude, block_eids).any()
|
||||
|
||||
if i == 10:
|
||||
break
|
||||
|
||||
|
||||
def test_edge_dataloader_exclusion_with_reverse_seed_nodes():
|
||||
utype, etype, vtype = ("A", "AB", "B")
|
||||
s = torch.randint(0, 20, (500,), device=F.ctx())
|
||||
d = torch.randint(0, 20, (500,), device=F.ctx())
|
||||
s, d = _remove_duplicates(s, d)
|
||||
g = dgl.heterograph({("A", "AB", "B"): (s, d), ("B", "BA", "A"): (d, s)})
|
||||
sampler = dgl.dataloading.as_edge_prediction_sampler(
|
||||
dgl.dataloading.NeighborSampler(fanouts=[2, 2, 2]),
|
||||
exclude="reverse_types",
|
||||
reverse_etypes={"AB": "BA", "BA": "AB"},
|
||||
)
|
||||
seed_edges = {
|
||||
"AB": torch.arange(g.number_of_edges("AB"), device=F.ctx()),
|
||||
"BA": torch.arange(g.number_of_edges("BA"), device=F.ctx()),
|
||||
}
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g,
|
||||
seed_edges,
|
||||
sampler,
|
||||
batch_size=2,
|
||||
device=F.ctx(),
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
)
|
||||
for _, pos_graph, mfgs in dataloader:
|
||||
s, d = pos_graph["AB"].edges()
|
||||
AB_pos = list(zip(s.tolist(), d.tolist()))
|
||||
s, d = pos_graph["BA"].edges()
|
||||
BA_pos = list(zip(s.tolist(), d.tolist()))
|
||||
|
||||
s, d = mfgs[-1]["AB"].edges()
|
||||
AB_mfg = list(zip(s.tolist(), d.tolist()))
|
||||
s, d = mfgs[-1]["BA"].edges()
|
||||
BA_mfg = list(zip(s.tolist(), d.tolist()))
|
||||
|
||||
assert all(edge not in AB_mfg for edge in AB_pos)
|
||||
assert all(edge not in BA_mfg for edge in BA_pos)
|
||||
|
||||
|
||||
def test_edge_dataloader_exclusion_without_all_reverses():
|
||||
data_dict = {
|
||||
("A", "AB", "B"): (torch.tensor([0, 1]), torch.tensor([0, 1])),
|
||||
("B", "BA", "A"): (torch.tensor([0, 1]), torch.tensor([0, 1])),
|
||||
("B", "BC", "C"): (torch.tensor([0]), torch.tensor([0])),
|
||||
("C", "CA", "A"): (torch.tensor([0, 1]), torch.tensor([0, 1])),
|
||||
}
|
||||
g = dgl.heterograph(data_dict=data_dict)
|
||||
block_sampler = dgl.dataloading.MultiLayerNeighborSampler(
|
||||
fanouts=[1], replace=True
|
||||
)
|
||||
block_sampler = dgl.dataloading.as_edge_prediction_sampler(
|
||||
block_sampler,
|
||||
exclude="reverse_types",
|
||||
reverse_etypes={"AB": "BA"},
|
||||
)
|
||||
d = dgl.dataloading.DataLoader(
|
||||
graph=g,
|
||||
indices={
|
||||
"AB": torch.tensor([0]),
|
||||
"BC": torch.tensor([0]),
|
||||
},
|
||||
graph_sampler=block_sampler,
|
||||
batch_size=2,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
num_workers=0,
|
||||
device=F.ctx(),
|
||||
use_ddp=False,
|
||||
)
|
||||
|
||||
next(iter(d))
|
||||
|
||||
|
||||
def dummy_worker_init_fn(worker_id):
|
||||
pass
|
||||
|
||||
|
||||
def test_dataloader_worker_init_fn():
|
||||
dataset = dgl.data.CoraFullDataset()
|
||||
g = dataset[0]
|
||||
sampler = dgl.dataloading.MultiLayerNeighborSampler([2])
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g,
|
||||
torch.arange(100),
|
||||
sampler,
|
||||
batch_size=4,
|
||||
num_workers=4,
|
||||
worker_init_fn=dummy_worker_init_fn,
|
||||
)
|
||||
for _ in dataloader:
|
||||
pass
|
||||
|
||||
|
||||
def test_distributed_dataloaders():
|
||||
# Test distributed dataloaders could be successfully imported.
|
||||
try:
|
||||
from dgl.dataloading import (
|
||||
DistDataLoader,
|
||||
DistEdgeDataLoader,
|
||||
DistNodeDataLoader,
|
||||
EdgeCollator,
|
||||
NodeCollator,
|
||||
)
|
||||
except ImportError:
|
||||
pytest.fail("Distributed DataLoader from dataloading import failed")
|
||||
|
||||
try:
|
||||
from dgl.distributed import (
|
||||
DistDataLoader,
|
||||
DistEdgeDataLoader,
|
||||
DistNodeDataLoader,
|
||||
EdgeCollator,
|
||||
NodeCollator,
|
||||
)
|
||||
except ImportError:
|
||||
pytest.fail("Distributed DataLoader from dataloading import failed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# test_node_dataloader(F.int32, 'neighbor', None)
|
||||
test_edge_dataloader_excludes(
|
||||
"reverse_types", False, 1, dgl.dataloading.ShaDowKHopSampler([5])
|
||||
)
|
||||
test_edge_dataloader_exclusion_without_all_reverses()
|
||||
@@ -0,0 +1,83 @@
|
||||
from collections.abc import Mapping
|
||||
|
||||
import dgl
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
|
||||
def _create_homogeneous():
|
||||
s = torch.randint(0, 200, (1000,))
|
||||
d = torch.randint(0, 200, (1000,))
|
||||
g = dgl.graph((s, d), num_nodes=200)
|
||||
reverse_eids = torch.cat([torch.arange(1000, 2000), torch.arange(0, 1000)])
|
||||
seed_edges = torch.arange(0, 1000)
|
||||
return g, reverse_eids, seed_edges
|
||||
|
||||
|
||||
def _find_edges_to_exclude(g, pair_eids, degree_threshold):
|
||||
src, dst = g.find_edges(pair_eids)
|
||||
head_degree = g.in_degrees(src)
|
||||
tail_degree = g.in_degrees(dst)
|
||||
degree = torch.min(head_degree, tail_degree)
|
||||
degree_mask = degree < degree_threshold
|
||||
low_degree_pair_eids = pair_eids[degree_mask]
|
||||
low_degree_pair_eids = torch.cat(
|
||||
[low_degree_pair_eids, low_degree_pair_eids + 1000]
|
||||
)
|
||||
return low_degree_pair_eids
|
||||
|
||||
|
||||
@pytest.mark.parametrize("degree_threshold", [1, 2, 3, 4, 5])
|
||||
@pytest.mark.parametrize("batch_size", [1, 10, 50])
|
||||
def test_spot_target_excludes(degree_threshold, batch_size):
|
||||
g, reverse_eids, seed_edges = _create_homogeneous()
|
||||
sampler = dgl.dataloading.MultiLayerFullNeighborSampler(1)
|
||||
low_degree_excluder = dgl.dataloading.SpotTarget(
|
||||
g,
|
||||
exclude="reverse_id",
|
||||
degree_threshold=degree_threshold,
|
||||
reverse_eids=reverse_eids,
|
||||
)
|
||||
sampler = dgl.dataloading.as_edge_prediction_sampler(
|
||||
sampler,
|
||||
exclude=low_degree_excluder,
|
||||
negative_sampler=dgl.dataloading.negative_sampler.Uniform(1),
|
||||
)
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g, seed_edges, sampler, batch_size=batch_size
|
||||
)
|
||||
|
||||
for i, (input_nodes, pair_graph, neg_pair_graph, blocks) in enumerate(
|
||||
dataloader
|
||||
):
|
||||
if isinstance(blocks, list):
|
||||
subg = blocks[0]
|
||||
else:
|
||||
subg = blocks
|
||||
pair_eids = pair_graph.edata[dgl.EID]
|
||||
block_eids = subg.edata[dgl.EID]
|
||||
edges_to_exclude = _find_edges_to_exclude(
|
||||
g, pair_eids, degree_threshold
|
||||
)
|
||||
if edges_to_exclude is None:
|
||||
continue
|
||||
edges_to_exclude = dgl.utils.recursive_apply(
|
||||
edges_to_exclude, lambda x: x.cpu().numpy()
|
||||
)
|
||||
block_eids = dgl.utils.recursive_apply(
|
||||
block_eids, lambda x: x.cpu().numpy()
|
||||
)
|
||||
|
||||
if isinstance(edges_to_exclude, Mapping):
|
||||
for k in edges_to_exclude.keys():
|
||||
assert not np.isin(edges_to_exclude[k], block_eids[k]).any()
|
||||
else:
|
||||
assert not np.isin(edges_to_exclude, block_eids).any()
|
||||
|
||||
if i == 10:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_spot_target_excludes(degree_threshold=2, batch_size=10)
|
||||
@@ -0,0 +1,200 @@
|
||||
import os
|
||||
|
||||
os.environ["OMP_NUM_THREADS"] = "1"
|
||||
import multiprocessing as mp
|
||||
import pickle
|
||||
import random
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import backend as F
|
||||
|
||||
import dgl
|
||||
import numpy as np
|
||||
import torch as th
|
||||
from dgl import function as fn
|
||||
from dgl.distributed import (
|
||||
DistEmbedding,
|
||||
DistGraph,
|
||||
DistGraphServer,
|
||||
load_partition_book,
|
||||
partition_graph,
|
||||
)
|
||||
from dgl.distributed.optim import SparseAdagrad, SparseAdam
|
||||
from scipy import sparse as spsp
|
||||
|
||||
# Set seeds to make tests fully reproducible.
|
||||
SEED = 12345 # random.randint(1, 99999)
|
||||
F.seed(SEED)
|
||||
|
||||
|
||||
def create_random_graph(n):
|
||||
arr = (
|
||||
spsp.random(n, n, density=0.001, format="coo", random_state=100) != 0
|
||||
).astype(np.int64)
|
||||
return dgl.from_scipy(arr)
|
||||
|
||||
|
||||
def get_local_usable_addr():
|
||||
"""Get local usable IP and port
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
IP address, e.g., '192.168.8.12:50051'
|
||||
"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
# doesn't even have to be reachable
|
||||
sock.connect(("10.255.255.255", 1))
|
||||
ip_addr = sock.getsockname()[0]
|
||||
except ValueError:
|
||||
ip_addr = "127.0.0.1"
|
||||
finally:
|
||||
sock.close()
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(("", 0))
|
||||
sock.listen(1)
|
||||
port = sock.getsockname()[1]
|
||||
sock.close()
|
||||
|
||||
return ip_addr + " " + str(port)
|
||||
|
||||
|
||||
def prepare_dist():
|
||||
ip_config = open("optim_ip_config.txt", "w")
|
||||
ip_addr = get_local_usable_addr()
|
||||
ip_config.write("{}\n".format(ip_addr))
|
||||
ip_config.close()
|
||||
|
||||
|
||||
def run_server(graph_name, server_id, server_count, num_clients, shared_mem):
|
||||
g = DistGraphServer(
|
||||
server_id,
|
||||
"optim_ip_config.txt",
|
||||
num_clients,
|
||||
server_count,
|
||||
"/tmp/dist_graph/{}.json".format(graph_name),
|
||||
disable_shared_mem=not shared_mem,
|
||||
)
|
||||
print("start server", server_id)
|
||||
g.start()
|
||||
|
||||
|
||||
def initializer(shape, dtype):
|
||||
arr = th.zeros(shape, dtype=dtype)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(arr, 0, 1.0)
|
||||
return arr
|
||||
|
||||
|
||||
def run_client(graph_name, cli_id, part_id, server_count):
|
||||
device = F.ctx()
|
||||
time.sleep(5)
|
||||
os.environ["DGL_NUM_SERVER"] = str(server_count)
|
||||
dgl.distributed.initialize("optim_ip_config.txt")
|
||||
gpb, graph_name, _, _ = load_partition_book(
|
||||
"/tmp/dist_graph/{}.json".format(graph_name), part_id
|
||||
)
|
||||
g = DistGraph(graph_name, gpb=gpb)
|
||||
policy = dgl.distributed.PartitionPolicy("node", g.get_partition_book())
|
||||
num_nodes = g.num_nodes()
|
||||
emb_dim = 4
|
||||
dgl_emb = DistEmbedding(
|
||||
num_nodes,
|
||||
emb_dim,
|
||||
name="optim",
|
||||
init_func=initializer,
|
||||
part_policy=policy,
|
||||
)
|
||||
dgl_emb_zero = DistEmbedding(
|
||||
num_nodes,
|
||||
emb_dim,
|
||||
name="optim-zero",
|
||||
init_func=initializer,
|
||||
part_policy=policy,
|
||||
)
|
||||
dgl_adam = SparseAdam(params=[dgl_emb, dgl_emb_zero], lr=0.01)
|
||||
dgl_adam._world_size = 1
|
||||
dgl_adam._rank = 0
|
||||
|
||||
torch_emb = th.nn.Embedding(num_nodes, emb_dim, sparse=True)
|
||||
torch_emb_zero = th.nn.Embedding(num_nodes, emb_dim, sparse=True)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(torch_emb.weight, 0, 1.0)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(torch_emb_zero.weight, 0, 1.0)
|
||||
torch_adam = th.optim.SparseAdam(
|
||||
list(torch_emb.parameters()) + list(torch_emb_zero.parameters()),
|
||||
lr=0.01,
|
||||
)
|
||||
|
||||
labels = th.ones((4,)).long()
|
||||
idx = th.randint(0, num_nodes, size=(4,))
|
||||
dgl_value = dgl_emb(idx, device).to(th.device("cpu"))
|
||||
torch_value = torch_emb(idx)
|
||||
torch_adam.zero_grad()
|
||||
torch_loss = th.nn.functional.cross_entropy(torch_value, labels)
|
||||
torch_loss.backward()
|
||||
torch_adam.step()
|
||||
|
||||
dgl_adam.zero_grad()
|
||||
dgl_loss = th.nn.functional.cross_entropy(dgl_value, labels)
|
||||
dgl_loss.backward()
|
||||
dgl_adam.step()
|
||||
|
||||
assert F.allclose(
|
||||
dgl_emb.weight[0 : num_nodes // 2], torch_emb.weight[0 : num_nodes // 2]
|
||||
)
|
||||
|
||||
|
||||
def check_sparse_adam(num_trainer=1, shared_mem=True):
|
||||
prepare_dist()
|
||||
g = create_random_graph(2000)
|
||||
num_servers = num_trainer
|
||||
num_clients = num_trainer
|
||||
num_parts = 1
|
||||
|
||||
graph_name = "dist_graph_test"
|
||||
partition_graph(g, graph_name, num_parts, "/tmp/dist_graph")
|
||||
|
||||
# let's just test on one partition for now.
|
||||
# We cannot run multiple servers and clients on the same machine.
|
||||
serv_ps = []
|
||||
ctx = mp.get_context("spawn")
|
||||
for serv_id in range(num_servers):
|
||||
p = ctx.Process(
|
||||
target=run_server,
|
||||
args=(graph_name, serv_id, num_servers, num_clients, shared_mem),
|
||||
)
|
||||
serv_ps.append(p)
|
||||
p.start()
|
||||
|
||||
cli_ps = []
|
||||
for cli_id in range(num_clients):
|
||||
print("start client", cli_id)
|
||||
p = ctx.Process(
|
||||
target=run_client, args=(graph_name, cli_id, 0, num_servers)
|
||||
)
|
||||
p.start()
|
||||
cli_ps.append(p)
|
||||
|
||||
for p in cli_ps:
|
||||
p.join()
|
||||
|
||||
for p in serv_ps:
|
||||
p.join()
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
def test_sparse_opt():
|
||||
os.environ["DGL_DIST_MODE"] = "distributed"
|
||||
check_sparse_adam(1, True)
|
||||
check_sparse_adam(1, False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.makedirs("/tmp/dist_graph", exist_ok=True)
|
||||
test_sparse_opt()
|
||||
@@ -0,0 +1,254 @@
|
||||
import backend as F
|
||||
|
||||
import dgl
|
||||
import dgl.nn
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch as th
|
||||
from dgl import DGLError
|
||||
from dgl.base import DGLWarning
|
||||
from dgl.geometry import farthest_point_sampler, neighbor_matching
|
||||
from utils import parametrize_idtype
|
||||
from utils.graph_cases import get_cases
|
||||
|
||||
|
||||
def test_fps():
|
||||
N = 1000
|
||||
batch_size = 5
|
||||
sample_points = 10
|
||||
x = th.tensor(np.random.uniform(size=(batch_size, int(N / batch_size), 3)))
|
||||
ctx = F.ctx()
|
||||
if F.gpu_ctx():
|
||||
x = x.to(ctx)
|
||||
res = farthest_point_sampler(x, sample_points)
|
||||
assert res.shape[0] == batch_size
|
||||
assert res.shape[1] == sample_points
|
||||
assert res.sum() > 0
|
||||
|
||||
|
||||
def test_fps_start_idx():
|
||||
N = 1000
|
||||
batch_size = 5
|
||||
sample_points = 10
|
||||
x = th.tensor(np.random.uniform(size=(batch_size, int(N / batch_size), 3)))
|
||||
ctx = F.ctx()
|
||||
if F.gpu_ctx():
|
||||
x = x.to(ctx)
|
||||
res = farthest_point_sampler(x, sample_points, start_idx=0)
|
||||
assert th.any(res[:, 0] == 0)
|
||||
|
||||
|
||||
def _test_knn_common(device, algorithm, dist, exclude_self):
|
||||
x = th.randn(8, 3).to(device)
|
||||
kg = dgl.nn.KNNGraph(3)
|
||||
if dist == "euclidean":
|
||||
d = th.cdist(x, x).to(F.cpu())
|
||||
else:
|
||||
x = x + th.randn(1).item()
|
||||
tmp_x = x / (1e-5 + F.sqrt(F.sum(x * x, dim=1, keepdims=True)))
|
||||
d = 1 - F.matmul(tmp_x, tmp_x.T).to(F.cpu())
|
||||
|
||||
def check_knn(g, x, start, end, k, exclude_self, check_indices=True):
|
||||
assert g.device == x.device
|
||||
g = g.to(F.cpu())
|
||||
for v in range(start, end):
|
||||
src, _ = g.in_edges(v)
|
||||
src = set(src.numpy())
|
||||
assert len(src) == k
|
||||
if check_indices:
|
||||
i = v - start
|
||||
src_ans = set(
|
||||
th.topk(
|
||||
d[start:end, start:end][i],
|
||||
k + (1 if exclude_self else 0),
|
||||
largest=False,
|
||||
)[1].numpy()
|
||||
+ start
|
||||
)
|
||||
if exclude_self:
|
||||
# remove self
|
||||
src_ans.remove(v)
|
||||
assert src == src_ans
|
||||
|
||||
def check_batch(g, k, expected_batch_info):
|
||||
assert F.array_equal(g.batch_num_nodes(), F.tensor(expected_batch_info))
|
||||
assert F.array_equal(
|
||||
g.batch_num_edges(), k * F.tensor(expected_batch_info)
|
||||
)
|
||||
|
||||
# check knn with 2d input
|
||||
g = kg(x, algorithm, dist, exclude_self)
|
||||
check_knn(g, x, 0, 8, 3, exclude_self)
|
||||
check_batch(g, 3, [8])
|
||||
|
||||
# check knn with 3d input
|
||||
g = kg(x.view(2, 4, 3), algorithm, dist, exclude_self)
|
||||
check_knn(g, x, 0, 4, 3, exclude_self)
|
||||
check_knn(g, x, 4, 8, 3, exclude_self)
|
||||
check_batch(g, 3, [4, 4])
|
||||
|
||||
# check segmented knn
|
||||
# there are only 2 edges per node possible when exclude_self with 3 nodes in the segment
|
||||
# and this test case isn't supposed to warn, so limit it when exclude_self is True
|
||||
adjusted_k = 3 - (1 if exclude_self else 0)
|
||||
kg = dgl.nn.SegmentedKNNGraph(adjusted_k)
|
||||
g = kg(x, [3, 5], algorithm, dist, exclude_self)
|
||||
check_knn(g, x, 0, 3, adjusted_k, exclude_self)
|
||||
check_knn(g, x, 3, 8, adjusted_k, exclude_self)
|
||||
check_batch(g, adjusted_k, [3, 5])
|
||||
|
||||
# check k > num_points
|
||||
kg = dgl.nn.KNNGraph(10)
|
||||
with pytest.warns(DGLWarning):
|
||||
g = kg(x, algorithm, dist, exclude_self)
|
||||
# there are only 7 edges per node possible when exclude_self with 8 nodes total
|
||||
adjusted_k = 8 - (1 if exclude_self else 0)
|
||||
check_knn(g, x, 0, 8, adjusted_k, exclude_self)
|
||||
check_batch(g, adjusted_k, [8])
|
||||
|
||||
with pytest.warns(DGLWarning):
|
||||
g = kg(x.view(2, 4, 3), algorithm, dist, exclude_self)
|
||||
# there are only 3 edges per node possible when exclude_self with 4 nodes per segment
|
||||
adjusted_k = 4 - (1 if exclude_self else 0)
|
||||
check_knn(g, x, 0, 4, adjusted_k, exclude_self)
|
||||
check_knn(g, x, 4, 8, adjusted_k, exclude_self)
|
||||
check_batch(g, adjusted_k, [4, 4])
|
||||
|
||||
kg = dgl.nn.SegmentedKNNGraph(5)
|
||||
with pytest.warns(DGLWarning):
|
||||
g = kg(x, [3, 5], algorithm, dist, exclude_self)
|
||||
# there are only 2 edges per node possible when exclude_self in the segment with
|
||||
# only 3 nodes, and the current implementation reduces k for all segments
|
||||
# in that case
|
||||
adjusted_k = 3 - (1 if exclude_self else 0)
|
||||
check_knn(g, x, 0, 3, adjusted_k, exclude_self)
|
||||
check_knn(g, x, 3, 8, adjusted_k, exclude_self)
|
||||
check_batch(g, adjusted_k, [3, 5])
|
||||
|
||||
# check k == 0
|
||||
# that's valid for exclude_self, but -1 is not, so check -1 instead for exclude_self
|
||||
adjusted_k = 0 - (1 if exclude_self else 0)
|
||||
kg = dgl.nn.KNNGraph(adjusted_k)
|
||||
with pytest.raises(DGLError):
|
||||
g = kg(x, algorithm, dist, exclude_self)
|
||||
kg = dgl.nn.SegmentedKNNGraph(adjusted_k)
|
||||
with pytest.raises(DGLError):
|
||||
g = kg(x, [3, 5], algorithm, dist, exclude_self)
|
||||
|
||||
# check empty
|
||||
x_empty = th.tensor([])
|
||||
kg = dgl.nn.KNNGraph(3)
|
||||
with pytest.raises(DGLError):
|
||||
g = kg(x_empty, algorithm, dist, exclude_self)
|
||||
kg = dgl.nn.SegmentedKNNGraph(3)
|
||||
with pytest.raises(DGLError):
|
||||
g = kg(x_empty, [3, 5], algorithm, dist, exclude_self)
|
||||
|
||||
# check all coincident points
|
||||
x = th.zeros((20, 3)).to(device)
|
||||
kg = dgl.nn.KNNGraph(3)
|
||||
g = kg(x, algorithm, dist, exclude_self)
|
||||
# different algorithms may break the tie differently, so don't check the indices
|
||||
check_knn(g, x, 0, 20, 3, exclude_self, False)
|
||||
check_batch(g, 3, [20])
|
||||
|
||||
# check all coincident points
|
||||
kg = dgl.nn.SegmentedKNNGraph(3)
|
||||
g = kg(x, [4, 7, 5, 4], algorithm, dist, exclude_self)
|
||||
# different algorithms may break the tie differently, so don't check the indices
|
||||
check_knn(g, x, 0, 4, 3, exclude_self, False)
|
||||
check_knn(g, x, 4, 11, 3, exclude_self, False)
|
||||
check_knn(g, x, 11, 16, 3, exclude_self, False)
|
||||
check_knn(g, x, 16, 20, 3, exclude_self, False)
|
||||
check_batch(g, 3, [4, 7, 5, 4])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"algorithm", ["bruteforce-blas", "bruteforce", "kd-tree"]
|
||||
)
|
||||
@pytest.mark.parametrize("dist", ["euclidean", "cosine"])
|
||||
@pytest.mark.parametrize("exclude_self", [False, True])
|
||||
def test_knn_cpu(algorithm, dist, exclude_self):
|
||||
_test_knn_common(F.cpu(), algorithm, dist, exclude_self)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"algorithm", ["bruteforce-blas", "bruteforce", "bruteforce-sharemem"]
|
||||
)
|
||||
@pytest.mark.parametrize("dist", ["euclidean", "cosine"])
|
||||
@pytest.mark.parametrize("exclude_self", [False, True])
|
||||
def test_knn_cuda(algorithm, dist, exclude_self):
|
||||
if not th.cuda.is_available():
|
||||
return
|
||||
_test_knn_common(F.cuda(), algorithm, dist, exclude_self)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_points", [8, 64, 256, 1024])
|
||||
def test_knn_sharedmem_large(num_points):
|
||||
if not th.cuda.is_available():
|
||||
return
|
||||
x = th.randn(num_points, 5, device="cuda")
|
||||
y = th.randn(num_points, 5, device="cuda")
|
||||
k = 4
|
||||
|
||||
def ground_truth(x, y, k):
|
||||
dist = (
|
||||
th.sum(x * x, dim=1)
|
||||
+ th.sum(y * y, dim=1).unsqueeze(-1)
|
||||
- 2 * th.mm(y, x.T)
|
||||
)
|
||||
ret = th.topk(dist, k, dim=-1, largest=False)[1]
|
||||
return th.sort(ret, dim=-1)[0]
|
||||
|
||||
gt = ground_truth(x, y, k)
|
||||
actual = th.sort(
|
||||
dgl.functional.knn(
|
||||
k, x, [num_points], y, [num_points], algorithm="bruteforce-sharemem"
|
||||
)[1].reshape(-1, k),
|
||||
-1,
|
||||
)[0]
|
||||
assert th.all(actual == gt).item()
|
||||
|
||||
|
||||
@parametrize_idtype
|
||||
@pytest.mark.parametrize("g", get_cases(["homo"], exclude=["dglgraph"]))
|
||||
@pytest.mark.parametrize("weight", [True, False])
|
||||
@pytest.mark.parametrize("relabel", [True, False])
|
||||
def test_edge_coarsening(idtype, g, weight, relabel):
|
||||
num_nodes = g.num_nodes()
|
||||
g = dgl.to_bidirected(g)
|
||||
g = g.astype(idtype).to(F.ctx())
|
||||
edge_weight = None
|
||||
if weight:
|
||||
edge_weight = F.abs(F.randn((g.num_edges(),))).to(F.ctx())
|
||||
node_labels = neighbor_matching(g, edge_weight, relabel_idx=relabel)
|
||||
unique_ids, counts = th.unique(node_labels, return_counts=True)
|
||||
num_result_ids = unique_ids.size(0)
|
||||
|
||||
# shape correct
|
||||
assert node_labels.shape == (g.num_nodes(),)
|
||||
|
||||
# all nodes marked
|
||||
assert F.reduce_sum(node_labels < 0).item() == 0
|
||||
|
||||
# number of unique node ids correct.
|
||||
assert num_result_ids >= num_nodes // 2 and num_result_ids <= num_nodes
|
||||
|
||||
# each unique id has <= 2 nodes
|
||||
assert F.reduce_sum(counts > 2).item() == 0
|
||||
|
||||
# if two nodes have the same id, they must be neighbors
|
||||
idxs = F.arange(0, num_nodes, idtype)
|
||||
for l in unique_ids:
|
||||
l = l.item()
|
||||
idx = idxs[(node_labels == l)]
|
||||
if idx.size(0) == 2:
|
||||
u, v = idx[0].item(), idx[1].item()
|
||||
assert g.has_edges_between(u, v)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_fps()
|
||||
test_fps_start_idx()
|
||||
test_knn()
|
||||
test_knn_sharedmem_large()
|
||||
@@ -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
|
||||
@@ -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()),
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
0 127.0.0.1 40050
|
||||
1 127.0.0.1 40051
|
||||
2 127.0.0.1 40052
|
||||
3 127.0.0.1 40053
|
||||
@@ -0,0 +1,209 @@
|
||||
import random
|
||||
|
||||
import backend as F
|
||||
|
||||
import dgl
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from utils import parametrize_idtype
|
||||
|
||||
random.seed(42)
|
||||
np.random.seed(42)
|
||||
dgl.seed(42)
|
||||
torch.random.manual_seed(42)
|
||||
|
||||
|
||||
@parametrize_idtype
|
||||
@pytest.mark.parametrize("feat_size", [(5,), ()])
|
||||
def test_copy_u(idtype, feat_size):
|
||||
ctx = F.ctx()
|
||||
g = dgl.rand_graph(30, 100)
|
||||
g = g.astype(idtype).to(ctx)
|
||||
x = torch.randn(
|
||||
(g.num_nodes(),) + feat_size, requires_grad=True, device=ctx
|
||||
)
|
||||
|
||||
y = dgl.copy_u(g, x)
|
||||
y.sum().backward()
|
||||
x_grad = x.grad
|
||||
|
||||
x.grad.zero_()
|
||||
u, v = g.edges()
|
||||
y_true = x[u.long()]
|
||||
y_true.sum().backward()
|
||||
x_grad_true = x.grad
|
||||
|
||||
assert torch.allclose(y, y_true)
|
||||
assert torch.allclose(x_grad, x_grad_true)
|
||||
|
||||
|
||||
@parametrize_idtype
|
||||
@pytest.mark.parametrize("feat_size", [(5,), ()])
|
||||
def test_copy_u_hetero(idtype, feat_size):
|
||||
ctx = F.ctx()
|
||||
hg = dgl.heterograph(
|
||||
{
|
||||
("user", "follow", "user"): ([0, 1, 2], [2, 3, 4]),
|
||||
("user", "like", "movie"): ([3, 3, 1, 2], [0, 0, 1, 1]),
|
||||
}
|
||||
)
|
||||
|
||||
hg = hg.astype(idtype).to(ctx)
|
||||
x = torch.randn(
|
||||
(hg.num_nodes("user"),) + feat_size, requires_grad=True, device=ctx
|
||||
)
|
||||
|
||||
y = dgl.copy_u(hg, x, etype="like")
|
||||
y.sum().backward()
|
||||
x_grad = x.grad
|
||||
|
||||
x.grad.zero_()
|
||||
u, v = hg.edges(etype="like")
|
||||
y_true = x[u.long()]
|
||||
y_true.sum().backward()
|
||||
x_grad_true = x.grad
|
||||
|
||||
assert torch.allclose(y, y_true)
|
||||
assert torch.allclose(x_grad, x_grad_true)
|
||||
|
||||
|
||||
@parametrize_idtype
|
||||
@pytest.mark.parametrize("feat_size", [(5,), ()])
|
||||
def test_copy_v(idtype, feat_size):
|
||||
ctx = F.ctx()
|
||||
g = dgl.rand_graph(30, 100)
|
||||
g = g.astype(idtype).to(ctx)
|
||||
x = torch.randn(
|
||||
(g.num_nodes(),) + feat_size, requires_grad=True, device=ctx
|
||||
)
|
||||
|
||||
y = dgl.copy_v(g, x)
|
||||
y.sum().backward()
|
||||
x_grad = x.grad
|
||||
|
||||
x.grad.zero_()
|
||||
u, v = g.edges()
|
||||
y_true = x[v.long()]
|
||||
y_true.sum().backward()
|
||||
x_grad_true = x.grad
|
||||
|
||||
assert torch.allclose(y, y_true)
|
||||
assert torch.allclose(x_grad, x_grad_true)
|
||||
|
||||
|
||||
@parametrize_idtype
|
||||
@pytest.mark.parametrize("feat_size", [(5,), ()])
|
||||
def test_copy_v_hetero(idtype, feat_size):
|
||||
ctx = F.ctx()
|
||||
hg = dgl.heterograph(
|
||||
{
|
||||
("user", "follow", "user"): ([0, 1, 2], [2, 3, 4]),
|
||||
("user", "like", "movie"): ([3, 3, 1, 2], [0, 0, 1, 1]),
|
||||
}
|
||||
)
|
||||
|
||||
hg = hg.astype(idtype).to(ctx)
|
||||
x = torch.randn(
|
||||
(hg.num_nodes("movie"),) + feat_size, requires_grad=True, device=ctx
|
||||
)
|
||||
|
||||
y = dgl.copy_v(hg, x, etype="like")
|
||||
y.sum().backward()
|
||||
x_grad = x.grad
|
||||
|
||||
x.grad.zero_()
|
||||
u, v = hg.edges(etype="like")
|
||||
y_true = x[v.long()]
|
||||
y_true.sum().backward()
|
||||
x_grad_true = x.grad
|
||||
|
||||
assert torch.allclose(y, y_true)
|
||||
assert torch.allclose(x_grad, x_grad_true)
|
||||
|
||||
|
||||
binary_arg_sizes = [
|
||||
((5,), (5,)),
|
||||
((5,), ()),
|
||||
((), (5,)),
|
||||
((1, 3, 3), (4, 1, 3)),
|
||||
((3, 3), (4, 1, 3)),
|
||||
((4, 1, 3), (3, 3)),
|
||||
]
|
||||
|
||||
dot_arg_sizes = [
|
||||
((5,), (5,)),
|
||||
((1, 3, 3), (4, 1, 3)),
|
||||
((3, 3), (4, 1, 3)),
|
||||
((4, 1, 3), (3, 3)),
|
||||
]
|
||||
|
||||
ops = ["add", "sub", "mul", "div"]
|
||||
|
||||
|
||||
def pad_shape(x, y, x_size, y_size):
|
||||
xy_size = torch.broadcast_shapes(x_size, y_size)
|
||||
new_x_size = (1,) * (len(xy_size) - len(x_size)) + x_size
|
||||
new_y_size = (1,) * (len(xy_size) - len(y_size)) + y_size
|
||||
new_x = x.view(-1, *new_x_size)
|
||||
new_y = y.view(-1, *new_y_size)
|
||||
return new_x, new_y
|
||||
|
||||
|
||||
@parametrize_idtype
|
||||
@pytest.mark.parametrize("op", ops)
|
||||
@pytest.mark.parametrize("x_size,y_size", binary_arg_sizes)
|
||||
def test_u_op_v(idtype, op, x_size, y_size):
|
||||
ctx = F.ctx()
|
||||
g = dgl.rand_graph(30, 100)
|
||||
g = g.astype(idtype).to(ctx)
|
||||
x = torch.randn((g.num_nodes(),) + x_size, requires_grad=True, device=ctx)
|
||||
y = torch.randn((g.num_nodes(),) + y_size, requires_grad=True, device=ctx)
|
||||
|
||||
f_dgl = getattr(dgl, f"u_{op}_v")
|
||||
z = f_dgl(g, x, y)
|
||||
z.sum().backward()
|
||||
x_grad = x.grad
|
||||
y_grad = y.grad
|
||||
|
||||
x_grad.zero_()
|
||||
y_grad.zero_()
|
||||
u, v = g.edges()
|
||||
f_torch = getattr(torch, op)
|
||||
x_u, y_v = pad_shape(x[u.long()], y[v.long()], x_size, y_size)
|
||||
z_true = f_torch(x_u, y_v)
|
||||
z_true.sum().backward()
|
||||
x_grad_true = x.grad
|
||||
y_grad_true = y.grad
|
||||
|
||||
assert torch.allclose(z, z_true)
|
||||
assert torch.allclose(x_grad, x_grad_true)
|
||||
assert torch.allclose(y_grad, y_grad_true)
|
||||
|
||||
|
||||
@parametrize_idtype
|
||||
@pytest.mark.parametrize("x_size,y_size", dot_arg_sizes)
|
||||
def test_u_dot_v(idtype, x_size, y_size):
|
||||
ctx = F.ctx()
|
||||
g = dgl.rand_graph(30, 100)
|
||||
g = g.astype(idtype).to(ctx)
|
||||
x = torch.randn((g.num_nodes(),) + x_size, requires_grad=True, device=ctx)
|
||||
y = torch.randn((g.num_nodes(),) + y_size, requires_grad=True, device=ctx)
|
||||
|
||||
z = dgl.u_dot_v(g, x, y)
|
||||
z.sum().backward()
|
||||
x_grad = x.grad
|
||||
y_grad = y.grad
|
||||
|
||||
x_grad.zero_()
|
||||
y_grad.zero_()
|
||||
u, v = g.edges()
|
||||
x_u, y_v = pad_shape(x[u.long()], y[v.long()], x_size, y_size)
|
||||
z_true = (x_u * y_v).sum(-1).unsqueeze(-1)
|
||||
z_true.sum().backward()
|
||||
x_grad_true = x.grad
|
||||
y_grad_true = y.grad
|
||||
|
||||
assert torch.allclose(z, z_true, atol=1e-4, rtol=1e-4)
|
||||
assert torch.allclose(x_grad, x_grad_true)
|
||||
assert torch.allclose(y_grad, y_grad_true)
|
||||
@@ -0,0 +1,26 @@
|
||||
import io
|
||||
|
||||
import backend as F
|
||||
|
||||
import dgl.nn.pytorch as nn
|
||||
import pytest
|
||||
from utils import parametrize_idtype
|
||||
from utils.graph_cases import get_cases
|
||||
|
||||
tmp_buffer = io.BytesIO()
|
||||
|
||||
|
||||
@parametrize_idtype
|
||||
@pytest.mark.parametrize("g", get_cases(["homo"], exclude=["zero-degree"]))
|
||||
def test_gatedgcn_conv(g, idtype):
|
||||
ctx = F.ctx()
|
||||
g = g.astype(idtype).to(ctx)
|
||||
gatedgcnconv = nn.GatedGCNConv(10, 10, 5)
|
||||
feat = F.randn((g.num_nodes(), 10))
|
||||
efeat = F.randn((g.num_edges(), 10))
|
||||
gatedgcnconv = gatedgcnconv.to(ctx)
|
||||
|
||||
h, edge_h = gatedgcnconv(g, feat, efeat)
|
||||
# current we only do shape check
|
||||
assert h.shape == (g.number_of_dst_nodes(), 5)
|
||||
assert edge_h.shape == (g.number_of_edges(), 5)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,133 @@
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import backend as F
|
||||
import pytest
|
||||
import torch as th
|
||||
|
||||
from dgl.nn import NodeEmbedding
|
||||
from dgl.optim import SparseAdam
|
||||
|
||||
|
||||
def initializer(emb):
|
||||
th.manual_seed(0)
|
||||
emb.uniform_(-1.0, 1.0)
|
||||
return emb
|
||||
|
||||
|
||||
def check_all_set_all_get_emb(device, init_emb):
|
||||
num_embs = init_emb.shape[0]
|
||||
emb_dim = init_emb.shape[1]
|
||||
dgl_emb = NodeEmbedding(num_embs, emb_dim, "test", device=device)
|
||||
dgl_emb.all_set_embedding(init_emb)
|
||||
|
||||
out_emb = dgl_emb.all_get_embedding()
|
||||
assert F.allclose(init_emb, out_emb)
|
||||
|
||||
|
||||
def check_all_set_all_get_optm_state(
|
||||
device, state_step, state_mem, state_power
|
||||
):
|
||||
num_embs = state_mem.shape[0]
|
||||
emb_dim = state_mem.shape[1]
|
||||
dgl_emb = NodeEmbedding(num_embs, emb_dim, "test", device=device)
|
||||
optm = SparseAdam(params=[dgl_emb], lr=0.01)
|
||||
|
||||
dgl_emb._all_set_optm_state((state_step, state_mem, state_power))
|
||||
|
||||
out_step, out_mem, out_power = dgl_emb._all_get_optm_state()
|
||||
|
||||
assert F.allclose(state_step, out_step)
|
||||
assert F.allclose(state_mem, out_mem)
|
||||
assert F.allclose(state_power, out_power)
|
||||
|
||||
|
||||
def start_sparse_worker(rank, world_size, test, args):
|
||||
print("start sparse worker {}".format(rank))
|
||||
dist_init_method = "tcp://{master_ip}:{master_port}".format(
|
||||
master_ip="127.0.0.1", master_port="12345"
|
||||
)
|
||||
backend = "gloo"
|
||||
device = F.ctx()
|
||||
if device.type == "cuda":
|
||||
device = th.device(rank)
|
||||
th.cuda.set_device(device)
|
||||
th.distributed.init_process_group(
|
||||
backend=backend,
|
||||
init_method=dist_init_method,
|
||||
world_size=world_size,
|
||||
rank=rank,
|
||||
)
|
||||
|
||||
test(device, *args)
|
||||
th.distributed.barrier()
|
||||
th.distributed.destroy_process_group()
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@pytest.mark.parametrize("num_workers", [1, 2, 3])
|
||||
def test_multiprocess_sparse_emb_get_set(num_workers):
|
||||
if F.ctx().type == "cuda" and th.cuda.device_count() < num_workers:
|
||||
pytest.skip("Not enough GPUs to run test.")
|
||||
|
||||
worker_list = []
|
||||
|
||||
init_emb = th.rand([1000, 8])
|
||||
|
||||
ctx = mp.get_context("spawn")
|
||||
for i in range(num_workers):
|
||||
p = ctx.Process(
|
||||
target=start_sparse_worker,
|
||||
args=(i, num_workers, check_all_set_all_get_emb, (init_emb,)),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
for p in worker_list:
|
||||
assert p.exitcode == 0
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@pytest.mark.parametrize("num_workers", [1, 2, 3])
|
||||
def test_multiprocess_sparse_emb_get_set_optm_state(num_workers):
|
||||
if F.ctx().type == "cuda" and th.cuda.device_count() < num_workers:
|
||||
pytest.skip("Not enough GPUs to run test.")
|
||||
|
||||
worker_list = []
|
||||
|
||||
num_embs, emb_dim = 1000, 8
|
||||
state_step = th.randint(1000, (num_embs,))
|
||||
state_mem = th.rand((num_embs, emb_dim))
|
||||
state_power = th.rand((num_embs, emb_dim))
|
||||
|
||||
ctx = mp.get_context("spawn")
|
||||
for i in range(num_workers):
|
||||
p = ctx.Process(
|
||||
target=start_sparse_worker,
|
||||
args=(
|
||||
i,
|
||||
num_workers,
|
||||
check_all_set_all_get_optm_state,
|
||||
(state_step, state_mem, state_power),
|
||||
),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
for p in worker_list:
|
||||
assert p.exitcode == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# test_multiprocess_sparse_emb_get_set(1)
|
||||
# test_multiprocess_sparse_emb_get_set(2)
|
||||
# test_multiprocess_sparse_emb_get_set(3)
|
||||
|
||||
test_multiprocess_sparse_emb_get_set_optm_state(1)
|
||||
# test_multiprocess_sparse_emb_get_set_optm_state(2)
|
||||
# test_multiprocess_sparse_emb_get_set_optm_state(3)
|
||||
@@ -0,0 +1,698 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import backend as F
|
||||
import pytest
|
||||
import torch as th
|
||||
import torch.multiprocessing as mp
|
||||
|
||||
from dgl.nn import NodeEmbedding
|
||||
from dgl.optim import SparseAdagrad, SparseAdam
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@pytest.mark.parametrize("emb_dim", [1, 4, 101, 1024])
|
||||
def test_sparse_adam(emb_dim):
|
||||
num_embs = 10
|
||||
device = F.ctx()
|
||||
dgl_emb = NodeEmbedding(num_embs, emb_dim, "test")
|
||||
torch_emb = th.nn.Embedding(num_embs, emb_dim, sparse=True)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(torch_emb.weight, 0, 1.0)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(dgl_emb.weight, 0, 1.0)
|
||||
|
||||
dgl_adam = SparseAdam(params=[dgl_emb], lr=0.01)
|
||||
torch_adam = th.optim.SparseAdam(list(torch_emb.parameters()), lr=0.01)
|
||||
|
||||
# first step
|
||||
idx = th.randint(0, num_embs, size=(4,))
|
||||
dgl_value = dgl_emb(idx, device).to(th.device("cpu"))
|
||||
torch_value = torch_emb(idx)
|
||||
labels = th.zeros((4,)).long()
|
||||
print("dgl_value = {}".format(dgl_value))
|
||||
print("labels = {}".format(labels))
|
||||
|
||||
dgl_adam.zero_grad()
|
||||
torch_adam.zero_grad()
|
||||
dgl_loss = th.nn.functional.cross_entropy(dgl_value, labels)
|
||||
torch_loss = th.nn.functional.cross_entropy(torch_value, labels)
|
||||
dgl_loss.backward()
|
||||
torch_loss.backward()
|
||||
|
||||
dgl_adam.step()
|
||||
torch_adam.step()
|
||||
assert F.allclose(dgl_emb.weight, torch_emb.weight)
|
||||
|
||||
# Can not test second step
|
||||
# Pytorch sparseAdam maintains a global step
|
||||
# DGL sparseAdam use a per embedding step
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@pytest.mark.parametrize("use_uva", [False, True, None])
|
||||
@pytest.mark.parametrize("emb_dim", [1, 4, 101, 1024])
|
||||
def test_sparse_adam_uva(use_uva, emb_dim):
|
||||
if F.ctx().type == "cpu" and use_uva == True:
|
||||
# we want to only test values of False and None when not using GPU
|
||||
pytest.skip("UVA cannot be used without GPUs.")
|
||||
|
||||
num_embs = 10
|
||||
device = F.ctx()
|
||||
dgl_emb = NodeEmbedding(num_embs, emb_dim, "test_uva{}".format(use_uva))
|
||||
torch_emb = th.nn.Embedding(num_embs, emb_dim, sparse=True)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(torch_emb.weight, 0, 1.0)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(dgl_emb.weight, 0, 1.0)
|
||||
|
||||
dgl_adam = SparseAdam(params=[dgl_emb], lr=0.01, use_uva=use_uva)
|
||||
torch_adam = th.optim.SparseAdam(list(torch_emb.parameters()), lr=0.01)
|
||||
|
||||
# first step
|
||||
idx = th.randint(0, num_embs, size=(4,))
|
||||
dgl_value = dgl_emb(idx, device).to(th.device("cpu"))
|
||||
torch_value = torch_emb(idx)
|
||||
labels = th.zeros((4,)).long()
|
||||
|
||||
dgl_adam.zero_grad()
|
||||
torch_adam.zero_grad()
|
||||
dgl_loss = th.nn.functional.cross_entropy(dgl_value, labels)
|
||||
torch_loss = th.nn.functional.cross_entropy(torch_value, labels)
|
||||
dgl_loss.backward()
|
||||
torch_loss.backward()
|
||||
|
||||
dgl_adam.step()
|
||||
torch_adam.step()
|
||||
assert F.allclose(dgl_emb.weight, torch_emb.weight)
|
||||
|
||||
# Can not test second step
|
||||
# Pytorch sparseAdam maintains a global step
|
||||
# DGL sparseAdam use a per embedding step
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@pytest.mark.parametrize("dtype", [th.float32, th.float16])
|
||||
@pytest.mark.parametrize("emb_dim", [1, 4, 101, 1024])
|
||||
def test_sparse_adam_dtype(dtype, emb_dim):
|
||||
num_embs = 10
|
||||
device = F.ctx()
|
||||
dgl_emb = NodeEmbedding(num_embs, emb_dim, "test_dtype{}".format(dtype))
|
||||
torch_emb = th.nn.Embedding(num_embs, emb_dim, sparse=True)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(torch_emb.weight, 0, 1.0)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(dgl_emb.weight, 0, 1.0)
|
||||
|
||||
dgl_adam = SparseAdam(params=[dgl_emb], lr=0.01, dtype=dtype)
|
||||
torch_adam = th.optim.SparseAdam(list(torch_emb.parameters()), lr=0.01)
|
||||
|
||||
# first step
|
||||
idx = th.randint(0, num_embs, size=(4,))
|
||||
dgl_value = dgl_emb(idx, device).to(th.device("cpu"))
|
||||
torch_value = torch_emb(idx)
|
||||
labels = th.zeros((4,)).long()
|
||||
|
||||
dgl_adam.zero_grad()
|
||||
torch_adam.zero_grad()
|
||||
dgl_loss = th.nn.functional.cross_entropy(dgl_value, labels)
|
||||
torch_loss = th.nn.functional.cross_entropy(torch_value, labels)
|
||||
dgl_loss.backward()
|
||||
torch_loss.backward()
|
||||
|
||||
dgl_adam.step()
|
||||
torch_adam.step()
|
||||
assert F.allclose(dgl_emb.weight, torch_emb.weight)
|
||||
|
||||
# Can not test second step
|
||||
# Pytorch sparseAdam maintains a global step
|
||||
# DGL sparseAdam use a per embedding step
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
def test_sparse_adam_zero_step():
|
||||
num_embs = 10
|
||||
emb_dim = 4
|
||||
device = F.ctx()
|
||||
dgl_emb = NodeEmbedding(num_embs, emb_dim, "test")
|
||||
torch_emb = th.nn.Embedding(num_embs, emb_dim, sparse=True)
|
||||
dgl_emb_zero = NodeEmbedding(num_embs, emb_dim, "test2")
|
||||
torch_emb_zero = th.nn.Embedding(num_embs, emb_dim, sparse=True)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(torch_emb.weight, 0, 1.0)
|
||||
th.nn.init.uniform_(torch_emb_zero.weight, 0, 1.0)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(dgl_emb.weight, 0, 1.0)
|
||||
th.nn.init.uniform_(dgl_emb_zero.weight, 0, 1.0)
|
||||
|
||||
dgl_adam = SparseAdam(params=[dgl_emb, dgl_emb_zero], lr=0.01)
|
||||
torch_adam = th.optim.SparseAdam(
|
||||
list(torch_emb.parameters()) + list(torch_emb_zero.parameters()),
|
||||
lr=0.01,
|
||||
)
|
||||
|
||||
# first step
|
||||
idx = th.randint(0, num_embs, size=(4,))
|
||||
dgl_value = dgl_emb(idx, device).to(th.device("cpu"))
|
||||
torch_value = torch_emb(idx)
|
||||
labels = th.ones((4,)).long()
|
||||
|
||||
dgl_adam.zero_grad()
|
||||
torch_adam.zero_grad()
|
||||
dgl_loss = th.nn.functional.cross_entropy(dgl_value, labels)
|
||||
torch_loss = th.nn.functional.cross_entropy(torch_value, labels)
|
||||
dgl_loss.backward()
|
||||
torch_loss.backward()
|
||||
|
||||
dgl_adam.step()
|
||||
torch_adam.step()
|
||||
assert F.allclose(dgl_emb.weight, torch_emb.weight)
|
||||
|
||||
|
||||
def initializer(emb):
|
||||
th.manual_seed(0)
|
||||
emb.uniform_(-1.0, 1.0)
|
||||
return emb
|
||||
|
||||
|
||||
def start_sparse_adam_worker(
|
||||
rank,
|
||||
device,
|
||||
world_size,
|
||||
weight,
|
||||
tensor_dev="cpu",
|
||||
has_zero_grad=False,
|
||||
backend="gloo",
|
||||
num_embs=128,
|
||||
emb_dim=10,
|
||||
zero_comm=True,
|
||||
):
|
||||
print("start sparse worker for adam {}".format(rank))
|
||||
dist_init_method = "tcp://{master_ip}:{master_port}".format(
|
||||
master_ip="127.0.0.1", master_port="12345"
|
||||
)
|
||||
|
||||
if device.type == "cuda":
|
||||
th.cuda.set_device(device)
|
||||
|
||||
th.distributed.init_process_group(
|
||||
backend=backend,
|
||||
init_method=dist_init_method,
|
||||
world_size=world_size,
|
||||
rank=rank,
|
||||
)
|
||||
|
||||
init_weight = th.empty((num_embs, emb_dim))
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(init_weight, -1.0, 1.0)
|
||||
dgl_emb = NodeEmbedding(
|
||||
num_embs, emb_dim, "test", init_func=initializer, device=tensor_dev
|
||||
)
|
||||
dgl_emb.all_set_embedding(init_weight)
|
||||
|
||||
if has_zero_grad:
|
||||
dgl_emb_zero = NodeEmbedding(
|
||||
num_embs, emb_dim, "zero", init_func=initializer, device=tensor_dev
|
||||
)
|
||||
dgl_adam = SparseAdam(params=[dgl_emb, dgl_emb_zero], lr=0.01)
|
||||
else:
|
||||
dgl_adam = SparseAdam(params=[dgl_emb], lr=0.01)
|
||||
|
||||
th.manual_seed(rank)
|
||||
if zero_comm:
|
||||
start = (num_embs // world_size) * rank
|
||||
end = (num_embs // world_size) * (rank + 1)
|
||||
idx = th.randint(start, end, size=(4,)).to(tensor_dev)
|
||||
else:
|
||||
idx = th.randint(0, num_embs, size=(4,)).to(tensor_dev)
|
||||
dgl_value = dgl_emb(idx, device)
|
||||
labels = th.ones((4,)).long().to(device)
|
||||
dgl_loss = th.nn.functional.cross_entropy(dgl_value, labels)
|
||||
dgl_adam.zero_grad()
|
||||
dgl_loss.backward()
|
||||
dgl_adam.step()
|
||||
th.distributed.barrier()
|
||||
dgl_weight = dgl_emb.all_get_embedding().detach()
|
||||
after_step = dgl_emb(idx, device).cpu()
|
||||
|
||||
if rank == 0:
|
||||
dgl_value = dgl_value.detach().cpu()
|
||||
assert F.allclose(dgl_value, after_step) is False
|
||||
weight[:] = dgl_weight[:]
|
||||
th.distributed.barrier()
|
||||
|
||||
|
||||
def start_torch_adam_worker(
|
||||
rank,
|
||||
world_size,
|
||||
weight,
|
||||
has_zero_grad=False,
|
||||
num_embs=128,
|
||||
emb_dim=10,
|
||||
zero_comm=True,
|
||||
):
|
||||
print("start sparse worker for adam {}".format(rank))
|
||||
dist_init_method = "tcp://{master_ip}:{master_port}".format(
|
||||
master_ip="127.0.0.1", master_port="12345"
|
||||
)
|
||||
backend = "gloo"
|
||||
|
||||
th.distributed.init_process_group(
|
||||
backend=backend,
|
||||
init_method=dist_init_method,
|
||||
world_size=world_size,
|
||||
rank=rank,
|
||||
)
|
||||
|
||||
torch_emb = th.nn.Embedding(num_embs, emb_dim, sparse=True)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(torch_emb.weight, -1.0, 1.0)
|
||||
torch_emb = th.nn.parallel.DistributedDataParallel(torch_emb)
|
||||
if has_zero_grad:
|
||||
torch_emb_zero = th.nn.Embedding(num_embs, emb_dim, sparse=True)
|
||||
torch_emb_zero = torch_emb_zero.to(tensor_dev)
|
||||
th.manual_seed(0)
|
||||
th.nn.init.uniform_(torch_emb_zero.weight, -1.0, 1.0)
|
||||
torch_emb_zero = th.nn.parallel.DistributedDataParallel(torch_emb_zero)
|
||||
torch_adam = th.optim.SparseAdam(
|
||||
list(torch_emb.module.parameters())
|
||||
+ list(torch_emb_zero.module.parameters()),
|
||||
lr=0.01,
|
||||
)
|
||||
else:
|
||||
torch_adam = th.optim.SparseAdam(
|
||||
list(torch_emb.module.parameters()), lr=0.01
|
||||
)
|
||||
|
||||
th.manual_seed(rank)
|
||||
if zero_comm:
|
||||
start = (num_embs // world_size) * rank
|
||||
end = (num_embs // world_size) * (rank + 1)
|
||||
idx = th.randint(start, end, size=(4,))
|
||||
else:
|
||||
idx = th.randint(0, num_embs, size=(4,))
|
||||
labels = th.ones((4,)).long()
|
||||
torch_value = torch_emb(idx)
|
||||
torch_loss = th.nn.functional.cross_entropy(torch_value, labels)
|
||||
torch_adam.zero_grad()
|
||||
torch_loss.backward()
|
||||
torch_adam.step()
|
||||
th.distributed.barrier()
|
||||
|
||||
if rank == 0:
|
||||
weight[:] = torch_emb.module.weight.cpu()[:]
|
||||
th.distributed.barrier()
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@unittest.skipIf(F.ctx().type != "cpu", reason="cpu only test")
|
||||
@pytest.mark.parametrize("num_workers", [2, 4])
|
||||
def test_multiprocess_cpu_sparse_adam(num_workers):
|
||||
backend = "gloo"
|
||||
worker_list = []
|
||||
num_embs = 128
|
||||
emb_dim = 10
|
||||
dgl_weight = th.empty((num_embs, emb_dim))
|
||||
ctx = mp.get_context("spawn")
|
||||
for i in range(num_workers):
|
||||
device = F.ctx()
|
||||
p = ctx.Process(
|
||||
target=start_sparse_adam_worker,
|
||||
args=(
|
||||
i,
|
||||
device,
|
||||
num_workers,
|
||||
dgl_weight,
|
||||
th.device("cpu"),
|
||||
True,
|
||||
backend,
|
||||
),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
|
||||
worker_list = []
|
||||
torch_weight = th.empty((num_embs, emb_dim))
|
||||
for i in range(num_workers):
|
||||
p = ctx.Process(
|
||||
target=start_torch_adam_worker,
|
||||
args=(i, num_workers, torch_weight, False),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
|
||||
assert F.allclose(dgl_weight, torch_weight)
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@unittest.skipIf(F.ctx().type == "cpu", reason="gpu only test")
|
||||
@pytest.mark.parametrize("num_workers", [2, 4, 8])
|
||||
@pytest.mark.parametrize("backend", ["nccl", "gloo"])
|
||||
@pytest.mark.parametrize("zero_comm", [True, False])
|
||||
def test_multiprocess_sparse_adam(num_workers, backend, zero_comm):
|
||||
if F.ctx().type == "cuda" and th.cuda.device_count() < num_workers:
|
||||
pytest.skip("Not enough GPUs to run test.")
|
||||
|
||||
worker_list = []
|
||||
num_embs = 128
|
||||
emb_dim = 10
|
||||
dgl_weight = th.empty((num_embs, emb_dim))
|
||||
ctx = mp.get_context("spawn")
|
||||
for i in range(num_workers):
|
||||
device = F.ctx()
|
||||
if device.type == "cuda":
|
||||
# make sure each process has a unique GPU
|
||||
device = th.device(i)
|
||||
p = ctx.Process(
|
||||
target=start_sparse_adam_worker,
|
||||
args=(
|
||||
i,
|
||||
device,
|
||||
num_workers,
|
||||
dgl_weight,
|
||||
th.device("cpu"),
|
||||
True,
|
||||
backend,
|
||||
num_embs,
|
||||
emb_dim,
|
||||
zero_comm,
|
||||
),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
|
||||
worker_list = []
|
||||
torch_weight = th.empty((num_embs, emb_dim))
|
||||
for i in range(num_workers):
|
||||
p = ctx.Process(
|
||||
target=start_torch_adam_worker,
|
||||
args=(
|
||||
i,
|
||||
num_workers,
|
||||
torch_weight,
|
||||
False,
|
||||
num_embs,
|
||||
emb_dim,
|
||||
zero_comm,
|
||||
),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
|
||||
assert F.allclose(dgl_weight, torch_weight)
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@unittest.skipIf(
|
||||
F.ctx().type == "cpu", reason="cuda tensor is not supported for cpu"
|
||||
)
|
||||
@pytest.mark.parametrize("num_workers", [2, 4, 8])
|
||||
def test_multiprocess_sparse_adam_cuda_tensor(num_workers):
|
||||
if F.ctx().type == "cpu":
|
||||
pytest.skip("Do not test CPU")
|
||||
if F.ctx().type == "cuda" and th.cuda.device_count() < num_workers:
|
||||
pytest.skip("Not enough GPUs to run test.")
|
||||
|
||||
backend = "nccl"
|
||||
worker_list = []
|
||||
num_embs = 128
|
||||
emb_dim = 10
|
||||
dgl_weight = th.empty((num_embs, emb_dim))
|
||||
ctx = mp.get_context("spawn")
|
||||
for i in range(num_workers):
|
||||
device = th.device(i)
|
||||
p = ctx.Process(
|
||||
target=start_sparse_adam_worker,
|
||||
args=(i, device, num_workers, dgl_weight, device, False, backend),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
|
||||
worker_list = []
|
||||
torch_weight = th.empty((num_embs, emb_dim))
|
||||
for i in range(num_workers):
|
||||
p = ctx.Process(
|
||||
target=start_torch_adam_worker,
|
||||
args=(i, num_workers, torch_weight, False),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
|
||||
assert F.allclose(dgl_weight, torch_weight)
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@unittest.skipIf(F.ctx().type != "cpu", reason="cpu only test")
|
||||
@pytest.mark.parametrize("num_workers", [2, 4])
|
||||
def test_multiprocess_sparse_adam_cpu_zero_step(num_workers):
|
||||
backend = "gloo"
|
||||
|
||||
worker_list = []
|
||||
num_embs = 128
|
||||
emb_dim = 10
|
||||
dgl_weight = th.empty((num_embs, emb_dim))
|
||||
ctx = mp.get_context("spawn")
|
||||
for i in range(num_workers):
|
||||
device = F.ctx()
|
||||
p = ctx.Process(
|
||||
target=start_sparse_adam_worker,
|
||||
args=(
|
||||
i,
|
||||
device,
|
||||
num_workers,
|
||||
dgl_weight,
|
||||
th.device("cpu"),
|
||||
True,
|
||||
backend,
|
||||
),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
|
||||
worker_list = []
|
||||
torch_weight = th.empty((num_embs, emb_dim))
|
||||
for i in range(num_workers):
|
||||
p = ctx.Process(
|
||||
target=start_torch_adam_worker,
|
||||
args=(i, num_workers, torch_weight, False),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
|
||||
assert F.allclose(dgl_weight, torch_weight)
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@unittest.skipIf(F.ctx().type == "cpu", reason="gpu only test")
|
||||
@pytest.mark.parametrize("num_workers", [2, 4, 8])
|
||||
@pytest.mark.parametrize("backend", ["nccl", "gloo"])
|
||||
def test_multiprocess_sparse_adam_zero_step(num_workers, backend):
|
||||
if F.ctx().type == "cuda" and th.cuda.device_count() < num_workers:
|
||||
pytest.skip("Not enough GPUs to run test.")
|
||||
|
||||
worker_list = []
|
||||
num_embs = 128
|
||||
emb_dim = 10
|
||||
dgl_weight = th.empty((num_embs, emb_dim))
|
||||
ctx = mp.get_context("spawn")
|
||||
for i in range(num_workers):
|
||||
device = F.ctx()
|
||||
if device.type == "cuda":
|
||||
# make sure each process has a unique GPU
|
||||
device = th.device(i)
|
||||
p = ctx.Process(
|
||||
target=start_sparse_adam_worker,
|
||||
args=(
|
||||
i,
|
||||
device,
|
||||
num_workers,
|
||||
dgl_weight,
|
||||
th.device("cpu"),
|
||||
True,
|
||||
backend,
|
||||
),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
|
||||
worker_list = []
|
||||
torch_weight = th.empty((num_embs, emb_dim))
|
||||
for i in range(num_workers):
|
||||
p = ctx.Process(
|
||||
target=start_torch_adam_worker,
|
||||
args=(i, num_workers, torch_weight, False),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
|
||||
assert F.allclose(dgl_weight, torch_weight)
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@unittest.skipIf(
|
||||
F.ctx().type == "cpu", reason="cuda tensor is not supported for cpu"
|
||||
)
|
||||
@pytest.mark.parametrize("num_workers", [2, 4, 8])
|
||||
def test_multiprocess_sparse_adam_zero_step_cuda_tensor(num_workers):
|
||||
if F.ctx().type == "cuda" and th.cuda.device_count() < num_workers:
|
||||
pytest.skip("Not enough GPUs to run test.")
|
||||
|
||||
backend = "nccl"
|
||||
worker_list = []
|
||||
num_embs = 128
|
||||
emb_dim = 10
|
||||
dgl_weight = th.empty((num_embs, emb_dim))
|
||||
ctx = mp.get_context("spawn")
|
||||
for i in range(num_workers):
|
||||
device = th.device(i)
|
||||
p = ctx.Process(
|
||||
target=start_sparse_adam_worker,
|
||||
args=(i, device, num_workers, dgl_weight, device, True, backend),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
|
||||
worker_list = []
|
||||
torch_weight = th.empty((num_embs, emb_dim))
|
||||
for i in range(num_workers):
|
||||
p = ctx.Process(
|
||||
target=start_torch_adam_worker,
|
||||
args=(i, num_workers, torch_weight, False),
|
||||
)
|
||||
p.start()
|
||||
worker_list.append(p)
|
||||
for p in worker_list:
|
||||
p.join()
|
||||
|
||||
assert F.allclose(dgl_weight, torch_weight)
|
||||
|
||||
|
||||
def start_sparse_adam_state_dict_worker(
|
||||
rank,
|
||||
world_size,
|
||||
init_weight,
|
||||
backend,
|
||||
num_embs,
|
||||
emb_dim,
|
||||
):
|
||||
print("start sparse worker for adam {}".format(rank))
|
||||
dist_init_method = "tcp://{master_ip}:{master_port}".format(
|
||||
master_ip="127.0.0.1", master_port="12345"
|
||||
)
|
||||
|
||||
device = th.device(f"cuda:{rank}")
|
||||
th.cuda.set_device(device)
|
||||
tensor_dev = device if backend == "nccl" else th.device("cpu")
|
||||
|
||||
th.distributed.init_process_group(
|
||||
backend=backend,
|
||||
init_method=dist_init_method,
|
||||
world_size=world_size,
|
||||
rank=rank,
|
||||
)
|
||||
|
||||
th.manual_seed(0)
|
||||
dgl_emb = NodeEmbedding(
|
||||
num_embs, emb_dim, "test", init_func=initializer, device=tensor_dev
|
||||
)
|
||||
dgl_emb.all_set_embedding(init_weight)
|
||||
|
||||
dgl_adam = SparseAdam(params=[dgl_emb], lr=0.01)
|
||||
|
||||
start = (num_embs // world_size) * rank
|
||||
end = (num_embs // world_size) * (rank + 1)
|
||||
th.manual_seed(rank)
|
||||
idx = th.randint(start, end, size=(4,)).to(tensor_dev)
|
||||
dgl_value = dgl_emb(idx, device)
|
||||
labels = th.ones((4,)).long().to(device)
|
||||
dgl_loss = th.nn.functional.cross_entropy(dgl_value, labels)
|
||||
dgl_adam.zero_grad()
|
||||
dgl_loss.backward()
|
||||
dgl_adam.step()
|
||||
th.distributed.barrier()
|
||||
|
||||
worker_state_dict = [t.detach().clone() for t in dgl_emb.optm_state]
|
||||
state_dict = dgl_adam.state_dict()
|
||||
for t in dgl_emb.optm_state:
|
||||
t.zero_()
|
||||
dgl_adam.load_state_dict(state_dict)
|
||||
|
||||
for i, j in zip(worker_state_dict, dgl_emb.optm_state):
|
||||
F.allclose(i, j)
|
||||
|
||||
th.distributed.barrier()
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
@unittest.skipIf(F.ctx().type == "cpu", reason="gpu only test")
|
||||
@pytest.mark.parametrize("num_workers", [1, 2, 4, 8])
|
||||
@pytest.mark.parametrize("backend", ["nccl", "gloo"])
|
||||
def test_multiprocess_sparse_adam_state_dict(num_workers, backend):
|
||||
if F.ctx().type == "cuda" and th.cuda.device_count() < num_workers:
|
||||
pytest.skip("Not enough GPUs to run test.")
|
||||
|
||||
num_embs = 128
|
||||
emb_dim = 10
|
||||
init_weight = th.rand((num_embs, emb_dim))
|
||||
mp.spawn(
|
||||
start_sparse_adam_state_dict_worker,
|
||||
(
|
||||
num_workers,
|
||||
init_weight,
|
||||
backend,
|
||||
num_embs,
|
||||
emb_dim,
|
||||
),
|
||||
nprocs=num_workers,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_sparse_adam(1)
|
||||
test_sparse_adam(4)
|
||||
test_sparse_adam(101)
|
||||
test_sparse_adam(1024)
|
||||
test_sparse_adam_zero_step()
|
||||
|
||||
test_multiprocess_cpu_sparse_adam(2)
|
||||
test_multiprocess_cpu_sparse_adam(4)
|
||||
test_multiprocess_cpu_sparse_adam(8)
|
||||
test_multiprocess_sparse_adam_cpu_zero_step(2)
|
||||
|
||||
test_multiprocess_sparse_adam(2, backend="gloo")
|
||||
test_multiprocess_sparse_adam(4, backend="gloo")
|
||||
test_multiprocess_sparse_adam(8, backend="gloo")
|
||||
test_multiprocess_sparse_adam(2, backend="nccl")
|
||||
test_multiprocess_sparse_adam(4, backend="nccl")
|
||||
test_multiprocess_sparse_adam(8, backend="nccl")
|
||||
|
||||
test_multiprocess_sparse_adam_zero_step(2, backend="gloo")
|
||||
test_multiprocess_sparse_adam_zero_step(4, backend="nccl")
|
||||
|
||||
test_multiprocess_sparse_adam_cuda_tensor(2)
|
||||
test_multiprocess_sparse_adam_zero_step_cuda_tensor(4)
|
||||
|
||||
test_multiprocess_sparse_adam_state_dict(2, "nccl")
|
||||
test_multiprocess_sparse_adam_state_dict(2, "gloo")
|
||||
@@ -0,0 +1 @@
|
||||
""" DGL sparse tests"""
|
||||
@@ -0,0 +1,45 @@
|
||||
import operator
|
||||
|
||||
import backend as F
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from dgl.sparse import sp_broadcast_v
|
||||
|
||||
from .utils import rand_coo
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [(3, 4), (1, 5), (5, 1)])
|
||||
@pytest.mark.parametrize("nnz", [1, 4])
|
||||
@pytest.mark.parametrize("nz_dim", [None, 2])
|
||||
@pytest.mark.parametrize("op", ["add", "sub", "mul", "truediv"])
|
||||
def test_sp_broadcast_v(shape, nnz, nz_dim, op):
|
||||
dev = F.ctx()
|
||||
A = rand_coo(shape, nnz, dev, nz_dim)
|
||||
|
||||
v = torch.randn(A.shape[1], device=dev)
|
||||
res1 = sp_broadcast_v(A, v, op)
|
||||
if A.val.dim() == 1:
|
||||
rhs = v[A.col]
|
||||
else:
|
||||
rhs = v[A.col].view(-1, 1)
|
||||
res2 = getattr(operator, op)(A.val, rhs)
|
||||
assert torch.allclose(res1.val, res2)
|
||||
|
||||
v = torch.randn(1, A.shape[1], device=dev)
|
||||
res1 = sp_broadcast_v(A, v, op)
|
||||
if A.val.dim() == 1:
|
||||
rhs = v.view(-1)[A.col]
|
||||
else:
|
||||
rhs = v.view(-1)[A.col].view(-1, 1)
|
||||
res2 = getattr(operator, op)(A.val, rhs)
|
||||
assert torch.allclose(res1.val, res2)
|
||||
|
||||
v = torch.randn(A.shape[0], 1, device=dev)
|
||||
res1 = sp_broadcast_v(A, v, op)
|
||||
if A.val.dim() == 1:
|
||||
rhs = v.view(-1)[A.row]
|
||||
else:
|
||||
rhs = v.view(-1)[A.row].view(-1, 1)
|
||||
res2 = getattr(operator, op)(A.val, rhs)
|
||||
assert torch.allclose(res1.val, res2)
|
||||
@@ -0,0 +1,242 @@
|
||||
import operator
|
||||
|
||||
import backend as F
|
||||
|
||||
import dgl.sparse as dglsp
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from dgl.sparse import diag, power
|
||||
|
||||
|
||||
@pytest.mark.parametrize("opname", ["add", "sub", "mul", "truediv"])
|
||||
def test_diag_op_diag(opname):
|
||||
op = getattr(operator, opname)
|
||||
ctx = F.ctx()
|
||||
shape = (3, 4)
|
||||
D1 = diag(torch.arange(1, 4).to(ctx), shape=shape)
|
||||
D2 = diag(torch.arange(10, 13).to(ctx), shape=shape)
|
||||
result = op(D1, D2)
|
||||
assert torch.allclose(result.val, op(D1.val, D2.val), rtol=1e-4, atol=1e-4)
|
||||
assert result.shape == D1.shape
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"v_scalar", [2, 2.5, torch.tensor(2), torch.tensor(2.5)]
|
||||
)
|
||||
def test_diag_op_scalar(v_scalar):
|
||||
ctx = F.ctx()
|
||||
shape = (3, 4)
|
||||
D1 = diag(torch.arange(1, 4).to(ctx), shape=shape)
|
||||
|
||||
# D * v
|
||||
D2 = D1 * v_scalar
|
||||
assert torch.allclose(D1.val * v_scalar, D2.val, rtol=1e-4, atol=1e-4)
|
||||
assert D1.shape == D2.shape
|
||||
|
||||
# v * D
|
||||
D2 = v_scalar * D1
|
||||
assert torch.allclose(v_scalar * D1.val, D2.val, rtol=1e-4, atol=1e-4)
|
||||
assert D1.shape == D2.shape
|
||||
|
||||
# D / v
|
||||
D2 = D1 / v_scalar
|
||||
assert torch.allclose(D1.val / v_scalar, D2.val, rtol=1e-4, atol=1e-4)
|
||||
assert D1.shape == D2.shape
|
||||
|
||||
# D ^ v
|
||||
D1 = diag(torch.arange(1, 4).to(ctx))
|
||||
D2 = D1**v_scalar
|
||||
assert torch.allclose(D1.val**v_scalar, D2.val, rtol=1e-4, atol=1e-4)
|
||||
assert D1.shape == D2.shape
|
||||
|
||||
# pow(D, v)
|
||||
D2 = power(D1, v_scalar)
|
||||
assert torch.allclose(D1.val**v_scalar, D2.val, rtol=1e-4, atol=1e-4)
|
||||
assert D1.shape == D2.shape
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
D1 + v_scalar
|
||||
with pytest.raises(TypeError):
|
||||
v_scalar + D1
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
D1 - v_scalar
|
||||
with pytest.raises(TypeError):
|
||||
v_scalar - D1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("val_shape", [(), (2,)])
|
||||
@pytest.mark.parametrize("opname", ["add", "sub"])
|
||||
def test_addsub_coo(val_shape, opname):
|
||||
op = getattr(operator, opname)
|
||||
func = getattr(dglsp, opname)
|
||||
ctx = F.ctx()
|
||||
row = torch.tensor([1, 0, 2]).to(ctx)
|
||||
col = torch.tensor([0, 3, 2]).to(ctx)
|
||||
val = torch.randn(row.shape + val_shape).to(ctx)
|
||||
A = dglsp.from_coo(row, col, val)
|
||||
|
||||
row = torch.tensor([1, 0]).to(ctx)
|
||||
col = torch.tensor([0, 2]).to(ctx)
|
||||
val = torch.randn(row.shape + val_shape).to(ctx)
|
||||
B = dglsp.from_coo(row, col, val, shape=A.shape)
|
||||
|
||||
C1 = op(A, B).to_dense()
|
||||
C2 = func(A, B).to_dense()
|
||||
dense_C = op(A.to_dense(), B.to_dense())
|
||||
|
||||
assert torch.allclose(dense_C, C1)
|
||||
assert torch.allclose(dense_C, C2)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
op(A, 2)
|
||||
with pytest.raises(TypeError):
|
||||
op(2, A)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("val_shape", [(), (2,)])
|
||||
@pytest.mark.parametrize("opname", ["add", "sub"])
|
||||
def test_addsub_csr(val_shape, opname):
|
||||
op = getattr(operator, opname)
|
||||
func = getattr(dglsp, opname)
|
||||
ctx = F.ctx()
|
||||
indptr = torch.tensor([0, 1, 2, 3]).to(ctx)
|
||||
indices = torch.tensor([3, 0, 2]).to(ctx)
|
||||
val = torch.randn(indices.shape + val_shape).to(ctx)
|
||||
A = dglsp.from_csr(indptr, indices, val)
|
||||
|
||||
indptr = torch.tensor([0, 1, 2, 2]).to(ctx)
|
||||
indices = torch.tensor([2, 0]).to(ctx)
|
||||
val = torch.randn(indices.shape + val_shape).to(ctx)
|
||||
B = dglsp.from_csr(indptr, indices, val, shape=A.shape)
|
||||
|
||||
C1 = op(A, B).to_dense()
|
||||
C2 = func(A, B).to_dense()
|
||||
dense_C = op(A.to_dense(), B.to_dense())
|
||||
|
||||
assert torch.allclose(dense_C, C1)
|
||||
assert torch.allclose(dense_C, C2)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
op(A, 2)
|
||||
with pytest.raises(TypeError):
|
||||
op(2, A)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("val_shape", [(), (2,)])
|
||||
@pytest.mark.parametrize("opname", ["add", "sub"])
|
||||
def test_addsub_csc(val_shape, opname):
|
||||
op = getattr(operator, opname)
|
||||
func = getattr(dglsp, opname)
|
||||
ctx = F.ctx()
|
||||
indptr = torch.tensor([0, 1, 1, 2, 3]).to(ctx)
|
||||
indices = torch.tensor([1, 2, 0]).to(ctx)
|
||||
val = torch.randn(indices.shape + val_shape).to(ctx)
|
||||
A = dglsp.from_csc(indptr, indices, val)
|
||||
|
||||
indptr = torch.tensor([0, 1, 1, 2, 2]).to(ctx)
|
||||
indices = torch.tensor([1, 0]).to(ctx)
|
||||
val = torch.randn(indices.shape + val_shape).to(ctx)
|
||||
B = dglsp.from_csc(indptr, indices, val, shape=A.shape)
|
||||
|
||||
C1 = op(A, B).to_dense()
|
||||
C2 = func(A, B).to_dense()
|
||||
dense_C = op(A.to_dense(), B.to_dense())
|
||||
|
||||
assert torch.allclose(dense_C, C1)
|
||||
assert torch.allclose(dense_C, C2)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
op(A, 2)
|
||||
with pytest.raises(TypeError):
|
||||
op(2, A)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("val_shape", [(), (2,)])
|
||||
@pytest.mark.parametrize("opname", ["add", "sub"])
|
||||
def test_addsub_diag(val_shape, opname):
|
||||
op = getattr(operator, opname)
|
||||
func = getattr(dglsp, opname)
|
||||
ctx = F.ctx()
|
||||
shape = (3, 4)
|
||||
val_shape = (shape[0],) + val_shape
|
||||
D1 = dglsp.diag(torch.randn(val_shape).to(ctx), shape=shape)
|
||||
D2 = dglsp.diag(torch.randn(val_shape).to(ctx), shape=shape)
|
||||
|
||||
C1 = op(D1, D2).to_dense()
|
||||
C2 = func(D1, D2).to_dense()
|
||||
dense_C = op(D1.to_dense(), D2.to_dense())
|
||||
|
||||
assert torch.allclose(dense_C, C1)
|
||||
assert torch.allclose(dense_C, C2)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
op(D1, 2)
|
||||
with pytest.raises(TypeError):
|
||||
op(2, D1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("val_shape", [(), (2,)])
|
||||
def test_add_sparse_diag(val_shape):
|
||||
ctx = F.ctx()
|
||||
row = torch.tensor([1, 0, 2]).to(ctx)
|
||||
col = torch.tensor([0, 3, 2]).to(ctx)
|
||||
val = torch.randn(row.shape + val_shape).to(ctx)
|
||||
A = dglsp.from_coo(row, col, val)
|
||||
|
||||
shape = (3, 4)
|
||||
val_shape = (shape[0],) + val_shape
|
||||
D = dglsp.diag(torch.randn(val_shape).to(ctx), shape=shape)
|
||||
|
||||
sum1 = (A + D).to_dense()
|
||||
sum2 = (D + A).to_dense()
|
||||
sum3 = dglsp.add(A, D).to_dense()
|
||||
sum4 = dglsp.add(D, A).to_dense()
|
||||
dense_sum = A.to_dense() + D.to_dense()
|
||||
|
||||
assert torch.allclose(dense_sum, sum1)
|
||||
assert torch.allclose(dense_sum, sum2)
|
||||
assert torch.allclose(dense_sum, sum3)
|
||||
assert torch.allclose(dense_sum, sum4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("val_shape", [(), (2,)])
|
||||
def test_sub_sparse_diag(val_shape):
|
||||
ctx = F.ctx()
|
||||
row = torch.tensor([1, 0, 2]).to(ctx)
|
||||
col = torch.tensor([0, 3, 2]).to(ctx)
|
||||
val = torch.randn(row.shape + val_shape).to(ctx)
|
||||
A = dglsp.from_coo(row, col, val)
|
||||
|
||||
shape = (3, 4)
|
||||
val_shape = (shape[0],) + val_shape
|
||||
D = dglsp.diag(torch.randn(val_shape).to(ctx), shape=shape)
|
||||
|
||||
diff1 = (A - D).to_dense()
|
||||
diff2 = (D - A).to_dense()
|
||||
diff3 = dglsp.sub(A, D).to_dense()
|
||||
diff4 = dglsp.sub(D, A).to_dense()
|
||||
dense_diff = A.to_dense() - D.to_dense()
|
||||
|
||||
assert torch.allclose(dense_diff, diff1)
|
||||
assert torch.allclose(dense_diff, -diff2)
|
||||
assert torch.allclose(dense_diff, diff3)
|
||||
assert torch.allclose(dense_diff, -diff4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("op", ["pow"])
|
||||
def test_error_op_sparse_diag(op):
|
||||
ctx = F.ctx()
|
||||
row = torch.tensor([1, 0, 2]).to(ctx)
|
||||
col = torch.tensor([0, 3, 2]).to(ctx)
|
||||
val = torch.randn(row.shape).to(ctx)
|
||||
A = dglsp.from_coo(row, col, val)
|
||||
|
||||
shape = (3, 4)
|
||||
D = dglsp.diag(torch.randn(row.shape[0]).to(ctx), shape=shape)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
getattr(operator, op)(A, D)
|
||||
with pytest.raises(TypeError):
|
||||
getattr(operator, op)(D, A)
|
||||
@@ -0,0 +1,157 @@
|
||||
import sys
|
||||
|
||||
import backend as F
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from dgl.sparse import div, from_coo, mul, power, spmatrix, val_like
|
||||
|
||||
from .utils import (
|
||||
rand_coo,
|
||||
rand_csc,
|
||||
rand_csr,
|
||||
rand_diag,
|
||||
sparse_matrix_to_dense,
|
||||
)
|
||||
|
||||
|
||||
def all_close_sparse(A, row, col, val, shape):
|
||||
rowA, colA = A.coo()
|
||||
valA = A.val
|
||||
assert torch.allclose(rowA, row)
|
||||
assert torch.allclose(colA, col)
|
||||
assert torch.allclose(valA, val)
|
||||
assert A.shape == shape
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"v_scalar", [2, 2.5, torch.tensor(2), torch.tensor(2.5)]
|
||||
)
|
||||
def test_muldiv_scalar(v_scalar):
|
||||
ctx = F.ctx()
|
||||
row = torch.tensor([1, 0, 2]).to(ctx)
|
||||
col = torch.tensor([0, 3, 2]).to(ctx)
|
||||
val = torch.randn(len(row)).to(ctx)
|
||||
A1 = from_coo(row, col, val, shape=(3, 4))
|
||||
|
||||
# A * v
|
||||
A2 = A1 * v_scalar
|
||||
assert torch.allclose(A1.val * v_scalar, A2.val, rtol=1e-4, atol=1e-4)
|
||||
assert A1.shape == A2.shape
|
||||
|
||||
# v * A
|
||||
A2 = v_scalar * A1
|
||||
assert torch.allclose(A1.val * v_scalar, A2.val, rtol=1e-4, atol=1e-4)
|
||||
assert A1.shape == A2.shape
|
||||
|
||||
# A / v
|
||||
A2 = A1 / v_scalar
|
||||
assert torch.allclose(A1.val / v_scalar, A2.val, rtol=1e-4, atol=1e-4)
|
||||
assert A1.shape == A2.shape
|
||||
|
||||
# v / A
|
||||
with pytest.raises(TypeError):
|
||||
v_scalar / A1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("val_shape", [(3,), (3, 2)])
|
||||
def test_pow(val_shape):
|
||||
# A ** v
|
||||
ctx = F.ctx()
|
||||
row = torch.tensor([1, 0, 2]).to(ctx)
|
||||
col = torch.tensor([0, 3, 2]).to(ctx)
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
A = from_coo(row, col, val, shape=(3, 4))
|
||||
exponent = 2
|
||||
A_new = A**exponent
|
||||
assert torch.allclose(A_new.val, val**exponent)
|
||||
assert A_new.shape == A.shape
|
||||
new_row, new_col = A_new.coo()
|
||||
assert torch.allclose(new_row, row)
|
||||
assert torch.allclose(new_col, col)
|
||||
|
||||
# power(A, v)
|
||||
A_new = power(A, exponent)
|
||||
assert torch.allclose(A_new.val, val**exponent)
|
||||
assert A_new.shape == A.shape
|
||||
new_row, new_col = A_new.coo()
|
||||
assert torch.allclose(new_row, row)
|
||||
assert torch.allclose(new_col, col)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("op", ["add", "sub"])
|
||||
@pytest.mark.parametrize(
|
||||
"v_scalar", [2, 2.5, torch.tensor(2), torch.tensor(2.5)]
|
||||
)
|
||||
def test_error_op_scalar(op, v_scalar):
|
||||
ctx = F.ctx()
|
||||
row = torch.tensor([1, 0, 2]).to(ctx)
|
||||
col = torch.tensor([0, 3, 2]).to(ctx)
|
||||
val = torch.randn(len(row)).to(ctx)
|
||||
A = from_coo(row, col, val, shape=(3, 4))
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
A + v_scalar
|
||||
with pytest.raises(TypeError):
|
||||
v_scalar + A
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
A - v_scalar
|
||||
with pytest.raises(TypeError):
|
||||
v_scalar - A
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"create_func1", [rand_coo, rand_csr, rand_csc, rand_diag]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"create_func2", [rand_coo, rand_csr, rand_csc, rand_diag]
|
||||
)
|
||||
@pytest.mark.parametrize("shape", [(5, 5), (5, 3)])
|
||||
@pytest.mark.parametrize("nnz1", [5, 15])
|
||||
@pytest.mark.parametrize("nnz2", [1, 14])
|
||||
@pytest.mark.parametrize("nz_dim", [None, 3])
|
||||
def test_spspmul(create_func1, create_func2, shape, nnz1, nnz2, nz_dim):
|
||||
dev = F.ctx()
|
||||
A = create_func1(shape, nnz1, dev, nz_dim)
|
||||
B = create_func2(shape, nnz2, dev, nz_dim)
|
||||
C = mul(A, B)
|
||||
assert not C.has_duplicate()
|
||||
|
||||
DA = sparse_matrix_to_dense(A)
|
||||
DB = sparse_matrix_to_dense(B)
|
||||
DC = DA * DB
|
||||
|
||||
grad = torch.rand_like(C.val)
|
||||
C.val.backward(grad)
|
||||
DC_grad = sparse_matrix_to_dense(val_like(C, grad))
|
||||
DC.backward(DC_grad)
|
||||
|
||||
assert torch.allclose(sparse_matrix_to_dense(C), DC, atol=1e-05)
|
||||
assert torch.allclose(
|
||||
val_like(A, A.val.grad).to_dense(), DA.grad, atol=1e-05
|
||||
)
|
||||
assert torch.allclose(
|
||||
val_like(B, B.val.grad).to_dense(), DB.grad, atol=1e-05
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"create_func", [rand_coo, rand_csr, rand_csc, rand_diag]
|
||||
)
|
||||
@pytest.mark.parametrize("shape", [(5, 5), (5, 3)])
|
||||
@pytest.mark.parametrize("nnz", [1, 14])
|
||||
@pytest.mark.parametrize("nz_dim", [None, 3])
|
||||
def test_spspdiv(create_func, nnz, shape, nz_dim):
|
||||
dev = F.ctx()
|
||||
A = create_func(shape, nnz, dev, nz_dim)
|
||||
|
||||
perm = torch.randperm(A.nnz, device=dev)
|
||||
rperm = torch.argsort(perm)
|
||||
B = spmatrix(A.indices()[:, perm], A.val[perm], A.shape)
|
||||
C = div(A, B)
|
||||
assert not C.has_duplicate()
|
||||
assert torch.allclose(C.val, A.val / B.val[rperm], atol=1e-05)
|
||||
assert torch.allclose(C.indices(), A.indices(), atol=1e-05)
|
||||
|
||||
# No need to test backward here, since it is handled by Pytorch
|
||||
@@ -0,0 +1,218 @@
|
||||
import warnings
|
||||
|
||||
import backend as F
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from dgl.sparse import bspmm, diag, from_coo, val_like
|
||||
from dgl.sparse.matmul import matmul
|
||||
|
||||
from .utils import (
|
||||
clone_detach_and_grad,
|
||||
dense_mask,
|
||||
rand_coo,
|
||||
rand_csc,
|
||||
rand_csr,
|
||||
rand_stride,
|
||||
sparse_matrix_to_dense,
|
||||
sparse_matrix_to_torch_sparse,
|
||||
)
|
||||
|
||||
|
||||
def _torch_sparse_mm(torch_A1, torch_A2):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", category=UserWarning)
|
||||
return torch.sparse.mm(torch_A1, torch_A2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("create_func", [rand_coo, rand_csr, rand_csc])
|
||||
@pytest.mark.parametrize("shape", [(2, 7), (5, 2)])
|
||||
@pytest.mark.parametrize("nnz", [1, 10])
|
||||
@pytest.mark.parametrize("out_dim", [None, 10])
|
||||
def test_spmm(create_func, shape, nnz, out_dim):
|
||||
dev = F.ctx()
|
||||
A = create_func(shape, nnz, dev)
|
||||
if out_dim is not None:
|
||||
X = torch.randn(shape[1], out_dim, requires_grad=True, device=dev)
|
||||
else:
|
||||
X = torch.randn(shape[1], requires_grad=True, device=dev)
|
||||
|
||||
X = rand_stride(X)
|
||||
sparse_result = matmul(A, X)
|
||||
grad = torch.randn_like(sparse_result)
|
||||
sparse_result.backward(grad)
|
||||
|
||||
adj = sparse_matrix_to_dense(A)
|
||||
XX = clone_detach_and_grad(X)
|
||||
dense_result = torch.matmul(adj, XX)
|
||||
if out_dim is None:
|
||||
dense_result = dense_result.view(-1)
|
||||
dense_result.backward(grad)
|
||||
assert torch.allclose(sparse_result, dense_result, atol=1e-05)
|
||||
assert torch.allclose(X.grad, XX.grad, atol=1e-05)
|
||||
assert torch.allclose(
|
||||
dense_mask(adj.grad, A),
|
||||
sparse_matrix_to_dense(val_like(A, A.val.grad)),
|
||||
atol=1e-05,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("create_func", [rand_coo, rand_csr, rand_csc])
|
||||
@pytest.mark.parametrize("shape", [(2, 7), (5, 2)])
|
||||
@pytest.mark.parametrize("nnz", [1, 10])
|
||||
def test_bspmm(create_func, shape, nnz):
|
||||
dev = F.ctx()
|
||||
A = create_func(shape, nnz, dev, 2)
|
||||
X = torch.randn(shape[1], 10, 2, requires_grad=True, device=dev)
|
||||
X = rand_stride(X)
|
||||
|
||||
sparse_result = matmul(A, X)
|
||||
grad = torch.randn_like(sparse_result)
|
||||
sparse_result.backward(grad)
|
||||
|
||||
XX = clone_detach_and_grad(X)
|
||||
torch_A = A.to_dense().clone().detach().requires_grad_()
|
||||
torch_result = torch_A.permute(2, 0, 1) @ XX.permute(2, 0, 1)
|
||||
|
||||
torch_result.backward(grad.permute(2, 0, 1))
|
||||
assert torch.allclose(
|
||||
sparse_result.permute(2, 0, 1), torch_result, atol=1e-05
|
||||
)
|
||||
assert torch.allclose(X.grad, XX.grad, atol=1e-05)
|
||||
assert torch.allclose(
|
||||
dense_mask(torch_A.grad, A),
|
||||
sparse_matrix_to_dense(val_like(A, A.val.grad)),
|
||||
atol=1e-05,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("create_func1", [rand_coo, rand_csr, rand_csc])
|
||||
@pytest.mark.parametrize("create_func2", [rand_coo, rand_csr, rand_csc])
|
||||
@pytest.mark.parametrize("shape_n_m", [(5, 5), (5, 6)])
|
||||
@pytest.mark.parametrize("shape_k", [3, 4])
|
||||
@pytest.mark.parametrize("nnz1", [1, 10])
|
||||
@pytest.mark.parametrize("nnz2", [1, 10])
|
||||
def test_spspmm(create_func1, create_func2, shape_n_m, shape_k, nnz1, nnz2):
|
||||
dev = F.ctx()
|
||||
shape1 = shape_n_m
|
||||
shape2 = (shape_n_m[1], shape_k)
|
||||
A1 = create_func1(shape1, nnz1, dev)
|
||||
A2 = create_func2(shape2, nnz2, dev)
|
||||
A3 = matmul(A1, A2)
|
||||
grad = torch.randn_like(A3.val)
|
||||
A3.val.backward(grad)
|
||||
|
||||
torch_A1 = sparse_matrix_to_torch_sparse(A1)
|
||||
torch_A2 = sparse_matrix_to_torch_sparse(A2)
|
||||
torch_A3 = _torch_sparse_mm(torch_A1, torch_A2)
|
||||
torch_A3_grad = sparse_matrix_to_torch_sparse(A3, grad)
|
||||
torch_A3.backward(torch_A3_grad)
|
||||
|
||||
with torch.no_grad():
|
||||
assert torch.allclose(A3.to_dense(), torch_A3.to_dense(), atol=1e-05)
|
||||
assert torch.allclose(
|
||||
val_like(A1, A1.val.grad).to_dense(),
|
||||
torch_A1.grad.to_dense(),
|
||||
atol=1e-05,
|
||||
)
|
||||
assert torch.allclose(
|
||||
val_like(A2, A2.val.grad).to_dense(),
|
||||
torch_A2.grad.to_dense(),
|
||||
atol=1e-05,
|
||||
)
|
||||
|
||||
|
||||
def test_spspmm_duplicate():
|
||||
dev = F.ctx()
|
||||
|
||||
row = torch.tensor([1, 0, 0, 0, 1]).to(dev)
|
||||
col = torch.tensor([1, 1, 1, 2, 2]).to(dev)
|
||||
val = torch.randn(len(row)).to(dev)
|
||||
shape = (4, 4)
|
||||
A1 = from_coo(row, col, val, shape)
|
||||
|
||||
row = torch.tensor([1, 0, 0, 1]).to(dev)
|
||||
col = torch.tensor([1, 1, 2, 2]).to(dev)
|
||||
val = torch.randn(len(row)).to(dev)
|
||||
shape = (4, 4)
|
||||
A2 = from_coo(row, col, val, shape)
|
||||
|
||||
try:
|
||||
matmul(A1, A2)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
assert False, "Should raise error."
|
||||
|
||||
try:
|
||||
matmul(A2, A1)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
assert False, "Should raise error."
|
||||
|
||||
|
||||
@pytest.mark.parametrize("create_func", [rand_coo, rand_csr, rand_csc])
|
||||
@pytest.mark.parametrize("sparse_shape", [(5, 5), (5, 6)])
|
||||
@pytest.mark.parametrize("nnz", [1, 10])
|
||||
def test_sparse_diag_mm(create_func, sparse_shape, nnz):
|
||||
dev = F.ctx()
|
||||
diag_shape = sparse_shape[1], sparse_shape[1]
|
||||
A = create_func(sparse_shape, nnz, dev)
|
||||
diag_val = torch.randn(sparse_shape[1], device=dev, requires_grad=True)
|
||||
D = diag(diag_val, diag_shape)
|
||||
B = matmul(A, D)
|
||||
grad = torch.randn_like(B.val)
|
||||
B.val.backward(grad)
|
||||
|
||||
torch_A = sparse_matrix_to_torch_sparse(A)
|
||||
torch_D = sparse_matrix_to_torch_sparse(D)
|
||||
torch_B = _torch_sparse_mm(torch_A, torch_D)
|
||||
torch_B_grad = sparse_matrix_to_torch_sparse(B, grad)
|
||||
torch_B.backward(torch_B_grad)
|
||||
|
||||
with torch.no_grad():
|
||||
assert torch.allclose(B.to_dense(), torch_B.to_dense(), atol=1e-05)
|
||||
assert torch.allclose(
|
||||
val_like(A, A.val.grad).to_dense(),
|
||||
torch_A.grad.to_dense(),
|
||||
atol=1e-05,
|
||||
)
|
||||
assert torch.allclose(
|
||||
diag(D.val.grad, D.shape).to_dense(),
|
||||
torch_D.grad.to_dense(),
|
||||
atol=1e-05,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("create_func", [rand_coo, rand_csr, rand_csc])
|
||||
@pytest.mark.parametrize("sparse_shape", [(5, 5), (5, 6)])
|
||||
@pytest.mark.parametrize("nnz", [1, 10])
|
||||
def test_diag_sparse_mm(create_func, sparse_shape, nnz):
|
||||
dev = F.ctx()
|
||||
diag_shape = sparse_shape[0], sparse_shape[0]
|
||||
A = create_func(sparse_shape, nnz, dev)
|
||||
diag_val = torch.randn(sparse_shape[0], device=dev, requires_grad=True)
|
||||
D = diag(diag_val, diag_shape)
|
||||
B = matmul(D, A)
|
||||
grad = torch.randn_like(B.val)
|
||||
B.val.backward(grad)
|
||||
|
||||
torch_A = sparse_matrix_to_torch_sparse(A)
|
||||
torch_D = sparse_matrix_to_torch_sparse(D)
|
||||
torch_B = _torch_sparse_mm(torch_D, torch_A)
|
||||
torch_B_grad = sparse_matrix_to_torch_sparse(B, grad)
|
||||
torch_B.backward(torch_B_grad)
|
||||
|
||||
with torch.no_grad():
|
||||
assert torch.allclose(B.to_dense(), torch_B.to_dense(), atol=1e-05)
|
||||
assert torch.allclose(
|
||||
val_like(A, A.val.grad).to_dense(),
|
||||
torch_A.grad.to_dense(),
|
||||
atol=1e-05,
|
||||
)
|
||||
assert torch.allclose(
|
||||
diag(D.val.grad, D.shape).to_dense(),
|
||||
torch_D.grad.to_dense(),
|
||||
atol=1e-05,
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
import backend as F
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from .utils import (
|
||||
rand_coo,
|
||||
rand_csc,
|
||||
rand_csr,
|
||||
rand_diag,
|
||||
sparse_matrix_to_dense,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"create_func", [rand_diag, rand_csr, rand_csc, rand_coo]
|
||||
)
|
||||
@pytest.mark.parametrize("dim", [0, 1])
|
||||
@pytest.mark.parametrize("index", [None, (1, 3), (4, 0, 2)])
|
||||
def test_compact(create_func, dim, index):
|
||||
ctx = F.ctx()
|
||||
shape = (5, 5)
|
||||
ans_idx = []
|
||||
if index is not None:
|
||||
ans_idx = list(dict.fromkeys(index))
|
||||
index = torch.tensor(index).to(ctx)
|
||||
|
||||
A = create_func(shape, 8, ctx)
|
||||
|
||||
A_compact, ret_id = A.compact(dim, index)
|
||||
A_compact_dense = sparse_matrix_to_dense(A_compact)
|
||||
|
||||
A_dense = sparse_matrix_to_dense(A)
|
||||
|
||||
for i in range(shape[dim]):
|
||||
if dim == 0:
|
||||
row = list(A_dense[i, :].nonzero().reshape(-1))
|
||||
else:
|
||||
row = list(A_dense[:, i].nonzero().reshape(-1))
|
||||
if (i not in list(ans_idx)) and len(row) > 0:
|
||||
ans_idx.append(i)
|
||||
if len(ans_idx):
|
||||
ans_idx = torch.tensor(ans_idx).to(ctx)
|
||||
A_dense_select = sparse_matrix_to_dense(A.index_select(dim, ans_idx))
|
||||
|
||||
assert A_compact_dense.shape == A_dense_select.shape
|
||||
assert torch.allclose(A_compact_dense, A_dense_select)
|
||||
assert torch.allclose(ans_idx, ret_id)
|
||||
@@ -0,0 +1,160 @@
|
||||
import doctest
|
||||
import operator
|
||||
import sys
|
||||
|
||||
import backend as F
|
||||
|
||||
import dgl.sparse as dglsp
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
dgl_op_map = {
|
||||
"sum": "sum",
|
||||
"amin": "smin",
|
||||
"amax": "smax",
|
||||
"mean": "smean",
|
||||
"prod": "sprod",
|
||||
}
|
||||
default_entry = {
|
||||
"sum": 0,
|
||||
"amin": float("inf"),
|
||||
"amax": float("-inf"),
|
||||
"mean": 0,
|
||||
"prod": 1,
|
||||
}
|
||||
binary_op_map = {
|
||||
"sum": operator.add,
|
||||
"amin": torch.min,
|
||||
"amax": torch.max,
|
||||
"mean": operator.add,
|
||||
"prod": operator.mul,
|
||||
}
|
||||
|
||||
NUM_ROWS = 10
|
||||
NUM_COLS = 15
|
||||
|
||||
|
||||
def _coalesce_dense(row, col, val, nrows, ncols, op):
|
||||
# Sparse matrix coalescing on a dense matrix.
|
||||
#
|
||||
# It is done by stacking every non-zero entry on an individual slice
|
||||
# of an (nrows x ncols x nnz), that is, construct a tensor A with
|
||||
# shape (nrows, ncols, len(val)) where
|
||||
#
|
||||
# A[row[i], col[i], i] = val[i]
|
||||
#
|
||||
# and then reducing on the third "nnz" dimension.
|
||||
#
|
||||
# The mask matrix M has the same sparsity pattern as A with 1 being
|
||||
# the non-zero entries. This is used for division if the reduce
|
||||
# operator is mean.
|
||||
M = torch.zeros(NUM_ROWS, NUM_COLS, device=F.ctx())
|
||||
A = torch.full(
|
||||
(NUM_ROWS, NUM_COLS, 20) + val.shape[1:],
|
||||
default_entry[op],
|
||||
device=F.ctx(),
|
||||
dtype=val.dtype,
|
||||
)
|
||||
A = torch.index_put(A, (row, col, torch.arange(20)), val)
|
||||
for i in range(20):
|
||||
M[row[i], col[i]] += 1
|
||||
if op == "mean":
|
||||
A = A.sum(2)
|
||||
else:
|
||||
A = getattr(A, op)(2)
|
||||
M = M.view(NUM_ROWS, NUM_COLS, *([1] * (val.dim() - 1)))
|
||||
return A, M
|
||||
|
||||
|
||||
# Add docstring tests of dglsp.reduction to unit tests
|
||||
@pytest.mark.parametrize(
|
||||
"func", ["reduce", "sum", "smin", "smax", "sprod", "smean"]
|
||||
)
|
||||
def test_docstring(func):
|
||||
globs = {"torch": torch, "dglsp": dglsp}
|
||||
runner = doctest.DebugRunner()
|
||||
finder = doctest.DocTestFinder()
|
||||
obj = getattr(dglsp, func)
|
||||
for test in finder.find(obj, func, globs=globs):
|
||||
runner.run(test)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [(20,), (20, 20)])
|
||||
@pytest.mark.parametrize("op", ["sum", "amin", "amax", "mean", "prod"])
|
||||
@pytest.mark.parametrize("use_reduce", [False, True])
|
||||
def test_reduce_all(shape, op, use_reduce):
|
||||
row = torch.randint(0, NUM_ROWS, (20,), device=F.ctx())
|
||||
col = torch.randint(0, NUM_COLS, (20,), device=F.ctx())
|
||||
val = torch.randn(*shape, device=F.ctx())
|
||||
val2 = val.clone()
|
||||
val = val.requires_grad_()
|
||||
val2 = val2.requires_grad_()
|
||||
A = dglsp.from_coo(row, col, val, shape=(NUM_ROWS, NUM_COLS))
|
||||
|
||||
A2, M = _coalesce_dense(row, col, val2, NUM_ROWS, NUM_COLS, op)
|
||||
|
||||
if not use_reduce:
|
||||
output = getattr(A, dgl_op_map[op])()
|
||||
else:
|
||||
output = A.reduce(rtype=dgl_op_map[op])
|
||||
|
||||
if op == "mean":
|
||||
output2 = A2.sum((0, 1)) / M.sum()
|
||||
elif op == "prod":
|
||||
output2 = A2.prod(0).prod(0) # prod() does not support tuple of dims
|
||||
else:
|
||||
output2 = getattr(A2, op)((0, 1))
|
||||
assert (output - output2).abs().max() < 1e-4
|
||||
|
||||
head = torch.randn(*output.shape).to(val) if output.dim() > 0 else None
|
||||
output.backward(head)
|
||||
output2.backward(head)
|
||||
assert (val.grad - val2.grad).abs().max() < 1e-4
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [(20,), (20, 20)])
|
||||
@pytest.mark.parametrize("dim", [0, 1])
|
||||
@pytest.mark.parametrize("empty_nnz", [False, True])
|
||||
@pytest.mark.parametrize("op", ["sum", "amin", "amax", "mean", "prod"])
|
||||
@pytest.mark.parametrize("use_reduce", [False, True])
|
||||
def test_reduce_along(shape, dim, empty_nnz, op, use_reduce):
|
||||
row = torch.randint(0, NUM_ROWS, (20,), device=F.ctx())
|
||||
col = torch.randint(0, NUM_COLS, (20,), device=F.ctx())
|
||||
if dim == 0:
|
||||
mask = torch.bincount(col, minlength=NUM_COLS) == 0
|
||||
else:
|
||||
mask = torch.bincount(row, minlength=NUM_ROWS) == 0
|
||||
val = torch.randn(*shape, device=F.ctx())
|
||||
val2 = val.clone()
|
||||
val = val.requires_grad_()
|
||||
val2 = val2.requires_grad_()
|
||||
|
||||
# empty_nnz controls whether at least one column or one row has no
|
||||
# non-zero entry.
|
||||
if empty_nnz:
|
||||
row[row == 0] = 1
|
||||
col[col == 0] = 1
|
||||
|
||||
A = dglsp.from_coo(row, col, val, shape=(NUM_ROWS, NUM_COLS))
|
||||
|
||||
A2, M = _coalesce_dense(row, col, val2, NUM_ROWS, NUM_COLS, op)
|
||||
|
||||
if not use_reduce:
|
||||
output = getattr(A, dgl_op_map[op])(dim)
|
||||
else:
|
||||
output = A.reduce(dim=dim, rtype=dgl_op_map[op])
|
||||
|
||||
if op == "mean":
|
||||
output2 = A2.sum(dim) / M.sum(dim)
|
||||
else:
|
||||
output2 = getattr(A2, op)(dim)
|
||||
zero_entry_idx = (M.sum(dim) != 0).nonzero(as_tuple=True)[0]
|
||||
output3 = torch.index_put(
|
||||
torch.zeros_like(output2), (zero_entry_idx,), output2[zero_entry_idx]
|
||||
)
|
||||
assert (output - output3).abs().max() < 1e-4
|
||||
|
||||
head = torch.randn(*output.shape).to(val) if output.dim() > 0 else None
|
||||
output.backward(head)
|
||||
output3.backward(head)
|
||||
assert (val.grad - val2.grad).abs().max() < 1e-4
|
||||
@@ -0,0 +1,92 @@
|
||||
import sys
|
||||
|
||||
import backend as F
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from dgl.sparse import bsddmm, sddmm
|
||||
|
||||
from .utils import (
|
||||
clone_detach_and_grad,
|
||||
rand_coo,
|
||||
rand_csc,
|
||||
rand_csr,
|
||||
rand_stride,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("create_func", [rand_coo, rand_csr, rand_csc])
|
||||
@pytest.mark.parametrize("shape", [(5, 5), (5, 4)])
|
||||
@pytest.mark.parametrize("nnz", [2, 10])
|
||||
@pytest.mark.parametrize("hidden", [1, 5])
|
||||
def test_sddmm(create_func, shape, nnz, hidden):
|
||||
dev = F.ctx()
|
||||
A = create_func(shape, nnz, dev)
|
||||
if hidden > 1:
|
||||
B = torch.rand(shape[0], hidden, requires_grad=True, device=dev)
|
||||
C = torch.rand(hidden, shape[1], requires_grad=True, device=dev)
|
||||
else:
|
||||
B = torch.rand(shape[0], requires_grad=True, device=dev)
|
||||
C = torch.rand(shape[1], requires_grad=True, device=dev)
|
||||
|
||||
B = rand_stride(B)
|
||||
C = rand_stride(C)
|
||||
|
||||
A_val_clone = clone_detach_and_grad(A.val)
|
||||
dense_B = clone_detach_and_grad(B)
|
||||
dense_C = clone_detach_and_grad(C)
|
||||
|
||||
sparse_result = sddmm(A, B, C)
|
||||
|
||||
grad = torch.rand_like(sparse_result.val)
|
||||
sparse_result.val.backward(grad)
|
||||
|
||||
if hidden == 1:
|
||||
dense_result = dense_B.view(-1, 1) @ dense_C.view(1, -1)
|
||||
else:
|
||||
dense_result = dense_B @ dense_C
|
||||
|
||||
row, col = A.coo()
|
||||
dense_val = dense_result[row, col] * A_val_clone
|
||||
dense_val.backward(grad)
|
||||
|
||||
assert torch.allclose(dense_val, sparse_result.val, atol=1e-05)
|
||||
assert torch.allclose(dense_C.grad, C.grad, atol=1e-05)
|
||||
assert torch.allclose(dense_B.grad, B.grad, atol=1e-05)
|
||||
assert torch.allclose(A_val_clone.grad, A.val.grad, atol=1e-05)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("create_func", [rand_coo, rand_csr, rand_csc])
|
||||
@pytest.mark.parametrize("shape", [(5, 5), (5, 4)])
|
||||
@pytest.mark.parametrize("nnz", [2, 10])
|
||||
@pytest.mark.parametrize("nz_dim", [2, 10])
|
||||
def test_bsddmm(create_func, shape, nnz, nz_dim):
|
||||
dev = F.ctx()
|
||||
hidden = 2
|
||||
A = create_func(shape, nnz, dev, nz_dim)
|
||||
B = torch.rand(shape[0], hidden, nz_dim, requires_grad=True, device=dev)
|
||||
C = torch.rand(hidden, shape[1], nz_dim, requires_grad=True, device=dev)
|
||||
|
||||
B = rand_stride(B)
|
||||
C = rand_stride(C)
|
||||
|
||||
A_val_clone = clone_detach_and_grad(A.val)
|
||||
dense_B = clone_detach_and_grad(B)
|
||||
dense_C = clone_detach_and_grad(C)
|
||||
|
||||
sparse_result = bsddmm(A, B, C)
|
||||
|
||||
grad = torch.rand_like(sparse_result.val)
|
||||
sparse_result.val.backward(grad)
|
||||
|
||||
dense_result = dense_B.permute(2, 0, 1) @ dense_C.permute(2, 0, 1)
|
||||
dense_result = dense_result.permute(1, 2, 0)
|
||||
|
||||
row, col = A.coo()
|
||||
dense_val = dense_result[row, col] * A_val_clone
|
||||
dense_val.backward(grad)
|
||||
|
||||
assert torch.allclose(dense_val, sparse_result.val, atol=1e-05)
|
||||
assert torch.allclose(dense_C.grad, C.grad, atol=1e-05)
|
||||
assert torch.allclose(dense_B.grad, B.grad, atol=1e-05)
|
||||
assert torch.allclose(A_val_clone.grad, A.val.grad, atol=1e-05)
|
||||
@@ -0,0 +1,43 @@
|
||||
import sys
|
||||
|
||||
import backend as F
|
||||
|
||||
import dgl
|
||||
import pytest
|
||||
import torch
|
||||
from dgl.sparse import from_coo, softmax
|
||||
|
||||
|
||||
@pytest.mark.parametrize("val_D", [None, 2])
|
||||
@pytest.mark.parametrize("csr", [True, False])
|
||||
@pytest.mark.parametrize("dim", [0, 1])
|
||||
def test_softmax(val_D, csr, dim):
|
||||
dev = F.ctx()
|
||||
row = torch.tensor([0, 0, 1, 1]).to(dev)
|
||||
col = torch.tensor([0, 2, 1, 2]).to(dev)
|
||||
nnz = len(row)
|
||||
if val_D is None:
|
||||
val = torch.randn(nnz).to(dev)
|
||||
else:
|
||||
val = torch.randn(nnz, val_D).to(dev)
|
||||
|
||||
val_sparse = val.clone().requires_grad_()
|
||||
A = from_coo(row, col, val_sparse)
|
||||
|
||||
if csr:
|
||||
# Test CSR
|
||||
A.csr()
|
||||
|
||||
A_max = softmax(A, dim)
|
||||
if dim == 1:
|
||||
g = dgl.graph((col, row), num_nodes=max(A.shape))
|
||||
else:
|
||||
g = dgl.graph((row, col), num_nodes=max(A.shape))
|
||||
val_g = val.clone().requires_grad_()
|
||||
score = dgl.nn.functional.edge_softmax(g, val_g)
|
||||
assert torch.allclose(A_max.val, score, atol=1e-05)
|
||||
|
||||
grad = torch.randn_like(score).to(dev)
|
||||
A_max.val.backward(grad)
|
||||
score.backward(grad)
|
||||
assert torch.allclose(A.val.grad, val_g.grad, atol=1e-05)
|
||||
@@ -0,0 +1,878 @@
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
import backend as F
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from dgl.sparse import (
|
||||
diag,
|
||||
from_coo,
|
||||
from_csc,
|
||||
from_csr,
|
||||
from_torch_sparse,
|
||||
identity,
|
||||
to_torch_sparse_coo,
|
||||
to_torch_sparse_csc,
|
||||
to_torch_sparse_csr,
|
||||
val_like,
|
||||
)
|
||||
|
||||
from .utils import (
|
||||
rand_coo,
|
||||
rand_csc,
|
||||
rand_csr,
|
||||
rand_diag,
|
||||
sparse_matrix_to_dense,
|
||||
)
|
||||
|
||||
|
||||
def _torch_sparse_csr_tensor(indptr, indices, val, torch_sparse_shape):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", category=UserWarning)
|
||||
return torch.sparse_csr_tensor(indptr, indices, val, torch_sparse_shape)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dense_dim", [None, 4])
|
||||
@pytest.mark.parametrize("row", [(0, 0, 1, 2), (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("col", [(0, 1, 2, 2), (1, 3, 3, 4)])
|
||||
@pytest.mark.parametrize("shape", [None, (5, 5), (5, 6)])
|
||||
def test_from_coo(dense_dim, row, col, shape):
|
||||
val_shape = (len(row),)
|
||||
if dense_dim is not None:
|
||||
val_shape += (dense_dim,)
|
||||
ctx = F.ctx()
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
row = torch.tensor(row).to(ctx)
|
||||
col = torch.tensor(col).to(ctx)
|
||||
mat = from_coo(row, col, val, shape)
|
||||
|
||||
if shape is None:
|
||||
shape = (torch.max(row).item() + 1, torch.max(col).item() + 1)
|
||||
|
||||
mat_row, mat_col = mat.coo()
|
||||
mat_val = mat.val
|
||||
|
||||
assert mat.shape == shape
|
||||
assert mat.nnz == row.numel()
|
||||
assert mat.dtype == val.dtype
|
||||
assert torch.allclose(mat_val, val)
|
||||
assert torch.allclose(mat_row, row)
|
||||
assert torch.allclose(mat_col, col)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dense_dim", [None, 4])
|
||||
@pytest.mark.parametrize("indptr", [(0, 0, 1, 4), (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("indices", [(0, 1, 2, 3), (1, 2, 3, 4)])
|
||||
@pytest.mark.parametrize("shape", [None, (3, 5)])
|
||||
def test_from_csr(dense_dim, indptr, indices, shape):
|
||||
val_shape = (len(indices),)
|
||||
if dense_dim is not None:
|
||||
val_shape += (dense_dim,)
|
||||
ctx = F.ctx()
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
indptr = torch.tensor(indptr).to(ctx)
|
||||
indices = torch.tensor(indices).to(ctx)
|
||||
mat = from_csr(indptr, indices, val, shape)
|
||||
|
||||
if shape is None:
|
||||
shape = (indptr.numel() - 1, torch.max(indices).item() + 1)
|
||||
|
||||
assert mat.device == val.device
|
||||
assert mat.shape == shape
|
||||
assert mat.nnz == indices.numel()
|
||||
assert mat.dtype == val.dtype
|
||||
mat_indptr, mat_indices, value_indices = mat.csr()
|
||||
mat_val = mat.val if value_indices is None else mat.val[value_indices]
|
||||
assert torch.allclose(mat_indptr, indptr)
|
||||
assert torch.allclose(mat_indices, indices)
|
||||
assert torch.allclose(mat_val, val)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dense_dim", [None, 4])
|
||||
@pytest.mark.parametrize("indptr", [(0, 0, 1, 4), (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("indices", [(0, 1, 2, 3), (1, 2, 3, 4)])
|
||||
@pytest.mark.parametrize("shape", [None, (5, 3)])
|
||||
def test_from_csc(dense_dim, indptr, indices, shape):
|
||||
val_shape = (len(indices),)
|
||||
if dense_dim is not None:
|
||||
val_shape += (dense_dim,)
|
||||
ctx = F.ctx()
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
indptr = torch.tensor(indptr).to(ctx)
|
||||
indices = torch.tensor(indices).to(ctx)
|
||||
mat = from_csc(indptr, indices, val, shape)
|
||||
|
||||
if shape is None:
|
||||
shape = (torch.max(indices).item() + 1, indptr.numel() - 1)
|
||||
|
||||
assert mat.device == val.device
|
||||
assert mat.shape == shape
|
||||
assert mat.nnz == indices.numel()
|
||||
assert mat.dtype == val.dtype
|
||||
mat_indptr, mat_indices, value_indices = mat.csc()
|
||||
mat_val = mat.val if value_indices is None else mat.val[value_indices]
|
||||
assert torch.allclose(mat_indptr, indptr)
|
||||
assert torch.allclose(mat_indices, indices)
|
||||
assert torch.allclose(mat_val, val)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("val_shape", [(3), (3, 2)])
|
||||
def test_dense(val_shape):
|
||||
ctx = F.ctx()
|
||||
|
||||
row = torch.tensor([1, 1, 2]).to(ctx)
|
||||
col = torch.tensor([2, 4, 3]).to(ctx)
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
A = from_coo(row, col, val)
|
||||
A_dense = A.to_dense()
|
||||
|
||||
shape = A.shape + val.shape[1:]
|
||||
mat = torch.zeros(shape, device=ctx)
|
||||
mat[row, col] = val
|
||||
assert torch.allclose(A_dense, mat)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dense_dim", [None, 4])
|
||||
@pytest.mark.parametrize("indptr", [(0, 0, 1, 4), (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("indices", [(0, 1, 2, 3), (1, 4, 3, 2)])
|
||||
@pytest.mark.parametrize("shape", [None, (3, 5)])
|
||||
def test_csr_to_coo(dense_dim, indptr, indices, shape):
|
||||
ctx = F.ctx()
|
||||
val_shape = (len(indices),)
|
||||
if dense_dim is not None:
|
||||
val_shape += (dense_dim,)
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
indptr = torch.tensor(indptr).to(ctx)
|
||||
indices = torch.tensor(indices).to(ctx)
|
||||
mat = from_csr(indptr, indices, val, shape)
|
||||
|
||||
if shape is None:
|
||||
shape = (indptr.numel() - 1, torch.max(indices).item() + 1)
|
||||
|
||||
row = (
|
||||
torch.arange(0, indptr.shape[0] - 1)
|
||||
.to(ctx)
|
||||
.repeat_interleave(torch.diff(indptr))
|
||||
)
|
||||
col = indices
|
||||
mat_row, mat_col = mat.coo()
|
||||
mat_val = mat.val
|
||||
|
||||
assert mat.shape == shape
|
||||
assert mat.nnz == row.numel()
|
||||
assert mat.device == row.device
|
||||
assert mat.dtype == val.dtype
|
||||
assert torch.allclose(mat_val, val)
|
||||
assert torch.allclose(mat_row, row)
|
||||
assert torch.allclose(mat_col, col)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dense_dim", [None, 4])
|
||||
@pytest.mark.parametrize("indptr", [(0, 0, 1, 4), (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("indices", [(0, 1, 2, 3), (1, 4, 3, 2)])
|
||||
@pytest.mark.parametrize("shape", [None, (5, 3)])
|
||||
def test_csc_to_coo(dense_dim, indptr, indices, shape):
|
||||
ctx = F.ctx()
|
||||
val_shape = (len(indices),)
|
||||
if dense_dim is not None:
|
||||
val_shape += (dense_dim,)
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
indptr = torch.tensor(indptr).to(ctx)
|
||||
indices = torch.tensor(indices).to(ctx)
|
||||
mat = from_csc(indptr, indices, val, shape)
|
||||
|
||||
if shape is None:
|
||||
shape = (torch.max(indices).item() + 1, indptr.numel() - 1)
|
||||
|
||||
col = (
|
||||
torch.arange(0, indptr.shape[0] - 1)
|
||||
.to(ctx)
|
||||
.repeat_interleave(torch.diff(indptr))
|
||||
)
|
||||
row = indices
|
||||
mat_row, mat_col = mat.coo()
|
||||
mat_val = mat.val
|
||||
|
||||
assert mat.shape == shape
|
||||
assert mat.nnz == row.numel()
|
||||
assert mat.device == row.device
|
||||
assert mat.dtype == val.dtype
|
||||
assert torch.allclose(mat_val, val)
|
||||
assert torch.allclose(mat_row, row)
|
||||
assert torch.allclose(mat_col, col)
|
||||
|
||||
|
||||
def _scatter_add(a, index, v=1):
|
||||
index = index.tolist()
|
||||
for i in index:
|
||||
a[i] += v
|
||||
return a
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dense_dim", [None, 4])
|
||||
@pytest.mark.parametrize("row", [(0, 0, 1, 2), (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("col", [(0, 1, 2, 2), (1, 3, 3, 4)])
|
||||
@pytest.mark.parametrize("shape", [None, (5, 5), (5, 6)])
|
||||
def test_coo_to_csr(dense_dim, row, col, shape):
|
||||
val_shape = (len(row),)
|
||||
if dense_dim is not None:
|
||||
val_shape += (dense_dim,)
|
||||
ctx = F.ctx()
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
row = torch.tensor(row).to(ctx)
|
||||
col = torch.tensor(col).to(ctx)
|
||||
mat = from_coo(row, col, val, shape)
|
||||
|
||||
if shape is None:
|
||||
shape = (torch.max(row).item() + 1, torch.max(col).item() + 1)
|
||||
|
||||
mat_indptr, mat_indices, value_indices = mat.csr()
|
||||
mat_val = mat.val if value_indices is None else mat.val[value_indices]
|
||||
indptr = torch.zeros(shape[0] + 1).to(ctx)
|
||||
indptr = _scatter_add(indptr, row + 1)
|
||||
indptr = torch.cumsum(indptr, 0).long()
|
||||
indices = col
|
||||
|
||||
assert mat.shape == shape
|
||||
assert mat.nnz == row.numel()
|
||||
assert mat.dtype == val.dtype
|
||||
assert torch.allclose(mat_val, val)
|
||||
assert torch.allclose(mat_indptr, indptr)
|
||||
assert torch.allclose(mat_indices, indices)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dense_dim", [None, 4])
|
||||
@pytest.mark.parametrize("indptr", [(0, 0, 1, 4), (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("indices", [(0, 1, 2, 3), (1, 4, 3, 2)])
|
||||
@pytest.mark.parametrize("shape", [None, (5, 3)])
|
||||
def test_csc_to_csr(dense_dim, indptr, indices, shape):
|
||||
ctx = F.ctx()
|
||||
val_shape = (len(indices),)
|
||||
if dense_dim is not None:
|
||||
val_shape += (dense_dim,)
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
indptr = torch.tensor(indptr).to(ctx)
|
||||
indices = torch.tensor(indices).to(ctx)
|
||||
mat = from_csc(indptr, indices, val, shape)
|
||||
mat_indptr, mat_indices, value_indices = mat.csr()
|
||||
mat_val = mat.val if value_indices is None else mat.val[value_indices]
|
||||
|
||||
if shape is None:
|
||||
shape = (torch.max(indices).item() + 1, indptr.numel() - 1)
|
||||
|
||||
col = (
|
||||
torch.arange(0, indptr.shape[0] - 1)
|
||||
.to(ctx)
|
||||
.repeat_interleave(torch.diff(indptr))
|
||||
)
|
||||
row = indices
|
||||
row, sort_index = row.sort(stable=True)
|
||||
col = col[sort_index]
|
||||
val = val[sort_index]
|
||||
indptr = torch.zeros(shape[0] + 1).to(ctx)
|
||||
indptr = _scatter_add(indptr, row + 1)
|
||||
indptr = torch.cumsum(indptr, 0).long()
|
||||
indices = col
|
||||
|
||||
assert mat.shape == shape
|
||||
assert mat.nnz == row.numel()
|
||||
assert mat.device == row.device
|
||||
assert mat.dtype == val.dtype
|
||||
assert torch.allclose(mat_val, val)
|
||||
assert torch.allclose(mat_indptr, indptr)
|
||||
assert torch.allclose(mat_indices, indices)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dense_dim", [None, 4])
|
||||
@pytest.mark.parametrize("row", [(0, 0, 1, 2), (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("col", [(0, 1, 2, 2), (1, 3, 3, 4)])
|
||||
@pytest.mark.parametrize("shape", [None, (5, 5), (5, 6)])
|
||||
def test_coo_to_csc(dense_dim, row, col, shape):
|
||||
val_shape = (len(row),)
|
||||
if dense_dim is not None:
|
||||
val_shape += (dense_dim,)
|
||||
ctx = F.ctx()
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
row = torch.tensor(row).to(ctx)
|
||||
col = torch.tensor(col).to(ctx)
|
||||
mat = from_coo(row, col, val, shape)
|
||||
|
||||
if shape is None:
|
||||
shape = (torch.max(row).item() + 1, torch.max(col).item() + 1)
|
||||
|
||||
mat_indptr, mat_indices, value_indices = mat.csc()
|
||||
mat_val = mat.val if value_indices is None else mat.val[value_indices]
|
||||
indptr = torch.zeros(shape[1] + 1).to(ctx)
|
||||
_scatter_add(indptr, col + 1)
|
||||
indptr = torch.cumsum(indptr, 0).long()
|
||||
indices = row
|
||||
|
||||
assert mat.shape == shape
|
||||
assert mat.nnz == row.numel()
|
||||
assert mat.dtype == val.dtype
|
||||
assert torch.allclose(mat_val, val)
|
||||
assert torch.allclose(mat_indptr, indptr)
|
||||
assert torch.allclose(mat_indices, indices)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dense_dim", [None, 4])
|
||||
@pytest.mark.parametrize("indptr", [(0, 0, 1, 4), (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("indices", [(0, 1, 2, 3), (1, 2, 3, 4)])
|
||||
@pytest.mark.parametrize("shape", [None, (3, 5)])
|
||||
def test_csr_to_csc(dense_dim, indptr, indices, shape):
|
||||
val_shape = (len(indices),)
|
||||
if dense_dim is not None:
|
||||
val_shape += (dense_dim,)
|
||||
ctx = F.ctx()
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
indptr = torch.tensor(indptr).to(ctx)
|
||||
indices = torch.tensor(indices).to(ctx)
|
||||
mat = from_csr(indptr, indices, val, shape)
|
||||
mat_indptr, mat_indices, value_indices = mat.csc()
|
||||
mat_val = mat.val if value_indices is None else mat.val[value_indices]
|
||||
|
||||
if shape is None:
|
||||
shape = (indptr.numel() - 1, torch.max(indices).item() + 1)
|
||||
|
||||
row = (
|
||||
torch.arange(0, indptr.shape[0] - 1)
|
||||
.to(ctx)
|
||||
.repeat_interleave(torch.diff(indptr))
|
||||
)
|
||||
|
||||
col = indices
|
||||
col, sort_index = col.sort(stable=True)
|
||||
row = row[sort_index]
|
||||
val = val[sort_index]
|
||||
indptr = torch.zeros(shape[1] + 1).to(ctx)
|
||||
indptr = _scatter_add(indptr, col + 1)
|
||||
indptr = torch.cumsum(indptr, 0).long()
|
||||
indices = row
|
||||
|
||||
assert mat.shape == shape
|
||||
assert mat.nnz == row.numel()
|
||||
assert mat.device == row.device
|
||||
assert mat.dtype == val.dtype
|
||||
assert torch.allclose(mat_val, val)
|
||||
assert torch.allclose(mat_indptr, indptr)
|
||||
assert torch.allclose(mat_indices, indices)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [(3, 5), (5, 5), (5, 4)])
|
||||
def test_diag_conversions(shape):
|
||||
n_rows, n_cols = shape
|
||||
nnz = min(shape)
|
||||
ctx = F.ctx()
|
||||
val = torch.randn(nnz).to(ctx)
|
||||
D = diag(val, shape)
|
||||
row, col = D.coo()
|
||||
assert torch.allclose(row, torch.arange(nnz).to(ctx))
|
||||
assert torch.allclose(col, torch.arange(nnz).to(ctx))
|
||||
|
||||
indptr, indices, _ = D.csr()
|
||||
exp_indptr = list(range(0, nnz + 1)) + [nnz] * (n_rows - nnz)
|
||||
assert torch.allclose(indptr, torch.tensor(exp_indptr).to(ctx))
|
||||
assert torch.allclose(indices, torch.arange(nnz).to(ctx))
|
||||
|
||||
indptr, indices, _ = D.csc()
|
||||
exp_indptr = list(range(0, nnz + 1)) + [nnz] * (n_cols - nnz)
|
||||
assert torch.allclose(indptr, torch.tensor(exp_indptr).to(ctx))
|
||||
assert torch.allclose(indices, torch.arange(nnz).to(ctx))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("val_shape", [(3), (3, 2)])
|
||||
@pytest.mark.parametrize("shape", [(3, 5), (5, 5)])
|
||||
def test_val_like(val_shape, shape):
|
||||
def check_val_like(A, B):
|
||||
assert A.shape == B.shape
|
||||
assert A.nnz == B.nnz
|
||||
assert torch.allclose(torch.stack(A.coo()), torch.stack(B.coo()))
|
||||
assert A.val.device == B.val.device
|
||||
|
||||
ctx = F.ctx()
|
||||
|
||||
# COO
|
||||
row = torch.tensor([1, 1, 2]).to(ctx)
|
||||
col = torch.tensor([2, 4, 3]).to(ctx)
|
||||
val = torch.randn(3).to(ctx)
|
||||
coo_A = from_coo(row, col, val, shape)
|
||||
new_val = torch.randn(val_shape).to(ctx)
|
||||
coo_B = val_like(coo_A, new_val)
|
||||
check_val_like(coo_A, coo_B)
|
||||
|
||||
# CSR
|
||||
indptr, indices, _ = coo_A.csr()
|
||||
csr_A = from_csr(indptr, indices, val, shape)
|
||||
csr_B = val_like(csr_A, new_val)
|
||||
check_val_like(csr_A, csr_B)
|
||||
|
||||
# CSC
|
||||
indptr, indices, _ = coo_A.csc()
|
||||
csc_A = from_csc(indptr, indices, val, shape)
|
||||
csc_B = val_like(csc_A, new_val)
|
||||
check_val_like(csc_A, csc_B)
|
||||
|
||||
|
||||
def test_coalesce():
|
||||
ctx = F.ctx()
|
||||
|
||||
row = torch.tensor([1, 0, 0, 0, 1]).to(ctx)
|
||||
col = torch.tensor([1, 1, 1, 2, 2]).to(ctx)
|
||||
val = torch.arange(len(row)).to(ctx)
|
||||
A = from_coo(row, col, val, (4, 4))
|
||||
|
||||
assert A.has_duplicate()
|
||||
|
||||
A_coalesced = A.coalesce()
|
||||
|
||||
assert A_coalesced.nnz == 4
|
||||
assert A_coalesced.shape == (4, 4)
|
||||
assert list(A_coalesced.row) == [0, 0, 1, 1]
|
||||
assert list(A_coalesced.col) == [1, 2, 1, 2]
|
||||
# Values of duplicate indices are added together.
|
||||
assert list(A_coalesced.val) == [3, 3, 0, 4]
|
||||
assert not A_coalesced.has_duplicate()
|
||||
|
||||
|
||||
def test_has_duplicate():
|
||||
ctx = F.ctx()
|
||||
|
||||
row = torch.tensor([1, 0, 0, 0, 1]).to(ctx)
|
||||
col = torch.tensor([1, 1, 1, 2, 2]).to(ctx)
|
||||
val = torch.arange(len(row)).to(ctx)
|
||||
shape = (4, 4)
|
||||
|
||||
# COO
|
||||
coo_A = from_coo(row, col, val, shape)
|
||||
assert coo_A.has_duplicate()
|
||||
|
||||
# CSR
|
||||
indptr, indices, _ = coo_A.csr()
|
||||
csr_A = from_csr(indptr, indices, val, shape)
|
||||
assert csr_A.has_duplicate()
|
||||
|
||||
# CSC
|
||||
indptr, indices, _ = coo_A.csc()
|
||||
csc_A = from_csc(indptr, indices, val, shape)
|
||||
assert csc_A.has_duplicate()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"create_func", [rand_diag, rand_csr, rand_csc, rand_coo]
|
||||
)
|
||||
@pytest.mark.parametrize("shape", [(5, 5), (6, 4)])
|
||||
@pytest.mark.parametrize("dense_dim", [None, 4])
|
||||
@pytest.mark.parametrize("select_dim", [0, 1])
|
||||
@pytest.mark.parametrize("index", [(0, 1, 3), (1, 2)])
|
||||
def test_index_select(create_func, shape, dense_dim, select_dim, index):
|
||||
ctx = F.ctx()
|
||||
A = create_func(shape, 20, ctx, dense_dim)
|
||||
index = torch.tensor(index).to(ctx)
|
||||
A_select = A.index_select(select_dim, index)
|
||||
|
||||
dense = sparse_matrix_to_dense(A)
|
||||
dense_select = torch.index_select(dense, select_dim, index)
|
||||
|
||||
A_select_to_dense = sparse_matrix_to_dense(A_select)
|
||||
|
||||
assert A_select_to_dense.shape == dense_select.shape
|
||||
assert torch.allclose(A_select_to_dense, dense_select)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"create_func", [rand_diag, rand_csr, rand_csc, rand_coo]
|
||||
)
|
||||
@pytest.mark.parametrize("shape", [(5, 5), (6, 4)])
|
||||
@pytest.mark.parametrize("dense_dim", [None, 4])
|
||||
@pytest.mark.parametrize("select_dim", [0, 1])
|
||||
@pytest.mark.parametrize("rang", [slice(0, 2), slice(1, 3)])
|
||||
def test_range_select(create_func, shape, dense_dim, select_dim, rang):
|
||||
ctx = F.ctx()
|
||||
A = create_func(shape, 20, ctx, dense_dim)
|
||||
A_select = A.range_select(select_dim, rang)
|
||||
|
||||
dense = sparse_matrix_to_dense(A)
|
||||
if select_dim == 0:
|
||||
dense_select = dense[rang, :]
|
||||
else:
|
||||
dense_select = dense[:, rang]
|
||||
|
||||
A_select_to_dense = sparse_matrix_to_dense(A_select)
|
||||
|
||||
assert A_select_to_dense.shape == dense_select.shape
|
||||
assert torch.allclose(A_select_to_dense, dense_select)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"create_func", [rand_diag, rand_csr, rand_csc, rand_coo]
|
||||
)
|
||||
@pytest.mark.parametrize("index", [(0, 1, 2, 3, 4), (0, 1, 3), (1, 1, 2)])
|
||||
@pytest.mark.parametrize("replace", [False, True])
|
||||
@pytest.mark.parametrize("bias", [False, True])
|
||||
def test_sample_rowwise(create_func, index, replace, bias):
|
||||
ctx = F.ctx()
|
||||
shape = (5, 5)
|
||||
sample_dim = 0
|
||||
sample_num = 3
|
||||
A = create_func(shape, 10, ctx)
|
||||
A = val_like(A, torch.abs(A.val))
|
||||
|
||||
index = torch.tensor(index).to(ctx)
|
||||
|
||||
A_sample = A.sample(sample_dim, sample_num, index, replace, bias)
|
||||
A_dense = sparse_matrix_to_dense(A)
|
||||
A_sample_to_dense = sparse_matrix_to_dense(A_sample)
|
||||
|
||||
ans_shape = (index.size(0), shape[1])
|
||||
# Verify sample elements in origin rows
|
||||
for i, row in enumerate(list(index)):
|
||||
ans_ele = list(A_dense[row, :].nonzero().reshape(-1))
|
||||
ret_ele = list(A_sample_to_dense[i, :].nonzero().reshape(-1))
|
||||
for e in ret_ele:
|
||||
assert e in ans_ele
|
||||
if replace:
|
||||
# The number of sample elements in one row should be equal to
|
||||
# 'sample_num' if the row is not empty otherwise should be
|
||||
# equal to 0.
|
||||
assert list(A_sample.row).count(torch.tensor(i)) == (
|
||||
sample_num if len(ans_ele) != 0 else 0
|
||||
)
|
||||
else:
|
||||
assert len(ret_ele) == min(sample_num, len(ans_ele))
|
||||
|
||||
assert A_sample.shape == ans_shape
|
||||
if not replace:
|
||||
assert not A_sample.has_duplicate()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"create_func", [rand_diag, rand_csr, rand_csc, rand_coo]
|
||||
)
|
||||
@pytest.mark.parametrize("index", [(0, 1, 2, 3, 4), (0, 1, 3), (1, 1, 2)])
|
||||
@pytest.mark.parametrize("replace", [False, True])
|
||||
@pytest.mark.parametrize("bias", [False, True])
|
||||
def test_sample_columnwise(create_func, index, replace, bias):
|
||||
ctx = F.ctx()
|
||||
shape = (5, 5)
|
||||
sample_dim = 1
|
||||
sample_num = 3
|
||||
A = create_func(shape, 10, ctx)
|
||||
A = val_like(A, torch.abs(A.val))
|
||||
|
||||
index = torch.tensor(index).to(ctx)
|
||||
|
||||
A_sample = A.sample(sample_dim, sample_num, index, replace, bias)
|
||||
A_dense = sparse_matrix_to_dense(A)
|
||||
A_sample_to_dense = sparse_matrix_to_dense(A_sample)
|
||||
|
||||
ans_shape = (shape[0], index.size(0))
|
||||
# Verify sample elements in origin columns
|
||||
for i, col in enumerate(list(index)):
|
||||
ans_ele = list(A_dense[:, col].nonzero().reshape(-1))
|
||||
ret_ele = list(A_sample_to_dense[:, i].nonzero().reshape(-1))
|
||||
for e in ret_ele:
|
||||
assert e in ans_ele
|
||||
if replace:
|
||||
# The number of sample elements in one column should be equal to
|
||||
# 'sample_num' if the column is not empty otherwise should be
|
||||
# equal to 0.
|
||||
assert list(A_sample.col).count(torch.tensor(i)) == (
|
||||
sample_num if len(ans_ele) != 0 else 0
|
||||
)
|
||||
else:
|
||||
assert len(ret_ele) == min(sample_num, len(ans_ele))
|
||||
|
||||
assert A_sample.shape == ans_shape
|
||||
if not replace:
|
||||
assert not A_sample.has_duplicate()
|
||||
|
||||
|
||||
def test_print():
|
||||
ctx = F.ctx()
|
||||
|
||||
# basic
|
||||
row = torch.tensor([1, 1, 3]).to(ctx)
|
||||
col = torch.tensor([2, 1, 3]).to(ctx)
|
||||
val = torch.tensor([1.0, 1.0, 2.0]).to(ctx)
|
||||
A = from_coo(row, col, val)
|
||||
expected = (
|
||||
str(
|
||||
"""SparseMatrix(indices=tensor([[1, 1, 3],
|
||||
[2, 1, 3]]),
|
||||
values=tensor([1., 1., 2.]),
|
||||
shape=(4, 4), nnz=3)"""
|
||||
)
|
||||
if str(ctx) == "cpu"
|
||||
else str(
|
||||
"""SparseMatrix(indices=tensor([[1, 1, 3],
|
||||
[2, 1, 3]], device='cuda:0'),
|
||||
values=tensor([1., 1., 2.], device='cuda:0'),
|
||||
shape=(4, 4), nnz=3)"""
|
||||
)
|
||||
)
|
||||
assert str(A) == expected, print(A, expected)
|
||||
|
||||
# vector-shape non zero
|
||||
row = torch.tensor([1, 1, 3]).to(ctx)
|
||||
col = torch.tensor([2, 1, 3]).to(ctx)
|
||||
val = torch.tensor(
|
||||
[[1.3080, 1.5984], [-0.4126, 0.7250], [-0.5416, -0.7022]]
|
||||
).to(ctx)
|
||||
A = from_coo(row, col, val)
|
||||
expected = (
|
||||
str(
|
||||
"""SparseMatrix(indices=tensor([[1, 1, 3],
|
||||
[2, 1, 3]]),
|
||||
values=tensor([[ 1.3080, 1.5984],
|
||||
[-0.4126, 0.7250],
|
||||
[-0.5416, -0.7022]]),
|
||||
shape=(4, 4), nnz=3, val_size=(2,))"""
|
||||
)
|
||||
if str(ctx) == "cpu"
|
||||
else str(
|
||||
"""SparseMatrix(indices=tensor([[1, 1, 3],
|
||||
[2, 1, 3]], device='cuda:0'),
|
||||
values=tensor([[ 1.3080, 1.5984],
|
||||
[-0.4126, 0.7250],
|
||||
[-0.5416, -0.7022]], device='cuda:0'),
|
||||
shape=(4, 4), nnz=3, val_size=(2,))"""
|
||||
)
|
||||
)
|
||||
assert str(A) == expected, print(A, expected)
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
F._default_context_str == "cpu",
|
||||
reason="Device conversions don't need to be tested on CPU.",
|
||||
)
|
||||
@pytest.mark.parametrize("device", ["cpu", "cuda"])
|
||||
def test_to_device(device):
|
||||
row = torch.tensor([1, 1, 2])
|
||||
col = torch.tensor([1, 2, 0])
|
||||
mat = from_coo(row, col, shape=(3, 4))
|
||||
|
||||
target_row = row.to(device)
|
||||
target_col = col.to(device)
|
||||
target_val = mat.val.to(device)
|
||||
|
||||
mat2 = mat.to(device=device)
|
||||
assert mat2.shape == mat.shape
|
||||
assert torch.allclose(mat2.row, target_row)
|
||||
assert torch.allclose(mat2.col, target_col)
|
||||
assert torch.allclose(mat2.val, target_val)
|
||||
|
||||
mat2 = getattr(mat, device)()
|
||||
assert mat2.shape == mat.shape
|
||||
assert torch.allclose(mat2.row, target_row)
|
||||
assert torch.allclose(mat2.col, target_col)
|
||||
assert torch.allclose(mat2.val, target_val)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dtype", [torch.float, torch.double, torch.int, torch.long]
|
||||
)
|
||||
def test_to_dtype(dtype):
|
||||
row = torch.tensor([1, 1, 2])
|
||||
col = torch.tensor([1, 2, 0])
|
||||
mat = from_coo(row, col, shape=(3, 4))
|
||||
|
||||
target_val = mat.val.to(dtype=dtype)
|
||||
|
||||
mat2 = mat.to(dtype=dtype)
|
||||
assert mat2.shape == mat.shape
|
||||
assert torch.allclose(mat2.val, target_val)
|
||||
|
||||
func_name = {
|
||||
torch.float: "float",
|
||||
torch.double: "double",
|
||||
torch.int: "int",
|
||||
torch.long: "long",
|
||||
}
|
||||
mat2 = getattr(mat, func_name[dtype])()
|
||||
assert mat2.shape == mat.shape
|
||||
assert torch.allclose(mat2.val, target_val)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dense_dim", [None, 2])
|
||||
@pytest.mark.parametrize("row", [[0, 0, 1, 2], (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("col", [(0, 1, 2, 2), (1, 3, 3, 4)])
|
||||
@pytest.mark.parametrize("extra_shape", [(0, 1), (2, 1)])
|
||||
def test_sparse_matrix_transpose(dense_dim, row, col, extra_shape):
|
||||
mat_shape = (max(row) + 1 + extra_shape[0], max(col) + 1 + extra_shape[1])
|
||||
val_shape = (len(row),)
|
||||
if dense_dim is not None:
|
||||
val_shape += (dense_dim,)
|
||||
ctx = F.ctx()
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
row = torch.tensor(row).to(ctx)
|
||||
col = torch.tensor(col).to(ctx)
|
||||
mat = from_coo(row, col, val, mat_shape).transpose()
|
||||
mat_row, mat_col = mat.coo()
|
||||
mat_val = mat.val
|
||||
|
||||
assert mat.shape == mat_shape[::-1]
|
||||
assert torch.allclose(mat_val, val)
|
||||
assert torch.allclose(mat_row, col)
|
||||
assert torch.allclose(mat_col, row)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("row", [[0, 0, 1, 2], (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("col", [(0, 1, 2, 2), (1, 3, 3, 4)])
|
||||
@pytest.mark.parametrize("nz_dim", [None, 2])
|
||||
@pytest.mark.parametrize("shape", [(5, 5), (6, 7)])
|
||||
def test_torch_sparse_coo_conversion(row, col, nz_dim, shape):
|
||||
dev = F.ctx()
|
||||
row = torch.tensor(row).to(dev)
|
||||
col = torch.tensor(col).to(dev)
|
||||
indices = torch.stack([row, col])
|
||||
torch_sparse_shape = shape
|
||||
val_shape = (row.shape[0],)
|
||||
if nz_dim is not None:
|
||||
torch_sparse_shape += (nz_dim,)
|
||||
val_shape += (nz_dim,)
|
||||
val = torch.randn(val_shape).to(dev)
|
||||
torch_sparse_coo = torch.sparse_coo_tensor(indices, val, torch_sparse_shape)
|
||||
spmat = from_torch_sparse(torch_sparse_coo)
|
||||
|
||||
def _assert_spmat_equal_to_torch_sparse_coo(spmat, torch_sparse_coo):
|
||||
assert torch_sparse_coo.layout == torch.sparse_coo
|
||||
# Use .data_ptr() to check whether indices and values are on the same
|
||||
# memory address
|
||||
assert (
|
||||
spmat.indices().data_ptr() == torch_sparse_coo._indices().data_ptr()
|
||||
)
|
||||
assert spmat.val.data_ptr() == torch_sparse_coo._values().data_ptr()
|
||||
assert spmat.shape == torch_sparse_coo.shape[:2]
|
||||
|
||||
_assert_spmat_equal_to_torch_sparse_coo(spmat, torch_sparse_coo)
|
||||
torch_sparse_coo = to_torch_sparse_coo(spmat)
|
||||
_assert_spmat_equal_to_torch_sparse_coo(spmat, torch_sparse_coo)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("indptr", [(0, 0, 1, 4), (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("indices", [(0, 1, 2, 3), (1, 2, 3, 4)])
|
||||
@pytest.mark.parametrize("shape", [(3, 5), (3, 7)])
|
||||
def test_torch_sparse_csr_conversion(indptr, indices, shape):
|
||||
dev = F.ctx()
|
||||
indptr = torch.tensor(indptr).to(dev)
|
||||
indices = torch.tensor(indices).to(dev)
|
||||
torch_sparse_shape = shape
|
||||
val_shape = (indices.shape[0],)
|
||||
val = torch.randn(val_shape).to(dev)
|
||||
torch_sparse_csr = _torch_sparse_csr_tensor(
|
||||
indptr, indices, val, torch_sparse_shape
|
||||
)
|
||||
spmat = from_torch_sparse(torch_sparse_csr)
|
||||
|
||||
def _assert_spmat_equal_to_torch_sparse_csr(spmat, torch_sparse_csr):
|
||||
indptr, indices, value_indices = spmat.csr()
|
||||
assert torch_sparse_csr.layout == torch.sparse_csr
|
||||
assert value_indices is None
|
||||
# Use .data_ptr() to check whether indices and values are on the same
|
||||
# memory address
|
||||
assert indptr.data_ptr() == torch_sparse_csr.crow_indices().data_ptr()
|
||||
assert indices.data_ptr() == torch_sparse_csr.col_indices().data_ptr()
|
||||
assert spmat.val.data_ptr() == torch_sparse_csr.values().data_ptr()
|
||||
assert spmat.shape == torch_sparse_csr.shape[:2]
|
||||
|
||||
_assert_spmat_equal_to_torch_sparse_csr(spmat, torch_sparse_csr)
|
||||
torch_sparse_csr = to_torch_sparse_csr(spmat)
|
||||
_assert_spmat_equal_to_torch_sparse_csr(spmat, torch_sparse_csr)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("indptr", [(0, 0, 1, 4), (0, 1, 2, 4)])
|
||||
@pytest.mark.parametrize("indices", [(0, 1, 2, 3), (1, 2, 3, 4)])
|
||||
@pytest.mark.parametrize("shape", [(8, 3), (5, 3)])
|
||||
def test_torch_sparse_csc_conversion(indptr, indices, shape):
|
||||
dev = F.ctx()
|
||||
indptr = torch.tensor(indptr).to(dev)
|
||||
indices = torch.tensor(indices).to(dev)
|
||||
torch_sparse_shape = shape
|
||||
val_shape = (indices.shape[0],)
|
||||
val = torch.randn(val_shape).to(dev)
|
||||
torch_sparse_csc = torch.sparse_csc_tensor(
|
||||
indptr, indices, val, torch_sparse_shape
|
||||
)
|
||||
spmat = from_torch_sparse(torch_sparse_csc)
|
||||
|
||||
def _assert_spmat_equal_to_torch_sparse_csc(spmat, torch_sparse_csc):
|
||||
indptr, indices, value_indices = spmat.csc()
|
||||
assert torch_sparse_csc.layout == torch.sparse_csc
|
||||
assert value_indices is None
|
||||
# Use .data_ptr() to check whether indices and values are on the same
|
||||
# memory address
|
||||
assert indptr.data_ptr() == torch_sparse_csc.ccol_indices().data_ptr()
|
||||
assert indices.data_ptr() == torch_sparse_csc.row_indices().data_ptr()
|
||||
assert spmat.val.data_ptr() == torch_sparse_csc.values().data_ptr()
|
||||
assert spmat.shape == torch_sparse_csc.shape[:2]
|
||||
|
||||
_assert_spmat_equal_to_torch_sparse_csc(spmat, torch_sparse_csc)
|
||||
torch_sparse_csc = to_torch_sparse_csc(spmat)
|
||||
_assert_spmat_equal_to_torch_sparse_csc(spmat, torch_sparse_csc)
|
||||
|
||||
|
||||
### Diag foramt related tests ###
|
||||
|
||||
|
||||
@pytest.mark.parametrize("val_shape", [(3,), (3, 2)])
|
||||
@pytest.mark.parametrize("mat_shape", [None, (3, 5), (5, 3)])
|
||||
def test_diag(val_shape, mat_shape):
|
||||
ctx = F.ctx()
|
||||
# creation
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
mat = diag(val, mat_shape)
|
||||
|
||||
# val, shape attributes
|
||||
assert torch.allclose(mat.val, val)
|
||||
if mat_shape is None:
|
||||
mat_shape = (val_shape[0], val_shape[0])
|
||||
assert mat.shape == mat_shape
|
||||
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
|
||||
# nnz
|
||||
assert mat.nnz == val.shape[0]
|
||||
# dtype
|
||||
assert mat.dtype == val.dtype
|
||||
# device
|
||||
assert mat.device == val.device
|
||||
|
||||
# row, col, val
|
||||
edge_index = torch.arange(len(val)).to(mat.device)
|
||||
row, col = mat.coo()
|
||||
val = mat.val
|
||||
assert torch.allclose(row, edge_index)
|
||||
assert torch.allclose(col, edge_index)
|
||||
assert torch.allclose(val, val)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [(3, 3), (3, 5), (5, 3)])
|
||||
@pytest.mark.parametrize("d", [None, 2])
|
||||
def test_identity(shape, d):
|
||||
ctx = F.ctx()
|
||||
# creation
|
||||
mat = identity(shape, d)
|
||||
# shape
|
||||
assert mat.shape == shape
|
||||
# val
|
||||
len_val = min(shape)
|
||||
if d is None:
|
||||
val_shape = len_val
|
||||
else:
|
||||
val_shape = (len_val, d)
|
||||
val = torch.ones(val_shape)
|
||||
assert torch.allclose(val, mat.val)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("val_shape", [(3,), (3, 2)])
|
||||
@pytest.mark.parametrize("mat_shape", [None, (3, 5), (5, 3)])
|
||||
def test_diag_matrix_transpose(val_shape, mat_shape):
|
||||
ctx = F.ctx()
|
||||
val = torch.randn(val_shape).to(ctx)
|
||||
mat = diag(val, mat_shape).transpose()
|
||||
|
||||
assert torch.allclose(mat.val, val)
|
||||
if mat_shape is None:
|
||||
mat_shape = (val_shape[0], val_shape[0])
|
||||
assert mat.shape == mat_shape[::-1]
|
||||
@@ -0,0 +1,40 @@
|
||||
import sys
|
||||
|
||||
import backend as F
|
||||
import torch
|
||||
|
||||
from dgl.sparse import diag, spmatrix
|
||||
|
||||
|
||||
def test_neg():
|
||||
ctx = F.ctx()
|
||||
row = torch.tensor([1, 1, 3]).to(ctx)
|
||||
col = torch.tensor([1, 2, 3]).to(ctx)
|
||||
val = torch.tensor([1.0, 1.0, 2.0]).to(ctx)
|
||||
A = spmatrix(torch.stack([row, col]), val)
|
||||
neg_A = -A
|
||||
assert A.shape == neg_A.shape
|
||||
assert A.nnz == neg_A.nnz
|
||||
assert torch.allclose(-A.val, neg_A.val)
|
||||
assert torch.allclose(torch.stack(A.coo()), torch.stack(neg_A.coo()))
|
||||
assert A.val.device == neg_A.val.device
|
||||
|
||||
|
||||
def test_diag_neg():
|
||||
ctx = F.ctx()
|
||||
val = torch.arange(3).float().to(ctx)
|
||||
D = diag(val)
|
||||
neg_D = -D
|
||||
assert D.shape == neg_D.shape
|
||||
assert torch.allclose(-D.val, neg_D.val)
|
||||
assert D.val.device == neg_D.val.device
|
||||
|
||||
|
||||
def test_diag_inv():
|
||||
ctx = F.ctx()
|
||||
val = torch.arange(1, 4).float().to(ctx)
|
||||
D = diag(val)
|
||||
inv_D = D.inv()
|
||||
assert D.shape == inv_D.shape
|
||||
assert torch.allclose(1.0 / D.val, inv_D.val)
|
||||
assert D.val.device == inv_D.val.device
|
||||
@@ -0,0 +1,163 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from dgl.sparse import diag, from_csc, from_csr, SparseMatrix, spmatrix
|
||||
|
||||
np.random.seed(42)
|
||||
torch.random.manual_seed(42)
|
||||
|
||||
|
||||
def clone_detach_and_grad(t):
|
||||
t = t.clone().detach()
|
||||
t.requires_grad_()
|
||||
return t
|
||||
|
||||
|
||||
def rand_stride(t):
|
||||
"""Add stride to the last dimension of a tensor."""
|
||||
stride = np.random.randint(2, 4)
|
||||
ret = torch.stack([t] * stride, dim=-1)[..., 0]
|
||||
ret = ret.detach()
|
||||
if torch.is_floating_point(t):
|
||||
ret.requires_grad_()
|
||||
return ret
|
||||
|
||||
|
||||
def rand_coo(shape, nnz, dev, nz_dim=None):
|
||||
# Create a sparse matrix without duplicate entries.
|
||||
nnzid = np.random.choice(shape[0] * shape[1], nnz, replace=False)
|
||||
nnzid = torch.tensor(nnzid, device=dev).long()
|
||||
row = torch.div(nnzid, shape[1], rounding_mode="floor")
|
||||
col = nnzid % shape[1]
|
||||
if nz_dim is None:
|
||||
val = torch.randn(nnz, device=dev, requires_grad=True)
|
||||
else:
|
||||
val = torch.randn(nnz, nz_dim, device=dev, requires_grad=True)
|
||||
indices = torch.stack([row, col])
|
||||
indices = rand_stride(indices)
|
||||
val = rand_stride(val)
|
||||
return spmatrix(indices, val, shape)
|
||||
|
||||
|
||||
def rand_csr(shape, nnz, dev, nz_dim=None):
|
||||
# Create a sparse matrix without duplicate entries.
|
||||
nnzid = np.random.choice(shape[0] * shape[1], nnz, replace=False)
|
||||
nnzid = torch.tensor(nnzid, device=dev).long()
|
||||
row = torch.div(nnzid, shape[1], rounding_mode="floor")
|
||||
col = nnzid % shape[1]
|
||||
if nz_dim is None:
|
||||
val = torch.randn(nnz, device=dev, requires_grad=True)
|
||||
else:
|
||||
val = torch.randn(nnz, nz_dim, device=dev, requires_grad=True)
|
||||
indptr = torch.zeros(shape[0] + 1, device=dev, dtype=torch.int64)
|
||||
for r in row.tolist():
|
||||
indptr[r + 1] += 1
|
||||
indptr = torch.cumsum(indptr, 0)
|
||||
row_sorted, row_sorted_idx = torch.sort(row)
|
||||
indices = col[row_sorted_idx]
|
||||
indptr = rand_stride(indptr)
|
||||
indices = rand_stride(indices)
|
||||
val = rand_stride(val)
|
||||
return from_csr(indptr, indices, val, shape=shape)
|
||||
|
||||
|
||||
def rand_csc(shape, nnz, dev, nz_dim=None):
|
||||
# Create a sparse matrix without duplicate entries.
|
||||
nnzid = np.random.choice(shape[0] * shape[1], nnz, replace=False)
|
||||
nnzid = torch.tensor(nnzid, device=dev).long()
|
||||
row = torch.div(nnzid, shape[1], rounding_mode="floor")
|
||||
col = nnzid % shape[1]
|
||||
if nz_dim is None:
|
||||
val = torch.randn(nnz, device=dev, requires_grad=True)
|
||||
else:
|
||||
val = torch.randn(nnz, nz_dim, device=dev, requires_grad=True)
|
||||
indptr = torch.zeros(shape[1] + 1, device=dev, dtype=torch.int64)
|
||||
for c in col.tolist():
|
||||
indptr[c + 1] += 1
|
||||
indptr = torch.cumsum(indptr, 0)
|
||||
col_sorted, col_sorted_idx = torch.sort(col)
|
||||
indices = row[col_sorted_idx]
|
||||
indptr = rand_stride(indptr)
|
||||
indices = rand_stride(indices)
|
||||
val = rand_stride(val)
|
||||
return from_csc(indptr, indices, val, shape=shape)
|
||||
|
||||
|
||||
def rand_diag(shape, nnz, dev, nz_dim=None):
|
||||
nnz = min(shape)
|
||||
if nz_dim is None:
|
||||
val = torch.randn(nnz, device=dev, requires_grad=True)
|
||||
else:
|
||||
val = torch.randn(nnz, nz_dim, device=dev, requires_grad=True)
|
||||
return diag(val, shape)
|
||||
|
||||
|
||||
def rand_coo_uncoalesced(shape, nnz, dev):
|
||||
# Create a sparse matrix with possible duplicate entries.
|
||||
row = torch.randint(shape[0], (nnz,), device=dev)
|
||||
col = torch.randint(shape[1], (nnz,), device=dev)
|
||||
val = torch.randn(nnz, device=dev, requires_grad=True)
|
||||
indices = torch.stack([row, col])
|
||||
indices = rand_stride(indices)
|
||||
return spmatrix(indices, val, shape)
|
||||
|
||||
|
||||
def rand_csr_uncoalesced(shape, nnz, dev):
|
||||
# Create a sparse matrix with possible duplicate entries.
|
||||
row = torch.randint(shape[0], (nnz,), device=dev)
|
||||
col = torch.randint(shape[1], (nnz,), device=dev)
|
||||
val = torch.randn(nnz, device=dev, requires_grad=True)
|
||||
indptr = torch.zeros(shape[0] + 1, device=dev, dtype=torch.int64)
|
||||
for r in row.tolist():
|
||||
indptr[r + 1] += 1
|
||||
indptr = torch.cumsum(indptr, 0)
|
||||
row_sorted, row_sorted_idx = torch.sort(row)
|
||||
indices = col[row_sorted_idx]
|
||||
indptr = rand_stride(indptr)
|
||||
indices = rand_stride(indices)
|
||||
val = rand_stride(val)
|
||||
return from_csr(indptr, indices, val, shape=shape)
|
||||
|
||||
|
||||
def rand_csc_uncoalesced(shape, nnz, dev):
|
||||
# Create a sparse matrix with possible duplicate entries.
|
||||
row = torch.randint(shape[0], (nnz,), device=dev)
|
||||
col = torch.randint(shape[1], (nnz,), device=dev)
|
||||
val = torch.randn(nnz, device=dev, requires_grad=True)
|
||||
indptr = torch.zeros(shape[1] + 1, device=dev, dtype=torch.int64)
|
||||
for c in col.tolist():
|
||||
indptr[c + 1] += 1
|
||||
indptr = torch.cumsum(indptr, 0)
|
||||
col_sorted, col_sorted_idx = torch.sort(col)
|
||||
indices = row[col_sorted_idx]
|
||||
indptr = rand_stride(indptr)
|
||||
indices = rand_stride(indices)
|
||||
val = rand_stride(val)
|
||||
return from_csc(indptr, indices, val, shape=shape)
|
||||
|
||||
|
||||
def sparse_matrix_to_dense(A: SparseMatrix):
|
||||
dense = A.to_dense()
|
||||
return clone_detach_and_grad(dense)
|
||||
|
||||
|
||||
def sparse_matrix_to_torch_sparse(A: SparseMatrix, val=None):
|
||||
row, col = A.coo()
|
||||
edge_index = torch.cat((row.unsqueeze(0), col.unsqueeze(0)), 0)
|
||||
shape = A.shape
|
||||
if val is None:
|
||||
val = A.val
|
||||
val = val.clone().detach()
|
||||
if len(A.val.shape) > 1:
|
||||
shape += (A.val.shape[-1],)
|
||||
ret = torch.sparse_coo_tensor(edge_index, val, shape).coalesce()
|
||||
ret.requires_grad_()
|
||||
return ret
|
||||
|
||||
|
||||
def dense_mask(dense, sparse):
|
||||
ret = torch.zeros_like(dense)
|
||||
row, col = sparse.coo()
|
||||
for r, c in zip(row, col):
|
||||
ret[r, c] = dense[r, c]
|
||||
return ret
|
||||
@@ -0,0 +1,201 @@
|
||||
import unittest
|
||||
from statistics import mean
|
||||
|
||||
import backend as F
|
||||
|
||||
import dgl
|
||||
import dgl.ndarray as nd
|
||||
import dgl.ops as OPS
|
||||
import numpy as np
|
||||
import torch
|
||||
from dgl import rand_graph
|
||||
from dgl._ffi.streams import _dgl_get_stream, to_dgl_stream_handle
|
||||
from dgl.utils import to_dgl_context
|
||||
|
||||
|
||||
# borrowed from PyTorch, torch/testing/_internal/common_utils.py
|
||||
def _get_cycles_per_ms() -> float:
|
||||
"""Measure and return approximate number of cycles per millisecond for torch.cuda._sleep"""
|
||||
|
||||
def measure() -> float:
|
||||
start = torch.cuda.Event(enable_timing=True)
|
||||
end = torch.cuda.Event(enable_timing=True)
|
||||
start.record()
|
||||
torch.cuda._sleep(1000000)
|
||||
end.record()
|
||||
end.synchronize()
|
||||
cycles_per_ms = 1000000 / start.elapsed_time(end)
|
||||
return cycles_per_ms
|
||||
|
||||
# Get 10 values and remove the 2 max and 2 min and return the avg.
|
||||
# This is to avoid system disturbance that skew the results, e.g.
|
||||
# the very first cuda call likely does a bunch of init, which takes
|
||||
# much longer than subsequent calls.
|
||||
num = 10
|
||||
vals = []
|
||||
for _ in range(num):
|
||||
vals.append(measure())
|
||||
vals = sorted(vals)
|
||||
return mean(vals[2 : num - 2])
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
F._default_context_str == "cpu", reason="stream only runs on GPU."
|
||||
)
|
||||
def test_basics():
|
||||
g = rand_graph(10, 20, device=F.cpu())
|
||||
x = torch.ones(g.num_nodes(), 10)
|
||||
result = OPS.copy_u_sum(g, x).to(F.ctx())
|
||||
|
||||
# launch on default stream used in DGL
|
||||
xx = x.to(device=F.ctx())
|
||||
gg = g.to(device=F.ctx())
|
||||
OPS.copy_u_sum(gg, xx)
|
||||
assert torch.equal(OPS.copy_u_sum(gg, xx), result)
|
||||
|
||||
# launch on new stream created via torch.cuda
|
||||
s = torch.cuda.Stream(device=F.ctx())
|
||||
with torch.cuda.stream(s):
|
||||
xx = x.to(device=F.ctx(), non_blocking=True)
|
||||
gg = g.to(device=F.ctx())
|
||||
OPS.copy_u_sum(gg, xx)
|
||||
s.synchronize()
|
||||
assert torch.equal(OPS.copy_u_sum(gg, xx), result)
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
F._default_context_str == "cpu", reason="stream only runs on GPU."
|
||||
)
|
||||
def test_set_get_stream():
|
||||
current_stream = torch.cuda.current_stream()
|
||||
# test setting another stream
|
||||
s = torch.cuda.Stream(device=F.ctx())
|
||||
torch.cuda.set_stream(s)
|
||||
assert (
|
||||
to_dgl_stream_handle(s).value
|
||||
== _dgl_get_stream(to_dgl_context(F.ctx())).value
|
||||
)
|
||||
# revert to default stream
|
||||
torch.cuda.set_stream(current_stream)
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
F._default_context_str == "cpu", reason="stream only runs on GPU."
|
||||
)
|
||||
# borrowed from PyTorch, test/test_cuda.py: test_record_stream()
|
||||
def test_record_stream_ndarray():
|
||||
cycles_per_ms = _get_cycles_per_ms()
|
||||
|
||||
t = nd.array(np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float32), ctx=nd.cpu())
|
||||
t.pin_memory_()
|
||||
result = nd.empty([4], ctx=nd.gpu(0))
|
||||
stream = torch.cuda.Stream()
|
||||
ptr = [None]
|
||||
|
||||
# Performs the CPU->GPU copy in a background stream
|
||||
def perform_copy():
|
||||
with torch.cuda.stream(stream):
|
||||
tmp = t.copyto(nd.gpu(0))
|
||||
ptr[0] = F.from_dgl_nd(tmp).data_ptr()
|
||||
torch.cuda.current_stream().wait_stream(stream)
|
||||
tmp.record_stream(to_dgl_stream_handle(torch.cuda.current_stream()))
|
||||
torch.cuda._sleep(int(50 * cycles_per_ms)) # delay the copy
|
||||
result.copyfrom(tmp)
|
||||
|
||||
perform_copy()
|
||||
with torch.cuda.stream(stream):
|
||||
tmp2 = nd.empty([4], ctx=nd.gpu(0))
|
||||
assert (
|
||||
F.from_dgl_nd(tmp2).data_ptr() != ptr[0]
|
||||
), "allocation re-used too soon"
|
||||
|
||||
assert torch.equal(
|
||||
F.from_dgl_nd(result).cpu(), torch.tensor([1.0, 2.0, 3.0, 4.0])
|
||||
)
|
||||
|
||||
# Check that the block will be re-used after the main stream finishes
|
||||
torch.cuda.current_stream().synchronize()
|
||||
with torch.cuda.stream(stream):
|
||||
tmp3 = nd.empty([4], ctx=nd.gpu(0))
|
||||
assert (
|
||||
F.from_dgl_nd(tmp3).data_ptr() == ptr[0]
|
||||
), "allocation not re-used"
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
F._default_context_str == "cpu", reason="stream only runs on GPU."
|
||||
)
|
||||
def test_record_stream_graph_positive():
|
||||
cycles_per_ms = _get_cycles_per_ms()
|
||||
|
||||
g = rand_graph(10, 20, device=F.cpu())
|
||||
g.create_formats_()
|
||||
x = torch.ones(g.num_nodes(), 10).to(F.ctx())
|
||||
g1 = g.to(F.ctx())
|
||||
# this is necessary to initialize the cusparse handle
|
||||
result = OPS.copy_u_sum(g1, x)
|
||||
torch.cuda.current_stream().synchronize()
|
||||
|
||||
stream = torch.cuda.Stream()
|
||||
results2 = torch.zeros_like(result)
|
||||
|
||||
# Performs the computing in a background stream
|
||||
def perform_computing():
|
||||
with torch.cuda.stream(stream):
|
||||
g2 = g.to(F.ctx())
|
||||
torch.cuda.current_stream().wait_stream(stream)
|
||||
g2.record_stream(torch.cuda.current_stream())
|
||||
torch.cuda._sleep(int(50 * cycles_per_ms)) # delay the computing
|
||||
results2.copy_(OPS.copy_u_sum(g2, x))
|
||||
|
||||
perform_computing()
|
||||
with torch.cuda.stream(stream):
|
||||
# since we have called record stream for g2, g3 won't reuse its memory
|
||||
g3 = rand_graph(10, 20, device=F.ctx())
|
||||
g3.create_formats_()
|
||||
torch.cuda.current_stream().synchronize()
|
||||
assert torch.equal(result, results2)
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
F._default_context_str == "cpu", reason="stream only runs on GPU."
|
||||
)
|
||||
def test_record_stream_graph_negative():
|
||||
cycles_per_ms = _get_cycles_per_ms()
|
||||
|
||||
g = rand_graph(10, 20, device=F.cpu())
|
||||
g.create_formats_()
|
||||
x = torch.ones(g.num_nodes(), 10).to(F.ctx())
|
||||
g1 = g.to(F.ctx())
|
||||
# this is necessary to initialize the cusparse handle
|
||||
result = OPS.copy_u_sum(g1, x)
|
||||
torch.cuda.current_stream().synchronize()
|
||||
|
||||
stream = torch.cuda.Stream()
|
||||
results2 = torch.zeros_like(result)
|
||||
|
||||
# Performs the computing in a background stream
|
||||
def perform_computing():
|
||||
with torch.cuda.stream(stream):
|
||||
g2 = g.to(F.ctx())
|
||||
torch.cuda.current_stream().wait_stream(stream)
|
||||
# omit record_stream will produce a wrong result
|
||||
# g2.record_stream(torch.cuda.current_stream())
|
||||
torch.cuda._sleep(int(50 * cycles_per_ms)) # delay the computing
|
||||
results2.copy_(OPS.copy_u_sum(g2, x))
|
||||
|
||||
perform_computing()
|
||||
with torch.cuda.stream(stream):
|
||||
# g3 will reuse g2's memory block, resulting a wrong result
|
||||
g3 = rand_graph(10, 20, device=F.ctx())
|
||||
g3.create_formats_()
|
||||
torch.cuda.current_stream().synchronize()
|
||||
assert not torch.equal(result, results2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_basics()
|
||||
test_set_get_stream()
|
||||
test_record_stream_ndarray()
|
||||
test_record_stream_graph_positive()
|
||||
test_record_stream_graph_negative()
|
||||
@@ -0,0 +1,31 @@
|
||||
import io
|
||||
import pickle
|
||||
|
||||
import dgl
|
||||
|
||||
import networkx as nx
|
||||
import torch
|
||||
|
||||
|
||||
def _reconstruct_pickle(obj):
|
||||
f = io.BytesIO()
|
||||
pickle.dump(obj, f)
|
||||
f.seek(0)
|
||||
obj = pickle.load(f)
|
||||
f.close()
|
||||
return obj
|
||||
|
||||
|
||||
def test_pickling_batched_graph():
|
||||
# NOTE: this is a test for a wierd bug mentioned in
|
||||
# https://github.com/dmlc/dgl/issues/438
|
||||
glist = [nx.path_graph(i + 5) for i in range(5)]
|
||||
glist = [dgl.from_networkx(g) for g in glist]
|
||||
bg = dgl.batch(glist)
|
||||
bg.ndata["x"] = torch.randn((35, 5))
|
||||
bg.edata["y"] = torch.randn((60, 3))
|
||||
new_bg = _reconstruct_pickle(bg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_pickling_batched_graph()
|
||||
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import dgl
|
||||
|
||||
import torch as th
|
||||
import torch.multiprocessing as mp
|
||||
|
||||
|
||||
def sub_ipc(g):
|
||||
print(g)
|
||||
return g
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", reason="Do not support windows yet")
|
||||
def test_torch_ipc():
|
||||
g = dgl.graph(([0, 1, 2], [1, 2, 3]))
|
||||
ctx = mp.get_context("spawn")
|
||||
p = ctx.Process(target=sub_ipc, args=(g,))
|
||||
|
||||
p.start()
|
||||
p.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_torch_ipc()
|
||||
@@ -0,0 +1,96 @@
|
||||
import backend as F
|
||||
|
||||
import dgl
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
F._default_context_str == "cpu", reason="Need gpu for this test."
|
||||
)
|
||||
def test_pin_noncontiguous():
|
||||
t = torch.empty([10, 100]).transpose(0, 1)
|
||||
|
||||
assert not t.is_contiguous()
|
||||
assert not F.is_pinned(t)
|
||||
|
||||
with pytest.raises(dgl.DGLError):
|
||||
dgl.utils.pin_memory_inplace(t)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
F._default_context_str == "cpu", reason="Need gpu for this test."
|
||||
)
|
||||
def test_pin_view():
|
||||
t = torch.empty([100, 10])
|
||||
v = t[10:20]
|
||||
|
||||
assert v.is_contiguous()
|
||||
assert not F.is_pinned(t)
|
||||
|
||||
with pytest.raises(dgl.DGLError):
|
||||
dgl.utils.pin_memory_inplace(v)
|
||||
|
||||
# make sure an empty view does not generate an error
|
||||
u = t[10:10]
|
||||
u = dgl.utils.pin_memory_inplace(u)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
F._default_context_str == "cpu", reason="Need gpu for this test."
|
||||
)
|
||||
def test_unpin_automatically():
|
||||
# run a sufficient number of iterations such that the memory pool should be
|
||||
# re-used
|
||||
for j in range(10):
|
||||
t = torch.ones(10000, 10)
|
||||
assert not F.is_pinned(t)
|
||||
nd = dgl.utils.pin_memory_inplace(t)
|
||||
assert F.is_pinned(t)
|
||||
del nd
|
||||
# dgl.ndarray will unpin its data upon destruction
|
||||
assert not F.is_pinned(t)
|
||||
del t
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
F._default_context_str == "cpu", reason="Need gpu for this test."
|
||||
)
|
||||
def test_pin_unpin_column():
|
||||
g = dgl.graph(([1, 2, 3, 4], [0, 0, 0, 0]))
|
||||
|
||||
g.ndata["x"] = torch.randn(g.num_nodes())
|
||||
g.pin_memory_()
|
||||
assert g.is_pinned()
|
||||
assert g.ndata["x"].is_pinned()
|
||||
for col in g._node_frames[0].values():
|
||||
assert col.pinned_by_dgl
|
||||
assert col._data_nd is not None
|
||||
|
||||
g.ndata["x"] = torch.randn(g.num_nodes()) # unpin the old ndata['x']
|
||||
assert g.is_pinned()
|
||||
for col in g._node_frames[0].values():
|
||||
assert not col.pinned_by_dgl
|
||||
assert col._data_nd is None
|
||||
assert not g.ndata["x"].is_pinned()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
F._default_context_str == "cpu", reason="Need gpu for this test."
|
||||
)
|
||||
def test_pin_empty():
|
||||
t = torch.tensor([])
|
||||
assert not t.is_pinned()
|
||||
|
||||
# Empty tensors will not be pinned or unpinned. It's a no-op.
|
||||
# This is also the default behavior in PyTorch.
|
||||
# We just check that it won't raise an error.
|
||||
nd = dgl.utils.pin_memory_inplace(t)
|
||||
assert not t.is_pinned()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_pin_noncontiguous()
|
||||
test_pin_view()
|
||||
test_unpin_automatically()
|
||||
test_pin_unpin_column()
|
||||
Reference in New Issue
Block a user