The PageRank algorithm, famously introduced by Larry Page and Sergey Brin, revolutionized information retrieval by shifting the focus of search engines from simple keyword matching and term frequency toward a more global notion of importance. Instead of treating web pages as isolated documents, PageRank models the entire Web as a directed graph and evaluates authority through link structure.
It operates on the Random Surfer model, which assumes that a user repeatedly follows hyperlinks with some probability and occasionally jumps to a random page. Under this interpretation, the PageRank value of a page corresponds to the long-term probability that the surfer visits it. The core intuition is recursive and self-reinforcing: a page is important if it is referenced by other important pages.
This project explores the parallelization of PageRank using OpenMP on shared-memory systems. In particular, we compare three different parallelization strategies, each making different trade-offs between load balancing, synchronization overhead, and algorithmic efficiency on modern multi-core architectures.
The PageRank value
Where:
-
$N$ : The total number of nodes in the graph. -
$d$ : The damping factor (often set to$0.85$ ), representing the probability that a surfer follows a link. -
$D$ : The set of Dangling Nodes (nodes with no outgoing edges, i.e.,$Out(u) = \emptyset$ ). -
$In(u)$ : The set of nodes that point to$u$ (in-neighbors). -
$Out(u)$ : The set of nodes that begins from to$u$ (out-neighbors). -
$w_{v,u}$ : The weight of the edge from$v$ to$u$ .
In this project, the graph is represented using the Compressed Sparse Row (CSR) format, which stores adjacency information compactly and allows efficient traversal of edges. The choice of traversal strategy—whether iterating over sources or destinations—has a significant impact on memory access patterns, synchronization requirements, and overall parallel performance.
1. Push (Source-Centric)
This model iterates over source nodes pr_new[v].
- Pros: The algorithm is straightforward to implement and maps naturally to the mathematical definition. It also allows reductions to be expressed directly over edges.
- Cons: The algorithm writes to random memory locations (
pr_new[v]). In a parallel environment, multiple threads may attempt to update the same destination node concurrently, requiring atomic operations or locks. These operations serialize execution and significantly degrade scalability.
2. Pull (Destination-Centric)
This model iterates over destination nodes converse).
- Pros: Each thread writes exclusively to its own
pr_new[u], resulting in sequential, conflict-free writes. Since no two threads update the same element, no atomics or locks are required. This property makes the Pull model inherently parallel-friendly. - Cons: While writes are well-behaved, the reads from
pr_normalized[v]are effectively random, which may lead to cache misses and reduced memory locality.
Despite the cost of irregular reads, the Pull model’s lock-free nature makes it a better choice for parallel PageRank on shared-memory systems. All implementations in this project are therefore based on the Pull paradigm.
I implemented three variations of the parallel PageRank algorithm to investigate the trade-offs between load balancing, scheduling overhead, and algorithmic efficiency.
Real-world graphs often exhibit Power-Law degree distributions, where a small number of hub nodes have extremely high
degree while the majority of nodes have very few edges. Under such conditions, a naive static partitioning strategy (
assigning
Through empirical testing, a chunk size of 64 was found to provide the best trade-off:
- Chunk too small: excessive scheduling overhead.
- Chunk too large: insufficient responsiveness to highly skewed degree distributions.
Optimization:
In a naive implementation, the contribution would be calculated directly as w * pr[v] * damping / out_w[v] inside the
inner loop. Since v represents a neighbor index, accessing both pr[v] and out_w[v] would trigger two separate
random memory accesses (cache misses). So I introduced a pre-calculation loop for pr_normalized.
#pragma omp parallel
{
// Pre-calculate normalized values to avoid a random memory access
// Also collect the dangling nodes
#pragma omp for schedule(static) reduction(+: dangling_sum)
for (int i = 0; i < n; ++i) {
if (out_w[i] != 0) {
pr_normalized[i] = pr[i] * damping / (double) out_w[i];
} else {
dangling_sum += pr[i];
}
}
#pragma omp single
base_score = (1.0 - damping + damping * dangling_sum) / n;
#pragma omp for schedule(dynamic, 64) reduction(+: diff)
for (int u = 0; u < n; ++u) {
double sum = base_score;
const int start = converse->offset[u], end = converse->offset[u + 1];
for (int i = start; i < end; ++i) {
const int v = converse->m[i].v;
const int w = converse->m[i].w;
// Here we have one random memory access (bottleneck)
sum += w * pr_normalized[v];
}
pr_new[u] = sum;
diff += fabs(sum - pr[u]);
}
}- Pros: Robustness. It is easy to implement using standard OpenMP clauses.
-
Cons: High Overhead. For graphs with low average degrees (like
roadNet-CA, where avg degree$\approx 2.5$ ), the computation inside the inner loop is trivial. The overhead of the OpenMP runtime managing the dynamic queue becomes a dominant factor, limiting the maximum speedup.
To overcome the overhead of dynamic scheduling while maintaining load balance, I implemented a static partitioning strategy based on edge counts rather than node counts.
The goal is to define thread boundaries such that every thread processes exactly offset array is monotonic. We can use binary search (lower_bound) to find the exact node
indices that divide the edge array into equal segments.
Partitioning:
// Calculate partition boundaries based on edge counts
int *start_v = malloc(sizeof(int) * (num_threads + 1));
start_v[num_threads] = n;
#pragma omp parallel for schedule(static)
for (int i = 0; i < num_threads; i++) {
// Find the node index where the cumulative edge count reaches i * (total_edges / threads)
start_v[i] = lower_bound(converse->offset, 0, n + 1, i * e / num_threads);
}Inside the parallel region, each thread queries its ID (tid) and retrieves its pre-calculated range
[start_v[tid], start_v[tid + 1]).
// Distribute the nodes
#pragma omp parallel reduction(+: diff)
{
const int tid = omp_get_thread_num();
const int end_v = start_v[tid + 1];
for (int u = start_v[tid]; u < end_v; u++) {
// ... (standard Pull logic) ...
}
}-
Pros:
- Less Scheduling Overhead: The cost for scheduling is lower. This is a massive advantage for low-degree graphs (
roadNet) where the dynamic scheduler was a bottleneck. - Theoretical Load Balance: Every thread performs almost exactly the same number of memory reads and floating-point additions.
- Cache Locality: Threads process contiguous blocks of memory.
- Less Scheduling Overhead: The cost for scheduling is lower. This is a massive advantage for low-degree graphs (
-
Cons:
Lack of flexibility. It cannot adapt to system noise or background processes. Also, on modern CPUs with Hybrid Architectures (e.g., Intel P-cores vs. E-cores or ARM big.LITTLE), this approach suffers.
Also, although the workload is theoretically evenly distributed, due to factors such as processor architecture, some cores may exhibit severe slowdown (see the performance analysis section below for details).
The standard PageRank algorithm updates every node in every iteration. However, as the algorithm progresses, many nodes converge quickly and their values stop changing. Updating these stable nodes wastes memory bandwidth. The third implementation introduces an approximation by keeping an active list.
We maintain a list of active nodes (frontier). A node is added to the next iteration's frontier only if its PageRank
value changed significantly (
By skipping updates for stable nodes, we introduce a cumulative approximation error. The final PageRank scores will not be numerically identical to the exact eigenvector solution. However, for most practical applications, the exact probability value is less important than the relative ranking of the nodes. While the absolute values fluctuate slightly, the ordering of the top-ranked nodes remains somehow consistent with the exact version.
The Frontier Logic:
// 1. Compute updates only for nodes in current 'frontier'
#pragma omp parallel for schedule(static)
for (int i = 0; i < frontier_size; i++) {
const int u = frontier[i];
// ... compute sum ...
pr_new[u] = sum;
// Mark if this node needs to be active next time
next_idx_prefix[i] = fabs(sum - pr[u]) > eps;
}
// 2. Parallel Prefix Sum to calculate write positions
omp_prefix_sum(next_idx_prefix, frontier_size, prefix_block_sum);
// 3. Culling
#pragma omp parallel for schedule(static)
for (int i = 0; i < frontier_size; i++) {
const int prev_val = (i == 0) ? 0 : next_idx_prefix[i - 1];
if (next_idx_prefix[i] > prev_val) {
next_frontier[next_idx_prefix[i] - 1] = frontier[i];
}
}- Pros: The workload decreases exponentially, for both serial and parallel algorithms.
- Cons:
- Overhead: The parallel prefix sum and array compaction add overhead. In practice, the speedup and scalability of the parallelized algorithm isn't that good.
- Complexity: Requires managing explicit frontier arrays and implementing parallel scan primitives.
The following experiments were conducted on:
AMD EPYC 7742 64-Core Processor (2250 MHz, 128 Threads, 2 NUMA Nodes)
Ubuntu 22.04.5 LTS (x86_64)
GCC 11.4.0
OpenMP 201511
| Graph | Nodes | Edges | Average Degree |
|---|---|---|---|
| data/web-Google.mtx | 875713 | 5105039 | 5.83 |
| data/web-Stanford.mtx | 281903 | 2312497 | 8.20 |
| data/web-ShanghaiTech.mtx | 108676 | 4879773 | 44.90 |
| data/roadNet-CA.mtx | 1965206 | 2766607 | 1.41 |
| data/soc-LiveJournal1.mtx | 4847571 | 68993773 | 14.23 |
| data/com-orkut.ungraph.mtx | 3072441 | 117185083 | 38.14 |
web-ShanghaiTech is a dataset I crawled myself, starting from https://sist.shanghaitech.edu.cn. Code can be found in
spider.py
| Graph | Serial Time | 1 Thread Time | Threads Number | Parallel Time | Speedup | Scalability |
|---|---|---|---|---|---|---|
| data/web-Google.mtx | 1.35 | 1.30 | 50 | 0.20 | 6.79 | 6.57 |
| data/web-Stanford.mtx | 0.39 | 0.39 | 57 | 0.04 | 9.11 | 9.22 |
| data/web-ShanghaiTech.mtx | 0.45 | 0.32 | 8 | 0.09 | 4.84 | 3.48 |
| data/roadNet-CA.mtx | 1.48 | 1.29 | 57 | 0.20 | 7.31 | 6.38 |
| data/soc-LiveJournal1.mtx | 18.68 | 16.64 | 64 | 0.94 | 19.85 | 17.68 |
| data/com-orkut.ungraph.mtx | 9.16 | 8.73 | 57 | 0.47 | 19.45 | 18.56 |
| Graph | Serial Time | 1 Thread Time | Threads Number | Parallel Time | Speedup | Scalability |
|---|---|---|---|---|---|---|
| data/web-Google.mtx | 1.36 | 1.25 | 43 | 0.15 | 8.84 | 8.14 |
| data/web-Stanford.mtx | 0.39 | 0.38 | 57 | 0.03 | 12.01 | 11.84 |
| data/web-ShanghaiTech.mtx | 0.45 | 0.32 | 64 | 0.02 | 24.93 | 18.03 |
| data/roadNet-CA.mtx | 1.46 | 1.30 | 57 | 0.04 | 36.70 | 32.69 |
| data/soc-LiveJournal1.mtx | 18.85 | 16.49 | 29 | 1.08 | 17.40 | 15.23 |
| data/com-orkut.ungraph.mtx | 9.08 | 8.69 | 15 | 1.02 | 8.87 | 8.48 |
| Graph | Serial Time | 1 Thread Time | Threads Number | Parallel Time | Speedup | Scalability |
|---|---|---|---|---|---|---|
| data/web-Google.mtx | 0.31 | 0.30 | 50 | 0.03 | 8.97 | 8.79 |
| data/web-Stanford.mtx | 0.10 | 0.10 | 43 | 0.01 | 8.53 | 8.62 |
| data/web-ShanghaiTech.mtx | 0.19 | 0.19 | 64 | 0.07 | 2.65 | 2.67 |
| data/roadNet-CA.mtx | 0.25 | 0.25 | 57 | 0.01 | 21.64 | 22.31 |
| data/soc-LiveJournal1.mtx | 1.72 | 1.70 | 64 | 0.15 | 11.84 | 11.73 |
| data/com-orkut.ungraph.mtx | 2.30 | 2.38 | 57 | 0.16 | 14.67 | 15.13 |
Scalability:
Graph Degrees:
The distribution of web-Stanford is relatively uniform, whereas the distribution of web-ShanghaiTech is more skewed.
The performance data reveals a distinct trade-off between scheduling overhead and memory access latency.
For roadNet-CA and web-ShanghaiTech, Strategy 2 demonstrates good performance.
roadNet-CA has a very low average degree (1.41). The computational load per node is so small that the runtime overhead of
the dynamic scheduler in Strategy 1 becomes the bottleneck.
For web-ShanghaiTech: This confirms that static job
distribution effectively neutralizes the load imbalance caused by hub nodes in Power-Law graphs (Since almost all
pages would connect to the homepage), which node-based dynamic scheduling failed to handle efficiently.
However, a significant performance degradation is observed for Strategy 2 on large-scale social networks like com-orkut and soc-LiveJournal1 (Check the figures below).
Strategy 2 relies on static partitioning. Threads are assigned fixed ranges of the graph.
Some threads inevitably process data located on a different NUMA node (or CPU die).
Accessing the remote memory from different cores requires traversing the CPU interconnect bus, which incurs high
latency.
Therefore, the entire system must wait for the slowest threads (those suffering from remote memory latency) to finish. Strategy 1 (Dynamic), conversely, masks this latency. If one thread stalls waiting for memory, other threads continue fetching chunks from the dynamic queue, keeping the execution units busy and saturating the memory bandwidth more evenly.
Strategy 3 consistently yields the lowest execution times by algorithmically reducing the workload. It is
particularly effective on roadNet-CA, where the large diameter of the graph means many nodes stabilize quickly,
allowing the "frontier" logic to skip the vast majority of the graph in later iterations.
Overall, Strategy 2 proves to be highly suitable for Web Graph PageRank computations. Real-world web graphs (e.g.,
web-ShanghaiTech, web-Stanford) inherently exhibit a Power-Law distribution with significant degree skew, where a
few
nodes possess millions of links. Strategy 2's mechanism is practically ideal for this topology, as it effectively
neutralizes the workload imbalance caused by "super-nodes" without the runtime cost of dynamic scheduling.
The parallelization of PageRank is a well-studied field, with various frameworks addressing the challenges of irregular graph processing.
Ligra: A Lightweight Graph Processing Framework for Shared Memory introduced an adaptive strategy for graph traversal. It dynamically switches between a sparse method (processing only active nodes) and a dense method (iterating over all destinations) based on the size of the active frontier. This hybrid approach allows the system to optimize memory bandwidth and computation for both sparse and dense phases of the algorithm.
GPU Acceleration: Implementations on GPUs (e.g., using CUDA) exploit massive parallelism. However, they face performance challenges such as "warp divergence" and uncoalesced memory access due to the irregular structure of graph data.
MapReduce: Google’s original implementation utilized the MapReduce framework to handle web-scale graphs that exceed the memory capacity of a single machine. This distributed approach focuses on fault tolerance and scalability across clusters, though it introduces significant network communication overhead compared to shared-memory solutions.
This project demonstrated that efficient parallelization of PageRank requires analyzing the trade-offs between load balancing, scheduling overhead, and algorithmic efficiency. We showed that Dynamic Scheduling adapts to skewed degree distributions, while Manual Edge-Balancing tries to minimize overhead for exact results, and Approximation significantly improves speed by skipping redundant computations. Therefore, achieving optimal performance on shared-memory systems requires selecting the specific strategy based on the graph scenarios, balancing strict precision against computational speed.









