Skip to content

Sparsify Dedup Hook - #60

Merged
Jacob-Chmura merged 10 commits into
mainfrom
perf/dict_idx
Jul 2, 2025
Merged

Sparsify Dedup Hook#60
Jacob-Chmura merged 10 commits into
mainfrom
perf/dict_idx

Conversation

@Jacob-Chmura

@Jacob-Chmura Jacob-Chmura commented Jun 24, 2025

Copy link
Copy Markdown
Member

Purpose

The purpose of this PR is to make an implementation change in our deduplication hook to speed up it's execution.

Given that each batch has ~600 nodes, but the node easily on the order of 10-100k, the following is inefficient in both time and space:

tgm/tgm/hooks.py

Lines 128 to 132 in 5b4949e

all_nids = torch.cat(nids, dim=0)
unique_nids = torch.unique(all_nids)
max_nid = int(unique_nids.max().item())
nid_to_idx = torch.full((max_nid + 1,), -1, dtype=torch.long, device=device)
nid_to_idx[unique_nids] = torch.arange(len(unique_nids), device=device)

Hence, I switched to sparse tensor implementation here ( I tried native Python dict and only got ~5% setup).

Outcome

End to end latency reduction by ~33%

Case (master)

Screenshot from 2025-06-24 15-55-49

Control (perf/dict_idx)

Screenshot from 2025-06-24 16-17-56

Key Changes

  • sparsified index lookup in the dedup hook
  • deferred index lookup in hook to forward pass (e.g. instead of z[batch.src_idx], user will manually issue z[batch.global_to_local(batch.src)]
  • rename nid_to_idx -> global_to_local to make it more understandable
  • updated all examples to use this new hook

Relevant Prs

Close #59

@Jacob-Chmura Jacob-Chmura self-assigned this Jun 24, 2025
@Jacob-Chmura Jacob-Chmura changed the title Dict-based Dedup Hook Sparsify Dedup Hook Jun 24, 2025
@Jacob-Chmura
Jacob-Chmura marked this pull request as ready for review June 24, 2025 20:29
Comment thread tgm/hooks.py
nbr_nids_idx = torch.full_like(hop_nids, -1)
nbr_nids_idx[hop_mask] = nid_to_idx[hop_nids[hop_mask]]
batch.nbr_nids_idx.append(nbr_nids_idx) # type: ignore
batch.global_to_local = lambda x: torch.searchsorted(unique_nids, x) # type: ignore

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caching is possible but needs to be clearly useful (right now it's not really) to justify

@shenyangHuang

Copy link
Copy Markdown
Collaborator

are we getting the sparse tensor implementation speedup from using it on GPU? or it is just the difference between python dictionary and tensor implementation? It is quite interesting if both are on CPU as I thought python dictionaries would be quite efficient for this. Good optimization to have over all.

@shenyangHuang shenyangHuang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good, global to local is more intuitive, though it is long to type, but we can keep it for now.

device = batch.src.device
z = torch.zeros(len(batch.unique_nids), self.embed_dim, device=device)

for hop in reversed(range(self.num_layers)):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah this should go into nn layers, also after we changed the torch.zeros as hard coded feat

edge_weight = batch.edge_weight if hasattr(batch, 'edge_weight') else None # type: ignore
z, h_0, c_0 = self.encoder(node_feat, edge_index, edge_weight, h_0, c_0)
z_src, z_dst, z_neg = z[batch.src_idx], z[batch.dst_idx], z[batch.neg_idx] # type: ignore
z_src = z[batch.global_to_local(batch.src)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a better name. Flagging this to show to users in documentation

(*batch.nids[hop].shape, self.embed_dim), device=device
)
node_time_feat = self.time_encoder(torch.zeros_like(batch.nids[hop]))
node_feat = torch.zeros((*seed_nodes.shape, self.embed_dim), device=device)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, flag to read static node features, even if it is zeros, we should read it

# If next next hops embeddings exist, use them instead of raw features
if hop < self.num_layers - 1:
nbr_feat = z[batch.nbr_nids_idx[hop]]
nbr_feat = z[batch.global_to_local(nbrs)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

global_to_local is very intuitive, though slightly long to type given how often we use it. maybe g2l? not sure

@codecov

codecov Bot commented Jul 2, 2025

Copy link
Copy Markdown

Codecov Report

All modified and coverable lines are covered by tests ✅

📢 Thoughts on this report? Let us know!

@Jacob-Chmura
Jacob-Chmura merged commit 16b524f into main Jul 2, 2025
6 checks passed
@Jacob-Chmura
Jacob-Chmura deleted the perf/dict_idx branch July 2, 2025 15:23
@Jacob-Chmura Jacob-Chmura mentioned this pull request Jul 3, 2025
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Switch Dedup Hook LocalGlobal Index to Sparse Layout

2 participants