Skip to content

Commit b6edda5

Browse files
authored
Reuse fetched PPR neighbor cache entries (#703)
1 parent 1d0af92 commit b6edda5

2 files changed

Lines changed: 42 additions & 43 deletions

File tree

gigl-core/core/sampling/ppr_forward_push.cpp

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,10 @@ std::optional<std::unordered_map<int32_t, torch::Tensor>> PPRForwardPush::drainQ
148148

149149
void PPRForwardPush::pushResiduals(
150150
const std::unordered_map<int32_t, std::tuple<torch::Tensor, torch::Tensor, torch::Tensor>>& fetchedByEtypeId) {
151-
// Step 1: Unpack the input map into a C++ map keyed by packKey(nodeId, edgeTypeId)
152-
// for fast lookup during the residual-push loop below.
153-
std::unordered_map<uint64_t, std::vector<int32_t>> fetched;
151+
// Step 1: Persist fetched neighbor lists in the per-state cache. drainQueue()
152+
// consults this cache before requesting future lookups, so storing every
153+
// fetched row here avoids re-fetching a (node, edge type) pair if it re-enters
154+
// the frontier later in the same PPR channel.
154155
for (const auto& [edgeTypeId, neighborTensors] : fetchedByEtypeId) {
155156
const auto& nodeIdsTensor = std::get<0>(neighborTensors);
156157
const auto& flatNeighborIdsTensor = std::get<1>(neighborTensors);
@@ -172,7 +173,10 @@ void PPRForwardPush::pushResiduals(
172173
for (int64_t neighborIdx = 0; neighborIdx < count; ++neighborIdx) {
173174
neighborIds[neighborIdx] = static_cast<int32_t>(flatNeighborIdsAccessor[offset + neighborIdx]);
174175
}
175-
fetched[packKey(nodeId, edgeTypeId)] = std::move(neighborIds);
176+
uint64_t cacheKey = packKey(nodeId, edgeTypeId);
177+
if (_neighborCache.find(cacheKey) == _neighborCache.end()) {
178+
_neighborCache.emplace(cacheKey, std::move(neighborIds));
179+
}
176180
offset += count;
177181
}
178182
}
@@ -197,21 +201,16 @@ void PPRForwardPush::pushResiduals(
197201
srcNodeTypeState.pprScores[sourceNodeId] += sourceResidual;
198202
srcNodeTypeState.residuals[sourceNodeId] = 0.0;
199203

200-
// b. Count total fetched/cached neighbors across all edge types for
204+
// b. Count total cached neighbors across all edge types for
201205
// this source node. We normalise by the number of neighbors we
202206
// actually retrieved, not the true degree, so residual is fully
203207
// distributed among known neighbors rather than leaking to unfetched
204208
// ones (which matters when num_neighbors_per_hop < true_degree).
205-
int32_t totalFetched = 0;
209+
int32_t totalCachedNeighbors = 0;
206210
for (int32_t edgeTypeId : _nodeTypeToEdgeTypeIds[nodeTypeId]) {
207-
auto fetchedEntry = fetched.find(packKey(sourceNodeId, edgeTypeId));
208-
if (fetchedEntry != fetched.end()) {
209-
totalFetched += static_cast<int32_t>(fetchedEntry->second.size());
210-
} else {
211-
auto cachedEntry = _neighborCache.find(packKey(sourceNodeId, edgeTypeId));
212-
if (cachedEntry != _neighborCache.end()) {
213-
totalFetched += static_cast<int32_t>(cachedEntry->second.size());
214-
}
211+
auto cachedEntry = _neighborCache.find(packKey(sourceNodeId, edgeTypeId));
212+
if (cachedEntry != _neighborCache.end()) {
213+
totalCachedNeighbors += static_cast<int32_t>(cachedEntry->second.size());
215214
}
216215
}
217216
// Two cases reach here:
@@ -221,32 +220,23 @@ void PPRForwardPush::pushResiduals(
221220
// This overstates src and understates its neighbors. This is expected
222221
// behavior when max_fetch_iterations is set, which intentionally trades
223222
// theoretical PPR correctness for better throughput.
224-
if (totalFetched == 0) {
223+
if (totalCachedNeighbors == 0) {
225224
continue;
226225
}
227226

228-
double residualPerNeighbor = (1.0 - _alpha) * sourceResidual / static_cast<double>(totalFetched);
227+
double residualPerNeighbor =
228+
(1.0 - _alpha) * sourceResidual / static_cast<double>(totalCachedNeighbors);
229229

230230
for (int32_t edgeTypeId : _nodeTypeToEdgeTypeIds[nodeTypeId]) {
231-
// Invariant: fetched and _neighborCache are mutually exclusive for
232-
// any given (node, etype) key within one iteration. drainQueue()
233-
// only requests a fetch for nodes absent from _neighborCache, so a
234-
// key is in at most one of the two.
235-
//
236231
// Neighbor list for this (src, edgeTypeId) pair, borrowed from whichever
237232
// map holds it. reference_wrapper is used because std::optional cannot
238233
// hold a reference directly, and we want to avoid copying the vector —
239-
// the data already exists in fetched or _neighborCache and both outlive
240-
// this loop body. Access via neighborList->get().
234+
// the data already exists in _neighborCache and outlives this loop body.
235+
// Access via neighborList->get().
241236
std::optional<std::reference_wrapper<const std::vector<int32_t>>> neighborList;
242-
auto fetchedEntry = fetched.find(packKey(sourceNodeId, edgeTypeId));
243-
if (fetchedEntry != fetched.end()) {
244-
neighborList = std::cref(fetchedEntry->second);
245-
} else {
246-
auto cachedEntry = _neighborCache.find(packKey(sourceNodeId, edgeTypeId));
247-
if (cachedEntry != _neighborCache.end()) {
248-
neighborList = std::cref(cachedEntry->second);
249-
}
237+
auto cachedEntry = _neighborCache.find(packKey(sourceNodeId, edgeTypeId));
238+
if (cachedEntry != _neighborCache.end()) {
239+
neighborList = std::cref(cachedEntry->second);
250240
}
251241
if (!neighborList || neighborList->get().empty()) {
252242
continue;
@@ -267,18 +257,6 @@ void PPRForwardPush::pushResiduals(
267257
dstNodeTypeState.residuals[neighborNodeId] >= threshold) {
268258
dstNodeTypeState.queue.insert(neighborNodeId);
269259
++_numNodesInQueue;
270-
271-
// Promote neighbor lists to the persistent cache: this node will
272-
// be processed next iteration, so caching avoids a re-fetch.
273-
for (int32_t neighborEdgeTypeId : _nodeTypeToEdgeTypeIds[dstNodeTypeId]) {
274-
uint64_t packedKey = packKey(neighborNodeId, neighborEdgeTypeId);
275-
if (_neighborCache.find(packedKey) == _neighborCache.end()) {
276-
auto fetchedNeighborEntry = fetched.find(packedKey);
277-
if (fetchedNeighborEntry != fetched.end()) {
278-
_neighborCache[packedKey] = fetchedNeighborEntry->second;
279-
}
280-
}
281-
}
282260
}
283261
}
284262
}

gigl-core/tests/ppr_forward_push_test.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,27 @@ TEST(PPRForwardPush, ResidualDistributedToNeighbor) {
8989
EXPECT_NEAR(weights[1].item<float>(), static_cast<float>((1.0 - alpha) * alpha), 1e-5F);
9090
}
9191

92+
// Once a (node, edge type) neighbor list is fetched, it should be cached for the
93+
// rest of the PPR state. In a cycle, revisiting node 0 should therefore require
94+
// no second lookup for node 0.
95+
TEST(PPRForwardPush, NeighborCacheAvoidsRefetchingPreviouslyFetchedNode) {
96+
auto state = makeState(/*seeds=*/{0}, /*alpha=*/0.5, /*requeueThresholdFactor=*/1e-9, /*degrees=*/{1, 1});
97+
98+
state.drainQueue();
99+
state.pushResiduals(makeFetched(/*edgeTypeId=*/0, /*nodeIds=*/{0}, /*flatNeighborIds=*/{1}, /*counts=*/{1}));
100+
101+
auto iter2 = state.drainQueue();
102+
ASSERT_TRUE(iter2.has_value());
103+
ASSERT_NE(iter2->find(0), iter2->end());
104+
ASSERT_EQ(iter2->at(0).size(0), 1);
105+
EXPECT_EQ(iter2->at(0)[0].item<int64_t>(), 1);
106+
state.pushResiduals(makeFetched(/*edgeTypeId=*/0, /*nodeIds=*/{1}, /*flatNeighborIds=*/{0}, /*counts=*/{1}));
107+
108+
auto iter3 = state.drainQueue();
109+
ASSERT_TRUE(iter3.has_value());
110+
EXPECT_TRUE(iter3->empty());
111+
}
112+
92113
// Two seeds (0 and 1) both push residual to sink node 2. The neighbor-lookup
93114
// request must deduplicate to one entry for node 2, yet both seeds must still
94115
// accumulate a PPR score for it.

0 commit comments

Comments
 (0)