@@ -148,9 +148,10 @@ std::optional<std::unordered_map<int32_t, torch::Tensor>> PPRForwardPush::drainQ
148148
149149void 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 }
0 commit comments