Skip to content

Commit b54e0bf

Browse files
committed
Vectorized edgebank
1 parent 32f7bfc commit b54e0bf

2 files changed

Lines changed: 11 additions & 66 deletions

File tree

examples/linkproppred/edgebank.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ def eval(
6464
seed_everything(args.seed)
6565
evaluator = Evaluator(name=args.dataset)
6666

67-
train_data, val_data, test_data = DGData.from_tgb(args.dataset).split()
67+
full_data = DGData.from_tgb(args.dataset)
68+
train_data, val_data, test_data = full_data.split()
6869
train_dg = DGraph(train_data)
6970
val_dg = DGraph(val_data)
7071
test_dg = DGraph(test_data)
@@ -82,6 +83,7 @@ def eval(
8283
train_data.edge_src,
8384
train_data.edge_dst,
8485
train_data.edge_time,
86+
N=full_data.num_nodes,
8587
memory_mode=args.memory_mode,
8688
window_ratio=args.window_ratio,
8789
pos_prob=args.pos_prob,

tgm/nn/modules/edgebank.py

Lines changed: 8 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, Literal, Optional, Tuple
1+
from typing import Any, Literal, Optional, Tuple
22

33
import torch
44

@@ -28,6 +28,7 @@ def __init__(
2828
src: torch.Tensor,
2929
dst: torch.Tensor,
3030
ts: torch.Tensor,
31+
N: int,
3132
memory_mode: Literal['unlimited', 'fixed'] = 'unlimited',
3233
window_ratio: float = 0.15,
3334
pos_prob: float = 1.0,
@@ -46,6 +47,7 @@ def __init__(
4647
src (torch.Tensor): Source node IDs of edges used for initialization.
4748
dst (torch.Tensor): Destination node IDs of edges used for initialization.
4849
ts (torch.Tensor): Timestamps of edges used for initialization.
50+
N (int): total number of nodes in the graph.
4951
memory_mode (Literal['unlimited', 'fixed'], optional):
5052
- ``'unlimited'``: Keeps all observed edges in memory.
5153
- ``'fixed'``: Keeps only edges within a sliding window of time.
@@ -83,27 +85,10 @@ def __init__(
8385
self._window_start = ts.max() - window_ratio * (ts.max() - ts.min())
8486
self._window_size = self._window_end - self._window_start
8587

86-
self.memory: Dict[Tuple[int, int], int] = {}
87-
# maintain bidirectional linked list with 2 pointers
88-
self._head: Optional[_Event] = None
89-
self._tail: Optional[_Event] = None
90-
91-
logger.warning(
92-
'EdgeBank will be slow if events are added/updated out of order.'
93-
)
88+
self._memory = torch.zeros((N, N), dtype=ts.dtype, device=ts.device)
9489

9590
self.update(src, dst, ts)
9691

97-
def _clean_up(self) -> None:
98-
"""Clean up edges that are out of window in memory."""
99-
while self._head and self._head.ts < self._window_start:
100-
curr_event = self._head
101-
if self.memory.get(curr_event.edge, -1) == curr_event.ts:
102-
self.memory.pop(curr_event.edge)
103-
self._head = curr_event.right
104-
if self._head == None:
105-
self._tail = None
106-
10792
def update(self, src: torch.Tensor, dst: torch.Tensor, ts: torch.Tensor) -> None:
10893
"""Update EdgeBank memory with a batch of edges.
10994
@@ -119,43 +104,7 @@ def update(self, src: torch.Tensor, dst: torch.Tensor, ts: torch.Tensor) -> None
119104
self._check_input_data(src, dst, ts)
120105
self._window_end = torch.max(self._window_end, ts.max())
121106
self._window_start = self._window_end - self._window_size
122-
123-
if (
124-
self._fixed_memory
125-
and self._head is not None
126-
and self._tail is not None
127-
and self._head.ts < self._window_start
128-
):
129-
self._clean_up()
130-
131-
for src_, dst_, ts_ in zip(src, dst, ts):
132-
src_, dst_, ts_ = src_.item(), dst_.item(), ts_.item()
133-
if ts_ >= self._window_start:
134-
self.memory[(src_, dst_)] = ts_
135-
if self._head == self._tail == None:
136-
self._head = self._tail = _Event((src_, dst_), ts_, None, None)
137-
elif self._head is not None and self._tail is not None:
138-
new_event = _Event((src_, dst_), ts_, left=None, right=None)
139-
curr: _Event | None = self._tail
140-
141-
# This while loop should never run assuming events added in time-ascending order.
142-
# When events added out of order, time complexity would be O(n)
143-
while curr is not None and ts_ < curr.ts:
144-
curr = curr.left
145-
146-
if curr == None:
147-
new_event.right = self._head
148-
if self._head is not None:
149-
self._head.left = new_event
150-
self._head = new_event
151-
else:
152-
new_event.left = curr
153-
new_event.right = curr.right # type: ignore[union-attr]
154-
if curr.right is not None: # type: ignore[union-attr]
155-
curr.right.left = new_event # type: ignore[union-attr]
156-
curr.right = new_event # type: ignore[union-attr]
157-
if curr == self._tail:
158-
self._tail = new_event
107+
self._memory[src, dst] = torch.maximum(self._memory[src, dst], ts)
159108

160109
def __call__(
161110
self, query_src: torch.Tensor, query_dst: torch.Tensor
@@ -172,15 +121,9 @@ def __call__(
172121
its probability is ``self.pos_prob``.
173122
- Otherwise, the probability is ``0.0``.
174123
"""
175-
pred = torch.zeros_like(query_src)
176-
src_list = query_src.tolist()
177-
dst_list = query_dst.tolist()
178-
for i, (s, d) in enumerate(zip(src_list, dst_list)):
179-
mem_val = self.memory.get((s, d))
180-
if mem_val is not None:
181-
if not self._fixed_memory or mem_val >= self.window_start:
182-
pred[i] = self.pos_prob
183-
return pred
124+
return (
125+
self._memory[query_src, query_dst] >= self.window_start
126+
).float() * self.pos_prob
184127

185128
@property
186129
def window_start(self) -> int | float:

0 commit comments

Comments
 (0)