Graph algorithms have poor memory locality, small work per vertex (so high IO vs compute cost) and changing parallelism during execution
- Graph based data is more common now than in the past (social media, web pages)
- Distribution over many machines increases the chance of failure.
- No general purpose, scalable system exists for implementing arbitrary graph algorithms over arbitrary graph representations.
/// if the vertex is active
fn (S: Superstep, V: vertex, M: message) -> bool {
M.get(V); // get messages from S - 1
let other: VertexID = V.some_conected_v();
M.send(other); // send message to another V at S + 1 (can be any node with known identifier)
// determine if active for the next superstep
active
}- Pregel programs are inherently deadlock & data race free, all communication is happening between supersteps
- Edges have no associated computation.
- Vertices are active at the start, deactivate themselves until messaged by some other vertex.
- When all vertices vote to halt (no longer active) the algorithm terminates.
Messages are delivered (no duplication, but also no order guarentee).
- No need for remote access for the algorithms to be used, message passing is sufficient
- Latency of reads cannot easily be hidden, but message passing latency can be hidden/amortized by asynchronously delivering batches of messages. The vertices and edges are stored on the machine doing the vertex's computation, machines only need to communicate messages.
Some other optimisations include:
-
Combiners, user defined functions for combining messages sent to a given$(S, V)$ to reduce network traffic.
Each vertex in each step can provide a value to an aggregator, which are combined with a reduction operator.
- some are predefined (i.e.
min,max,sum)
When there are conflicts, an arbitrary decision is chosen, unless the user specifies how to remove conflicts.
| [[MapReduce]] | [[Pregel]] |
|---|---|
| Entire State is transfered between machines | Vertecies and edges stay on the machines computing them, only transfer messages over network. |
template class Vertex {
public:
virtual void Compute(MessageIterator* msgs) = 0;
const string& vertex_id() const;
int64 superstep() const;
const VertexValue& GetValue();
VertexValue* MutableValue();
OutEdgeIterator GetOutEdgeIterator();
void SendMessageTo(const string& dest_vertex, const MessageValue& message);
void VoteToHalt();
};- Inspired by [[Valiant’s Bulk Synchronous Parallel Model]]
The user's program is compiled and downloaded to all the machines
- One of the copies is a master responsible for coordinating all the activity, the cluster's management system/name service is used to discover locations.
- Master determines the number of partitions, and assigns partitions to machines. Partitions determined by has (can be user provided)
- The data is loaded by workers (not related to their partitions), if they load data from a partition that is not theirs, they send it to the responsible worker.
- All vertices are marked as active, the master can now instruct workers to do supersteps (they respond with the number of active verticies at the end of each superstep)
- Messages are asynchronous, all delivered by end of the superstep.
- Once no vertices are active, the computation ends.
Checkpointing is used at the start of a superstep (for vertices, edges, any extra state & messages)
- Workers are pinged frequently to check if alive.
- On failure, reset back to checkpoints - can require lots of repetition.
- Confined Recovery where outgoing messages are logged, to allow only the failed node to require catch-up (it restarts from checkpoint with the messages from the healthy partitions). This requires determinism