Take a Break
5:00
Inhale…
Give your mind a break — no phone, no music, just idle time or a quick walk.
Parallelism Models, MapReduce, and Iterative Computation
3.1 Top-Down Design
3.1.1 The Divide-and-Conquer Idea
Hook: How do you turn one job too big for one machine into many jobs that finish fast? Take the job apart, solve the pieces, and glue the answers back together — that simple loop, taken to a cluster of machines, is the engine behind nearly every big-data system, including MapReduce later in this lecture.
Top-down design is the practice of taking a larger problem, dividing it into smaller problems, solving those sub-problems, and then combining the results of the sub-problems to arrive at the solution of the larger problem. The technique should feel familiar: the divide-and-conquer strategy taught in a data structures course works the same way — divide the problem, solve the sub-problems, combine the results. What changes as we move toward distributed programming is not the idea but the constraints under which the division happens: how many processors exist, how the work is shared among them, and how the final combination is done.
Think of cooking a dinner with friends. The whole job — a three-course meal — is too much for one cook. So you split the menu into dishes, one person takes the soup, another the main course, a third the dessert, and in the end everyone brings their dish to the same table. The dinner (the larger problem) is solved by combining the dishes (the sub-problem solutions). The mapping to parallel programming is direct: the meal is the data or the task, the cooks are the processors, and "the table" is the combine step. The analogy breaks in one important place: in the kitchen the dishes are usually independent of each other, while in parallel programming the sub-problems often need one another's results — and that dependency is exactly what decides whether a division is even possible (we come back to this in the quick sort discussion in Section 3.5).
In a sequential context — a single processor — the division is easy to execute because the sub-problems run one by one. How you divide the problem is left entirely to the programmer. The classic picture comes from C programming, which most of us have touched at some point: the execution starts in a main function, and the program calls a separate function for each functionality whenever it needs that functionality. The call sequence is strictly linear — say F1 is called first, then F3, then F4; inside F1 there may be a call to F2, and F2 may call F5. At any point in time exactly one function is executing, and at the end the solution of the problem simply emerges from that single chain of execution.
3.1.2 Top-Down Design in a Parallel Context
In a parallel context the division is no longer left to the programmer's taste; it is dictated by the number of processors. The objectives change too. In a sequential setting with one processor, a typical goal is modularity — writing one function per functionality so the code stays organized — plus keeping the sub-problems manageable: you do not want so many sub-problems that you lose track of the computation progress of each one. In the parallel context the main objective is different: processor utilization — how busy the processors are — and a faster overall runtime. If there are \(n\) processors, we can potentially have \(n\) sub-problems, and that gives efficient use of the resources.
The two failure modes sit on either side of that sweet spot:
- Fewer sub-problems than processors: with \(n\) processors and fewer than \(n\) sub-problems, every processor that gets a sub-problem takes one, but some processors stay idle. The resources are under-utilized.
- More sub-problems than processors: equal to \(n\) sub-problems can run at once, one per processor, and the rest wait for processor time. That is acceptable — the processors are fully used — but it costs scheduling and waiting time.
There is a second set of questions the programmer must answer when dividing a problem for parallel execution. First, how many processors do we have, and can we assign each sub-problem to its own processor to get an efficient runtime? Second, is the problem itself divisible — can the solutions of the sub-problems actually be combined at the end? Combining is the real constraint: each sub-problem now runs on a different processor, so we must decide how to combine. Is the combination trivial, just collecting the outputs from each processor and appending them into the final result? Or does it need extra logic? And if a small combination logic is needed, can that logic itself run in parallel? The combination should stay straightforward — not very, very complex — because a heavy combine step eats the time the parallelism saved.
Worked example — how many processors do you need? Suppose the dependency plan for a problem is a little graph:
F1runs first. It finishes alone.- Two copies of
F2run at the same time — call themF2aandF2b. The two are independent, so they can run in parallel. - Their results combine into
F4, which waits until both copies have finished. - Three copies of
F5run at the same time, each using part ofF4's output.
To run this graph, the minimum number of processors you need is three, because at the widest moment — step 4 — three sub-problems are running simultaneously. The rule of thumb: count the maximum number of sub-problems that are alive at any point of time — that is the minimum number of processors needed to exploit the parallelism fully.
Sense-check: with two processors, step 4 would run F5a and F5b together and queue F5c; the queue wastes time. With three processors, every F5 starts at once, and no processor sits idle at the widest moment. The number "three" is a property of the widest moment of the graph, not of the total number of sub-problems.
The same reasoning applies level by level in a decomposition tree: a problem \(P\) divides into \(P_1\) and \(P_2\); at the next level \(P_1\) divides further, and so does \(P_2\). Whenever the sub-problems at a level can run in parallel and there is no interdependency among them, the parallelism is clean and the execution time stays low. Interdependency is the enemy: if a sub-problem's result is needed before a sibling can start, the sibling just waits.
Pitfalls — dividing without thinking:
- Dividing by data alone without checking combinability. Splitting a list into four pieces is easy; the hard part is whether the four partial answers can be merged into one answer. If the combine step is expensive or sequential, the speed-up can vanish.
- Forgetting that "equal parts" is relative to processor count. Dividing a 1000-element list into 4 parts when you have 10 processors leaves 6 processors idle. The number of sub-problems should track the number of processors, not some favorite constant.
- Letting a heavy combine step eat the savings. A combine step that costs as much as the whole parallel phase turns the "parallel" run into a sequential one with overhead on top. Keep the combine cheap, or rethink the division.
3.1.3 Student Questions and Answers
Q: I suppose we need to divide the array into four arrays to execute in parallel?
A: It depends on the number of processors you have. Typically it depends on the number of processes: if there are four processes, it is good to divide the data into four equal parts and execute the search on those four parts in parallel — that is how you achieve the speed-up. That understanding is correct. The number of parts follows the number of processors, not the other way around: with \(p\) processes you divide into \(p\) equal parts so that every process has work and none sits idle.
Q: If a processor finds the key position, does that mean the position inside the sub-problem it was given?
A: Correct, that is also fine — the position refers to the position within the sub-problem that the processor was handed, and the processor number identifies which processor found it. So the answer to the search is a pair: (processor number, position inside that processor's chunk). One number alone would be ambiguous — position 3 in processor 0's chunk and position 3 in processor 7's chunk are different locations.
Q: Can this be further enhanced using multi-threading?
A: Yes — that is exactly the kind of thing we are doing here, running the searches in parallel. Threads are one way to create that parallelism on a single machine's cores; processes across a cluster are another. The divide-and-conquer structure stays the same; only the unit that executes each sub-problem changes.
3.2 Running Time Complexity and the Parallel Keyword Search
3.2.1 Asymptotic Complexity Recap
Hook: A list of a million numbers — how long does a search take? The answer is not a number of seconds but a curve: how the work grows as the list grows. That curve, big-O notation, is the language in which every claim of this lecture's speed-up is written.
The running time complexity of an algorithm is a function we associate with the algorithm with respect to its input size — how many numbers are in the list. The standard notation is big-O, the asymptotic notation. Two classic search algorithms from the data structures course set the pattern:
- Linear search compares the target with the first element, then the second, then the third, and so on until the element is found, or until the list ends. In the worst case you make \(n\) comparisons, so the running time complexity is \(O(n)\) — it varies linearly with the input size. Double the list, double the worst-case work.
- Binary search requires a sorted list. You compare the target with the middle element directly. If it matches, you are done. If the target is less than the middle element, you search in the left half; if it is greater, you search in the right half. Then you compute the middle of that half and repeat. Every step halves the search space: initially \(n\), then \(n/2\), then \(n/4\), then \(n/8\), and so on. The complexity function is \(\log_2 n\), written \(O(\log_2 n)\). The one hard requirement: the list must be sorted.
The two complexity functions, side by side. For a sorted list \(LS\) of size \(N\) searched for a key \(K\):
\[ \text{linear search: } O(N) \qquad \text{binary search: } O(\log_2 N) \]
Every symbol is named: \(N\) is the number of elements in the list (the input size), and \(\log_2 N\) is the base-2 logarithm — the number of times you can halve \(N\) before reaching 1. Think of the paper-folding picture: if you fold a sheet in half, the number of layers doubles; reversing that, starting from \(N\) layers and halving to one layer takes about \(\log_2 N\) folds. For \(N = 1000\), \(\log_2 1000 \approx 9.97\), so at most ten halvings. The logarithm says "how many times does the problem shrink by half?" — and because each comparison throws away half the remaining candidates, binary search grows slowly with \(N\). That is the whole advantage: linear search's work grows in step with the data; binary search's work grows like the number of digits of the data.
So in the sequential context, searching a sorted list \(LS\) of size \(N\) for a key \(K\) costs \(O(N)\) with linear search and \(O(\log_2 N)\) with binary search.
3.2.2 Worked Example: Searching a Nine-Element List
Worked example — nine elements, key 10. Take the list \(1, 2, 3, 4, 5, 6, 7, 8, 9\) and search for the element 10 — which is not in the list.
Linear search: compare 10 with 1, then 2, then 3, then 4, then 5, then 6, then 7, then 8, then 9, and finally conclude that 10 is not present. That is nine comparisons. Why nine? Because the list size is nine — if the list had a hundred elements you would make a hundred comparisons, if it had a thousand you would make a thousand. The number of comparisons is a linear function of \(n\), which is why we call it \(O(n)\).
Binary search:
- Compare 10 with the middle element, 5. It does not match, and 10 is greater than 5, so search in the right half \(\{6, 7, 8, 9\}\).
- The middle of that half is 7 (or 8; we take 7). 10 is greater than 7, so search in \(\{8, 9\}\).
- Take the middle again and compare; 10 is greater than 8, so only \(\{9\}\) remains.
- On the fourth comparison — 9 compared with 10 — we conclude the number is not present.
So nine numbers took four comparisons: \(\log_2 9 \approx 4.1\), and taking the floor or the ceiling gives at most four or five comparisons. That is the sense in which the complexity function of binary search is \(O(\log_2 n)\): the maximum number of comparisons grows like the logarithm of the input size.
Sense-check: the two answers fit the two curves. Linear search took exactly \(N = 9\) comparisons. Binary search took \(\lceil \log_2 9 \rceil = 4\) — one more than the whole of 9 halves down to about 1, because we compare each new middle before halving again. A search for 10, which is not present, always pays the worst case: the list is exhausted before the key can be ruled out.
If the asymptotic notation is new or fuzzy, it is worth reading a page or two on it — not a deep dive, just enough to be comfortable with the idea.
3.2.3 Parallel Binary Search over \(P\) Processors
Now suppose the sorted list \(LS\) of size \(N\) is distributed over a cluster of \(P\) processors. Divide the data equally: each processor gets \(N/P\) elements. If the list has a thousand numbers and there are ten processors, each processor gets \(N/P = 100\) numbers.
Each processor now runs a binary search on its own chunk in parallel. The search space per processor is no longer \(N\) but \(N/P\), so the running time complexity becomes:
\[ O\!\left(\log \frac{N}{P}\right) \]
Since all the processors search at the same time, the wall-clock time is that of a single processor doing \(\log(N/P)\) work — the parallelism adds no extra time. The ideal speed-up over the sequential version is \(p\) times:
\[ \text{speed-up} = \frac{\log_2 N}{\log_2 (N/P)} \approx P \]
This is theoretical — there is some time spent collecting and combining the results from the different processors, which we are not counting here — but as a first model, the speed-up is \(P\). (A note on the speed-up rule: the speed-up is always \(p\) times for \(p\) processors; when the example happens to use the three-processor dependency diagram from Section 3.1, the same rule gives three times. The "three" is the number of processors in that picture, not a different formula.)
How does the search report its answer? Each processor runs a binary search on its own chunk, and wherever a processor finds the key \(K\), it returns two values: \(I\), the processor number, and \(J\), the index at which the key is present on that processor. The processors that do not find the key return nothing useful. One processor, say \(P_0\), is designated to collect the results, so the one or more \((I, J)\) positions get gathered at \(P_0\).
Why both numbers? A single index is ambiguous across processors — position 3 on processor 1 and position 3 on processor 4 are different memory locations. The pair \((I, J)\) says where in the distributed list the key lives: "processor \(I\), position \(J\) inside that processor's chunk." The collecting step is part of the combine cost we excluded from the speed-up formula; in practice it is a cheap gather of a handful of \((I, J)\) pairs. This collect step is the conquer picture of divide-and-conquer — the combine diagram where eight results combine into four, four into two, two into one, all in parallel, until one final result emerges.
With four processors the chunk size is \(N/4\), and the search completes in \(\log(N/4)\) steps — each processor halves its own quarter, not the whole list.
Assumptions and scope — when the parallel binary search works:
- The list must be sorted. This is inherited from binary search; if the chunks are not sorted, binary search gives wrong answers.
- The chunks must be balanced. The formula \(O(\log(N/P))\) assumes each processor holds about \(N/P\) elements. If one processor holds \(N/2\) and the others share the rest, that processor's \(\log(N/2)\) dominates.
- The speed-up is ideal. Real runs pay for splitting the data, collecting the \((I, J)\) answers, and scheduling. The formula counts only the search phase.
Pitfalls:
- Forgetting the sorted requirement and running binary search on an unsorted chunk — the professor flagged this as the one hard requirement.
- Quoting the speed-up as exactly \(P\) — it is a theoretical upper bound; the combine step and the start-up overhead are real.
- Confusing \(\log N\) with \(N\): with a thousand numbers on ten processors, each processor does \(\log 100 \approx 6.6\) steps, not \(100\) steps — the logarithmic form is what keeps the parallel version fast.
3.2.4 Worked Example: A Thousand Numbers on Ten Processors
Worked example — \(N = 1000\) numbers, \(P = 10\) processors. Concretely:
- Each processor holds \(N/P = 100\) numbers.
- Every processor runs a binary search on its 100 elements in parallel.
- The complexity formula changes from \(\log N\) to \(\log(N/P) = \log 100\). Each processor solves its part in that time: \(\log_2 100 \approx 6.6\), so about seven comparisons per processor, versus ten for the whole thousand-element list.
- The speed-up is \(p = 10\) times ideally.
There is one extra step to select and collect the output from each of the ten processors; that collection time exists but is not part of the execution-time comparison that gives the speed-up of \(p\).
Sense-check: one processor searching 1000 elements needs at most \(\lceil \log_2 1000 \rceil = 10\) comparisons. Ten processors each searching 100 elements need at most \(\lceil \log_2 100 \rceil = 7\) comparisons each — and they run at the same time. The wall-clock time drops from 10 comparison-steps to about 7, while the total work across the cluster is \(10 \times 7 = 70\) comparison-steps. The speed-up in wall-clock time is roughly \(10/7 \approx 1.4\) for this tiny input — the ideal factor of 10 only shows up when \(N\) is so large that the log ratio dominates. This is how we get parallelism for searching.
3.2.5 Student Questions and Answers
Q: Can you explain the search example again, with a small example?
A: Take nine elements, \(1\) through \(9\), and search for 10. With linear search you compare 10 with the first element, second, third, fourth, fifth, sixth, up to nine, and then you say the element is not present — nine comparisons, because the list size is nine. That is why the function is linear, order \(n\). With binary search you compare 10 with the middle element 5; it does not match and 10 is greater, so you search the right half; you find the middle of that half, say 7; 10 is still greater, so you search the next half; and within the fourth comparison you conclude the number is not present. Nine numbers, four comparisons — that is \(\log_2 9 \approx 4.1\), so the maximum number of comparisons is four or five, the floor or the ceiling of the log. That is how we associate \(O(\log_2 n)\) with binary search. If the asymptotic notation is unclear, go through a page or two on asymptotic notations — do not deep dive, just the basics.
Recap and bridge: Sequential search costs \(O(N)\) (linear) or \(O(\log_2 N)\) (binary, sorted list only). Split the list over \(P\) processors, and the per-processor cost shrinks to \(O(N/P)\) for linear search and \(O(\log(N/P))\) for binary search, with an ideal speed-up of \(P\). Next we test this recipe on two more real search problems — fingerprints and documents — and meet the design rule that decides when the partition itself may be redone.
Exam note: Know the four forms cold: \(O(n)\) linear search, \(O(\log_2 n)\) binary search, \(O(\log(N/P))\) parallel binary search, and the theoretical speed-up of \(p\).
3.3 More Parallel Search Examples
3.3.1 Fingerprint Matching
Hook: You cannot binary search a fingerprint — fingerprints have no order, so no middle element. But you can still search a million of them fast, by dividing the set itself. This is the second search pattern: parallel linear search over partitions of the data.
Fingerprint matching is a search problem with a different flavor. We have a data set \(D\) of fingerprints — say \(n\) fingerprints total — and we want to search for a particular fingerprint \(f\) in that set. To parallelize, we partition \(D\) based on the number of processors, and the partitions are evenly stored in a distributed database: the first processor stores one chunk, the second processor stores another, and so on.
The match itself is a linear search — comparing fingerprint images is not a sorted-list operation, so there is no binary search here. With \(n\) fingerprints and \(p\) processors, each processor gets \(n/p\) fingerprints, and the complexity drops from \(O(n)\) to:
\[ O\!\left(\frac{n}{p}\right) \]
All processors run their linear search in parallel, so the result is again a speed-up of \(p\) equal to the number of processes in hand.
The assumptions matter — application requirements decide the design. The key assumption is that partitioning is an infrequent activity. Fingerprint data keeps growing — new fingerprints keep being added — but we do not repartition for every new arrival, because repartitioning would require redistributing the data every time. Searching, by contrast, is the frequent activity: searches happen constantly, on the partition layout that already exists. So the designer looks at how the application behaves and decides that repartitioning is rare and searching is common.
This is the design pattern of the whole lecture: the frequency of an operation decides whether it must be cheap or whether it is allowed to be expensive. Partitioning is expensive (it moves data around the cluster), so the application must arrange for it to be rare. Searching is cheap per element, so it can happen all the time — on whatever layout already exists.
3.3.2 Worked Example: When Do We Repartition?
Worked example — a thousand fingerprints, then a trickle, then a flood. Start with a thousand fingerprints divided among ten processors, 100 each.
- Two more fingerprints are added. Do we divide by ten again? No — you do not repartition for one or two additions. Redistributing 1002 fingerprints across ten processors to shave two items off some chunks is wasted network traffic; the imbalance is negligible.
- Another 100 fingerprints are added. The data set has grown to 1,100, and now it is worth reconsidering the partition — at that point you repartition so that each processor gets its fair share again: \(1100/10 = 110\) each.
The rule of thumb: when many new entries are made into the data, you go for a partition; for a few entries, you leave the layout alone.
Sense-check: the decision is a trade-off between two costs. Repartitioning costs the time to redistribute data across the cluster. Not repartitioning costs imbalance — one processor holding slightly more than its fair share. A couple of additions move that balance by a fraction of a percent, so the repartition cost wins. A hundred additions move the balance by ten percent of a chunk, and the imbalance is now big enough to justify the shuffle. Partitioning is your infrequent activity; searching is your frequent activity. As long as that trade-off holds, you keep the speed-up of \(p\) equal to the number of processes you have in hand.
3.3.3 Document Search in a Distributed Collection
The third example is search again, presented differently. We have a distributed document collection \(D\), a set of documents \(d_1, d_2, \ldots, d_n\), and a set of keywords to search in each document. We want to find which documents contain those keywords.
The parallel recipe is the same: divide the documents among the number of processors you have, then run the search in parallel instead of scanning the documents one by one. What each search returns — the location where the keyword is present, or simply a flag that the document matches — is an application requirement: the search logic returns whatever the application needs. The results are collected, and the answer is: these are the documents that contain these keywords.
Scope and pitfalls of the partition-once, search-often pattern:
- The pattern needs a workload where searches dominate. If your application ingests documents continuously and users rarely search, the balance flips: repartitioning becomes the frequent activity and the design is wrong. Reconsider the partition policy, or the storage system.
- "Equal partitions" assumes homogeneous processors. On a cluster with mixed hardware, equal-sized chunks make the slow nodes the bottleneck (Section 3.4). The partition logic then has to give low-end nodes fewer documents.
- Return shape is an application decision. One application wants the matching documents only; another wants the matched positions for highlighting. The search routine must return what the application needs — there is no universal answer, and designing the return value is part of the problem statement, not an afterthought.
Real-world: This is the kind of workload MongoDB was built for. MongoDB is a document-oriented database — we saw it in the previous discussion. If you host MongoDB on a cluster of nodes and divide your documents across the cluster, then a query — say a search for keywords inside documents — can execute in parallel on the different nodes of the cluster, and it runs much faster than the same query on a single node. The same pattern appears in biometric identity systems (fingerprint matching over a partitioned, distributed fingerprint database — Section 3.3.1) and in full-text search across a document warehouse. In every case the economics are identical: build the partition once, search it many times, and let the parallelism divide the search — not the search logic divide the data set per query.
3.4 The Data Parallel Execution Model
3.4.1 Same Task on Different Data (SIMD)
Hook: Every search example so far shared one secret: the task never changed — only the slice of data did. That one idea has a name and a taxonomy slot: data parallelism, and in Flynn's taxonomy it is SIMD.
Everything we did in the three search examples was an instance of the data parallel execution model. The defining move: the data is partitioned across multiple nodes — not the tasks. The task stays identical on every node, and each node works on its own slice. In the keyword search, every processor ran the same search routine on its hundred numbers. In fingerprint matching, every node performed the same match task, but the data set within was different: processor one had \(d_1\), processor two had \(d_2\), processor three had \(d_3\), and so on up to \(d_p\). One task \(T\) runs against data \(d_1, d_2, \ldots, d_p\).
This maps directly onto one of the four architecture classes from Flynn's taxonomy: SIMD — single instruction, multiple data. There is a single instruction — searching, for instance — that runs across the processors, but the data is multiple: each processor has a different set of data.
Flynn's four classes, with data parallelism in place. Flynn classified parallel machines by two questions: how many instruction streams, and how many data streams?
| Class | Name | Instruction streams | Data streams | Everyday example |
|---|---|---|---|---|
| SISD | Single instruction, single data | 1 | 1 | A normal single-core CPU running one program |
| SIMD | Single instruction, multiple data | 1 | Many | Data parallel model: one search task, \(p\) data chunks |
| MISD | Multiple instruction, single data | Many | 1 | Task parallel model (Section 3.6): different tasks, same data |
| MIMD | Multiple instruction, multiple data | Many | Many | A general-purpose cluster: different programs on different data |
Data parallelism occupies the SIMD slot: the instruction (the task) is one, the data streams are many. Modern GPUs are the extreme SIMD machine — thousands of cores all running the same kernel on different pixels or matrix entries. In our course, the "cores" are the processors of a distributed cluster, and the "kernel" is the map task we meet in Section 3.9.
3.4.2 Homogeneous and Heterogeneous Nodes
The goal of the partition logic is to make the partitions equal or balanced, because balanced partitions give the fastest results. Whether balance is easy depends on the hardware:
- Homogeneous nodes — nodes with the same level of hardware configuration. Here we try to balance the partitions, and each node performs an equal amount of work. With balanced partitions on homogeneous nodes, the processors stay 100 percent busy and the execution time is minimal.
- Heterogeneous nodes — some nodes have high-end processors while others are low-end. The partition logic becomes complex, because we want to balance the load: the nodes with low-end processors should get less work, and the nodes with high-end processors should get more.
Two failure modes follow. If the nodes are homogeneous but the partitioning logic is unable to balance the partitions — say the data sizes end up unequal — the execution time rises. If the partitions are balanced but the nodes are heterogeneous, the slowest node — the one with the low-end configuration, weaker memory, and so on — becomes the bottleneck: we cannot present the result until all the nodes have finished, because the final result is collected from every processor. So balance is needed on both sides: equal work if the nodes are equal, proportionate work if they are not.
3.4.3 Balancing Partitions and the Slowest-Node Bottleneck
The slowest-node bottleneck — the hidden cost of "one big result". The result is presented to the end user only once it is collected from all the processors. If some node is slow, we wait — we cannot finalize the result until every node has completed its task.
Picture the timeline of one run. Draw the \(p\) processors down the side, and time along the horizontal axis. Each processor draws a bar for its work; the bars start at the same moment (the parallel phase begins together). One bar is much longer than the others — that is the heavy partition, or the slow node. The final result is marked at the end of the longest bar: the fast nodes finish early and sit idle, and the answer arrives only when the longest bar ends. The other bars' early finish times are wasted capacity.
The two ways the bars get unbalanced:
- Homogeneous nodes, unequal partitions. One chunk holds far more data than the others. Fix by balancing the partition sizes.
- Heterogeneous nodes, equal partitions. Every chunk has the same size, but one node is slower. Fix by partitioning by capability: the low-end node gets a smaller share.
One-sentence takeaway: in data parallelism, the whole run is only as fast as its slowest participant — so the partitioning logic has to be taken care of: if the nodes are homogeneous, balance the work; if they are heterogeneous, the partition logic must account for each node's capability.
3.4.4 When Data Parallelism Is Not Possible
There are two scenarios where data parallelism cannot be used effectively:
- You cannot divide the work equally. If the data cannot be split into roughly equal chunks, the data parallel model has nothing to balance.
- You cannot divide the work independently. If the sub-problems are not independent — if one sub-problem needs the result of another before it can proceed — they cannot run in parallel, and data parallelism is out of the question.
Quick sort is the classic example that trips both conditions, which is why it gets its own treatment.
Recap and bridge: Data parallelism keeps one task and splits the data; balanced partitions on matching hardware keep every processor busy, and the slowest node or the most unbalanced partition sets the finish line. When the data refuses to split equally or independently — as quick sort's pivot forces — data parallelism steps aside and a second model, tree parallelism, takes over. That is next.
3.5 Quick Sort: When Data Parallelism Fails, Tree Parallelism Steps In
3.5.1 How Quick Sort Partitions
Hook: Sorting is the most famous divide-and-conquer problem — and quick sort, the most famous sorting algorithm, is the one that refuses to be divided evenly. Understanding why it refuses unlocks the third parallelism model of the lecture.
Quick sort is a comparison-based sorting technique from the data structures course. You are given an unsorted list; quick sort returns a sorted list. At each step it chooses one pivot element — say the last element of the current segment — and it tries to fix the position of that pivot within the list. When the pivot's position is fixed, it is fixed in a specific way: all elements to the left of the pivot are less than the pivot, and all elements to the right are greater than the pivot.
Notice what that buys and what it does not. The pivot's own position is now final — it will never move again. But the left part is not necessarily sorted yet, and neither is the right part; we have only fixed one element. The algorithm then recurses: choose a pivot in each part, fix it, and continue until each part is too small to divide.
3.5.2 Why the Pivot Breaks Balanced Partitioning
The problem for parallelism is that the partition size depends on the pivot. Suppose the pivot you choose is the second smallest element of the list: its fixed position is near the front, so on the left of it you have just one element, and on the right you have \(n-2\) elements. The list has not been cut into anything close to equal halves — it has been cut into \(1\) and \(n-2\).
That violates the requirement of the data parallel execution model, which wants a balanced partition at each step: you should be able to divide the data in a balanced way. If the pivot divides the list so that one side has many more elements than the other, the partition is unequal, and you cannot achieve the efficiency data parallelism promises. A 45/55 split is still a good partition — 55 percent of the elements on one side is fine. A 10/90 split is bad, and a 1/99 split is worse. In the pathological worst case, if every pivot lands so that one element sits on one side and \(n-1\) on the other, the running time is as bad as the sequential version — the same one-and-\(n-1\) pattern repeats at every level. The probability of that happening is very low, but it can happen in the worst case, so at maximum the time taken may be as bad as sequential with bad partitioning.
There is a second, subtler reason quick sort is not a one-shot data parallel problem: the division is stepwise, not a single level. In the search examples we took a thousand elements, divided into ten chunks of a hundred once, and that division was done. In quick sort, we first find the position of the pivot in the full list and split it into two parts; then each part is handed to a processor, which picks its own pivot and splits again; and so on. The partition at every level depends on the partition at the parent level — it is not a single-level problem, and the choice of pivot position \(m\) cannot guarantee equal partitions. One set of processors can end up with a large partition while another set gets a small one.
Pitfalls — the pivot traps:
- Judging a pivot choice by the current step alone. A single unbalanced split is survivable; the worst case is the same \(1/(n-1)\) pattern repeating at every level, so the recursion degenerates into a long chain instead of a balanced tree.
- Thinking the expected case is guaranteed. The probability of a repeated worst-case pivot is very low — that is why quick sort is fast in practice — but the worst case still exists. On the exam, distinguish the expected \(O(n \log n)\) from the worst case that matches sequential.
- Assuming the first split is the whole story. Even a perfect first split does not make quick sort data-parallel: every deeper level must wait for its parent level's split, which is a dependency the data parallel model cannot tolerate.
3.5.3 Tree Parallelism: Parallel Partitioning at Each Level
So quick sort is not suited to data level parallelism — but it is suited to a third kind of parallelism, the tree parallel execution model. The insight: even though the partition sizes are unpredictable, the partitioning operations at the same level are independent of each other and can run in parallel.
The picture is a tree. Level zero: the full list is partitioned into two parts. Level one: the two parts are partitioned in parallel — each of the two processors picks a pivot and splits its own part — producing four parts. Level two: those four are partitioned in parallel, and so on. The parallelism we can achieve is "dividing the list into two at each step; this division can be done in parallel; and finally you combine the result from each of the nodes to get your final result." Each level of partitioning happens in parallel, and the result is a sorted list when you append the outputs of all the processes.
The stopping rule for the recursion comes back to the number of processors: at each step you ask whether to keep dividing. If the problem is divided into \(k\) sub-problems at a level and the processors can execute them in parallel, that level takes roughly one step, and that is the speed-up you achieve. Three cases:
- Sub-problems equal to processors: every processor gets exactly one sub-problem — every processor fully busy, best outcome.
- Fewer sub-problems than processors: some processors sit idle.
- More sub-problems than processors: all processors are busy, and the rest wait in line — fine, but they add wait time.
So the number of processors is the key factor in deciding whether to keep dividing: stop when the sub-problems match the processors you have.
Tree parallelism in one picture. Draw the recursion as a binary tree, root at the top. At the root, one partition happens: 1 task. Level 1: two partitions, run on two processors at the same time: 2 tasks. Level 2: four partitions in parallel: 4 tasks. The tree widens as it grows — the number of parallel tasks doubles each level — until it reaches the processor count, where the widening stops being useful. The horizontal axis is one time-step per level; the vertical axis is the number of partitions. Landmarks: the level where the number of tasks equals the number of processors (that level is the first fully-busy one), and the later levels where tasks queue behind busy processors (the wait time). One-sentence takeaway: tree parallelism is data parallelism's slower sibling — the data is still the same task, but the division itself is a sequence of dependent levels, and only the partitions within a level run at the same time.
3.5.4 Student Questions and Answers
Q: For quick sort, how is the three-level parallelism or task parallelism relevant?
A: Careful — in quick sort the task is the same throughout. The task remains the same: you have to divide the list, pick up a pivot, and divide the list by positioning the pivot element. That is one task, executed again and again. The three-level parallelism refers to something different: it is about dividing the list at each level. At level one one partition happens, and you can hand it to two processors; those two partitions can be partitioned in parallel and handed to two more processors, and so on at the same time. So it is "partitioning, partitioning, partitioning — at the same level the partitioning can be done in parallel." Task parallelism is different: there you run different tasks on the same data. In quick sort the data is different at each processor; it is your partitioning that gives each processor different data, and the partition depends on the parent level.
Recap and bridge: Quick sort cannot split its data evenly up front, so data parallelism is out — but the partitions at each level are independent of each other, so a tree of parallel partition steps works. The same "depend on the parent level" structure returns in Section 3.9, where the reduce step is described as inverse tree parallelism. Before that, the next model flips the picture entirely: the data stays whole, and it is the tasks that get divided.
Exam note: Expect to compare data, tree, and task level parallelism, and know why quick sort is not suited to data parallelism: the pivot cannot guarantee equal partitions, the division is stepwise and depends on the parent level, and in the worst case the time is as bad as sequential.
3.6 Task Level Parallelism
3.6.1 Different Tasks on the Same Data (MISD)
Hook: Search divides the data. Quick sort divides the data, level by level. What if you keep the data whole and divide the work instead — one core types, another spell-checks, a third counts words, all on the same document at the same time? That is task level parallelism.
Task level parallelism is the third kind. In data level parallelism the data gets divided, and every processor runs the same task. In tree level parallelism the data gets divided too, but stepwise and level by level. Task level parallelism flips the relationship: the data does not get divided. Instead, the tasks get divided into sub-tasks, and the sub-tasks may work on the same data instance — possibly on the same data copy, or on different copies that must be kept in sync.
The tasks are different: one processor computes one thing, another computes something else, and they run in parallel on the same data. This maps onto another class of Flynn's taxonomy: MISD — multiple instruction, single data stream. There are multiple instructions, and a single data stream is shared.
The contrast in one table. The three models answer different questions, and the exam asks for exactly this contrast:
| Data parallelism | Tree parallelism | Task parallelism | |
|---|---|---|---|
| What is divided? | The data, in one shot | The data, level by level | The tasks |
| What stays whole? | The task | The task | The data |
| Same task everywhere? | Yes | Yes | No — tasks differ |
| Same data everywhere? | No — each node gets its own slice | No — each node gets its own slice | Yes — the same data instance |
| Typical Flynn class | SIMD | (parallel tree of the same task) | MISD |
| Example | Keyword search over chunks | Quick sort partitioning levels | Mean, median, and mode on one list |
When to pick which: if the data divides cleanly, use data parallelism; if the data refuses to divide but the operations at one level are independent, try tree parallelism; if the data is small and shared but you want several different computations on it, task parallelism — and expect it to scale the worst (Section 3.6.4).
3.6.2 Examples: A Spell Checker and Mean-Median-Mode
Two examples make it concrete.
The word processor on a multi-core machine. You are typing something in a word processor on a multi-core machine. While you type, your spell checker runs in the background on a different core, highlighting the incorrect words wherever you missed a spelling. The typing/editing task and the spell-checking task are different tasks running on the same document — the same data — at the same time, on different cores.
Mean, median, and mode. You are given a list of integers and you want to compute three independent statistics: the mean, the median, and the mode. Here the tasks differ for each statistic — the instructions are not the same — but the data is the same: the same list. Give each processor the same data and a different task, and run them in parallel: one processor finds the mean, another finds the median, a third finds the mode. Multiple instructions, one data stream — a textbook MISD pattern — and the three results come back at the same time, giving high performance.
Worked example — three statistics, one list. Take the eight numbers \(1, 2, 3, 3, 4, 5, 7, 9\). Three processors, each holding the same copy of this list, compute three different things at the same time:
- Processor 1 — the mean. Sum the values: \(1 + 2 + 3 + 3 + 4 + 5 + 7 + 9 = 34\). Divide by the count \(8\): \(\frac{34}{8} = 4.25\). The mean is 4.25.
- Processor 2 — the median. Sort the list (it already is sorted). With 8 values, the median is the average of the middle two: positions 4 and 5 hold \(3\) and \(4\), so the median is \(\frac{3+4}{2} = 3.5\). The median is 3.5.
- Processor 3 — the mode. Count occurrences: 3 appears twice, every other value appears once. The mode is 3.
Sense-check: the three answers come from the same eight numbers but measure different things — 4.25 (the average), 3.5 (the midpoint of the sorted list), and 3 (the most repeated value). All three are valid, all three needed the full data set, and none needed another processor's result — which is exactly why they could run at the same time on the same data copy.
3.6.3 Characteristics of Task Parallel Subtasks
Three important points to note about task level parallelism:
- Subtasks are identified by functionality, with no common function. In tree and data parallelism the tasks are identical functions; in task parallelism you identify the sub-tasks by what they compute, and the sub-tasks share nothing.
- Independent sub-tasks can be executed in parallel. The parallelism only exists if the sub-tasks do not depend on one another — which is why mean, median, and mode work so cleanly.
- Sub-tasks are often limited and known statically in advance. They do not change frequently. In the word processor example we know the sub-tasks up front: editing the document, running the spell checker, statistical analysis. In the statistics example we know we want mean, median, and mode. These are known before the run starts.
3.6.4 Scalability: Data Parallelism vs Task Parallelism
Scalability — what happens when you add more resources — separates the three models sharply. Take data parallelism with a thousand numbers divided into ten sets of a hundred, run on ten processors. Now suppose the data grows to 5,000 numbers. If we increase the resources to 50 processors, each node again gets a hundred numbers to search, and the results come back in the same time as before. We added 40 nodes, and the per-node workload did not change: the system is scalable by adding more resources — economies of scale. This works because data level parallelism divides the data; the workload splits evenly no matter how big it gets.
Task level parallelism does not scale this way. If you simply add more nodes, you cannot split the tasks further — one task may differ from another, and one task may take much longer than the others. The heavy-task node becomes the bottleneck: the other nodes finish quickly, but the result cannot be presented to the end user until the heavy-task node completes its work. So task parallelism is not easily scalable by adding more resources. When it comes to data level parallelism — whether the tree form or one-step division — it makes sense to add resources and get scalability; for task parallelism, adding resources does not buy the same benefit.
Scope and pitfalls of task parallelism:
- The independent-subtask requirement is absolute. Add a dependency between the tasks — say the median needs the mean's answer — and the parallelism collapses into waiting. Task parallelism works only for tasks that share data but not results.
- Scaling by adding machines hits a wall. There are only so many different tasks the problem defines. Once every task has its own node, extra nodes are idle by construction; you cannot split "compute the median" into sub-tasks the way you split data into chunks.
- "Known statically" is a constraint, not a virtue. If the set of tasks changes during the run, the schedule must change with it — the static, known-up-front structure is what made the simple parallel schedule possible in the first place.
Real-world: The word-processor example is how a modern laptop works: the main thread renders and edits, background threads run spell-check, auto-save, and telemetry — all over the same document buffer on different cores. The statistics example is the shape of embarrassingly parallel analytics dashboards where several independent aggregates (counts, sums, percentiles) are computed over one data frame at the same time. In the distributed world, task parallelism appears wherever a small, fixed set of heterogeneous jobs runs over a shared data set — and the lesson of this section is that such systems scale by making the data bigger or the task set larger, not by adding machines to the same fixed task set.
3.7 Request Level Parallelism
3.7.1 The Client-Server Use Case
Hook: Everything so far divided one job across many machines. Here is the opposite setup: many users arrive with many independent jobs, and one server must serve them all — at the same time. This is the parallelism of the web itself.
Request level parallelism is the typical pattern in a client-server architecture. Multiple clients are requesting something from a server, and the server responds by executing tasks in parallel — some tasks happen for one request, some for another, possibly several tasks at the server end at the same time — and after parallelizing all of it, the server returns the results. The concurrent requests are typically executed as different threads.
The difference from the previous models is where the parallelism lives. Data, tree, and task parallelism are all one program, one problem, many workers. Request level parallelism is many problems, many users, one serving program: the parallelism comes from the requests themselves, not from splitting any single request. Each user's request is already a separate unit of work; the server's job is to run as many of them concurrently as it can.
Real-world: The typical examples are an email server, which serves many clients at once, any sort of API interface where multiple requests come in and must be handled at the same time, a reservation system, or an online banking application. All of these handle request level parallelism on the server side.
3.7.2 The Throughput Metric
The scalability metric for request level parallelism is how many requests per unit of time the server can serve — that is, the throughput the system can generate. That is the number you have to achieve when you configure a server to handle many concurrent clients.
Throughput, defined with numbers. Throughput is the rate at which completed requests leave the server, measured in requests per second. If a server handles 2,000 requests in one minute, its throughput is \(2000/60 \approx 33\) requests per second. The design target is set by the workload: a bank that expects 100 logins per second at peak must configure enough threads, cores, and machines so that throughput stays at or above 100 requests per second — otherwise requests queue and users wait.
Two quantities pull against each other:
- Concurrency — how many requests are being worked on at once (how many threads are active).
- Per-request latency — how long one request takes from arrival to reply.
Throughput is roughly concurrency divided by latency: 50 concurrent requests, each taking 0.5 seconds, sustain about \(50/0.5 = 100\) requests per second. Raise concurrency (more threads) or cut latency (faster handling), and throughput rises. Scale this by adding machines, and the server farm is data-parallel across requests — the recurring theme of the lecture: divide the work, and the whole is faster.
3.7.3 Student Questions and Answers
Q: What is sharding?
A: Sharding is your partitioning, basically. The term is typically used in MongoDB, and it is relevant to how you partition your data — that is, how you maintain your shards among the different nodes of the cluster. When you shard a collection, you split it into shards and spread those shards across nodes; a request can then work on the relevant shards in parallel. Sharding is the same divide-the-data idea, wearing a database-flavored name.
Q: Can you explain the difference between data level and tree level parallelism?
A: In data level parallelism you divide your data in the list in one go and give it to the processors, and the task is the same everywhere. In tree level parallelism the task is also the same, but the division step is not straightforward — it depends on the parent level. At each step you are dividing the data; you cannot straight away divide the data into ten parts as you would in data parallelism. With ten processes in data parallelism, you divide once and give each process its part.
Q: What is the difference between data parallelism and task parallelism?
A: Data parallelism: the same tasks are performed on different subsets of the same data. If I have data \(d\), I divide it into ten sets — ideally each set is different from each other — so each processor is working on a different part of the data. Task parallelism: different tasks are performed on the same data. I do not divide the data; it remains intact, and the same data is replicated across three nodes, and different tasks run in parallel on those nodes.
Recap and bridge: Four models are now on the table — data, tree, task, and request level parallelism — and request level is the one measured in throughput: requests per unit time. With those four patterns in hand, the lecture turns to the infrastructure underneath them: what a distributed system actually looks like (no shared memory, message passing, data kept local), and then the programming model that industrializes the divide-and-conquer recipe — MapReduce.
3.8 Setting the Stage: The Distributed Context for MapReduce
3.8.1 Loosely Coupled Systems and Message Passing
Hook: Everything so far assumed the pieces could talk. Now the real condition of distributed computing: the machines do not share memory — the only way one node sees another's results is by sending a message. MapReduce is built on top of exactly that constraint.
Distributed systems are typically loosely coupled: you do not have a shared memory. When you divide the data, the memory and storage of each node are separate — memory lives on separate nodes, and the data is segregated. Any exchange of data between nodes happens through a message passing mechanism.
This is different from a tightly coupled system with shared memory. With shared memory, combining results is simpler: each process can write into a memory location, and other processors can read the value from that memory location. In a distributed system, we need to collect the data from the different nodes explicitly.
The two memory worlds, side by side.
| Tightly coupled (shared memory) | Loosely coupled (distributed) | |
|---|---|---|
| Where does data live? | One shared address space | Each node's own memory and storage |
| How do nodes exchange results? | Write to a shared location, others read | Message passing — explicit send and receive |
| Combining results | Near-free: just read the shared slot | Must collect data from every node, over the network |
| Cost of a naive combine | Low | High — network transfers dominate |
This table explains the combine anxiety of the earlier sections: in a loosely coupled system, the "collect the answers" step is a real network operation, and a combine step that is sequential or chatty can eat the entire speed-up. Keeping the combine simple is not a style preference; it is the main engineering constraint of the whole model.
3.8.2 Locality of Reference: Move the Task to the Data
The important design rule when dividing a problem in a distributed system: divide the problem in a way that the computation task can run on the local data. This is the concept of locality of reference (LOR) from earlier: we try to move the task to the data, because moving the data around is a quite costly thing. Each node's memory is its own, exchanges go through message passing, so the computation should be scheduled where its data already lives.
Think of a library. To answer a question, you do not haul every book into a central reading room and then search them — you walk to the shelf where the relevant book lives and read it there. Moving the book is expensive and risks damage; walking (moving the reader) is cheap. In a cluster, the "books" are data chunks on disks, and the "readers" are computation tasks: schedule the task on the node that already holds the chunk, and the network stays quiet. This is why Hadoop later assigns a map task to the very node that stores its input split — the data never moves; only the code moves.
3.8.3 The Conquer Step: Combining Results
The conquer step states that we have to combine the results from the different processes or nodes. In some cases the combining may be as simple as just collecting the results and giving them to the end user — as in the search examples, where merging means collecting the results at low cost. In other cases it may involve logic. Keeping the combination simple makes the processing more and more efficient.
In the quick sort example, if you ultimately append the output of all your processes, you get your sorted list — the combine step is nearly free. But sometimes the merge is sequential, and that hurts. K-means clustering is the example the discussion sets up for this: the reduce step in k-means is not as simple as in quick sort or the parallel linear search — it is not straightforward, and if the combine is going to be sequential, it hampers the performance of the distributed system.
3.8.4 K-Means: The Clustering Problem
K-means clustering is a data mining problem — clustering is a data mining problem. The setting: you have data about your customers, and you want to group them into different clusters — say three or four groups — based on the demographics of their purchasing behavior. You have no historical data and no label data; you only have demographic data, and you want to partition the customers into four groups. That is the clustering problem, and k-means is the algorithm used to cluster such data.
The steps that appear in the material: guess clusters in parallel to improve the clusters, but checking whether we have found the right clusters is sequential. What this ultimately says is that the reduce step of k-means is not as simple as the combine steps of the search examples or quick sort: the reduce step is going to be a sequential one, and a sequential combine at the end hampers the performance of the distributed system.
Scope — why clustering has no labels, and why that matters for parallelism. Clustering is unsupervised: the data has no "correct group" column to check against. The algorithm must invent the groups from distances alone. That has two consequences:
- The quality check ("are these the right groups?") cannot be a simple comparison — it is another distance computation over the whole data set, and that check is sequential in the naive version.
- The result is only as good as the initialization: different starting centroids give different clusters (the reference material shows the same data clustering differently with different random starts), so k-means is often run several times and the stable result kept.
The full working of k-means — including how it runs on MapReduce — comes in the iterative MapReduce section (Section 3.11). If the algorithm is unfamiliar, a quick look at k-means and k-means++ is enough for now — no deep dive, just understand what we are trying to do; a small visual in the material shows how the clusters form: gray data points on a plane, circle-shaped centroids, and each point joining the nearest circle.
Real-world: Customer segmentation is the canonical deployment — an online retailer groups customers by purchasing demographics and serves each group a different campaign. The same algorithm clusters sensor readings, image pixels, and search result sets. The domain lesson for this lecture: whenever a clustering step must run on distributed data, its sequential combine makes it the weakest link — which is exactly the problem the iterative MapReduce frameworks of Section 3.11 were built to fix.
3.9 The MapReduce Programming Model
3.9.1 Map as Data Parallelism, Reduce as Inverse Tree Parallelism
Hook: You already know how to count words in a file — a loop and a dictionary. Now count the words of the entire web. The loop cannot scale; the recipe that can is MapReduce, and it is built from two of the four models you already know.
We touched the word count problem before; now we define MapReduce properly, in terms of data parallelism and tree parallelism.
In word count, we take a sentence, break it into multiple partitions, and give each partition to one processor. Each processor returns key-value pairs — we will study exactly what those are. Then a reduce task counts the number of values corresponding to each key, giving the occurrence of each word in the text.
Seen through the parallelism lens:
- The map step is a kind of data parallelism: you divide your data into multiple sets and run the same logic on each set. You are dividing the problem into sub-problems based on the data.
- The reduce step is a kind of inverse tree parallelism. Look at the combine picture: the sub-problems keep being combined — eight results combine into four, four into two, two into one — until we arrive at the final result. That is the conquer step of the merge sort diagram: dividing until a base case, then combining upward, with both the dividing and the combining happening in parallel.
So map is parallel, reduce is parallel, and depending on the problem the reduce step may be as simple as collecting the outputs, or may need logic to combine the results of different reduce tasks.
3.9.2 Word Count: Map, Shuffle and Sort, Reduce
The word count example in detail. We want to count the occurrences of each word in a sentence. The map job picks up each word in the sentence and emits a key-value pair: the word followed by the value 1. It is not counting how many times the word appears — each occurrence emits its own \((word, 1)\). Tokenizing gives a stream of \((word, 1)\) pairs.
Then comes the step that was asked about earlier: where does the sorting and shuffling happen? Between the map job and the reduce job, the MapReduce runtime performs a shuffle and sort: the pairs are sorted by key, and all the values belonging to the same unique key are grouped together. In the example, the word that starts with "b" comes first, then the "c" words, then "da" — the pairs are sorted. The values for a unique key are united: if the word "blue" got two separate 1s from two map emissions, those two values are grouped into one list. This is a hidden step performed by the MapReduce runtime — we do not write code for it. The shuffling also moves the data around among the different nodes, and the framework takes care of that too.
Finally, the reduce job sums the values for each key and emits another key-value pair: the key (the word) together with its frequency of occurrence. That is the final count.
The three moving parts, in one sentence each.
- Map — one pass over your data, emitting a key-value pair per interesting thing it sees. Pure data parallelism: every map worker runs the same code on its own slice.
- Shuffle and sort — the framework's hidden step: sort all pairs by key, and group the values of each key into one list. No programmer code, no programmer control.
- Reduce — one pass over the grouped lists, producing one answer per key (here, the sum of the 1s). Inverse tree parallelism: many partial lists collapse into few final answers.
A mapper's output is \((k, v)\) pairs; a reducer's output is \((k, \text{result})\) pairs — the same shape, different content. That uniformity is what lets the framework chain them.
3.9.3 Worked Example: One Sentence, Word by Word
Worked example — a tokenized sentence, word by word. The example sentence is a sequence of single tokens — the tokens themselves are the data:
"da", "blue", "chip", "on", "again", "blue", "and", "c"
One word, "blue", appears twice. Now run the three stages:
Map — emit one pair per occurrence: for each word in the text, emit \((word, 1)\). So we emit:
\[ (\text{da},1),\ (\text{blue},1),\ (\text{chip},1),\ (\text{on},1),\ (\text{again},1),\ (\text{blue},1),\ (\text{and},1),\ (\text{c},1) \]
Even though "blue" is present twice, the map produces two separate emissions for it — one per occurrence. Map does not count.
Shuffle and sort (runtime): the pairs are sorted by key, and the values for each unique key are collected into a list. "blue" now carries the list \([1, 1]\), and every other word carries \([1]\):
\[ (\text{again}, [1]),\ (\text{and}, [1]),\ (\text{blue}, [1, 1]),\ (\text{c}, [1]),\ (\text{chip}, [1]),\ (\text{da}, [1]),\ (\text{on}, [1]) \]
Reduce — sum the values per key: for "blue", the two elements are summed to 2 and emitted as \((\text{blue}, 2)\). For a word with a single 1, the sum is the element itself, so it is emitted unchanged.
The reducer pseudocode is exactly this: for each key \(w\), for each count \(v\) in values — the list attached to that key — sum them, and emit \((w, \text{sum})\).
Sense-check: the final output has 7 unique keys: six words with count 1 and "blue" with count 2. The counts must sum to the token count: \(6 \times 1 + 2 = 8\), and there were indeed 8 tokens. The total is preserved through map (8 emissions) and through reduce (the sums total 8). Map never lost or duplicated a word, and reduce recombined exactly what map emitted.
3.9.4 The Hadoop Word Count Diagram and Reducer Assignment
The Hadoop-flavored version of the same problem works over a bigger input. The input — "welcome to hadoop class hadoop is good hadoop is and bad" — is split into four pieces, and this is where the "welcome to Hadoop" style of the classic example comes from.
- Input splitting: the input is split into four parts, each handled by its own map.
- Mapping: every map function does the same job — tokenize and return key-value pairs: (welcome,1), (to,1), (hadoop,1), (class,1), (hadoop,1), (is,1), (good,1), (hadoop,1), (is,1), (and,1), (bad,1). Every map does the same job on its own slice — this is the data parallelism connection.
- Sorting: the hidden step — the pairs get sorted by key.
- Shuffling: the crisscross arrows in the diagram show the data moving around among the different processors, distributing the sorted pairs to the different reducers.
- Reducing: each reducer takes the values for its keys and produces the final counts — "hadoop" comes out with a count of 3.
- Combining: the results of the reducers are combined into one and you get the final output file.
Worked example — how many reducers, and which keys may share one? The example runs four mappers over the 11-token input. How many reducers should there be?
- One reducer per unique key: count the unique words in "welcome to hadoop class hadoop is good hadoop is and bad": welcome, to, hadoop, class, is, good, and, bad — eight unique keys, so eight reducers, one per key. (The discussion says seven reducers here; counting the unique words in the example sentence gives eight — the exact number is not the point. What matters, and what the professor stressed, is the rule below: the number of reducers may be fewer than the number of keys, and one reducer may serve several keys.)
- The critical rule of reducer assignment: the same unique key must go to only one reducer. A key cannot be split across multiple reducers — that is not possible, because otherwise the reduction step would become difficult: the counts for that word would be scattered and would need an extra aggregation to reunite. Every occurrence of "hadoop" goes to exactly one reducer.
- What is possible: use fewer than seven reducers, and let one reducer handle the values of more than one key — one reducer counts two keys' lists, another reducer counts another two, and so on. One reducer counting more than one key list is fine; splitting one unique key across reducers is not.
Sense-check: "hadoop" appears in tokens 3, 5, and 8 — three occurrences, all 1s. Whether they land in the same reducer directly or after a combine, the sum must come out 3, and it can only come out 3 if all three values for "hadoop" meet in one place. Split "hadoop" across two reducers and you get \(1+1\) and \(1\) — two partial counts with no reducer knowing the total. The one-key-one-reducer rule is what makes the count correct.
3.9.5 A Third View: Key-Value Pairs and GROUP BY
There is a third way to look at the MapReduce steps, adapted from Jeff Ullman's course slides — a good reference to look at. The input to map is a set of key-value pairs: say a document ID as the key and the document content as the value. Four documents: doc 1 = "welcome to hadoop", doc 2 = "class hadoop", doc 3 = "is good", doc 4 = "hadoop is".
- The map step generates intermediate key-value pairs: one pair per word occurrence.
- These intermediate pairs go through shuffle and sort and are then handed to the reduce step.
- The reduce step is grouped by unique key — the same GROUP BY you know from SQL, where you group data based on a particular attribute. Here we group by key: for a key with four or five entries, we pick up those values and make a list.
- The reduce step is the aggregation function: sum the values, produce the reducer output, combine the outputs, and present the final result to the end user.
The three views answer the recurring question — where shuffle and sort happen: the shuffle and sort step sits between map and reduce, and it is taken care of by the MapReduce framework itself.
3.9.6 Formal Definition: From Input to Output
Formally, MapReduce works like this:
- Partitions. You partition your data into a bunch of partitions.
- Record readers. Each partition is read by a record reader — every programming language has something to read data: scanner classes in Java and similar facilities elsewhere.
- Map function. The map function generates key-value pairs, termed intermediate key-value pairs.
- Shuffle and sort. Some sorting happens — the shuffle and sort acts on these key-value pairs.
- Reduce function. The sorted, shuffled data is fed to the reducer, which generates another key-value pair for each key.
- Combine. You combine the output of the different reducers — that is your final output.
The value flow, in notation. In terms of the value notation: the input to map is \((k_1, v_1)\) — a document ID and a sentence, say. The map function takes each such key-value pair and generates \(k_1\) and \(v_1\) pairs for the keys present in the document it received, where the value is typically 1 in every case. The reduce function accepts the sorted and shuffled list given by the framework; for each unique key it finds the list of values corresponding to that key and produces a value \(v_2\): the unique key stays the same (it does not change), and the values are summed, so \(v_1\) changes to \(v_2\). If the list holds a single number, \(v_2\) equals that \(v_1\); if it holds multiple numbers, they are summed to become \(v_2\). The output is \((k_1, v_2)\) — and in general we write the final output as the \((k_1, v_2)\) pairs that emerge from the MapReduce programming model:
\[ (k_1, v_1) \xrightarrow{\text{map}} (k_1, 1) \text{ per occurrence} \xrightarrow{\text{shuffle/sort}} (k_1, [1, 1, \ldots]) \xrightarrow{\text{reduce}} (k_1, v_2) \]
Every symbol is named: \(k_1\) is the key (a document ID, or in word count the word itself), \(v_1\) is the input value (the document content), the intermediate value is the constant 1 emitted per occurrence, the brackets \([\ldots]\) mark the grouped list produced by shuffle and sort, and \(v_2\) is the output value — the sum of the list. Notice the key survives all three stages unchanged; only the value is transformed. That invariance is what makes grouping safe: values travel to the reducer that owns their key, and the key tells the reducer where each list belongs.
3.9.7 When to Use MapReduce Instead of SQL
When do we choose MapReduce over a traditional relational approach? Suppose you have a huge set of documents that do not fit into memory: you need file-based processing in stages. If you used SQL — the GROUP BY and COUNT functions — the RDBMS would compute the count by unique keys fine, but only for a certain range of data. An RDBMS is where you store data in relational tables, and that fits only a limited range.
MapReduce is suited to the problems of huge data sets — billions of records that cannot fit into the main memory — because it needs a lot of data partitioning and high data level parallelism, and then it simply merges the partitions to get the final result. So the decision rule is practical: if you have a small set of records that fits in an RDBMS, use GROUP BY and be done. When you have billions of records that do not fit into main memory, partition them, store them across multiple nodes, retrieve them, run a map-reduce model, and retrieve the final result.
3.9.8 Worked Example: Transactions by Country
Worked example — how many transactions happened by country? We have transaction data with multiple attributes: the product purchased, its price, the payment mode, the customer name, the city, the state, the country, the account creation date, and so on.
With SQL: the data has a country attribute, each record corresponds to one transaction, so we write COUNT(*) — or count some column — with GROUP BY country. The result looks like: Argentina 1, Australia 38, and so on. This works only if the data set is limited and can be stored in an RDBMS.
With MapReduce: for a very large data set — millions or billions of records — the same job is well suited to MapReduce.
- Map function: read the country field and emit \((country, 1)\) for each record. Multiple map jobs run in parallel.
- Shuffle and sort: the system performs it — all values for "Argentina" gather into one list, all values for "Australia" into another.
- Reduce function: count the values corresponding to each country and produce the output: one count per country. Australia 38, Argentina 1, and so on — the same table SQL would produce, now at web scale.
Looking at the problem, you have to figure out what the map logic is and what the reduce logic is, and then run those mapper and reducer jobs. There are different jars available to execute mappers and reducers; you can write the map and reduce jobs in Java or Python — preferably Python, because writing it is easier.
Sense-check: every transaction emits exactly one pair for its country, so the reduce sums count transactions exactly — no record is counted twice (each record is a separate emission) and none is lost (every record passes through map). The GROUP BY semantics of SQL and the shuffle-and-sort of MapReduce produce the same answer; only the scale differs.
3.9.9 Student Questions and Answers
Q: Where does the sorting and shuffling happen in the word count problem?
A: Between the map job and the reduce job. The shuffle and sort is a hidden step provided by the MapReduce runtime — you do not have to do it explicitly. The map outputs get sorted by key, the values for each unique key get united into a list, and the shuffling also moves the data around among the different nodes; the framework takes care of it. The three views of MapReduce we just went through all place this step in the same place: after the map, before the reduce.
Recap and bridge: MapReduce is three stages — map (emit \((k,1)\) per occurrence), shuffle and sort (the framework groups values by key), reduce (aggregate each key's list into one answer). The pipeline is formal: partitions → record readers → map → shuffle and sort → reduce → combine, and the value flows \((k_1, v_1) \to (k_1, v_2)\). One rule carries the whole design: a unique key belongs to exactly one reducer.
Exam note: Know the MapReduce pipeline by heart — input splitting → record readers → map (emits intermediate key-value pairs) → shuffle and sort (runtime-provided hidden step) → reduce (group by key, aggregate) → combine — and the rule that one unique key must go to exactly one reducer. The next section shows how this pipeline actually runs on a cluster: who the workers are, where the intermediate files live, and what the framework does for you.
3.10 Running MapReduce on a Cluster
3.10.1 Master, Slaves, and Workers
Hook: You write two functions — map and reduce — and a cluster does everything else. Who are the "everyone else"? A master that coordinates, slaves that hold data, and workers that execute your two functions.
MapReduce runs on a cluster with a master node and slave nodes kind of configuration. A concept introduced here is the worker: workers are assigned to perform the map and reduce jobs. Given a bunch of documents where we want to count the words, the workers take these documents; which worker executes on which processor is taken care of by the system itself. The framework creates some workers to do the map jobs and some to do the reduce jobs.
Think of a restaurant kitchen. The master is the head chef who plans the service; the workers are the line cooks; the slave nodes are the stations where the ingredients sit. The head chef does not tell each cook which ingredient to buy — the ingredients are already at their stations — he only decides who cooks what. In the same way the master schedules map and reduce work onto workers, and the data does not travel to the code; the code travels to the data (Section 3.8.2).
3.10.2 Intermediate Files and the Shuffle and Sort Step
The execution flow is two-stage. Each map worker performs a map job on its share of the input and writes its output to an intermediate file — you can see this in the diagrams as files drawn in double lines. The output is saved to a file on disk. Then another worker on the reducer side reads this input; in between, the data is shuffled and sorted — that is why there are two steps: first the output is written, then it is shuffled and sorted, and then it is given to the reduce workers, who come up with the final output.
If there are multiple reducers, they write to multiple files. You can combine the results at the end and present a unified output.
This is a data-centric design: move computation closer to the data. The intermediate results are stored on the disk — saved into files — because the volume is too large for memory.
The two-stage data flow, in a picture. Draw the pipeline left to right:
- Input files on the left, split into partitions.
- A row of map workers, each reading one partition and writing an intermediate file to local disk — draw these files as double-outlined boxes between the map row and the reduce row.
- Shuffle arrows fanning out from the intermediate files: each key's values are gathered toward the reducer that owns that key. The arrows cross (the "crisscross" of the diagrams) because a reducer takes values from every mapper.
- A row of reduce workers, each reading the grouped lists for its keys and writing one output file.
- The combine step merging those output files into the final answer on the right.
Landmarks: the intermediate files are the boundary between the two stages — they are what makes the shuffle a separate step; and the crossing arrows are the only place where data moves between machines. One-sentence takeaway: MapReduce achieves parallelism by keeping each stage's writes local (cheap) and accepting exactly one network pass — the shuffle — between stages.
3.10.3 What the Framework Handles
The MapReduce library does all the work of allocating resources, starting workers, managing them, moving data, and handling failures — if one worker fails, shifting its task to the other workers is also taken care of by the system itself. What is important for the programmer: you only need to write the code for a map task and a reduce task; the rest is taken care of by the system. If you know how to divide the problem effectively — the divide-and-conquer habit — and how to write the combine logic, that is good enough; the system handles distribution, job tracking, and fault tolerance.
The programming model is a restricted programming interface to the system: you map your data this way, you reduce your data this way, and the efficient execution is taken care of by the runtime itself. You do not bother about resource allocation or scheduling.
The professor's analogy — MapReduce is like SQL. You write a simple plain query and the low-level code executes behind the query — the backend brings the data from the table, applies the conditions, and returns the result. MapReduce is the same philosophy on distributed data: the programmer states what computation the map and reduce should perform, and the runtime decides where it runs, how much parallelism to use, and what to do when a machine fails. In both cases the value is the same: the difficult, repetitive machinery is hidden behind a small, declarative interface.
3.10.4 History: GFS, Hadoop, and Functional Programming
A bit of history puts the model in context. Google created a distributed file system known as the Google File System (GFS), and then started running analytics on top of that file system. The open-source version of that stack was created as Apache Hadoop. Hadoop performs map and reduce on data using many machines; the system takes care of distributing the data and managing fault tolerance — if one node fails, how to divide its task is handled by the system.
The intellectual lineage matters: this is an older idea from functional programming, transferred onto large-scale distributed computing. In functional programming you write each function yourself; here we have found that mapping something and reducing something are the two pieces of logic a programmer must write — the "which part goes to which processor", "how it sorts", "how resources are allocated" parts are handled by the framework. Divide-and-conquer thinking plus a map and a reduce function is all the programmer contributes.
Scope and pitfalls of the MapReduce runtime model:
- The model assumes file-sized data. The intermediate files exist because the volume is too large for memory; if your data fits in a single machine's RAM, MapReduce's two-stage file choreography is overhead, not speed — that is a case for plain SQL or an in-memory engine.
- The "restricted interface" is a trade, not a loss. You give up control over scheduling, partitioning internals, and failure recovery; in exchange you get a system that handles exactly those things. If your computation needs fine-grained control (iterative loops, shared state), the basic model fights you — Section 3.11 shows the cost.
- One network pass is the ideal, not a guarantee. The shuffle is the one planned data movement; but a naive combine (collecting every reducer's file to one node) adds a second pass. Keep the combine cheap, or the two-stage design loses its advantage.
Real-world: GFS and Hadoop are the founding pair of the modern data stack — GFS showed how to store petabytes on commodity disks with replication, and Hadoop's MapReduce showed how to analyze that data with plain map/reduce code. The same two-stage philosophy lives on in its successors: Hive translates SQL to MapReduce-style jobs, Pig translates scripts, and Spark (Section 3.11) keeps the map/reduce model but moves the intermediate data from disk to memory. Whenever a company says "we ran a Hadoop job," what ran is exactly this: master, workers, intermediate files, shuffle, and reduce — with the framework silently doing everything except the two functions the programmer wrote.
3.11 Iterative MapReduce
3.11.1 One-Pass Computation vs Iterative Computation
Hook: Word count runs once and finishes. Training a model does not — it needs the data many times, each pass improving the answer. MapReduce as we built it reads the data once; making it loop is the last big problem of the lecture.
All the MapReduce examples so far are one-pass computations: the static data is fed to the map jobs, they run, the output is saved somewhere; the runtime shuffles and sorts it and gives it to the reducer; the reducer does some logic and places its output into a file. One pass — you load your data once.
But many applications — especially in machine learning and data mining — need to process the data iteratively, because training models is an iterative process. Those applications need iterative execution of map-reduce jobs: once the result is produced by the reduce, it should be fed back into the system again. The diagram shows what happens: we feed the variable data in, call map and reduce, and when this output is achieved, we call them again, time and again.
One way to do this is to write a driver program — a main program — that runs a loop: take the output, feed it back to the map as the new variable data, call map and reduce again. But the iteration has a special structure: there is variable data, which changes each iteration, and static data, which remains the same. Every time a map job runs, it needs the static data — it must load that static data again. So a naive loop over map and reduce carries real costs:
- Loading the static data every iteration — it has to be loaded from files again and again.
- Combining the results of the different reducers every iteration, then feeding the combination back to the map.
You run these iterations until you converge, and the convergence criteria is defined based on the application. Two classic examples: k-means clustering and PageRank.
3.11.2 Worked Example: K-Means Clustering
K-means clustering, worked through by hand. We are given a bunch of data points — plotted on a two-dimensional plane as gray points. The objective: find the groups among these data points based on the distance matrix — group the points that are close together.
Setup. First we decide how many groups we need; in this example, three groups. Three centroid points are chosen randomly — these are the cluster centers, marked as circles.
Iteration 1 — assign. Compute the distance of each gray data point from each of the three centroids. Whichever centroid a point is closest to, that point joins that centroid's group. For example: take a point and measure its distance to centroid one, centroid two, and centroid three; it is closer to centroid one, so it forms a group with centroid one. Another point is closer to the green centroid, so it goes to the green cluster. Doing this for all the points gives the clusters after the first iteration: one cluster ends up with just two points; another cluster ends up with six points plus its centroid — seven total, counting the centroid itself.
Iteration 2 — recompute centroids. We do not stop here — the clusters are not done. We compute a new centroid for each cluster: take the data points of the cluster and compute the mean of each value — the mean of the x-coordinates and the mean of the y-coordinates. The new centroid of each cluster moves: the red centroid shifts a little; the green centroid moves downward because the density of points there is higher; the blue centroid stays more or less in place, shifting slightly, because it is computed as the mean of all its points.
Iteration 3 — reassign and check for change. Compute distances again and see if anything changed. In the example, with the new centroids, two points are now closer to the red centroid than to the green one, so they move to the red cluster. The green cluster is left with its remaining points.
Repeat until no change. Another iteration: take the mean of the points in each cluster, recompute the three centroids, compute distances again, reshuffle the points into clusters. Keep doing this until there is no change — until no points shift from one cluster to another. At that point, stop: the k-means clustering is done.
Worked example — k-means by hand, with exact numbers. Ten data points on a plane, three groups, three random starting centroids:
Points: \((1,2), (2,1)\) — near the red centroid; \((4,4), (4,5), (5,5), (6,5), (5,6), (6,7)\) — the middle group; \((9,1), (10,2)\) — near the blue centroid.
Initial centroids: red \(C_1 = (1,1)\), green \(C_2 = (6,6)\), blue \(C_3 = (10,1)\).
Iteration 1 — assign by nearest centroid (using squared Euclidean distance, which preserves the ordering of comparisons):
- \((1,2)\): distance to \(C_1\) is 1, to \(C_2\) is \(\sqrt{41} \approx 6.4\), to \(C_3\) is about 9.1 → joins red.
- \((2,1)\): distance to \(C_1\) is 1 → joins red.
- \((4,4)\): to \(C_1\) \(\sqrt{18} \approx 4.2\), to \(C_2\) \(\sqrt{8} \approx 2.8\), to \(C_3\) \(\sqrt{45} \approx 6.7\) → joins green.
- \((4,5), (5,5), (6,5), (5,6), (6,7)\): all closer to \(C_2\) than to \(C_1\) or \(C_3\) → join green.
- \((9,1)\): distance to \(C_3\) is 1 → joins blue.
- \((10,2)\): distance to \(C_3\) is 1 → joins blue.
After iteration 1: red = 2 points, green = 6 points, blue = 2 points — one cluster with just two points, another with six points plus its centroid (seven total, counting the centroid itself). These are the exact counts the lecture's visual showed.
Iteration 2 — recompute centroids as means:
\[ C_1 = \left(\frac{1+2}{2}, \frac{2+1}{2}\right) = (1.5, 1.5) \]
\[ C_2 = \left(\frac{4+4+5+6+5+6}{6}, \frac{4+5+5+5+6+7}{6}\right) = \left(\frac{30}{6}, \frac{32}{6}\right) = (5, 5.33) \]
\[ C_3 = \left(\frac{9+10}{2}, \frac{1+2}{2}\right) = (9.5, 1.5) \]
The red centroid shifts a little (from \((1,1)\) to \((1.5,1.5)\)); the green centroid moves downward, from \(y = 6\) to \(y = 5.33\), because the density of its points is higher lower down; the blue centroid stays more or less in place, shifting slightly to \((9.5, 1.5)\).
Iteration 3 — reassign and check for change. Recompute distances to the new centroids:
- \((1,2)\): to \(C_1 = (1.5,1.5)\) is about 0.71, to \(C_2 = (5, 5.33)\) is about 4.8 → stays red.
- \((2,1)\): to \(C_1\) about 0.71 → stays red.
- \((4,4)\): to \(C_1\) about 3.5, to \(C_2\) about 1.7 → stays green.
- The other green points stay closer to \((5, 5.33)\) than to \((1.5,1.5)\) → stay green.
- \((9,1), (10,2)\): to \(C_3 = (9.5,1.5)\) about 0.71 → stay blue.
No point changes cluster, so the clusters have stabilized: red = 2, green = 6, blue = 2, and the run stops. (In the lecture's visual, the starting centroids were placed differently, and at this same step two boundary points flipped from the green cluster to the red one — the reassignment mechanism is identical: a centroid's move can shrink some distances and grow others, and any point whose nearest centroid changes switches clusters.)
Sense-check: the algorithm stopped because iteration 3 changed nothing. Recomputing the centroids again would return the same means, so the run has reached a fixed point. The final answer is a partition of the ten points into three groups where every point is closer to its own centroid than to either other centroid — that is the k-means convergence condition.
3.11.3 K-Means on MapReduce: Static and Variable Data
Now the same algorithm on a MapReduce cluster. Suppose we have 100 data points and three centroids, and ten processors. The data points are divided into sets of ten; each processor computes the distance of its ten points to the three centroids; the reduce step gives the result — which points belong to which cluster. That is one iteration.
But after the reduce, we want to compute the new centroids and feed that variable data back in. The static data — the ten points each map job runs on — does not change; those points are stable. What changes are the circle points, the centroids, because we compute new centroids every time.
A simple driver loop that calls map and reduce again and again with the new variable data is not performance-optimized: the static data has to be loaded each and every time — loaded from files, stored back to files, read from files again. Each iteration goes: load the file, run the map job, shuffle and sort, run the reduce job, run the user program, and start again — with the static data re-loaded from files on every pass. Reading from and writing to a file is a costly thing, so the whole loop is dominated by I/O rather than computation.
The professor's warning — why the naive loop is not performance-optimized. Reading from and writing to a file is a costly thing. Think of a chef who must fetch the same crate of ingredients from the storage room before every single dish — not once per meal, but once per dish. The cooking itself (the computation) is fast; the fetching (the file I/O) dominates the total time. In the naive iterative loop, every iteration re-reads the static data from disk and re-writes the reducer output back to disk, so the loop's cost is mostly I/O, not arithmetic. The static data never changes — yet it is reloaded as if it were new every time.
3.11.4 PageRank: The Problem
PageRank is the algorithm behind ranking in the Google search engine, and it is another iterative problem — though the algorithm itself is a bit difficult, and this represents a simplified version. When you search, certain pages are retrieved, and the question is how to show them in a particular sequence — whether this website should come first or that website should come first. Each web page has a rank associated with it, and based on that rank the relevance of the page is decided and the pages are shown in order in the search results.
The major difficulty is calculating the rank of each web page. The rank of a page depends on a number of inbound and outbound links — the links coming into the page and going out of it — and inbound links are more important than outbound links.
The professor's intuition — why inbound links matter more. When you decide a page's rank, the links coming into the page matter more than the links going out of it. Each page transfers its rank to the pages it links to, split evenly among its outbound links; so a page's rank is assembled from the ranks other pages send it. A page can inflate its outbound links freely — linking out costs nothing — but it cannot conjure inbound links; those are votes from other pages. Rank flows inward, so the inbound stream is the source of authority. (Where the analogy has limits: this simple view ignores link farms and paid links, which real search engines detect and discount.)
The setup for one page: there is a page \(x\), and web pages \(t_1, t_2, \ldots, t_n\) around it. From each \(t_i\) there is a hyperlink pointing to \(x\) — all \(n\) pages link to \(x\) — and there are some other outbound links from the \(t_i\) pages as well. How does a user reach page \(x\)? Two ways: directly — you have a link and go straight to the page — or by browsing, coming from \(t_1\) or \(t_2\) or any other page that points to \(x\).
3.11.5 The PageRank Equation
Let \(p\) be the probability of a random jump, and \(N\) the total number of nodes in the graph. To calculate the rank of \(x\): there is a probability that the user jumps directly to a particular node, and a probability that the user visits it from other nodes. How much rank does a page \(t_i\) transfer to \(x\)? Page \(x\) has an inbound link from \(t_i\), so it takes some rank from \(t_i\). The rank it takes is the page rank of \(t_i\) divided by the number of outbound links of \(t_i\) — the rank is split evenly among everything \(t_i\) links to.
Look at \(t_1\): its outbound links are 1, 2, and 3, so it transfers \(\frac{1}{3}\) of its rank to \(x\), \(\frac{1}{3}\) to the second page, \(\frac{1}{3}\) to the third. So \(\frac{PR(t_1)}{3}\) contributes to \(x\). Look at \(t_2\): it has four outbound links, 1, 2, 3, and 4, so \(\frac{PR(t_2)}{4}\) contributes to \(x\). Similarly for all the other pages. In the example numbers, 0.5 is the probability that you arrive through the sequence of pages, and 0.5 is the probability that you directly access page \(x\), and that is how the page rank equation is written in general:
\[ PR(x) = (1-p) \cdot \frac{1}{N} + p \cdot \sum_{t_i \,:\, t_i \text{ links to } x} \frac{PR(t_i)}{C(t_i)} \]
where \(p\) is the probability of a random jump, \(N\) is the total number of nodes, and \(C(t_i)\) is the out-degree of \(t_i\) — the number of outbound links from \(t_i\) — since a page splits its rank equally among its outbound links.
Reading the equation term by term. Every symbol is named:
- \(PR(x)\) — the rank of page \(x\), the answer we are computing.
- \((1-p) \cdot \frac{1}{N}\) — the direct-access term: with probability \(1-p\) the user jumps straight to a page, and if all \(N\) pages are equally likely targets, page \(x\) receives \(\frac{1}{N}\) of that probability mass.
- \(p\) — in the equation as written, the probability that the user arrives by following links (browsing) — the professor's example uses \(p = 0.5\) and \(1-p = 0.5\) for the two ways of reaching \(x\). Note the professor calls \(p\) "the probability of a random jump"; in the equation above \(p\) multiplies the link-following term, so it plays the role of the damping factor in standard references (where the direct-access weight is written \(1-d\) and the link weight \(d\)). Whichever name is used, the structure is the same: a direct-jump term plus a link-following term, with the two weights summing to 1.
- \(\sum_{t_i : t_i \text{ links to } x}\) — the sum runs over exactly the pages that link to \(x\); pages that do not link to \(x\) contribute nothing.
- \(\frac{PR(t_i)}{C(t_i)}\) — the rank transfer: page \(t_i\) splits its rank evenly across its \(C(t_i)\) outbound links, and \(x\) receives one of those shares.
The equation is self-referential: \(PR(x)\) depends on the ranks of the \(t_i\), and their ranks depend on \(x\)'s. There is no closed-form one-pass computation — which is precisely why the problem is iterative.
3.11.6 Why PageRank Is Iterative — and the Convergence Rule
The page rank keeps on updating — the process is iterative. You start with an initial page rank \(PR_i\) for each node — assumed from some probability distribution — and then you compute the page ranks for every page in every iteration. Because the ranks feed each other, each iteration changes the values: if the rank of one page changes, the rank of the pages that receive its contribution changes too, since that page contributes to their rank. You keep iterating until you converge.
The convergence criteria is application-defined. Each iteration you compute the difference between the ranks of the previous iteration and the new iteration; if the difference is minute, you may stop; if it is a significant gap, you keep going — whether to stop depends on the threshold value of the difference that has been kept. If there is no change in the rank at all, that is also possible, and you stop there. (For a deeper understanding of how the values change iteration by iteration, working through one of the worked examples on Wikipedia is recommended.)
Worked example — the rank transfer, with the numbers the lecture used. Suppose page \(x\) is linked to by two pages, \(t_1\) and \(t_2\).
- Page \(t_1\) has three outbound links: link 1, link 2, and link 3 (one of them pointing to \(x\)). Its rank \(PR(t_1)\) is split evenly, so \(x\) receives \(\frac{PR(t_1)}{3}\).
- Page \(t_2\) has four outbound links: links 1, 2, 3, and 4 (one of them pointing to \(x\)). So \(x\) receives \(\frac{PR(t_2)}{4}\).
With, say, \(PR(t_1) = 0.6\), \(PR(t_2) = 0.8\), \(N = 10\) pages, and \(p = 0.5\), the first iteration gives:
\[ PR(x) = (1-0.5)\cdot\frac{1}{10} + 0.5\cdot\left(\frac{0.6}{3} + \frac{0.8}{4}\right) = 0.05 + 0.5\cdot(0.2 + 0.2) = 0.05 + 0.2 = 0.25 \]
Sense-check: the direct term is small (\(0.05\)) because the random jump spreads its probability over all 10 pages; the link term dominates because two pages point at \(x\). Notice \(t_2\)'s transfer \(\frac{0.8}{4} = 0.2\) equals \(t_1\)'s \(\frac{0.6}{3} = 0.2\) — a smaller-rank page with fewer outbound links can give as much as a bigger-rank page with more. In the next iteration, this new \(PR(x)\) feeds back into the pages that link to \(x\)'s neighbors, and the values shift again until the threshold test says stop.
Structurally, PageRank is exactly like k-means from MapReduce's point of view: the variable data — the current rank vector — has to be fed in time and again, and a general map-reduce implementation would read the data from files again and again, which is not performance-optimized.
3.11.7 The Challenges — and the Frameworks That Solve Them
Running these problems with a general MapReduce implementation brings large overheads: the reinitialization of tasks, the reloading of static data, and communication and data transfers that happen multiple times. After the reduce, the output is saved onto multiple files; then this data has to be fed back to the map; the static data is loaded in every iteration. Each iteration carries the cost of reading from and writing to files.
There are ways to handle it. One is the caching technique; another is configuring map and reduce with the static data. Some other frameworks do iterative map-reduce jobs this way:
- MapReduce++ — an iterative MapReduce variant. A configure step is added, so loading the static data is optimized; some of the results are cached to enable faster performance; a combiner operation is introduced to collect all the reduced outputs, so you do not spend much time combining the results of different reducers; and caching and indexing techniques are used for faster retrieval of the data — it is not like reading from the files time and again. MapReduce++ provides configure-map and configure-reduce functions that optimize the loading of the static data, and an efficient combine operation that runs every time a reducer produces results.
- Twister — a framework used for iterative MapReduce.
- HaLoop — an extension of Hadoop that supports iterative jobs.
- Spark — not discussed in detail in this course (it comes later), but for reading: Spark uses in-memory computing to speed up the iterations; the data remains in memory, and Spark makes use of something called an RDD — resilient distributed dataset — to make the computation faster. A link to example code is provided for anyone who wants to go through it.
The performance comparison for running k-means clustering for multiple iterations — 16 iterations, average time taken — ranks the frameworks in order: the time is maximum for Hadoop, then DryadLINK, then Twister, and then MPI, which is the fastest. (The four bars on the comparison chart are Hadoop, DryadLINK, Twister, and MPI — the stated ordering is consistent: the first two take the longest and the last two are more optimized.) So if you want to go for iterative MapReduce, Twister and MPI are more optimized than Hadoop and DryadLINK. The material links further reading on HaLoop and the Twister architecture.
3.11.8 Student Questions and Answers
Q: The shuffle and reduce part is taken care of by the underlying Hadoop framework — that is clear. But after the map step, how are the messages passed, and is the shuffle step done in memory or through the data?
A: The shuffling is also taken care of by the framework itself — including which reducer is going to get which data, and starting the reducer jobs where the output of the map results sits. Shuffling is basically moving around your task, or starting your task somewhere where your data resides. The map tasks leave their output into files; those files are taken by the MapReduce framework and shuffled. The framework looks at the data and decides which reducer should get what, and then the reducers run accordingly. The shuffling also has to be optimized, and that is taken care of by the runtime environment itself.
Q: Any follow-up on the shuffle question?
A: If the answer is not fully clear, post a follow-up question. The key takeaway: the static data, the combining step, the spreading of data back, and the reinitialization of tasks — everything in iterative MapReduce has to go through once again per iteration, and that is exactly what the specialized frameworks optimize away.
Recap and bridge: One-pass MapReduce loads the data once and finishes; iterative computations — k-means clustering and PageRank are the two exemplars — loop over map and reduce until a threshold-based convergence test passes. The naive driver loop pays the same costs every iteration: reloading static data from files, recombining reducer outputs, reinitializing tasks. The specialized frameworks (MapReduce++ with configure-map/configure-reduce and caching, Twister, HaLoop, Spark with RDDs) optimize exactly those costs away — the last of them, Spark, gets its full treatment later in the course.
Exam note: Know the static-data versus variable-data structure of iterative MapReduce (k-means: static points, variable centroids; PageRank: static link graph, variable rank vector), why the naive loop is not performance-optimized (reloading static data from files, re-combining reducer outputs, reinitializing tasks every iteration), and the frameworks that fix it.
Exam Guidance Summary
No explicit mark distribution or question-pattern guidance was given in this session, but the following study signals were explicit and worth carrying into revision:
- Asymptotic notation is assumed knowledge. The complexity arguments — \(O(n)\) for linear search, \(O(\log_2 n)\) for binary search, and the \(O(\log(N/P))\), \(O(N/P)\) parallel forms with a theoretical speed-up of \(p\) — are the working language of the whole session. If the notation is not yet comfortable, read a page or two on asymptotic notations; no deep dive is needed, but the basics should be solid.
- Exam note: Be able to state why quick sort defeats data parallelism (pivot-dependent, unbalanced partitions; worst case as bad as sequential) and which parallelism model it uses instead (tree parallelism: partitioning at each level runs in parallel).
- Exam note: The three-fold classification — data, tree, and task level parallelism — plus request level parallelism, with the SIMD (single instruction, multiple data) and MISD (multiple instruction, single data) mappings from Flynn's taxonomy, is core material. Expect to compare them: same task on divided data vs different tasks on the same data vs parallel partitioning levels.
- Exam note: Know the MapReduce pipeline by heart: input splitting → record readers → map (emits intermediate key-value pairs) → shuffle and sort (runtime-provided hidden step) → reduce (group by key, aggregate) → combine. Know the rule that one unique key must go to exactly one reducer.
- Exam note: Know the static-data vs variable-data structure of iterative MapReduce, the two example algorithms (k-means clustering and PageRank), why a naive loop over map and reduce is not performance-optimized (reloading static data from files, re-combining reducer outputs, reinitializing tasks every iteration), and the frameworks that fix it (MapReduce++ with configure-map/configure-reduce and caching, Twister, HaLoop, Spark with RDDs).
- The level of depth requested for supporting algorithms: understand what k-means does (a quick look at k-means and k-means++ suffices — no deep dive), and work through one PageRank iteration example on Wikipedia for the iterative update mechanism.
Key Industry Applications
- Real-world: Fingerprint matching — biometric identity search over a partitioned, distributed fingerprint database; linear search per partition, with repartitioning only when the database grows substantially. The partition-once, search-often trade-off is the design rule: a few new fingerprints never justify redistribution; many do.
- Real-world: Distributed document search, and MongoDB as a document-oriented database hosted on a cluster: queries execute in parallel on the different nodes of the cluster and run much faster than on a single node. MongoDB's sharding is exactly the partitioning idea of Section 3.3 wearing a database name.
- Real-world: Word processing on multi-core machines — editing and background spell-checking running as different tasks on the same document (task level parallelism). The same shape appears in dashboards computing several independent aggregates over one data frame at the same time.
- Real-world: Email servers, API interfaces, reservation systems, and online banking — request level parallelism, with throughput (requests per unit time) as the scalability metric. Server capacity planning is a throughput question: concurrency divided by latency sets the request rate.
- Real-world: Google File System (GFS) and Apache Hadoop — the original open-source MapReduce stack; Hadoop performs map and reduce over many machines and handles distribution and fault tolerance itself, scheduling tasks onto the nodes that already hold the data (locality of reference).
- Real-world: Google's PageRank — web search result ordering driven by an iterative rank computation over inbound/outbound link structure; inbound links matter more than outbound. The rank vector is recomputed iteratively until the per-iteration differences fall below the threshold.
- Real-world: Iterative MapReduce frameworks in production and research: MapReduce++, Twister, HaLoop, DryadLINK, MPI, and Spark (in-memory computing over resilient distributed datasets, RDDs) — benchmarked on 16 iterations of k-means, with Hadoop slowest and MPI fastest among the compared frameworks. Spark's in-memory RDDs are the direct answer to the reload-static-data cost the naive loop pays.
- Real-world: SQL GROUP BY and COUNT remain the right tool for count-by-key queries on data sets that fit in an RDBMS; MapReduce is the choice at billions of records that cannot fit in main memory — the same aggregation, executed as partition → map → shuffle and sort → reduce → combine.
- Reference: The key-value view of MapReduce (document ID → content as input pairs, intermediate pairs, group-by-key, aggregation) is adapted from Jeff Ullman's course slides.
BDS Lecture 3 notes
Sections Breakdown
Divide-and-conquer in parallel: sub-problems per processor, the two failure modes, and the minimum processor count set by the widest moment of a dependency graph.
Linear versus binary search complexity, parallel binary search over P processors at O(log(N/P)) with an ideal speed-up of P, and the (processor, index) answer pair.
Fingerprint matching and distributed document search as parallel linear search, with the partition-once, search-often design rule.
One task on many data slices (Flynn SIMD), homogeneous versus heterogeneous nodes, and the slowest-node bottleneck.
Why the pivot defeats balanced partitioning and how tree parallelism runs each level of partitioning in parallel.
Different tasks on the same data (Flynn MISD), the spell-checker and mean-median-mode examples, and why task parallelism does not scale by adding resources.
The client-server pattern: many requests served in parallel as threads, with throughput as the scalability metric.
Loosely coupled systems, message passing, locality of reference, the conquer step, and k-means as a sequential-combine problem.
Map as data parallelism, reduce as inverse tree parallelism, the shuffle and sort step, the formal pipeline, and the one-key-one-reducer rule.
Master, slaves, and workers; intermediate files and the two-stage data flow; what the framework handles; GFS and Hadoop history.
One-pass versus iterative computation, k-means and PageRank worked by hand, static versus variable data, and the frameworks that fix the naive loop.
The study signals of the session: complexity forms, the four parallelism models, the MapReduce pipeline, and iterative MapReduce.
Where the concepts meet production systems: fingerprint matching, MongoDB sharding, web servers, GFS and Hadoop, PageRank, and iterative frameworks.
Exam Revision Notes
Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.
Top-Down Design
Must-know: Divide the problem into as many sub-problems as there are processors; the minimum number of processors equals the maximum number of simultaneously-alive sub-problems.
Top pitfall: Fewer sub-problems than processors leaves processors idle; more sub-problems adds scheduling and waiting time; a heavy combine step eats the parallelism savings.
Self-check: If a dependency graph runs F1, then two F2 copies in parallel, then three F5 copies in parallel, what is the minimum number of processors? (Three.)
Connects to: 3.2 Running Time Complexity and the Parallel Keyword Search
Running Time Complexity and the Parallel Keyword Search
Must-know: The four complexity forms: O(n) linear search, O(log2 n) binary search (sorted list only), O(log(N/P)) parallel binary search, and the theoretical speed-up of p.
\[O(n),\ O(\log_2 n),\ O(\log(N/P)),\ \text{speed-up} = \frac{\log_2 N}{\log_2 (N/P)} \approx P\]
Top pitfall: Running binary search on an unsorted list; quoting the speed-up as exactly P while ignoring the collect/combine step.
Self-check: How many comparisons does binary search need for a nine-element list searched for 10? (Four: 5, 7, 8, 9.)
Connects to: 3.1 Top-Down Design, 3.3 More Parallel Search Examples, 3.9 The MapReduce Programming Model
More Parallel Search Examples
Must-know: Parallel linear search gives O(n/p) and speed-up p; repartition only when many new entries arrive, not for a few.
\[O(n/p),\ \text{speed-up } p\]
Top pitfall: Repartitioning after every small addition, paying redistribution cost for negligible imbalance.
Self-check: With 1000 fingerprints on 10 processors, two new fingerprints arrive — do you repartition? (No; repartition after many additions, e.g. after the data grows to 1100.)
Connects to: 3.2 Running Time Complexity and the Parallel Keyword Search, 3.4 The Data Parallel Execution Model, 3.7 Request Level Parallelism
The Data Parallel Execution Model
Must-know: Data parallelism = SIMD: one task, multiple data slices; the slowest node is the bottleneck because the final result is collected from all processors.
Top pitfall: Equal-sized partitions on heterogeneous nodes (the slow node drags everyone) and unequal partitions on homogeneous nodes (execution time rises).
Self-check: Why is the slowest node the bottleneck in data parallelism? (The result cannot be finalized until every node completes its task.)
Connects to: 3.3 More Parallel Search Examples, 3.5 Quick Sort: When Data Parallelism Fails, Tree Parallelism Steps In, 3.6 Task Level Parallelism
Quick Sort: When Data Parallelism Fails, Tree Parallelism Steps In
Must-know: Why quick sort is not data-parallel (pivot cannot guarantee equal partitions; division is stepwise and depends on the parent level; worst case as bad as sequential) and that it uses tree parallelism instead.
Top pitfall: Confusing tree parallelism with task parallelism: in quick sort the task stays the same throughout; only the partitioning at each level is parallel.
Self-check: A 1/99 split of the list is to a 45/55 split what a bad pivot is to a good one — which split satisfies the balanced-partition requirement? (45/55.)
Connects to: 3.4 The Data Parallel Execution Model, 3.6 Task Level Parallelism, 3.9 The MapReduce Programming Model
Task Level Parallelism
Must-know: Task parallelism = MISD: different tasks on the same data; sub-tasks are independent, identified by functionality, and known statically; adding nodes does not split the tasks further, so it does not scale.
Top pitfall: Expecting task parallelism to scale like data parallelism — the heavy-task node becomes the bottleneck and extra nodes sit idle.
Self-check: Mean, median, and mode on one list is which model? (Task level parallelism: three different tasks, one data set.)
Connects to: 3.4 The Data Parallel Execution Model, 3.5 Quick Sort: When Data Parallelism Fails, Tree Parallelism Steps In, 3.7 Request Level Parallelism
Request Level Parallelism
Must-know: Request level parallelism: many requests, served in parallel by the server (email servers, APIs, reservations, banking); throughput (requests per unit time) is the scalability metric.
Top pitfall: Confusing sharding with the other parallelism models — sharding is just partitioning, a MongoDB term for how shards are maintained among cluster nodes.
Self-check: What is the scalability metric for request level parallelism? (Throughput — how many requests per unit of time the server can serve.)
Connects to: 3.1 Top-Down Design, 3.4 The Data Parallel Execution Model, 3.6 Task Level Parallelism
Setting the Stage: The Distributed Context for MapReduce
Must-know: Loosely coupled systems have no shared memory; combining results needs explicit collection; move the task to the data (locality of reference); a sequential combine (k-means' reduce) hampers distributed performance.
Top pitfall: Moving data around instead of moving the computation — data movement is costly in a message-passing system.
Self-check: Why does the computation task run on the local data? (Moving data around is costly; each node's memory is its own.)
Connects to: 3.2 Running Time Complexity and the Parallel Keyword Search, 3.11 Iterative MapReduce
The MapReduce Programming Model
Must-know: The pipeline: partitions → record readers → map (intermediate key-value pairs) → shuffle and sort (hidden runtime step) → reduce (group by key, aggregate) → combine; one unique key goes to exactly one reducer.
\[(k_1, v_1) \xrightarrow{\text{map}} (k_1, 1) \text{ per occurrence} \xrightarrow{\text{shuffle/sort}} (k_1, [1,1,\ldots]) \xrightarrow{\text{reduce}} (k_1, v_2)\]
Top pitfall: Splitting one unique key across multiple reducers — the counts become scattered and need an extra aggregation to reunite.
Self-check: Where does sorting and shuffling happen in word count? (Between the map job and the reduce job, provided by the runtime.)
Connects to: 3.4 The Data Parallel Execution Model, 3.5 Quick Sort: When Data Parallelism Fails, Tree Parallelism Steps In, 3.8 Setting the Stage: The Distributed Context for MapReduce, 3.10 Running MapReduce on a Cluster
Running MapReduce on a Cluster
Must-know: Two-stage execution: map workers write intermediate files to disk, shuffle and sort moves data between stages, reduce workers write output files; the framework handles allocation, worker management, data movement, and failures.
Top pitfall: Expecting to control scheduling and partitioning — the model is a restricted interface; only map and reduce logic are the programmer's job.
Self-check: What does the MapReduce library handle so the programmer does not? (Allocating resources, starting and managing workers, moving data, and handling failures.)
Connects to: 3.8 Setting the Stage: The Distributed Context for MapReduce, 3.9 The MapReduce Programming Model, 3.11 Iterative MapReduce
Iterative MapReduce
Must-know: Static data (k-means points, PageRank link graph) vs variable data (centroids, rank vector); the naive loop is not performance-optimized because it reloads static data from files, re-combines reducer outputs, and reinitializes tasks every iteration; MapReduce++ (configure-map/configure-reduce, caching), Twister, HaLoop, Spark (RDDs) fix it.
\[PR(x) = (1-p)\cdot\frac{1}{N} + p\cdot\sum_{t_i\,:\, t_i \text{ links to } x}\frac{PR(t_i)}{C(t_i)}\]
Top pitfall: Splitting a key across reducers in iterative runs, or believing the naive driver loop is acceptable — the loop is dominated by file I/O, not computation.
Self-check: Why is the naive iterative MapReduce loop not performance-optimized? (Static data is loaded from files every iteration; reading and writing files is costly.)
Connects to: 3.8 Setting the Stage: The Distributed Context for MapReduce, 3.9 The MapReduce Programming Model, 3.10 Running MapReduce on a Cluster
Exam Guidance Summary
Must-know: MapReduce pipeline by heart and the one-unique-key-to-one-reducer rule; the four parallelism models; why quick sort defeats data parallelism; iterative MapReduce's static/variable structure and fixing frameworks.
Top pitfall: Treating the theoretical speed-up p as guaranteed — collection, scheduling, and combine costs are excluded from the ideal formula.
Self-check: Which four models of parallelism does the lecture classify, and which Flynn classes map to data and task parallelism? (Data, tree, task, request; SIMD and MISD.)
Connects to: 3.2 Running Time Complexity and the Parallel Keyword Search, 3.5 Quick Sort: When Data Parallelism Fails, Tree Parallelism Steps In, 3.9 The MapReduce Programming Model, 3.11 Iterative MapReduce
Key Industry Applications
Must-know: Each parallelism model has a named deployment: fingerprints (data parallel linear search), MongoDB (sharding + parallel queries), word processing (task parallel), email/API/banking servers (request parallel), GFS/Hadoop (MapReduce), PageRank (iterative), Spark RDDs (in-memory iterative).
Top pitfall: Using MapReduce for data that fits in an RDBMS — GROUP BY and COUNT remain the right tool for limited data sets.
Self-check: Which framework benchmarked on 16 k-means iterations came out fastest, and which slowest? (MPI fastest; Hadoop slowest.)
Connects to: 3.3 More Parallel Search Examples, 3.7 Request Level Parallelism, 3.9 The MapReduce Programming Model, 3.11 Iterative MapReduce